fix: un abonnement en échec n'empoisonne plus le document, et un dépôt ne change plus de destinataire
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { test, expect, mock, afterAll } from "bun:test";
|
||||
import { docChangeType, subscribeDoc, subscribeDocs } from "../src/surface/subscribe";
|
||||
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";
|
||||
@@ -225,3 +231,93 @@ test("the real subscription is released only when the LAST subscriber leaves", a
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user