# ng-eventually A generic polyfill layer over the [NextGraph](https://nextgraph.org) JS SDK. NextGraph's JS SDK does not yet expose cross-wallet reads, capabilities, inboxes or group stores. `ng-eventually` lets a consumer application behave as if those existed today, by emulating them on top of a single shared wallet / broker. It is generic: it contains no application domain — the consumer application injects its shapes and the acts of granting access. The name: *eventually* NextGraph will ship these features; until then this layer fills the gap (and nods at eventual consistency / events). ## The boundary — mature face out, compensation in The asymmetry is the point. The consumer application writes SDK-shaped code as if NextGraph were finished: per-entity documents in public/protected/private stores, capabilities, inboxes. This library owns the current-state NextGraph knowledge and the simulation that fabricates that mature face — a shared-wallet emulation — so the application never sees it. As NextGraph matures, this library changes; the consumer application's code does not. Docs (this library's own engineering doctrine, under [`docs/`](./docs/)): - [`docs/nextgraph-current-state.md`](./docs/nextgraph-current-state.md) — the authoritative reference on what the current SDK/broker do and do not expose (the ground truth each polyfill compensates for). - [`docs/simulation.md`](./docs/simulation.md) — how this lib emulates the mature behaviour on one shared wallet (shim, per-document ReadCaps, emulated inbox, write guard, the two axes, the double-proxy constraint). - [`docs/read-model.md`](./docs/read-model.md) — the read model the polyfill implements: you follow links, you never enumerate; listing via a bounded set of per-doc anchored `sparql_query`s; reactivity via re-query on a change signal. - [`docs/decisions/`](./docs/decisions/) — current-SDK ADRs (private-store scope, SPARQL delete, shared-wallet identity). - [`docs/fork-inbox-fallback.md`](./docs/fork-inbox-fallback.md) — the Rust-patch / self-host inbox path not taken (kept as a fallback). - [`docs/migration-guide.md`](./docs/migration-guide.md) — the checklist for when real NextGraph matures. ## What is emulated Nothing in this library is a real NextGraph feature. Each behaviour below is emulated — a stopgap fabricated on top of the current, immature NextGraph (one shared wallet, everything physically readable). The consumer application always sees the mature SDK face; the emulation lives entirely here. The table reads: what the consumer application does, the real NextGraph target it is written against, the current NextGraph implementation status (why a workaround is needed), and how this lib emulates it today. | Capability | What the consumer application does | Real NextGraph target | Current NextGraph status (why a workaround) | Current emulation | |---|---|---|---|---| | Multi-identity / per-identity wallet | Treats each identity id as its own wallet with its own documents | Each identity opens its own real wallet; native cross-wallet reads | Not-yet-implemented: the JS SDK exposes no cross-wallet read, so one session cannot read another identity's wallet | One shared wallet everyone opens; "identities" are virtual users — shim accounts keyed by an id, each mapped to its documents in `store-registry.ts` | | Three native stores per identity | Places entities by scope `public` / `protected` / `private` | The identity's three real native stores hold the entity documents | Not-yet-implemented: `doc_create`/ORM can target only the private (and protected) native store today; a `public`/arbitrary `StoreRepo` is not JS-constructible | Three emulated scope-index documents per account — each "store" is an index doc listing its entity-doc NURIs; all physically live in the one shared private store, and scope is a logical label | | Per-document read isolation | Nothing to declare: creating a document records its cap on its store, and its creator holds it. Reading is `capFor(doc)` — you hold the key or you do not read | The broker/verifier delivers only documents the wallet holds a ReadCap for; accessing a document without the cap yields an empty result in a union read (a targeted read of an unheld repo errors with `RepoNotFound`) | The model itself is the point: reading is key possession, and there is no read-ACL to introspect — a client cannot ask "may this identity read this doc?" because that question does not exist upstream | Caps recorded per identity: `AddRepo` on the store's emulated Store branch for documents it creates, `AddLink` on its User branch for caps received; `caps.ts` caches them for the session. A read filter (`read-filter.ts`) plus the boundary (`reach.ts`) keep only documents whose cap is held. The cap value is the stand-in `OK` — enforcement is P1b | | Directed read sharing | Owns the relationship concept ("who is connected to whom") itself, and on acceptance shares one document's cap to the other's inbox (`shareCap(cap, theirInbox)`) | The cap sealed to the recipient's inbox key (`ContactDetails.read_cap`), opened by their own verifier while processing the inbox | Not-yet-implemented — a **gap, not a disagreement**: the field exists but the message construction is `unimplemented!()`, its only caller passes "without read_cap", and the receiver discards the cap. The shape is right; the implementation is absent | `shareCap` deposits the cap into the recipient's inbox document; the recipient's existing `inbox.watch` absorbs it into what they hold. No "receive" operation, and no principal is ever named to the registry | | Inbox (registration notifications) | `inbox.post` / `read` / `watch` | A message is sealed to the recipient's key and queued in their inbox; the recipient's own verifier unseals and applies each queued message inline while processing the inbox | Not-yet-implemented: the sender-side seal-into-inbox call (`inbox_post_link`) is proposed/future, not exposed in the JS SDK | Deposits written as RDF into an inbox document via SPARQL; `read`/`watch` read the deposits back — an in-lib stand-in for the recipient's own inbox processing | | ~~Discovery of all public events~~ **REMOVED 2026-07-30** | Circulates the link itself — into inboxes, or into a document the reader already holds | **There is no discovery.** You cannot discover, you can only follow links: publishing = place the data in your public store **and** circulate the link, seen only by those who received it (a foundation of local-first) | Not a gap to be filled — a global index is not a NextGraph shape, and it would pool data across wallets | Nothing. `discovery.ts` and its global index were removed: they emulated a capability the target will never have. See [`docs/readcap-and-nuri-model.md`](docs/readcap-and-nuri-model.md) §4ter-bis | | Reads / listing | Lists the documents it needs, by scope, and reads them | Native per-wallet reads over the real per-identity stores | Bug/perf: an anchorless union query spans every named graph in the session store, which on a shared / accumulating wallet is O(wallet size) and stalls | A bounded, by-need set of per-doc anchored `sparql_query`s (each anchored to one repo's default graph), independent of wallet size | | Reactivity | Lists update on change | Native reactive reads | Not-yet-implemented: there is no reactive union query across graphs | Re-query the bounded per-doc anchored set on a lightweight change signal (`doc_subscribe` / ORM on an already-opened single store) | | Writes | Writes an entity to its scope | Writes land in the entity's real store via native primitives | Not-yet-implemented: `doc_create` can target only the private/protected store today (`StoreRepo` not JS-constructible) | Per-entity documents via direct SPARQL (`docs.sparqlUpdate` on the real injected `ng`) | | Current identity | Sets the current identity id (established at wallet import) via the SDK's current-identity call | Opening one's own wallet at the broker gate establishes the session identity | Not-yet-implemented for the shared-wallet case: everyone shares one wallet, so the broker cannot distinguish identities | A relayed id (`accounts.ts` `IdentityStore` persists it); the read filter and inbox `from` read it | | Write-guard | Writes refused without the write cap | The broker/verifier enforces the write cap natively | Partial: the guard fires only on the public proxy, but the real write paths call the injected `ng` directly (the `DataCloneError` constraint), so it is best-effort today | A `sparql_update` override (`ng-proxy.ts`) checking the emulated write cap | ## Packages | Package | Role | |---|---| | `@ng-eventually/client` | The SDK-identical wrapper the app imports instead of `@ng-org/web` / `@ng-org/orm`. It adds the polyfills the broker/verifier will do natively (shared-wallet identity, capability enforcement, anticipated cap/inbox methods). As NextGraph matures, the app points back at the real SDK (build alias removed) and this package falls away. | A global-index package is deferred. Data common to all of an application's users comes from a **singleton app**: a document or store shared by all users and hardcoded in the app, write-owned by the developer and delegable — but never to all users, so user contributions reach it **through an inbox** (nothing in NextGraph is freely writable by everyone). That is the direction the NextGraph developer has named; it is **not implemented**, and several points are still open (what exactly is hardcoded, how delegation travels, who materializes the inbox). So there is no second package for now — it will be introduced once the mechanism exists, and it will be separate from the client. See [`docs/nextgraph-current-state.md`](docs/nextgraph-current-state.md) § Apps & services. ## Design principle The application code is written as if the target NextGraph existed. All compensation lives here, beside the app. As NextGraph matures, this layer falls away; the app code (SDK-shaped) is unchanged. - SDK-identical surface: the client wraps the real `ng` (a Proxy that forwards everything and overrides only what must be emulated) and `useShape`. The real SDK is injected via `configure()` (no hard import → build-alias safe and testable). - Authorization is emulated capabilities: documents carry grants; the client enforces them generically (read filter + write guard). The app declares a document, shares one document's cap to an inbox — the same acts it will perform in the target. No policy is injected. - Inbox: the client `inbox` namespace deposits (`post`) and, in the shared-wallet emulation, reads the deposits back (`read` / `materialize` / `watch`) in place of the recipient's own inbox processing. - Tests of the polyfill (against a real broker) live in this repo, so a consuming app can test its features against a clean, mocked API. ## Status Implemented. The polyfill mechanisms are wired against a real broker, not stubbed: - Shared-wallet shim — `store-registry.ts` (`(account, scope) → document NURI`, `createEntityDoc` / `listMyEntityDocs` + per-user stores, cross-device via the RDF shim anchored in the private store). - Document / SPARQL primitive — `docs.ts`, calling the real injected `ng` directly (avoids the `@ng-org` double-proxy `DataCloneError`). - Emulated ReadCaps — `caps.ts` (`CapRegistry`, per-document, directed grants) + read filter `read-filter.ts` (reactive-set `Proxy` view), applied by `use-shape.ts` only once a cap exists (`caps.isEnforcing()`). - Write guard — `ng-proxy.ts` (`sparql_update` override, emulated write cap). - Inbox — `inbox.ts` (`post` / `read` / `materialize` / `watch`). - Identity — `accounts.ts` (`IdentityStore`, injected storage). - SPARQL hardening — `sparql.ts` (`escapeLiteral` / `escapeIri` / `assertNuri`). The remaining `TODO` markers are narrow: the shared-wallet credential passthrough in the `session_start` proxy branch, and the anticipated cap/inbox SDK signatures to reconcile if the official API differs. See [`docs/simulation.md`](./docs/simulation.md) for what each piece does and [`docs/migration-guide.md`](./docs/migration-guide.md) for what changes as NextGraph matures.