feat(client): generic shared-wallet shim surface (docs/storeRegistry/isolation/accounts)
Port the shared-wallet shim mechanics from the Festipod app into the
generic @ng-eventually/client library — zero app-specific knowledge, the
consumer injects the domain (entity->scope mapping, connections, storage).
New namespaces exposed from src/index.ts:
- docs docCreate/sparqlUpdate/sparqlQuery via the REAL injected `ng`
(getConfig().ng), never the public makeNg proxy — the JS-over-
iframe double proxy breaks doc_create postMessage marshaling
(DataCloneError). Validated hard constraint.
- storeRegistry generic (account,scope)->NURI resolver, createEntityDoc/
listEntityDocs + per-scope index, sharedWalletShim in the
private_store, cache. Consumer wiring injected via
configureStoreRegistry({ getSession, normalizeUser }).
- isolation pure applyIsolation (public=all / protected=owner+connections
/ private=owner); accessors + connection graph injected.
- accounts AccountStore (localStorage-backed faux login, storage injected)
+ normalizeUsername. React wrapper intentionally NOT ported.
polyfill.ts gains configureStoreRegistry/getStoreRegistryDeps + resetConfig.
36/36 bun test, tsc --noEmit rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { test, expect, mock } from "bun:test";
|
||||
import { docCreate, sparqlUpdate, sparqlQuery } from "../src/docs";
|
||||
import * as ngProxy from "../src/ng-proxy";
|
||||
|
||||
// NOTE ORDER: the "not configured → throw" case MUST run before any configure()
|
||||
// call, because configure() sets a module-level singleton with no public reset.
|
||||
|
||||
test("throws a clear error when configure() was not called", async () => {
|
||||
await expect(docCreate("sid", "Graph", "data:graph", "store")).rejects.toThrow(
|
||||
/configure\(\) must be called before use/,
|
||||
);
|
||||
await expect(sparqlUpdate("sid", "INSERT DATA {}")).rejects.toThrow(
|
||||
/configure\(\) must be called before use/,
|
||||
);
|
||||
await expect(sparqlQuery("sid", "SELECT * {}")).rejects.toThrow(
|
||||
/configure\(\) must be called before use/,
|
||||
);
|
||||
});
|
||||
|
||||
// From here on, a fake real `ng` is injected via configure().
|
||||
import { configure } from "../src/polyfill";
|
||||
|
||||
function fakeNg() {
|
||||
return {
|
||||
doc_create: mock(async (..._a: unknown[]) => "did:ng:o:new-doc"),
|
||||
sparql_update: mock(async (..._a: unknown[]) => undefined),
|
||||
sparql_query: mock(async (..._a: unknown[]) => ({ results: { bindings: [] } })),
|
||||
// A sentinel: makeNg(), if ever used, would `.bind` and call THIS through
|
||||
// the JS Proxy. We assert the primitives call the raw fns above directly.
|
||||
};
|
||||
}
|
||||
|
||||
function inject() {
|
||||
const ng = fakeNg();
|
||||
configure({ ng: ng as any, useShape: (() => {}) as any });
|
||||
return ng;
|
||||
}
|
||||
|
||||
test("docCreate calls the real injected ng.doc_create with the exact args", async () => {
|
||||
const ng = inject();
|
||||
const nuri = await docCreate("sid-1", "Graph", "data:graph", "store", undefined);
|
||||
expect(nuri).toBe("did:ng:o:new-doc");
|
||||
expect(ng.doc_create).toHaveBeenCalledTimes(1);
|
||||
expect(ng.doc_create.mock.calls[0]).toEqual(["sid-1", "Graph", "data:graph", "store", undefined]);
|
||||
});
|
||||
|
||||
test("sparqlUpdate forwards (sessionId, query, anchor) to the real ng.sparql_update", async () => {
|
||||
const ng = inject();
|
||||
await sparqlUpdate("sid-2", "INSERT DATA { GRAPH <did:ng:o:a> { <s> <p> <o> } }", "did:ng:o:a");
|
||||
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
||||
expect(ng.sparql_update.mock.calls[0]).toEqual([
|
||||
"sid-2",
|
||||
"INSERT DATA { GRAPH <did:ng:o:a> { <s> <p> <o> } }",
|
||||
"did:ng:o:a",
|
||||
]);
|
||||
});
|
||||
|
||||
test("sparqlUpdate passes anchor=undefined when omitted", async () => {
|
||||
const ng = inject();
|
||||
await sparqlUpdate("sid-3", "INSERT DATA {}");
|
||||
expect(ng.sparql_update.mock.calls[0]).toEqual(["sid-3", "INSERT DATA {}", undefined]);
|
||||
});
|
||||
|
||||
test("sparqlQuery forwards (sessionId, query, base, anchor) and returns the raw result", async () => {
|
||||
const ng = inject();
|
||||
const res = await sparqlQuery("sid-4", "SELECT ?e { GRAPH <g> { ?s ?p ?e } }", undefined, "did:ng:o:g");
|
||||
expect(res).toEqual({ results: { bindings: [] } });
|
||||
expect(ng.sparql_query).toHaveBeenCalledTimes(1);
|
||||
expect(ng.sparql_query.mock.calls[0]).toEqual([
|
||||
"sid-4",
|
||||
"SELECT ?e { GRAPH <g> { ?s ?p ?e } }",
|
||||
undefined,
|
||||
"did:ng:o:g",
|
||||
]);
|
||||
});
|
||||
|
||||
test("the primitives do NOT route through the public ng proxy (makeNg)", async () => {
|
||||
// makeNg builds a JS Proxy over the injected ng. If a primitive went through
|
||||
// it, calls would land on the proxy's `get` trap, not on our raw mock fns.
|
||||
// Spy on makeNg: it must never be invoked by the docs primitives.
|
||||
const spy = mock(ngProxy.makeNg);
|
||||
const ng = inject();
|
||||
await docCreate("sid", "Graph", "data:graph", "store");
|
||||
await sparqlUpdate("sid", "INSERT DATA {}");
|
||||
await sparqlQuery("sid", "SELECT * {}");
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
// And the raw injected fns were reached directly:
|
||||
expect(ng.doc_create).toHaveBeenCalledTimes(1);
|
||||
expect(ng.sparql_update).toHaveBeenCalledTimes(1);
|
||||
expect(ng.sparql_query).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
Reference in New Issue
Block a user