feat!: useShapeQuery lit par la requete de forme du SDK
This commit is contained in:
+21
-13
@@ -48,6 +48,7 @@ The prose labels above map onto the annotation's closed `kind` set, and where th
|
||||
export interface EventuallyConfig {
|
||||
ng: NgLike; // the REAL @ng-org/web ng
|
||||
useShape: UseShapeLike; // the REAL @ng-org/orm useShape
|
||||
ormSubscription?: OrmSubscriptionLike; // the REAL @ng-org/orm RdfOrmSubscription.getOrCreate
|
||||
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
|
||||
sharedWallet?: SharedWalletConfig; // the gate's, § 2bis
|
||||
debugAccessLog?: boolean;
|
||||
@@ -75,7 +76,9 @@ export function configure(c: EventuallyConfig): void;
|
||||
|
||||
Both remain substitutable through `configureStoreRegistry` (`shared-wallet/bootstrap.ts`), which the published entry does not re-export: the unit suites have no browser and the e2e harness holds a session the broker handed it directly, and neither is an application.
|
||||
|
||||
So an application's whole bootstrap is `configure({ ng, useShape, init, sharedWallet })` plus `await ensureIdentity()` — and the second of those keeps its call site after migration.
|
||||
**One field ARRIVED in `1.0.0-dev.6`: `ormSubscription`.** `useShapeQuery` reads through the SDK's own shape query now (§ 5), and the SDK is injected here rather than imported — so the query has to arrive the same way the `ng` and the `useShape` beside it do. Pass `RdfOrmSubscription.getOrCreate` as it stands; it is assignable to `OrmSubscriptionLike` with no wrapper. It is OPTIONAL because an application that never calls `useShapeQuery` has nothing to give, and `useShapeQuery` without it publishes `isError` naming the field rather than an empty set.
|
||||
|
||||
So an application's whole bootstrap is `configure({ ng, useShape, ormSubscription, init, sharedWallet })` plus `await ensureIdentity()` — and the second of those keeps its call site after migration.
|
||||
|
||||
### Target
|
||||
|
||||
@@ -224,7 +227,7 @@ Divergence to note: the wrapper types everything `unknown`, losing the generic `
|
||||
|
||||
```ts
|
||||
// use-shape-query.ts — ShapeQuery
|
||||
export interface ShapeQuery<T = UnionSubject> {
|
||||
export interface ShapeQuery<T extends BaseType = BaseType> {
|
||||
data: T[];
|
||||
isPending: boolean;
|
||||
isSuccess: boolean;
|
||||
@@ -232,22 +235,27 @@ export interface ShapeQuery<T = UnionSubject> {
|
||||
error: unknown;
|
||||
}
|
||||
// use-shape-query.ts — ShapeObservable
|
||||
export interface ShapeObservable<T = UnionSubject> {
|
||||
export interface ShapeObservable<T extends BaseType = BaseType> {
|
||||
getSnapshot(): ShapeQuery<T>;
|
||||
subscribe(onChange: () => void): () => void;
|
||||
refetch(): void;
|
||||
}
|
||||
// use-shape-query.ts — useShapeQuery. `Scope` here is the SDK's own
|
||||
// (`sdk/js/orm/src/types.ts:Scope`), re-exported by the entry — NOT a store scope.
|
||||
export function useShapeQuery<T = UnionSubject>(
|
||||
shapeType: unknown,
|
||||
// use-shape-query.ts — useShapeQuery. Both parameters are the SDK's own:
|
||||
// `ShapeType` (`sdk/js/shex-orm/src/types.ts`) and `Scope` (`sdk/js/orm/src/types.ts`),
|
||||
// both re-exported by the entry. `Scope` is NOT a store scope.
|
||||
export function useShapeQuery<T extends BaseType = BaseType>(
|
||||
shapeType: ShapeType<T>,
|
||||
scope: Scope,
|
||||
): ShapeObservable<T>;
|
||||
```
|
||||
|
||||
**The name and the scope, since `1.0.0-dev.5`.** It was `watchShape(shapeType, "public" | "protected" | "private")`. `watchShape` has ZERO occurrences in `nextgraph-rs` and spends `watch`, a verb the ORM already spends on signal observation; the target's own name is `useShapeQuery`, announced by NextGraph's developers and on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, which is what the symbol's `announced-not-written` annotation cites. The three literals were this package's own and named the scope of a READ, which the SDK answers on two axes instead: `graphs` (one document's graph per entry, `did:ng:i` or `""` for the whole dataset, `[]` or `undefined` for no objects at all) and `subjects`. Only the whole-dataset reading still needs translating — it is served by enumerating the identity's own documents across its three stores — and that translation goes when the read queries the store itself.
|
||||
**What it reads THROUGH, since `1.0.0-dev.6`: the SDK's own shape query.** `RdfOrmSubscription.getOrCreate(shapeType, scope)`, injected at `configure` as `ormSubscription` (§ 1). Four steps went with the change and they were one mechanism: resolving the logical scope to a document set (`listMyEntityDocs`), opening those documents through `ensureReposOpen` for a sync barrier, `readUnion` over them, and filtering the union by the shape's `@type` IRI. All four existed because the parameter had to be TRANSLATED; once it is upstream's own `Scope`, the question goes to the thing that answers it, and `graphs` / `subjects` / `did:ng:i` are read by the engine literally. It does NOT delegate to the SDK's `useShape` — that is React's hook and its `DeepSignalSet` cannot carry the request state, which is the whole reason this symbol exists. What is taken from the SDK is the QUERY; the observable, the snapshot and the state tracking stay here, and so does the capability filter (`emulated-verifier/read-filter.ts`, applied per `@graph` on what comes back, exactly where § 4 applies it).
|
||||
|
||||
**A scope that did not answer is `isError`, never `isSuccess` with `data: []` — since 2026-08-17.** Step 1 of the pipeline asks `listMyEntityDocs` which documents are mine in this scope. That failure used to be caught and logged, and the empty set flowed on: a barrier over zero documents is trivially reached, so the surface published `{ data: [], isPending: false, isSuccess: true }` — byte for byte the synced-but-empty snapshot, which means the one distinction this module exists for was the one it destroyed. An application rendered "you have created nothing" for "the store did not answer". `listMyEntityDocs` had stopped handing out that reading the day before (see § 12); catching it here put it straight back one floor up. It now travels the LOAD-STATE channel, which is where "this is not an answer" already lives on this surface, and which an application must already read to tell pending from empty — so the third state costs it no new vocabulary.
|
||||
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were `UnionSubject` property bags; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery).
|
||||
|
||||
**The name and the scope, since `1.0.0-dev.5`.** It was `watchShape(shapeType, "public" | "protected" | "private")`. `watchShape` has ZERO occurrences in `nextgraph-rs` and spends `watch`, a verb the ORM already spends on signal observation; the target's own name is `useShapeQuery`, announced by NextGraph's developers and on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, which is what the symbol's `announced-not-written` annotation cites. The three literals were this package's own and named the scope of a READ, which the SDK answers on two axes instead: `graphs` (one document's graph per entry, `did:ng:i` or `""` for the whole dataset, `[]` or `undefined` for no objects at all) and `subjects`. Nothing is translated any more: the `Scope` object a caller writes is the object the SDK receives.
|
||||
|
||||
**"There is nothing" and "I could not find out" are still two different answers — and the second one moved.** Until `1.0.0-dev.6` the failure this surface had to keep apart was its own first step: `listMyEntityDocs` not answering, caught and logged, the empty set flowing on, and `{ data: [], isPending: false, isSuccess: true }` published — byte for byte the synced-but-empty snapshot, so an application rendered "you have created nothing" for "the store did not answer". That step is gone with the pipeline. What can fail now is opening the shape query, and that is what `isError` carries: a shape the ORM cannot subscribe, or a `configure()` that was never given the query. A start the ENGINE refuses does NOT reach `isError`, and that is upstream's doing rather than a choice made here — `orm_start_graph` is awaited inside a `catch` that only logs, and the readiness promise is built with `resolve` alone — so such a read stays `isPending` for ever. The distinction holds either way (an application is never told "there is nothing" for "nobody answered"), but an interface that only handles `isError` will show a spinner where it used to show an error. `isError` stays published because this package enriches the SDK in its announced direction rather than stopping at what it exposes today; the day the ORM reports a failed start, that is where it lands.
|
||||
|
||||
**`data` survives an error rather than emptying.** A one-shot call rejects and is done; an observable has already handed a list to a subscriber that rendered it, and cannot un-emit. Collapsing `data` to `[]` on failure would put the empty answer back in the one field a view actually paints, for exactly the case that must never read as empty — so `data` keeps the last read that ANSWERED, `isSuccess` stays false so that array is never offered as a reply to the question that just failed, and the subscriber is notified so a view gating on the load state can say so. Before any answer there is nothing to keep and `data` is `[]`, published under `isError` and never under `isSuccess`.
|
||||
|
||||
@@ -611,7 +619,7 @@ export async function openDocumentInbox(doc: NuriLike): Promise<Nuri>;
|
||||
// resolveWriteGraph — it returned the very reference the three accessors return.
|
||||
|
||||
// NOT published — internal, kept here because the target rulings below still cover them.
|
||||
// userStoreDoc, userInbox, documentInboxAddress, isOwnInbox, myInboxes,
|
||||
// userInbox, documentInboxAddress, isOwnInbox, myInboxes,
|
||||
// addLink, readLinks, resolveAccount, ensureAccount, reservedAccount,
|
||||
// resetRegistryCache, and the VirtualUserRecord type.
|
||||
// `RegistrySession` joined them on 2026-08-12: it was published for ONE reason — a consumer
|
||||
@@ -629,7 +637,7 @@ export async function openDocumentInbox(doc: NuriLike): Promise<Nuri>;
|
||||
|
||||
- **`createEntityDoc(id, scope)` → level 2, VERIFIED direction.** Target: `doc_create(session_id, crdt, class_name, destination, store_repo)` aimed at the identity's real per-scope store (see § 7 for the store-targeting nuance — the nodejs SDK already takes `store_type`/`store_repo` strings). The two writes the lib performs by hand are **native side effects** of `doc_create` upstream: the `ldp:contains` listing on the store's Main branch and the `AddRepo { read_cap }` on its Store branch (`engine/verifier/src/request_processor.rs:697-710`). The `id` parameter is already gone from the published call (2026-08-10); expect `createEntityDoc(scope)` to become `doc_create(sid, …, storeOf(scope))` with no listing/cap bookkeeping.
|
||||
- **`listMyEntityDocs(id, scope)` → level 1/2, VERIFIED mechanism.** Upstream the listing is the store's `ldp:contains` graph (written at `request_processor.rs:706-708`), readable with an anchored `sparql_query` on the store; the caps come back by replaying the Store branch (`AddRepo::verify` → `load_repo_from_read_cap`). The function's shape (give me my per-scope doc NURIs) survives; its implementation becomes one native read.
|
||||
- **`userStoreDoc(id, scope)` / `resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `sdk/js/web/src/index.ts:Session`, installed `index.d.ts:265-273`). The store IS the container; the per-scope index document disappears.
|
||||
- **`resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** (`userStoreDoc` stood beside them and was REMOVED in `1.0.0-dev.6`: it handed out the scope-index document so a reactive reader could follow it, and `useShapeQuery` reading through the SDK's shape query left it with no caller.) The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `sdk/js/web/src/index.ts:Session`, installed `index.d.ts:265-273`). The store IS the container; the per-scope index document disappears.
|
||||
- **`userInbox(id)` → level 1, VERIFIED counterpart with a different granularity.** Upstream a user's inboxes are their public and protected STORE repos' inboxes — the only two `AddInboxCap` commits in the engine (`engine/verifier/src/site.rs:128,149`). An identity-level "my inbox" therefore maps to a store inbox; the resolution moves into the lib/SDK and the consumer's act (deposit to an address, process my own) is unchanged.
|
||||
- **`openDocumentInbox(doc)` / `documentInboxAddress(doc)` → level 1, VERIFIED support, no exerciser.** Every `Repo` carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); `AddInboxCapV0` is keyed by `repo_id` with no is-store restriction (`engine/repo/src/types.rs:1973`; applied at `engine/verifier/src/verifier.rs:1920-1928`); but no code path creates one for a plain document (`doc_create` → `new_repo_default` → `Store::create_repo_default` → `create_repo_with_keys`, which builds the `Repo` with `inbox: None` — `engine/verifier/src/verifier.rs:3004`, `engine/repo/src/store.rs:264,284,691`) and no level-2/3 API exposes any of it. So: the *capability* is engine-verified; the *functions* are invented surface; and the **address publication is a real, deliberate divergence** (upstream transmits addresses, never publishes them — § 9), with the ownership guard compensating our design, not mirroring an upstream rule.
|
||||
- **`addLink(cap)` / `readLinks()` → annotated `declared-not-wired` (level 1).** The emulated `AddLink { read_cap }` register (`engine/repo/src/types.rs:AddLinkV0` — *"so that a user can share with all its device a new Link they received"*, external repos only). **Verified 2026-08-16, and this is the load-bearing fact:** the type is DEFINED, its `CommitVerifier::verify` is a no-op `Ok(())` (`engine/verifier/src/commits/mod.rs`), and **nothing in the engine constructs one** — the only other occurrences are the `CommitBodyV0::AddLink` enum variant and two match arms classifying body kinds. Its sibling `AddRepoV0` *is* constructed (`Verifier::send_add_repo_to_store`), which is why the Store-branch register next door is annotated `aligned` and this one is not. A maintainer read that absence and concluded the function was this library's own invention; it is not — an absent implementation says nothing about the target. Upstream this filing happens inside the verifier when it processes the inbox; the future SDK most likely never exposes these as calls, so consumers should not code against them (§ 15).
|
||||
@@ -698,7 +706,7 @@ The entry used to say `export * from "./model/types"`, a blanket re-export publi
|
||||
export type { Nuri, NuriLike, PrincipalId, NgLike, UseShapeLike } from "./model/types";
|
||||
```
|
||||
|
||||
Each one's warrant: `Nuri` is what every reference-returning call returns and `NuriLike` what every entry accepts (§ 10, § 11); `Scope` left this list in `1.0.0-dev.5` — the read takes the SDK's own `Scope`, re-exported from `@ng-org/orm`, and a store is named by its reference (§ 5, § 12); `PrincipalId` is `ensureIdentity`'s return and a field of `Deposit`, `PostOptions` and `EventuallyConfig` (§ 2bis, § 9, § 1); `NgLike` and `UseShapeLike` type the two injected objects in `EventuallyConfig` (§ 1).
|
||||
Each one's warrant: `Nuri` is what every reference-returning call returns and `NuriLike` what every entry accepts (§ 10, § 11); `Scope` left this list in `1.0.0-dev.5` — the read takes the SDK's own `Scope`, re-exported from `@ng-org/orm`, and a store is named by its reference (§ 5, § 12); `PrincipalId` is `ensureIdentity`'s return and a field of `Deposit`, `PostOptions` and `EventuallyConfig` (§ 2bis, § 9, § 1); `NgLike`, `UseShapeLike` and `OrmSubscriptionLike` type the three injected members of `EventuallyConfig` (§ 1). `OrmSubscriptionHandle` — what the last of them RETURNS — is deliberately not published: an application hands the shape query in and reads the snapshot back, and publishing the handle would offer a path around the capability filter and the shared-wallet identity boundary.
|
||||
|
||||
Two types the blanket export published are now internal, each because **nothing published names it**:
|
||||
|
||||
@@ -742,7 +750,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
### `@ng-eventually/polyfill` — `src/index.ts` (the only entry since 2026-08-07)
|
||||
|
||||
```text
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, NG, NgLike, Nuri, NuriLike, PrincipalId, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, useShapeQuery
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, NG, NgLike, Nuri, NuriLike, OrmSubscriptionLike, PrincipalId, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, useShapeQuery
|
||||
docs: docCreate, sparqlQuery, sparqlUpdate
|
||||
inbox: Deposit, PostOptions, post, postToDocument, processInbox, read, readForDocument, readSynced, readSyncedForDocument, share, watch
|
||||
storeRegistry: createEntityDoc, openDocumentInbox, privateStore, protectedStore, publicStore
|
||||
|
||||
@@ -169,11 +169,11 @@ export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
||||
### 9b. Scope-index resolution
|
||||
|
||||
```ts
|
||||
// store-registry.ts:1079
|
||||
export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri>;
|
||||
// REMOVED in `1.0.0-dev.6`
|
||||
// export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri>;
|
||||
```
|
||||
|
||||
The scope-INDEX document of one account (what `watchShape` subscribes to for container changes). Same resolution as the published `resolveWriteGraph`, without doubling as a write target.
|
||||
The scope-INDEX document of one account, which the reactive shape read subscribed to for container changes. That read goes through the SDK's own shape query since `1.0.0-dev.6`, which follows the data rather than a container this package maintains, so the function lost its only caller and went with it. The ruling below is kept for the record, because the question it answers is the one the target closes.
|
||||
|
||||
- **Level 2, VERIFIED counterpart with the indirection removed**: in the target the container IS the store, and its id is on the session (`Session.private_store_id | protected_store_id | public_store_id`, `index.d.ts:264-272`); the listing a subscriber watches is the store's `ldp:contains` graph, written natively by `doc_create` (`request_processor.rs:706-708`). The function's question survives; the per-account parameter and the index-document indirection disappear.
|
||||
|
||||
@@ -249,7 +249,7 @@ export function inspectOutbox(): void;
|
||||
|
||||
**F3 — incomplete citation in `subscribe.ts`.** `subscribe.ts:31` cites the ORM fan-out abort as "`initialize.rs:125-128`" with no path. The file is `engine/verifier/src/orm/graph/initialize.rs`; lines 125-128 are the graph loop calling `self.open_for_target(&nuri.target, true).await?` — verified, the `?` propagates `RepoNotFound` and aborts the whole subscription. Substance correct; the bare filename is unfindable without this note.
|
||||
|
||||
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts` routes through `surface/placement.ts`) those are internal. That file is being edited concurrently; noted here, deliberately not fixed by this document.
|
||||
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts` routes through `surface/placement.ts`) those are internal. That file is being edited concurrently; noted here, deliberately not fixed by this document.
|
||||
|
||||
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`shared-wallet/bootstrap.ts`), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on `StoreRegistryDeps.normalizeId`, or reject NUL-prefixed raw ids at `accountKey`.
|
||||
|
||||
@@ -266,6 +266,6 @@ export function inspectOutbox(): void;
|
||||
|
||||
Fully internal modules: `shared-wallet/access-log.ts` (`AccessOp`, `setAccessLog`, `enabled`, `activeIdentity`, `accessLogPrefix`, `logStage`, `shortNuri`, `logAccess`); `emulated-verifier/machinery.ts` (`MACHINERY_NS`, `isMachinerySubject`); `surface/ng-proxy.ts` (`makeNg`); `emulated-verifier/open-repo.ts` (`SyncState`, `setOpenTimeoutForTests`, `resetOpenedRepos`, `getSyncState`, `ensureRepoOpen`, `ensurePhysicalRepoOpen`, `ensureReposOpen`); `shared-wallet/outbox-log.ts` (`inspectOutbox`); `shared-wallet/physical.ts` (`physicalCreate`, `physicalQuery`, `physicalUpdate`); `emulated-verifier/reach.ts` (`declareInfrastructure`, `isInfrastructure`, `resetInfrastructure`, `mayReach`, `assertMayReach`, `mustNotAttempt`); `emulated-verifier/read-filter.ts` (`filterReadable`, `makeReadFilteredView`).
|
||||
|
||||
Internal slices of partially-published modules: `model/nuri.ts` (`targetOf`, `parseNuri`) and `emulated-verifier/caps.ts` (`mintCap`); `emulated-verifier/connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `shared-wallet/account-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
|
||||
Internal slices of partially-published modules: `model/nuri.ts` (`targetOf`, `parseNuri`) and `emulated-verifier/caps.ts` (`mintCap`); `emulated-verifier/connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `shared-wallet/account-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
|
||||
|
||||
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/virtualUsers.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the entry point.
|
||||
|
||||
Reference in New Issue
Block a user