test: les deux cas qui ne doivent pas bouger, et la course que la barriere asynchrone ouvre

(p) `graphs: ""` est l'autre orthographe du dataset entier, jamais un nom de
document : le filtre reste seul a decider. C'est une garde, pas un test
discriminant - `isNuri` ecarte deja la chaine vide -, et elle epingle le
comportement plutot que le chemin.

(q) se desabonner pendant que la barriere est en train d'etre franchie ne doit
laisser aucune requete ouverte. La barriere rend l'ouverture asynchrone, donc il
existe desormais une fenetre ou l'observable a des abonnes, pas de requete, et une
ouverture en chemin ; verifie en retirant le controle de generation, ou la
requete est creee apres le dernier auditeur et plus rien ne la ferme.
This commit is contained in:
Sylvain Duchesne
2026-09-21 15:12:09 +02:00
parent b0eec7b4dc
commit 534d8f91b7
2 changed files with 52 additions and 0 deletions
@@ -186,6 +186,10 @@ const WHOLE_DATASET = "did:ng:i";
* none, and so is anything that is not a NextGraph reference - an ordinary IRI names no * none, and so is anything that is not a NextGraph reference - an ordinary IRI names no
* repo this library could hold a cap for, and the engine is left to match it or not. * repo this library could hold a cap for, and the engine is left to match it or not.
* Deduplicated, so a scope repeating a document opens it once. * Deduplicated, so a scope repeating a document opens it once.
*
* `""` is named beside the marker although `isNuri` would drop it anyway. It is the SDK's
* own alias for `did:ng:i`, and a reader should be able to see that here rather than have
* to work out that a guard two files away happens to exclude it.
*/ */
function namedDocuments(scope: Scope): Nuri[] { function namedDocuments(scope: Scope): Nuri[] {
const out: Nuri[] = []; const out: Nuri[] = [];
@@ -609,6 +609,54 @@ describe("useShapeQuery", () => {
unsub(); unsub();
}); });
it("(p) the empty-string `graphs` is the whole dataset too, and is not guarded either", async () => {
const ng = makeFake();
const orm = makeOrm();
inject(ng, orm);
setCurrentUser("omar");
const mine = await createEntityDoc("omar", "protected");
seedEvent(orm, mine, `${mine}:q:one`, "Mine");
// Somebody else's, in the same local dataset: the filter is what keeps it out, and
// must stay the one that decides here - `""` names no document to refuse.
seedEvent(orm, "did:ng:o:not-omars", "did:ng:o:not-omars:q:one", "Theirs");
// `normalizeScope` maps `""` onto `did:ng:i` (`@ng-org/orm`'s `types.ts`), so the two
// spellings are one marker. A barrier that treated `""` as a document name would
// refuse the whole dataset for a reference nobody wrote.
const obs = useShapeQuery(EventShape, { graphs: "" });
const unsub = obs.subscribe(() => {});
await tick();
await tick();
const snap = obs.getSnapshot();
expect(snap.isError).toBe(false);
expect(snap.isSuccess).toBe(true);
expect(titles(snap.data)).toEqual(["Mine"]);
unsub();
});
it("(q) unsubscribing while the barrier is still crossing leaves no query open", async () => {
const ng = makeFake();
const orm = makeOrm();
inject(ng, orm);
setCurrentUser("pia");
const mine = await createEntityDoc("pia", "protected");
seedEvent(orm, mine, `${mine}:q:one`, "Mine");
// The barrier is asynchronous now, so there is a window in which the observable has
// subscribers, no query, and an open on its way. A teardown landing in that window
// must not be overtaken by the open it superseded - otherwise the shape query is
// created after the last listener left and nothing is ever going to close it.
const obs = useShapeQuery(EventShape, { graphs: [mine] });
const unsub = obs.subscribe(() => {});
unsub(); // synchronously, while `ensureRepoOpen` is still awaiting the first `State`
await tick();
await tick();
await tick();
expect(orm.openCount()).toBe(0);
});
// -- teardown ------------------------------------------------------------ // -- teardown ------------------------------------------------------------
it("(k) the last unsubscribe closes the shape query", async () => { it("(k) the last unsubscribe closes the shape query", async () => {