feat!: useShapeQuery lit par la requete de forme du SDK

This commit is contained in:
Sylvain Duchesne
2026-09-21 11:41:31 +02:00
parent a5e553e65b
commit 77e78ec81d
29 changed files with 1190 additions and 971 deletions
+63 -40
View File
@@ -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,