feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox
This commit is contained in:
+77
-2
@@ -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}`;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user