89 lines
4.7 KiB
TypeScript
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()]);
|
|
}
|
|
}
|