Inbox deposits and the per-(account,scope) index append were written into an explicit GRAPH <plainNuri> named graph, which the real broker stores separately from the repo's default graph (repo_graph_name with overlay suffix) — so an anchored default-graph read (read-model.readDoc) never saw them and deposits did not round-trip. Drop the GRAPH wrapper: the anchor scopes the write to the repo's default graph, matching the read. Mocks updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ng-eventually
A generic polyfill layer over the NextGraph JS SDK.
NextGraph's JS SDK does not yet expose cross-wallet reads, capabilities, inboxes
or group stores. ng-eventually lets an app 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 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 whole point. Consumers write SDK-shaped code as if NextGraph were finished: per-entity documents in public/protected/private stores, capabilities, inboxes. This library owns all the current-state NextGraph knowledge and the simulation that fabricates that mature face — a shared-wallet emulation — so the application never sees it. When NextGraph matures, only this library changes; the consumer's code does not.
Docs (this library's own engineering doctrine, under docs/):
docs/nextgraph-current-state.md— the authoritative reference on what the CURRENT SDK/broker do and do NOT expose (the ground truth every polyfill compensates for).docs/simulation.md— how this lib emulates the mature behaviour on ONE shared wallet (shim, per-document ReadCaps, emulated inbox+curator, write guard, faux login, the two axes, the double-proxy constraint).docs/read-model.md— the READ MODEL the polyfill implements: events via the global index, everything else by following a shared graph; listing via a bounded set of per-doc anchoredsparql_querys (never an anchorless union-scan of the physical wallet, never the ORM fan-out — both hang/time out); reactivity via re-query on a change signal.docs/decisions/— historical current-SDK ADRs (private-store scope, SPARQL delete, shared-wallet login, discovery mechanism).docs/fork-inbox-fallback.md— the Rust-patch / self-host inbox path NOT taken (kept as fallback).docs/migration-guide.md— the checklist for when real NextGraph matures.
What is emulated (and how it goes away)
Nothing in this library is a real NextGraph feature. Every behaviour below is
emulated — a stopgap fabricated on top of the current, immature NextGraph
(one shared wallet, everything physically readable). Each has a real NextGraph
target, and the switch to it is a lib-only swap: the consumer's SDK-shaped code
does not change (see docs/migration-guide.md). The
consumer always sees the mature SDK face; the emulation lives entirely here.
| Behavior | What the consumer sees (SDK-shaped API) | How it's emulated today (on one shared wallet) | Real NextGraph target | Migration (what changes; consumer unchanged) |
|---|---|---|---|---|
| Multi-user / per-user wallet | Each username is its own identity with its own documents | One shared wallet everyone opens; "users" are virtual wallets — shim accounts keyed by a virtual-wallet id, each mapped to its documents in store-registry.ts (simulation.md) |
One real per-user wallet each; native cross-wallet reads | Each virtual wallet → a real wallet; drop the shim; the physical/virtual split dissolves |
| 3 native stores per user | public / protected / private scopes |
3 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 (docCreate(..., undefined)), scope is a logical label (simulation.md) |
The user's 3 real native stores hold the entity documents | The 3 index docs become the 3 real stores; the logical scope label becomes real placement |
| Per-document read isolation | getCaps().open(doc, scope, owner) — the acts of granting |
Emulated CapRegistry (caps.ts, per-document ReadCap) + read filter (read-filter.ts, defence-in-depth view) + read-set construction in read-model.ts; owner/connections injected as principals (simulation.md) |
Broker/verifier only delivers documents the wallet holds a ReadCap for; useShape already returns an authorized subset |
Translate the registry to real caps; delete the read filter (dead code) — access unit is already the document (@graph) |
| Bilateral connections | declareConnections(peers) — declare your own side |
Emulated connection registry (connections.ts): directed assertions, a link is live only when both sides assert (two-sided) → drives protected read grants (simulation.md) |
Mutual capability exchange between two wallets | Real mutual caps replace the materialized link; drop the registry |
| Inbox (registration notifications) | inbox.post / read / watch |
Emulated deposits via SPARQL into an inbox document + in-client materialization (curator played inline); no native inbox_post exists in the JS SDK (simulation.md, nextgraph-current-state.md) |
Native per-document inbox: inbox_post_link seals a deposit; a separate curator materializes it |
post → native inbox_post_link; read side → a separate curator package (deferred) |
| Discovery of all public events | submitToIndex(ref) / readIndex() |
Emulated global index = a document owned by a reserved special account (@index), fed via its inbox + inline curator (dedup); a stable NURI every client resolves. NOT a physical-wallet scan (simulation.md, read-model.md) |
A real owned global document (undecided owner — singleton-app path only glimpsed), fed via its native inbox, materialized by a curator | Special account disappears; ownership moves to the decided owner; submitToIndex → native inbox post; curator queries the real index |
| Reads / listing | listMyMeetingPoints(), listEvents(), … by need |
Per-doc ANCHORED sparql_query over the virtual wallet's by-need doc set — never an anchorless scan of the physical wallet (O(wallet), ~90s timeout) and never the ORM fan-out (~75s hang) (read-model.md) |
Native per-wallet reads over real per-user stores | The anchored read is already native; only bringing a repo into the session becomes a real broker sync (the OpenRepo TODO) |
| Reactivity | Lists update on change | Re-query the bounded per-doc anchored set on a lightweight change signal (doc_subscribe / ORM on an already-opened single store) — there is no reactive union query (read-model.md) |
Native reactive reads | Re-query pattern collapses onto native reactive primitives |
| Writes | Write an entity to its scope | Per-entity documents via direct SPARQL (docs.sparqlUpdate on the real injected ng); doc_create can only target the private store today (StoreRepo not JS-constructible) (simulation.md) |
Writes land in the entity's real store via native primitives | docCreate targets the real per-scope store once the SDK lets you construct one |
| Login | login(username) / logout() |
Faux localStorage identity (accounts.ts): declarative username, no password, no NextGraph call — the shared-wallet broker gate stays open underneath (simulation.md) |
Opening your own wallet at the broker gate IS the login | Remove faux login; the broker redirect becomes the real per-user login (flow shape unchanged) |
| Write-guard | Writes refused without the write cap | Best-effort: the guard (ng-proxy.ts) fires only on the public proxy, but real write paths call the injected ng directly (the DataCloneError constraint) → not guarded today (simulation.md) |
Broker/verifier enforces the write cap natively | Native enforcement replaces the guard; delete it (dead code) |
Packages
| Package | Role | At migration |
|---|---|---|
@ng-eventually/client |
SDK-identical wrapper the app imports instead of @ng-org/web / @ng-org/orm. Adds the polyfills the broker/verifier will do natively (shared-wallet login, capability enforcement, anticipated cap/inbox methods). |
Disappears: the app points back at the real SDK (build alias removed). |
A global-index curator package is deferred. NextGraph is mono-user with no global data (apps/services see only what the user shares; there is no multi-user backend). A global index would come from a singleton app (a global document administered by the developer) — not implemented and uncertain, and simpler paths may exist. So no second package for now; it will be (re)introduced once the global-index mechanism is decided. The curator must never be bundled in the client → it will be a separate package when it lands.
Design principle
The application code is written as if the target NextGraph existed. All compensation lives here, beside the app. Migration = remove this layer; 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) anduseShape. The real SDK is injected viaconfigure()(no hard import → build-alias safe + testable). - Authorization = emulated capabilities: documents carry grants; the client enforces them generically (read filter + write guard). The app attaches grants via cap operations — same as it will in the target. No policy is injected.
- Inbox: the client
inboxnamespace deposits (post) and, in the shared-wallet emulation, also plays the curator inline (read/materialize/watch). At migration the read side moves to a separate curator package, deferred with the global-index mechanism — see the note above. - Tests of the polyfill (against a real broker) live in this repo, so the consuming app can test its features against a mocked, clean 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/listEntityDocs+ per-scope index, cross-device via the RDF shim anchored in the private store). - Document / SPARQL primitive —
docs.ts, calling the real injectedngdirectly (avoids the@ng-orgdouble-proxyDataCloneError). - Emulated ReadCaps —
caps.ts(CapRegistry, per-document) + read filterread-filter.ts(reactive-setProxyview), applied byuse-shape.tsonly when a policy is declared. - Write guard —
ng-proxy.ts(sparql_updateoverride, emulated write cap). - Inbox —
inbox.ts(post/read/materialize/watch, emulated curator inline). - Isolation —
isolation.ts(pure social-visibility matrix, distinct axis from ReadCaps). - Accounts —
accounts.ts(faux username login, injected storage). - SPARQL hardening —
sparql.ts(escapeLiteral/escapeIri/assertNuri).
Remaining TODO markers are narrow: the shared-wallet credential passthrough in
the login/session_start proxy branch, and the anticipated cap/inbox SDK
signatures to reconcile if the official API differs. See
docs/simulation.md for what each piece does and
docs/migration-guide.md for what is removed at
migration.