chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* BT-X-01. Shared abort and deadline mechanics.
|
||||
*
|
||||
* Several adapters independently reimplemented "race a promise against a
|
||||
* caller signal and a deadline, then clean up listeners and timers". Only the
|
||||
* mechanics are shared here; every subsystem keeps its own result taxonomy and
|
||||
* recovery vocabulary, so this module deliberately imports none of them and is
|
||||
* not a generic middleware layer.
|
||||
*/
|
||||
|
||||
export type AbortTerminalReason = "CALLER_ABORT" | "DEADLINE" | "CLOSED";
|
||||
|
||||
/**
|
||||
* TR-RR-05. A collaborator's own rejection is evidence about the work, so it is
|
||||
* a distinct outcome. Forging it into `TERMINAL` erased the reason the
|
||||
* operation actually failed and made `race()` disagree with `terminal()`, which
|
||||
* still reported no owner.
|
||||
*/
|
||||
export type AbortRace<Value> =
|
||||
| Readonly<{ kind: "VALUE"; value: Value }>
|
||||
| Readonly<{ kind: "REJECTED"; reason: unknown }>
|
||||
| Readonly<{ kind: "TERMINAL"; terminal: AbortTerminalReason }>;
|
||||
|
||||
export type AbortableOperation = Readonly<{
|
||||
/** The composed signal: caller abort, deadline and close all feed it. */
|
||||
readonly signal: AbortSignal;
|
||||
/**
|
||||
* The first terminal owner, or `null` while the operation is still live.
|
||||
* This is a live accessor, not a snapshot.
|
||||
*/
|
||||
terminal(): AbortTerminalReason | null;
|
||||
/**
|
||||
* Resolves with the operation's value, its own rejection, or the first
|
||||
* terminal owner. A terminal race never returns a bare value, and the
|
||||
* `terminal` it reports is always the same owner `terminal()` reports.
|
||||
*
|
||||
* `compensate` is invoked at most once, and only for a value that arrived
|
||||
* after the operation already ended.
|
||||
*/
|
||||
race<Value>(
|
||||
operation: Promise<Value>,
|
||||
compensate?: (value: Value) => void,
|
||||
): Promise<AbortRace<Value>>;
|
||||
/**
|
||||
* Idempotent. Removes listeners, clears the deadline timer and marks the
|
||||
* operation `CLOSED` if nothing terminal happened first.
|
||||
*/
|
||||
close(): void;
|
||||
}>;
|
||||
|
||||
export type AbortableOperationInput = Readonly<{
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
/** Scheduler seam; a throwing scheduler must not leak a listener. */
|
||||
setTimer?: (callback: () => void, delayMs: number) => unknown;
|
||||
clearTimer?: (handle: unknown) => void;
|
||||
}>;
|
||||
|
||||
export type AbortTimerSnapshot = Readonly<{
|
||||
setTimer: (callback: () => void, delayMs: number) => unknown;
|
||||
clearTimer: (handle: unknown) => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* X-AUDIT-02. Captures a scheduler's timer callables once, bound to their
|
||||
* receiver. Consumers that kept the scheduler object and re-read `setTimeout`
|
||||
* per request validated one function and executed another, so replacing a
|
||||
* method after composition silently changed how work was bounded.
|
||||
*/
|
||||
export function snapshotAbortTimers<Handle>(
|
||||
scheduler: Readonly<{
|
||||
setTimeout(callback: () => void, milliseconds: number): Handle;
|
||||
clearTimeout(handle: Handle): void;
|
||||
}>,
|
||||
): AbortTimerSnapshot {
|
||||
const set = scheduler?.setTimeout;
|
||||
const clear = scheduler?.clearTimeout;
|
||||
if (typeof set !== "function" || typeof clear !== "function") {
|
||||
throw new TypeError("Timer scheduler is invalid.");
|
||||
}
|
||||
return Object.freeze({
|
||||
setTimer: set.bind(scheduler) as (
|
||||
callback: () => void,
|
||||
delayMs: number,
|
||||
) => unknown,
|
||||
clearTimer: clear.bind(scheduler) as (handle: unknown) => void,
|
||||
});
|
||||
}
|
||||
|
||||
export function createAbortableOperation(
|
||||
input: AbortableOperationInput = {},
|
||||
): AbortableOperation {
|
||||
const controller = new AbortController();
|
||||
// TR-RR-05. The scheduler and the caller signal are captured once, so
|
||||
// replacing a method on the input object after construction cannot change how
|
||||
// an operation already in flight is bounded or cleaned up.
|
||||
const callerSignal = input.signal;
|
||||
const setTimer =
|
||||
input.setTimer ??
|
||||
((callback: () => void, delayMs: number) => setTimeout(callback, delayMs));
|
||||
const clearTimer =
|
||||
input.clearTimer ??
|
||||
((handle: unknown) => {
|
||||
clearTimeout(handle as ReturnType<typeof setTimeout>);
|
||||
});
|
||||
|
||||
let terminalReason: AbortTerminalReason | null = null;
|
||||
let disposed = false;
|
||||
let timer: unknown;
|
||||
|
||||
/** First terminal owner wins; later owners never overwrite it. */
|
||||
const settle = (reason: AbortTerminalReason) => {
|
||||
terminalReason ??= reason;
|
||||
if (!controller.signal.aborted) controller.abort();
|
||||
};
|
||||
|
||||
const onCallerAbort = () => {
|
||||
settle("CALLER_ABORT");
|
||||
dispose();
|
||||
};
|
||||
|
||||
function dispose(): void {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
try {
|
||||
callerSignal?.removeEventListener("abort", onCallerAbort);
|
||||
} catch {
|
||||
// A hostile signal facade cannot block cleanup of the rest.
|
||||
}
|
||||
if (timer !== undefined) {
|
||||
try {
|
||||
clearTimer(timer);
|
||||
} catch {
|
||||
// A throwing scheduler cannot leave the operation un-disposed.
|
||||
}
|
||||
timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (callerSignal?.aborted) {
|
||||
settle("CALLER_ABORT");
|
||||
disposed = true;
|
||||
} else if (callerSignal) {
|
||||
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
||||
}
|
||||
|
||||
if (
|
||||
terminalReason === null &&
|
||||
input.timeoutMs !== undefined &&
|
||||
Number.isFinite(input.timeoutMs) &&
|
||||
input.timeoutMs >= 0
|
||||
) {
|
||||
try {
|
||||
timer = setTimer(() => {
|
||||
settle("DEADLINE");
|
||||
dispose();
|
||||
}, input.timeoutMs);
|
||||
} catch {
|
||||
// TR-RR-05. A scheduler that cannot install the deadline leaves the
|
||||
// operation unbounded. Removing the caller listener and leaving no
|
||||
// terminal owner made a later abort invisible, so installation failure is
|
||||
// itself terminal: the operation closes atomically with its resources.
|
||||
timer = undefined;
|
||||
settle("CLOSED");
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
signal: controller.signal,
|
||||
terminal: () => terminalReason,
|
||||
race<Value>(
|
||||
operation: Promise<Value>,
|
||||
compensate?: (value: Value) => void,
|
||||
): Promise<AbortRace<Value>> {
|
||||
let compensated = false;
|
||||
const compensateOnce = (value: Value) => {
|
||||
if (compensated || !compensate) return;
|
||||
compensated = true;
|
||||
try {
|
||||
compensate(value);
|
||||
} catch {
|
||||
// Compensation is best effort and never changes the outcome.
|
||||
}
|
||||
};
|
||||
const terminalRace = (): AbortRace<Value> =>
|
||||
Object.freeze({
|
||||
kind: "TERMINAL" as const,
|
||||
// The owner reported here is always the owner `terminal()` reports.
|
||||
terminal: terminalReason ?? ("CLOSED" as const),
|
||||
});
|
||||
|
||||
if (terminalReason !== null) {
|
||||
// Already owned before the task was ever raced: nothing it produces can
|
||||
// be admitted, so a value is compensated and a rejection absorbed.
|
||||
void operation.then(
|
||||
(value) => compensateOnce(value),
|
||||
() => undefined,
|
||||
);
|
||||
return Promise.resolve(terminalRace());
|
||||
}
|
||||
|
||||
// X-AUDIT-01. Task settlement and the terminal event share one settle-once
|
||||
// state machine, so the outcome is whichever callback actually ran first.
|
||||
// Draining a fixed number of microtasks to guess whether a promise "had
|
||||
// already settled" made the answer depend on scheduling rather than on
|
||||
// observation, and let a rejection overwrite an owner that was fixed
|
||||
// synchronously before it.
|
||||
return new Promise<AbortRace<Value>>((resolve) => {
|
||||
let claimed = false;
|
||||
let onAbort: (() => void) | undefined;
|
||||
const release = () => {
|
||||
if (!onAbort) return;
|
||||
controller.signal.removeEventListener("abort", onAbort);
|
||||
onAbort = undefined;
|
||||
};
|
||||
const claim = (outcome: AbortRace<Value>): boolean => {
|
||||
if (claimed) return false;
|
||||
claimed = true;
|
||||
release();
|
||||
resolve(outcome);
|
||||
return true;
|
||||
};
|
||||
|
||||
operation.then(
|
||||
(value) => {
|
||||
if (!claim(Object.freeze({ kind: "VALUE" as const, value }))) {
|
||||
// The work landed after the operation already ended.
|
||||
compensateOnce(value);
|
||||
}
|
||||
},
|
||||
(reason: unknown) => {
|
||||
// A rejection that loses the claim is absorbed here, so it can never
|
||||
// surface as an unhandled rejection.
|
||||
claim(Object.freeze({ kind: "REJECTED" as const, reason }));
|
||||
},
|
||||
);
|
||||
|
||||
if (controller.signal.aborted) {
|
||||
claim(terminalRace());
|
||||
return;
|
||||
}
|
||||
onAbort = () => {
|
||||
claim(terminalRace());
|
||||
};
|
||||
controller.signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
},
|
||||
close() {
|
||||
settle("CLOSED");
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compensates a native handle that arrives after the operation ended. The
|
||||
* compensation itself is best effort and can never change the already selected
|
||||
* outcome.
|
||||
*/
|
||||
export function compensateLateHandle(
|
||||
handle: Promise<Readonly<{ body?: { cancel(): Promise<void> } | null }> | null>,
|
||||
): void {
|
||||
void handle
|
||||
.then(async (value) => {
|
||||
await value?.body?.cancel();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
Reference in New Issue
Block a user