docs: qui traite une inbox, acteur par acteur — et watch absorbe sans classer

This commit is contained in:
Sylvain Duchesne
2026-08-20 14:31:24 +02:00
parent 0e57f20ca4
commit 1b4fcb567e
3 changed files with 56 additions and 3 deletions
@@ -37,3 +37,4 @@ This repo is the **provider** of `polyfill-surface`; consuming applications live
- `rule_no-divergence-from-nextgraph` — the test that decides what may be built at all.
- `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have.
- `knowledge_what-an-inbox-is-upstream` — what the target actually offers around inboxes, and where a choice of ours begins.
- `knowledge_who-processes-an-inbox` — the division of labour along a deposit's path: what this package applies, what it merely delivers, and what a layer above owes itself.
@@ -0,0 +1,50 @@
---
type: knowledge
summary: Who does what along a deposit's path — the application deposits, this package applies capabilities and only capabilities, a layer above curates its own payloads, and upstream none of it is reachable yet
last_checked: 2026-08-20
---
# Who processes an inbox
[[knowledge_what-an-inbox-is-upstream]] settles what the target *offers*: a document may be given an inbox, nothing above the engine exposes one, and the processor set admits nothing from outside. It says nothing about who does the work **here**. That is this leaf.
The question is hard to answer out loud — *"does the polyfill process the inbox automatically?"* took one agent a long exchange to fail at — because **processing means a different thing at each layer**. Naming the actor first makes it easy. Everything below is read off the code; symbols are cited so `git grep` settles any doubt.
## The application
It **deposits** by naming a document (`inbox.postToDocument`) or a person (`inbox.share`), **reads its own** with `inbox.readForDocument` / `readSyncedForDocument`, and may **watch** one with `inbox.watch`.
What it never does: **process an inbox so that a `ReadCap` sent to it arrives**. `inbox.processInbox` is published, but no application has to call it — connecting drains, and every arrival afterwards is applied. It never resolves someone else's inbox address (the only published call that returns one is `storeRegistry.openDocumentInbox`, on a document it owns), and it has no way to reach, hurry or disable the deferred stand-in below.
## `@ng-eventually/polyfill`
**At connection**`emulated-verifier/connect.ts`, `connectedUser`: restore every durable register, then `processInbox` on each inbox this identity holds, then start the continuous observation. Failing to apply one queue is reported and denies nobody their session; failing to *reach* the queues rejects.
**Continuously, while the identity is connected**`emulated-verifier/inbox-observer.ts`, `startObservingInboxes`, whose only caller is `connectedUser`: one subscription per inbox, and every push runs `processInbox` on it. *Which* inboxes exist is itself subscribed to, on two channels that do not overlap: the **register** (the User branch of the private store, where `openDocumentInbox` appends its record — so an inbox opened mid-session is picked up) and **what this identity holds** (`CapRegistry.onChange` — which is what makes a person's *first* session watched at all, since provisioning is lazy and they connect owning nothing). Push-driven throughout; it never polls. It stops on identity change, on disconnection, and on `configure` / `resetConfig` (`stopObservingInboxes`, called only from `shared-wallet/bootstrap`). Every step re-checks its holder, so a run that outlives its identity abandons rather than file into the wrong ring.
**What it applies: capabilities, and nothing else.** VERIFIED in `surface/inbox.ts`. `read` keys on `capOfPayload`, which accepts a payload only when its `kind` equals `LINK_KIND` *and* its `cap` passes `hasReadCap` (a NURI carrying its cap segment). Those are absorbed and withheld from the caller; every other deposit is returned as consumer data. `processInbox` then iterates **the caps that read just observed** (`capsSeenIn`) — not the deposits — and files each durably with `addLink`.
So a deposit carrying anything else is read, counted, logged under the access-log flag, and **produces no effect** — not even a reported failure, because it is not a failed application: it is a deposit nobody claimed. `processInbox` hands it back to its caller, and both internal callers (`connectedUser`'s drain loop, the observation's `applyWhatArrived`) discard what they get.
Two senses of *applied*, worth keeping apart: `read` learns a cap **in memory, for this session**; only `processInbox` makes it **durable**. `inbox.watch` re-reads on every push, so it absorbs caps and files none.
**After a deposit whose recipient is absent.** A deposit that landed arms a deferred stand-in for the session the owner does not have. What it achieves is the only part written down outside the package: **deposits converge** — a share becomes readable without the recipient re-opening the page. *How* is deliberately not documented here. The mechanism is unpublished by design: nothing lets a caller ask for it, name another user's inbox, or turn it off, and the confidentiality of the whole shared-wallet emulation rests on its being unreachable. Its limit is stated rather than hidden — it lives in a page, so if the page goes away the deposit waits for its owner's next connection, which is the real path and not a repair.
**Nothing is ever consumed.** No path retires an applied deposit: an inbox here is re-readable where upstream it is a queue. That is what makes every abandon, every reported failure and every watch that could not be opened harmless — what was not applied is still there for the next push or the next connection.
## A package built on top — `@ng-helpers/indexing`
The layer below cannot curate an index: an index deposit is a bare reference (`deposit.ts`), which is exactly what the polyfill hands back untouched. So this package does for its own payloads what the polyfill does for caps, and does it itself:
- **finds** what to serve — `observeOwnIndexes` lists this identity's public documents and keeps those declaring an index field. Only the owner can, since reading an inbox is refused to everyone else;
- **watches, then processes**, in that order (`observation.ts`): a deposit landing between the two is processed twice, which costs nothing, where the other order loses it until the next connection;
- **applies** by resolving the reference and looking (`curator.ts`): the object is opened and indexed under the value it holds, or nothing happens. A deposit is an invitation to re-examine, never an instruction;
- **re-sees everything on every run**, since deposits are not consumed — affordable only because curation is convergent and an index only ever grows.
It holds exactly one inbox address, obtained from `storeRegistry.openDocumentInbox` in `polyfill-adapter.ts`, and no other.
## NextGraph itself
The **broker** routes a sealed message by inbox key (`inboxes: PubKey → RepoId`, `engine/verifier/src/verifier.rs`) and queues it while the recipient is away. The **recipient's own verifier** applies it inline as it arrives — `LocalBrokerMessage::Inbox``session.verifier.inbox(&msg, from_queue)` in `sdk/rust/src/local_broker.rs`; `from_queue` distinguishes the backlog handed over at connection from the messages that follow, and **both go through the same door**, which is why the continuous regime above is the rule and the connection drain the special case.
And **none of it is reachable from JS**: the processor set is closed, the variants that would carry a payload carry nothing, and the post command has no handler arm — all in [[knowledge_what-an-inbox-is-upstream]]. Every actor above except the broker and that verifier is therefore ours, emulating one thing: a session applying its own inbox, continuously.
@@ -23,9 +23,11 @@
*
* ── Watching is not applying ──────────────────────────────────────────────
* `inbox.watch` looks like this and is not: it NOTIFIES an application that made a call and
* named one inbox, and it applies nothing. This applies, on the whole set, without anybody
* asking — because processing an inbox is the library's job and not the app's, which is the
* same ruling `connect.ts` opens with.
* named one inbox. Its read does absorb the caps it passes over into memory for the session
* — that much is unavoidable, reading is what surfaces them — but it FILES nothing durably,
* which only `processInbox` does. This applies, on the whole set, without anybody asking —
* because processing an inbox is the library's job and not the app's, which is the same
* ruling `connect.ts` opens with.
*
* ── Subscription, never a poll ────────────────────────────────────────────
* Every half is push-driven. The inboxes themselves are subscribed to individually. WHICH