fix: un dépôt est délivré une fois et retiré, comme en amont

This commit is contained in:
Sylvain Duchesne
2026-08-26 14:11:56 +02:00
parent abfb619c76
commit f6a48a4507
24 changed files with 1486 additions and 176 deletions
@@ -122,7 +122,7 @@ What was shared with you becomes readable after `ensureIdentity()`.
`urn:ng-eventually:` is reserved, and the reservation has two faces. In a document: triples whose **subject** falls under that prefix are dropped on read and never returned by `readUnion`; every other IRI is returned. In a deposit: two payload shapes are this package's own and it acts on them — a payload that IS a reference (a bare `did:ng:…` string and nothing else), and an object whose `kind` property is a string starting with `urn:ng-eventually:inbox:`. **Every other payload is yours, and is delivered to you untouched.**
**Do not use either shape to carry your own data.** Depositing one is still accepted and it is still returned to you by `inbox.read` and its variants — nothing is withheld — but this package now also acts on it, and what you observe is one of two things: it writes an entry into the document the inbox belongs to, if that document is one it can index; otherwise it reports a refusal on its own log stream (`console.error`, ungated, as below). That refusal does not clear on its own the way an unreachable inbox does: a deposit is not consumed, so the same refusal is reported again at every arrival on that inbox and at every connection, for as long as the deposit sits there. Carry your own data in an object with a `kind` of your own — `{ kind: "join", … }` is yours and stays yours — and never as a bare reference on its own.
**Do not use either shape to carry your own data.** Depositing one is still accepted and it is still returned to you by `inbox.read` and its variants — nothing is withheld — but this package now also acts on it, and what you observe is one of two things: it writes an entry into the document the inbox belongs to, if that document is one it can index; otherwise it reports a refusal on its own log stream (`console.error`, ungated, as below). That refusal is reported ONCE and then the deposit is gone: this package consumes what it claims, whether or not the arm succeeded. Until `1.0.0-dev.4` the same refusal came back at every arrival on that inbox and at every connection, for as long as the deposit sat there. The same now applies when the referenced document resolves but carries **nothing for the field its index declares** — a draft, say: the reference is discarded and reported, where before it sat in the queue and would have been indexed by a later drain once the document gained that field. Deposit the reference again after the document says what it needs to say. Carry your own data in an object with a `kind` of your own — `{ kind: "join", … }` is yours and stays yours — and never as a bare reference on its own.
Only a document's owner writes to it. Holding its read key never grants a write.
@@ -162,7 +162,15 @@ The same rule reaches what a call hands BACK, not only what it looked up first:
**The watching is in place by the time `ensureIdentity()` resolves**, whatever else that call made of its own work: a connection that could not restore something still rejects, and the identity it settled is watched all the same. It lasts exactly as long as that identity stays connected — changing identity or clearing it stops it, and whoever connects next is watched in their own right, so nothing of the previous one keeps applying.
**Failing to apply one inbox denies nothing.** It is reported on this package's own log stream (`console.error`, carrying this package's prefix) and never gated by `debugAccessLog` — a diagnostic may be opt-in, a failure may not. The deposit stays in its queue, so the next arrival on that inbox, or the next connection, applies it; the other inboxes were never involved, and nobody is refused anything.
**A deposit this package applies is delivered ONCE, and a failure is not retried.** It is taken out of its inbox before the arm that handles it runs, so it is gone whether that arm succeeded or not — the same order the target has, where the broker removes a message from the queue as it hands it over and the verifier merely logs what it could not apply. A failure is reported on this package's own log stream (`console.error`, carrying this package's prefix), never gated by `debugAccessLog` — a diagnostic may be opt-in, a failure may not — and then the next deposit is processed. **Do not count on a failed deposit being applied later: there is no retry, no dead-letter queue and no way to ask what was lost.** If an arrival matters to your application, the record you keep of it must be your own. What this buys is the end of the opposite behaviour: an inbox that grew for ever, and one malformed deposit re-reported at every arrival and every connection for the life of the session.
**An unapplicable item and a failed one are not the same event.** A deposit this package cannot make sense of is reported and the next one is processed — one malformed item never holds up the queue behind it. A deposit whose handling could not be COMPLETED is different: the drain STOPS, the inbox is reported, and everything not yet taken out is preserved. Only the deposit in flight is lost. So a failing broker costs one deposit per attempt, not the whole queue, and a drain that stopped is retried by your next `ensureIdentity()`.
**A reference this package cannot read counts as failed, not as absent** — including a document you hold no capability for. That is deliberate and it costs latency: such a deposit stops one drain, and the deposits behind it land on the next connection instead of this one. The alternative was worse and was measured: treating "cannot read it" as "it is not there" destroyed deposits on a transient outage, and destroyed the re-deposits sent to repair them, because a failed capability fetch is remembered for the session. Only a document actually queried, from a synced repo, that still reads empty is called absent. **Consequence to plan for:** a queue holding many references you cannot read drains one per connection.
**`inbox.processInbox` now REJECTS on a broker failure**, where it used to resolve — including when a document a deposit referenced could not be READ. A referenced document that is merely absent is a different case: that is reported and the queue carries on. Awaiting it, handle the rejection: the deposit it was applying at that moment is already consumed, the rest of that inbox is intact, and calling again resumes from the next one. Connection-time draining is unaffected — `ensureIdentity()` reports such a queue and still grants the session, exactly as before.
**Failing to apply one inbox still denies nothing else.** The other inboxes were never involved, the session is not refused, and deposits this package does not claim — your own payloads — are untouched and stay readable through `inbox.read` and `inbox.watch`.
**Any number of subscriptions on one document coexist.** Opening a document, watching an inbox and following a scope no longer silence one another. Until 2026-08-17 a second `subscribeDoc` on a document killed the first, silently — nothing rejected, the first caller's unsubscribe still appeared to work, and what an application saw was a view that stopped re-reading and an inbox that stopped notifying, with no trace anywhere near the cause. A subscriber that joins a document somebody else already opened is handed the initial `State` its own subscription would have pushed it, so joining late is not the same as never firing; and unsubscribing silences that caller and no other, including when it happens from inside a push.
@@ -202,10 +210,10 @@ What each level means here, in this package's own terms:
- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way).
- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`.
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.3`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.4`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.3` is this package, `ng-e2e-helpers/v…` is the other one. A bare `v…` tag would say nothing about which surface it froze the day the two diverge — which is the day one of them takes a major and the other does not.
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.4` is this package, `ng-e2e-helpers/v…` is the other one. A bare `v…` tag would say nothing about which surface it froze the day the two diverge — which is the day one of them takes a major and the other does not.
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.3`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.3`, the string you pinned, never the version it is heading for. `1.0.0-dev.3` narrows an existing call rather than moving a signature: two payload shapes an application could carry freely are now this package's own, and one it cannot apply is reported — a state you did not have to handle before, which is a **major** by the rules above. It lands inside the pre-release line rather than on a new major number because `1.0.0` has not been cut, so there is nothing yet to break away from.
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.4`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.4`, the string you pinned, never the version it is heading for. `1.0.0-dev.4` narrows a **guarantee** and adds a rejection: a deposit this package applies is now consumed, so a failure that used to be retried at the next arrival or the next connection is now final and reported once; and `inbox.processInbox` rejects on a broker failure where it used to resolve. An application that leaned on the retry — however implicitly — has to keep its own record instead, and one that awaited `processInbox` without a `catch` now has a rejection to handle. Either is a **major** by the rules above. `1.0.0-dev.3` before it narrowed an existing call the same way: two payload shapes an application could carry freely became this package's own, and one it cannot apply is reported. It lands inside the pre-release line rather than on a new major number because `1.0.0` has not been cut, so there is nothing yet to break away from.
There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it, and a symbol is never left published-but-dead as a courtesy. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first.