-
Notifications
You must be signed in to change notification settings - Fork 674
Expand file tree
/
Copy pathcursor-cache.ts
More file actions
88 lines (75 loc) · 2.91 KB
/
Copy pathcursor-cache.ts
File metadata and controls
88 lines (75 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { readFile, writeFile, mkdir, rename, stat, unlink } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'
import { randomBytes } from 'crypto'
import type { ParsedProviderCall } from './providers/types.js'
// Bumped to 3 for the workspace-aware breakdown change: the cursor parser
// now derives `sessionId` from the bubble row key (the real composer id)
// rather than the empty `conversationId` JSON field, and the workspace
// router relies on those composer ids to bucket calls per project.
// Version 2 caches contain `sessionId: 'unknown'` for every call and would
// route everything to the orphan project, so we invalidate them.
// Bumped to 4 after skipping bubble rows without createdAt: version 3 caches
// could contain historical Cursor bubbles timestamped with the parse time.
const CURSOR_CACHE_VERSION = 4
type ResultCache = {
version?: number
dbMtimeMs: number
dbSizeBytes: number
calls: ParsedProviderCall[]
}
const CACHE_FILE = 'cursor-results.json'
function getCacheDir(): string {
return process.env['CODEBURN_CACHE_DIR'] ?? join(homedir(), '.cache', 'codeburn')
}
function getCachePath(): string {
return join(getCacheDir(), CACHE_FILE)
}
async function getDbFingerprint(dbPath: string): Promise<{ mtimeMs: number; size: number } | null> {
try {
const s = await stat(dbPath)
return { mtimeMs: s.mtimeMs, size: s.size }
} catch {
return null
}
}
export async function readCachedResults(dbPath: string): Promise<ParsedProviderCall[] | null> {
try {
const fp = await getDbFingerprint(dbPath)
if (!fp) return null
const raw = await readFile(getCachePath(), 'utf-8')
const cache = JSON.parse(raw) as ResultCache
if (cache.version === CURSOR_CACHE_VERSION && cache.dbMtimeMs === fp.mtimeMs && cache.dbSizeBytes === fp.size) {
return cache.calls
}
return null
} catch {
return null
}
}
export async function writeCachedResults(dbPath: string, calls: ParsedProviderCall[]): Promise<void> {
const fp = await getDbFingerprint(dbPath)
if (!fp) return
const dir = getCacheDir()
await mkdir(dir, { recursive: true }).catch(() => {})
const cache: ResultCache = {
version: CURSOR_CACHE_VERSION,
dbMtimeMs: fp.mtimeMs,
dbSizeBytes: fp.size,
calls,
}
// Atomic write: stage to a randomized temp file in the same directory,
// then rename onto the final path. rename() is atomic on POSIX, so a
// crash mid-write never leaves a half-written cache, and concurrent
// CLI invocations using their own random temp names cannot interleave
// bytes in the destination file (they only race on the final rename,
// last-writer-wins, both with valid content).
const target = getCachePath()
const tempPath = `${target}.${randomBytes(8).toString('hex')}.tmp`
try {
await writeFile(tempPath, JSON.stringify(cache), 'utf-8')
await rename(tempPath, target)
} catch {
await unlink(tempPath).catch(() => {})
}
}