import { test, expect, mock, afterAll } from "bun:test"; import { docChangeType, resubscribeDocs, 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(), hangFor: Set = new Set()) { const subs = new Map void>(); // A call the broker has neither answered nor refused yet, so a LATER call can overtake it // and this one can settle afterwards. Held on an object rather than in a `let` so its // type survives being written from one closure and read from another. const hung: { reject: ((error: unknown) => void) | null; resolve: ((unsub: () => void) => void) | null; cb: ((r: unknown) => void) | null; live: boolean; } = { reject: null, resolve: null, cb: null, live: false }; const doc_subscribe = mock(async (nuri: string, _sid: unknown, cb: (r: unknown) => void) => { if (failFor.has(nuri)) throw new Error(`RepoNotFound: ${nuri}`); if (hangFor.has(nuri)) { return await new Promise((resolve, reject) => { hung.reject = reject; hung.resolve = resolve as (unsub: () => void) => void; hung.cb = cb; }); } 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); /** The call that was left hanging finally answers — with a refusal. */ const rejectHung = (): void => { const reject = hung.reject; hung.reject = null; reject?.(new Error("RepoNotFound: late")); }; /** * The call that was left hanging finally answers — SUCCESSFULLY, and from the session it * was placed against, which by now is the PREVIOUS one. * * Modelled apart from `subs` on purpose: a session is a verifier of its own, with its own * `branch_subscriptions`, so this channel neither evicted nor was evicted by the one the * new session opened (`engine/verifier/src/verifier.rs:361` inserts into `self`). It is * genuinely live and genuinely pushing, and nothing but its own unsubscribe closes it — * which is the whole reason a superseded attempt has to be released rather than dropped. */ const resolveHung = (): void => { const resolve = hung.resolve; hung.resolve = null; hung.reject = null; hung.live = true; resolve?.(() => { hung.live = false; }); }; /** * The PREVIOUS session pushes over the channel it just handed back. * * Unconditionally, even once that channel has been released: a cancel does not reach back * and un-send what the broker already dispatched, which is precisely why the callback * carries a guard of its own instead of trusting the unsubscribe to have taken effect. */ const pushHung = (payload: unknown): void => { hung.cb?.(payload); }; /** Is the previous session's channel still open upstream? */ const hungIsLive = (): boolean => hung.live; return { doc_subscribe, push, isSubscribed, rejectHung, resolveHung, pushHung, hungIsLive, _subs: subs, }; } function inject(failFor?: Set, hangFor?: Set) { const ng = makeFakeNg(failFor, hangFor); 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(); }); /** * Two establishes over ONE fan-out, and the first one answering last. * * `resubscribeDocs` re-opens the channel of a fan-out whose first `doc_subscribe` has not * settled yet — that is the whole point of it, since the session it was opened against is * gone — and it re-opens it on the SAME entry so the listeners are kept. The two calls * therefore race, and the broker is under no obligation to answer them in order. */ async function hangingThenReopened(): Promise<{ ng: ReturnType; failures: unknown[]; seen: unknown[]; stop: Unsubscribe; }> { const hangFor = new Set([A]); const ng = inject(new Set(), hangFor); const failures: unknown[] = []; const seen: unknown[] = []; const stop = subscribeDocReportingSetupFailure( A, (r) => seen.push(r), (error) => failures.push(error), ); // Awaited before the broker is allowed to answer: `establish` resolves the session id // first, so the call this has to leave hanging has not been placed yet. await tick(); hangFor.delete(A); return { ng, failures, seen, stop }; } test("a SUPERSEDED setup rejecting late is not reported as this document failing", async () => { const { ng, failures, seen, stop } = await hangingThenReopened(); expect(ng.isSubscribed(A)).toBe(false); // the first call has not answered resubscribeDocs(); // the session rotated: a second establish, on the same fan-out await tick(); expect(ng.isSubscribed(A)).toBe(true); const before = seen.length; expect(before).toBeGreaterThan(0); // …and it is pushing ng.rejectHung(); // …and only now does the first call refuse await tick(); // Nothing failed: the document is subscribed and pushing. Told otherwise, the caller whose // job depends on this subscription (the inbox observation) releases the entry it holds — // which closes the WORKING channel and reports an inbox that is watched as unwatchable. expect(failures).toEqual([]); expect(ng.isSubscribed(A)).toBe(true); ng.push(A); expect(seen.length).toBeGreaterThan(before); stop(); }); test("a SUPERSEDED setup rejecting late does not let the next joiner evict the live channel", async () => { const { ng, stop } = await hangingThenReopened(); resubscribeDocs(); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(2); ng.rejectHung(); await tick(); // The second establish still stands, so this joiner must join it. Counting the late // rejection as "nothing is running any more" opens a THIRD `doc_subscribe` — and a second // subscribe on a branch evicts the one before it, so the joiner's own call is what silences // everybody already listening. const late: unknown[] = []; const stopLate = subscribeDoc(A, (r) => late.push(r)); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(2); expect(late.length).toBeGreaterThan(0); // replayed the barrier, as any late joiner is ng.push(A); expect(late.length).toBeGreaterThan(1); stopLate(); stop(); }); /** A `State` that could only have come from the session `resubscribeDocs` walked away from. */ const OLD_SESSION_STATE = { V0: { State: { doc: A, from: "the previous session" } } }; test("a SUPERSEDED setup that SUCCEEDS pushes to nobody, re-seeds nothing, and is released", async () => { const { ng, seen, stop } = await hangingThenReopened(); resubscribeDocs(); // the session rotated: a second establish, on the SAME fan-out await tick(); expect(ng.isSubscribed(A)).toBe(true); const afterReopen = seen.length; expect(afterReopen).toBeGreaterThan(0); // the current session is pushing // …and only now does the FIRST call answer — successfully, from the session that is gone. ng.resolveHung(); await tick(); ng.pushHung(OLD_SESSION_STATE); await tick(); // Its channel was released as it resolved: nothing else would ever close it, because it // sits on the previous session's verifier where the new subscription never displaced it. expect(ng.hungIsLive()).toBe(false); // Its push reaches nobody. The listeners are following the establish that replaced it, and // handing them a verifier nobody is talking to any more is the staleness, not the cure. expect(seen).not.toContainEqual(OLD_SESSION_STATE); expect(seen.length).toBe(afterReopen); // Nor is the abandoned `State` left as the barrier: a joiner is replayed the CURRENT // session's, which is what `resubscribeDocs` cleared `lastState` to guarantee. Replayed the // other one, a bootstrap open calls a repo this session has never synced "synced". const late: unknown[] = []; const stopLate = subscribeDoc(A, (r) => late.push(r)); await tick(); expect(late.length).toBeGreaterThan(0); expect(late).not.toContainEqual(OLD_SESSION_STATE); // And `realUnsub` is still the live call's, so the last listener out leaves NOTHING // subscribed upstream — neither session's channel. stopLate(); stop(); await tick(); expect(ng.isSubscribed(A)).toBe(false); expect(ng.hungIsLive()).toBe(false); }); test("a SUPERSEDED setup that FAILS is logged, even though it is reported to nobody", async () => { const { ng, failures, stop } = await hangingThenReopened(); resubscribeDocs(); await tick(); const logged: string[] = []; const realError = console.error; console.error = ((...a: unknown[]) => { logged.push(a.map(String).join(" ")); }) as typeof console.error; try { ng.rejectHung(); await tick(); } finally { console.error = realError; } // Told to nobody — the call that replaced it owns the outcome — but never passed over in // silence: a rejection that reaches no caller AND no log is a failure that never happened, // and this one is the only trace that the abandoned session refused at all. expect(failures).toEqual([]); expect(logged.some((l) => l.startsWith("[subscribe] doc_subscribe failed for"))).toBe(true); stop(); }); test("an establish SUPERSEDED while the session id resolves never reaches the broker at all", async () => { const ng = makeFakeNg(); configure({ ng: ng as unknown as Parameters[0]["ng"], useShape: (() => {}) as never }); // A session lookup this test holds open, so the supersession lands while the first // establish is between "started" and "placed its call" — the window `resubscribeDocs` // actually opens, since it fires from a `resetOpenedRepos` that a session read preceded. const gate: { release: (() => void) | null } = { release: null }; const held = new Promise((r) => { gate.release = r; }); configureStoreRegistry({ getSession: async () => { await held; return SESSION; }, }); const seen: unknown[] = []; const stop = subscribeDoc(A, (r) => seen.push(r)); await tick(); expect(ng.doc_subscribe).toHaveBeenCalledTimes(0); // still resolving the session resubscribeDocs(); // superseded before it ever placed its call gate.release?.(); await tick(); // ONE call, and it is the current attempt's. This is what makes "release a superseded // attempt" safe rather than a coin flip: every attempt that OWNS a channel placed its call // before the attempt replacing it existed, so the broker — which serialises the whole call // under one lock — served it first, and whoever inserts last holds the branch. The // superseded one is therefore always the evicted one, never the live channel. expect(ng.doc_subscribe).toHaveBeenCalledTimes(1); expect(ng.isSubscribed(A)).toBe(true); expect(seen.length).toBeGreaterThan(0); stop(); expect(ng.isSubscribed(A)).toBe(false); });