45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
|
|
/** A one-shot, observable scheduling boundary. An unexercised gate is a failure. */
|
|
export class Checkpoint<T> {
|
|
private readonly reached = Promise.withResolvers<T>();
|
|
private readonly released = Promise.withResolvers<void>();
|
|
public fired = false;
|
|
public resumed = false;
|
|
public value?: T;
|
|
|
|
public async enter(value: T, signal?: AbortSignal | null): Promise<void> {
|
|
assert(!this.fired, "Checkpoint entered twice");
|
|
this.fired = true;
|
|
this.value = structuredClone(value);
|
|
this.reached.resolve(this.value);
|
|
signal?.throwIfAborted();
|
|
let abort = () => {};
|
|
try {
|
|
await Promise.race([
|
|
this.released.promise,
|
|
new Promise<never>((_, reject) => {
|
|
abort = () =>
|
|
reject(
|
|
signal?.reason ?? new Error("Checkpoint aborted")
|
|
);
|
|
signal?.addEventListener("abort", abort, { once: true });
|
|
})
|
|
]);
|
|
} finally {
|
|
signal?.removeEventListener("abort", abort);
|
|
}
|
|
}
|
|
|
|
public wait(): Promise<T> {
|
|
return this.reached.promise;
|
|
}
|
|
public resume(): void {
|
|
this.resumed = true;
|
|
this.released.resolve();
|
|
}
|
|
public assertConsumed(): void {
|
|
assert(this.fired, "Checkpoint never reached");
|
|
assert(this.resumed, "Checkpoint never released");
|
|
}
|
|
}
|