fix: la barriere n'est retenue que tant que la tentative tient
Trouve en enumerant moi-meme les ecritures de `establishing` et de `lastState` plutot qu'en croyant l'argument du commit precedent, qui affirmait que le garde retire etait inatteignable. Il ne l'etait pas. `isCurrentAttempt` dit que cette entree et cet appel sont encore les bons ; il ne dit rien sur le fait que l'appel ait REUSSI. Le courtier expedie avant de repondre, donc un `State` peut arriver apres que l'appel a deja ete rapporte en echec - et ce module tient lui-meme pour atteignable qu'une poussee survive a son demontage, c'est la raison d'etre du garde sur ce callback. Mesure : la poussee tardive recreait la barriere morte que `reportSetupFailure` venait d'oublier, sans canal derriere, et `getSyncState` y repondait `"synced"` - donc autorite. `lastState !== null` implique desormais qu'une tentative tient, a l'ecriture comme a l'effacement, donc les deux ne peuvent plus diverger et aucun lecteur n'a a le reimposer. La poussee reste diffusee aux auditeurs : seule la BARRIERE est une affirmation sur le document.
This commit is contained in:
@@ -513,8 +513,20 @@ async function establish(
|
|||||||
// this fan-out's listeners nor its barrier are this call's to touch any more.
|
// this fan-out's listeners nor its barrier are this call's to touch any more.
|
||||||
if (!isCurrentAttempt(nuri, entry, attempt)) return;
|
if (!isCurrentAttempt(nuri, entry, attempt)) return;
|
||||||
const type = docChangeType(resp);
|
const type = docChangeType(resp);
|
||||||
// Remember the barrier for whoever joins next; a later `State` replaces it.
|
// Remember the barrier for whoever joins next; a later `State` replaces it - but ONLY
|
||||||
if (type === "State") entry.lastState = { resp, type };
|
// while this attempt still stands. `isCurrentAttempt` above says this entry and this
|
||||||
|
// call are still the ones, and says nothing about whether the call SUCCEEDED: a broker
|
||||||
|
// dispatches before it answers, so a `State` can land after the call has already been
|
||||||
|
// reported as failed, and this module's own doctrine is that a push outliving its
|
||||||
|
// teardown is reachable (see the header, and the guard on this very callback). Recording
|
||||||
|
// it then re-created the dead barrier {@link reportSetupFailure} had just forgotten, with
|
||||||
|
// no channel behind it - and `getSyncState` answers `"synced"` to that, which is AUTHORITY.
|
||||||
|
//
|
||||||
|
// With this, `lastState !== null` IMPLIES an attempt stands, at every write as well as at
|
||||||
|
// every clear, so the two cannot disagree and no reader has to re-impose it. The push is
|
||||||
|
// still fanned out below: a listener may want the change signal, and only the BARRIER is
|
||||||
|
// a claim about the document.
|
||||||
|
if (type === "State" && entry.establishing) entry.lastState = { resp, type };
|
||||||
fanOut(nuri, entry, resp, type);
|
fanOut(nuri, entry, resp, type);
|
||||||
})) as (() => void) | undefined;
|
})) as (() => void) | undefined;
|
||||||
if (!isCurrentAttempt(nuri, entry, attempt)) {
|
if (!isCurrentAttempt(nuri, entry, attempt)) {
|
||||||
|
|||||||
@@ -788,3 +788,34 @@ test("a doc_subscribe that answers without a cancel function is still an OPEN ch
|
|||||||
expect(docChannel(A).barrierCrossed).toBe(true);
|
expect(docChannel(A).barrierCrossed).toBe(true);
|
||||||
unsub();
|
unsub();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A BARRIER BELONGS TO THE ATTEMPT THAT PRODUCED IT, and the attempt can fail after the push.
|
||||||
|
//
|
||||||
|
// The broker dispatches before it answers the call, so a `State` can land after `doc_subscribe`
|
||||||
|
// has already rejected - and this module's own doctrine is that a push outliving its teardown is
|
||||||
|
// reachable, which is why the callback carries a guard at all. Recorded as a barrier, it
|
||||||
|
// re-creates the dead one `reportSetupFailure` has just forgotten, with no channel behind it,
|
||||||
|
// and `emulated-verifier/open-repo.ts` answers `"synced"` to that - which is AUTHORITY, so a
|
||||||
|
// zero-row read over the document becomes a definitive absence and a deposit is destroyed.
|
||||||
|
test("a State that lands AFTER the call was reported failed is not a barrier", async () => {
|
||||||
|
let dispatch: ((r: unknown) => void) | null = null;
|
||||||
|
const doc_subscribe = mock(async (_n: string, _s: unknown, cb: (r: unknown) => void) => {
|
||||||
|
dispatch = cb; // the broker keeps the channel it was handed...
|
||||||
|
throw new Error("RepoNotFound: the call did not take"); // ...and then refuses the call
|
||||||
|
});
|
||||||
|
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("none");
|
||||||
|
expect(docChannel(A).barrierCrossed).toBe(false);
|
||||||
|
|
||||||
|
// A `State` the broker had already sent before it answered.
|
||||||
|
dispatch!({ V0: { State: {} } });
|
||||||
|
// `lastState !== null` must IMPLY an attempt stands - at the write as well as at the clear,
|
||||||
|
// or the two disagree and a reader is left to re-impose it.
|
||||||
|
expect(docChannel(A).barrierCrossed).toBe(false);
|
||||||
|
expect(docChannel(A).state).toBe("none");
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user