feat!: useShapeQuery prend le Scope du SDK, et un store se nomme par sa reference
This commit is contained in:
@@ -15,7 +15,11 @@ import {
|
||||
resetStoreRegistry,
|
||||
setCurrentUser,
|
||||
} from "../src/shared-wallet/bootstrap";
|
||||
import { createEntityDoc as registryCreateEntityDoc, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import {
|
||||
createEntityDoc as registryCreateEntityDoc,
|
||||
listMyEntityDocs as registryListMyEntityDocs,
|
||||
resetRegistryCache,
|
||||
} from "../src/shared-wallet/account-registry";
|
||||
import { share } from "../src/surface/inbox";
|
||||
|
||||
const SHIM = "urn:ng-eventually:shim";
|
||||
@@ -126,18 +130,30 @@ test("ensureIdentity returns the identity it settled", async () => {
|
||||
test("a placement call before signing in names the mistake", async () => {
|
||||
inject();
|
||||
setCurrentUser(null);
|
||||
await expect(storeRegistry.createEntityDoc("protected")).rejects.toThrow(/ensureIdentity/i);
|
||||
await expect(storeRegistry.listMyEntityDocs("protected")).rejects.toThrow(/no identity/i);
|
||||
await expect(storeRegistry.createEntityDoc()).rejects.toThrow(/ensureIdentity/i);
|
||||
await expect(storeRegistry.protectedStore()).rejects.toThrow(/no identity/i);
|
||||
});
|
||||
|
||||
test("placement acts as the connected user, with nothing passed", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
expect(await storeRegistry.listMyEntityDocs("protected")).toContain(doc);
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
expect(await registryListMyEntityDocs("alice", "protected")).toContain(doc);
|
||||
|
||||
setCurrentUser("bob");
|
||||
expect(await storeRegistry.listMyEntityDocs("protected")).not.toContain(doc);
|
||||
expect(await registryListMyEntityDocs("bob", "protected")).not.toContain(doc);
|
||||
});
|
||||
|
||||
// A store reference that is not one of the connected identity's three is a caller error,
|
||||
// and silence here would be the worst answer available: the document would be created,
|
||||
// successfully, somewhere its author did not ask for — and only a later listing that comes
|
||||
// back short would say so.
|
||||
test("createEntityDoc refuses a store that is not one of this identity's", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const hers = await storeRegistry.protectedStore();
|
||||
setCurrentUser("bob");
|
||||
await expect(storeRegistry.createEntityDoc(hers)).rejects.toThrow(/not one of this identity/i);
|
||||
});
|
||||
|
||||
// ── sharing names someone who exists ───────────────────────────────────────
|
||||
@@ -145,7 +161,7 @@ test("placement acts as the connected user, with nothing passed", async () => {
|
||||
test("share refuses a recipient nobody has signed in as, instead of creating them", async () => {
|
||||
inject();
|
||||
setCurrentUser("alice");
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
// "bpb" is a typo for "bob". It used to mint that name's three stores and an inbox,
|
||||
// and the cap landed where nobody will ever look — with no error at all.
|
||||
await expect(share(doc, "bpb")).rejects.toThrow(/no such recipient/i);
|
||||
@@ -157,7 +173,7 @@ test("share refuses a recipient nobody has signed in as, instead of creating the
|
||||
test("a failed lookup surfaces as a failure, not as 'no such recipient'", async () => {
|
||||
const { quads } = inject();
|
||||
setCurrentUser("alice");
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
setCurrentUser("bob");
|
||||
await registryCreateEntityDoc("bob", "private"); // bob genuinely exists
|
||||
setCurrentUser("alice");
|
||||
@@ -178,7 +194,7 @@ test("a failed lookup surfaces as a failure, not as 'no such recipient'", async
|
||||
test("createEntityDoc reports a half-written document instead of returning its reference", async () => {
|
||||
inject(/shim:contains/); // the LISTING write fails
|
||||
setCurrentUser("alice");
|
||||
await expect(storeRegistry.createEntityDoc("protected")).rejects.toThrow(/not listed in its store/i);
|
||||
await expect(storeRegistry.createEntityDoc(await storeRegistry.protectedStore())).rejects.toThrow(/not listed in its store/i);
|
||||
});
|
||||
|
||||
// Both writes are attempted before the failure is raised — the cap must land even when
|
||||
@@ -187,14 +203,14 @@ test("createEntityDoc reports a half-written document instead of returning its r
|
||||
test("a failed listing does not cost the document its key", async () => {
|
||||
const { quads } = inject(/shim:contains/);
|
||||
setCurrentUser("alice");
|
||||
await storeRegistry.createEntityDoc("protected").catch(() => {});
|
||||
await storeRegistry.createEntityDoc(await storeRegistry.protectedStore()).catch(() => {});
|
||||
expect(quads.some((q) => q.p === `${SHIM}:readCap`)).toBe(true);
|
||||
});
|
||||
|
||||
test("a failed key write is reported too, and names that half", async () => {
|
||||
inject(/shim:readCap/);
|
||||
setCurrentUser("alice");
|
||||
await expect(storeRegistry.createEntityDoc("protected")).rejects.toThrow(/key is not recorded/i);
|
||||
await expect(storeRegistry.createEntityDoc(await storeRegistry.protectedStore())).rejects.toThrow(/key is not recorded/i);
|
||||
});
|
||||
|
||||
// The third write a PUBLIC creation makes, and the one that was still silent: exposing the
|
||||
@@ -206,6 +222,6 @@ test("a failed key write is reported too, and names that half", async () => {
|
||||
test("a public document whose cap could not be exposed is reported, not returned", async () => {
|
||||
inject(/shim:exposedReadCap/);
|
||||
setCurrentUser("alice");
|
||||
await expect(storeRegistry.createEntityDoc("public")).rejects.toThrow(/broker refused/i);
|
||||
await expect(storeRegistry.createEntityDoc(await storeRegistry.publicStore())).rejects.toThrow(/broker refused/i);
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import { test, expect, describe, afterAll, beforeEach } from "bun:test";
|
||||
import { docs, storeRegistry } from "../src/index";
|
||||
import { adoptCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||
import { getSyncState } from "../src/emulated-verifier/open-repo";
|
||||
import { listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
||||
import { bootPage, forgetEverything, reloadPage, signIn, SESSION, type Quad } from "./wallet-fake";
|
||||
import type { Nuri, PrincipalId } from "../src/model/types";
|
||||
|
||||
@@ -59,7 +60,7 @@ function settledButNotConnected(id: PrincipalId): void {
|
||||
async function aNoteOpenedForMessages(quads: Quad[]): Promise<Nuri> {
|
||||
bootPage(quads, COLD);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await docs.sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <${note}> <${TITLE}> "Courses" }`,
|
||||
@@ -92,9 +93,14 @@ function herPrivateStore(quads: Quad[]): Nuri {
|
||||
return record.o as Nuri;
|
||||
}
|
||||
|
||||
/** Alice's note, found the way her application finds it: by listing her own store. */
|
||||
/** Alice's note, found by listing her own store.
|
||||
*
|
||||
* That listing is the LIBRARY's own path now, not an application's: `listMyEntityDocs`
|
||||
* stopped being published in `1.0.0-dev.5` (no counterpart at any level upstream), so
|
||||
* this reaches it where it lives. What is under test is unchanged - the cold store still
|
||||
* has to answer, and it still has to refile the caps while it does. */
|
||||
async function myNote(): Promise<Nuri> {
|
||||
const mine = await storeRegistry.listMyEntityDocs("public");
|
||||
const mine = await listMyEntityDocs("alice", "public");
|
||||
const note = mine[0];
|
||||
if (!note) throw new Error("the note Alice wrote is not in her store");
|
||||
return note;
|
||||
@@ -154,7 +160,7 @@ describe("opening my note for messages again, on a page that has just loaded", (
|
||||
const quads: Quad[] = [];
|
||||
bootPage(quads, COLD);
|
||||
await signIn("alice");
|
||||
const bare = await storeRegistry.createEntityDoc("public");
|
||||
const bare = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await docs.sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <${bare}> <${TITLE}> "Vierge" }`,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { test, expect, describe, afterAll, beforeEach } from "bun:test";
|
||||
import { docs, inbox as inboxSurface, storeRegistry } from "../src/index";
|
||||
import { getSyncState } from "../src/emulated-verifier/open-repo";
|
||||
import { listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
||||
import { bootPage, forgetEverything, reloadPage, signIn, SESSION, type Quad } from "./wallet-fake";
|
||||
import type { Nuri } from "../src/model/types";
|
||||
|
||||
@@ -47,7 +48,7 @@ const COLD = { unsyncedUntilSubscribed: true } as const;
|
||||
async function aNoteWithAMessageOnIt(quads: Quad[]): Promise<Nuri> {
|
||||
bootPage(quads, COLD);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await docs.sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <${note}> <${TITLE}> "Courses" }`,
|
||||
@@ -75,9 +76,14 @@ function inboxOnTheNote(quads: Quad[], note: Nuri): Nuri {
|
||||
return record.o.split(" ")[1] as Nuri;
|
||||
}
|
||||
|
||||
/** Alice's note, found the way her application finds it: by listing her own store. */
|
||||
/** Alice's note, found by listing her own store.
|
||||
*
|
||||
* That listing is the LIBRARY's own path now, not an application's: `listMyEntityDocs`
|
||||
* stopped being published in `1.0.0-dev.5` (no counterpart at any level upstream), so
|
||||
* this reaches it where it lives. What is under test is unchanged - the cold store still
|
||||
* has to answer, and it still has to refile the caps while it does. */
|
||||
async function myNote(): Promise<Nuri> {
|
||||
const mine = await storeRegistry.listMyEntityDocs("public");
|
||||
const mine = await listMyEntityDocs("alice", "public");
|
||||
const note = mine[0];
|
||||
if (!note) throw new Error("the note Alice wrote is not in her store");
|
||||
return note;
|
||||
@@ -141,7 +147,7 @@ describe("reading the messages left on my note, on a page that has just loaded",
|
||||
const quads: Quad[] = [];
|
||||
bootPage(quads, COLD);
|
||||
await signIn("alice");
|
||||
const bare = await storeRegistry.createEntityDoc("public");
|
||||
const bare = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
|
||||
reloadPage(quads, COLD);
|
||||
await signIn("alice");
|
||||
|
||||
@@ -151,9 +151,9 @@ async function whileWatchingTheLog(body: () => Promise<void>): Promise<string[]>
|
||||
*/
|
||||
async function bobSharesWithAlice(): Promise<{ doc: Nuri; inTransit: Quad[] }> {
|
||||
await signIn("alice");
|
||||
await storeRegistry.createEntityDoc("protected"); // her first visit — now she exists
|
||||
await storeRegistry.createEntityDoc(await storeRegistry.protectedStore()); // her first visit — now she exists
|
||||
await signIn("bob");
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
await inboxSurface.share(doc, "alice");
|
||||
// Test-side inspection only — the address is read off the wallet to say WHICH document
|
||||
// is in transit, and is never handed to an actor.
|
||||
@@ -217,7 +217,7 @@ describe("an inbox opened in the middle of a session", () => {
|
||||
await signIn("alice");
|
||||
// The note and its inbox come into existence AFTER connecting, so nothing the
|
||||
// connection enumerated could have included them.
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await storeRegistry.openDocumentInbox(note);
|
||||
await converge();
|
||||
|
||||
@@ -237,7 +237,7 @@ describe("an inbox opened in the middle of a session", () => {
|
||||
|
||||
test("the messages left on it are readable, on the document its owner named", async () => {
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await storeRegistry.openDocumentInbox(note);
|
||||
await converge();
|
||||
|
||||
@@ -411,7 +411,7 @@ describe("a deposit that cannot be applied", () => {
|
||||
// subscribes again as if it had never been attempted, and the initial push of that new
|
||||
// subscription finds the deposit still waiting.
|
||||
refusing = false;
|
||||
await storeRegistry.createEntityDoc("protected");
|
||||
await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
await converge();
|
||||
expect(getCaps().capForHolder("alice", doc)).toBeDefined();
|
||||
expect(await documentsGivenTo("alice")).toContain(doc);
|
||||
@@ -513,7 +513,7 @@ describe("listing the inboxes when one of the two registers cannot be read", ()
|
||||
await signIn("alice");
|
||||
// A document inbox: a record on the User branch of the private store, which is the
|
||||
// register the broker is about to stop answering for.
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await storeRegistry.openDocumentInbox(note);
|
||||
await converge();
|
||||
const store = (await resolveAccount("alice"))?.docPrivate;
|
||||
@@ -574,7 +574,7 @@ describe("listing the inboxes when one of the two registers cannot be read", ()
|
||||
|
||||
test("still REJECTS for the caller that cannot use a partial list", async () => {
|
||||
await signIn("alice");
|
||||
await storeRegistry.openDocumentInbox(await storeRegistry.createEntityDoc("public"));
|
||||
await storeRegistry.openDocumentInbox(await storeRegistry.createEntityDoc(await storeRegistry.publicStore()));
|
||||
await converge();
|
||||
const store = (await resolveAccount("alice"))?.docPrivate;
|
||||
if (store === undefined) throw new Error("the fixture did not give Alice a private store");
|
||||
@@ -597,12 +597,12 @@ describe("listing the inboxes when one of the two registers cannot be read", ()
|
||||
*/
|
||||
async function bobSharesTwiceWithAlice(): Promise<Quad[][]> {
|
||||
await signIn("alice");
|
||||
await storeRegistry.createEntityDoc("protected"); // her first visit — now she exists
|
||||
await storeRegistry.createEntityDoc(await storeRegistry.protectedStore()); // her first visit — now she exists
|
||||
await signIn("bob");
|
||||
const aliceInbox = await userInbox("alice", "protected");
|
||||
const batches: Quad[][] = [];
|
||||
for (let round = 0; round < 2; round += 1) {
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
await inboxSurface.share(doc, "alice");
|
||||
batches.push(heldByTheBroker(aliceInbox));
|
||||
}
|
||||
@@ -680,7 +680,7 @@ describe("a register that stays unreadable", () => {
|
||||
// the observation look again (`CapRegistry.onChange`).
|
||||
fake._failReadsOn.delete(shim);
|
||||
fake._failReadsOn.add(store);
|
||||
await storeRegistry.createEntityDoc("protected");
|
||||
await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
await converge();
|
||||
});
|
||||
fake._failReadsOn.delete(store);
|
||||
|
||||
@@ -525,7 +525,7 @@ test("nothing about the deferred service reaches the published surface", () => {
|
||||
"subscribeDoc",
|
||||
"subscribeDocs",
|
||||
"useShape",
|
||||
"watchShape",
|
||||
"useShapeQuery",
|
||||
]);
|
||||
expect(Object.keys(polyfill.inbox).sort()).toEqual([
|
||||
"post",
|
||||
@@ -547,11 +547,14 @@ test("nothing about the deferred service reaches the published surface", () => {
|
||||
"sparqlQuery",
|
||||
"sparqlUpdate",
|
||||
]);
|
||||
// `1.0.0-dev.5`: `listMyEntityDocs`, `resolveScopeGraph` and `resolveWriteGraph` left,
|
||||
// and the three store accessors arrived in their place — an application names a store by
|
||||
// the reference one of them hands it, never by a word this package made up.
|
||||
expect(Object.keys(polyfill.storeRegistry).sort()).toEqual([
|
||||
"createEntityDoc",
|
||||
"listMyEntityDocs",
|
||||
"openDocumentInbox",
|
||||
"resolveScopeGraph",
|
||||
"resolveWriteGraph",
|
||||
"privateStore",
|
||||
"protectedStore",
|
||||
"publicStore",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -87,13 +87,13 @@ async function reportedDuring(body: () => Promise<void>): Promise<string[]> {
|
||||
*/
|
||||
async function aliceHasBeenHere(): Promise<void> {
|
||||
await signIn("alice");
|
||||
await storeRegistry.createEntityDoc("protected");
|
||||
await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
}
|
||||
|
||||
/** Bob shares a document of his with Alice, by name. Answers the document. */
|
||||
async function bobSharesWithAlice(): Promise<Nuri> {
|
||||
await signIn("bob");
|
||||
const doc = await storeRegistry.createEntityDoc("protected");
|
||||
const doc = await storeRegistry.createEntityDoc(await storeRegistry.protectedStore());
|
||||
await inboxSurface.share(doc, "alice");
|
||||
return doc;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ describe("a deposit no arm can apply", () => {
|
||||
await signIn("alice");
|
||||
// PUBLIC, so Bob can read it — the address rides on the document, so a depositor who
|
||||
// cannot read it cannot reach its owner. That is the model, not a fixture detail.
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await openDocumentInbox(note);
|
||||
await signIn("bob");
|
||||
await inboxSurface.postToDocument(note, { payload: { kind: UNARMED_KIND } });
|
||||
@@ -364,9 +364,9 @@ test("an identity that moves DURING the pop does not get the deposit applied for
|
||||
// Alice opens an index and Bob deposits a bare reference into it. That arm is the one
|
||||
// that reads AMBIENT state, so it is the one an identity switch can mis-file.
|
||||
await signIn("alice");
|
||||
const index = await storeRegistry.createEntityDoc("public");
|
||||
const index = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await openDocumentInbox(index);
|
||||
const object = await storeRegistry.createEntityDoc("public");
|
||||
const object = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await signIn("bob");
|
||||
await inboxSurface.postToDocument(index, { payload: object });
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ import { test, expect, describe, mock, beforeEach } from "bun:test";
|
||||
import { configure, storeRegistry, readUnion } from "../src/index";
|
||||
import { configureStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { sparqlUpdate } from "../src/surface/docs";
|
||||
import type { NgLike, Nuri, Scope, UseShapeLike } from "../src/model/types";
|
||||
import { SESSION, forgetEverything, makeWallet, signIn, type Quad } from "./wallet-fake";
|
||||
import type { NgLike, Nuri, StoreType, UseShapeLike } from "../src/model/types";
|
||||
import { listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
||||
import { SESSION, forgetEverything, makeWallet, signIn, storeOfType, type Quad } from "./wallet-fake";
|
||||
|
||||
const SHIM = "urn:ng-eventually:shim";
|
||||
/** The Main-branch read — "which documents are in this store". */
|
||||
@@ -74,12 +75,12 @@ function bootPage(quads: Quad[], lostAt: (query: string) => boolean): void {
|
||||
* Returns the reference she keeps, and the quads the wallet now holds — the durable half
|
||||
* that outlives the page.
|
||||
*/
|
||||
async function aFirstSessionThatCreated(scope: Scope): Promise<{ quads: Quad[]; note: Nuri }> {
|
||||
async function aFirstSessionThatCreated(scope: StoreType): Promise<{ quads: Quad[]; note: Nuri }> {
|
||||
const quads: Quad[] = [];
|
||||
forgetEverything();
|
||||
bootPage(quads, () => false);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc(scope);
|
||||
const note = await storeRegistry.createEntityDoc(await storeOfType(scope));
|
||||
await sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <urn:test:note> <${TITLE}> "the note" }`,
|
||||
@@ -101,7 +102,7 @@ async function aSecondSessionLosingTheBrokerOn(quads: Quad[], read: string): Pro
|
||||
armed = true;
|
||||
}
|
||||
|
||||
const ALL_SCOPES: Scope[] = ["public", "protected", "private"];
|
||||
const ALL_SCOPES: StoreType[] = ["public", "protected", "private"];
|
||||
|
||||
beforeEach(() => {
|
||||
forgetEverything();
|
||||
@@ -119,7 +120,7 @@ describe("listMyEntityDocs answers a verified listing or surfaces", () => {
|
||||
bootPage(quads, () => false);
|
||||
await signIn("alice");
|
||||
|
||||
expect(await storeRegistry.listMyEntityDocs(scope)).toContain(note);
|
||||
expect(await listMyEntityDocs("alice", scope)).toContain(note);
|
||||
const subjects = await readUnion([note]);
|
||||
expect(subjects.map((s) => s.props[TITLE]?.[0])).toEqual(["the note"]);
|
||||
});
|
||||
@@ -132,10 +133,10 @@ describe("listMyEntityDocs answers a verified listing or surfaces", () => {
|
||||
const { quads, note } = await aFirstSessionThatCreated(scope);
|
||||
await aSecondSessionLosingTheBrokerOn(quads, KEY_READ);
|
||||
|
||||
await expect(storeRegistry.listMyEntityDocs(scope)).rejects.toThrow(/BrokerError/);
|
||||
await expect(listMyEntityDocs("alice", scope)).rejects.toThrow(/BrokerError/);
|
||||
// And the point of the refusal, stated as the caller sees it: nothing came back that
|
||||
// could be mistaken for "here are your documents".
|
||||
const answered = await storeRegistry.listMyEntityDocs(scope).catch(() => null);
|
||||
const answered = await listMyEntityDocs("alice", scope).catch(() => null);
|
||||
expect(answered).toBeNull();
|
||||
expect(answered).not.toEqual([note]);
|
||||
});
|
||||
@@ -149,8 +150,8 @@ describe("listMyEntityDocs answers a verified listing or surfaces", () => {
|
||||
const { quads } = await aFirstSessionThatCreated(scope);
|
||||
await aSecondSessionLosingTheBrokerOn(quads, LISTING_READ);
|
||||
|
||||
await expect(storeRegistry.listMyEntityDocs(scope)).rejects.toThrow(/BrokerError/);
|
||||
const answered = await storeRegistry.listMyEntityDocs(scope).catch(() => null);
|
||||
await expect(listMyEntityDocs("alice", scope)).rejects.toThrow(/BrokerError/);
|
||||
const answered = await listMyEntityDocs("alice", scope).catch(() => null);
|
||||
expect(answered).toBeNull();
|
||||
expect(answered).not.toEqual([]);
|
||||
});
|
||||
|
||||
@@ -331,10 +331,10 @@ test("every announced-not-written annotation cites a record that is really here"
|
||||
});
|
||||
|
||||
test("announced-not-written accepts a statement on record and refuses every weaker form", () => {
|
||||
// The kind exists for a target ANNOUNCED and not yet written, so its first real
|
||||
// annotation cannot be in the tree to exercise it. This is that annotation written out
|
||||
// — the form `useShapeQuery` will carry — checked against the document that holds the
|
||||
// statement today.
|
||||
// The kind exists for a target ANNOUNCED and not yet written. The form below is the one
|
||||
// `useShapeQuery` carries in `surface/use-shape-query.ts` since `1.0.0-dev.5`, restated
|
||||
// here so the REFUSALS beneath it have something well-formed to depart from — and
|
||||
// checked against the document that holds the statement.
|
||||
const onRecord: Provenance = {
|
||||
name: "useShapeQuery",
|
||||
kind: ANNOUNCED,
|
||||
@@ -357,7 +357,7 @@ test("announced-not-written accepts a statement on record and refuses every weak
|
||||
// `ref=none` is what `invention` carries, and this kind is the opposite of invention.
|
||||
refused({ ref: "none" }, "must cite the record of the statement");
|
||||
// Our own source standing as the evidence for its own shape.
|
||||
refused({ ref: "packages/polyfill/src/surface/watch-shape.ts:useShapeQuery" }, "never a source file");
|
||||
refused({ ref: "packages/polyfill/src/surface/use-shape-query.ts:useShapeQuery" }, "never a source file");
|
||||
// An announcement names the layer that will carry the thing.
|
||||
refused({ level: "none" }, "level must be 1, 2 or 3");
|
||||
// And the evidence cannot be lifted onto a weaker claim to dress it up.
|
||||
|
||||
@@ -33,6 +33,7 @@ import { test, expect, describe, afterAll, beforeEach } from "bun:test";
|
||||
import { docs, inbox as inboxSurface, storeRegistry } from "../src/index";
|
||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { runScheduledInboxProcessingNow } from "../src/emulated-verifier/inbox-processor";
|
||||
import { listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
||||
import { bootPage, forgetEverything, reloadPage, signIn, SESSION, type Quad } from "./wallet-fake";
|
||||
import type { Nuri } from "../src/model/types";
|
||||
|
||||
@@ -49,7 +50,7 @@ const MESSAGE = "j'apporte le café";
|
||||
async function aNoteOpenForMessages(quads: Quad[]): Promise<Nuri> {
|
||||
bootPage(quads);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc("public");
|
||||
const note = await storeRegistry.createEntityDoc(await storeRegistry.publicStore());
|
||||
await docs.sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <${note}> <${TITLE}> "Courses" }`,
|
||||
@@ -74,9 +75,14 @@ function inboxOnTheNote(quads: Quad[], note: Nuri): Nuri {
|
||||
return record.o.split(" ")[1] as Nuri;
|
||||
}
|
||||
|
||||
/** Alice's note, found the way her application finds it: by listing her own store. */
|
||||
/** Alice's note, found by listing her own store.
|
||||
*
|
||||
* That listing is the LIBRARY's own path now, not an application's: `listMyEntityDocs`
|
||||
* stopped being published in `1.0.0-dev.5` (no counterpart at any level upstream), so
|
||||
* this reaches it where it lives. What is under test is unchanged - the cold store still
|
||||
* has to answer, and it still has to refile the caps while it does. */
|
||||
async function myNote(): Promise<Nuri> {
|
||||
const mine = await storeRegistry.listMyEntityDocs("public");
|
||||
const mine = await listMyEntityDocs("alice", "public");
|
||||
const note = mine[0];
|
||||
if (!note) throw new Error("the note Alice wrote is not in her store");
|
||||
return note;
|
||||
|
||||
@@ -40,12 +40,13 @@ import {
|
||||
resetStoreRegistry,
|
||||
getCaps,
|
||||
} from "../src/shared-wallet/bootstrap";
|
||||
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import { listMyEntityDocs, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import { resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
||||
import { resetPublicStoreFetches } from "../src/emulated-verifier/public-store";
|
||||
import { connectedUser } from "../src/emulated-verifier/connect";
|
||||
import type { NgLike, Nuri, Scope, UseShapeLike } from "../src/model/types";
|
||||
import { storeOfType } from "./wallet-fake";
|
||||
import type { NgLike, Nuri, StoreType, UseShapeLike } from "../src/model/types";
|
||||
|
||||
const SESSION: RegistrySession = { sessionId: "sid-reload", privateStoreId: "PRIV-RELOAD" };
|
||||
|
||||
@@ -55,7 +56,7 @@ const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
||||
/** The application's own predicate — the note's title, in the document it created. */
|
||||
const TITLE = "urn:test:title";
|
||||
|
||||
const ALL_SCOPES: Scope[] = ["public", "protected", "private"];
|
||||
const ALL_SCOPES: StoreType[] = ["public", "protected", "private"];
|
||||
|
||||
// --- the fake broker -------------------------------------------------------
|
||||
|
||||
@@ -303,10 +304,10 @@ async function signIn(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
/** The first visit: sign in, create the note, write its title into it. */
|
||||
async function firstVisit(quads: Quad[], scope: Scope): Promise<Nuri> {
|
||||
async function firstVisit(quads: Quad[], scope: StoreType): Promise<Nuri> {
|
||||
boot(quads);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc(scope);
|
||||
const note = await storeRegistry.createEntityDoc(await storeOfType(scope));
|
||||
await docs.sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <${note}> <${TITLE}> "the note" }`,
|
||||
@@ -349,7 +350,7 @@ describe("after a reload, the creator comes back to its own document", () => {
|
||||
reload(quads);
|
||||
await signIn("alice");
|
||||
|
||||
expect(await storeRegistry.listMyEntityDocs(scope)).toContain(note);
|
||||
expect(await listMyEntityDocs("alice", scope)).toContain(note);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+20
-16
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* `watchShape` says "there is nothing" or "I could not find out" — never the first for the second.
|
||||
* `useShapeQuery` says "there is nothing" or "I could not find out" — never the first for the second.
|
||||
*
|
||||
* The whole reason this surface exists is one distinction: an application must be able to
|
||||
* tell a scope that is EMPTY from a scope it has not heard back about. `useShape` upstream
|
||||
* cannot — it returns "an empty set, if still loading" — and `watchShape` publishes
|
||||
* cannot — it returns "an empty set, if still loading" — and `useShapeQuery` publishes
|
||||
* `isPending` / `isSuccess` / `isError` precisely to answer the question three ways.
|
||||
*
|
||||
* Until 2026-08-17 it destroyed that distinction on the one input that mattered. Step 1 of
|
||||
* its pipeline asks `listMyEntityDocs` which documents are mine in this scope; the failure
|
||||
* its pipeline asks `listMyEntityDocs` which documents are mine; the failure
|
||||
* was caught, logged, and the empty set flowed on. A barrier over zero documents is
|
||||
* trivially reached, so the surface published `{ data: [], isPending: false, isSuccess:
|
||||
* true }` — the exact snapshot a genuinely empty scope produces, byte for byte. An
|
||||
@@ -32,12 +32,12 @@
|
||||
*/
|
||||
|
||||
import { test, expect, describe, mock, beforeEach, afterAll } from "bun:test";
|
||||
import { configure, watchShape, storeRegistry } from "../src/index";
|
||||
import type { ShapeObservable, ShapeQuery } from "../src/surface/watch-shape";
|
||||
import { configure, useShapeQuery, storeRegistry } from "../src/index";
|
||||
import type { ShapeObservable, ShapeQuery } from "../src/surface/use-shape-query";
|
||||
import { configureStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { sparqlUpdate } from "../src/surface/docs";
|
||||
import type { NgLike, Nuri, Scope, UseShapeLike } from "../src/model/types";
|
||||
import { SESSION, forgetEverything, makeWallet, signIn, type Quad } from "./wallet-fake";
|
||||
import type { NgLike, Nuri, StoreType, UseShapeLike } from "../src/model/types";
|
||||
import { SESSION, forgetEverything, makeWallet, signIn, storeOfType, type Quad } from "./wallet-fake";
|
||||
|
||||
const SHIM = "urn:ng-eventually:shim";
|
||||
/** The Main-branch read of a store — "which documents are in here". Step 1 of the pipeline. */
|
||||
@@ -104,12 +104,12 @@ function bootPage(quads: Quad[], lostAt: (query: string) => boolean): void {
|
||||
* broker that works. Returns the quads the wallet now holds — the durable half that
|
||||
* outlives the page.
|
||||
*/
|
||||
async function aFirstSessionThatCreated(scope: Scope): Promise<{ quads: Quad[]; note: Nuri }> {
|
||||
async function aFirstSessionThatCreated(scope: StoreType): Promise<{ quads: Quad[]; note: Nuri }> {
|
||||
const quads: Quad[] = [];
|
||||
forgetEverything();
|
||||
bootPage(quads, () => false);
|
||||
await signIn("alice");
|
||||
const note = await storeRegistry.createEntityDoc(scope);
|
||||
const note = await storeRegistry.createEntityDoc(await storeOfType(scope));
|
||||
await sparqlUpdate(
|
||||
SESSION.sessionId,
|
||||
`INSERT DATA { <urn:test:evt> <${RDF_TYPE}> <${EVENT}> ; <${TITLE}> "the note" }`,
|
||||
@@ -160,7 +160,11 @@ async function subscribeAndSettle(
|
||||
return { renders: () => renders, stop };
|
||||
}
|
||||
|
||||
const ALL_SCOPES: Scope[] = ["public", "protected", "private"];
|
||||
const ALL_SCOPES: StoreType[] = ["public", "protected", "private"];
|
||||
|
||||
/** The whole local dataset - upstream's own marker, and the reading that still has to
|
||||
* enumerate this identity's documents, which is the step these tests make fail. */
|
||||
const EVERYTHING = { graphs: "did:ng:i" };
|
||||
|
||||
beforeEach(() => {
|
||||
forgetEverything();
|
||||
@@ -171,11 +175,11 @@ afterAll(() => {
|
||||
// put the possession gate in force for a suite that declares none, and `subscribe.test.ts`
|
||||
// is then refused at the door for documents it legitimately owns; a config left behind
|
||||
// hands `access-log.test.ts` this file's `ng`. Both failed exactly that way before this
|
||||
// ran, and neither names watch-shape anywhere. `forgetEverything` is the whole teardown.
|
||||
// ran, and neither names use-shape-query anywhere. `forgetEverything` is the whole teardown.
|
||||
forgetEverything();
|
||||
});
|
||||
|
||||
describe("watchShape tells 'there is nothing' from 'I could not find out'", () => {
|
||||
describe("useShapeQuery tells 'there is nothing' from 'I could not find out'", () => {
|
||||
// The NORMAL cases first, and they are not a formality: a surface that reported `isError`
|
||||
// unconditionally would pass every failure case below. These two are what those are a
|
||||
// departure from — and the first of them is the reading that must never be reused for a
|
||||
@@ -188,7 +192,7 @@ describe("watchShape tells 'there is nothing' from 'I could not find out'", () =
|
||||
bootPage(quads, () => false);
|
||||
await signIn("alice");
|
||||
|
||||
const obs = watchShape(EventShape, scope) as ShapeObservable;
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING) as ShapeObservable;
|
||||
const { stop } = await subscribeAndSettle(obs);
|
||||
|
||||
const snap = obs.getSnapshot();
|
||||
@@ -208,7 +212,7 @@ describe("watchShape tells 'there is nothing' from 'I could not find out'", () =
|
||||
bootPage(quads, () => false);
|
||||
await signIn("alice");
|
||||
|
||||
const obs = watchShape(EventShape, scope) as ShapeObservable;
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING) as ShapeObservable;
|
||||
const { stop } = await subscribeAndSettle(obs);
|
||||
|
||||
const snap = obs.getSnapshot();
|
||||
@@ -232,7 +236,7 @@ describe("watchShape tells 'there is nothing' from 'I could not find out'", () =
|
||||
await signIn("alice");
|
||||
armed = true;
|
||||
|
||||
const obs = watchShape(EventShape, scope) as ShapeObservable;
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING) as ShapeObservable;
|
||||
const { stop } = await subscribeAndSettle(obs);
|
||||
|
||||
const snap = obs.getSnapshot();
|
||||
@@ -263,7 +267,7 @@ describe("watchShape tells 'there is nothing' from 'I could not find out'", () =
|
||||
bootPage(quads, (query) => armed && query.includes(LISTING_READ));
|
||||
await signIn("alice");
|
||||
|
||||
const obs = watchShape(EventShape, scope) as ShapeObservable;
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING) as ShapeObservable;
|
||||
const { renders, stop } = await subscribeAndSettle(obs);
|
||||
expect(whatTheAppSees(obs.getSnapshot())).toBe("some");
|
||||
const rendersBefore = renders();
|
||||
+124
-9
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* watch-shape.test.ts — behavioural tests for `watchShape` (src/watch-shape.ts),
|
||||
* use-shape-query.test.ts — behavioural tests for `useShapeQuery`
|
||||
* (src/surface/use-shape-query.ts),
|
||||
* against a STATEFUL fake `ng` with a CONTROLLABLE `doc_subscribe`.
|
||||
*
|
||||
* The fake emulates just enough of the broker:
|
||||
@@ -20,11 +21,13 @@
|
||||
* (a) isPending at first, isSuccess after the first State (barrier);
|
||||
* (b) isSuccess + data:[] on a synced-but-EMPTY scope (the key distinction);
|
||||
* (c) a write then push → data updates (reactivity, no polling);
|
||||
* (d) timed-out → isSuccess (best-effort), NOT isError.
|
||||
* (d) timed-out → isSuccess (best-effort), NOT isError;
|
||||
* plus the three readings of the SDK `Scope` the surface now takes: the whole dataset,
|
||||
* a named `graphs` list, and the empty list that upstream answers with no objects.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test";
|
||||
import { watchShape } from "../src/surface/watch-shape";
|
||||
import { useShapeQuery } from "../src/surface/use-shape-query";
|
||||
import { configure } from "../src/index";
|
||||
import { configureStoreRegistry, setCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
@@ -32,6 +35,8 @@ import { resetRegistryCache, createEntityDoc } from "../src/shared-wallet/accoun
|
||||
import { resetOpenedRepos, setOpenTimeoutForTests, getSyncState } from "../src/emulated-verifier/open-repo";
|
||||
|
||||
const TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
||||
/** The whole local dataset - upstream's own marker, what `""` normalises to. */
|
||||
const EVERYTHING = { graphs: "did:ng:i" };
|
||||
const FP = "http://festipod.org/";
|
||||
const SESSION = { sessionId: "sid-ws", privateStoreId: "PRIV-WS" };
|
||||
|
||||
@@ -230,6 +235,18 @@ function seed(ng: ReturnType<typeof makeFake>, doc: string, p: string, o: string
|
||||
ng._quads.push({ g: doc, s: doc, p, o });
|
||||
}
|
||||
|
||||
// The same, under a subject of its own rather than the document's reference: one document
|
||||
// may hold several subjects, which is what the scope's `subjects` axis filters on.
|
||||
function seedSubject(
|
||||
ng: ReturnType<typeof makeFake>,
|
||||
doc: string,
|
||||
subject: string,
|
||||
p: string,
|
||||
o: string,
|
||||
): void {
|
||||
ng._quads.push({ g: doc, s: subject, p, o });
|
||||
}
|
||||
|
||||
const tick = () => new Promise((r) => setTimeout(r, 5));
|
||||
|
||||
// A minimal SHEX ShapeType pinning rdf:type to `${FP}Event`.
|
||||
@@ -256,7 +273,7 @@ afterAll(() => {
|
||||
resetCaps();
|
||||
});
|
||||
|
||||
describe("watchShape", () => {
|
||||
describe("useShapeQuery", () => {
|
||||
it("(a) isPending at first, then isSuccess after the first State (barrier)", async () => {
|
||||
const ng = makeFake({ holdState: true });
|
||||
inject(ng);
|
||||
@@ -266,7 +283,7 @@ describe("watchShape", () => {
|
||||
seed(ng, doc, TYPE, `${FP}Event`);
|
||||
seed(ng, doc, `${FP}title`, "Alpha");
|
||||
|
||||
const obs = watchShape(EventShape, "protected");
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING);
|
||||
let notes = 0;
|
||||
const unsub = obs.subscribe(() => {
|
||||
notes += 1;
|
||||
@@ -300,7 +317,7 @@ describe("watchShape", () => {
|
||||
setCurrentUser("bob");
|
||||
// bob has NO entity docs in this scope — the scope is genuinely empty.
|
||||
|
||||
const obs = watchShape(EventShape, "protected");
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING);
|
||||
const unsub = obs.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
@@ -321,7 +338,7 @@ describe("watchShape", () => {
|
||||
seed(ng, doc, TYPE, `${FP}Event`);
|
||||
seed(ng, doc, `${FP}title`, "One");
|
||||
|
||||
const obs = watchShape(EventShape, "protected");
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING);
|
||||
const unsub = obs.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
@@ -360,14 +377,17 @@ describe("watchShape", () => {
|
||||
seed(ng, doc, TYPE, `${FP}Event`);
|
||||
seed(ng, doc, `${FP}title`, "Timed");
|
||||
|
||||
const obs = watchShape(EventShape, "protected");
|
||||
const obs = useShapeQuery(EventShape, EVERYTHING);
|
||||
const unsub = obs.subscribe(() => {});
|
||||
await tick();
|
||||
// Before the fallback fires: still pending (subscribed, no State).
|
||||
expect(obs.getSnapshot().isPending).toBe(true);
|
||||
|
||||
// Let the bounded fallback elapse → open-repo marks each opened doc timed-out.
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
// Long enough for FOUR of them in a row: the whole-dataset reading opens the three
|
||||
// store index documents as well as the entity document, and with the `State` held
|
||||
// back each one waits out the full fallback before the next begins.
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
@@ -382,4 +402,99 @@ describe("watchShape", () => {
|
||||
unsub();
|
||||
});
|
||||
|
||||
// ── the scope axis: the three readings upstream gives it ──────────────────
|
||||
//
|
||||
// These are not variants of one case. `graphs` names ONE document's graph per entry;
|
||||
// `did:ng:i` (or `""`) discards the rest and means the whole dataset; `[]` or
|
||||
// `undefined` returns no objects at all. The first two answer differently over the SAME
|
||||
// wallet, and the third is the one a caller reaches by accident - so each is pinned.
|
||||
|
||||
it("(e) a named `graphs` list reads those documents and no others", async () => {
|
||||
const ng = makeFake();
|
||||
inject(ng);
|
||||
setCurrentUser("erin");
|
||||
const kept = await createEntityDoc("erin", "protected");
|
||||
seed(ng, kept, TYPE, `${FP}Event`);
|
||||
seed(ng, kept, `${FP}title`, "Named");
|
||||
const other = await createEntityDoc("erin", "protected");
|
||||
seed(ng, other, TYPE, `${FP}Event`);
|
||||
seed(ng, other, `${FP}title`, "Unnamed");
|
||||
|
||||
// The whole dataset sees both - that is the reading the named list departs from.
|
||||
const all = useShapeQuery(EventShape, EVERYTHING);
|
||||
const unsubAll = all.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
expect(all.getSnapshot().data.flatMap((s) => s.props[`${FP}title`] ?? []).sort()).toEqual([
|
||||
"Named",
|
||||
"Unnamed",
|
||||
]);
|
||||
unsubAll();
|
||||
|
||||
const obs = useShapeQuery(EventShape, { graphs: [kept] });
|
||||
const unsub = obs.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const snap = obs.getSnapshot();
|
||||
expect(snap.isSuccess).toBe(true);
|
||||
expect(snap.data.flatMap((s) => s.props[`${FP}title`] ?? [])).toEqual(["Named"]);
|
||||
unsub();
|
||||
});
|
||||
|
||||
it("(f) an empty `graphs` returns no objects, and says so as SUCCESS", async () => {
|
||||
const ng = makeFake();
|
||||
inject(ng);
|
||||
setCurrentUser("frank");
|
||||
const doc = await createEntityDoc("frank", "protected");
|
||||
seed(ng, doc, TYPE, `${FP}Event`);
|
||||
seed(ng, doc, `${FP}title`, "Unreachable");
|
||||
|
||||
// Upstream: "Setting value to `[]` or leaving it `undefined`, no objects are returned."
|
||||
// It is an ANSWER, not a failure - and not the pending state either, which is what an
|
||||
// application would otherwise be left staring at.
|
||||
const obs = useShapeQuery(EventShape, {});
|
||||
const unsub = obs.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
const snap = obs.getSnapshot();
|
||||
expect(snap.isPending).toBe(false);
|
||||
expect(snap.isSuccess).toBe(true);
|
||||
expect(snap.isError).toBe(false);
|
||||
expect(snap.data).toEqual([]);
|
||||
unsub();
|
||||
});
|
||||
|
||||
it("(g) `subjects` narrows to the subject IRIs named, literally", async () => {
|
||||
const ng = makeFake();
|
||||
inject(ng);
|
||||
setCurrentUser("gina");
|
||||
const doc = await createEntityDoc("gina", "protected");
|
||||
// Two subjects in ONE document: the `subjects` axis is what tells them apart, and
|
||||
// `graphs` cannot - both carry the same `@graph`.
|
||||
seedSubject(ng, doc, `${FP}one`, TYPE, `${FP}Event`);
|
||||
seedSubject(ng, doc, `${FP}one`, `${FP}title`, "First");
|
||||
seedSubject(ng, doc, `${FP}two`, TYPE, `${FP}Event`);
|
||||
seedSubject(ng, doc, `${FP}two`, `${FP}title`, "Second");
|
||||
|
||||
const both = useShapeQuery(EventShape, EVERYTHING);
|
||||
const unsubBoth = both.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
expect(both.getSnapshot().data.flatMap((s) => s.props[`${FP}title`] ?? []).sort()).toEqual([
|
||||
"First",
|
||||
"Second",
|
||||
]);
|
||||
unsubBoth();
|
||||
|
||||
const obs = useShapeQuery(EventShape, { graphs: "did:ng:i", subjects: [`${FP}two`] });
|
||||
const unsub = obs.subscribe(() => {});
|
||||
await tick();
|
||||
await tick();
|
||||
|
||||
expect(obs.getSnapshot().data.flatMap((s) => s.props[`${FP}title`] ?? [])).toEqual(["Second"]);
|
||||
unsub();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { mock } from "bun:test";
|
||||
import { configure } from "../src/index";
|
||||
import { configure, storeRegistry } from "../src/index";
|
||||
import {
|
||||
configureStoreRegistry,
|
||||
setCurrentUser,
|
||||
@@ -32,7 +32,7 @@ import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import { resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
||||
import { resetPublicStoreFetches } from "../src/emulated-verifier/public-store";
|
||||
import { connectedUser } from "../src/emulated-verifier/connect";
|
||||
import type { NgLike, UseShapeLike } from "../src/model/types";
|
||||
import type { NgLike, Nuri, StoreType, UseShapeLike } from "../src/model/types";
|
||||
|
||||
export const SESSION: RegistrySession = { sessionId: "sid-wallet", privateStoreId: "PRIV-WALLET" };
|
||||
|
||||
@@ -438,3 +438,20 @@ export async function signIn(id: string): Promise<void> {
|
||||
setCurrentUser(id);
|
||||
await connectedUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* The published reference of one of the connected identity's three stores.
|
||||
*
|
||||
* Placement stopped being spelled as a word on the published surface in `1.0.0-dev.5`:
|
||||
* an application names a store by the reference `privateStore()`, `protectedStore()` or
|
||||
* `publicStore()` hands it, and passes that to `createEntityDoc`. A suite that walks the
|
||||
* three still needs to go from one to the other, and this is that step - through the
|
||||
* published calls, never around them.
|
||||
*/
|
||||
export function storeOfType(type: StoreType): Promise<Nuri> {
|
||||
return type === "public"
|
||||
? storeRegistry.publicStore()
|
||||
: type === "protected"
|
||||
? storeRegistry.protectedStore()
|
||||
: storeRegistry.privateStore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user