feat!: useShapeQuery lit par la requete de forme du SDK
This commit is contained in:
@@ -16,6 +16,7 @@ This package covers placement (creating an application's documents in one of its
|
||||
An application using this package must:
|
||||
|
||||
- serve a wallet file (`.ngw`) from its own bundle, and pass its URL and password to `configure` as `sharedWallet: { fileUrl, password }`;
|
||||
- pass `configure` the SDK's shape query (`ormSubscription: RdfOrmSubscription.getOrCreate`, from `@ng-org/orm`) and its `initNg`, and call this package's `initNg(ng, session)` once a session has arrived, if it uses `useShapeQuery`. That read goes through the SDK now, and the SDK is injected here rather than imported; without the injection the read publishes `isError` naming the field;
|
||||
- call `init(…)` — this package's, not the one it passed to `configure` — and then await `ensureIdentity()`, in a browser context, before rendering its interface. `ensureIdentity()` resolves once a session is open, and a session arrives only through `init`: awaited before `init` has been called, it throws and names the call to make first;
|
||||
- **declare this package exactly once**, so that everything in the application resolves to one copy of it — see the single-copy rule under `## Non-guarantees`, which is the one packaging mistake this surface cannot protect you from.
|
||||
|
||||
@@ -31,6 +32,9 @@ export function configure(c: EventuallyConfig): void;
|
||||
export interface EventuallyConfig {
|
||||
ng: NgLike; // the `ng` object from @ng-org/web
|
||||
useShape: UseShapeLike; // `useShape` from @ng-org/orm
|
||||
// `RdfOrmSubscription.getOrCreate` from @ng-org/orm - the SDK's shape query, which
|
||||
// `useShapeQuery` reads through. Pass it as it stands; no wrapper, no cast.
|
||||
ormSubscription?: OrmSubscriptionLike;
|
||||
sharedWallet?: SharedWalletConfig; // { fileUrl, password, importUrl? }
|
||||
debugAccessLog?: boolean;
|
||||
init?: (...args: any[]) => any;
|
||||
@@ -61,15 +65,19 @@ export const storeRegistry: { // no identity parameter — a session is
|
||||
export async function readUnion(docs: NuriLike[]): Promise<UnionSubject[]>;
|
||||
export interface UnionSubject { subject: string; graph: Nuri; props: Record<string, string[]> }
|
||||
export function useShape(shapeType: unknown, scope: unknown): unknown; // read-filtered view
|
||||
// TWO positional arguments — the same pair `useShape` takes, and the same `Scope`.
|
||||
// `ShapeQuery` is what you READ BACK (the snapshot), never what you pass in.
|
||||
export function useShapeQuery<T = UnionSubject>(shapeType: unknown, scope: Scope): ShapeObservable<T>;
|
||||
export interface ShapeObservable<T = UnionSubject> {
|
||||
// TWO positional arguments — the same pair `useShape` takes, both of them the SDK's own
|
||||
// types. `ShapeQuery` is what you READ BACK (the snapshot), never what you pass in, and
|
||||
// its items are the SHAPE's objects: `@id`, `@graph` and the shape's readable predicates.
|
||||
export function useShapeQuery<T extends BaseType = BaseType>(
|
||||
shapeType: ShapeType<T>,
|
||||
scope: Scope,
|
||||
): ShapeObservable<T>;
|
||||
export interface ShapeObservable<T extends BaseType = BaseType> {
|
||||
getSnapshot(): ShapeQuery<T>; // stable reference until the state changes
|
||||
subscribe(onChange: () => void): () => void; // returns its own unsubscribe
|
||||
refetch(): void; // re-resolve and re-read now; never polls
|
||||
refetch(): void; // ask again now; never polls
|
||||
}
|
||||
export interface ShapeQuery<T = UnionSubject> {
|
||||
export interface ShapeQuery<T extends BaseType = BaseType> {
|
||||
data: T[]; isPending: boolean; isSuccess: boolean; isError: boolean; error: unknown;
|
||||
}
|
||||
export function subscribeDoc(nuri: NuriLike, onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
|
||||
@@ -157,9 +165,11 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
**`storeRegistry.openDocumentInbox(doc)` is idempotent, including when calls overlap.** Asks for the same document that are in flight together are answered by one call, and every one of them gets the same inbox — you do not have to serialise them yourself, and firing one per component as they mount is a supported way to use it. This holds **within one page**; two pages doing it in the same moment is a non-guarantee below, and it is the only part of this you have to think about.
|
||||
|
||||
**A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds no document of that shape — the distinction the surface exists for. Until 2026-08-17 a scope whose listing did not answer published that very snapshot, so an interface showed "you have created nothing" for "the store did not answer"; it now publishes `isError` carrying the error. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too.
|
||||
**A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds nothing of that shape — the distinction the surface exists for, and the one thing it will never do is publish that snapshot for a read that did not happen. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too.
|
||||
|
||||
**A read is scoped by document and by subject, never by store.** `Scope` has two axes and there is no third: `graphs` names one document's graph per entry — `did:ng:i` or `""` for the whole local dataset, `[]` or `undefined` for no objects at all — and `subjects` filters subject IRIs literally, with no prefix or hierarchy matching. There is no call that lists the documents in a store, and naming a store in `graphs` is not the question you meant: a store's graph holds its containment triples and nothing of the shape you asked for, so it answers empty rather than raising. That is the target's own shape, not a narrowing this package chose.
|
||||
**Which failure lands in `isError` changed in `1.0.0-dev.6`, and one of them now stays PENDING.** The read goes through the SDK's own shape query, so what `isError` carries is the query failing to open: a shape the ORM cannot subscribe, or a `configure` that was never given `ormSubscription`. A start the underlying engine REFUSES does not reach it: NextGraph catches that failure and logs it, and its readiness signal is only ever resolved, never rejected, so such a read stays `isPending` indefinitely. The guarantee above is unaffected — you are still never told "there is nothing" for "nobody answered" — but an interface that handles only `isError` will show its loading state where it used to show an error. Handle a `isPending` that does not end.
|
||||
|
||||
**A read is scoped by document and by subject, never by store.** `Scope` has two axes and there is no third: `graphs` names one document's graph per entry — `did:ng:i` or `""` for the whole local dataset, `[]` or `undefined` for no objects at all — and `subjects` filters subject IRIs literally, with no prefix or hierarchy matching. There is no call that lists the documents in a store, and naming a store in `graphs` is not the question you meant: a store's graph holds its containment triples and nothing of the shape you asked for, so it answers empty rather than raising. That is the target's own shape, not a narrowing this package chose — and since `1.0.0-dev.6` nothing is translated on the way: the `Scope` object you write is the object the SDK receives.
|
||||
|
||||
**A deposit made while a person is looking at the page arrives while they are looking at it.** For as long as an identity is connected, every inbox it may read is watched and what lands in one is applied as it lands — its own inbox, and the inbox of every document it has opened one on, including a document whose inbox it opens later in the same session. So a `ReadCap` sent with `inbox.share` becomes usable in the recipient's live session with no reload and no call from the application, and a `useShapeQuery` that was empty for want of that `ReadCap` re-reads and publishes the document it now opens. Until 2026-08-17 only the backlog waiting at connection was applied, and a deposit made in front of its recipient converged only when that person reloaded the page.
|
||||
|
||||
@@ -183,7 +193,7 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
**Naming an identity proves nothing about who named it.** Any visitor may type any identifier at the barrier, or arrive on a URL that already carries one, and act as that identity — the wallet and its password are handed out on the barrier itself. The identifier is a choice of space, never a proof: anyone who knows one can act as it, so do not treat `ensureIdentity()`'s answer as an authenticated subject.
|
||||
|
||||
**No live read through `useShape` against a deployed broker.** The reactive subscription it opens is dispatched by method name at two hops below this package, and a broker deployed before that name last changed does not recognise it: the read never starts, its set stays empty, its readiness never settles, and one console error is the only trace — indistinguishable from "this scope is empty". The cause is upstream in NextGraph and the remedy is a broker redeployment; nothing in this package can work around it. Use `useShapeQuery` for a reactive read and `readUnion` for a one-shot one — neither goes through that path.
|
||||
**A shape query the engine refuses never finishes loading.** Both shape reads — `useShape` and `useShapeQuery` — open a subscription through NextGraph's ORM, and a start the engine refuses is caught there, written to the console and nothing more: the set stays empty, its readiness never settles, and the only trace is that one console line. `useShapeQuery` reports it as a `isPending` that does not end (see the guarantee above); `useShape` returns a set you cannot tell from an empty one. The cause is upstream and nothing in this package can work around it. This was a total outage until the client packages caught up with the deployed broker on 2026-09-21; it is the residual failure mode, not the ordinary case.
|
||||
|
||||
**No revocation.** `inbox.share` cannot be undone.
|
||||
|
||||
@@ -201,6 +211,8 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
**No unfiltered read through `useShape`.** Members that yield items are filtered and mutations pass through; anything else throws. A document reached through that view alone, read nowhere else first, does not appear.
|
||||
|
||||
**Neither shape read fetches a public store's key for you.** A document in a public store gives up its key to whoever asks, but asking is a round-trip and neither `useShape` nor `useShapeQuery` spends one — since `1.0.0-dev.6` the second reads through the SDK and no longer opens a document of its own. So a public document you have read nowhere else does not appear in either. Read it once by naming it (`readUnion`, `docs.sparqlQuery`) and both pick it up: the key lands in what you hold, and that is a change both of them re-read on.
|
||||
|
||||
**A watch on one inbox that could not be opened does not come back on its own.** Opening it can fail — a broker that does not answer in that moment — and the failure is reported rather than passed over, but what follows is event-driven and this package deliberately never polls. The watch is opened again at the next moment this identity comes to hold something it did not: it creates a document, it opens an inbox on a document, a `ReadCap` reaches it through an inbox still being watched, it reads a public-store document for the first time. A session that does none of those goes on without that inbox — deposits made into it are not applied, nothing raises, and they wait unconsumed for the next connection. The exposed case is the identity that only ever READS, since it produces none of those events, where an identity that goes on creating recovers as a by-product of its own work. Awaiting `ensureIdentity()` again applies what is waiting — it drains every one of this identity's queues before it resolves, and shows no barrier a second time — but the watching itself comes back only on a fresh page.
|
||||
|
||||
## Change policy
|
||||
@@ -209,17 +221,25 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
What each level means here, in this package's own terms:
|
||||
|
||||
- **major** — a published symbol is removed (`getSession`, `normalizeId`, `currentUser`, `RegistrySession`, `inbox.materialize`, `storeRegistry.listMyEntityDocs`, `resolveScopeGraph` and `resolveWriteGraph` all left this way), **or** a published symbol is renamed (`watchShape` became `useShapeQuery`), **or** an existing call narrows: it now throws where it returned (`ensureIdentity` rejects rather than resolve on an incomplete restore), or it reports a state you did not have to handle before (`useShapeQuery` publishing `isError` where it used to publish a synced-empty snapshot). A signature change a caller must react to counts; one that only accepts more than before does not.
|
||||
- **major** — a published symbol is removed (`getSession`, `normalizeId`, `currentUser`, `RegistrySession`, `inbox.materialize`, `storeRegistry.listMyEntityDocs`, `resolveScopeGraph` and `resolveWriteGraph` all left this way), **or** a published symbol is renamed (`watchShape` became `useShapeQuery`), **or** an existing call narrows: it now throws where it returned (`ensureIdentity` rejects rather than resolve on an incomplete restore), it reports a state you did not have to handle before (`useShapeQuery` publishing `isError` where it used to publish a synced-empty snapshot), or what it HANDS BACK changes shape (`useShapeQuery`'s `data` carrying the shape's own objects where it carried property bags). A signature change a caller must react to counts; one that only accepts more than before does not.
|
||||
- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way).
|
||||
- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`.
|
||||
|
||||
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.5`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
|
||||
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.6`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
|
||||
|
||||
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.5` is this package, `ng-e2e-helpers/v…` is the other one. A bare `v…` tag would say nothing about which surface it froze the day the two diverge — which is the day one of them takes a major and the other does not.
|
||||
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.6` is this package, `ng-e2e-helpers/v…` is the other one. A bare `v…` tag would say nothing about which surface it froze the day the two diverge — which is the day one of them takes a major and the other does not.
|
||||
|
||||
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.5`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.5`, the string you pinned, never the version it is heading for.
|
||||
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.6`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.6`, the string you pinned, never the version it is heading for.
|
||||
|
||||
`1.0.0-dev.5` is the largest **major** of the line so far: it renames one call, changes the parameter of two, and removes three. Every change is one step toward the surface NextGraph will ship, so none of them is reversible by asking. Symbol by symbol, with what replaces it:
|
||||
`1.0.0-dev.6` is a **major**, and all of it is one change: `useShapeQuery` reads through NextGraph's OWN shape query instead of a read this package built. Nothing was renamed and nothing was removed from the entry, but three things move under a caller:
|
||||
|
||||
- **`data` items are the shape's objects, not property bags.** `ShapeQuery<T>`'s `T` is now the shape's type: each item carries `@id`, `@graph` and one field per predicate the shape DECLARES, under that predicate's readable name. `note.props["urn:notebook:title"]?.[0]` becomes `note.title`, and `note.graph` becomes `note["@graph"]`. **A predicate your shape does not name is not projected**, so a shape written only to match — an `rdf:type` constraint and nothing else — now comes back with no fields at all: declare the ones you read. `readUnion` is unchanged and still returns `UnionSubject`.
|
||||
- **`shapeType` is typed.** It was `unknown`; it is `ShapeType<T>` (the SDK's own, re-exported here), so the snapshot is typed without a cast — and a hand-written shape object must now satisfy that type, which means `readablePredicate`, `minCardinality` and `maxCardinality` on every predicate.
|
||||
- **`configure` takes one more thing.** `ormSubscription: RdfOrmSubscription.getOrCreate`, plus `initNg` and a call to this package's `initNg(ng, session)` once the session has arrived. Optional in the type, required in practice for this read: without it `useShapeQuery` publishes `isError` naming the field.
|
||||
|
||||
And one guarantee narrows, which is the part to read twice: a shape query the engine REFUSES no longer reaches `isError` — it stays `isPending` for ever, because NextGraph swallows that failure. See `## Guarantees`.
|
||||
|
||||
`1.0.0-dev.5` before it was the largest **major** of the line: it renames one call, changes the parameter of two, and removes three. Every change is one step toward the surface NextGraph will ship, so none of them is reversible by asking. Symbol by symbol, with what replaces it:
|
||||
|
||||
- **`watchShape(shapeType, scope)` → `useShapeQuery(shapeType, scope)`.** The name is the target's own; `watchShape` appears nowhere in NextGraph and spends a verb the ORM already uses for signal observation. Everything you read back is unchanged — the same `ShapeObservable`, the same `ShapeQuery` snapshot, the same three states.
|
||||
- **`Scope` is no longer `"public" | "protected" | "private"`.** The published `Scope` is now the SDK's own, `{ graphs?: string[] | string; subjects?: string[] }`, re-exported from `@ng-org/orm`, and it is what `useShapeQuery` takes. `watchShape(Shape, "protected")` becomes `useShapeQuery(Shape, { graphs: "did:ng:i" })` for everything you hold, or `useShapeQuery(Shape, { graphs: [doc1, doc2] })` for named documents. **There is no translation for "the documents in my protected store"**: that question is not expressible against the target, and the three strings were this package's own way of asking it.
|
||||
|
||||
Reference in New Issue
Block a user