Deux affirmations à corriger, toutes deux écrites par moi et démenties par le bug d'aujourd'hui. La feuille disait que se connecter « restaure ce qui vous a été partagé ». C'est la moitié : le portefeuille garde vos propres clés ailleurs, et ne rejouer que le registre des partages refusait à une application son PROPRE document après un rechargement. La règle qui reste : rejouer TOUS les registres durables, pas celui qui se trouve sur le chemin qu'on teste. Et la règle « tout échec remonte » était trop grossière. Elle était juste pour la restauration ; appliquée à chaque étape, elle verrouillait. Un dépôt inapplicable n'est pas consommé par son échec, donc il refusait la session à chaque tentative — une personne qui ne peut plus entrer, là où il n'y avait qu'un partage manquant. La distinction consignée : ATTEINDRE les files et restaurer sont de l'infrastructure, et refuser la session y est juste ; APPLIQUER un élément est de la donnée, et ça se signale sans priver personne de sa session. Plus la forme générale, parce qu'elle se reproduira : devant un « tout échec remonte », demander lesquels se rattrapent en réessayant plus tard et lesquels sont définitifs pour l'acteur. Les définitifs ne doivent jamais garder une porte qu'il n'a pas d'autre moyen de franchir.
4.9 KiB
type, summary
| type | summary |
|---|---|
| knowledge | 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 thebarrier, 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.
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.