listMyEntityDocs rendait la liste des documents même quand la lecture des clés échouait. Vu de l'appelant, une liste dont les documents ne s'ouvrent pas est INDISCERNABLE d'une liste dont ils s'ouvrent : rien ne signale la différence jusqu'à une lecture ultérieure qui revient vide, la cause étant alors loin derrière. C'était un choix délibéré — ne pas transformer un appel publié en levée, la liste étant déjà en main à ce moment-là. C'est précisément ce qui en faisait une demi-vérité plutôt qu'un raccourci. Et c'était le dernier membre connu de la famille qui a produit une panne chez une application cette semaine. Les deux lectures sur lesquelles l'appel repose remontent désormais : la branche Main dit quels documents sont là, la branche Store dit ce qui ouvre chacun. Un tableau vide signifie donc que ce compte n'a rien créé dans cette portée, et jamais que le store n'a pas été lu. readUserStore avalait le même échec pour son propre compte ; son autre appelant, ownsDocument, garde le comportement actuel par un catch explicite et documenté — toutes ses réponses étant des refus, il échoue en fermeture, ce qui est la règle qu'e32b6d0 avait posée. C'est un changement de comportement d'un appel publié, donc le contrat le dit, et docs/api-contract.md aussi. Au passage, deux citations pourries corrigées en citant un SYMBOLE plutôt qu'une ligne — types.rs:4251 désignait DialogRequest et non Link, index.d.ts:138 désignait const ng et non le type NG. Les douze autres références numériques du voisinage ont été vérifiées : aucune n'avait bougé.
11 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 and listing an application's documents by scope), reading (a document's subjects, one-shot or reactive), 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 }; - 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.
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
useShape: UseShapeLike; // `useShape` 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;
export type Scope = "public" | "protected" | "private";
// ── placement: where an application's documents live ─────────────────────
export const storeRegistry: { // no identity parameter — a session is one user's
createEntityDoc(scope: Scope): Promise<Nuri>;
listMyEntityDocs(scope: Scope): Promise<Nuri[]>;
resolveScopeGraph(scope: Scope): Promise<Nuri>;
resolveWriteGraph(scope: Scope): Promise<Nuri>;
openDocumentInbox(doc: NuriLike): Promise<Nuri>;
};
// ── reading ──────────────────────────────────────────────────────────────
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
export function watchShape(query: ShapeQuery): ShapeObservable;
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>;
};
// ── 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[]>;
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`), 88 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 listMyEntityDocs, not UnionSubject.subject / .graph. 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().
readUnion returns one entry per distinct subject present in a document. subject is that subject's IRI exactly as written, and is a string, because a subject may be any IRI; graph is the document reference you passed in, and is the Nuri to hand back to this surface. Properties of different subjects are never merged, and the same subject IRI found in two documents stays two entries, told apart by graph. Several objects in one document are allowed. Recommended placement is one document per business entity: access is granted per document.
urn:ng-eventually: is reserved. Triples whose subject falls under that prefix are dropped on read and never returned by readUnion; every other IRI is returned.
Only a document's owner writes to it. Holding its read key never grants a write.
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.
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, or a queue could not be drained, it throws instead of returning. So a resolved call means everything shared with you is readable — and a rejected one must not be rendered past, since the interface would show an empty account rather than an empty screen.
ensureIdentity() mounts a full-screen barrier on every top-level load, and takes it down itself — past the broker round-trip it never appears. 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.
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.
The same rule reaches what a call hands BACK, not only what it looked up first: listMyEntityDocs returns a listing whose documents you can open, or it throws. It reads which documents are in the store and what opens each, and it throws if either did not answer — including when the documents came back and their keys did not. Nothing about a keyless listing is visible to you: it is the same Nuri[], and the difference would only appear at the next read, empty, long after the cause. An empty array therefore means this account created nothing.
Non-guarantees
No display name. ensureIdentity() returns an opaque identifier: do not parse it, split it, or render it as a readable name.
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.
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.
Change policy
This surface changes, and shrinks. The package does not offer semantic-version stability.
Re-pull this contract at every upgrade.