fix: un rejet tardif ne ferme plus un canal vivant, un registre illisible ne perd plus toutes les inbox
This commit is contained in:
@@ -175,12 +175,26 @@ interface DocFanOut {
|
||||
* It therefore stays true once the setup has SUCCEEDED: the attempt still stands.
|
||||
*
|
||||
* Cleared on the two ways the attempt stops standing: the last listener leaving
|
||||
* ({@link releaseFanOut}), and a setup that FAILED ({@link reportSetupFailure}). Left true
|
||||
* on failure it stopped meaning "one is already running" and started meaning "this NURI is
|
||||
* finished" — no later joiner ever attempted it again, for the whole session, over one
|
||||
* ({@link releaseFanOut}), and the CURRENT setup FAILING ({@link reportSetupFailure}). Left
|
||||
* true on failure it stopped meaning "one is already running" and started meaning "this NURI
|
||||
* is finished" — no later joiner ever attempted it again, for the whole session, over one
|
||||
* transient rejection.
|
||||
*/
|
||||
establishing: boolean;
|
||||
/**
|
||||
* WHICH establish this fan-out is waiting on — bumped by {@link beginEstablish} every time
|
||||
* one is kicked off, and captured by that call for the whole of its life.
|
||||
*
|
||||
* Two establishes can be in flight over the SAME entry: {@link resubscribeDocs} re-opens the
|
||||
* channel of a fan-out whose first attempt has not settled yet, and it re-opens it on the
|
||||
* entry rather than on a fresh one, precisely so the listeners are kept. So `entry` identity
|
||||
* — the only currency this module had — answers "is this fan-out still the one for this
|
||||
* NURI", and cannot answer "is this attempt still the one this fan-out is waiting on". A
|
||||
* counter can, and that second question is the one a LATE outcome has to ask: the first
|
||||
* attempt rejecting long after the second SUCCEEDED is not this document failing, it is one
|
||||
* superseded call finally answering.
|
||||
*/
|
||||
attempt: number;
|
||||
/**
|
||||
* The most recent `State` push, replayed to a listener that joins later.
|
||||
* Its own `doc_subscribe` would have pushed one; the fan-out owes it the same.
|
||||
@@ -216,6 +230,25 @@ function fanOut(nuri: Nuri, entry: DocFanOut, resp: DocChange, type: DocChangeTy
|
||||
}
|
||||
}
|
||||
|
||||
/** Is `attempt` still the establish this fan-out is waiting on? */
|
||||
function isCurrentAttempt(nuri: Nuri, entry: DocFanOut, attempt: number): boolean {
|
||||
return fanOuts.get(nuri) === entry && entry.attempt === attempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off the one real subscription for `entry` — the ONLY way an establish is started.
|
||||
*
|
||||
* It stamps the attempt (see {@link DocFanOut.attempt}) as it starts it, so an outcome that
|
||||
* arrives late can tell whether it is still the one being waited on. Being the only door is
|
||||
* what makes that true: a second call site that forgot to bump the counter would hand its
|
||||
* establish the number of the one it just superseded.
|
||||
*/
|
||||
function beginEstablish(nuri: Nuri, entry: DocFanOut, ng: { doc_subscribe?: unknown }): void {
|
||||
entry.establishing = true;
|
||||
entry.attempt += 1;
|
||||
void establish(nuri, entry, ng, entry.attempt);
|
||||
}
|
||||
|
||||
/**
|
||||
* The one real subscription could not be OPENED: forget the attempt, and say so.
|
||||
*
|
||||
@@ -226,11 +259,24 @@ function fanOut(nuri: Nuri, entry: DocFanOut, resp: DocChange, type: DocChangeTy
|
||||
* **Visible** — the log always, plus the callers who asked to be told, so a caller whose job
|
||||
* depends on the subscription (the inbox observation) can report it and try again rather
|
||||
* than sit on a dead entry.
|
||||
*
|
||||
* ── Only for the attempt still being WAITED ON ─────────────────────────────
|
||||
* Both halves act on the fan-out as it stands NOW, so a SUPERSEDED attempt must do neither.
|
||||
* A rejection that arrives after {@link resubscribeDocs} has already re-opened the channel is
|
||||
* not this document failing — the channel is open and pushing. Told anyway, the inbox
|
||||
* observation dropped its entry and released it, which closed the WORKING subscription,
|
||||
* printed "this inbox could not be watched" about an inbox that was, and left the identity
|
||||
* with a silent queue. Clearing `establishing` was the same mistake one level down: it says
|
||||
* "nothing is running" while the second attempt is, so the next joiner opens a third — and a
|
||||
* second `doc_subscribe` on a branch EVICTS the one before it.
|
||||
*
|
||||
* Superseded, it is therefore LOGGED and nothing else: the call that replaced it owns the
|
||||
* outcome, and reports its own failure if it has one.
|
||||
*/
|
||||
function reportSetupFailure(nuri: Nuri, entry: DocFanOut, error: unknown): void {
|
||||
function reportSetupFailure(nuri: Nuri, entry: DocFanOut, error: unknown, attempt: number): void {
|
||||
console.error("[subscribe] doc_subscribe failed for", nuri, error);
|
||||
if (!isCurrentAttempt(nuri, entry, attempt)) return;
|
||||
entry.establishing = false;
|
||||
if (fanOuts.get(nuri) !== entry) return;
|
||||
for (const listener of [...entry.listeners]) {
|
||||
const tell = listener.onSetupFailed;
|
||||
if (tell === null || !entry.listeners.has(listener)) continue;
|
||||
@@ -262,7 +308,12 @@ function releaseFanOut(nuri: Nuri, entry: DocFanOut): void {
|
||||
* Errors are isolated to this document (they never reject a shared batch — see
|
||||
* {@link subscribeDocs}); a failed setup simply leaves the document silent.
|
||||
*/
|
||||
async function establish(nuri: Nuri, entry: DocFanOut, ng: { doc_subscribe?: unknown }): Promise<void> {
|
||||
async function establish(
|
||||
nuri: Nuri,
|
||||
entry: DocFanOut,
|
||||
ng: { doc_subscribe?: unknown },
|
||||
attempt: number,
|
||||
): Promise<void> {
|
||||
// No reactive primitive on the injected `ng` (the fake in the unit suite): there is
|
||||
// nothing to call, so this document simply never pushes — the same documented no-op
|
||||
// `openRepoUnguarded` takes for the same injection, and not a failure to report. A real
|
||||
@@ -287,7 +338,7 @@ async function establish(nuri: Nuri, entry: DocFanOut, ng: { doc_subscribe?: unk
|
||||
}
|
||||
entry.realUnsub = typeof unsub === "function" ? unsub : null;
|
||||
} catch (error) {
|
||||
reportSetupFailure(nuri, entry, error);
|
||||
reportSetupFailure(nuri, entry, error, attempt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +396,7 @@ export function resubscribeDocs(): void {
|
||||
console.error("[subscribe] unsubscribe failed for", nuri, error);
|
||||
}
|
||||
}
|
||||
entry.establishing = true;
|
||||
void establish(nuri, entry, ng);
|
||||
beginEstablish(nuri, entry, ng);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +472,13 @@ export function subscribeDocUnguarded(
|
||||
|
||||
let entry = fanOuts.get(nuri);
|
||||
if (!entry) {
|
||||
entry = { listeners: new Set(), realUnsub: null, establishing: false, lastState: null };
|
||||
entry = {
|
||||
listeners: new Set(),
|
||||
realUnsub: null,
|
||||
establishing: false,
|
||||
attempt: 0,
|
||||
lastState: null,
|
||||
};
|
||||
fanOuts.set(nuri, entry);
|
||||
}
|
||||
const joined = entry;
|
||||
@@ -443,8 +499,7 @@ export function subscribeDocUnguarded(
|
||||
}
|
||||
|
||||
if (!joined.establishing) {
|
||||
joined.establishing = true;
|
||||
void establish(nuri, joined, ng as { doc_subscribe?: unknown });
|
||||
beginEstablish(nuri, joined, ng as { doc_subscribe?: unknown });
|
||||
}
|
||||
|
||||
let stopped = false;
|
||||
|
||||
Reference in New Issue
Block a user