feat!: le paquet crée un index et lui ajoute une référence, rien de plus
This commit is contained in:
+35
-179
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user