mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 12:21:20 +00:00
fix(usage): coalesce refresh and force live sync on refresh
- share one full-refresh promise across usage loaders to avoid duplicate work - sync CLIProxy snapshot before source refresh and start sync lazily - make refresh handler execute full refresh with proper error response - reset pending cache state and guard sync interval double-start
This commit is contained in:
@@ -7,13 +7,7 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import { loadAllUsageData } from './data-aggregator';
|
||||||
loadDailyUsageData,
|
|
||||||
loadMonthlyUsageData,
|
|
||||||
loadSessionData,
|
|
||||||
loadAllUsageData,
|
|
||||||
loadHourlyUsageData,
|
|
||||||
} from './data-aggregator';
|
|
||||||
import type { DailyUsage, HourlyUsage, MonthlyUsage, SessionUsage } from './types';
|
import type { DailyUsage, HourlyUsage, MonthlyUsage, SessionUsage } from './types';
|
||||||
import {
|
import {
|
||||||
readDiskCache,
|
readDiskCache,
|
||||||
@@ -29,6 +23,7 @@ import {
|
|||||||
loadCachedCliproxyData,
|
loadCachedCliproxyData,
|
||||||
startCliproxySync,
|
startCliproxySync,
|
||||||
stopCliproxySync,
|
stopCliproxySync,
|
||||||
|
syncCliproxyUsage,
|
||||||
} from './cliproxy-usage-syncer';
|
} from './cliproxy-usage-syncer';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -268,6 +263,14 @@ let diskCacheInitialized = false;
|
|||||||
// Track if background refresh is in progress
|
// Track if background refresh is in progress
|
||||||
let isRefreshing = false;
|
let isRefreshing = false;
|
||||||
|
|
||||||
|
// Coalesced full refresh promise shared across all usage loaders
|
||||||
|
let pendingFullRefresh: Promise<{
|
||||||
|
daily: DailyUsage[];
|
||||||
|
hourly: HourlyUsage[];
|
||||||
|
monthly: MonthlyUsage[];
|
||||||
|
session: SessionUsage[];
|
||||||
|
}> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist cache to disk when we have enough data to be useful.
|
* Persist cache to disk when we have enough data to be useful.
|
||||||
*/
|
*/
|
||||||
@@ -293,6 +296,10 @@ async function refreshFromSource(): Promise<{
|
|||||||
monthly: MonthlyUsage[];
|
monthly: MonthlyUsage[];
|
||||||
session: SessionUsage[];
|
session: SessionUsage[];
|
||||||
}> {
|
}> {
|
||||||
|
// Try to sync CLIProxy snapshot before reading it.
|
||||||
|
// Non-fatal: syncer handles unavailability and stale fallback.
|
||||||
|
await syncCliproxyUsage();
|
||||||
|
|
||||||
// Load default data (from ~/.claude/projects/ or CLAUDE_CONFIG_DIR)
|
// Load default data (from ~/.claude/projects/ or CLAUDE_CONFIG_DIR)
|
||||||
const defaultData = await loadAllUsageData();
|
const defaultData = await loadAllUsageData();
|
||||||
|
|
||||||
@@ -365,10 +372,35 @@ async function refreshFromSource(): Promise<{
|
|||||||
return { daily, hourly, monthly, session };
|
return { daily, hourly, monthly, session };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshFromSourceCoalesced(force = false): Promise<{
|
||||||
|
daily: DailyUsage[];
|
||||||
|
hourly: HourlyUsage[];
|
||||||
|
monthly: MonthlyUsage[];
|
||||||
|
session: SessionUsage[];
|
||||||
|
}> {
|
||||||
|
if (force) {
|
||||||
|
return refreshFromSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingFullRefresh) {
|
||||||
|
return pendingFullRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingFullRefresh = refreshFromSource().finally(() => {
|
||||||
|
pendingFullRefresh = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return pendingFullRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize in-memory cache from disk cache (lazy - called on first API request).
|
* Initialize in-memory cache from disk cache (lazy - called on first API request).
|
||||||
*/
|
*/
|
||||||
function ensureDiskCacheLoaded(): void {
|
function ensureDiskCacheLoaded(): void {
|
||||||
|
// Start sync when usage APIs are actually accessed.
|
||||||
|
// startCliproxySync() is idempotent.
|
||||||
|
startCliproxySync();
|
||||||
|
|
||||||
if (diskCacheInitialized) return;
|
if (diskCacheInitialized) return;
|
||||||
diskCacheInitialized = true;
|
diskCacheInitialized = true;
|
||||||
|
|
||||||
@@ -445,28 +477,28 @@ async function getCachedData<T>(key: string, ttl: number, loader: () => Promise<
|
|||||||
/** Cached loader for daily usage data */
|
/** Cached loader for daily usage data */
|
||||||
export async function getCachedDailyData(): Promise<DailyUsage[]> {
|
export async function getCachedDailyData(): Promise<DailyUsage[]> {
|
||||||
return getCachedData('daily', CACHE_TTL.daily, async () => {
|
return getCachedData('daily', CACHE_TTL.daily, async () => {
|
||||||
return await loadDailyUsageData();
|
return (await refreshFromSourceCoalesced()).daily;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cached loader for monthly usage data */
|
/** Cached loader for monthly usage data */
|
||||||
export async function getCachedMonthlyData(): Promise<MonthlyUsage[]> {
|
export async function getCachedMonthlyData(): Promise<MonthlyUsage[]> {
|
||||||
return getCachedData('monthly', CACHE_TTL.monthly, async () => {
|
return getCachedData('monthly', CACHE_TTL.monthly, async () => {
|
||||||
return await loadMonthlyUsageData();
|
return (await refreshFromSourceCoalesced()).monthly;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cached loader for session data */
|
/** Cached loader for session data */
|
||||||
export async function getCachedSessionData(): Promise<SessionUsage[]> {
|
export async function getCachedSessionData(): Promise<SessionUsage[]> {
|
||||||
return getCachedData('session', CACHE_TTL.session, async () => {
|
return getCachedData('session', CACHE_TTL.session, async () => {
|
||||||
return await loadSessionData();
|
return (await refreshFromSourceCoalesced()).session;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cached loader for hourly usage data */
|
/** Cached loader for hourly usage data */
|
||||||
export async function getCachedHourlyData(): Promise<HourlyUsage[]> {
|
export async function getCachedHourlyData(): Promise<HourlyUsage[]> {
|
||||||
return getCachedData('hourly', CACHE_TTL.daily, async () => {
|
return getCachedData('hourly', CACHE_TTL.daily, async () => {
|
||||||
return await loadHourlyUsageData();
|
return (await refreshFromSourceCoalesced()).hourly;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,9 +507,12 @@ export async function getCachedHourlyData(): Promise<HourlyUsage[]> {
|
|||||||
*/
|
*/
|
||||||
export function clearUsageCache(): void {
|
export function clearUsageCache(): void {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
|
pendingRequests.clear();
|
||||||
|
pendingFullRefresh = null;
|
||||||
clearDiskCache();
|
clearDiskCache();
|
||||||
// Reset so next API call will try to reload from disk/source
|
// Reset so next API call will try to reload from disk/source
|
||||||
diskCacheInitialized = false;
|
diskCacheInitialized = false;
|
||||||
|
lastFetchTimestamp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -537,7 +572,7 @@ export async function prewarmUsageCache(): Promise<{
|
|||||||
// Background refresh
|
// Background refresh
|
||||||
if (!isRefreshing) {
|
if (!isRefreshing) {
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
refreshFromSource()
|
refreshFromSourceCoalesced()
|
||||||
.then(() => console.log(ok('Background refresh complete')))
|
.then(() => console.log(ok('Background refresh complete')))
|
||||||
.catch((err) => console.error(fail(`Background refresh failed: ${err}`)))
|
.catch((err) => console.error(fail(`Background refresh failed: ${err}`)))
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -550,7 +585,7 @@ export async function prewarmUsageCache(): Promise<{
|
|||||||
|
|
||||||
// No usable disk cache - refresh from source (blocking for first startup only)
|
// No usable disk cache - refresh from source (blocking for first startup only)
|
||||||
console.log(info('No disk cache, loading from source...'));
|
console.log(info('No disk cache, loading from source...'));
|
||||||
await refreshFromSource();
|
await refreshFromSourceCoalesced();
|
||||||
|
|
||||||
const elapsed = Date.now() - start;
|
const elapsed = Date.now() - start;
|
||||||
console.log(ok(`Usage cache ready (${elapsed}ms)`));
|
console.log(ok(`Usage cache ready (${elapsed}ms)`));
|
||||||
@@ -567,3 +602,13 @@ export async function prewarmUsageCache(): Promise<{
|
|||||||
export function shutdownUsageAggregator(): void {
|
export function shutdownUsageAggregator(): void {
|
||||||
stopCliproxySync();
|
stopCliproxySync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force refresh usage cache from all sources.
|
||||||
|
* Used by manual refresh endpoint.
|
||||||
|
*/
|
||||||
|
export async function refreshUsageCache(): Promise<void> {
|
||||||
|
// Ensure periodic sync is running for subsequent updates.
|
||||||
|
startCliproxySync();
|
||||||
|
await refreshFromSourceCoalesced(true);
|
||||||
|
}
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ export async function syncCliproxyUsage(): Promise<void> {
|
|||||||
* Performs an immediate sync on startup.
|
* Performs an immediate sync on startup.
|
||||||
*/
|
*/
|
||||||
export function startCliproxySync(): void {
|
export function startCliproxySync(): void {
|
||||||
|
if (syncIntervalId !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(info('Starting CLIProxy usage sync (interval: 5 min)'));
|
console.log(info('Starting CLIProxy usage sync (interval: 5 min)'));
|
||||||
|
|
||||||
// Fire-and-forget initial sync
|
// Fire-and-forget initial sync
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
getCachedMonthlyData,
|
getCachedMonthlyData,
|
||||||
getCachedSessionData,
|
getCachedSessionData,
|
||||||
getCachedHourlyData,
|
getCachedHourlyData,
|
||||||
clearUsageCache,
|
|
||||||
getLastFetchTimestamp,
|
getLastFetchTimestamp,
|
||||||
|
refreshUsageCache,
|
||||||
} from './aggregator';
|
} from './aggregator';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -602,9 +602,13 @@ export async function handleMonthly(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleRefresh(_req: Request, res: Response): void {
|
export async function handleRefresh(_req: Request, res: Response): Promise<void> {
|
||||||
clearUsageCache();
|
try {
|
||||||
res.json({ success: true, message: 'Usage cache cleared' });
|
await refreshUsageCache();
|
||||||
|
res.json({ success: true, message: 'Usage cache refreshed' });
|
||||||
|
} catch (error) {
|
||||||
|
errorResponse(res, error, 'Failed to refresh usage cache');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleStatus(_req: Request, res: Response): void {
|
export function handleStatus(_req: Request, res: Response): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user