feat(client): per-document reactive subscription (doc_subscribe), drop polling

Expose subscribeDoc(nuri, onChange) / subscribeDocs(nuris, onChange) wrapping the
real ng.doc_subscribe — per-document, event-driven (initial State + a Patch per
commit, local or broker-synced from a remote peer), returning a sync unsubscribe.
subscribeDocs isolates per doc (a failing/unsynced doc never aborts the others),
so it sidesteps the ORM fan-out hang (never orm_start_graph(graphs:[…])).

Replace the setInterval polling in inbox.watch() and discovery.watchIndex() with
doc_subscribe — same public contract, now push not poll. NextGraph is subscription-
first; no polling remains.

Verified against the real broker (@data harness spike): the doc_subscribe callback
marshals across the iframe RPC (@ng-org/web strips the callback and drives it via a
MessagePort — no DataCloneError) and fires on the initial push and on a real write.
Lib bun test 91 pass, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-06 23:04:32 +02:00
parent 7ebb03a3f3
commit c0498a6ebc
7 changed files with 396 additions and 22 deletions
+21 -1
View File
@@ -57,6 +57,25 @@ function makeFakeNg() {
const quads: Quad[] = [];
let docCounter = 0;
// Reactive subscriptions (see inbox.test.ts): doc_subscribe registers a
// callback per anchor + fires an initial push; sparql_update pushes a Patch to
// that anchor's subscribers, so discovery.watchIndex (now event-driven) works
// without a timer.
const subs = new Map<string, Set<(r: unknown) => void>>();
const doc_subscribe = mock(async (nuri: string, _sid: unknown, cb: (r: unknown) => void) => {
let set = subs.get(nuri);
if (!set) {
set = new Set();
subs.set(nuri, set);
}
set.add(cb);
queueMicrotask(() => cb({ V0: { State: { doc: nuri } } }));
return () => set!.delete(cb);
});
const pushTo = (anchor: string): void => {
for (const cb of subs.get(anchor) ?? []) cb({ V0: { Patch: { doc: anchor } } });
};
const doc_create = mock(async (..._a: unknown[]) => `did:ng:o:doc${++docCounter}`);
const sparql_update = mock(async (...a: unknown[]) => {
@@ -93,6 +112,7 @@ function makeFakeNg() {
const o = m[2] !== undefined ? unescapeLiteral(m[2]) : (m[3] ?? "");
quads.push({ g, s, p, o });
}
pushTo(g); // local-push to the written graph's subscribers
return undefined;
});
@@ -157,7 +177,7 @@ function makeFakeNg() {
return { results: { bindings: [] } };
});
return { doc_create, sparql_update, sparql_query, _quads: quads };
return { doc_create, doc_subscribe, sparql_update, sparql_query, _quads: quads };
}
const SESSION: RegistrySession = { sessionId: "sid-1", privateStoreId: "PRIV" };