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:
@@ -38,6 +38,7 @@
|
||||
*/
|
||||
|
||||
import * as inbox from "./inbox";
|
||||
import { subscribeDoc } from "./subscribe";
|
||||
import { ensureAccount, reservedAccount } from "./store-registry";
|
||||
import { getCaps } from "./polyfill";
|
||||
import type { Nuri, PrincipalId } from "./types";
|
||||
@@ -156,18 +157,25 @@ export async function readIndex(): Promise<IndexEntry[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the discovery index. Polls {@link readIndex} and fires `onEntries`
|
||||
* whenever the index changes. Returns an unsubscribe. Fires once immediately.
|
||||
* (Deduplication is applied on each read.)
|
||||
* Watch the discovery index — **event-driven, not polled**. Subscribes to the
|
||||
* index document via {@link subscribeDoc} (the platform's `doc_subscribe` push):
|
||||
* `onEntries` fires once on the initial state push and again on every subsequent
|
||||
* change to the index document — a local submission OR a broker-synced remote one.
|
||||
* Returns an unsubscribe. (Deduplication is applied on each read.)
|
||||
*
|
||||
* The `intervalMs` option is accepted for signature compatibility but IGNORED:
|
||||
* there is no polling. The index is a single document, so this is immune to the
|
||||
* ORM fan-out hang (see {@link subscribeDoc}).
|
||||
*/
|
||||
export function watchIndex(
|
||||
onEntries: (entries: IndexEntry[]) => void,
|
||||
opts?: { intervalMs?: number },
|
||||
_opts?: { intervalMs?: number },
|
||||
): () => void {
|
||||
let stopped = false;
|
||||
let lastCount = -1;
|
||||
const intervalMs = opts?.intervalMs ?? 1000;
|
||||
const tick = async (): Promise<void> => {
|
||||
let unsubscribe: (() => void) | null = null;
|
||||
|
||||
const refresh = async (): Promise<void> => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
const entries = await readIndex();
|
||||
@@ -179,10 +187,25 @@ export function watchIndex(
|
||||
console.error("[discovery] watchIndex read failed:", error);
|
||||
}
|
||||
};
|
||||
void tick();
|
||||
const handle = setInterval(() => void tick(), intervalMs);
|
||||
|
||||
// The index document NURI is resolved async (ensureAccount); subscribe once it
|
||||
// is known. The initial State push fires the first read (onEntries fires once),
|
||||
// each later Patch a re-read.
|
||||
void (async () => {
|
||||
try {
|
||||
const anchor = await indexInboxNuri();
|
||||
if (stopped) return;
|
||||
unsubscribe = subscribeDoc(anchor, () => void refresh());
|
||||
} catch (error) {
|
||||
console.error("[discovery] watchIndex subscribe failed:", error);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
clearInterval(handle);
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
unsubscribe = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user