feat!: le paquet crée un index et lui ajoute une référence, rien de plus

This commit is contained in:
Sylvain Duchesne
2026-08-21 19:56:11 +02:00
parent c2f9ff4674
commit ff78a70f14
26 changed files with 735 additions and 2651 deletions
+35 -179
View File
@@ -1,5 +1,5 @@
import { mock } from "bun:test";
import type { Nuri, UnionSubject } from "../src/port";
import type { Nuri } from "../src/port";
/**
* `@ng-eventually/polyfill` itself, in memory — NOT a mock returning constants.
@@ -9,15 +9,12 @@ import type { Nuri, UnionSubject } from "../src/port";
* one stands BELOW the adapter, so the real adapter runs on top of it and every
* behavioural test becomes a test of the wiring too.
*
* ## Why a real implementation and not a recorder returning `[]`
* ## Why a real implementation and not a recorder returning constants
*
* The previous gate mocked `readUnion` to a constant `[]`. That single constant
* was an escape hatch: a destructive statement guarded by
* `if ((await readUnion([graph])).length > 0)` never ran, so it was never
* recorded, and the gate passed. A mock that always answers the same thing tests
* one state of the world. This answers what was actually written, so a curation
* run that adds an entry to a document already holding a descriptor DOES take the
* non-empty branch.
* A mock that always answers the same thing tests one state of the world, and a
* statement guarded by what a read returned would never run under it. This one
* answers what was actually written, so `contentsOf` and `depositsIn` report what
* the adapter really left behind rather than what a test arranged.
*
* ## Why the SPARQL is EXECUTED and not pattern-matched
*
@@ -33,29 +30,20 @@ import type { Nuri, UnionSubject } from "../src/port";
* ## Every rule below is one the polyfill actually enforces
*
* - only a document's owner writes to it (`docs.sparqlUpdate`'s `assertMayWrite`);
* - anyone may DEPOSIT into a document's inbox, but reading one throws for anyone
* but its owner (`inbox.read`'s `assertOwnInbox`);
* - depositing into a document whose owner never opened an inbox THROWS
* (`inbox.postToDocument`), rather than going nowhere;
* - a document with no inbox READS as `[]` — a state, not an error
* (`depositsForDocument`);
* - anyone may DEPOSIT into a document's inbox (`inbox.postToDocument`);
* - depositing into a document whose owner never opened an inbox THROWS, rather
* than going nowhere;
* - opening an inbox is refused to anyone but the document's owner
* (`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
* `(props[p] ??= []).push(o)` — inherited members and all.
* (`openDocumentInbox`: doing so publishes the document's address);
* - a document with no inbox has no address to read — a state, not an error.
*
* ## What is NOT modelled, and what happens then
*
* Anything the adapter reaches for on `docs`, `inbox` or `storeRegistry` that is
* absent here THROWS by name (see {@link namespace}) instead of returning
* `undefined` — so routing a query through `docs.sparqlQuery` is a red test with a
* message that says so. A brand-new TOP-LEVEL import is the one case that degrades:
* message that says so. This package reads nothing at all, so `readUnion` is not
* modelled either: reaching for it is the same red. A brand-new TOP-LEVEL import is the one case that degrades:
* Bun's `mock.module` materialises the module namespace from own keys, so a Proxy
* there is lost and an unmodelled top-level export arrives as `undefined`. Still
* red (`undefined is not a function`), just with a duller message.
@@ -265,25 +253,14 @@ export interface FakePolyfill {
signIn(user: string): void;
/** The session id for the signed-in identity, as `init`'s callback hands it over. */
sessionId(): string;
/**
* The broker can no longer answer about this document. `readDoc` catches and
* yields `[]`, so it arrives indistinguishable from an empty document — which is
* exactly the confusion `resolution.ts` exists to refuse.
*/
breakReadsOf(doc: string, reason: string): void;
healReadsOf(doc: string): void;
/** `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. */
/** Every triple in a document, read from outside the adapter. */
contentsOf(doc: string): { subject: string; predicate: string; values: string[] }[];
/**
* What is waiting in a document's inbox, oldest first — or `null` when its owner
* never opened one. An INSPECTION, not an entry of the polyfill: this package
* never reads an inbox, so nothing it does may depend on being able to.
*/
depositsIn(doc: string): readonly Deposit[] | null;
}
/**
@@ -293,13 +270,7 @@ export interface FakePolyfill {
*/
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;
let clock = 0;
@@ -310,28 +281,6 @@ export function installFakePolyfill(): FakePolyfill {
return stored;
}
function readOne(doc: string): UnionSubject[] {
const broken = unreachable.get(doc);
if (broken !== undefined) throw new Error(`cannot reach ${doc}: ${broken}`);
const stored = require(doc);
const out: UnionSubject[] = [];
for (const [subject, properties] of stored.subjects) {
// Built EXACTLY as `readUnion` builds it — a plain object literal filled by
// `(props[p] ??= []).push(o)`. Neither detail is cosmetic: the literal
// inherits from `Object.prototype`, and `??=` does NOT assign over an
// inherited truthy member, so a predicate named `constructor` or `toString`
// leaves `.push` undefined and the read THROWS.
const props: Record<string, string[]> = {};
for (const [predicate, values] of properties) {
for (const value of values) {
(props[predicate] ??= []).push(value);
}
}
out.push({ subject, graph: doc as Nuri, props });
}
return out;
}
const docsImpl = {
async sparqlUpdate(sessionId: unknown, query: unknown, anchor?: unknown): Promise<unknown> {
if (typeof query !== "string") throw new Error("[fake-polyfill] the query must be a string");
@@ -371,7 +320,7 @@ export function installFakePolyfill(): FakePolyfill {
async sparqlQuery(_sessionId: unknown, query: unknown): Promise<never> {
// Reached only if this package starts querying, which it does not: it reads
// through `readUnion`. The message splits the two reasons someone lands here,
// nothing at all. The message splits the two reasons someone lands here,
// because one of them is an attempt to write through the read door.
if (typeof query === "string" && DESTRUCTIVE.test(blankLiterals(query))) {
throw new Error(
@@ -381,8 +330,8 @@ export function installFakePolyfill(): FakePolyfill {
);
}
throw new Error(
"[fake-polyfill] docs.sparqlQuery is not modelled — this package reads through " +
"`readUnion`. Model it READ-ONLY here before using it.",
"[fake-polyfill] docs.sparqlQuery is not modelled — this package does not read. " +
"Model it READ-ONLY here before using it.",
);
},
};
@@ -401,46 +350,8 @@ 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[]> {
const stored = require(String(doc));
// No inbox → there is no address to read, which is a state and not an error.
if (stored.deposits === undefined) return [];
if (stored.owner !== currentUser) {
throw new Error(
`${currentUser} may not read the inbox of ${String(doc)}: you may DEPOSIT into ` +
"anyone's inbox, you may only READ your own",
);
}
return [...stored.deposits].sort((a, b) => a.ts - b.ts);
},
};
const storeRegistryImpl = {
@@ -464,41 +375,13 @@ export function installFakePolyfill(): FakePolyfill {
}
stored.deposits ??= [];
// 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;
// resolves that same address rather than opening a second inbox. The address
// goes no further — this package drops it, which is the point of `openInbox`
// returning nothing.
return `${stored.nuri}:inbox`;
},
};
async function readUnionImpl(docsLike: unknown): Promise<UnionSubject[]> {
if (unionFailure !== undefined) throw new Error(unionFailure);
const list = Array.isArray(docsLike) ? docsLike : [];
const out: UnionSubject[] = [];
for (const doc of [...new Set(list.filter(Boolean))]) {
try {
out.push(...readOne(String(doc)));
} catch (error) {
// `readDoc` is `try {…} catch { return [] }`: a failing document is skipped
// and never aborts the batch, so failure and emptiness arrive identical.
console.error("[fake-polyfill] read failed for", doc, String(error));
}
}
return out;
}
function userOfSession(sessionId: unknown): string {
const id = String(sessionId);
const user = id.startsWith("session:") ? id.slice("session:".length) : undefined;
@@ -513,8 +396,8 @@ export function installFakePolyfill(): FakePolyfill {
/**
* A namespace whose every member is recorded, and whose UNKNOWN members throw by
* name rather than arriving as `undefined`. `Object.hasOwn` and not `impl[name]`,
* because a plain object literal inherits `toString` and friends — the same trap
* `valuesOf` guards against in `src/index-document.ts`.
* because a plain object literal inherits `toString` and friends, and an
* inherited member is not a modelled entry.
*/
function namespace(name: string, impl: Record<string, (...args: never[]) => unknown>): unknown {
return new Proxy(impl, {
@@ -540,49 +423,22 @@ export function installFakePolyfill(): FakePolyfill {
docs: namespace("docs", docsImpl),
inbox: namespace("inbox", inboxImpl),
storeRegistry: namespace("storeRegistry", storeRegistryImpl),
async readUnion(docsLike: unknown) {
record("readUnion", [docsLike]);
return readUnionImpl(docsLike);
},
}));
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 = [];
}
// This fake has ONE signed-in identity at a time, as a page does: a polyfill
// session IS one identity, and no call takes an identifier.
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}`;
},
breakReadsOf(doc: string, reason: string) {
unreachable.set(doc, reason);
},
healReadsOf(doc: string) {
unreachable.delete(doc);
},
breakReadUnion(reason: string) {
unionFailure = reason;
},
healReadUnion() {
unionFailure = undefined;
depositsIn(doc: string) {
const stored = require(doc);
if (stored.deposits === undefined) return null;
return [...stored.deposits].sort((a, b) => a.ts - b.ts);
},
contentsOf(doc: string) {
const stored = require(doc);