import { mock } from "bun:test";
import type { Nuri } from "../src/port";
/**
* `@ng-eventually/polyfill` itself, in memory — NOT a mock returning constants.
*
* `fake-nextgraph.ts` stands in one layer higher, behind `NextGraphPort`, so it
* exercises the indexing rules and leaves `polyfill-adapter.ts` untouched. This
* 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 constants
*
* 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
*
* {@link applyInsertData} is a strict parser for the one statement this package
* writes. It is not a regex looking for `DELETE`: it is an engine that can only
* ADD, so a removal is not "detected", it is **unrunnable**. `DELETE WHERE …`
* fails at the first keyword; `INSERT DATA { … } ; DROP GRAPH <…>` fails on the
* trailing statement; and a keyword hidden inside a literal stays inside the
* literal, because a parser tokenises where a regex only matches. A split literal
* (`"DEL" + "ETE …"`) is one string by the time it arrives, so the split buys
* nothing.
*
* ## 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 (`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`: 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. 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.
*/
// --- the deposit, as the polyfill publishes it ----------------------------
/** Mirrors `inbox.Deposit`: sender when identified, opaque payload, ms timestamp. */
interface Deposit {
readonly from: string | null;
readonly payload: unknown;
readonly ts: number;
}
/** Mirrors `inbox.PostOptions` for the two fields this package can reach. */
interface PostOptions {
readonly payload?: unknown;
readonly from?: string | null;
}
// --- the one statement this fake can execute ------------------------------
/** A triple as this package writes them: two IRIs and one string literal. */
interface Triple {
readonly subject: string;
readonly predicate: string;
readonly value: string;
}
const IRI_BREAKERS = new Set(['"', "<", ">", "\\", "^", "`", "{", "|", "}"]);
/**
* A strict reader for `INSERT DATA {
"v" . … }` and NOTHING else.
*
* Deliberately whole-string: it consumes the query to its end, so a second
* statement smuggled after the closing brace is a parse failure rather than
* something the engine quietly ignores.
*/
class Reader {
#at = 0;
constructor(private readonly query: string) {}
refuse(what: string): never {
throw new Error(
`[fake-polyfill] refused to execute: ${what} at offset ${this.#at}. ` +
"This fake implements ONE statement — an anchored `INSERT DATA` of literal " +
"triples — because that is the only thing this package may ever emit. " +
`Query: ${JSON.stringify(this.query)}`,
);
}
#skipSpace(): void {
while (this.#at < this.query.length && /\s/.test(this.query[this.#at] ?? "")) this.#at += 1;
}
keyword(word: string): void {
this.#skipSpace();
const slice = this.query.slice(this.#at, this.#at + word.length);
const next = this.query[this.#at + word.length] ?? " ";
if (slice.toUpperCase() !== word || /[A-Za-z0-9_]/.test(next)) {
this.refuse(`expected the keyword ${word}, found ${JSON.stringify(slice)}`);
}
this.#at += word.length;
}
symbol(character: string): void {
if (!this.trySymbol(character)) this.refuse(`expected ${JSON.stringify(character)}`);
}
trySymbol(character: string): boolean {
this.#skipSpace();
if (this.query[this.#at] !== character) return false;
this.#at += 1;
return true;
}
/** An IRI between angle brackets — no character that could close it early. */
iri(): string {
this.#skipSpace();
if (this.query[this.#at] === "G" || this.query[this.#at] === "g") {
// Named after the shape it is refusing, because this one is a CONVENTION and
// not a broker limit: `packages/polyfill/e2e/` verified an anchored
// `GRAPH ` write round-trips upstream too. The polyfill writes the
// anchored DEFAULT graph with no wrapper and calls that the canonical,
// always-safe shape (`src/surface/inbox.ts`), so this layer writes it too —
// two layers writing the same data two ways is diagnosis work bought for later.
this.refuse(
"an explicit GRAPH clause — `docs.sparqlUpdate(sid, update, anchor)` already " +
"scopes the write to the anchor's default graph, and the polyfill writes " +
"that shape with no GRAPH wrapper",
);
}
this.symbol("<");
let out = "";
while (true) {
const character = this.query[this.#at];
if (character === undefined) this.refuse("an IRI that is never closed");
this.#at += 1;
if (character === ">") return out;
if (IRI_BREAKERS.has(character) || (character.codePointAt(0) ?? 0) <= 0x20) {
this.refuse(`${JSON.stringify(character)} inside an IRI`);
}
out += character;
}
}
/** A quoted literal, unescaped back to the string the caller passed in. */
literal(): string {
this.#skipSpace();
this.symbol('"');
let out = "";
while (true) {
const character = this.query[this.#at];
if (character === undefined) this.refuse("a literal that is never closed");
this.#at += 1;
if (character === '"') return out;
if (character !== "\\") {
out += character;
continue;
}
const escaped = this.query[this.#at];
this.#at += 1;
if (escaped === undefined) this.refuse("a trailing backslash");
const known: Record = { n: "\n", r: "\r", t: "\t", "\\": "\\", '"': '"' };
const decoded = Object.hasOwn(known, escaped) ? known[escaped] : undefined;
if (decoded === undefined) this.refuse(`the escape \\${escaped}`);
out += decoded;
}
}
end(): void {
this.#skipSpace();
if (this.#at !== this.query.length) this.refuse("a second statement");
}
}
/** Reads the ONLY query form this package emits. Everything else throws. */
export function applyInsertData(query: string): Triple[] {
const reader = new Reader(query);
reader.keyword("INSERT");
reader.keyword("DATA");
reader.symbol("{");
const triples: Triple[] = [];
while (!reader.trySymbol("}")) {
const subject = reader.iri();
const predicate = reader.iri();
const value = reader.literal();
triples.push({ subject, predicate, value });
reader.trySymbol(".");
}
reader.end();
return triples;
}
// --- the lexical tripwire, kept as a SECOND and independent layer ---------
/**
* Blanks every SPARQL string literal, so a keyword INSIDE one reads as what it is:
* inert text. If escaping ever breaks, the injected statement lands OUTSIDE a
* literal and survives this — which is the whole point.
*/
export function blankLiterals(value: string): string {
let out = "";
let inside = false;
for (let i = 0; i < value.length; i += 1) {
const character = value[i];
if (inside && character === "\\") {
i += 1; // an escaped character can never close the literal
continue;
}
if (character === '"') {
inside = !inside;
out += '"';
continue;
}
if (!inside) out += character;
}
return out;
}
/**
* Every SPARQL 1.1 form that can destroy or displace data, plus the modifiers that
* introduce one. For a tripwire a false positive is far cheaper than a miss.
*/
export const DESTRUCTIVE = /\b(DELETE|DROP|CLEAR|MOVE|COPY|ADD|LOAD|MODIFY|WITH|SILENT)\b/i;
// --- what was called ------------------------------------------------------
export interface RecordedCall {
/** `docs.sparqlUpdate`, `inbox.postToDocument`, … */
readonly entry: string;
readonly args: readonly unknown[];
}
interface StoredDocument {
readonly nuri: Nuri;
readonly owner: string;
readonly subjects: Map>;
/** `undefined` until the owner opens one — the state `postToDocument` refuses. */
deposits: Deposit[] | undefined;
}
export interface FakePolyfill {
/** Every call the adapter made into the polyfill, in order. */
readonly calls: readonly RecordedCall[];
/** Acts under this identity from now on — a polyfill session IS one identity. */
signIn(user: string): void;
/** The session id for the signed-in identity, as `init`'s callback hands it over. */
sessionId(): string;
/** 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;
}
/**
* Installs the fake in place of `@ng-eventually/polyfill` and hands back the
* handle on it. Call at the TOP of a test file: the adapter must be imported
* after this runs, so import it with `await import(…)` inside the tests.
*/
export function installFakePolyfill(): FakePolyfill {
const documents = new Map();
const calls: RecordedCall[] = [];
let currentUser = "nobody";
let documentCount = 0;
let clock = 0;
function require(doc: string): StoredDocument {
const stored = documents.get(doc);
if (stored === undefined) throw new Error(`cannot open ${doc}`);
return stored;
}
const docsImpl = {
async sparqlUpdate(sessionId: unknown, query: unknown, anchor?: unknown): Promise {
if (typeof query !== "string") throw new Error("[fake-polyfill] the query must be a string");
const writer = userOfSession(sessionId);
if (writer !== currentUser) {
// A session belongs to ONE identity upstream, so these cannot disagree.
throw new Error(
`[fake-polyfill] session of ${writer} used while ${currentUser} is signed in`,
);
}
if (typeof anchor !== "string") {
throw new Error(
"[fake-polyfill] docs.sparqlUpdate without an anchor: the write would not be " +
"scoped to a document. This package always anchors — see polyfill-adapter.ts.",
);
}
const stored = require(anchor);
if (stored.owner !== writer) {
throw new Error(
`${writer} may not write ${anchor}: only a document's owner writes to it, and ` +
"holding its read key never grants a write",
);
}
for (const triple of applyInsertData(query)) {
let properties = stored.subjects.get(triple.subject);
if (properties === undefined) {
properties = new Map();
stored.subjects.set(triple.subject, properties);
}
const values = properties.get(triple.predicate);
// An INSERT of a triple already present is a no-op in RDF: a graph is a set.
if (values === undefined) properties.set(triple.predicate, [triple.value]);
else if (!values.includes(triple.value)) values.push(triple.value);
}
return [];
},
async sparqlQuery(_sessionId: unknown, query: unknown): Promise {
// Reached only if this package starts querying, which it does not: it reads
// 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(
"[fake-polyfill] a destructive statement was routed through docs.sparqlQuery. " +
"An index only ever grows — there is no door for this. " +
`Query: ${JSON.stringify(query)}`,
);
}
throw new Error(
"[fake-polyfill] docs.sparqlQuery is not modelled — this package does not read. " +
"Model it READ-ONLY here before using it.",
);
},
};
const inboxImpl = {
async postToDocument(doc: unknown, options: unknown): Promise {
const stored = require(String(doc));
if (stored.deposits === undefined) {
throw new Error(
`[ng-eventually] inbox.postToDocument: this document has no inbox — either its ` +
`owner never opened one, or you cannot read the document: ${JSON.stringify(doc)}`,
);
}
const opts = (options ?? {}) as PostOptions;
// "Defaults to the current polyfill user when the property is entirely absent."
const from = Object.hasOwn(opts, "from") ? (opts.from ?? null) : currentUser;
clock += 1;
stored.deposits.push({ from, payload: opts.payload ?? null, ts: clock });
},
};
const storeRegistryImpl = {
async createEntityDoc(scope: unknown): Promise {
if (scope !== "public" && scope !== "mine") {
throw new Error(`[fake-polyfill] unknown scope ${JSON.stringify(scope)}`);
}
documentCount += 1;
const nuri = `did:ng:o:doc-${documentCount}` as Nuri;
documents.set(nuri, { nuri, owner: currentUser, subjects: new Map(), deposits: undefined });
return nuri;
},
async openDocumentInbox(doc: unknown): Promise {
const stored = require(String(doc));
if (stored.owner !== currentUser) {
throw new Error(
`${currentUser} may not open an inbox on ${String(doc)}: opening one publishes ` +
"the document's address, so a non-owner would route the owner's deposits to itself",
);
}
stored.deposits ??= [];
// Idempotent within a page: asking again for a document that already has one
// 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`;
},
};
function userOfSession(sessionId: unknown): string {
const id = String(sessionId);
const user = id.startsWith("session:") ? id.slice("session:".length) : undefined;
if (user === undefined) throw new Error(`[fake-polyfill] not a session id: ${id}`);
return user;
}
function record(entry: string, args: readonly unknown[]): void {
calls.push({ entry, args });
}
/**
* 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, and an
* inherited member is not a modelled entry.
*/
function namespace(name: string, impl: Record unknown>): unknown {
return new Proxy(impl, {
get(target, property) {
if (typeof property === "symbol" || property === "then") return undefined;
const entry = `${name}.${property}`;
return (...args: unknown[]) => {
record(entry, args);
if (!Object.hasOwn(target, property)) {
throw new Error(
`[fake-polyfill] ${entry} is not modelled. The adapter reached for a polyfill ` +
"entry this fake knows nothing about, so nothing here can vouch for what it " +
"does. Model it (read-only if it queries) before using it.",
);
}
return (target[property] as (...a: unknown[]) => unknown)(...args);
};
},
});
}
mock.module("@ng-eventually/polyfill", () => ({
docs: namespace("docs", docsImpl),
inbox: namespace("inbox", inboxImpl),
storeRegistry: namespace("storeRegistry", storeRegistryImpl),
}));
return {
calls,
signIn(user: string) {
// 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;
},
sessionId() {
return `session:${currentUser}`;
},
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);
const out: { subject: string; predicate: string; values: string[] }[] = [];
for (const [subject, properties] of stored.subjects) {
for (const [predicate, values] of properties) out.push({ subject, predicate, values });
}
return out;
},
};
}