feat!: useShapeQuery lit par la requete de forme du SDK
This commit is contained in:
@@ -1,20 +1,12 @@
|
||||
/**
|
||||
* The page for the LIVE ORM measurement — the one thing `e2e/polyfill-entry.ts` cannot do.
|
||||
* The page for the LIVE ORM measurement - what `e2e/run.ts` still does not measure.
|
||||
*
|
||||
* That harness injects a hand-written Set-like into `configure({ useShape })`, because the
|
||||
* read filter is pure and does not need the real ORM to be exercised. What it therefore
|
||||
* never touches is the only hop that reaches the broker: `ng.orm_start_graph`, the streamed
|
||||
* method `OrmSubscription` opens and the one `bug_useshape-live-read-needs-a-newer-broker`
|
||||
* is about. So this page injects the REAL `@ng-org/orm` — nothing else about the bootstrap
|
||||
* differs — and the published `useShape` runs over a live subscription.
|
||||
*
|
||||
* ── The dependency, stated because it is not installed by default ──────────
|
||||
* `@ng-org/orm` is an OPTIONAL PEER of this package (`package.json`,
|
||||
* `peerDependenciesMeta`), so `bun install` does not fetch it and `bun.lock` records it
|
||||
* under `optionalPeers` with no resolution. This page is the only thing in the repository
|
||||
* that imports it: `e2e/run.ts` and its harness do not, so a checkout without it still
|
||||
* builds and runs the 42-check suite. Here the bundle simply will not build, and the
|
||||
* runner says so rather than reporting a broker verdict it did 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`):
|
||||
@@ -247,51 +239,6 @@ async function seededTripleCount(doc: Nuri, subject: string): Promise<number> {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* CHARACTERISATION of the deployed broker's `orm_start_graph` ARITY.
|
||||
*
|
||||
* The failure the runner reports named a parameter - `config` - that the client of
|
||||
* the day did not send. Two upstream arrangements could put one there, and they differ
|
||||
* by POSITION:
|
||||
* - config LAST, after the callback (upstream 2026-04-12 to 2026-04-21) — an
|
||||
* unsupplied `config`, never a Function;
|
||||
* - config at index 4, BEFORE the callback: the 5-argument call of the client of
|
||||
* the day then lands its callback exactly there, which is what the error described.
|
||||
*
|
||||
* THE UPDATE ANSWERED THAT QUESTION. `@ng-org/web@0.1.2-alpha.14` declares six
|
||||
* arguments with `config` at index 4 and routes the stream callback from index 5
|
||||
* (`dist/ngweb.js`: `orm_start_graph: 5`), and `@ng-org/orm@0.1.2-alpha.21` sends six
|
||||
* to match (`dist/connector/RdfOrmSubscription.js:159`). So the second arrangement was
|
||||
* the right reading, and the ordinary `liveStart` path now exercises it.
|
||||
*
|
||||
* This is therefore kept as a FALLBACK, not as the live question: the runner only asks
|
||||
* it when `liveStart` did not become ready, to say whether the engine still refuses on
|
||||
* `config` when the client no longer can be blamed. It calls the SDK member directly
|
||||
* rather than through the ORM. The app-side proxy takes index 5 as the stream callback,
|
||||
* so the `{}` at index 4 is read as a config and nothing usable comes back; what is
|
||||
* being read is only whether the start is refused.
|
||||
*/
|
||||
async liveStartWithConfig() {
|
||||
const s = await sessionReady;
|
||||
const doc = (await docs.docCreate(
|
||||
s.session_id,
|
||||
"Graph",
|
||||
"data:graph",
|
||||
"store",
|
||||
undefined,
|
||||
)) as Nuri;
|
||||
// Called against the PUBLISHED declaration, which now has these six arguments, so the
|
||||
// widening this used to need is gone: the arity is checked here rather than asserted.
|
||||
// What remains unknowable at compile time is the DEPLOYED engine's arity, which is
|
||||
// the question this asks.
|
||||
try {
|
||||
await realNg.orm_start_graph([doc], [], LIVE_SHAPE, s.session_id, {}, () => {});
|
||||
return { doc, error: null };
|
||||
} catch (e) {
|
||||
return { doc, error: e instanceof Error ? e.message : String(e) };
|
||||
}
|
||||
},
|
||||
|
||||
/** The document's own answer: how many triples the seeded subject still has. */
|
||||
async liveSeededTriples() {
|
||||
if (!probe) return -1;
|
||||
|
||||
@@ -2,43 +2,33 @@
|
||||
* Does a live `useShape` read reach the DEPLOYED broker, and does deleting from the set it
|
||||
* returns reach the document?
|
||||
*
|
||||
* Standalone, and deliberately outside `e2e/run.ts`: this is the one measurement that needs
|
||||
* `@ng-org/orm`, an OPTIONAL PEER this package does not install (`bun.lock`,
|
||||
* `optionalPeers`). Folding it into the 42-check suite would make that suite unbuildable on
|
||||
* a checkout without it. Here, a missing ORM fails the build of this runner alone, with the
|
||||
* reason said out loud — which is the honest outcome, because a measurement that did not
|
||||
* run must not be reported as a verdict either way.
|
||||
* Standalone, and deliberately outside `e2e/run.ts`: what it measures is the PUBLISHED
|
||||
* `useShape` - the bare `DeepSignalSet` the ORM hook hands an application, and a
|
||||
* `.delete()` on it reaching the document. `e2e/run.ts` exercises the other shape read,
|
||||
* `useShapeQuery`, which since `1.0.0-dev.6` reads through the SDK's shape query too. Two
|
||||
* surfaces, two runs.
|
||||
*
|
||||
* Run: `bun run e2e/orm-live-read.ts` from `packages/polyfill`.
|
||||
*
|
||||
* ── What it decides ────────────────────────────────────────────────────────
|
||||
* -- What it decides ----------------------------------------
|
||||
* The verdict is readable in one number: does `readyPromise` settle. It does on one path
|
||||
* only — the arrival of an initial payload from the engine
|
||||
* (`GraphOrmSubscription.ts:handleInitialResponse`) — so a refused start leaves it pending
|
||||
* for ever, which is exactly why the failure is silent and why a bound, not an await, is
|
||||
* what turns it into a report. Nothing here infers a verdict from a version string.
|
||||
*
|
||||
* ── What it found, 2026-09-16, and why the last row exists ─────────────────
|
||||
* The read does NOT start, and the reason is not the one
|
||||
* `bug_useshape-live-read-needs-a-newer-broker` names. That leaf's mechanism — a broker
|
||||
* built before `orm_start` was renamed `orm_start_graph` — is REFUTED here: the method
|
||||
* resolves, and the call fails INSIDE it, on an argument. The engine answers
|
||||
* `Deserialization error of config Error: invalid type: JsValue(Function), expected any
|
||||
* valid JSON value`.
|
||||
* -- What it found, and the row that went with the answer ----------------
|
||||
* On 2026-09-16 the read did not start: the engine answered `Deserialization error of
|
||||
* config`, and a sixth row of this run established WHERE that `config` sits - position 4,
|
||||
* before the callback, where the five-argument client of the day landed its callback. The
|
||||
* deployment was AHEAD of the pinned client packages, not behind them.
|
||||
*
|
||||
* The last row establishes where that `config` sits, because the position is the whole
|
||||
* diagnosis: calling the SDK member directly with `{}` in position 4 is ACCEPTED, and the
|
||||
* engine then streams back. So the deployed `orm_start_graph` is
|
||||
* `(graph_scope, subject_scope, shapeType, session_id, config, callback)`, and the
|
||||
* five-argument call the published `@ng-org/orm` makes lands its callback in `config`.
|
||||
* That arity is in no commit of `nextgraph-rs` at the read revision — the one window that
|
||||
* had a `config` at all (2026-04-12 to 2026-04-21) put it LAST, after the callback. The
|
||||
* deployment is therefore AHEAD of the client packages this repository pins, not behind
|
||||
* them, which inverts what the leaf says to do about it.
|
||||
*
|
||||
* The run is EXPECTED TO EXIT NON-ZERO while that holds: it is a measurement of a
|
||||
* deployment, not a gate on this package. Green again means the two ends agree, and the
|
||||
* bug leaf can close.
|
||||
* Updating them answered it: `@ng-org/web@0.1.2-alpha.14` declares six arguments with
|
||||
* `config` at index 4 and `@ng-org/orm@0.1.2-alpha.21` sends six to match, and the run
|
||||
* measured `6 passed, 0 failed` with the read ready in 16ms and the delete reaching the
|
||||
* document. The characterisation row was retired with the question it answered: there is
|
||||
* nothing left for it to find out, and a row that always prints the same sentence teaches
|
||||
* whoever reads this report to skip it.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
@@ -107,7 +97,6 @@ const CHECKS: readonly string[] = [
|
||||
"the initial payload carries the seeded object, with its `@id` and `@graph`",
|
||||
"`.delete()` on that set is accepted",
|
||||
"the deletion reached the DOCUMENT: the seeded subject's triples are gone",
|
||||
"[finding] the ARITY the deployed `orm_start_graph` expects",
|
||||
];
|
||||
|
||||
type Check = { name: string; ok: boolean; detail?: string };
|
||||
@@ -116,17 +105,6 @@ function check(name: string, ok: boolean, detail?: string): void {
|
||||
results.push({ name, ok, detail });
|
||||
console.log(` [${ok ? "PASS" : "FAIL"}] ${name}${detail ? " — " + detail : ""}`);
|
||||
}
|
||||
/**
|
||||
* A row that REPORTS rather than judges — `e2e/run.ts`'s convention for characterising the
|
||||
* broker rather than asserting about this package. The last row is one: whichever arity the
|
||||
* deployment expects, it is upstream's to change, so failing on it would only teach whoever
|
||||
* reads the report to ignore this file.
|
||||
*/
|
||||
function record(name: string, detail: string): void {
|
||||
results.push({ name, ok: true, detail });
|
||||
console.log(` [INFO] ${name} — ${detail}`);
|
||||
}
|
||||
|
||||
/** The bridge as the browser side of `frame.evaluate` sees it — no `any`, and no import
|
||||
* the serialized callback could not carry across. */
|
||||
type Bridge = Record<string, (...a: unknown[]) => unknown>;
|
||||
@@ -261,27 +239,6 @@ async function main(): Promise<void> {
|
||||
}
|
||||
check(CHECKS[4]!, remaining === 0, `triples left for the seeded subject: ${remaining}`);
|
||||
|
||||
// Only worth asking when the published call did NOT start: it is the diagnosis of
|
||||
// that failure, and a deployment that answers the published arity has nothing to
|
||||
// characterise. Since `@ng-org/web@0.1.2-alpha.14` the published call is itself the
|
||||
// six-argument one, so this asks whether the engine refuses a start the client got
|
||||
// right - no longer whether the client was sending the wrong number.
|
||||
if (readyMs === null) {
|
||||
const withConfig = await sdk<{ doc: string; error: string | null }>(
|
||||
frame,
|
||||
"liveStartWithConfig",
|
||||
);
|
||||
record(
|
||||
CHECKS[5]!,
|
||||
withConfig.error === null
|
||||
? "six arguments with `{}` in position 4 are ACCEPTED — the deployed " +
|
||||
"`orm_start_graph` takes a `config` BEFORE the callback, which is what the " +
|
||||
"published client now sends, so the refusal is not an arity mismatch"
|
||||
: `six arguments were refused too: ${withConfig.error}`,
|
||||
);
|
||||
} else {
|
||||
record(CHECKS[5]!, "not asked — the published six-argument call started");
|
||||
}
|
||||
} finally {
|
||||
if (page) await closeQuietly("the ORM live-read page", () => page!.close());
|
||||
if (ctx) await closeContext("orm-live", ctx);
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
*/
|
||||
|
||||
import { ng as realNg, init as realInit } from "@ng-org/web";
|
||||
// The REAL ORM. `useShapeQuery` reads through the SDK's own shape query since
|
||||
// `1.0.0-dev.6`, so the harness has to hand it one: a fake Set-like is enough for the
|
||||
// read FILTER (which is pure) and says nothing about a read that reaches the broker.
|
||||
// `initNg` is what resolves the ORM's own session promise; nothing starts before it.
|
||||
import { initNg as ormInitNg, RdfOrmSubscription } from "@ng-org/orm";
|
||||
import {
|
||||
configure,
|
||||
docs,
|
||||
@@ -26,6 +31,7 @@ import {
|
||||
storeRegistry,
|
||||
useShape as libUseShape,
|
||||
useShapeQuery,
|
||||
initNg,
|
||||
} from "@ng-eventually/polyfill";
|
||||
// The harness tests the LIBRARY, so it legitimately reaches machinery a consumer
|
||||
// application must not — but through the internal path, never the published entry.
|
||||
@@ -60,13 +66,50 @@ import { isNuri } from "../src/model/nuri";
|
||||
/** The whole local dataset - upstream's own marker, what `""` normalises to. */
|
||||
const EVERYTHING = { graphs: "did:ng:i" };
|
||||
|
||||
/**
|
||||
* The shape the two `useShapeQuery` journeys read - built here rather than inline, since
|
||||
* both of them need the same one and it is now handed to the SDK.
|
||||
*
|
||||
* 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 constraining nothing would match every subject in the dataset.
|
||||
* The title is the second one, and it is what proves the real data landed rather than an
|
||||
* empty object of the right type.
|
||||
*/
|
||||
function wsShape(cls: string) {
|
||||
return {
|
||||
shape: "urn:e2e:ws:Shape",
|
||||
schema: {
|
||||
"urn:e2e:ws:Shape": {
|
||||
iri: "urn:e2e:ws:Shape",
|
||||
predicates: [
|
||||
{
|
||||
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
||||
readablePredicate: "type",
|
||||
maxCardinality: 1,
|
||||
minCardinality: 1,
|
||||
dataTypes: [{ literals: [cls], valType: "iri" }],
|
||||
},
|
||||
{
|
||||
iri: "urn:e2e:ws:title",
|
||||
readablePredicate: "title",
|
||||
maxCardinality: 1,
|
||||
minCardinality: 1,
|
||||
dataTypes: [{ valType: "string" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies ShapeType<BaseType>;
|
||||
}
|
||||
|
||||
const storeOfType = (t: "public" | "protected" | "private"): Promise<Nuri> =>
|
||||
t === "public"
|
||||
? storeRegistry.publicStore()
|
||||
: t === "protected"
|
||||
? storeRegistry.protectedStore()
|
||||
: storeRegistry.privateStore();
|
||||
import type { Nuri, ShapeObservable, ShapeQuery } from "@ng-eventually/polyfill";
|
||||
import type { BaseType, Nuri, ShapeObservable, ShapeQuery, ShapeType } from "@ng-eventually/polyfill";
|
||||
|
||||
const { IdentityStore } = virtualUsers;
|
||||
|
||||
@@ -120,7 +163,12 @@ function fakeUseShape(_shape: unknown, _scope: unknown): any {
|
||||
configure({
|
||||
ng: realNg,
|
||||
useShape: fakeUseShape,
|
||||
// The SDK's own shape query, passed as it stands - no wrapper, no cast. This is what
|
||||
// `useShapeQuery` reads through, so the journeys below run against the REAL broker
|
||||
// rather than against anything this page composed.
|
||||
ormSubscription: RdfOrmSubscription.getOrCreate,
|
||||
init: realInit,
|
||||
initNg: ormInitNg,
|
||||
// The harness already holds an open wallet (the suite imports it itself), so it never
|
||||
// needs the gate's assisted import. These values exist only so the gate has something
|
||||
// to render when the access-gate steps exercise it — they are never used to import.
|
||||
@@ -794,9 +842,9 @@ const identity = new IdentityStore(
|
||||
},
|
||||
|
||||
// ── useShapeQuery (reactive useQuery-shaped read) ────────────────────────
|
||||
// A minimal SHEX-ish ShapeType pinning rdf:type to an e2e class IRI. useShapeQuery
|
||||
// reads only `.shape`/`.schema[...].predicates[rdf:type].dataTypes[].literals`
|
||||
// to know which @type to keep — generic, no application domain.
|
||||
// The shape is `wsShape(cls)` above: a hand-written `ShapeType`, two required
|
||||
// predicates, generic and with no application domain. The read goes through the
|
||||
// SDK's own shape query now, so these two journeys measure the REAL broker.
|
||||
_wsProbes: {} as Record<
|
||||
string,
|
||||
{ obs: ShapeObservable; initial: ShapeQuery; unsub: () => void }
|
||||
@@ -828,23 +876,7 @@ const identity = new IdentityStore(
|
||||
if (listed.includes(doc)) break;
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
const shape = {
|
||||
shape: "urn:e2e:ws:Shape",
|
||||
schema: {
|
||||
"urn:e2e:ws:Shape": {
|
||||
iri: "urn:e2e:ws:Shape",
|
||||
predicates: [
|
||||
{
|
||||
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
||||
readablePredicate: "type",
|
||||
maxCardinality: 1,
|
||||
minCardinality: 1,
|
||||
dataTypes: [{ literals: [cls], valType: "iri" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const shape = wsShape(cls);
|
||||
const obs = useShapeQuery(shape, EVERYTHING) as ShapeObservable;
|
||||
const unsub = obs.subscribe(() => {});
|
||||
// First snapshot immediately after subscribe — the barrier is not yet crossed
|
||||
@@ -866,8 +898,10 @@ const identity = new IdentityStore(
|
||||
isSuccess: snap.isSuccess,
|
||||
isError: snap.isError,
|
||||
dataLen: snap.data.length,
|
||||
// The seeded title, if the datum is present (proves the real data landed).
|
||||
titles: snap.data.flatMap((d: any) => d.props?.["urn:e2e:ws:title"] ?? []),
|
||||
// The seeded title, if the datum is present (proves the real data landed). It is the
|
||||
// shape's own readable predicate now, not a property bag: the objects come from the
|
||||
// ORM.
|
||||
titles: snap.data.map((d) => d.title).filter((v) => typeof v === "string"),
|
||||
};
|
||||
},
|
||||
useShapeQueryStop(handle: string) {
|
||||
@@ -884,23 +918,7 @@ const identity = new IdentityStore(
|
||||
registryInternals.resetRegistryCache();
|
||||
const id = "@ws-empty-" + handle;
|
||||
setCurrentUser(id);
|
||||
const shape = {
|
||||
shape: "urn:e2e:ws:Shape",
|
||||
schema: {
|
||||
"urn:e2e:ws:Shape": {
|
||||
iri: "urn:e2e:ws:Shape",
|
||||
predicates: [
|
||||
{
|
||||
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
||||
readablePredicate: "type",
|
||||
maxCardinality: 1,
|
||||
minCardinality: 1,
|
||||
dataTypes: [{ literals: [cls], valType: "iri" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const shape = wsShape(cls);
|
||||
const obs = useShapeQuery(shape, EVERYTHING) as ShapeObservable;
|
||||
const unsub = obs.subscribe(() => {});
|
||||
const initial = obs.getSnapshot();
|
||||
@@ -1105,6 +1123,11 @@ const identity = new IdentityStore(
|
||||
(event: { session: BrokerSession }) => {
|
||||
session = event.session;
|
||||
state.status = "connected";
|
||||
// The ORM reaches the SDK through a module-level promise of its own, resolved
|
||||
// once. Nothing it opens starts before this, so it goes here rather than beside
|
||||
// the first read - through the PACKAGE's `initNg`, which is the call an
|
||||
// application makes.
|
||||
initNg(realNg, session);
|
||||
sessionResolve(session);
|
||||
},
|
||||
true,
|
||||
|
||||
@@ -387,16 +387,18 @@ async function main(): Promise<void> {
|
||||
});
|
||||
|
||||
// ── useShapeQuery (reactive useQuery-shaped read) ───────────────────────
|
||||
// The real cycle: first subscription reads isPending (barrier not yet crossed),
|
||||
// then after the broker sync it reaches isSuccess with the seeded datum present.
|
||||
// A separate empty scope reaches isSuccess with data:[] (synced-but-empty — the
|
||||
// distinction the announced `useShapeQuery` makes native, surfaced here from getSyncState).
|
||||
// The real cycle, through the SDK's own shape query since `1.0.0-dev.6`: the first
|
||||
// subscription reads isPending (the ORM's initial payload has not arrived), then the
|
||||
// broker answers and it reaches isSuccess with the seeded datum present. A separate
|
||||
// identity with nothing of that shape reaches isSuccess with data:[] - which over
|
||||
// `did:ng:i` is the CAPABILITY FILTER doing the bounding, since the engine adds nothing
|
||||
// to narrow the whole dataset by what the reader may open.
|
||||
console.log("\n── useShapeQuery (reactive useQuery-shaped read) ──");
|
||||
await step("useShapeQuery: first subscribe isPending → isSuccess with data present", async () => {
|
||||
const h = "cyc" + Date.now();
|
||||
const CLS = "urn:e2e:ws:Event";
|
||||
const seed = await sdk<any>(frame, "useShapeQuerySeedAndSubscribe", h, CLS);
|
||||
check("first snapshot after subscribe is isPending (barrier not crossed)", seed.initial.isPending === true && seed.initial.isSuccess === false, `initial=${JSON.stringify(seed.initial)}`);
|
||||
check("first snapshot after subscribe is isPending (no initial payload yet)", seed.initial.isPending === true && seed.initial.isSuccess === false, `initial=${JSON.stringify(seed.initial)}`);
|
||||
// Event-driven: wait for the barrier to cross + the datum to land.
|
||||
await frame.waitForFunction(
|
||||
(hh) => {
|
||||
@@ -423,7 +425,7 @@ async function main(): Promise<void> {
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
const snap = await sdkGet<any>(frame, "useShapeQuerySnapshot", h);
|
||||
check("empty scope: isSuccess, data:[] (synced-but-empty, not stuck pending)", snap.isSuccess && !snap.isPending && !snap.isError && snap.dataLen === 0, `snap=${JSON.stringify(snap)}`);
|
||||
check("nothing of that shape: isSuccess, data:[] (answered-and-empty, not stuck pending)", snap.isSuccess && !snap.isPending && !snap.isError && snap.dataLen === 0, `snap=${JSON.stringify(snap)}`);
|
||||
await sdk(frame, "useShapeQueryStop", h);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user