62 lines
6.7 KiB
Markdown
62 lines
6.7 KiB
Markdown
---
|
|
type: knowledge
|
|
summary: Deciding which identity acts needs no session; connecting does — conflating them deadlocks one way and silently breaks sharing the other
|
|
---
|
|
|
|
# Settling is not connecting
|
|
|
|
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, 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.
|
|
|
|
They must stay apart, and the reason is not tidiness.
|
|
|
|
## Why they cannot be one call
|
|
|
|
`init()` starts the broker redirect as its first statement when the page is top-level, so anything the barrier needs to show must already be in place. The `barrier` must therefore appear **before** `init()`.
|
|
|
|
But an identity cannot connect before `init()` either: connecting needs the session, which arrives only through `init()`'s callback. Await the whole of sign-in before `init()` and it deadlocks; call it after and the `barrier` never appears.
|
|
|
|
The way out is that only *half* of it has that dependency. Settling is awaited by this package's `init()` wrapper before it delegates; connecting stays in the published call, awaited where a session exists.
|
|
|
|
**The invariant is carried by the composition from `init()` onwards** — which is why an application must call this package's `init`, not the one it injected: calling it settles the identity whether or not the published call has run.
|
|
|
|
It does **not** cover calling the published sign-in strictly first and awaiting it: the connection work it adds waits for a session only `init()` resolves, so it deadlocks in silence. Half an invariant carried by a mechanism, half still owed to the caller — and the half still owed fails silently, which is the worst kind.
|
|
|
|
## The failure this cost
|
|
|
|
Settling once reached the session, because recording who is acting also fired the connection. At the time the session was assembled by the application: it built a promise around `init()`'s callback and handed the package a thunk reading it. The application called `init()` *from inside the executor building that promise*, so the thunk could not answer by construction. It threw, the account lookup answered null, and the connection run abandoned **without restoring or draining** — having already registered itself as in flight. The published call then joined that dead run and resolved having done nothing.
|
|
|
|
Symptom: a document shared with someone did not open for them. No error, just unreadable content.
|
|
|
|
Nothing had changed in the connection logic. What changed was *when* the identity was recorded. Before the split, nothing recorded an identity during module evaluation: a session existed, the run was healthy, and joining it was harmless.
|
|
|
|
Hence the shape of the fix — recording who acts and starting to connect are separate operations, and the session-free half only records. The session is now the package's own — its `init()` wrapper captures the event — so no application can wire this wrong again. To validate: nothing reachable from settling asks for a session.
|
|
|
|
## The lesson worth keeping
|
|
|
|
A "session-free" half that calls something which fires a session lookup is not session-free. When splitting on a dependency, check what the *side effects* of each remaining call reach, not only what the call itself does.
|
|
|
|
## Failing to reach a queue is not failing to apply one item in it
|
|
|
|
Making connection failures surface was right — a restore that did not happen makes shared documents silently invisible, and resolving as though it had is the defect family this project keeps finding. But applied to *every* step alike, that rule locked people out.
|
|
|
|
A deposit that cannot be applied is not consumed by failing. So a single unusable item denied the session, and denied it again at every future connection: not a missing share, a person who can never sign in. Strictly worse than the silence it replaced.
|
|
|
|
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.
|