fix: un State tardif EST la barriere, donc il est enregistre quand il arrive

En relisant ce que je venais d'ecrire dans le contrat - « si le document se
synchronise plus tard et apporte des donnees, la lecture repond d'elle-meme » -
j'ai constate que c'etait faux. `onState` sortait aussitot des lors que le repli
borne avait deja termine l'ouverture, donc un `State` tardif etait jete :
`"timed-out"` restait indefiniment sur un depot desormais synchronise, et le
lecteur qui consulte cet etat ne pouvait jamais l'apprendre.

Le repli veut dire « cette ouverture a cesse d'attendre », jamais « la barriere ne
sera pas franchie » : l'abonnement n'est pas ferme, donc le `State` arrive encore.
`onState` enregistre desormais `"synced"` avant le controle. `"timed-out"` ->
`"synced"` est la seule transition ajoutee, et elle va de moins d'autorite vers
plus ; les deux lecteurs de cette valeur la veulent ainsi.

(dd) l'epingle : elle tombe si l'enregistrement repasse sous le controle.
This commit is contained in:
Sylvain Duchesne
2026-09-24 11:09:02 +02:00
parent 4af0be433d
commit 2e5bc100b9
2 changed files with 42 additions and 1 deletions
@@ -185,6 +185,12 @@ function makeFake(options: { pushState?: boolean; subscribe?: boolean } = {}) {
// is withheld in `inject` rather than here, so the two suites that count subscribes
// keep a mock to count.
_noSubscribe: options.subscribe === false,
/** The broker catching up: push the first `State` to a subscription already open on
* `nuri`, late. A subscription is never dropped by the bounded fallback, so this is
* what a slow repo really does. */
_pushStateNow(nuri: string): void {
for (const sub of subs) if (sub.nuri === nuri) sub.cb({ V0: { State: {} } });
},
};
}
@@ -1135,6 +1141,33 @@ describe("useShapeQuery", () => {
unsub();
});
it("(dd) a `State` that lands AFTER the fallback answers the read, with no call from the app", async () => {
const ng = makeFake({ pushState: false });
const orm = makeOrm();
inject(ng, orm);
setOpenTimeoutForTests(30);
setCurrentUser("uma");
const mine = await createEntityDoc("uma", "protected");
const obs = useShapeQuery(EventShape, { graphs: [mine] });
const unsub = obs.subscribe(() => {});
for (let i = 0; i < 40 && getSyncState(mine) !== "timed-out"; i++) await tick();
expect(getSyncState(mine)).toBe("timed-out");
expect(obs.getSnapshot().isSuccess).toBe(false); // no authority yet, so no answer
// The broker catches up: the subscription was never dropped, so the first `State`
// still lands - late, but it IS the barrier - and it brings the document's data, which
// is what makes the ORM republish (`orm-fake.seed` pushes once delivery has happened,
// as an engine patch reaching the signal object does). Nothing here calls `refetch`.
ng._pushStateNow(mine);
seedEvent(orm, mine, `${mine}:q:one`, "Late");
for (let i = 0; i < 40 && !obs.getSnapshot().isSuccess; i++) await tick();
expect(getSyncState(mine)).toBe("synced");
expect(titles(obs.getSnapshot().data)).toEqual(["Late"]);
unsub();
});
// -- teardown ------------------------------------------------------------
it("(k) the last unsubscribe closes the shape query", async () => {