fix: un dépôt est délivré une fois et retiré, comme en amont
This commit is contained in:
@@ -30,8 +30,8 @@ What it never does: **process an inbox so that a `ReadCap` sent to it arrives**.
|
||||
|
||||
The two arms this package has:
|
||||
|
||||
- **A `ReadCap` delivery** (`inbox-dispatch.LINK_KIND`, whose name is upstream's own word for the variant) — held for the owner's ring and filed durably with `branch-registers.addLink`, the emulated `AddLink { 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 a `did: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 own `AddInboxCap` pairing, `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.
|
||||
- **A `ReadCap` delivery** (`inbox-dispatch.LINK_KIND`, whose name is upstream's own word for the variant) — held for the owner's ring and filed durably with `branch-registers.addLink`, the emulated `AddLink { 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. Being refused does not save it: the arm ran, so the deposit was already consumed.
|
||||
- **A bare `reference`** — a deposit whose payload IS a `did: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 own `AddInboxCap` pairing, `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. That once made an empty read harmless — absent, unreadable and failed all meant *not added this time*, and a later drain settled it. Consumption ends that, and forces a distinction the read layer erases: `read-model.readDoc` answers `[]` whether the document is empty or the broker failed. Three sources of "no rows" are now told apart (`index-deposit.noRowsMeansAbsent`). A read that **threw** — reported through the new `onUnreadable` hook — is `unreadable`. A repo whose sync this session never confirmed is also `unreadable`, and this is the one that is easy to miss: on a fresh session over a persistent wallet the repo is present but its commits have not arrived, so the anchored query matches nothing **and raises nothing** — `open-repo` keeps `synced` and `timed-out` apart for exactly this, and says so in those words. A document this holder cannot even attempt — no cap, so the read layer drops it before issuing a query — is `unreadable` too, and that one is counter-intuitive enough to state plainly: "no cap" LOOKS deterministic and is not, because for a stranger's public document possession comes from a network fetch that swallows its errors and memoises the failure, so one outage is indistinguishable from a document that never existed. Only a document that was actually QUERIED, from a confirmed repo, and still read empty is genuinely absent — that is `refused`: reported, costs its own deposit, and the deposits behind it keep their turn. `unreadable` THROWS and stops the drain, so an outage cannot walk the queue. Getting the split wrong is costly in both directions and both were shipped before it settled: treating a failure as an absence destroyed whole queues on a cold start, and treating an absence as a failure let one dead reference — deposited by anyone — stall an index's queue one deposit per connection. An unresolved reference costs its own deposit and is reported; an already-indexed object is passed over; a document declaring no field, or several, refuses rather than write something wrong (both are clean reads, so both are `refused` and neither stops the drain) — 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.)*
|
||||
|
||||
@@ -43,9 +43,15 @@ The two arms this package has:
|
||||
|
||||
**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.
|
||||
**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. It is left in the QUEUE too, not merely unapplied: what a stranger's drain declines to apply it must not consume either, or the owner would lose it to a session that was never going to apply it. 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.
|
||||
**What is applied is CONSUMED — one act, one place.** Since 2026-08-26 a deposit this package claims is retired from its inbox by `emulated-verifier/inbox-consume.ts`, which holds the only removal and the only loop that performs it; both triggers above call it and neither carries a copy. The order is upstream's, read in its source: the broker removes the message from the queue *before* delivering it (`take_first_msg` → `MSGS.take_first_value`, a read followed by a `del`), and the verifier then applies it and merely logs what it could not (`log_err!("Error during process_inbox {e}")`, never re-enqueued). So **remove, apply, log, move on** — a deposit is delivered once whether or not its arm succeeded, and a refusal is reported once instead of at every push and every connection for ever.
|
||||
|
||||
**What is NOT claimed is not consumed**, and the distinction carries the safety of the whole thing. Two things are left in the queue: a consumer's own payload, which declares no kind of this package's and is not a message at all; and an arm that belongs to the owner's own session while a stranger's is running — the `reference` case above. `inbox-dispatch.claimsDeposit` asks that question *before* the removal, precisely so that what a stranger's drain declines to apply it also declines to pop. Upstream the question does not arise, because only a registered reader is ever popped for (`inbox_pop_for_user`); here one inbox mixes both kinds, so it has to be read off the payload. Getting it backwards is the one way consuming could lose data outright — a stranger's timer retiring a reference the owner never indexed.
|
||||
|
||||
**A failure that happens before the pop still leaves the deposit.** A drain whose *read* fails takes nothing out, so what is queued waits for the next attempt; a drain whose *arm* fails has already consumed what it was given. The two are different sentences and the package's log says which is which.
|
||||
|
||||
**The idempotence that remains is not a replay compensation.** Filing the same Link twice is still a no-op, and a reference already in an index is still passed over — because the same cap or the same object can legitimately arrive as **two distinct deposits** (a re-share, a rotation, two depositors pointing at one thing), not because one deposit is seen twice. Upstream keeps the same check with a queue that pops (`ContactAlreadyExists`), which is the proof it never answered the replay.
|
||||
|
||||
## NextGraph itself
|
||||
|
||||
|
||||
Reference in New Issue
Block a user