feat!: useShapeQuery prend le Scope du SDK, et un store se nomme par sa reference
This commit is contained in:
+40
-20
@@ -218,12 +218,12 @@ Divergence to note: the wrapper types everything `unknown`, losing the generic `
|
||||
|
||||
---
|
||||
|
||||
## 5. Reactive typed reads with load state — `watchShape`
|
||||
## 5. Reactive typed reads with load state — `useShapeQuery`
|
||||
|
||||
### Today — `@ng-eventually/polyfill`
|
||||
|
||||
```ts
|
||||
// watch-shape.ts:73
|
||||
// use-shape-query.ts — ShapeQuery
|
||||
export interface ShapeQuery<T = UnionSubject> {
|
||||
data: T[];
|
||||
isPending: boolean;
|
||||
@@ -231,19 +231,22 @@ export interface ShapeQuery<T = UnionSubject> {
|
||||
isError: boolean;
|
||||
error: unknown;
|
||||
}
|
||||
// watch-shape.ts:90
|
||||
// use-shape-query.ts — ShapeObservable
|
||||
export interface ShapeObservable<T = UnionSubject> {
|
||||
getSnapshot(): ShapeQuery<T>;
|
||||
subscribe(onChange: () => void): () => void;
|
||||
refetch(): void;
|
||||
}
|
||||
// watch-shape.ts:166
|
||||
export function watchShape<T = UnionSubject>(
|
||||
// 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,
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
**`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`.
|
||||
@@ -259,7 +262,7 @@ get readyPromise(): Promise<void>; // resolves when the subscription is synced
|
||||
public close(): void;
|
||||
```
|
||||
|
||||
So the constraint on the bet: the target can already answer "synced?" (`readyPromise`), and `useShape` today returns "an empty set, if still loading" (its own doc comment, `useShape.ts:29-31`) — indistinguishable from synced-empty. `watchShape` surfaces the distinction with a TanStack-`useQuery`-minimal vocabulary (`isPending`/`isSuccess`/`isError`), which is a **shape of this library's choosing**. If the future hook exposes load state under different names, the consumer's binding code changes; the underlying distinction it teaches (pending ≠ empty) is target-expressible and safe to learn.
|
||||
So the constraint on the bet: the target can already answer "synced?" (`readyPromise`), and `useShape` today returns "an empty set, if still loading" (its own doc comment, `useShape.ts:29-31`) — indistinguishable from synced-empty. `useShapeQuery` surfaces the distinction with a TanStack-`useQuery`-minimal vocabulary (`isPending`/`isSuccess`/`isError`). That is **not** a shape of this library's choosing any more: NextGraph's developers have stated it is the direction, and the statement is on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md` with its date — which is what the symbol's `announced-not-written` annotation cites. Deviating toward an announced target is alignment; the distinction it teaches (pending is not empty) is target-expressible either way.
|
||||
|
||||
**But `readyPromise` answers two of those three questions, not three — VERIFIED 2026-08-17, and it bears on `isError` specifically.** It is constructed with `resolve` alone (`new Promise<void>((resolve) => { this.resolveReady = resolve; })`, `GraphOrmSubscription.ts` constructor), nothing anywhere rejects it, and `resolveReady()` is called on one path only — the arrival of initial data. The subscription that fails does not settle it: `orm_start_graph` is awaited inside a `try` whose `catch` is `console.error(e)`, so the promise stays pending forever and every `await this.readyPromise_` behind it hangs. Upstream's "I could not find out" IS its "still pending". So `isPending` and `isSuccess` map onto something real at level 3, while `isError` has **no counterpart at any level** — it is this library's third state, and the reason it exists is that swallowing a failed read into a plausible-looking result is the shape that took this project a week to close everywhere else. A consumer should keep reading it; if the future hook ships without one, the failure is the pending that never ends, which is a worse thing to have to render, not a reason to have learned less.
|
||||
|
||||
@@ -554,13 +557,16 @@ The escaping helpers were published until the surface was narrowed. Their remova
|
||||
|
||||
## 12. Scope resolution, per-entity documents, and the store registry
|
||||
|
||||
### Today — `@ng-eventually/polyfill` (namespace `storeRegistry`) — plus `Scope` from `types.ts`
|
||||
### Today — `@ng-eventually/polyfill` (namespace `storeRegistry`)
|
||||
|
||||
> **Narrowed twice.** 2026-08-03 the entry stopped re-exporting the whole `store-registry` module and kept an app-facing slice (`src/surface/placement.ts`). 2026-08-05 that slice lost its two inbox-ADDRESS functions as well: an application deposits with `inbox.postToDocument(doc, …)` and shares with `inbox.share(doc, toUser)` — always naming a document or a person, never an address, because upstream an address is resolved from a profile and never handled by a caller. **Five functions remain published**, listed first below; everything after them is kept for the record and is covered by `docs/internal-contract.md`.
|
||||
|
||||
```ts
|
||||
// types.ts:38 — NB: NOT the ORM's Scope (a graphs/subjects filter); this is the store scope
|
||||
export type Scope = "public" | "protected" | "private";
|
||||
// `Scope` (this package's, `"public" | "protected" | "private"`) was published here until
|
||||
// `1.0.0-dev.5`. It is now `StoreType`, DEFINED in `types.ts` and published nowhere: an
|
||||
// application names a store by the reference one of the three accessors below hands it.
|
||||
// The three strings are upstream's own (`doc_create`'s `store_type`), but the name `Scope`
|
||||
// was covering two questions at once — see § 5 for the other one.
|
||||
|
||||
// store-registry.ts:90,234
|
||||
// VirtualUserRecord is INTERNAL (shape kept here for the ruling below).
|
||||
@@ -584,12 +590,26 @@ interface RegistrySession {
|
||||
// identity to every placement call was a gesture with no successor — and it forced an
|
||||
// application to KNOW its identity, which it could only do by reading the access gate's
|
||||
// private storage key. `ensureIdentity()` returns it now; these take it from the session.
|
||||
export async function createEntityDoc(scope: Scope): Promise<Nuri>;
|
||||
export async function listMyEntityDocs(scope: Scope): Promise<Nuri[]>;
|
||||
export async function resolveScopeGraph(scope: Scope): Promise<Nuri>;
|
||||
export async function resolveWriteGraph(scope: Scope): Promise<Nuri>;
|
||||
// A store is named by its REFERENCE, since `1.0.0-dev.5`. Upstream the session hands the
|
||||
// application its three store ids (`Session.private_store_id | protected_store_id |
|
||||
// public_store_id`) and `doc_create` takes one of them, the private store when none is
|
||||
// given — so these three accessors answer what the session answers, and `createEntityDoc`
|
||||
// takes one of them exactly where `doc_create` takes its store.
|
||||
export async function privateStore(): Promise<Nuri>;
|
||||
export async function protectedStore(): Promise<Nuri>;
|
||||
export async function publicStore(): Promise<Nuri>;
|
||||
export async function createEntityDoc(store?: NuriLike): Promise<Nuri>;
|
||||
export async function openDocumentInbox(doc: NuriLike): Promise<Nuri>;
|
||||
|
||||
// UNPUBLISHED in `1.0.0-dev.5`, each for its own reason:
|
||||
// listMyEntityDocs — no counterpart at any level (no `doc_list`, nothing in the
|
||||
// binding). Listing what one created is an ordinary query over the
|
||||
// store's `ldp:contains`. Still internal: the read path calls it.
|
||||
// resolveScopeGraph — it handed out a value whose stated use was a READ scope, and a
|
||||
// store's graph holds containment triples and nothing else, so
|
||||
// upstream that query matches nothing and says so with an empty set.
|
||||
// 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,
|
||||
// addLink, readLinks, resolveAccount, ensureAccount, reservedAccount,
|
||||
@@ -675,10 +695,10 @@ The entry used to say `export * from "./model/types"`, a blanket re-export publi
|
||||
> **A type is published only if a PUBLISHED SIGNATURE uses it.**
|
||||
|
||||
```ts
|
||||
export type { Nuri, NuriLike, Scope, PrincipalId, NgLike, UseShapeLike } from "./model/types";
|
||||
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` types `storeRegistry.*` and `watchShape` (§ 12, § 5); `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` and `UseShapeLike` type the two injected objects in `EventuallyConfig` (§ 1).
|
||||
|
||||
Two types the blanket export published are now internal, each because **nothing published names it**:
|
||||
|
||||
@@ -705,12 +725,12 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
|
||||
### Places the current surface teaches something to unlearn
|
||||
|
||||
- ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `watchShape`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery".
|
||||
- ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `useShapeQuery`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery".
|
||||
- ~~**`share` is importable from both entries.**~~ **FIXED 2026-08-07** with the entry merge: there is one entry and one `share`, under `inbox`.
|
||||
- **One entry means the import line no longer says what disappears.** Until 2026-08-07 a second import path (`/polyfill`) WAS the deletion list. It is now the `POLYFILL-ERA` block in `src/index.ts`, this appendix's note above, and the per-subject rulings in this document. That is a documentation-carried signal where it used to be a mechanical one — the appendix is pinned by a test, the grouping is not.
|
||||
- **`inbox.read` as a mailbox** — enumerating raw deposits is emulation detail (§ 9); the durable contract is deposit-and-it-gets-applied. An app building UI on the deposit list should expect that surface to change shape entirely.
|
||||
- **`watchShape`'s "planned `useShape` upgrade"** — stated in the module header with no provenance in this repo or the clone (§ 5). The load-state *distinction* is safe; the claim that NextGraph plans this exact hook shape is an assumption and must not be cited as an announced API.
|
||||
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `watchShape`'s design already assumes).
|
||||
- ~~**`watchShape`'s "planned `useShape` upgrade"**~~ **ANSWERED in `1.0.0-dev.5`.** The claim was in the module header with no provenance anywhere. It has one now, and it is the strongest kind available to something the target has not written: NextGraph's developers stated the direction, the statement is written down in `.project/rules/nextgraph/surface-mirrors-the-sdk.md` with who said it and when, and the symbol carries `kind=announced-not-written` citing that record. The name went with it — `useShapeQuery`, not `watchShape` (§ 5).
|
||||
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `useShapeQuery`'s design already assumes).
|
||||
- **The sync-returning `subscribeDoc` unsubscribe** vs the target's promise-resolved one (§ 8) — a deliberate, documented ergonomic delta; an adapter is one line at migration, but it is a delta.
|
||||
|
||||
---
|
||||
@@ -722,10 +742,10 @@ 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, watchShape
|
||||
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
|
||||
docs: docCreate, sparqlQuery, sparqlUpdate
|
||||
inbox: Deposit, PostOptions, post, postToDocument, processInbox, read, readForDocument, readSynced, readSyncedForDocument, share, watch
|
||||
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
||||
storeRegistry: createEntityDoc, openDocumentInbox, privateStore, protectedStore, publicStore
|
||||
```
|
||||
|
||||
**Of these, exactly ONE is polyfill-era with no target counterpart** — `configure` (plus
|
||||
|
||||
Reference in New Issue
Block a user