Files
ng-eventually/packages/polyfill/e2e/orm-live-entry.ts
T
2026-09-21 11:41:31 +02:00

265 lines
10 KiB
TypeScript

/**
* The page for the LIVE ORM measurement - what `e2e/run.ts` still does not measure.
*
* The 42-check harness injects the REAL `RdfOrmSubscription.getOrCreate` since
* `1.0.0-dev.6` (that is what `useShapeQuery` reads through), so a live ORM read is no
* longer unreachable from there. What stays here is the PUBLISHED `useShape` half: the
* bare `DeepSignalSet` an application gets from the ORM hook, and a `.delete()` on it
* reaching the document. That is a different surface from the snapshot `useShapeQuery`
* publishes, and it has no other measurement.
*
* ── What the framework adapter contributes, and why it is not here ─────────
* Upstream's `useShape` is React's (`sdk/js/orm/src/frontendAdapters/react/useShape.ts`):
* it calls `OrmSubscription.getOrCreate(shape, scope)`, hands `signalObject` to
* `useDeepSignal`, and closes the subscription on unmount. The broker sees the first of
* those three and nothing else — `useDeepSignal` is re-render bookkeeping. So the hook
* injected below IS `getOrCreate(...).signalObject`, which is what a React render reaches
* the broker with, without a React tree to mount.
*/
import { ng as realNg, init as realInit } from "@ng-org/web";
import { initNg as ormInitNg, RdfOrmSubscription, type Scope as OrmScope } from "@ng-org/orm";
import { configure, docs, init, useShape as libUseShape } from "@ng-eventually/polyfill";
import type { BaseType, Nuri, ShapeType } from "@ng-eventually/polyfill";
import { adoptCurrentUser, configureStoreRegistry } from "../src/shared-wallet/bootstrap";
// ── the shape under measurement ─────────────────────────────────────────────
/**
* The smallest shape that can be matched at all, written by hand rather than generated.
*
* `rdf-orm build` turns a `.shex` file into exactly this object — `{ schema, shape }`
* (`sdk/js/shex-orm/src/types.ts:ShapeType`) — and the generator is not what is under
* measurement here. Two predicates, both `minCardinality: 1`, because that is what
* `engine/verifier/src/orm/graph/query.rs:schema_shape_to_sparql` turns into required
* triple patterns: a shape with none of them would match every subject in the document.
*/
const LIVE_SHAPE = {
shape: "did:ng:z:E2ELiveShape",
schema: {
"did:ng:z:E2ELiveShape": {
iri: "did:ng:z:E2ELiveShape",
predicates: [
{
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
dataTypes: [{ valType: "iri", literals: ["did:ng:z:E2ELive"] }],
minCardinality: 1,
maxCardinality: 1,
},
{
iri: "did:ng:z:liveMark",
readablePredicate: "liveMark",
dataTypes: [{ valType: "string" }],
minCardinality: 1,
maxCardinality: 1,
},
],
},
},
} satisfies ShapeType<BaseType>;
const TYPE_IRI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
// ── the broker session ──────────────────────────────────────────────────────
interface BrokerSession {
session_id: string;
private_store_id: string;
protected_store_id: string;
public_store_id: string;
[k: string]: unknown;
}
let session: BrokerSession | null = null;
let sessionResolve!: (s: BrokerSession) => void;
const sessionReady = new Promise<BrokerSession>((r) => (sessionResolve = r));
/**
* The injected hook — the real ORM's broker-facing half.
*
* It keeps the subscription aside so the probe can await `readyPromise`, which is the ORM's
* own answer to "did the initial payload arrive": it resolves on that path and on no other
* (`sdk/js/orm/src/connector/GraphOrmSubscription.ts:handleInitialResponse`), so a start
* the broker refused leaves it pending for ever — the silence this measurement is here to
* break.
*/
/**
* The handle this page keeps, DERIVED from `getOrCreate` rather than written by hand.
*
* `RdfOrmSubscription` (renamed from `OrmSubscription` in `@ng-org/orm@0.1.2-alpha.21`)
* now takes its config as a type parameter and `getOrCreate` returns a narrowed
* `RdfOrmSubscriptionFor<...>` - the paging methods are `Omit`ted when no `pageSize` is
* asked for. Naming the class directly would restate that narrowing and get it wrong the
* next time it moves; `ReturnType` asks the package what it actually hands back.
*/
type LiveSubscription = ReturnType<
typeof RdfOrmSubscription.getOrCreate<ShapeType<BaseType>, BaseType, OrmScope>
>;
let liveSubscription: LiveSubscription | null = null;
function realOrmUseShape(shapeType: unknown, scope: unknown): unknown {
const sub = RdfOrmSubscription.getOrCreate(shapeType as ShapeType<BaseType>, scope as OrmScope);
liveSubscription = sub;
return sub.signalObject;
}
configure({
ng: realNg,
useShape: realOrmUseShape,
init: realInit,
sharedWallet: { fileUrl: "/harness-not-used.ngw", password: "harness" },
});
adoptCurrentUser("e2e-orm-live");
configureStoreRegistry({
getSession: async () => {
const s = session ?? (await sessionReady);
return {
sessionId: s.session_id,
privateStoreId: s.private_store_id,
protectedStoreId: s.protected_store_id,
publicStoreId: s.public_store_id,
};
},
normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(),
pointerGuard: { attempts: 8, baseMs: 150, maxStepMs: 2000 },
});
// ── the probe ───────────────────────────────────────────────────────────────
const state: { status: string; error?: string } = { status: "connecting" };
interface LiveProbe {
doc: Nuri;
subject: string;
/** What the published `useShape` returned — a Set-like of the matched objects. */
view: Set<BaseType>;
startError: string | null;
readyMs: number | null;
deleteError: string | null;
}
let probe: LiveProbe | null = null;
/** Count the triples the document still holds for the seeded subject. */
async function seededTripleCount(doc: Nuri, subject: string): Promise<number> {
const s = await sessionReady;
const res = await docs.sparqlQuery(
s.session_id,
`SELECT ?p ?o WHERE { <${subject}> ?p ?o }`,
undefined,
doc,
);
const rows = Array.isArray(res)
? res
: ((res as { results?: { bindings?: unknown[] } })?.results?.bindings ?? []);
return rows.length;
}
(window as unknown as { __sdk: Record<string, unknown> }).__sdk = {
status: () => state.status,
error: () => state.error ?? null,
/**
* Seed a document with one shape-matching object, then open the live read through the
* PUBLISHED `useShape`.
*
* The subject IRI follows the ORM's own generator
* (`GraphOrmSubscription.ts:signalObjectPropGenerator`): the document NURI's first 53
* characters, then `:q:`, then a name. Nothing requires that form of a hand-written
* SPARQL insert, and using it removes one way for this measurement to differ from what
* the ORM itself would have written.
*/
async liveStart() {
const s = await sessionReady;
const doc = (await docs.docCreate(
s.session_id,
"Graph",
"data:graph",
"store",
undefined,
)) as Nuri;
const subject = `${doc.substring(0, 53)}:q:e2eLiveRead`;
await docs.sparqlUpdate(
s.session_id,
`INSERT DATA { <${subject}> <${TYPE_IRI}> <did:ng:z:E2ELive> ; <did:ng:z:liveMark> "measured" }`,
doc,
);
const seeded = await seededTripleCount(doc, subject);
// The ORM reaches the SDK through its own module-level promise, resolved once.
ormInitNg(realNg, s as unknown as Parameters<typeof ormInitNg>[1]);
let startError: string | null = null;
let view: Set<BaseType>;
const t0 = Date.now();
try {
view = libUseShape(LIVE_SHAPE, { graphs: [doc] }) as Set<BaseType>;
} catch (e) {
startError = e instanceof Error ? e.message : String(e);
probe = { doc, subject, view: new Set(), startError, readyMs: null, deleteError: null };
return { doc, subject, seeded, startError };
}
probe = { doc, subject, view, startError: null, readyMs: null, deleteError: null };
// Un-awaited on purpose: a refused start never resolves this, and the runner's own
// bound is what turns that silence into a verdict.
void liveSubscription?.readyPromise.then(() => {
if (probe) probe.readyMs = Date.now() - t0;
});
return { doc, subject, seeded, startError: null };
},
/** How many objects the published `useShape` view holds right now. */
liveCount: () => (probe ? probe.view.size : -1),
/** Milliseconds from the `useShape` call to `readyPromise`, or null while it is pending. */
liveReadyMs: () => probe?.readyMs ?? null,
liveItems: () =>
probe
? [...probe.view].map((i) => ({
id: i["@id"] as string,
graph: i["@graph"] as string,
mark: i.liveMark as string,
}))
: [],
/**
* Delete the seeded object through the set the hook returned — the gesture the consuming
* application reports as the shape for removal.
*/
liveDelete() {
if (!probe) return { deleted: false, error: "no probe" };
const item = [...probe.view][0];
if (item === undefined) return { deleted: false, error: "view is empty" };
try {
const deleted = probe.view.delete(item);
return { deleted, error: null };
} catch (e) {
const error = e instanceof Error ? e.message : String(e);
probe.deleteError = error;
return { deleted: false, error };
}
},
/** The document's own answer: how many triples the seeded subject still has. */
async liveSeededTriples() {
if (!probe) return -1;
return seededTripleCount(probe.doc, probe.subject);
},
};
(async () => {
try {
await init(
(event: { session: BrokerSession }) => {
session = event.session;
state.status = "connected";
sessionResolve(session);
},
true,
[],
);
} catch (e) {
state.status = "error";
state.error = e instanceof Error ? e.message : String(e);
}
})();