Skip to content

Commit 9cd44ac

Browse files
authored
Merge pull request #69 from kaitranntt/dev
perf(analytics): instant dashboard loading with disk cache persistence
2 parents 23e66c2 + 4c248ec commit 9cd44ac

File tree

6 files changed

+344
-29
lines changed

6 files changed

+344
-29
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.12.0
1+
5.12.0-dev.1

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kaitranntt/ccs",
3-
"version": "5.12.0",
3+
"version": "5.12.0-dev.1",
44
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
55
"keywords": [
66
"cli",

src/web-server/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,8 @@ export async function startServer(options: ServerOptions): Promise<ServerInstanc
7777
// Start listening
7878
return new Promise<ServerInstance>((resolve) => {
7979
server.listen(options.port, () => {
80-
// Non-blocking prewarm: load usage cache in background
81-
import('./usage-routes').then(({ prewarmUsageCache }) => {
82-
prewarmUsageCache().catch(() => {
83-
// Error already logged in prewarmUsageCache
84-
});
85-
});
86-
80+
// Usage cache loads on-demand when Analytics page is visited
81+
// This keeps server startup instant for users who don't need analytics
8782
resolve({ server, wss, cleanup });
8883
});
8984
});

src/web-server/usage-disk-cache.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Persistent Disk Cache for Usage Data
3+
*
4+
* Caches aggregated usage data to disk to avoid re-parsing 6000+ JSONL files
5+
* on every dashboard startup. Uses TTL-based invalidation with stale-while-revalidate.
6+
*
7+
* Cache location: ~/.ccs/usage-cache.json
8+
* Default TTL: 5 minutes (configurable)
9+
*/
10+
11+
import * as fs from 'fs';
12+
import * as path from 'path';
13+
import * as os from 'os';
14+
import type { DailyUsage, MonthlyUsage, SessionUsage } from 'better-ccusage/data-loader';
15+
16+
// Cache configuration
17+
const CCS_DIR = path.join(os.homedir(), '.ccs');
18+
const CACHE_FILE = path.join(CCS_DIR, 'usage-cache.json');
19+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
20+
const STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days (max age for stale data)
21+
22+
/** Structure of the disk cache file */
23+
export interface UsageDiskCache {
24+
version: number;
25+
timestamp: number;
26+
daily: DailyUsage[];
27+
monthly: MonthlyUsage[];
28+
session: SessionUsage[];
29+
}
30+
31+
// Current cache version - increment to invalidate old caches
32+
const CACHE_VERSION = 1;
33+
34+
/**
35+
* Ensure ~/.ccs directory exists
36+
*/
37+
function ensureCcsDir(): void {
38+
if (!fs.existsSync(CCS_DIR)) {
39+
fs.mkdirSync(CCS_DIR, { recursive: true });
40+
}
41+
}
42+
43+
/**
44+
* Read usage data from disk cache
45+
* Returns null if cache is missing, corrupted, or has incompatible version
46+
* NOTE: Does NOT reject based on age - caller handles staleness via SWR pattern
47+
*/
48+
export function readDiskCache(): UsageDiskCache | null {
49+
try {
50+
if (!fs.existsSync(CACHE_FILE)) {
51+
return null;
52+
}
53+
54+
const data = fs.readFileSync(CACHE_FILE, 'utf-8');
55+
const cache: UsageDiskCache = JSON.parse(data);
56+
57+
// Version check - invalidate if schema changed
58+
if (cache.version !== CACHE_VERSION) {
59+
console.log('[i] Cache version mismatch, will refresh');
60+
return null;
61+
}
62+
63+
// Always return cache regardless of age - SWR pattern handles staleness
64+
return cache;
65+
} catch (err) {
66+
// Cache corrupted or unreadable - treat as miss
67+
console.log('[i] Cache read failed, will refresh:', (err as Error).message);
68+
return null;
69+
}
70+
}
71+
72+
/**
73+
* Check if disk cache is fresh (within TTL)
74+
*/
75+
export function isDiskCacheFresh(cache: UsageDiskCache | null): boolean {
76+
if (!cache) return false;
77+
const age = Date.now() - cache.timestamp;
78+
return age < CACHE_TTL_MS;
79+
}
80+
81+
/**
82+
* Check if disk cache is stale but usable (between TTL and STALE_TTL)
83+
*/
84+
export function isDiskCacheStale(cache: UsageDiskCache | null): boolean {
85+
if (!cache) return false;
86+
const age = Date.now() - cache.timestamp;
87+
return age >= CACHE_TTL_MS && age < STALE_TTL_MS;
88+
}
89+
90+
/**
91+
* Write usage data to disk cache
92+
*/
93+
export function writeDiskCache(
94+
daily: DailyUsage[],
95+
monthly: MonthlyUsage[],
96+
session: SessionUsage[]
97+
): void {
98+
try {
99+
ensureCcsDir();
100+
101+
const cache: UsageDiskCache = {
102+
version: CACHE_VERSION,
103+
timestamp: Date.now(),
104+
daily,
105+
monthly,
106+
session,
107+
};
108+
109+
// Write atomically using temp file + rename
110+
const tempFile = CACHE_FILE + '.tmp';
111+
fs.writeFileSync(tempFile, JSON.stringify(cache), 'utf-8');
112+
fs.renameSync(tempFile, CACHE_FILE);
113+
114+
console.log('[OK] Disk cache updated');
115+
} catch (err) {
116+
// Non-fatal - we can still serve from memory
117+
console.log('[!] Failed to write disk cache:', (err as Error).message);
118+
}
119+
}
120+
121+
/**
122+
* Get cache age in human-readable format
123+
*/
124+
export function getCacheAge(cache: UsageDiskCache | null): string {
125+
if (!cache) return 'never';
126+
127+
const age = Date.now() - cache.timestamp;
128+
const seconds = Math.floor(age / 1000);
129+
const minutes = Math.floor(seconds / 60);
130+
const hours = Math.floor(minutes / 60);
131+
132+
if (hours > 0) return `${hours}h ${minutes % 60}m ago`;
133+
if (minutes > 0) return `${minutes}m ${seconds % 60}s ago`;
134+
return `${seconds}s ago`;
135+
}
136+
137+
/**
138+
* Delete disk cache (for manual refresh)
139+
*/
140+
export function clearDiskCache(): void {
141+
try {
142+
if (fs.existsSync(CACHE_FILE)) {
143+
fs.unlinkSync(CACHE_FILE);
144+
console.log('[OK] Disk cache cleared');
145+
}
146+
} catch (err) {
147+
console.log('[!] Failed to clear disk cache:', (err as Error).message);
148+
}
149+
}

0 commit comments

Comments
 (0)