import { test, expect, mock, afterAll } from "bun:test"; import { docChangeType, subscribeDoc, subscribeDocReportingSetupFailure, subscribeDocs, type Unsubscribe, } from "../src/surface/subscribe"; import { configure } from "../src/index"; import { configureStoreRegistry } from "../src/shared-wallet/bootstrap"; import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap"; import type { RegistrySession } from "../src/shared-wallet/account-registry"; // subscribeDoc/subscribeDocs wrap the REAL injected `ng.doc_subscribe`. This // suite injects a fake `ng` whose `doc_subscribe` records the callback per doc // and hands back an unsubscribe, so we can assert routing + isolation without a // broker. Restore the un-configured state at the end. afterAll(() => { resetConfig(); resetStoreRegistry(); }); const SESSION: RegistrySession = { sessionId: "sid-1", privateStoreId: "PRIV" }; /** * A fake reactive `ng`: `doc_subscribe(nuri, sid, cb)` takes over `nuri`, fires `cb` once * (initial State push), and returns an unsubscribe. `push(nuri)` drives a later change to * that doc's subscriber. A per-doc `failFor` set makes `doc_subscribe` reject for chosen * NURIs (a not-yet-synced doc). * * ── ONE subscriber per document, and a second one EVICTS it ─────────────── * Not a simplification — it is what the broker does. A branch holds a single sender * (`branch_subscriptions: HashMap>`) and * `create_branch_subscription` closes whatever it displaces, silently: the evicted * unsubscribe still returns cleanly and nothing anywhere errors. Confirmed against the real * broker on 2026-08-17 — with two subscriptions on one document, a write fired the second * callback and the first, which had been firing moments earlier, went quiet for good. * * A Set of callbacks here would model a world where every subscriber coexists. It is * exactly the assumption that cost this package a view that never re-read and an inbox that * never notified, and a fake that holds it cannot fail on either. */ function makeFakeNg(failFor: Set = new Set()) { const subs = new Map void>(); const doc_subscribe = mock(async (nuri: string, _sid: unknown, cb: (r: unknown) => void) => { if (failFor.has(nuri)) throw new Error(`RepoNotFound: ${nuri}`); subs.set(nuri, cb); // whoever held this branch is dropped, without a word // Initial State push, delivered async (as the real RPC does) — and only while this // callback still holds the branch. queueMicrotask(() => { if (subs.get(nuri) === cb) cb({ V0: { State: { doc: nuri } } }); }); return () => { if (subs.get(nuri) === cb) subs.delete(nuri); }; }); const push = (nuri: string): void => { subs.get(nuri)?.({ V0: { Patch: { doc: nuri } } }); }; const isSubscribed = (nuri: string): boolean => subs.has(nuri); return { doc_subscribe, push, isSubscribed, _subs: subs }; } function inject(failFor?: Set) { const ng = makeFakeNg(failFor); configure({ ng: ng as any, useShape: (() => {}) as any }); // Synchronous fake store → no sync lag; disable the anti-fork retry backoff. configureStoreRegistry({ getSession: async () => SESSION }); return ng; } const A = "did:ng:o:docA"; const B = "did:ng:o:docB"; const tick = () => new Promise((r) => setTimeout(r, 5)); test("subscribeDoc calls ng.doc_subscribe with (nuri, sessionId, callback)", async () => { const ng = inject(); const onChange = mock(() => {}); subscribeDoc(A, onChange); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); const call = ng.doc_subscribe.mock.calls[0]!; expect(call[0]).toBe(A); expect(call[1]).toBe("sid-1"); // sessionId from the injected session expect(typeof call[2]).toBe("function"); // the callback }); test("subscribeDoc routes the initial State push and every later change", async () => { const ng = inject(); const seen: unknown[] = []; subscribeDoc(A, (r) => seen.push(r)); await tick(); expect(seen).toHaveLength(1); // initial State push ng.push(A); ng.push(A); expect(seen).toHaveLength(3); // + 2 patches }); test("subscribeDoc unsubscribe stops further callbacks", async () => { const ng = inject(); const seen: unknown[] = []; const stop = subscribeDoc(A, (r) => seen.push(r)); await tick(); expect(seen).toHaveLength(1); stop(); expect(ng.isSubscribed(A)).toBe(false); // real unsubscribe was invoked ng.push(A); // ignored — no subscriber expect(seen).toHaveLength(1); }); test("subscribeDoc unsubscribe BEFORE async setup resolves cancels cleanly", async () => { const ng = inject(); const seen: unknown[] = []; const stop = subscribeDoc(A, (r) => seen.push(r)); stop(); // before the microtask/promise setup resolved await tick(); // The subscription was cancelled the moment setup resolved: no callbacks, and // no lingering subscriber. expect(seen).toHaveLength(0); expect(ng.isSubscribed(A)).toBe(false); }); test("subscribeDocs fans out one subscription per doc and reports the source nuri", async () => { const ng = inject(); const seen: Array<[string, unknown]> = []; subscribeDocs([A, B], (nuri, r) => seen.push([nuri, r])); await tick(); // Two initial pushes, one per doc. expect(seen.map((s) => s[0]).sort()).toEqual([A, B]); ng.push(B); expect(seen.filter((s) => s[0] === B)).toHaveLength(2); // initial + patch expect(seen.filter((s) => s[0] === A)).toHaveLength(1); // isolated: A didn't fire }); test("subscribeDocs isolates a failing doc — the others still fire", async () => { const ng = inject(new Set([A])); // A's subscription throws (RepoNotFound) const seen: Array<[string, unknown]> = []; subscribeDocs([A, B], (nuri, r) => seen.push([nuri, r])); await tick(); // A failed to subscribe (logged, not thrown); B is unaffected and fired. expect(seen.map((s) => s[0])).toEqual([B]); ng.push(B); expect(seen.filter((s) => s[0] === B)).toHaveLength(2); }); test("subscribeDocs unsubscribe tears down all subscriptions", async () => { const ng = inject(); const stop = subscribeDocs([A, B], () => {}); await tick(); expect(ng.isSubscribed(A)).toBe(true); expect(ng.isSubscribed(B)).toBe(true); stop(); expect(ng.isSubscribed(A)).toBe(false); expect(ng.isSubscribed(B)).toBe(false); }); test("subscribeDocs deduplicates repeated NURIs", async () => { const ng = inject(); subscribeDocs([A, A, A], () => {}); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); }); // --- two subscribers on ONE document --------------------------------------- // // A branch has room for exactly one subscriber and a second `doc_subscribe` evicts the // first (see `makeFakeNg`). Everything inside this package subscribes — `ensureRepoOpen` // holds a bootstrap subscription per document for the session, `watchShape` follows the // documents of a scope, `inbox.watch` follows an inbox, and the inbox observation follows // every inbox — so any two of them on the same document used to silence one another. // Nothing rejected and nothing logged; the view simply stopped re-reading. // // So the package opens ONE real subscription per document and fans it out. These are the // tests that say so. test("two subscribers on one document BOTH keep firing", async () => { const ng = inject(); const first: unknown[] = []; const second: unknown[] = []; subscribeDoc(A, (r) => first.push(r)); await tick(); expect(first).toHaveLength(1); // its initial State subscribeDoc(A, (r) => second.push(r)); await tick(); ng.push(A); // The one that was there first is not silenced by the one that came second. expect(first).toHaveLength(2); expect(second.length).toBeGreaterThanOrEqual(1); }); test("one real doc_subscribe serves every subscriber of a document", async () => { const ng = inject(); subscribeDoc(A, () => {}); subscribeDoc(A, () => {}); subscribeDoc(A, () => {}); await tick(); // Three callers, one branch taken. A second call would have evicted the first caller. expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); }); test("a subscriber that joins LATE still gets its initial State", async () => { inject(); subscribeDoc(A, () => {}); await tick(); // the initial State has come and gone const late: unknown[] = []; subscribeDoc(A, (r) => late.push(r)); await tick(); // Its own `doc_subscribe` would have pushed it a State; joining an open one owes it the // same, or "fires once immediately" quietly stops being true for whoever arrives second. expect(late).toHaveLength(1); expect(docChangeType(late[0])).toBe("State"); }); test("the real subscription is released only when the LAST subscriber leaves", async () => { const ng = inject(); const seen: unknown[] = []; const stopFirst = subscribeDoc(A, () => {}); const stopSecond = subscribeDoc(A, (r) => seen.push(r)); await tick(); stopFirst(); expect(ng.isSubscribed(A)).toBe(true); // somebody is still listening ng.push(A); expect(seen.length).toBeGreaterThanOrEqual(2); // and still hearing stopSecond(); expect(ng.isSubscribed(A)).toBe(false); // now nobody is }); // --- what sharing one subscription must not COST a caller -------------------- // // Three things a per-caller `doc_subscribe` gave for free. The fan-out took each of them // away when it was introduced, and each is invisible from the caller's side: nothing // rejects, nothing logs, the document simply stops speaking to somebody. test("two subscriptions with the SAME handler are two subscriptions", async () => { const ng = inject(); const seen: unknown[] = []; // One function, two callers. A module-level handler, a bound method or a shared arrow // makes this ordinary rather than exotic — and keyed by the function, the second caller // was never registered at all, so the first one's departure took it with it. const handler = (r: unknown): void => { seen.push(r); }; const stopFirst = subscribeDoc(A, handler); const stopSecond = subscribeDoc(A, handler); await tick(); const afterInitial = seen.length; stopFirst(); // only the FIRST caller has left expect(ng.isSubscribed(A)).toBe(true); ng.push(A); expect(seen.length).toBeGreaterThan(afterInitial); // the second one is still listening stopSecond(); expect(ng.isSubscribed(A)).toBe(false); // …and now the last one has gone }); test("a subscriber torn down inside another's handler does not receive that push", async () => { const ng = inject(); const seen: unknown[] = []; let stopSecond: Unsubscribe | null = null; let armed = false; // The first handler tears the second one down mid-push. Which of the two runs first is an // ordering no application controls, and `subscribeDoc` publishes that no `onChange` fires // after unsubscribe — so the answer must not depend on it. const stopFirst = subscribeDoc(A, () => { if (armed) stopSecond?.(); }); stopSecond = subscribeDoc(A, (r) => seen.push(r)); await tick(); seen.length = 0; armed = true; ng.push(A); expect(seen).toEqual([]); stopFirst(); }); test("a setup that FAILED does not poison the document for the next subscriber", async () => { const failFor = new Set([A]); const ng = inject(failFor); const stopFirst = subscribeDoc(A, () => {}); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); // …and it rejected // The document syncs; the broker can serve it now. A caller arriving after a transient // rejection must get a real subscription, exactly as its own `doc_subscribe` would have. failFor.delete(A); const seen: unknown[] = []; const stopSecond = subscribeDoc(A, (r) => seen.push(r)); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(2); // attempted again, not joined to a corpse expect(seen.length).toBeGreaterThan(0); // its initial State ng.push(A); expect(seen.length).toBeGreaterThan(1); // and the changes that follow stopFirst(); stopSecond(); }); test("a caller that asks to be told learns its subscription could not be opened", async () => { inject(new Set([A])); const failures: unknown[] = []; const stop = subscribeDocReportingSetupFailure( A, () => {}, (error) => failures.push(error), ); await tick(); // Upstream `doc_subscribe` is async and rejects, so its caller learns. This wrapper // returns synchronously, and a caller whose job depends on the subscription (the inbox // observation) cannot otherwise tell "quiet" from "never opened". expect(failures).toHaveLength(1); expect(String(failures[0])).toContain("RepoNotFound"); stop(); });