171 lines
6.0 KiB
TypeScript
171 lines
6.0 KiB
TypeScript
import type { NextGraphPort, Nuri, NuriLike } from "../src/port";
|
|
import { asNuri } from "../src/nuri";
|
|
|
|
/**
|
|
* An in-memory NextGraph, standing in for `@ng-eventually/polyfill` behind
|
|
* `NextGraphPort`.
|
|
*
|
|
* Every rule below is one the polyfill actually enforces, annotated with where it
|
|
* comes from — the published contract, or the code implementing it. The point is
|
|
* that no test here can reach a state the real system does not produce:
|
|
*
|
|
* - only a document's owner writes to it, and a read key never grants a write
|
|
* (`contract_polyfill-surface`, "Guarantees");
|
|
* - anyone may DEPOSIT into a document's inbox (`inbox.postToDocument`);
|
|
* - depositing into a document whose owner never opened an inbox THROWS, rather
|
|
* than silently going nowhere;
|
|
* - opening an inbox on a document is refused to anyone but its owner
|
|
* (`openDocumentInbox`: doing so publishes the document's address);
|
|
* - a document nobody created cannot be named: reaching for one REJECTS.
|
|
*
|
|
* ## Reading is done from OUTSIDE the port, on purpose
|
|
*
|
|
* This package neither reads a document nor reads an inbox — what an index receives
|
|
* is made into entries by the layer below. So `contentsOf` and `depositsIn` are
|
|
* inspections of this double, not operations of the port: a test asks what the two
|
|
* acts LEFT BEHIND, and cannot accidentally hand the package back a way to read.
|
|
*/
|
|
|
|
/** One deposit this double is holding, as its inbox holds it. */
|
|
export interface StoredDeposit {
|
|
/** The depositor, as the polyfill defaults it to the current user. */
|
|
readonly from: string;
|
|
readonly payload: unknown;
|
|
readonly ts: number;
|
|
}
|
|
|
|
type Properties = Map<string, string[]>;
|
|
|
|
interface StoredDocument {
|
|
readonly nuri: Nuri;
|
|
readonly owner: string;
|
|
readonly subjects: Map<string, Properties>;
|
|
/** `undefined` until the owner opens one — the state `postToDocument` refuses. */
|
|
deposits: StoredDeposit[] | undefined;
|
|
}
|
|
|
|
export class FakeNextGraph {
|
|
readonly #documents = new Map<string, StoredDocument>();
|
|
#documentCount = 0;
|
|
#clock = 0;
|
|
|
|
/** A handle bound to one identity, exactly as a polyfill session is one user's. */
|
|
portFor(user: string): NextGraphPort {
|
|
const network = this;
|
|
return {
|
|
async createPublicDocument(): Promise<Nuri> {
|
|
return network.#createDocument(user);
|
|
},
|
|
|
|
async addLiteralProperty(
|
|
doc: NuriLike,
|
|
subject: string,
|
|
predicate: string,
|
|
value: string,
|
|
): Promise<void> {
|
|
network.#add(user, asNuri(doc), subject, predicate, value);
|
|
},
|
|
|
|
async openInbox(doc: NuriLike): Promise<void> {
|
|
network.#openInbox(user, asNuri(doc));
|
|
},
|
|
|
|
async depositTo(doc: NuriLike, payload: unknown): Promise<void> {
|
|
network.#deposit(user, asNuri(doc), payload);
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Every triple a document holds, read from outside the port. */
|
|
contentsOf(doc: NuriLike): { subject: string; predicate: string; values: string[] }[] {
|
|
const stored = this.#require(asNuri(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;
|
|
}
|
|
|
|
/**
|
|
* What is waiting in a document's inbox, oldest first — or `null` when its owner
|
|
* never opened one, which is a state and not a failure.
|
|
*/
|
|
depositsIn(doc: NuriLike): readonly StoredDeposit[] | null {
|
|
const stored = this.#require(asNuri(doc));
|
|
if (stored.deposits === undefined) return null;
|
|
return [...stored.deposits].sort((a, b) => a.ts - b.ts);
|
|
}
|
|
|
|
#createDocument(owner: string): Nuri {
|
|
this.#documentCount += 1;
|
|
const nuri = `did:ng:o:doc-${this.#documentCount}` as Nuri;
|
|
this.#documents.set(nuri, { nuri, owner, subjects: new Map(), deposits: undefined });
|
|
return nuri;
|
|
}
|
|
|
|
#require(doc: Nuri): StoredDocument {
|
|
const stored = this.#documents.get(doc);
|
|
if (stored === undefined) throw new Error(`cannot open ${doc}`);
|
|
return stored;
|
|
}
|
|
|
|
#add(user: string, doc: Nuri, subject: string, predicate: string, value: string): void {
|
|
const stored = this.#require(doc);
|
|
if (stored.owner !== user) {
|
|
throw new Error(
|
|
`${user} may not write ${doc}: only a document's owner writes to it, and ` +
|
|
"holding its read key never grants a write",
|
|
);
|
|
}
|
|
let properties = stored.subjects.get(subject);
|
|
if (properties === undefined) {
|
|
properties = new Map();
|
|
stored.subjects.set(subject, properties);
|
|
}
|
|
const values = properties.get(predicate);
|
|
if (values === undefined) {
|
|
properties.set(predicate, [value]);
|
|
return;
|
|
}
|
|
// An INSERT of a triple already present is a no-op in RDF: a graph is a set.
|
|
if (!values.includes(value)) values.push(value);
|
|
}
|
|
|
|
#openInbox(user: string, doc: Nuri): void {
|
|
const stored = this.#require(doc);
|
|
if (stored.owner !== user) {
|
|
throw new Error(
|
|
`${user} may not open an inbox on ${doc}: opening one publishes the ` +
|
|
"document's address, so a non-owner would route the owner's deposits to itself",
|
|
);
|
|
}
|
|
stored.deposits ??= [];
|
|
}
|
|
|
|
#deposit(user: string, doc: Nuri, payload: unknown): void {
|
|
const stored = this.#require(doc);
|
|
if (stored.deposits === undefined) {
|
|
throw new Error(`${doc} has no inbox — its owner never opened one`);
|
|
}
|
|
this.#clock += 1;
|
|
stored.deposits.push({ from: user, payload, ts: this.#clock });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A public document holding one business entity with a value for `field` — what
|
|
* an application creates and then hands to an index.
|
|
*
|
|
* Built through the very same port an application has: nothing here reaches
|
|
* behind the surface to plant a state a real caller could not produce.
|
|
*/
|
|
export async function publishObject(
|
|
port: NextGraphPort,
|
|
field: string,
|
|
value: string,
|
|
): Promise<Nuri> {
|
|
const object = await port.createPublicDocument();
|
|
await port.addLiteralProperty(object, object, field, value);
|
|
return object;
|
|
}
|