fix(web): satisfy contracts-ready lint and dedup the stable-file read

Rescope nothing new: drop the needless async from the inherited readRaw
default (reject explicitly on abort), fix the void arrow shorthand in
downloadBlob, and share one revision-stable file-read loop between readRaw
and readPrefix in the JSONL backend.
This commit is contained in:
_Kerman
2026-08-11 12:49:27 +08:00
parent 118b1a8787
commit 3a6112f641
3 changed files with 28 additions and 28 deletions
@@ -38,5 +38,5 @@ export function downloadBlob(blob: Blob, filename: string): void {
anchor.download = filename anchor.download = filename
anchor.click() anchor.click()
// Revoke one tick later: some browsers read the blob URL after click(). // Revoke one tick later: some browsers read the blob URL after click().
setTimeout(() => URL.revokeObjectURL(url), 0) setTimeout(() => { URL.revokeObjectURL(url) }, 0)
} }
@@ -253,17 +253,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
signal?.throwIfAborted() signal?.throwIfAborted()
const path = await this.findLog(id, signal) const path = await this.findLog(id, signal)
if (path === undefined) return undefined if (path === undefined) return undefined
let buffer: Buffer const { buffer } = await this.readStableFile(path, signal)
// Revision-stable read: a writer appending between stat and readFile
// would yield a torn physical file (see readPrefix).
for (;;) {
signal?.throwIfAborted()
const before = fileRevision(await stat(path, { bigint: true }))
buffer = await readFile(path, { signal })
signal?.throwIfAborted()
const after = fileRevision(await stat(path, { bigint: true }))
if (before === after) break
}
let content: string let content: string
if (this.compression === 'zstd') { if (this.compression === 'zstd') {
const { frames } = scanZstdFrames(buffer) const { frames } = scanZstdFrames(buffer)
@@ -289,6 +279,28 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
return { meta, filename: 'session.jsonl', content } return { meta, filename: 'session.jsonl', content }
} }
/**
* Read a file's bytes under a revision-stable loop: a writer appending
* between stat and readFile would yield a torn physical file, so retry
* while the stat revision changes.
* @param path - the artifact file to read.
* @param signal - optional cancellation for the stat/read work.
* @returns the stable bytes and the revision that matched both stats.
*/
private async readStableFile(
path: string,
signal?: AbortSignal,
): Promise<{ buffer: Buffer; revision: PersistenceRevision }> {
for (;;) {
signal?.throwIfAborted()
const before = fileRevision(await stat(path, { bigint: true }))
const buffer = await readFile(path, { signal })
signal?.throwIfAborted()
const after = fileRevision(await stat(path, { bigint: true }))
if (before === after) return { buffer, revision: after }
}
}
/** /**
* Read a stored prefix and convert torn-tail state to the opaque marker the * Read a stored prefix and convert torn-tail state to the opaque marker the
* coordinator can round-trip without knowing the physical encoding. * coordinator can round-trip without knowing the physical encoding.
@@ -298,19 +310,7 @@ export class SessionPersistenceJsonl extends SessionPersistence implements Persi
expectedId?: SessionId, expectedId?: SessionId,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<StoredPrefix<JsonlTornMarker>> { ): Promise<StoredPrefix<JsonlTornMarker>> {
let buffer: Buffer const { buffer, revision } = await this.readStableFile(path, signal)
let revision: PersistenceRevision
for (;;) {
signal?.throwIfAborted()
const before = fileRevision(await stat(path, { bigint: true }))
buffer = await readFile(path, { signal })
signal?.throwIfAborted()
const after = fileRevision(await stat(path, { bigint: true }))
if (before === after) {
revision = after
break
}
}
let prefix: Omit<StoredPrefix<JsonlTornMarker>, 'revision'> let prefix: Omit<StoredPrefix<JsonlTornMarker>, 'revision'>
try { try {
if (this.compression === 'zstd') { if (this.compression === 'zstd') {
@@ -108,9 +108,9 @@ export abstract class SessionPersistence extends Service {
* @returns the raw artifact plus its parsed header, or `undefined` when the * @returns the raw artifact plus its parsed header, or `undefined` when the
* session is absent or the backend owns no per-session artifact. * session is absent or the backend owns no per-session artifact.
*/ */
async readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | undefined> { readRaw(_id: SessionId, signal?: AbortSignal): Promise<SessionRawArtifact | undefined> {
signal?.throwIfAborted() if (signal?.aborted === true) return Promise.reject(signal.reason)
return undefined return Promise.resolve(undefined)
} }
/** /**