/** * 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>(); /** * 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 `[][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): Promise { 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 { while (running.size > 0) { await Promise.all([...running.values()]); } }