Files
ng-eventually/packages/polyfill/src/emulated-verifier/inbox-drain.ts
T

89 lines
4.7 KiB
TypeScript

/**
* ONE drain at a time per inbox — the serializer both things that drain an inbox go through.
*
* Two of them exist in this package, and they are not coordinated by anything else: the
* CONTINUOUS observation of the connected identity's own inboxes
* (`emulated-verifier/inbox-observer.ts`) and the deferred timer that stands in for an
* ABSENT owner (`emulated-verifier/inbox-processor.ts`). Both can be pointed at the same
* inbox in the same page — a deposit into an inbox whose owner is also connected here arms
* the timer AND pushes to the observation.
*
* Draining writes what it applies (`branch-registers.addLink`, on the owner's User branch),
* so two passes over one queue interleave their reads and writes and apply the same records
* twice. Sequencing them is the same reasoning `connect.connectedUser` drains its inboxes
* one after another for, and it belongs HERE rather than in either caller: an invariant that
* holds only while both callers remember it is not an invariant.
*
* It is a queue, not a lock: a run that arrives while another is going is not dropped, it
* follows. Dropping would be wrong — the second run exists because something new landed.
*/
import { accessLogPrefix, shortNuri } from "../shared-wallet/access-log";
import type { Nuri } from "../model/types";
/** Drains in flight or queued, per inbox — the chain a new run appends itself to. */
const running = new Map<Nuri, Promise<void>>();
/**
* Where a drain's failure goes.
*
* It cannot throw: nothing awaits these — neither the timer nor a push is a caller — and an
* unhandled rejection would take down whatever runtime it is in over work the application
* never requested. But "nowhere" is not the alternative. A drain that swallows its failure
* is indistinguishable from one that succeeded, which is precisely the defect family removed
* from this package on 2026-08-13, and a CONTINUOUS drain that swallowed would repeat that
* silence for as long as the session lasts. So it goes to the package's own log stream — the
* same `console.error` with the same `[<identity>][polyfill]` prefix `startConnect` and
* `inbox.watch` use — and it is NOT gated by the access-log flag: a diagnostic may be
* opt-in, a failure may not.
*
* One failed item denies nothing, here as at connection. What it no longer promises is a
* RETRY: since 2026-08-26 a deposit is popped before its arm runs
* (`emulated-verifier/inbox-consume.ts`), so whatever had already been delivered when this
* drain fell over is gone, exactly as upstream loses a message the broker took out of the
* queue. What is still QUEUED is untouched and the next push or connection reaches it —
* which is a different sentence, and the wording says so rather than promising the old one.
*/
function reportFailure(inbox: Nuri, error: unknown): void {
console.error(
accessLogPrefix() + " could not finish draining this inbox — whatever it had already " +
"taken is consumed; what is still queued waits for the next attempt: " +
shortNuri(inbox) + ":",
error,
);
}
/**
* Run `process` for `inbox`, behind any run of the same inbox still in flight.
*
* Resolves when THIS run has finished (or failed, having reported itself), so a caller
* that wants to wait can — while a caller that does not simply drops the promise.
*/
// @provenance drainInboxSerially kind=aligned level=1 ref=engine/verifier/src/verifier.rs:inbox — upstream one verifier owns an inbox and applies its messages one at a time as they arrive; this is that serialization, for the two paths this package drains from
export function drainInboxSerially(inbox: Nuri, process: () => Promise<void>): Promise<void> {
const previous = running.get(inbox) ?? Promise.resolve();
const next = previous
// A previous run that FAILED must not cancel this one: its failure was reported where
// failures go, and the deposits it did not apply are exactly what this run is for.
.catch(() => undefined)
.then(process)
.catch((error: unknown) => reportFailure(inbox, error))
.then(() => {
if (running.get(inbox) === next) running.delete(inbox);
});
running.set(inbox, next);
return next;
}
/**
* Wait until no drain is in flight. The seam a suite needs to observe convergence without
* waiting for it — and the one `runScheduledInboxProcessingNow` awaits after firing its
* windows. Loops, because a drain can be chained behind the one being awaited.
*/
// @provenance drainsSettled kind=invention level=none ref=none — a test/lifecycle seam over the queue above; upstream nothing exposes "is the verifier done applying"
export async function drainsSettled(): Promise<void> {
while (running.size > 0) {
await Promise.all([...running.values()]);
}
}