feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox
This commit is contained in:
+97
-1
@@ -27,11 +27,29 @@ import { asNuri } from "../src/nuri";
|
||||
* (`openDocumentInbox`: doing so publishes the document's address);
|
||||
* - a document in a public store is readable by whoever knows its NURI;
|
||||
* - a document that cannot be read REJECTS, and a rejection means "unknown",
|
||||
* never "absent".
|
||||
* never "absent";
|
||||
* - being TOLD what landed in an inbox is reading it, so watching one is refused to
|
||||
* anyone but the document's owner, exactly as opening one is.
|
||||
*
|
||||
* ## Telling a watcher crosses the network, so it is a step of its own
|
||||
*
|
||||
* A deposit is stored the moment it is made — that is the durable fact, and it is
|
||||
* what the owner's next connection finds. Notifying a session that is watching goes
|
||||
* over the wire, and this fake holds those notifications until a test calls
|
||||
* {@link FakeNextGraph.deliverNotifications}. A test that never calls it is a test
|
||||
* in which the owner has not been told yet: a real state, and precisely the one the
|
||||
* catch-up at connection exists for.
|
||||
*/
|
||||
|
||||
type Properties = Map<string, string[]>;
|
||||
|
||||
/** One session watching one document's inbox. */
|
||||
interface Watch {
|
||||
readonly doc: Nuri;
|
||||
readonly user: string;
|
||||
readonly onDeposits: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface StoredDocument {
|
||||
readonly nuri: Nuri;
|
||||
readonly owner: string;
|
||||
@@ -44,6 +62,12 @@ export class FakeNextGraph {
|
||||
readonly #documents = new Map<string, StoredDocument>();
|
||||
/** Documents the broker currently cannot answer about. See `breakReadsOf`. */
|
||||
readonly #unreachable = 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`. */
|
||||
#undelivered: Watch[] = [];
|
||||
/** Why a store listing cannot answer, when a test has made it fail. */
|
||||
#listingFailure: string | undefined;
|
||||
#documentCount = 0;
|
||||
#clock = 0;
|
||||
|
||||
@@ -83,6 +107,14 @@ export class FakeNextGraph {
|
||||
async readDeposits(doc: NuriLike): Promise<readonly IncomingDeposit[]> {
|
||||
return network.#readDeposits(user, asNuri(doc));
|
||||
},
|
||||
|
||||
async watchDeposits(doc: NuriLike, onDeposits: () => Promise<void>): Promise<void> {
|
||||
network.#watchDeposits(user, asNuri(doc), onDeposits);
|
||||
},
|
||||
|
||||
async listPublicDocuments(): Promise<readonly Nuri[]> {
|
||||
return network.#listDocuments(user);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,6 +132,40 @@ export class FakeNextGraph {
|
||||
this.#unreachable.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
|
||||
* provoke, so this returns with nothing left in flight.
|
||||
*/
|
||||
async deliverNotifications(): Promise<void> {
|
||||
while (this.#undelivered.length > 0) {
|
||||
const batch = this.#undelivered;
|
||||
this.#undelivered = [];
|
||||
for (const watch of batch) await watch.onDeposits();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This identity's page is gone: every watch its sessions had opened stops, and
|
||||
* anything the broker was about to tell them is dropped. The polyfill's watching
|
||||
* lasts exactly as long as an identity stays connected, and so does this.
|
||||
*
|
||||
* Nothing durable is lost — the deposits are in their inboxes, which is what makes
|
||||
* the catch-up at the next connection enough on its own.
|
||||
*/
|
||||
disconnect(user: string): void {
|
||||
this.#undelivered = this.#undelivered.filter((watch) => watch.user !== user);
|
||||
this.#watches = this.#watches.filter((watch) => watch.user !== user);
|
||||
}
|
||||
|
||||
/**
|
||||
* The store can no longer say which documents an identity has. Upstream throws
|
||||
* rather than answer a listing it could not establish, so this does too.
|
||||
*/
|
||||
breakListing(reason: string): void {
|
||||
this.#listingFailure = reason;
|
||||
}
|
||||
|
||||
/** A NURI shaped like any other, that no document was ever created for. */
|
||||
neverCreatedNuri(): Nuri {
|
||||
return "did:ng:o:doc-never-created" as Nuri;
|
||||
@@ -216,6 +282,36 @@ export class FakeNextGraph {
|
||||
}
|
||||
this.#clock += 1;
|
||||
stored.deposits.push({ from: user, payload, ts: this.#clock });
|
||||
// Stored first, told afterwards: the deposit is a fact even if nobody is ever
|
||||
// told, which is what makes the catch-up at connection sufficient on its own.
|
||||
for (const watch of this.#watches) {
|
||||
if (watch.doc === doc) this.#undelivered.push(watch);
|
||||
}
|
||||
}
|
||||
|
||||
#watchDeposits(user: string, doc: Nuri, onDeposits: () => Promise<void>): void {
|
||||
const stored = this.#require(doc);
|
||||
if (stored.owner !== user) {
|
||||
throw new Error(
|
||||
`${user} may not watch the inbox of ${doc}: being told what landed in an inbox ` +
|
||||
"is reading it, and you may only READ your own",
|
||||
);
|
||||
}
|
||||
// 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 ??= [];
|
||||
this.#watches.push({ doc, user, onDeposits });
|
||||
}
|
||||
|
||||
#listDocuments(user: string): readonly Nuri[] {
|
||||
if (this.#listingFailure !== undefined) {
|
||||
throw new Error(`cannot list the public store: ${this.#listingFailure}`);
|
||||
}
|
||||
const mine: Nuri[] = [];
|
||||
for (const stored of this.#documents.values()) {
|
||||
if (stored.owner === user) mine.push(stored.nuri);
|
||||
}
|
||||
return mine;
|
||||
}
|
||||
|
||||
#readDeposits(user: string, doc: Nuri): readonly IncomingDeposit[] {
|
||||
|
||||
Reference in New Issue
Block a user