feat!: une seule reponse a « ce document est-il ouvert dans cette session »
Trois reponses coexistaient et pouvaient se contredire : un ensemble de souscriptions bootstrap et une carte d'etat de barriere dans `open-repo`, et les auditeurs du fan-out dans `surface/subscribe`. Les deux premieres survivaient a la disparition de la troisieme, parce que `resetDocSubscriptions()` - ce que fait le `configure()` publie - largue ses auditeurs sans rien dire a personne. Il n'en reste qu'une, l'etat de l'unique souscription reelle du document, exposee par `docChannel(nuri)`. `open-repo` n'enregistre plus que l'auditeur qu'il a pose - « est-ce que j'ecoute encore », rien de plus - et derive `getSyncState` a chaque lecture, si bien qu'aucune reponse ne peut survivre au canal qu'elle decrit. Disparaissent : `held`, `syncState`, `inFlight`, `settleInFlight` et le compteur de generation, remplace par l'identite de l'enregistrement. Les deux echecs prouves deviennent des tests qui echouent sans le changement : un second `configure()` apres une synchro reussie (`"synced"` au lieu de `"timed-out"`, zero re-souscription, et `isSuccess` cote application), et un etablissement encore en cours quand la couche de souscription est demontee - cas que le canal `onSetupFailed` ne peut pas rattraper, puisqu'il ne rapporte que la tentative encore attendue. La ligne qui ne casse pas : un document synchronise qui ne contient rien repond toujours un succes a zero objet. `1.0.0-dev.10`, les deux non-garanties nommees sont retirees et la garantie livree porte le test qui l'epingle.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { test, expect, mock, afterAll } from "bun:test";
|
||||
import {
|
||||
docChangeType,
|
||||
docChannel,
|
||||
resetDocSubscriptions,
|
||||
resubscribeDocs,
|
||||
subscribeDoc,
|
||||
subscribeDocReportingSetupFailure,
|
||||
@@ -712,3 +714,77 @@ test("served in the INVERTED order, the document goes silently dead — nothing
|
||||
alsoStop();
|
||||
stop();
|
||||
});
|
||||
|
||||
// -- the ONE answer to "is this document open in this session" ---------------
|
||||
//
|
||||
// `docChannel` is that answer, and the whole of what it has to get right is telling an
|
||||
// UNANSWERED call from an open channel. Every record of openness this package has kept
|
||||
// elsewhere got that wrong in the same way: written when the subscribe was REQUESTED, so it
|
||||
// meant "we asked". `emulated-verifier/open-repo.ts` used to keep two of them and they
|
||||
// outlived the subscription they described; it asks this instead.
|
||||
|
||||
test("docChannel says OPENING while the call is unanswered, OPEN once it is served", async () => {
|
||||
// Accepted and not yet SERVED: the call has been placed and the broker has said nothing.
|
||||
const ng = inject(undefined, undefined, new Set([A]));
|
||||
expect(docChannel(A).state).toBe("none"); // nobody is listening yet
|
||||
const unsub = subscribeDoc(A, () => {});
|
||||
await tick();
|
||||
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
||||
// The call is out and there is no channel. Reading this as `"open"` is the defect: a caller
|
||||
// skips the work it still has to do, and a document that may never be subscribed is treated
|
||||
// as one that is.
|
||||
expect(docChannel(A).state).toBe("opening");
|
||||
expect(docChannel(A).barrierCrossed).toBe(false);
|
||||
|
||||
ng.serveQueued(0);
|
||||
await tick();
|
||||
expect(docChannel(A).state).toBe("open");
|
||||
// ...and the barrier is the fan-out's own record of the first `State`, so it comes with it.
|
||||
expect(docChannel(A).barrierCrossed).toBe(true);
|
||||
|
||||
unsub();
|
||||
expect(docChannel(A).state).toBe("none");
|
||||
});
|
||||
|
||||
test("docChannel says NONE for a call the broker refused, and for a torn-down fan-out", async () => {
|
||||
const ng = inject(new Set([B]));
|
||||
const unsub = subscribeDoc(B, () => {});
|
||||
await tick();
|
||||
expect(ng.doc_subscribe).toHaveBeenCalledTimes(1);
|
||||
// Refused: nothing stands, and the next joiner must be free to make its own attempt.
|
||||
expect(docChannel(B).state).toBe("none");
|
||||
expect(docChannel(B).barrierCrossed).toBe(false);
|
||||
unsub();
|
||||
|
||||
// A fan-out dropped from under its listeners - what `resetDocSubscriptions` does, which
|
||||
// `configure()` calls. The answer follows the channel because it IS the channel.
|
||||
const ng2 = inject();
|
||||
const live = subscribeDoc(A, () => {});
|
||||
await tick();
|
||||
expect(docChannel(A).state).toBe("open");
|
||||
expect(ng2.doc_subscribe).toHaveBeenCalledTimes(1);
|
||||
resetDocSubscriptions();
|
||||
expect(docChannel(A).state).toBe("none");
|
||||
expect(docChannel(A).barrierCrossed).toBe(false);
|
||||
live();
|
||||
});
|
||||
|
||||
// An injected SDK that answers with something other than a cancel function still ANSWERED, and
|
||||
// the channel is open. Read as "not open", every bootstrap open over that document would take
|
||||
// out a fresh listener and wait the barrier again, for the rest of the session - over a
|
||||
// document that is perfectly subscribed. Upstream resolves to a function; a consumer's own
|
||||
// shim need not, and this is the one place that difference could be mistaken for a failure.
|
||||
test("a doc_subscribe that answers without a cancel function is still an OPEN channel", async () => {
|
||||
const doc_subscribe = mock(async (_n: string, _s: unknown, cb: (r: unknown) => void) => {
|
||||
queueMicrotask(() => cb({ V0: { State: {} } }));
|
||||
return undefined; // no cancel to hand back
|
||||
});
|
||||
configure({ ng: { doc_subscribe } as any, useShape: (() => {}) as any });
|
||||
configureStoreRegistry({ getSession: async () => SESSION });
|
||||
const unsub = subscribeDoc(A, () => {});
|
||||
await tick();
|
||||
expect(doc_subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(docChannel(A).state).toBe("open");
|
||||
expect(docChannel(A).barrierCrossed).toBe(true);
|
||||
unsub();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user