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
|
||||
|
||||
Reference in New Issue
Block a user