Files
ng-eventually/packages/polyfill/test/subscribe.test.ts

715 lines
29 KiB
TypeScript

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<BranchId, Sender<AppResponse>>`) 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<string> = new Set(),
hangFor: Set<string> = new Set(),
queueFor: Set<string> = new Set(),
) {
const subs = new Map<string, (r: unknown) => 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 };
/**
* The broker's side of ONE `doc_subscribe`, at the moment it is SERVED — taking the branch
* over for `cb` and handing back the cancel.
*
* Apart from the call because ACCEPTING a call and SERVING it are two moments, and the
* broker does not promise they happen in the same order (see {@link queued}). Everything
* that decides who holds the branch is here, so serving is one function call and a test can
* make it happen when it likes.
*/
const serveBranch = (nuri: string, cb: (r: unknown) => void): (() => void) => {
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);
};
};
/**
* Calls the broker has ACCEPTED and not yet SERVED, in the order they were issued.
*
* `subs.set` at call time would hardwire "served in the order issued", which is not
* something the platform offers: the whole call is serialised under one
* `async_std::sync::RwLock` (`sdk/rust/src/local_broker.rs:3057`) — but that IS
* `async_lock::RwLock` (async-std 1.13.2 `src/sync/mod.rs:181`), whose internal mutex is
* documented as "eventual fairness", explicitly not FIFO (async-lock 3.4.1
* `src/mutex.rs:22-24`), and whose anti-starvation fallback is compiled out on wasm
* (`src/mutex.rs:578-581`) — which is where this SDK runs. A fake that can only serve in
* issue order cannot fail on the state that assumption is wrong about.
*/
const queued: Array<() => void> = [];
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;
});
}
if (queueFor.has(nuri)) {
return await new Promise<() => void>((resolve) => {
queued.push(() => resolve(serveBranch(nuri, cb)));
});
}
return serveBranch(nuri, cb);
});
/**
* Serve the accepted calls, in the order given — `serveQueued(1, 0)` serves the SECOND
* call first, which is the inversion the module's release-the-loser reasoning bets against.
*/
const serveQueued = (...order: number[]): void => {
for (const i of order) {
const serve = queued[i];
if (!serve) throw new Error(`no call number ${i} was accepted (${queued.length} were)`);
serve();
}
};
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,
serveQueued,
_subs: subs,
};
}
function inject(failFor?: Set<string>, hangFor?: Set<string>, queueFor?: Set<string>) {
const ng = makeFakeNg(failFor, hangFor, queueFor);
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<typeof makeFakeNg>;
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<typeof configure>[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<void>((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. What that buys is exact and worth stating
// exactly: an establish superseded in THIS window opens nothing, so there is no channel of
// its own to release and no second call for the broker to order. It does not make releasing
// a superseded attempt safe in general — that rests on a bet about SERVICE order, which the
// two tests below take apart.
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
expect(ng.isSubscribed(A)).toBe(true);
expect(seen.length).toBeGreaterThan(0);
stop();
expect(ng.isSubscribed(A)).toBe(false);
});
/**
* Two attempts on ONE branch, and the broker free to serve them in either order.
*
* `resubscribeDocs` re-opens a fan-out whose first establish has not settled, and the last
* listener leaving mid-establish then re-joining does the same against the SAME session — so
* two calls contending for one branch is a state this module reaches. Which of them ends up
* holding the branch is the broker's to decide, not this module's: the call is serialised
* under one `async_std::sync::RwLock` (`sdk/rust/src/local_broker.rs:3057`), and serialised
* is not ordered. That lock IS `async_lock::RwLock` (async-std 1.13.2 `src/sync/mod.rs:181`),
* `write()` takes its internal `async_lock::Mutex` first (async-lock 3.4.1
* `src/rwlock/raw.rs:163-168`), and that mutex is documented as "eventual fairness" and
* explicitly not FIFO (`src/mutex.rs:22-24`) — with the anti-starvation fallback that would
* eventually force fairness compiled out on wasm (`src/mutex.rs:578-581`), which is where
* this SDK runs.
*
* Both orders are therefore real, and they do not end the same way. The fake serves on
* demand (`serveQueued`) rather than at call time precisely so both can be exercised: a fake
* that inserts in call order asserts the happy one into existence.
*/
async function twoAttemptsOnOneBranch(): Promise<{
ng: ReturnType<typeof makeFakeNg>;
seen: unknown[];
failures: unknown[];
stop: Unsubscribe;
}> {
const ng = inject(undefined, undefined, new Set([A]));
const seen: unknown[] = [];
const failures: unknown[] = [];
const stop = subscribeDocReportingSetupFailure(
A,
(r) => seen.push(r),
(e) => failures.push(e),
);
await tick(); // the first attempt has PLACED its call — the broker has it, unserved
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
resubscribeDocs(); // …and is superseded, after the call, not before it
await tick();
expect(ng.doc_subscribe).toHaveBeenCalledTimes(2);
return { ng, seen, failures, stop };
}
test("served in the order issued, the superseded attempt is the one the broker evicted", async () => {
const { ng, seen, failures, stop } = await twoAttemptsOnOneBranch();
ng.serveQueued(0, 1); // the first call first — the order the module bets on
await tick();
// The current attempt inserted last, so it holds the branch; the superseded one released a
// channel that had already been displaced. The document is live and pushing.
expect(ng.isSubscribed(A)).toBe(true);
const before = seen.length;
ng.push(A);
expect(seen.length).toBe(before + 1);
expect(failures).toEqual([]);
stop();
expect(ng.isSubscribed(A)).toBe(false);
});
test("served in the INVERTED order, the document goes silently dead — nothing rejects, nothing logs", async () => {
const { ng, seen, failures, stop } = await twoAttemptsOnOneBranch();
const logged: string[] = [];
const realError = console.error;
console.error = ((...a: unknown[]) => {
logged.push(a.map(String).join(" "));
}) as typeof console.error;
try {
ng.serveQueued(1, 0); // the SECOND call served first — the bet, broken
await tick();
} finally {
console.error = realError;
}
// The superseded call inserted LAST, so it took the branch and closed the current
// attempt's sender (`engine/verifier/src/verifier.rs:361`); this module then released the
// channel that call had opened, because the attempt behind it is no longer current. What
// upstream is left holding is a closed sender, which `push_app_response` drops at the first
// push (`verifier.rs:258-261`) — here, a branch with nobody on it.
expect(ng.isSubscribed(A)).toBe(false);
const before = seen.length;
ng.push(A);
expect(seen.length).toBe(before); // the listener is subscribed and hears nothing
// And nothing anywhere says so. No rejection reaches the caller that ASKED to be told, and
// the log is clean: every call resolved, so there was no failure to report.
expect(failures).toEqual([]);
expect(logged).toEqual([]);
// The fan-out believes itself subscribed, which is what makes this last the session: it
// holds a `realUnsub` and `establishing` never went back to false, so the next joiner is
// handed the dead entry instead of opening a channel of its own.
const alsoStop = subscribeDoc(A, () => {});
await tick();
expect(ng.doc_subscribe).toHaveBeenCalledTimes(2); // no third call — nobody re-opens it
alsoStop();
stop();
});