Files
orca/src/main/usage-cache-snapshot-writer.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

62 lines
2.5 KiB
TypeScript

import { mkdir } from 'node:fs/promises'
import { dirname } from 'node:path'
import {
durableWriteTempPath,
removeStaleDurableWriteTempFiles,
writeFileDurableIfCurrent
} from './durable-file-write'
/**
* Serialized durable writer for the per-provider usage caches. One per store: the multi-MB JSON must
* not block the Electron main thread, writes are queued so two overlapping renames can't both veto
* themselves, and a snapshot superseded before its turn is dropped instead of paying for a rewrite.
*/
export class UsageCacheSnapshotWriter {
private generation = 0
private pending: Promise<void>
constructor(
private readonly logTag: string,
private readonly resolveFile: () => string
) {
// Why: a crash between write and rename orphans a multi-MB temp file; reclaim once per launch.
// Seeding the write queue with it also orders the sweep ahead of the first write of this launch.
this.pending = removeStaleDurableWriteTempFiles(resolveFile())
}
/** `serialize` runs only if this snapshot is still the newest one when its turn comes. */
write(serialize: () => string): Promise<void> {
const generation = ++this.generation
const write = this.pending.then(() => this.commit(generation, serialize))
// Why: keep the queue usable after a failure — the awaiting caller still sees the rejection.
this.pending = write.catch((error: unknown) => {
console.error(`${this.logTag} Failed to persist usage cache:`, error)
})
return write
}
/** Await every write queued so far. Never rejects — the queue already logs its own failures. */
flush(): Promise<void> {
return this.pending
}
private async commit(generation: number, serialize: () => string): Promise<void> {
// Why: a newer snapshot is already queued behind this one, so rewriting the cache here is wasted.
if (generation !== this.generation) {
return
}
// Why serialize here and not at queue time: it blocks the main process for a multi-MB cache, and
// superseded generations must not pay for it. Synchronous, so no mutation can tear the JSON.
const payload = serialize()
const usageFile = this.resolveFile()
await mkdir(dirname(usageFile), { recursive: true }).catch(() => {})
// Why: atomic temp-file + durable rename so a crash cannot leave a truncated analytics file.
await writeFileDurableIfCurrent(
durableWriteTempPath(usageFile),
usageFile,
payload,
() => generation === this.generation
)
}
}