feat(client): discovery via a global index (special @index account)
Add a generic discovery-index surface: submitToIndex(ref) deposits a reference into the index document's inbox; readIndex() returns the materialized entries. A reserved special account (@index) owns the index document; deposits flow through the emulated inbox and are materialized by the emulated curator (the dedup/ moderation point). This replaces cross-account fan-out as the discovery path and is more faithful to the target (a single owned index fed via its inbox). Generic (the consumer supplies the reference to index). 79 tests pass; tsc rc=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,16 +60,26 @@ bound to the developer-user — **not implemented, uncertain**, to explore later
|
||||
This is why a global-index curator is a **deferred separate package** in this lib
|
||||
(see the top-level README).
|
||||
|
||||
## Polyfill reality (fan-out) vs target (global index)
|
||||
## Polyfill reality — the fan-out drift is now RESOLVED (special-account index)
|
||||
|
||||
What ships in the shared-wallet polyfill today is the **cross-account fan-out over
|
||||
The shared-wallet polyfill originally shipped a **cross-account fan-out over
|
||||
every account's public documents** (`store-registry.ts` `listEntityDocs('public')`
|
||||
/ `resolveReadGraphs`) — one account sees another's public entity **without a
|
||||
connection**. This ADR classified per-account fan-out as a **drift** to be
|
||||
replaced by the single global index; the target (inbox-fed global index) remains
|
||||
valid but the fan-out is the mechanism the shared-wallet staging actually runs on
|
||||
until the global-index owner is decided. Recorded here as mechanism history — the
|
||||
resolution belongs to [`../migration-guide.md`](../migration-guide.md).
|
||||
/ `resolveReadGraphs`) — one account saw another's public entity **without a
|
||||
connection**. This ADR classified that per-account fan-out as a **drift** to be
|
||||
replaced by the single global index.
|
||||
|
||||
**That drift is now resolved in the polyfill.** The inbox-fed global index of
|
||||
this ADR is implemented on top of a **RESERVED SPECIAL ACCOUNT** in the shim
|
||||
(`discovery.ts`, `INDEX_ACCOUNT = "@index"`) that owns the index document while
|
||||
the target owner stays undecided: `submitToIndex(ref)` deposits into the index
|
||||
document's inbox; `readIndex()` materializes (dedup) the entries. The app-facing
|
||||
discovery path is now **read the index**, exactly as this ADR prescribes — NOT
|
||||
the fan-out. The cross-account fan-out survives only as an **internal lib
|
||||
fallback** (it still powers per-scope listing like `resolveReadGraphs`), never
|
||||
the discovery route. The special account is the provisional owner; at migration
|
||||
it disappears and ownership moves to the decided global-index owner (see
|
||||
[`../migration-guide.md`](../migration-guide.md)) with the consumer surface
|
||||
(`submitToIndex` / `readIndex`) unchanged.
|
||||
|
||||
## Alternatives rejected (mechanism)
|
||||
|
||||
|
||||
@@ -274,6 +274,51 @@ the deferred global-index note in the top-level README and
|
||||
watcher is the ONE deposit/materialization mechanism reused for BOTH meeting-point
|
||||
registration AND submission to a discovery index — same `post` API, same watcher.
|
||||
|
||||
## Emulated discovery index + special account (`discovery.ts`)
|
||||
|
||||
Discovery is a **surface on top of the inbox**, not a new primitive. **Access ≠
|
||||
discovery**: a public entity is world-readable *with its NURI*; the discovery
|
||||
index is how a client learns that NURI **exists** without holding a connection
|
||||
to its creator (see [`decisions/discovery-model.md`](./decisions/discovery-model.md)).
|
||||
The model is: ONE global index = an **owned document** (public read), **fed via
|
||||
ITS inbox**, **materialized by a curator**. Nobody writes the index directly — a
|
||||
creator DEPOSITS a reference into the index's inbox; the curator ingests deposits
|
||||
into entries. Materialization is the natural **dedup / moderation point**.
|
||||
|
||||
- **The special account (polyfill owner).** "Who owns the global index" is
|
||||
undecided in the target (NextGraph is mono-user with no global data — a
|
||||
singleton app is the only glimpsed path). So the polyfill parks ownership on a
|
||||
**RESERVED SPECIAL ACCOUNT** in the shim — `INDEX_ACCOUNT = "@index"`. It is a
|
||||
normal shim account (so its 3 scope documents are created on first sight like
|
||||
any other), but never a real user; it only HOSTS the index document. Its
|
||||
`public` scope document IS the index document, and its inbox receives the
|
||||
deposits — a **stable NURI**: every client opening the same shared wallet
|
||||
resolves the same account → same document, so all clients read/write ONE
|
||||
shared index.
|
||||
- **`submitToIndex(ref, opts?)`** — the SDK act "make this discoverable".
|
||||
Deposits `ref` into the index document's inbox via `inbox.post`. `from` follows
|
||||
the inbox convention (anonymous when `null`). `ref` is **opaque** here — the
|
||||
consumer serializes whatever locates the entity (e.g. an entity document NURI +
|
||||
discovery metadata).
|
||||
- **`readIndex()`** — the EMULATED CURATOR. Reads every submission, **dedups by
|
||||
serialized `ref`** (the moderation point: a duplicate submission surfaces
|
||||
once), returns entries sorted by `ts`. `watchIndex(onEntries, opts?)` is the
|
||||
emulated watcher (polls `readIndex`).
|
||||
|
||||
**This REPLACES the cross-account fan-out** (`store-registry.ts`
|
||||
`listEntityDocs('public')` / `resolveReadGraphs`) as the app-facing discovery
|
||||
path: the app submits public entities to the index and reads the index, instead
|
||||
of fanning out over every account's public documents. The fan-out survives only
|
||||
as an **internal lib fallback** — kept for the per-scope listing it also powers
|
||||
(e.g. `resolveReadGraphs`), never the app's discovery route.
|
||||
|
||||
GENERIC: `discovery.ts` knows no application domain — the consumer defines the
|
||||
`ref` shape and its meaning. At migration the special account disappears:
|
||||
ownership moves to the decided global-index owner, `submitToIndex` becomes the
|
||||
native `inbox_post_link` on the index's inbox, and `readIndex` queries the real
|
||||
materialized index document. The consumer surface (`submitToIndex` / `readIndex`)
|
||||
is designed to survive that swap unchanged.
|
||||
|
||||
## Emulated write guard (`ng-proxy.ts`)
|
||||
|
||||
The public `ng` proxy overrides `sparql_update` to enforce an emulated **write
|
||||
|
||||
Reference in New Issue
Block a user