0f1e493e93
Le contrat disait qu'il « termine le travail de connexion qu'il entame », sans dire ce qui arrive quand il n'y parvient pas. Depuis que les échecs remontent, il lève — et c'est actionnable : une application qui rend son interface malgré un rejet affiche un compte vide, pas un écran vide, ce qu'aucun utilisateur ne sait interpréter. Dans l'autre sens c'est une garantie renforcée : résolu signifie que tout ce qui vous a été partagé est lisible.
140 lines
9.4 KiB
Markdown
140 lines
9.4 KiB
Markdown
---
|
|
type: contract
|
|
summary: 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 to `configure` as `sharedWallet: { fileUrl, password }`;
|
|
- 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.
|
|
|
|
## 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:
|
|
|
|
```ts
|
|
// ── 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: {
|
|
docCreate(sessionId: string, crdt: string, cls: string, dest: string, store?: unknown): Promise<Nuri>;
|
|
sparqlQuery(sessionId: string, query: string, base?: string, anchor?: NuriLike, label?: string): Promise<unknown>;
|
|
sparqlUpdate(sessionId: string, query: string, anchor?: NuriLike, label?: string): Promise<void>;
|
|
};
|
|
|
|
// ── 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: typeof read;
|
|
};
|
|
export interface Deposit { from: PrincipalId | null; payload: unknown; ts: number }
|
|
|
|
// ── the wrapped SDK objects ──────────────────────────────────────────────
|
|
export const ng: Record<string, any>; // call this instead of the `ng` passed to `configure`
|
|
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.
|
|
|
|
`createEntityDoc` throws if the document cannot be recorded in its store.
|
|
|
|
## 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.
|