diff --git a/packages/polyfill/src/surface/use-shape-query.ts b/packages/polyfill/src/surface/use-shape-query.ts index 1545b41..0188c45 100644 --- a/packages/polyfill/src/surface/use-shape-query.ts +++ b/packages/polyfill/src/surface/use-shape-query.ts @@ -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 * 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. + * + * `""` 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[] { const out: Nuri[] = []; diff --git a/packages/polyfill/test/use-shape-query.test.ts b/packages/polyfill/test/use-shape-query.test.ts index 7996775..0d96347 100644 --- a/packages/polyfill/test/use-shape-query.test.ts +++ b/packages/polyfill/test/use-shape-query.test.ts @@ -609,6 +609,54 @@ describe("useShapeQuery", () => { 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 ------------------------------------------------------------ it("(k) the last unsubscribe closes the shape query", async () => {