Trouve en balayant le correctif de ce round plutot qu-en raisonnant dessus. `Object.setPrototypeOf(vue, ...)` remplacait le prototype de la VRAIE collection - apres quoi l-iterer leve pour toutes les identites, c-est le probleme de `clear` dans une autre tonalite - et `Object.preventExtensions`, par quoi `Object.freeze` commence, la rendait non extensible : une ecriture, et de surcroit la projection d-index de `ownKeys` n-est licite que tant que la cible est extensible.
72 KiB
type, summary
| type | summary |
|---|---|
| contract | The API @ng-eventually/polyfill exposes to an application — signatures, guaranteed behaviour, and what it does not offer |
contract_polyfill-surface — @ng-eventually/polyfill
Scope
This package is a polyfill of NextGraph's SDK.
This package covers placement (creating an application's documents in one of its three stores), reading (a document's subjects, one-shot or reactive, by shape), writing one shape-typed object, sharing a document with a named user, and depositing into inboxes. It does not cover user management, display names, transport, or the operation of a deployment.
Deployment requirements
An application using this package must:
- serve a wallet file (
.ngw) from its own bundle, and pass its URL and password toconfigureassharedWallet: { fileUrl, password }; - pass
configurethe SDK's shape query (ormSubscription: RdfOrmSubscription.getOrCreate, from@ng-org/orm) and itsinitNg, and call this package'sinitNg(ng, session)once a session has arrived, if it usesuseShapeQuery. That read goes through the SDK now, and the SDK is injected here rather than imported; without the injection the read publishesisErrornaming the field; - pass
configurethe SDK'sgetObjectsandinsertObject(from@ng-org/orm, as they stand) if it calls the calls of the same name here. Same reason and same shape as the shape query: they are injected rather than imported, and without the injection each throws naming the field to pass; - call this package's
initNg, not the SDK's, whenever it uses anything of the ORM. That call decides whichngthe ORM reaches the engine through, and this package substitutes its own - the same objectngpublishes - so that an ORM write meets the same guard every other write meets. Calling@ng-org/orm'sinitNgdirectly leaves ORM writes ungoverned; - call
init(…)— this package's, not the one it passed toconfigure— and then awaitensureIdentity(), in a browser context, before rendering its interface.ensureIdentity()resolves once a session is open, and a session arrives only throughinit: awaited beforeinithas 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.
Obtaining it. This package is not published to a public registry, and it is not distributed as built output: its published entry point is TypeScript source, so whatever builds the application is what compiles it, and a toolchain that accepts only JavaScript cannot consume it as it stands. By which channel the source reaches a given application is agreed with that application rather than fixed here; what this contract fixes is the version you pin and the rules below.
Surface
Full typed shape: the package's types entry, @ng-eventually/polyfill. A type is published only when a published signature uses it. The load-bearing signatures:
// ── bootstrap ────────────────────────────────────────────────────────────
export function configure(c: EventuallyConfig): void;
export interface EventuallyConfig {
ng: NgLike; // the `ng` object from @ng-org/web
// The ORM's REACT hook. It is NOT on `@ng-org/orm`'s main entry - that resolves to
// `core`, which exports no `useShape` - so import it from `@ng-org/orm/react` (the
// default export) or as `reactUseShape` from `@ng-org/orm/all`. Pass it as it
// stands; no wrapper, no cast.
useShape: UseShapeLike;
// `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;
getObjects?: GetObjectsLike; // `getObjects` from @ng-org/orm
insertObject?: InsertObjectLike; // `insertObject` from @ng-org/orm
sharedWallet?: SharedWalletConfig; // { fileUrl, password, importUrl? }
debugAccessLog?: boolean;
init?: (...args: any[]) => any;
initNg?: (...args: any[]) => any;
}
// ── identity — one await before the application renders ──────────────────
export async function ensureIdentity(): Promise<PrincipalId>; // returns who you are
// ── addressing ───────────────────────────────────────────────────────────
export type Nuri = `did:ng:${string}`;
export type NuriLike = Nuri | string;
// The SDK's own scope, re-exported unchanged. It scopes a READ, on two axes and no third.
export type Scope = { graphs?: string[] | string; subjects?: string[] };
// ── placement: where an application's documents live ─────────────────────
// A store is named by its REFERENCE — one accessor per store, and `createEntityDoc` takes
// one of them. Omit it and the document goes to the private store.
export const storeRegistry: { // no identity parameter — a session is one user's
privateStore(): Promise<Nuri>;
protectedStore(): Promise<Nuri>;
publicStore(): Promise<Nuri>;
createEntityDoc(store?: NuriLike): Promise<Nuri>;
openDocumentInbox(doc: NuriLike): Promise<Nuri>;
};
// ── reading ──────────────────────────────────────────────────────────────
// Reading a document BY NAME is `docs.sparqlQuery` below - one anchored query, one
// document. `readUnion` and `UnionSubject` were published here until `1.0.0-dev.8`;
// nothing takes their place, because they were that call in a loop plus policies of
// this package's own. See `## Change policy`.
export function useShape<T extends BaseType>( // read-filtered view
shapeType: ShapeType<T>,
conf: RdfOrmConfig<T> | string | undefined,
): ShapeResult; // { data, promise, subscription, isLoading, nextPage?, previousPage? }
// 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; // ask again now; never polls
}
export interface ShapeQuery<T extends BaseType = BaseType> {
data: T[]; isPending: boolean; isSuccess: boolean; isError: boolean; error: unknown;
}
// The ORM's one-shot read, at the SDK's own name and signature. `config` is a `Scope`
// plus `where` and `orderBy`; the result is an ARRAY when it carries an `orderBy` and a
// `Set` otherwise, and it is a CLONE - mutating or deleting an item of it reaches nothing.
export async function getObjects<T extends BaseType>(
shapeType: ShapeType<T>,
config: Omit<RdfOrmConfig<T>, "pageSize" | "maxActivePages">,
): Promise<T[] | Set<T>>;
export type RdfOrmConfig<T> = Scope & { where?: ...; orderBy?: ... }; // the SDK's own
export function subscribeDoc(nuri: NuriLike, onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
// ── low-level document / SPARQL primitives ───────────────────────────────
export const docs: {
// `sessionId` is `string | number` — upstream's own declared type (`Session.session_id`).
// It is RELAYED, never converted: the wasm side deserializes a `u64`, and stringifying it
// fails for real (`Deserialization error of session_id JsValue("1")`).
docCreate(sessionId: string | number, crdt: string, cls: string, dest: string, store?: unknown): Promise<Nuri>;
sparqlQuery(sessionId: string | number, query: string, base?: string, anchor?: NuriLike, label?: string): Promise<unknown>;
// Returns the commits the update produced, as upstream does (it typed this `void` until
// 2026-08-14 while already relaying the value). A caller that ignores it is unaffected.
sparqlUpdate(sessionId: string | number, query: string, anchor?: NuriLike, label?: string): Promise<unknown>;
};
// -- writing one shape-typed object ---------------------------------------
// The SDK's own name and signature. The object MUST carry `@graph` - it is the only
// thing that says which document the write lands in - and what comes back is the object
// with its `@id` filled in where one was left empty.
export async function insertObject<T extends BaseType>(shapeType: ShapeType<T>, object: T): Promise<T>;
// ── inbox: giving to read, and depositing ────────────────────────────────
export const inbox: {
share(doc: NuriLike, toUser: string): Promise<void>; // give a reader the key
post(targetInbox: NuriLike, opts: PostOptions): Promise<void>;
postToDocument(doc: NuriLike, opts: PostOptions): Promise<void>;
read(targetInbox: NuriLike): Promise<Deposit[]>; // only your own
readForDocument(doc: NuriLike): Promise<Deposit[]>;
readSynced(targetInbox: NuriLike): Promise<Deposit[]>;
readSyncedForDocument(doc: NuriLike): Promise<Deposit[]>;
processInbox(targetInbox: NuriLike): Promise<Deposit[]>;
watch(targetInbox: NuriLike, onDeposits: (d: Deposit[]) => void): () => void;
// `materialize` (a second published name for `read`) was REMOVED on 2026-08-14 —
// an alias with no call site, and no counterpart upstream. Use `read`.
};
export interface Deposit { from: PrincipalId | null; payload: unknown; ts: number }
// ── the wrapped SDK objects ──────────────────────────────────────────────
export const ng: NG; // call this instead of the `ng` passed to `configure`
// `NG` is upstream's own type (`@ng-org/web`), fully typed
// members; it was `Record<string, any>` until 2026-08-14
export function init(...args: any[]): any; // likewise — not the `init` passed to `configure`
export function initNg(...args: any[]): any;
Guarantees
Every entry accepts NuriLike and validates at the door; what it returns is a precise Nuri. No type guard is published.
A returned reference carries no key — not createEntityDoc, not the three store accessors. A reference found inside a document yields a name, not a key.
You read a document whose key you hold: you created it, it was shared with you, or it sits in a public store, which serves its read key to whoever asks. No call answers "may I read this?".
What was shared with you becomes readable after ensureIdentity().
An anchored docs.sparqlQuery reads exactly the document you name, and returns its triples as written. The anchor makes that document's graph the query's default graph, so a body with no GRAPH <...> wrapper reads that document and nothing else; a body that DOES carry one iterates the named graphs regardless of the anchor, so do not write one. Subjects come back as the document carries them — several subjects in one document is allowed and each stays its own, since a subject may be any IRI. Reading several documents is this call once per document; there is no call that takes a list, and composing one with an anchorless query instead would silently widen what the read reaches. Recommended placement is one document per business entity: access is granted per document.
urn:ng-eventually: is reserved, and the reservation has two faces. In a document: this package writes its own registers under subjects carrying that prefix, in the same graph as your data — so an anchored docs.sparqlQuery returns them alongside your triples, and it is yours to ignore them. Until 1.0.0-dev.8 the removed readUnion dropped them for you; nothing does now. This is not a gain of rights: the two registers an ordinary document carries are a document's inbox address and, for a public document, the read key its store already serves to whoever asks. Nothing else of this package's is written into your documents. Do not write under that prefix yourself. In a deposit: two payload shapes are this package's own and it acts on them — a payload that IS a reference (a bare did:ng:… string and nothing else), and an object whose kind property is a string starting with urn:ng-eventually:inbox:. Every other payload is yours, and is delivered to you untouched.
Do not use either shape to carry your own data. Depositing one is still accepted and it is still returned to you by inbox.read and its variants — nothing is withheld — but this package now also acts on it, and what you observe is one of two things: it writes an entry into the document the inbox belongs to, if that document is one it can index; otherwise it reports a refusal on its own log stream (console.error, ungated, as below). That refusal is reported ONCE and then the deposit is gone: this package consumes what it claims, whether or not the arm succeeded. Until 1.0.0-dev.4 the same refusal came back at every arrival on that inbox and at every connection, for as long as the deposit sat there. The same now applies when the referenced document resolves but carries nothing for the field its index declares — a draft, say: the reference is discarded and reported, where before it sat in the queue and would have been indexed by a later drain once the document gained that field. Deposit the reference again after the document says what it needs to say. Carry your own data in an object with a kind of your own — { kind: "join", … } is yours and stays yours — and never as a bare reference on its own.
Only a document's owner writes to it. Holding its read key never grants a write. Since 1.0.0-dev.7 that covers what you write through NextGraph's ORM too - insertObject, and a mutation of the live set a shape query handed you (set.add(...), set.delete(...), assigning a property). It used to cover only docs.sparqlUpdate: an ORM write leaves by a different door, and nothing was standing at it. Refusal happens before the write is attempted, and one document you may not write refuses the whole batch the ORM was flushing.
getObjects(shapeType, config) answers with what you hold, and nothing else. Objects living in documents you have no key for are not in the result - the same rule the two shape reads follow, applied per document on what comes back. The collection keeps the shape the SDK gave it: an orderBy in the config yields an array, anything else a Set. What you get is a CLONE, detached from the database: changing an item of it changes nothing anywhere, and deleting an item of it removes nothing and reports nothing.
insertObject(shapeType, object) needs the object's @graph, and refuses a document you do not own. The @graph is the only thing that says which document the write lands in - upstream the insert subscribes to no document at all - and it is also what decides the refusal, so there is nothing extra to pass. An object with no @graph is forwarded and NextGraph refuses it in its own words.
"This document has no inbox" is an answer, and only an answer, since 1.0.0-dev.9. inbox.postToDocument refuses a document whose owner never opened an inbox, and inbox.readForDocument / readSyncedForDocument answer [] for one - both unchanged, both still the ordinary case. What changed is that a read which could not HAPPEN no longer arrives as one of those: a broker that cannot answer for the document, or a public store that cannot be asked for its key, now rejects. Until this version a depositor was told the owner had opened nothing, and a reader was handed an empty list, for a document whose inbox is full.
inbox.share(doc, toUser) names the document and the person; the recipient calls nothing. It refuses a recipient nobody has signed in as, rather than creating them.
inbox.post refuses a target that is not an inbox; to reach a document's owner, use inbox.postToDocument(doc, …). Anyone may deposit into an inbox; only its owner reads it.
You never resolve an inbox address, on either side. You deposit by naming a document (inbox.postToDocument), and you read what was left on your own by naming it too — inbox.readForDocument(doc) at any time, or inbox.readSyncedForDocument(doc) on a page that has just loaded. The second is the one to call when an empty answer has to MEAN empty: a session that has just loaded has synced neither the document nor its inbox, and an unsynced read of either comes back empty with no error — so the ungated form can answer [] for a document whose inbox holds messages. readSyncedForDocument waits for both before answering. inbox.readSynced is the same guarantee on an inbox you already hold the address of, which no application does: it takes an address, so nothing here hands you one.
ensureIdentity() settles the identity, completes the connection work it starts, and returns the identity. It takes no identifier, and no other call takes one.
It resolves only once that work has actually completed: if what was shared with you could not be restored, it throws instead of returning, and a rejected call must not be rendered past — the interface would show an empty account rather than an empty screen. A single queue that could not be drained is reported and does not reject: reaching your queues is infrastructure and must succeed, applying one deposit is data and must never cost you the session. So a resolved call means your own capabilities are restored; it does not promise that every deposit waiting for you has been applied, and those that were not stay in their queue.
ensureIdentity() mounts a full-screen barrier on every top-level load, and takes it down itself — past the broker round-trip it stays down, provided the identifier reached the other side. A person who comes back to the page from that round-trip finds the barrier live again, prefilled, and confirming it hands the page over a second time. The application's own page is never reloaded and nothing outside the barrier is touched.
What decides which identity you get. No call takes an identifier — not configure, not init, not ensureIdentity — so an application never chooses one, never keeps one, and never hands one over. It is settled once per page load, from the page itself, and the answer depends on which side of the broker round-trip is asking:
- Before the round-trip, on your own top-level page — the barrier decides. Whatever is already known fills the field, and the person may change it; the identity is the value they confirm. Confirming publishes that value into the address bar as
?ng-id=and records it in this browser. - After the round-trip, on the page the broker loads back —
?ng-id=decides, and the barrier stays down. The parameter also wins over anything the browser remembers, on either side: it is read first, and reading it replaces what was remembered. Arrive with neither — a URL that dropped the parameter, and a partition that remembers nothing — and the barrier asks on that side too. - Failing both — whatever this browsing context last recorded. This is the only path on which an identity is adopted with nobody confirming it. If the address bar lost
?ng-id=while a different identifier was on record here, that different identity is adopted, and nothing is raised anywhere; if nothing was on record either, the barrier asks again on that side too.
?ng-id= exists because it is the only thing that crosses. Your page before the round-trip and your page after it sit in two separate storage partitions — nothing the browser remembers on one side is visible on the other, and the address bar is the sole channel between them. The package writes the parameter itself, without navigating. An application that owns its URL must let it survive: a router that drops query parameters it does not know, or a redirect that rebuilds the URL, sends the round-trip off without the identifier, and the consequence is the silent mis-identification above rather than an error. In a context that can neither write the address bar nor use storage, the barrier simply asks on both sides.
Being remembered is a prefill, never a decision. A top-level reload asks again every time, with the field already filled — one click, no typing. A remembered identifier is therefore not a signed-in state, and two tabs, two browsers or two devices do not share one: each keeps its own record, and only a URL carrying ?ng-id= puts a second context under the same identity.
The session is the package's, not yours. You never build one, and no call takes one. Call this package's init (not the one you passed to configure): it captures the session the SDK delivers to init's callback and keeps it, then calls your callback with that same event untouched — so an application that wants the session_id for the docs primitives reads it there, and one that does not may pass no callback at all. Identity normalisation is the package's too: @Alice, alice and ALICE are one person.
Where a call must first find out whether something already exists — a document's record in its store, a user's inbox — it throws when it could not find out, instead of proceeding as though the answer were "nothing". So createEntityDoc throws if the document cannot be recorded in its store, and resolving an inbox throws rather than handing back a second one. A rejection means "unknown", never "absent" — retry it or surface it, but do not read it as an empty result.
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 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 document you NAME in graphs is reached before the read opens, since 1.0.0-dev.9. Naming a document in a scope is the same act as anchoring docs.sparqlQuery on it, and it is now answered the same way: the key is asked of a public store if you do not already hold it, the document is refused out loud if you hold no key for it, and its repo is synced before the question is put. A named document you cannot read publishes isError, where until this version it reached the engine, came back, and was emptied by the per-document filter into isSuccess with data: [] - the one snapshot this surface reserves for "this scope holds nothing". A named document that has never synced published that same snapshot for a document that is not empty; it now stays PENDING instead - the sync barrier is consulted on every publication, and a named document whose barrier was not crossed is not an answer. Since 1.0.0-dev.10 that half carries no qualification. It held only within one ordinary session until then, and the two situations that broke it - a second configure(...), which published an empty or stale success, and a broker refusing a subscription after this package had stopped waiting for the document's first push, which left the read withheld with no way back - are closed: "is this document open in this session" now has exactly one answer, the state of the document's one real subscription, rather than three records that could disagree. Nothing failed there, so it is not isError: the subscribe succeeded and the read did not throw, what is missing is the authority to say the document is empty. If the document later syncs and brings data, the read answers by itself. If it never does, the read stays pending for that session - handle a isPending that does not end, as you already must for a shape query the engine refuses. Naming SEVERAL documents fails as a whole when one of them cannot be reached: a short list is the same lie as an empty one, so nothing is answered rather than half of it. What is NOT touched, and each stays a success: graphs: "did:ng:i" or "" (you named no document - the per-document filter is the whole of the answer there, and an empty result means you hold nothing of that shape), graphs: [] (no objects, by NextGraph's own definition), and a named document you can read that holds nothing of the shape.
And it converges by itself. A named scope refused this way is not a dead view. Whether you may reach the documents you named is re-read on every publication rather than decided once, so a key arriving turns the refusal into the document through the same channel that already re-published a filtered read - and a scope whose barrier failed outright is attempted again on that signal. The ordinary sequences all converge with no call from the application: a page whose components subscribe before the connection work has restored its keys, a document shared with you while you are looking at the screen, a broker that hiccupped once and healed, and a key arriving in the same instant the read was deciding. One consequence to design for: an interface that renders isError will briefly show it where it used to show an empty success, so branch on the load state rather than on data alone. Nothing here polls, so a scope refused in a session whose keyring never moves again stays isError until your own refetch() - which is what isError has always meant here, and what it means for every other failure this call reports.
A named scope goes back to PENDING when the session changes under it. Signing in again inside the same page gives a new session, whose repos this package has synced nothing for yet - so a named scope that was answering returns to its loading state until the documents it names are confirmed again, and refetch() is what confirms them. Withholding is deliberate: a barrier crossed in the session that has just gone is not one the new session has crossed, and the alternative is answering from a subscription nothing re-established. This is the one case where a read that had answered stops answering, and it does not arise on an ordinary page load - there the whole page is new.
A standing failure is the SAME snapshot. While a refusal persists unchanged, the snapshot object does not change either, so useSyncExternalStore and anything else comparing by identity see no update - even though the read is being attempted again underneath. A failure that says something different is published normally.
What it costs. One public-store ask and one sync barrier per document named, once per session. The opens for a scope naming several documents run together, so the wait is the slowest of them rather than their sum. The wait itself is bounded internally, so a document that never answers does not hold the open for ever - but the READ over it stays pending, because a barrier that was never crossed is not authority to call anything empty.
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.
The watching is in place by the time ensureIdentity() resolves, whatever else that call made of its own work: a connection that could not restore something still rejects, and the identity it settled is watched all the same. It lasts exactly as long as that identity stays connected — changing identity or clearing it stops it, and whoever connects next is watched in their own right, so nothing of the previous one keeps applying.
A deposit this package applies is delivered ONCE, and a failure is not retried. It is taken out of its inbox before the arm that handles it runs, so it is gone whether that arm succeeded or not — the same order the target has, where the broker removes a message from the queue as it hands it over and the verifier merely logs what it could not apply. A failure is reported on this package's own log stream (console.error, carrying this package's prefix), never gated by debugAccessLog — a diagnostic may be opt-in, a failure may not — and then the next deposit is processed. Do not count on a failed deposit being applied later: there is no retry, no dead-letter queue and no way to ask what was lost. If an arrival matters to your application, the record you keep of it must be your own. What this buys is the end of the opposite behaviour: an inbox that grew for ever, and one malformed deposit re-reported at every arrival and every connection for the life of the session.
An unapplicable item and a failed one are not the same event. A deposit this package cannot make sense of is reported and the next one is processed — one malformed item never holds up the queue behind it. A deposit whose handling could not be COMPLETED is different: the drain STOPS, the inbox is reported, and everything not yet taken out is preserved. Only the deposit in flight is lost. So a failing broker costs one deposit per attempt, not the whole queue, and a drain that stopped is retried by your next ensureIdentity().
A reference this package cannot read counts as failed, not as absent — including a document you hold no capability for. That is deliberate and it costs latency: such a deposit stops one drain, and the deposits behind it land on the next connection instead of this one. The alternative was worse and was measured: treating "cannot read it" as "it is not there" destroyed deposits on a transient outage, and destroyed the re-deposits sent to repair them, because a failed capability fetch is remembered for the session. Only a document actually queried, from a synced repo, that still reads empty is called absent. Consequence to plan for: a queue holding many references you cannot read drains one per connection.
inbox.processInbox now REJECTS on a broker failure, where it used to resolve — including when a document a deposit referenced could not be READ. A referenced document that is merely absent is a different case: that is reported and the queue carries on. Awaiting it, handle the rejection: the deposit it was applying at that moment is already consumed, the rest of that inbox is intact, and calling again resumes from the next one. Connection-time draining is unaffected — ensureIdentity() reports such a queue and still grants the session, exactly as before.
Failing to apply one inbox still denies nothing else. The other inboxes were never involved, the session is not refused, and deposits this package does not claim — your own payloads — are untouched and stay readable through inbox.read and inbox.watch.
Any number of subscriptions on one document coexist. Opening a document, watching an inbox and following a scope no longer silence one another. Until 2026-08-17 a second subscribeDoc on a document killed the first, silently — nothing rejected, the first caller's unsubscribe still appeared to work, and what an application saw was a view that stopped re-reading and an inbox that stopped notifying, with no trace anywhere near the cause. A subscriber that joins a document somebody else already opened is handed the initial State its own subscription would have pushed it, so joining late is not the same as never firing; and unsubscribing silences that caller and no other, including when it happens from inside a push.
Non-guarantees
No display name. ensureIdentity() returns an opaque identifier: do not parse it, split it, or render it as a readable name.
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.
The wrapped ng is a wrapper, and a wrapper can be wrong. It forwards every member to the object you gave configure, adding two refusals of its own and nothing else. Until 1.0.0-dev.7 it did not forward them at all - every member came back as a promise, and calling one failed with NextGraph's own <member> is not a function. The lesson to keep is that a member of ng that misbehaves is worth reporting here rather than worked around: nothing in this package's own code goes through that wrapper, so this surface is the only place such a defect shows.
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 answers with the ORM's own two states, which do not carry the distinction - its isLoading says whether the initial payload has arrived and has no value for "it never will". 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. Since 1.0.0-dev.9 the commonest way in is closed for useShapeQuery: a document NAMED in graphs is opened and refused before the query is handed over, so an unreachable named document reads isError rather than pending for ever. What remains is the start the engine refuses for a document you can perfectly well reach, and everything under graphs: "did:ng:i", where there was no name to check.
No object removal call, and no document deletion at all. There is no deleteObject and no removeDocument at any level of NextGraph, so there is none here. You remove an object by deleting it from the LIVE set a shape query handed you - the item the set is holding, reached by iterating it, by .first() or by .getBy(graph, subject). What that removes is the predicates your SHAPE declares on that subject: triples under a predicate the shape does not name survive, the subject survives, and the document survives. Deleting an item of a getObjects result removes nothing, because that result is a clone. @ng-org/orm@0.1.2-alpha.21 ships a removeObject(graphNuri, subjectIri); it is not published here, because it exists in the package and not in the source this package is verified against, and a symbol whose only evidence is what an npm build happens to export is not something to build a guarantee on.
A refused ORM write does not always reach you as a failed call. insertObject rejects, and you can await it. A mutation of a live set does not: NextGraph batches those into one call it does not await, so a refusal surfaces as an unhandled rejection in the console rather than as an error at the line that made the change - the write did not happen, and nothing in your code was told. Write into documents you own, and treat a document you merely hold the key for as read-only.
Nothing of NextGraph's ORM is re-exported as it stands. getObjects and insertObject are the two this package offers, wrapped. RdfOrmSubscription, DiscreteOrmSubscription, ngSession, getRaw, watch and effect are NOT published and importing them from @ng-org/orm defeats everything above: one wallet hosts every identity here, so an unwrapped read returns every identity's objects and an unwrapped write reaches any document. RdfOrmSubscription in particular pools its subscriptions on the shape and the scope alone, with no identity in the key - what makes that safe is the filtering this package does on the way out.
No revocation. inbox.share cannot be undone.
Nothing per reader on a document in a public store. No grant, no revocation, no audience list.
No delegated writing. A received key never grants a write, and no call adds a writer to a document.
No mailbox model. Do not build on the raw deposit list.
No cross-broker reference. A returned reference resolves for users of the same broker.
openDocumentInbox does not coalesce across PAGES. Two tabs — or two sessions of the same person — that open the same document's inbox in the same moment can each create one, and the document is left with two: its owner drains one while deposits arrive in the other. Nothing raises, nothing reports it, and neither page can detect it afterwards. It is not an oversight to be patched later: a branch MERGES records rather than refusing the second, so there is no "create only if absent" to build the guarantee on, and the address a depositor reads is a separate record from the one the owner resolves — so the two cannot even be made to agree on which of the pair won. Open a document's inbox from one place: the page that creates the document, or one call the rest of the interface waits on.
TWO COPIES OF THIS PACKAGE IN ONE APPLICATION MISBEHAVE SILENTLY, and nothing here can detect it. What this package remembers it keeps in the package itself, not in any handle you hold: which documents are subscribed and who is listening for them, which identity the session settled on, which documents are open. None of that is shared between two copies. An application that ends up with two therefore runs two of everything — a document subscribed through one copy is invisible to the other, so changes to it simply never arrive; and the identity settled in one is not the identity the other acts as, so the same call writes as one user or the other depending on which copy it reached. Nothing raises, nothing warns, and no call can report it: from every entry point a second copy is indistinguishable from the first, and the symptoms surface far from the cause as missing updates and writes attributed to the wrong person. This is a packaging property, not a behaviour to code around — declare this package once, as one dependency of the application, and let your own code and every library built on it resolve to that copy. A library that builds on this package should declare it a peer rather than a dependency for exactly this reason, so that the application remains the one place it is named.
No unfiltered read through useShape, and no way round it through the object it hands back. What the call returns is NextGraph's own result, filtered: data and promise carry only items in documents you hold the key for, isLoading passes through, the two page commands run and whatever they hand back is held to the same rule as any other member, and subscription - the ORM's own handle, which reads and writes with no capability filter at all - is refused out loud rather than handed on. Any other member of that object is refused too unless what is behind it is a plain scalar, which is how a member a later @ng-org/orm adds is decided before anyone has seen it: whatever can carry an item is an object or a function, and both refuse. Inside data, every member that yields items is filtered, and an ordered result addresses and counts the items you may read: size, length, an index, Object.keys, in and JSON.stringify all agree, and none of them says how many you may not. Reading a member's DESCRIPTOR answers exactly what reading the member answers, which is not a detail: Object.getOwnPropertyDescriptor - and the spread, Object.entries and console inspection built on it - reads a property without going through the view's own read, so a refused member throws there too rather than handing its value over. That holds for an ACCESSOR as much as for a value: the getter you are handed reads through the view, and its setter is refused. A mutation reaches the collection underneath, and neither hands back nor acts past what the view shows. Writing is not broken - your own add reaches the document and the reactivity it drives. All three ask the same question: add into a document you hold no key for THROWS (it returns the collection whether or not it did anything, so it has no value meaning "that did not happen"), delete of an item the view does not show answers false - the Set contract's own word for "it was not there", and true of the view - and clear empties the view rather than the collection every identity shares. add hands back the VIEW rather than the collection, because Set.prototype.add returns the collection so calls can chain and chaining onto the unfiltered one gives it away entirely. Writing to the collection itself is refused - data[0] = x, delete data[0], data.length = 0, Object.defineProperty, Object.setPrototypeOf and Object.freeze - on this view's own account rather than because the ordered collection upstream happens to refuse some of them too. Every member @ng-org/alien-deepsignals declares non-mutating answers, computed over the items you may read: the iterator helpers, the array reads (at, slice, join, indexOf, toSorted, with) and the ES2025 set combinators (union, difference, isSubsetOf). A document reached through that view alone, read nowhere else first, does not appear.
isLoading does not mean what its name says, and the fault is upstream's. @ng-org/orm's declaration describes it as "true when no data is available yet"; the hook that ships computes it as the subscription's isReady, which is the inverse. Both artifacts were read and they disagree. This package forwards the value it is handed rather than correcting it, because a layer that quietly improves the target teaches you a behaviour you would have to unlearn. Branch on data === undefined, which is unambiguous, and treat isLoading as unusable until upstream settles it.
getObjects does not fetch a public store's key for you either, for the same reason as the two shape reads below: it goes through the same ORM query and spends no round-trip asking. Read the document once by naming it — docs.sparqlQuery anchored on it — and it appears.
useShape does not fetch a public store's key for you, and useShapeQuery does so only for what you NAME. A document in a public store gives up its key to whoever asks, but asking is a round-trip: useShape's signature is NextGraph's own and has no await to spend, so a public document you have read nowhere else never appears in it. useShapeQuery spent none either until 1.0.0-dev.9; it now asks once per document named in graphs, which is what lets a bare reference to a public document be read through that call. It still asks nothing under graphs: "did:ng:i" - the whole local dataset names no document, so there is nobody to ask about. For those two cases, read the document once by naming it - docs.sparqlQuery anchored on it, which is the whole of what readUnion did for this before it was removed in 1.0.0-dev.8 - and both reads 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
Semver, and majors are the normal case. This surface converges on a NextGraph that does not ship yet, so most steps toward the target remove or narrow something — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time.
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,resolveWriteGraph,readUnionandUnionSubjectall left this way), or a published symbol is renamed (watchShapebecameuseShapeQuery), or an existing call narrows: it now throws where it returned (ensureIdentityrejects rather than resolve on an incomplete restore), it reports a state you did not have to handle before (useShapeQuerypublishingisErrorwhere it used to publish a synced-empty snapshot), or what it HANDS BACK changes shape (useShapeQuery'sdatacarrying 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.readSyncedForDocumentarrived 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.11), 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.11 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.11, on a branch: pin that string exactly, and anchor your usage_ leaf's against: on it - against: @ng-eventually/polyfill@1.0.0-dev.11, the string you pinned, never the version it is heading for.
1.0.0-dev.11 is a major, and all of it is one rule: useShape hands back NextGraph's own RESULT OBJECT, filtered - and the filter no longer decides what to hand on by what a member is CALLED. Nothing is added, renamed or removed from the entry.
- Read your objects through
data, not by iterating what the call returned.@ng-org/orm's React hook returns{ data, promise, subscription, isLoading, nextPage?, previousPage? }around the collection, and it has for every release this package builds against. This package went on treating the return AS the collection:[...result]andresult.sizethrew, andresult.data,result.promiseandresult.subscriptioneach handed back every identity's items, with nothing raised anywhere. If you were reading.data, you were reading unfiltered. That is the defect this version closes, and it is why this entry is worth reading before upgrading rather than after. subscriptionis REFUSED where it used to be handed over. It is the ORM's ownRdfOrmSubscription, which reads and writes with no capability filter at all and which## Non-guaranteeshas always said this package does not publish. Reading it now throws, naming itself.data,promise,isLoadingand the two page commands are what remains, and they are what a caller needs: the SDK's own example destructuresdataandisLoadingand nothing else.- A member of that object that this package does not know is refused unless it is a plain scalar. The rule is the value's TYPE, not its name: whatever can carry an item is an object or a function, so a member the next
@ng-org/ormadds is already decided, while a flag or a count still passes. The previous rule - "anything that is not a function is harmless" - is precisely what let three members of the new shape through, so it has been replaced rather than extended with three names. - The two arguments are the SDK's own types, where the signature took
unknownfor both.useShape(shapeType: ShapeType<T>, conf: RdfOrmConfig<T> | string | undefined), which is what the surface already said the pair was. A caller passing the SDK's shape and config compiles unchanged; one passing something else now does not, which is why this counts. sizeandlengthcount what you may read, and so does every other way of asking. An ordered config (orderBy) makes the collection an array, which says how many items it holds in four places -length,Object.keys,in, and the descriptor of each index - and only the first was filtered.data[0]now names the first item you may read rather than the first the engine returned, and the positions past them do not exist.- A member's DESCRIPTOR answers what reading it answers.
Object.getOwnPropertyDescriptor(result, "data")returned the collection UNFILTERED, and...(result, "subscription")the handle that reading it refuses: that call does not go through the view's own read. Neither does a spread,Object.entries,JSON.stringifyor a console inspection, so all of them were reading round the filter. They now read through it - which also means{...result}andconsole.log(result)throw onsubscription, where they used to print it. Logdata. - A mutation no longer hands back the collection, nor acts past the view, and all three ask the same question.
addreturned the collection -Set.prototype.addreturnsthisso calls chain, measured on the plainSetand on the realDeepSignalSetalike - soview.add(x)gave every identity's items to a caller whose every way IN was filtered, through the first member the ORM's own example reaches for. It now hands back the view, and it now REFUSES an object naming a document you hold no key for: ungated, it let you plant an object into someone else's document, never see it again, and have the two-way binding commit it there.deleteremoved an item the view declines to confirm the existence of;clearemptied the collection every identity shares. Your own writes still reach the document - that is the one thing that must not change, ande2e/orm-live-read.tsmeasures it against the deployed broker. - A write to the collection itself is refused.
data[0] = x,delete data[0]anddata.length = 0all went through against a collection that allowed them, the last defeating the emptying rule outright;Object.setPrototypeOf(data, ...)replaced the real collection's prototype, after which iterating it throws for every identity, andObject.freeze(data)made it non-extensible. The view refuses all of them itself now, rather than relying on the ordered collection upstream refusing some of them for it. - The members upstream declares non-mutating answer again. Eighteen array reads on an ordered result, plus
includesand the seven set combinators, were throwing for the holder whose data it was - a leak traded for an outage. They are served over the items you may read. - An ordered result yields indices where it used to yield items.
keys()handed back items,entries()handed back[item, item]andforEachhanded the item as its second argument - the Set contract, applied to an array. No leak, and wrong output on your own data. - Unchanged: the read still spends no round-trip asking a public store for a key, a document you have read nowhere else still does not appear, and nothing about
useShapeQuery,getObjectsorinsertObjectmoves.
1.0.0-dev.10 is a major, and all of it is one rule: "is this document open in this session" now has exactly one answer. This package kept three records of that fact - a set of live bootstrap subscriptions, a per-document barrier state, and the subscription layer's own listeners - and the first two outlived the third. Only one remains, the state of the document's one real subscription; the other two are gone rather than reconciled, because two records of one fact can disagree and these did. Nothing is added, renamed or removed from the entry.
- The ARRIVAL half of a named scope's guarantee is delivered in full, and the two non-guarantees
1.0.0-dev.9carried against it are withdrawn. A secondconfigure(...)no longer leaves a document recorded as synced against a subscription it tore down - that one DID publish an empty or stale success, and now stays pending until the document is heard from. A subscription the broker refuses no longer leaves the document recorded as open, in the one case where that was unbounded: when the refusal arrives after the subscription layer has been torn down from under it, nothing was ever told, and the document could not be opened again for the rest of the session. In both, the document is now re-opened and the read answers once it has actually been heard from. - What a caller sees change, and the two situations differ - said per situation rather than lumped together. After a second
configure(...)a named scope publishesisPendingwhere it publishedisSuccess, and that is why this is counted a major on the conservative reading:isPendingis a state this call already reports and this surface already requires you to handle, so no new state arrives at the caller, but an interface branching onisSuccessis handed a different value. When the broker refuses late, the read was already withheld - it is the RECOVERY that changes: the retry now subscribes again instead of being blocked for the rest of the session, sorefetch()is an exit where it was not. Callingconfigureonce, at start-up, is still what the deployment requirements ask for; this makes a second call recoverable rather than supported. - Unchanged, and each is still a success: a named document you can read that holds nothing of the shape,
graphs: [], andgraphs: "did:ng:i"or"". Turning a genuine emptiness into a failure would only move the lie, and nothing here does.
1.0.0-dev.9 is a major, and all of it is one rule: a read that could not reach its document now FAILS, where it used to answer as though the document were empty. Nothing is added, nothing is renamed, nothing is removed from the entry - three existing calls narrow.
useShapeQuerypublishesisErrorfor a document you NAME and cannot read. Until this version a document named inscope.graphswas handed straight to the engine; whatever came back was filtered per document by what you hold, and a document you hold no key for was emptied out of the result. The snapshot wasisSuccesswithdata: []- indistinguishable, byte for byte, from the scope genuinely holding nothing. It now asks the public store for the key, refuses out loud if there is none to be had, and crosses the sync barrier first, so the same read answersisError. HandleisErroron a named scope; an interface that only branched onisPending/isSuccesswill show its success state with nothing in it. Naming several documents fails as a whole when one of them fails.- A named document that has not SYNCED keeps the read pending, where it used to publish the same empty success. Reaching a document and hearing back from it are two questions, and only the first was being asked. An interface must therefore expect a named scope to sit in its loading state while a document it named is not answering, rather than reading an empty success as "nothing there". The same applies after an in-page re-login, which returns a named scope to pending until its documents are confirmed under the new session - see
## Guarantees. Complete since1.0.0-dev.10; in1.0.0-dev.9it held only within one ordinary session, and the two situations that broke it were carried as non-guarantees. - A named scope now costs round-trips it did not cost before, once per named document per session: the key fetch, and the subscribe that syncs the repo. The opens run together, so several named documents cost the slowest wait rather than the sum.
graphs: "did:ng:i"is unchanged and spends nothing. - A refused named scope RECOVERS on its own. Reachability is re-read on every publication rather than remembered, and a scope whose barrier failed is attempted again when the keyring moves - including when the move happened while the read was still deciding. Without that a refusal would be terminal, and trading a silent empty answer for a permanently stuck error would have been no improvement at all. Expect to see
isErrorbriefly on a page that subscribes before its keys are restored. There is no polling: in a session whose keyring never moves again,refetch()is the recovery, as it is for every other failure this call reports. inbox.postToDocument,inbox.readForDocumentandinbox.readSyncedForDocumentreject where they used to report "no inbox". Resolving a document's inbox address reads that document, and when the read could not happen - the broker could not answer for it, or its public store could not be asked for the key - all three used to render the breakdown as the document's owner having opened nothing: a refusal on the deposit side, an empty list on the read side. They now reject. A document that genuinely has no inbox is unchanged, and still answers exactly as before.- Nothing that was already an ANSWER moves. An empty document is a success holding zero objects,
graphs: []is a success holding none, a document in a public store that exposes no key is a plain "no", and a document with no inbox answers[]. The whole change is that a failure stopped borrowing those shapes. - Why this direction, and not a richer set of outcomes. The step that collapsed the two was this package's own emulation of a public store serving its key; NextGraph has no counterpart for it, because upstream nothing consults a capability on the read path at all. So there is no signature to mirror, and the alignment falls to the engine's own observable: an anchored query on a repo the verifier does not hold answers
NgError::RepoNotFound, a rejected promise - a read rejects, or it does not. Inventing a third outcome for a caller to branch on would have been a vocabulary the target will never grow.
1.0.0-dev.8 is a major, and it is one removal: readUnion and UnionSubject are gone from this surface, and nothing replaces them.
- What they were.
readUnion(docs)issued one anchoreddocs.sparqlQueryper document, in parallel, and added four policies of its own on top: it swallowed a document it could not read into an empty answer, it dropped a document whose key you do not hold instead of refusing it, it grouped the rows into per-(document, subject) property bags, and it removed the reserved-prefix registers from the result. Nothing at any level of NextGraph answers that composition. Its own provenance annotation citedsparql_query, which is a citation a reader checks, finds correct, and moves on from - while the claim attached to it was false. A composition cannot be aligned on a single function, and that is the rule this removal serves. - What to call instead.
docs.sparqlQuery(sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, doc)- the primitivereadUnionwas built on, published beside it all along. It is the same anchored query and the same public-store key fetch, so a barereferenceis still enough to read a document in a public store: nothing about that property moves. Reading several documents is this call once per document. - What comes back to you, and it is not a detail. Three of the four policies were the batch's, and they are now yours: a read that FAILS rejects instead of reading as an empty document, so whether one unreadable document ends your loop is your decision at your call site; a document you hold no key for is REFUSED out loud instead of being dropped from a result; and the rows are rows - grouping them into objects is yours, or use
getObjects/useShapeQuery, which return the SDK's own typed objects. The fourth is in## Guarantees: subjects underurn:ng-eventually:are no longer filtered out for you. - Why the losses are alignment rather than regression. Upstream, an anchored query on a repo the verifier does not hold REJECTS (
resolve_target_for_sparql->NgError::RepoNotFound), which reaches JavaScript as a rejected promise. A "skip this one and carry on" policy is something the target does not have and will not grow, so a caller that learned it would have had to unlearn it. The one thing that genuinely disappears is the convenience, and it is three lines at your call site.
1.0.0-dev.7 is a major, and what makes it one is not the two calls it adds but the rule it starts enforcing. Symbol by symbol:
getObjectsandinsertObjectare published, at NextGraph's own names, parameters and returns, wrapping the SDK's. That part is a minor on its own: nothing existing moves. Import them from this package rather than from@ng-org/orm- which is the point, since the unwrapped ones bypass the capability filter and the shared-wallet identity boundary entirely. Both are injected throughconfigure(getObjects,insertObject), like the shape query beside them.- An ORM write into a document you do not own is now refused, where it used to succeed. This is the major. It applies to
insertObjectand to every mutation of a live set a shape query handed you, because both leave by a door that had no guard on it. The guarantee itself did not move - only a document's owner writes to it has been in this contract from the start - but the implementation only enforced it ondocs.sparqlUpdate. Code that relied on writing through the ORM into somebody else's document was relying on a hole. Note that a live-set mutation is refused WITHOUT your call failing: see the non-guarantee above. initNgmust be this package's, and it now substitutes thengit hands NextGraph's ORM. An application that calls@ng-org/orm'sinitNgdirectly still works and is still unguarded - which is exactly the state being left behind, so call this package's.RdfOrmConfigjoins the published types, becausegetObjects's signature names it. It is re-exported from the SDK's/allsubpath, which is where the SDK puts it.ngworks. Until this version the publishednganswered every one of its members with a promise instead of the function, song.<anything>(...)failed with NextGraph's own<member> is not a function. If you passed it anywhere - and the surface tells you to use it instead of the one you gaveconfigure- that is what you were hitting, including auseShapeQuerythat never leftisPendingbecause NextGraph swallows the failure it gets fromorm_start_graph. Nothing about the call changes; it simply answers now.
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:
dataitems are the shape's objects, not property bags.ShapeQuery<T>'sTis now the shape's type: each item carries@id,@graphand one field per predicate the shape DECLARES, under that predicate's readable name.note.props["urn:notebook:title"]?.[0]becomesnote.title, andnote.graphbecomesnote["@graph"]. A predicate your shape does not name is not projected, so a shape written only to match — anrdf:typeconstraint and nothing else — now comes back with no fields at all: declare the ones you read. (readUnionwas unchanged by this version, and was removed in1.0.0-dev.8.)shapeTypeis typed. It wasunknown; it isShapeType<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 meansreadablePredicate,minCardinalityandmaxCardinalityon every predicate.configuretakes one more thing.ormSubscription: RdfOrmSubscription.getOrCreate, plusinitNgand a call to this package'sinitNg(ng, session)once the session has arrived. Optional in the type, required in practice for this read: without ituseShapeQuerypublishesisErrornaming 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;watchShapeappears nowhere in NextGraph and spends a verb the ORM already uses for signal observation. Everything you read back is unchanged — the sameShapeObservable, the sameShapeQuerysnapshot, the same three states.Scopeis no longer"public" | "protected" | "private". The publishedScopeis now the SDK's own,{ graphs?: string[] | string; subjects?: string[] }, re-exported from@ng-org/orm, and it is whatuseShapeQuerytakes.watchShape(Shape, "protected")becomesuseShapeQuery(Shape, { graphs: "did:ng:i" })for everything you hold, oruseShapeQuery(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.storeRegistry.createEntityDoc(scope)→createEntityDoc(store?).storeis a reference, not a word:await storeRegistry.protectedStore(),publicStore()orprivateStore(). Omitting it places the document in the private store, exactly asdoc_createdoes when no store is given.createEntityDoc("protected")becomescreateEntityDoc(await storeRegistry.protectedStore()).storeRegistry.listMyEntityDocs(scope)is REMOVED, with no replacement call. There is nodoc_listupstream, at any level. What replaces it is the read itself: askuseShapeQueryfor a shape over{ graphs: "did:ng:i" }and what you hold of that shape comes back. An application that listed its documents in order to read them was doing in two steps what the read does in one — but it must now write something the shape can match, typically anrdf:typetriple, since nothing indexes a document for you.storeRegistry.resolveScopeGraph(scope)is REMOVED. It handed out a value whose stated use was a read scope, and a store's graph holds its containment triples and nothing else — so pointing a shape query at it matches nothing and says so with an empty set, silently. If you were using it as a write anchor, use the store's reference from the accessor of the same name.storeRegistry.resolveWriteGraph(scope)is REMOVED. It returned the very referenceprivateStore()/protectedStore()/publicStore()return. One value, one published name.
1.0.0-dev.4 before it narrowed a guarantee and added a rejection: a deposit this package applies is now consumed, so a failure that used to be retried at the next arrival or the next connection is now final and reported once; and inbox.processInbox rejects on a broker failure where it used to resolve. An application that leaned on the retry — however implicitly — has to keep its own record instead, and one that awaited processInbox without a catch now has a rejection to handle. 1.0.0-dev.3 before that narrowed an existing call the same way: two payload shapes an application could carry freely became this package's own, and one it cannot apply is reported. All of them land inside the pre-release line rather than on a new major number because 1.0.0 has not been cut, so there is nothing yet to break away from.
There is no changelog file and no deprecation window: the sections above are the release note. A removal or a narrowing lands in ## Surface and ## Guarantees in the same version that ships it, and a symbol is never left published-but-dead as a courtesy. Diff this leaf between two pulls — ## Guarantees and ## Non-guarantees before ## Surface, because that is where a narrowing shows up first.