Files
ng-eventually/packages/polyfill/src/emulated-verifier/inbox-processor.ts
T
Sylvain Duchesne 43aadbeb45 docs: chaque symbole dit d'où il vient
98 annotations posées à côté des déclarations, et un test qui les exige sur la
surface publiée. Elles portent trois choses : le niveau qui répond, la référence
amont, et la catégorie parmi les cinq.

La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT
la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que
j'avais classé « notre invention » en raisonnant depuis l'absence, alors que
c'est le meilleur alignement disponible.

Les références citent un SYMBOLE, jamais une ligne : trois citations du document
avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source —
un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout
docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos
noms, plus un argument jamais transmis. La sémantique survit à la migration,
les sites d'appel non, et la nuance disparaissait sous une étiquette trop
flatteuse.

Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à
une invention qui prétendrait citer une référence — vérifié en cassant les
trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé
dans index.ts se faisait analyser comme une annotation.

La classification couvre l'interne qui prétend ressembler à la cible — tout
emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine
portait sur une fonction non exportée ; n'être pas publié n'a protégé personne.

Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la
seconde nommée dans la note plutôt que lissée.
2026-08-16 22:53:50 +02:00

179 lines
9.3 KiB
TypeScript

/**
* The inbox-processing SERVICE this deployment has not got — emulated by a timer.
*
* ── What is missing, and what stands in for it ────────────────────────────
* Upstream a deposit does not wait for anybody: the broker routes the sealed message
* (`inboxes: PubKey → RepoId`, `engine/verifier/src/verifier.rs:105,1677`) and the
* recipient's verifier applies it when it processes the inbox
* (`engine/verifier/src/inbox_processor.rs`). A real NextGraph deployment will have
* something running continuously that does this; today, in this library, the ONLY thing
* that drains an inbox is its owner connecting (`connect.connectedUser` →
* `inbox.processInbox`), which can be hours away.
*
* So: a successful deposit arms a ONE-SHOT timer that processes the TARGET's inbox, twenty
* seconds later. Alice deposits into Bob's inbox; twenty seconds later this package drains
* Bob's inbox, while the connected session is still Alice's.
*
* ── That is identity usurpation, and it is deliberate ─────────────────────
* It is only possible because one shared wallet holds every virtual identity, and it is
* acceptable for exactly one reason: an application learns nothing false from it. What an
* application observes is that **deposits converge** — a share becomes readable without the
* recipient having to re-open the page — and that stays true when a real service takes over.
* What must never leak is the MECHANISM: nothing here is published, there is no way for a
* caller to ask for it, to name another user's inbox, or to turn it off. Publishing any of
* that would publish a call that processes someone else's inbox, and the confidentiality of
* the whole shared-wallet emulation rests on that being unreachable.
*
* ── Its accepted limit, stated rather than papered over ───────────────────
* A timer lives in a page. If the page goes away before it fires, the deposit simply waits
* for its owner's next connection — exactly where it waited before. There is deliberately
* no persistence, no retry and no longer window to hide that: a stand-in for an absent
* service should look like what it is, and the fallback (the owner connects and drains) is
* the real path, not a repair.
*/
import { accessLogPrefix, logStage, shortNuri } from "../shared-wallet/access-log";
import type { Nuri } from "../model/types";
/**
* How long a deposit waits before the emulated service picks it up.
*
* Not configurable, on purpose. A knob would be a published question — *how fast does
* delivery converge?* — that the target does not ask a caller, because upstream the answer
* belongs to the deployment. An application must be written so that the number does not
* matter, and the surest way to keep it that way is to give nobody a way to change it.
*/
const PROCESSING_DELAY_MS = 20_000;
/** A drain waiting to happen: the timer, and what it will do. */
interface Scheduled {
handle: ReturnType<typeof setTimeout>;
process: () => Promise<void>;
}
/**
* At most ONE pending drain per inbox — the coalescing this map exists for.
*
* Several deposits inside the window must produce ONE drain, not several: processing writes
* what it applies (`branch-registers.addLink`, on the owner's User branch), so two runs over
* the same inbox race each other or apply the same Link twice. The FIRST deposit arms the
* window and later ones join it, rather than each pushing it further out — a busy inbox must
* still converge, not be starved by its own traffic.
*/
const scheduled = new Map<Nuri, Scheduled>();
/**
* Drains in flight, per inbox — so a run that starts while another is still going CHAINS
* behind it instead of interleaving with it. Same reason `connect.connectedUser` drains its
* inboxes sequentially: two concurrent passes over one queue apply the same records twice.
*/
const running = new Map<Nuri, Promise<void>>();
/**
* Keep a pending drain from holding a runtime open.
*
* A browser's `setTimeout` answers a number and has no `unref`; a Node/Bun runtime answers a
* timer object that does, and without it a process would sit for twenty seconds after its
* last deposit waiting on work nobody asked for. Probed rather than assumed, because the two
* platforms genuinely differ and this library runs on both.
*/
function releaseFromTheEventLoop(handle: ReturnType<typeof setTimeout>): void {
const maybe: { unref?: unknown } = handle as unknown as { unref?: unknown };
if (typeof maybe.unref === "function") (maybe.unref as () => void).call(handle);
}
/**
* Where a deferred drain's failure goes.
*
* It cannot throw: nothing awaits it — the application never asked for this work, and an
* unhandled rejection would take down whatever runtime it is in over a drain it did not
* request. 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. So it goes to the package's own log stream — the same
* `console.error` with the same `[<identity>][polyfill]` prefix that `startConnect` and
* `inbox.watch` use for their un-awaitable failures, and it is NOT gated by the access-log
* flag: a diagnostic may be opt-in, a failure may not.
*/
function reportFailure(inbox: Nuri, error: unknown): void {
console.error(
accessLogPrefix() + " deferred inbox processing failed for " + shortNuri(inbox) + ":",
error,
);
}
/** Run the drain of `inbox`, behind any run of the same inbox still in flight. */
function fire(inbox: Nuri, process: () => Promise<void>): void {
scheduled.delete(inbox);
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);
}
/**
* Arm the emulated service for `inbox` — called by a deposit that LANDED, and by nothing
* else. A write that failed has nothing to converge.
*
* `process` is what to do when the window closes; the scheduler itself knows nothing about
* inboxes' contents, so the drain stays where the inbox's own vocabulary lives.
*
* Idempotent inside the window: a second deposit into the same inbox joins the pending run.
*/
// @provenance scheduleInboxProcessing kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — upstream the RECIPIENT's own verifier applies its inbox; here another identity's session drains it on a timer. Deliberate, unpublished, and only possible on one shared wallet
export function scheduleInboxProcessing(inbox: Nuri, process: () => Promise<void>): void {
if (scheduled.has(inbox)) return;
const handle = setTimeout(() => fire(inbox, process), PROCESSING_DELAY_MS);
releaseFromTheEventLoop(handle);
scheduled.set(inbox, { handle, process });
}
/**
* Close every open window NOW and wait for the drains to finish.
*
* **The emulation's clock, not a door.** It cannot name an inbox, so it can only bring
* forward work a deposit has already armed — the same runs, at the same holder, with the
* same effects, minus the twenty seconds. That is what makes it usable by a suite that has
* to observe convergence without waiting for it, and useless as a way to reach anyone's
* inbox. Never exported from the package.
*
* Resolves rather than rejects, like the timer path it stands in for: a drain's failure is
* reported where failures go, and a caller of this is not the party that asked for the work.
*/
// @provenance runScheduledInboxProcessingNow kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — the same drain, run at once; a test seam over the divergence above
export async function runScheduledInboxProcessingNow(): Promise<void> {
for (const [inbox, entry] of [...scheduled]) {
clearTimeout(entry.handle);
fire(inbox, entry.process);
}
await Promise.all([...running.values()]);
}
/**
* Drop every pending drain without running it — what an un-configured library must do.
*
* Called by `resetConfig`: once the injected `ng` is gone, a drain cannot read or write
* anything, so leaving armed timers behind would produce a burst of failures about a
* session that no longer exists. In-flight runs are not cancelled (nothing can un-issue a
* write already sent); they end where they end.
*/
// @provenance cancelScheduledInboxProcessing kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — cancels a pending drain — upstream there is no pending anything to cancel
export function cancelScheduledInboxProcessing(): void {
for (const entry of scheduled.values()) clearTimeout(entry.handle);
scheduled.clear();
}
/** Trace one completed drain — diagnostics, so gated by the access-log flag. */
// @provenance traceProcessed kind=invention level=none ref=none — a diagnostic line in this package's own log stream
export function traceProcessed(inbox: Nuri, holder: string, applied: number): void {
logStage(
"DEFERRED PROCESS " + shortNuri(inbox) + " for " + holder + " → " + applied + " link(s) applied",
);
}