docs: dire l'observation continue, sa limite, et ce qu'ensureIdentity ne promet pas

This commit is contained in:
Sylvain Duchesne
2026-08-17 14:47:40 +02:00
parent c7c6cb96ea
commit fafac38907
5 changed files with 24 additions and 30 deletions
-8
View File
@@ -1,8 +0,0 @@
# Doc-debt — app-contract
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED packages/polyfill/src/surface/subscribe.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
- TOUCHED packages/polyfill/src/surface/inbox.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
@@ -132,7 +132,7 @@ Only a document's owner writes to it. Holding its read key never grants a write.
`ensureIdentity()` settles the identity, completes the connection work it starts, and returns the identity. It takes no identifier, and no other call takes one.
It resolves **only once that work has actually completed**: if what was shared with you could not be restored, or a queue could not be drained, it throws instead of returning. So a resolved call means everything shared with you is readable — and a rejected one must not be rendered past, since the interface would show an empty account rather than an empty screen.
It resolves **only once that work has actually completed**: if what was shared with you could not be restored, it throws instead of returning, and a rejected call must not be rendered past — the interface would show an empty account rather than an empty screen. A single queue that could not be drained is reported and does not reject: reaching your queues is infrastructure and must succeed, applying one deposit is data and must never cost you the session. So a resolved call means your own capabilities are restored; it does not promise that every deposit waiting for you has been applied, and those that were not stay in their queue.
`ensureIdentity()` mounts a full-screen barrier on every top-level load, and takes it down itself — past the broker round-trip it stays down, provided the identifier reached the other side. A person who comes back to the page from that round-trip finds the barrier live again, prefilled, and confirming it hands the page over a second time. The application's own page is never reloaded and nothing outside the barrier is touched.
@@ -156,6 +156,14 @@ Where a call must first find out whether something already exists — a document
The same rule reaches what a call hands BACK, not only what it looked up first: **`listMyEntityDocs` returns a listing whose documents you can open, or it throws.** It reads which documents are in the store and what opens each, and it throws if either did not answer — including when the documents came back and their keys did not. Nothing about a keyless listing is visible to you: it is the same `Nuri[]`, and the difference would only appear at the next read, empty, long after the cause. An empty array therefore means this account created nothing.
**A deposit made while a person is looking at the page arrives while they are looking at it.** For as long as an identity is connected, every inbox it may read is watched and what lands in one is applied as it lands — its own inbox, and the inbox of every document it has opened one on, including a document whose inbox it opens later in the same session. So a `ReadCap` sent with `inbox.share` becomes usable in the recipient's live session with no reload and no call from the application, and a `watchShape` that was empty for want of that `ReadCap` re-reads and publishes the document it now opens. Until 2026-08-17 only the backlog waiting at connection was applied, and a deposit made in front of its recipient converged only when that person reloaded the page.
**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.
**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.
## Non-guarantees
**No display name.** `ensureIdentity()` returns an opaque identifier: do not parse it, split it, or render it as a readable name.
@@ -180,6 +188,8 @@ The same rule reaches what a call hands BACK, not only what it looked up first:
**No unfiltered read through `useShape`.** Members that yield items are filtered and mutations pass through; anything else throws. A document reached through that view alone, read nowhere else first, does not appear.
**A watch on one inbox that could not be opened does not come back on its own.** Opening it can fail — a broker that does not answer in that moment — and the failure is reported rather than passed over, but what follows is event-driven and this package deliberately never polls. The watch is opened again at the next moment this identity comes to hold something it did not: it creates a document, it opens an inbox on a document, a `ReadCap` reaches it through an inbox still being watched, it reads a public-store document for the first time. A session that does none of those goes on without that inbox — deposits made into it are not applied, nothing raises, and they wait unconsumed for the next connection. The exposed case is the identity that only ever READS, since it produces none of those events, where an identity that goes on creating recovers as a by-product of its own work. Awaiting `ensureIdentity()` again applies what is waiting — it drains every one of this identity's queues before it resolves, and shows no barrier a second time — but the watching itself comes back only on a fresh page.
## Change policy
**Semver, and majors are the normal case.** This surface converges on a NextGraph that does not ship yet, so most steps toward the target remove or narrow something — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time.
@@ -192,8 +202,8 @@ What each level means here, in this package's own terms:
**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.
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.1` 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.2` 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.1`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.1`, the string you pinned, never the version it is heading for.
`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.2`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.2`, the string you pinned, never the version it is heading for. `1.0.0-dev.2` added the continuous inbox observation and the coexisting document subscriptions above and moved no signature — a minor, landing inside the pre-release line because `1.0.0` has not been cut.
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.
-10
View File
@@ -1,10 +0,0 @@
# Doc-debt — e2e-harness
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED packages/polyfill/e2e/reactivity-doc-subscribe.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
- TOUCHED packages/polyfill/e2e/repro-fresh-wallet.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
- TOUCHED packages/polyfill/e2e/run.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
- TOUCHED packages/polyfill/e2e/notebook.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
-8
View File
@@ -1,8 +0,0 @@
# Doc-debt — sign-in
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED packages/polyfill/src/emulated-verifier/connect.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
- TOUCHED packages/polyfill/src/shared-wallet/bootstrap.ts @2026-08-17 (session f93872b5-293a-4916-a353-181409a96d42)
@@ -8,7 +8,7 @@ summary: Deciding which identity acts needs no session; connecting does — conf
Two acts of different nature hide behind "sign in":
- **`settle`** — decide which identity is acting, from the URL, from storage, or by asking at the `barrier`, then persist it. Pure DOM and storage. **No session required.**
- **connect** — put back in that identity's hands everything it can already open: what it OWNS as well as what was shared with it, then drain its queues. **Requires a live session.**
- **connect** — put back in that identity's hands everything it can already open: what it OWNS as well as what was shared with it, drain its queues, and then keep applying what arrives in them for as long as it stays connected. **Requires a live session.**
The wallet keeps those two in different places, and connecting once replayed only the second — so an application that reloaded and went straight to a document it had made itself was refused its own document. Whatever else changes here, connecting must replay **every** durable register, not the one that happens to be read on the path being tested.
@@ -49,3 +49,13 @@ A deposit that cannot be applied is not consumed by failing. So a single unusabl
The distinction to keep: **reaching** the queues, and the restore itself, are infrastructure — if they fail, the session genuinely cannot proceed, and rejecting is right. **Applying one item** is data — it must be reported loudly, the remaining queues must still be drained, and the session must still be granted.
The general shape, worth carrying to any similar rule: when a blanket "every failure surfaces" is imposed, ask which failures are *retried by simply trying again later* and which are *permanent for the actor*. The permanent ones must never gate something the actor cannot otherwise obtain.
## Connecting does not end when the drain does
The drain is the backlog; being connected is a regime. An identity that stays connected has its inboxes watched throughout, so a deposit made in front of it is applied as it arrives rather than waiting for a reload, and an inbox opened later in the session joins the watched set.
That third step runs whatever became of the two before it, and the failure it was added for says why. A restore that rejected used to skip it, leaving an identity connected — recording who acts is synchronous and had already taken effect — with nothing watching its inboxes for the rest of the session. One broker hiccup while signing in then cost that person every deposit made from then on, in silence, long after the broker had recovered. What the caller is told has not moved: reaching the queues still rejects. What changed is that being connected now *means* being watched, whatever the connection made of its own work.
The counterpart is that the watching belongs to one identity and dies with it. Recording a different identity — or none, which is a disconnection and not merely "no new work" — stops it, because every step of it resolves the current holder as it runs: left going, it would read the previous identity's registers under the new one and file the previous one's `ReadCap`s into the new one's hands. Whoever connects next starts their own.
There is deliberately no timer anywhere in this: the regime is push-driven, and its known cost — a watch that failed to open is re-opened only by a later event, so a session that only reads never gets it back — is stated to applications under the contract's non-guarantees rather than papered over with polling.