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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user