feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox

This commit is contained in:
Sylvain Duchesne
2026-08-20 11:04:31 +02:00
parent 2ce2113157
commit e2ed970cbd
21 changed files with 956 additions and 273 deletions
+77 -2
View File
@@ -40,7 +40,11 @@ import type { Nuri, UnionSubject } from "../src/port";
* - a document with no inbox READS as `[]` — a state, not an error
* (`depositsForDocument`);
* - opening an inbox is refused to anyone but the document's owner
* (`openDocumentInbox`);
* (`openDocumentInbox`), and so is watching one, since being told what landed in an
* inbox is reading it;
* - watching lasts exactly as long as the identity stays connected: signing in as
* somebody else stops every watch the previous identity had opened
* (`contract_polyfill-surface`, "Guarantees");
* - `readUnion` swallows a failing document into `[]` (`readDoc`'s
* `try {…} catch { return [] }`), and may also reject outright;
* - `readUnion` builds each subject's props as a plain object literal filled by
@@ -271,6 +275,13 @@ export interface FakePolyfill {
/** `readUnion` REJECTS outright — its session-level failure, not a per-document one. */
breakReadUnion(reason: string): void;
healReadUnion(): void;
/**
* Hands over every inbox notification the broker was holding, and waits for the
* watching session to finish with each — the callback is declared `void` upstream,
* so production never waits for it, and this does only because a test needs a point
* at which the work is over.
*/
deliverNotifications(): Promise<void>;
/** Every subject in a document, read from outside the adapter. */
contentsOf(doc: string): { subject: string; predicate: string; values: string[] }[];
}
@@ -284,6 +295,10 @@ export function installFakePolyfill(): FakePolyfill {
const documents = new Map<string, StoredDocument>();
const unreachable = new Map<string, string>();
const calls: RecordedCall[] = [];
/** address → the document whose inbox it is. Nothing else resolves one. */
const inboxAddresses = new Map<string, string>();
let watches: { readonly doc: string; readonly onDeposits: (d: Deposit[]) => unknown }[] = [];
let undelivered: { readonly doc: string; readonly onDeposits: (d: Deposit[]) => unknown }[] = [];
let unionFailure: string | undefined;
let currentUser = "nobody";
let documentCount = 0;
@@ -386,6 +401,32 @@ export function installFakePolyfill(): FakePolyfill {
const from = Object.hasOwn(opts, "from") ? (opts.from ?? null) : currentUser;
clock += 1;
stored.deposits.push({ from, payload: opts.payload ?? null, ts: clock });
// Stored first, told afterwards — and told over the wire, which is why the
// notification waits for `deliverNotifications` rather than firing inline.
for (const watch of watches) if (watch.doc === stored.nuri) undelivered.push(watch);
},
watch(targetInbox: unknown, onDeposits: unknown): () => void {
const doc = inboxAddresses.get(String(targetInbox));
if (doc === undefined) {
// "`inbox.post` refuses a target that is not an inbox" — so does watching one,
// and nothing outside `openDocumentInbox` ever hands an address out.
throw new Error(`[fake-polyfill] not an inbox address: ${String(targetInbox)}`);
}
if (require(doc).owner !== currentUser) {
throw new Error(
`${currentUser} may not watch the inbox of ${doc}: you may DEPOSIT into ` +
"anyone's inbox, you may only READ your own",
);
}
if (typeof onDeposits !== "function") {
throw new Error("[fake-polyfill] inbox.watch takes a callback");
}
const watch = { doc, onDeposits: onDeposits as (d: Deposit[]) => unknown };
watches.push(watch);
return () => {
watches = watches.filter((w) => w !== watch);
};
},
async readForDocument(doc: unknown): Promise<Deposit[]> {
@@ -422,7 +463,23 @@ export function installFakePolyfill(): FakePolyfill {
);
}
stored.deposits ??= [];
return `${stored.nuri}:inbox`;
// Idempotent within a page: asking again for a document that already has one
// resolves that same address rather than adding a second inbox.
const address = `${stored.nuri}:inbox`;
inboxAddresses.set(address, stored.nuri);
return address;
},
async listMyEntityDocs(scope: unknown): Promise<Nuri[]> {
if (scope !== "public" && scope !== "mine") {
throw new Error(`[fake-polyfill] unknown scope ${JSON.stringify(scope)}`);
}
// "listMyEntityDocs returns a listing whose documents you can open, or it throws."
const mine: Nuri[] = [];
for (const stored of documents.values()) {
if (stored.owner === currentUser) mine.push(stored.nuri);
}
return mine;
},
};
@@ -492,8 +549,26 @@ export function installFakePolyfill(): FakePolyfill {
return {
calls,
signIn(user: string) {
// "It lasts exactly as long as that identity stays connected — changing identity
// or clearing it stops it." This fake has ONE signed-in identity at a time, as a
// page does, so a watch cannot outlive the identity that opened it. Signing in as
// the same identity again is not a change, and stops nothing.
if (user !== currentUser) {
watches = [];
undelivered = [];
}
currentUser = user;
},
async deliverNotifications() {
while (undelivered.length > 0) {
const batch = undelivered;
undelivered = [];
for (const watch of batch) {
const stored = documents.get(watch.doc);
await watch.onDeposits([...(stored?.deposits ?? [])]);
}
}
},
sessionId() {
return `session:${currentUser}`;
},