feat(client): watchShape — lecture réactive à la forme TanStack useQuery
Phase A du refactor des lectures. Surface la barrière de sync interne
(open-repo `getSyncState`) dans une API useQuery-shaped, en anticipation de la
mise à jour prévue de useShape par NextGraph — distingue nativement « sync en
cours » de « synchronisé mais vide ».
`watchShape<T>(shapeType, scope): { getSnapshot(): ShapeQuery<T>, subscribe(cb),
refetch() }` avec `ShapeQuery = { data, isPending, isSuccess, isError, error }`.
OBSERVABLE (pas de dépendance React — l'app câblera useSyncExternalStore en
phase B) ; getSnapshot rend une référence stable.
- Scope LOGIQUE (public/protected/private) résolu au wallet virtuel :
listMyEntityDocs(getCurrentUser, scope) + découverte foldée pour public.
- isPending tant que la barrière n'est pas atteinte / 1er readUnion non rendu ;
isSuccess après ; timed-out → isSuccess (best-effort, pas isError).
- Réactif SANS polling : subscribeDoc sur les docs + le doc d'index de scope
(+ index découverte) → re-read/re-résolution au push ; souscriptions idempotentes.
- Générique : aucune logique domaine Festipod dans le lib ; filtre par la shape
SHEX (rdf:type). Pas de double filtre cap.
gate : tsc 0 ; bun test 120 (+4 : pending→success, synced-vide, réactif, timed-out) ;
test:e2e 42 passed (+scénario broker réel : isPending au 1er snapshot → isSuccess
avec données, scope vide → isSuccess data:[]).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -294,6 +294,47 @@ 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) ──────────────────────────
|
||||
// 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 () => {
|
||||
const h = "cyc" + Date.now();
|
||||
const CLS = "urn:e2e:ws:Event";
|
||||
const seed = await sdk<any>(frame, "watchShapeSeedAndSubscribe", 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);
|
||||
return s && s.isSuccess && s.dataLen >= 1;
|
||||
},
|
||||
h,
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
const snap = await sdkGet<any>(frame, "watchShapeSnapshot", 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 step("watchShape: 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 frame.waitForFunction(
|
||||
(hh) => {
|
||||
const s = (window as any).__sdk.watchShapeSnapshot(hh as string);
|
||||
return s && s.isSuccess;
|
||||
},
|
||||
h,
|
||||
{ timeout: 30000 },
|
||||
);
|
||||
const snap = await sdkGet<any>(frame, "watchShapeSnapshot", 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);
|
||||
});
|
||||
|
||||
// ── caps / read-filter (in-memory cap model) ────────────────────────────
|
||||
console.log("\n── caps / read-filter (in-memory cap model) ──");
|
||||
await step("read-filter: protected hidden from stranger", async () => {
|
||||
|
||||
Reference in New Issue
Block a user