9.5 KiB
type, summary, last_checked
| type | summary | last_checked |
|---|---|---|
| knowledge | Who does what along a deposit's path — the application deposits, this package DISPATCHES on what a deposit declares itself to be and applies it arm by arm, and upstream none of it is reachable yet | 2026-08-21 |
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.
Applying is a DISPATCH — one arm per declared type
emulated-verifier/inbox-dispatch.ts, applyDeposit. It is a switch over what a deposit DECLARES itself to be (kindOf), and the shape is the target's own: Verifier::process_inbox(msg, content) is literally match content { … }, one arm per InboxMsgContent variant, ending _ => Err(VerifierError::NotImplemented) (engine/verifier/src/inbox_processor.rs).
What an arm owes, read off upstream's clearest arm (InboxMsgContent::ContactDetails): validate the envelope — a missing from_inbox is InvalidInboxPost, a hard failure and not a skip; build its addresses from the typed content; read the recipient's own store to see whether the thing is already there (a SPARQL ASK); decide idempotently — ContactAlreadyExists rather than a duplicate; and only then write. An arm is a real program, not a branch.
The two arms this package has:
- A
ReadCapdelivery (inbox-dispatch.LINK_KIND, whose name is upstream's own word for the variant) — held for the owner's ring and filed durably withbranch-registers.addLink, the emulatedAddLink { read_cap }. A payload declaring itself one of these and carrying nothing that reads is REFUSED and reported, which is the envelope validation above; it used to be handed back as consumer data. - A bare
reference— a deposit whose payload IS adid:ng:reference and nothing else: no operation, no claim, no copy of any value.emulated-verifier/index-deposit.ts,applyIndexDeposit, resolves it and LOOKS: which document the inbox belongs to (the owner's ownAddInboxCappairing,readInboxCapPairs), what that document declares it indexes by (urn:ng-helpers:index:field, on the document's own NURI as subject), whether the reference is already in, what the referenced object says for that field — and only then writes ONE entry (urn:ng-helpers:index:value, subject = the object's NURI). Since anyone may deposit, a payload carrying an operation would be a licence to rewrite someone else's document, and a payload carrying a value would put in the index something the object does not say. An index only ever grows: nothing there removes an entry and nothing there can express a removal, which is what makes an empty read harmless — absent, unreadable and failed all mean not added this time, and a later deposit adds it. An unresolved reference is reported and costs nothing; an already-indexed object is passed over; a document declaring no field, or several, refuses rather than write something wrong — and that refusal leaves READING its entries untouched.
(Those two IRIs are @ng-helpers/indexing's namespace, kept letter for letter because renaming them is a data-format change nobody decided. This package writing another package's namespace is odd, and it is recorded as odd rather than acted on.)
A kind with no arm says so. A payload declaring itself in this package's reserved urn:ng-eventually:inbox: namespace for a variant nothing here applies is REPORTED — upstream's NotImplemented, which is an answer. Until 2026-08-21 it was read, counted, and produced nothing at all: no effect and no reported failure, the "failure disguised as an absence" this package has closed repeatedly elsewhere.
Consumer data is not a variant. A payload that declares no kind of this package's is claimed by no arm and nothing is reported about it: it is not an unapplied message, it is not a message. That is why the RESERVED NAMESPACE is the discriminator and the mere presence of a kind field is not — { kind: "join" } is a consumer's word for a consumer's message, and it stays one.
What still reaches the caller. Only a ReadCap is withheld: an application must never handle one. Everything else read found is returned unchanged, including the payloads an arm applies — a reference is public information, so withholding it would buy nothing. Two senses of applied stay apart: read learns a ReadCap in memory, for this session; only processInbox makes it durable and runs the dispatch. inbox.watch re-reads on every push, so it absorbs and files nothing.
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.
And it applies the registers, not the documents. Running for an owner who is not the one holding the page, it takes the arms that write the verifier's own bookkeeping and stops there: writing application data into an absent owner's documents from a stranger's session is a different act, and this package does not take it. So a reference deposit is left — traced, not reported, because waiting is not failing — and the owner's own next connection applies it. Nothing is lost: see below.
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 refusal and every watch that could not be opened harmless — what was not applied is still there for the next push or the next connection. It is also what makes re-applying safe: every arm is idempotent, so seeing every deposit again on every run lands on the same result.
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.