Files
ng-eventually/README.md
T
Sylvain Duchesne 5717e08f6d docs: "what is emulated (and how it goes away)" recap table + read-path reconcile
README: new section with a recap table (11 rows) — for each emulated behavior:
what the consumer sees (SDK-shaped API), how it's emulated on one shared wallet,
the real NextGraph target, and the lib-only migration. Makes "emulated ≠ real,
migration is a lib-only swap" explicit.
simulation.md: opening banner that EVERYTHING in the file is emulation pending
real NextGraph; corrected the stale read-path paragraph (per-doc anchored, never
an anchorless union-scan). read-model.md: reactivity bullet aligned to per-doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 11:18:10 +02:00

12 KiB

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 anchored sparql_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) and useShape. The real SDK is injected via configure() (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 inbox namespace 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 shimstore-registry.ts ((account, scope) → document NURI, createEntityDoc / listEntityDocs + per-scope index, cross-device via the RDF shim anchored in the private store).
  • Document / SPARQL primitivedocs.ts, calling the real injected ng directly (avoids the @ng-org double-proxy DataCloneError).
  • Emulated ReadCapscaps.ts (CapRegistry, per-document) + read filter read-filter.ts (reactive-set Proxy view), applied by use-shape.ts only when a policy is declared.
  • Write guardng-proxy.ts (sparql_update override, emulated write cap).
  • Inboxinbox.ts (post / read / materialize / watch, emulated curator inline).
  • Isolationisolation.ts (pure social-visibility matrix, distinct axis from ReadCaps).
  • Accountsaccounts.ts (faux username login, injected storage).
  • SPARQL hardeningsparql.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.