Files
rea/src/filesystem/OwnedFileHandle.ts
T
morluto be6272f261 fix(artifacts)!: preserve descriptor cleanup authority
Keep rejected closes unresolved after descriptor invalidation and share concurrent close attempts. Preserve primary admission failures and transfer the exact failed handle owner into inventory scopes.

BREAKING CHANGE: ArtifactResourceScope file-handle owners require OwnedFileHandle. Root inventory sources retain their single cleanup owner.
2026-10-11 05:17:10 +08:00

46 lines
1.5 KiB
TypeScript

import type { FileHandle } from "node:fs/promises";
type CloseState =
| { readonly kind: "open" }
| { readonly kind: "closing"; readonly attempt: Promise<void> }
| { readonly kind: "closed" }
| { readonly kind: "failed"; readonly cause: unknown };
/** One file handle whose close result must remain truthful across retries. */
export class OwnedFileHandle<
Handle extends Pick<FileHandle, "fd" | "close"> = FileHandle,
> {
#state: CloseState = { kind: "open" };
constructor(readonly handle: Handle) {}
/** Close the owned handle, retaining an unconfirmed close as a sticky failure. */
close(): Promise<void> {
if (this.#state.kind === "closed") return Promise.resolve();
if (this.#state.kind === "closing") return this.#state.attempt;
if (this.#state.kind === "failed") return Promise.reject(this.#state.cause);
if (this.handle.fd < 0) {
const cause = new Error(
"File handle was invalidated before this owner confirmed its close",
);
this.#state = { kind: "failed", cause };
return Promise.reject(cause);
}
const attempt = Promise.resolve().then(() => this.#attemptClose());
this.#state = { kind: "closing", attempt };
return attempt;
}
async #attemptClose(): Promise<void> {
try {
await this.handle.close();
this.#state = { kind: "closed" };
} catch (cause: unknown) {
this.#state =
this.handle.fd < 0 ? { kind: "failed", cause } : { kind: "open" };
throw cause;
}
}
}