feat!: useShapeQuery prend le Scope du SDK, et un store se nomme par sa reference
This commit is contained in:
@@ -561,8 +561,12 @@ async function signIn(ctx: BrowserContext, appUrl: string, id: string): Promise<
|
||||
// ── the acts, expressed as the application expresses them ───────────────────
|
||||
|
||||
/**
|
||||
* Show the notes of `scope` — the list is per-scope, so acting on a note means looking at
|
||||
* the right shelf first.
|
||||
* Choose a shelf, and wait for the list to settle.
|
||||
*
|
||||
* The shelf decides where the NEXT note is written and nothing else: since `1.0.0-dev.5`
|
||||
* a read cannot be scoped by store — the SDK has no way to say it and neither has this
|
||||
* package — so the application lists every note its identity holds, whatever shelf it went
|
||||
* to. Selecting one still re-renders, which is what the wait below is for.
|
||||
*
|
||||
* The wait is not decoration: the application's `change` handler runs `void refresh()`,
|
||||
* un-awaited, so reading `textContent` straight after `selectOption` reads the PREVIOUS
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
inbox,
|
||||
storeRegistry,
|
||||
useShape as libUseShape,
|
||||
watchShape,
|
||||
useShapeQuery,
|
||||
} 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.
|
||||
@@ -48,6 +48,24 @@ import { ensureIdentity } from "@ng-eventually/polyfill";
|
||||
// The harness narrows for its OWN assertions; a consumer never has to (the entries take
|
||||
// plain strings and validate inside). Internal path, like the rest of its machinery.
|
||||
import { isNuri } from "../src/model/nuri";
|
||||
|
||||
/**
|
||||
* The published reference of one of the connected identity's three stores.
|
||||
*
|
||||
* Placement stopped being spelled as a word on the published surface in `1.0.0-dev.5`:
|
||||
* an application names a store by what `privateStore()`, `protectedStore()` or
|
||||
* `publicStore()` hands it. The journeys below still walk the three, so this is the step
|
||||
* from one to the other - through the published calls.
|
||||
*/
|
||||
/** The whole local dataset - upstream's own marker, what `""` normalises to. */
|
||||
const EVERYTHING = { graphs: "did:ng:i" };
|
||||
|
||||
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";
|
||||
|
||||
const { IdentityStore } = virtualUsers;
|
||||
@@ -575,16 +593,16 @@ const identity = new IdentityStore(
|
||||
// and the caps of what you create are filed under the identity you were acting
|
||||
// as. Creating B's document while connected as A is not a thing the model has.
|
||||
setCurrentUser(idA);
|
||||
const dA1 = await storeRegistry.createEntityDoc("public");
|
||||
const dA2 = await storeRegistry.createEntityDoc("public");
|
||||
const dA1 = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
const dA2 = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
setCurrentUser(idB);
|
||||
const dB1 = await storeRegistry.createEntityDoc("public");
|
||||
const dB1 = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
// listMyEntityDocs(A) → only A's docs (poll: the index append can lag).
|
||||
setCurrentUser(idA);
|
||||
let listA: string[] = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
registryInternals.resetRegistryCache();
|
||||
listA = await storeRegistry.listMyEntityDocs("public");
|
||||
listA = await registryInternals.listMyEntityDocs(idA, "public");
|
||||
if (listA.includes(dA1) && listA.includes(dA2)) break;
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
@@ -616,7 +634,7 @@ const identity = new IdentityStore(
|
||||
// document is filed under nobody and the very session that created it is
|
||||
// refused the write below.
|
||||
setCurrentUser(id);
|
||||
const entityNuri = await storeRegistry.createEntityDoc(scope);
|
||||
const entityNuri = await storeRegistry.createEntityDoc(await storeOfType(scope));
|
||||
const marker = "recon-" + Date.now();
|
||||
await docs.sparqlUpdate(
|
||||
s.session_id,
|
||||
@@ -629,7 +647,7 @@ const identity = new IdentityStore(
|
||||
let listed: string[] = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
registryInternals.resetRegistryCache();
|
||||
listed = await storeRegistry.listMyEntityDocs(scope);
|
||||
listed = await registryInternals.listMyEntityDocs(id, scope);
|
||||
if (listed.includes(entityNuri)) break;
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
@@ -655,7 +673,7 @@ const identity = new IdentityStore(
|
||||
await connectedUser();
|
||||
|
||||
registryInternals.resetRegistryCache();
|
||||
const listed = await storeRegistry.listMyEntityDocs(scope);
|
||||
const listed = await registryInternals.listMyEntityDocs(id, scope);
|
||||
// DIAGNOSTIC: a RAW anchored read of the entity doc with NO open — reports how
|
||||
// many rows the bare anchored query resolves for a not-yet-opened repo (the
|
||||
// premise: 0 until opened). Uses the low-level docs primitive directly, bypassing
|
||||
@@ -703,10 +721,14 @@ const identity = new IdentityStore(
|
||||
const rec = await registryInternals.ensureAccount(id);
|
||||
return { docPublic: rec.docPublic, docProtected: rec.docProtected, docPrivate: rec.docPrivate };
|
||||
},
|
||||
// `resolveScopeGraph` stopped being published in `1.0.0-dev.5` — a store's graph holds
|
||||
// its containment triples and nothing else, so the value it handed out matched nothing
|
||||
// as a read scope upstream. The probe keeps measuring the mapping, at its internal path:
|
||||
// a harness may do that, an application may not.
|
||||
async scopeResolvers() {
|
||||
const priv = await storeRegistry.resolveScopeGraph("private");
|
||||
const prot = await storeRegistry.resolveScopeGraph("protected");
|
||||
const pub = await storeRegistry.resolveScopeGraph("public");
|
||||
const priv = await registryInternals.resolveScopeGraph("private");
|
||||
const prot = await registryInternals.resolveScopeGraph("protected");
|
||||
const pub = await registryInternals.resolveScopeGraph("public");
|
||||
return { priv, prot, pub };
|
||||
},
|
||||
|
||||
@@ -771,8 +793,8 @@ const identity = new IdentityStore(
|
||||
}
|
||||
},
|
||||
|
||||
// ── watchShape (reactive useQuery-shaped read) ───────────────────────────
|
||||
// A minimal SHEX-ish ShapeType pinning rdf:type to an e2e class IRI. watchShape
|
||||
// ── 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.
|
||||
_wsProbes: {} as Record<
|
||||
@@ -781,17 +803,17 @@ const identity = new IdentityStore(
|
||||
>,
|
||||
/**
|
||||
* REAL-BROKER cycle proof. Under a fresh identity, create ONE protected entity
|
||||
* document carrying an rdf:type=<e2e class> triple, then open a `watchShape` over
|
||||
* (that class shape, "protected"). Capture the FIRST snapshot right after subscribe
|
||||
* document carrying an rdf:type=<e2e class> triple, then open a `useShapeQuery` over
|
||||
* (that class shape, the whole dataset). Capture the FIRST snapshot right after subscribe
|
||||
* (must be isPending) so the caller can then wait event-driven for isSuccess with
|
||||
* the seeded datum present. Returns the handle + the initial snapshot + the seed
|
||||
* doc/type so the runner can assert the data landed.
|
||||
*/
|
||||
async watchShapeSeedAndSubscribe(handle: string, cls: string) {
|
||||
async useShapeQuerySeedAndSubscribe(handle: string, cls: string) {
|
||||
registryInternals.resetRegistryCache();
|
||||
const id = "@ws-" + handle;
|
||||
setCurrentUser(id);
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
const s = await sessionReady;
|
||||
// Seed the entity doc with the shape's type + a title (anchored default graph).
|
||||
await docs.sparqlUpdate(
|
||||
@@ -802,7 +824,7 @@ const identity = new IdentityStore(
|
||||
// Wait until this session sees the index append (data persisted on the broker).
|
||||
for (let i = 0; i < 15; i++) {
|
||||
registryInternals.resetRegistryCache();
|
||||
const listed = await storeRegistry.listMyEntityDocs("protected");
|
||||
const listed = await registryInternals.listMyEntityDocs(id, "protected");
|
||||
if (listed.includes(doc)) break;
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
@@ -823,19 +845,19 @@ const identity = new IdentityStore(
|
||||
},
|
||||
},
|
||||
};
|
||||
const obs = watchShape(shape, "protected") as ShapeObservable;
|
||||
const obs = useShapeQuery(shape, EVERYTHING) as ShapeObservable;
|
||||
const unsub = obs.subscribe(() => {});
|
||||
// First snapshot immediately after subscribe — the barrier is not yet crossed
|
||||
// (docs opening via doc_subscribe), so this MUST be pending.
|
||||
const initial = obs.getSnapshot();
|
||||
(window as any).__sdk._wsProbes[handle] = { obs, initial, unsub };
|
||||
// NB: leave the current user SET for the probe's lifetime — watchShape resolves
|
||||
// NB: leave the current user SET for the probe's lifetime — useShapeQuery resolves
|
||||
// the scope from getCurrentUser() on every (reactive) refresh, exactly as the app
|
||||
// keeps a stable identity. watchShapeStop clears it.
|
||||
// keeps a stable identity. useShapeQueryStop clears it.
|
||||
return { doc, cls, initial: { isPending: initial.isPending, isSuccess: initial.isSuccess, dataLen: initial.data.length } };
|
||||
},
|
||||
/** Current snapshot of a watchShape probe (event-driven poll target). */
|
||||
watchShapeSnapshot(handle: string) {
|
||||
/** Current snapshot of a useShapeQuery probe (event-driven poll target). */
|
||||
useShapeQuerySnapshot(handle: string) {
|
||||
const rec = (window as any).__sdk._wsProbes[handle];
|
||||
if (!rec) return null;
|
||||
const snap: ShapeQuery = rec.obs.getSnapshot();
|
||||
@@ -848,17 +870,17 @@ const identity = new IdentityStore(
|
||||
titles: snap.data.flatMap((d: any) => d.props?.["urn:e2e:ws:title"] ?? []),
|
||||
};
|
||||
},
|
||||
watchShapeStop(handle: string) {
|
||||
useShapeQueryStop(handle: string) {
|
||||
const rec = (window as any).__sdk._wsProbes[handle];
|
||||
if (rec) rec.unsub();
|
||||
setCurrentUser(null);
|
||||
},
|
||||
/**
|
||||
* EMPTY-scope proof: a brand-new identity with NO entity docs of `scope`. watchShape
|
||||
* EMPTY-scope proof: a brand-new identity with NO entity docs at all. `useShapeQuery`
|
||||
* must reach isSuccess with data:[] (synced-but-empty), NOT stay pending. Returns
|
||||
* the handle; poll watchShapeSnapshot for the transition.
|
||||
* the handle; poll useShapeQuerySnapshot for the transition.
|
||||
*/
|
||||
watchShapeEmptyStart(handle: string, cls: string) {
|
||||
useShapeQueryEmptyStart(handle: string, cls: string) {
|
||||
registryInternals.resetRegistryCache();
|
||||
const id = "@ws-empty-" + handle;
|
||||
setCurrentUser(id);
|
||||
@@ -879,11 +901,11 @@ const identity = new IdentityStore(
|
||||
},
|
||||
},
|
||||
};
|
||||
const obs = watchShape(shape, "protected") as ShapeObservable;
|
||||
const obs = useShapeQuery(shape, EVERYTHING) as ShapeObservable;
|
||||
const unsub = obs.subscribe(() => {});
|
||||
const initial = obs.getSnapshot();
|
||||
(window as any).__sdk._wsProbes[handle] = { obs, initial, unsub };
|
||||
// Keep the identity set for the probe's lifetime (watchShapeStop clears it): a
|
||||
// Keep the identity set for the probe's lifetime (useShapeQueryStop clears it): a
|
||||
// faithful "empty scope for a real identity", not "no identity at all".
|
||||
return { initial: { isPending: initial.isPending, isSuccess: initial.isSuccess } };
|
||||
},
|
||||
|
||||
@@ -386,45 +386,45 @@ async function main(): Promise<void> {
|
||||
check("scope resolvers return NURIs (private distinct from protected/public)", !!r.priv && !!r.prot && !!r.pub && r.priv !== r.prot, `priv=${r.priv?.slice(0,16)}… prot=${r.prot?.slice(0,16)}…`);
|
||||
});
|
||||
|
||||
// ── watchShape (reactive useQuery-shaped read) ──────────────────────────
|
||||
// ── 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 useShape's upgrade will make native, surfaced here from getSyncState).
|
||||
console.log("\n── watchShape (reactive useQuery-shaped read) ──");
|
||||
await step("watchShape: first subscribe isPending → isSuccess with data present", async () => {
|
||||
// distinction the announced `useShapeQuery` makes native, surfaced here from getSyncState).
|
||||
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, "watchShapeSeedAndSubscribe", h, CLS);
|
||||
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)}`);
|
||||
// Event-driven: wait for the barrier to cross + the datum to land.
|
||||
await frame.waitForFunction(
|
||||
(hh) => {
|
||||
const s = (window as any).__sdk.watchShapeSnapshot(hh as string);
|
||||
const s = (window as any).__sdk.useShapeQuerySnapshot(hh as string);
|
||||
return s && s.isSuccess && s.dataLen >= 1;
|
||||
},
|
||||
h,
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
const snap = await sdkGet<any>(frame, "watchShapeSnapshot", h);
|
||||
const snap = await sdkGet<any>(frame, "useShapeQuerySnapshot", h);
|
||||
check("reaches isSuccess with the seeded datum (titles include 'seeded')", snap.isSuccess && !snap.isPending && !snap.isError && snap.dataLen >= 1 && snap.titles.includes("seeded"), `snap=${JSON.stringify(snap)}`);
|
||||
await sdk(frame, "watchShapeStop", h);
|
||||
await sdk(frame, "useShapeQueryStop", h);
|
||||
});
|
||||
await step("watchShape: empty scope reaches isSuccess with data:[]", async () => {
|
||||
await step("useShapeQuery: empty scope reaches isSuccess with data:[]", async () => {
|
||||
const h = "empty" + Date.now();
|
||||
const CLS = "urn:e2e:ws:Event";
|
||||
await sdk<any>(frame, "watchShapeEmptyStart", h, CLS);
|
||||
await sdk<any>(frame, "useShapeQueryEmptyStart", h, CLS);
|
||||
await frame.waitForFunction(
|
||||
(hh) => {
|
||||
const s = (window as any).__sdk.watchShapeSnapshot(hh as string);
|
||||
const s = (window as any).__sdk.useShapeQuerySnapshot(hh as string);
|
||||
return s && s.isSuccess;
|
||||
},
|
||||
h,
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
const snap = await sdkGet<any>(frame, "watchShapeSnapshot", h);
|
||||
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)}`);
|
||||
await sdk(frame, "watchShapeStop", h);
|
||||
await sdk(frame, "useShapeQueryStop", h);
|
||||
});
|
||||
|
||||
// ── caps / read-filter (in-memory cap model) ────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user