mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
be6272f261
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.
46 lines
1.5 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|