docs: dire l'effet et non le canal, et n'exiger le NURI en dur que d'un index global

This commit is contained in:
Sylvain Duchesne
2026-08-20 14:47:55 +02:00
parent e2ed970cbd
commit ebaae15baf
8 changed files with 354 additions and 56 deletions
+51
View File
@@ -62,6 +62,10 @@ export class FakeNextGraph {
readonly #documents = new Map<string, StoredDocument>();
/** Documents the broker currently cannot answer about. See `breakReadsOf`. */
readonly #unreachable = new Map<string, string>();
/** Inboxes the broker currently cannot READ. See `breakInboxReadsOf`. */
readonly #inboxUnreadable = new Map<string, string>();
/** Inboxes the broker currently refuses to WATCH. See `breakWatchingOf`. */
readonly #inboxUnwatchable = new Map<string, string>();
/** Every live watch, across every identity — a session watching its own inbox. */
#watches: Watch[] = [];
/** Notifications the broker has not handed over yet. See `deliverNotifications`. */
@@ -132,6 +136,43 @@ export class FakeNextGraph {
this.#unreachable.delete(asNuri(doc));
}
/**
* The broker serves the DOCUMENT but not its INBOX.
*
* Not a contrivance: upstream an inbox is a repo of its own, reached through an
* address `openDocumentInbox` resolves and read with that repo's capability,
* while the document itself is read by `readUnion`. Two repos, two reads — so
* one answering while the other does not is what a partial failure looks like,
* and it is the state that makes a catch-up fail on one index and no other.
*/
breakInboxReadsOf(doc: NuriLike, reason: string): void {
this.#inboxUnreadable.set(asNuri(doc), reason);
}
/** The inbox can be read again. */
healInboxReadsOf(doc: NuriLike): void {
this.#inboxUnreadable.delete(asNuri(doc));
}
/**
* The broker refuses to keep this session posted about that inbox, while
* everything else about it still works.
*
* Watching is a live subscription, set up and held open for as long as the
* session lasts; reading an inbox is one question and one answer. A subscription
* can be refused where a read succeeds, which is the state that leaves an index
* caught up but unwatched — deposits into it going unnoticed until the next
* connection, exactly as the failure this models says.
*/
breakWatchingOf(doc: NuriLike, reason: string): void {
this.#inboxUnwatchable.set(asNuri(doc), reason);
}
/** The inbox can be watched again. */
healWatchingOf(doc: NuriLike): void {
this.#inboxUnwatchable.delete(asNuri(doc));
}
/**
* Hands over every inbox notification the broker was holding, and waits for the
* sessions watching to finish with them — including notifications those very runs
@@ -297,6 +338,12 @@ export class FakeNextGraph {
"is reading it, and you may only READ your own",
);
}
const unwatchable = this.#inboxUnwatchable.get(doc);
// Refused AFTER the owner check: resolving the address is an owner-only act, so
// a stranger is turned away before any subscription is ever attempted.
if (unwatchable !== undefined) {
throw new Error(`cannot watch the inbox of ${doc}: ${unwatchable}`);
}
// Watching resolves the inbox address, and the call that resolves one opens it
// when there is none — the same idempotent call `openInbox` makes.
stored.deposits ??= [];
@@ -324,6 +371,10 @@ export class FakeNextGraph {
"inbox, you may only READ your own",
);
}
const unreadable = this.#inboxUnreadable.get(doc);
if (unreadable !== undefined) {
throw new Error(`cannot read the inbox of ${doc}: ${unreadable}`);
}
return [...stored.deposits].sort((a, b) => a.ts - b.ts);
}
}