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:
@@ -63,6 +63,14 @@
|
||||
* fire, and `inbox.watch`'s "fires once immediately" would silently stop being true.
|
||||
* The real subscription is torn down when the LAST listener leaves.
|
||||
*
|
||||
* ── This module OWNS "is this document open in this session" ───────────────
|
||||
* Because the subscription is the only thing that makes a document open, the fan-out is the
|
||||
* only honest answer to whether one is - and {@link docChannel} is how the rest of the
|
||||
* package asks. Nobody keeps a copy: `emulated-verifier/open-repo.ts` kept two (a set of
|
||||
* live bootstrap subscriptions, and a per-document barrier state), and both outlived the
|
||||
* channel they described the moment `resetDocSubscriptions` dropped it, which is how a
|
||||
* document with no subscription at all came to report itself synced.
|
||||
*
|
||||
* ── What SHARING one subscription must not cost a caller ───────────────────
|
||||
* Three things a per-caller `doc_subscribe` gave for free, and that the fan-out has to give
|
||||
* back deliberately — each of them was lost when it was introduced on 2026-08-17:
|
||||
@@ -214,6 +222,63 @@ interface DocFanOut {
|
||||
*/
|
||||
const fanOuts = new Map<Nuri, DocFanOut>();
|
||||
|
||||
/**
|
||||
* What the one real subscription for a document IS, right now.
|
||||
*
|
||||
* - `"none"` nothing stands: no listener holds this document, or the establish that
|
||||
* was standing FAILED and the next joiner will make its own;
|
||||
* - `"opening"` an establish is in flight - the call has been made and has not answered,
|
||||
* so there is no channel yet and there may never be one;
|
||||
* - `"open"` the `doc_subscribe` call ANSWERED and this fan-out is still the one
|
||||
* waiting on it: pushes arrive here.
|
||||
*/
|
||||
// @provenance DocChannelState kind=invention level=none ref=none — names the states of this module's own fan-out; upstream a caller holds its own subscription and has nothing to ask
|
||||
export type DocChannelState = "none" | "opening" | "open";
|
||||
|
||||
/**
|
||||
* THE answer to "is this document open in this session", and the only one.
|
||||
*
|
||||
* Read it, never copy it. `emulated-verifier/open-repo.ts` used to keep records of its own
|
||||
* - a set of live bootstrap subscriptions and a per-document barrier state - and both
|
||||
* survived a teardown of the fan-out they described: `resetDocSubscriptions` drops every
|
||||
* listener without a word to anybody, so a document whose channel had gone was still
|
||||
* recorded as open and as SYNCED, nothing re-subscribed it, and a zero-row read over it
|
||||
* answered a success. Three records of one fact, and they disagreed. There is one now, it
|
||||
* is this one, and it cannot outlive the channel it describes because it IS the channel.
|
||||
*/
|
||||
export interface DocChannel {
|
||||
/** Whether a channel stands, and how far it got. */
|
||||
readonly state: DocChannelState;
|
||||
/**
|
||||
* Has a `State` push arrived on the channel standing NOW?
|
||||
*
|
||||
* The first `State` is the sync barrier (CONTRACT 3): past it, presence is guaranteed and
|
||||
* absence definitive, so a zero-row read MEANS zero. It is {@link DocFanOut.lastState}
|
||||
* that says so - the one the fan-out replays to a late joiner - which is exactly the right
|
||||
* record, because it is forgotten on the two events that invalidate a barrier: the
|
||||
* channel being released, and the session changing under it ({@link resubscribeDocs}).
|
||||
*/
|
||||
readonly barrierCrossed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of `nuri`'s one real subscription - see {@link DocChannel}.
|
||||
*
|
||||
* Internal. Exported for `emulated-verifier/open-repo.ts`, which asks it instead of
|
||||
* remembering the answer, and for nobody else; neither entry point re-exports it.
|
||||
*/
|
||||
// @provenance docChannel kind=invention level=none ref=none — a question about this module's own fan-out, which exists because a branch upstream holds a single subscriber; there is nothing to align it with
|
||||
export function docChannel(nuri: Nuri): DocChannel {
|
||||
const entry = fanOuts.get(nuri);
|
||||
if (!entry) return { state: "none", barrierCrossed: false };
|
||||
// `establishing` says an attempt STANDS (it is left true once one has succeeded, see
|
||||
// {@link DocFanOut.establishing}); `realUnsub` says that attempt's call has answered. The
|
||||
// two together are the only thing that separates "asked" from "open" - and recording the
|
||||
// first as if it were the second is the defect this accessor exists to make impossible.
|
||||
const state: DocChannelState = !entry.establishing ? "none" : entry.realUnsub !== null ? "open" : "opening";
|
||||
return { state, barrierCrossed: entry.lastState !== null };
|
||||
}
|
||||
|
||||
/** Hand one push to one listener, isolating a throwing handler from the others. */
|
||||
function deliver(nuri: Nuri, listener: DocListener, resp: DocChange, type: DocChangeType): void {
|
||||
try {
|
||||
@@ -420,7 +485,14 @@ async function establish(
|
||||
if (typeof unsub === "function") unsub();
|
||||
return;
|
||||
}
|
||||
entry.realUnsub = typeof unsub === "function" ? unsub : null;
|
||||
// A function is what upstream resolves to, and it is the cancel. When the injected SDK
|
||||
// hands back something else there is nothing to cancel WITH - but the call has still
|
||||
// ANSWERED, and that is what {@link docChannel} reads off this field. Left `null`, the
|
||||
// channel would read `"opening"` for the rest of the session and every `ensureRepoOpen`
|
||||
// would take out a fresh bootstrap listener over a document that is perfectly open. A
|
||||
// no-op costs exactly what `null` cost before: {@link releaseFanOut} returned early on
|
||||
// `null` rather than cancelling, so nothing is lost by calling nothing instead.
|
||||
entry.realUnsub = typeof unsub === "function" ? unsub : (): void => {};
|
||||
} catch (error) {
|
||||
reportSetupFailure(nuri, entry, error, attempt);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user