La doctrine disait que la suite applicative ne peut pas être mesurée sur une machine dont le réseau bouge. Elle vient de passer 27/27 quatre fois d'affilée sous exactement ce bruit. Ce qui tranche n'est donc pas l'état de la machine mais la FORME de l'échec — un délai nommé sur une opération broker désigne le transport, une assertion qui rend une valeur inattendue désigne le code — et la répétition. Et la feuille sur le règlement de l'identité décrivait encore la session comme un thunk fourni par l'application. Elle appartient désormais au paquet, dont le wrapper init() capture l'événement : plus aucune application ne peut la câbler de travers, ce qui était pourtant la cause exacte de la régression racontée juste au-dessus. Dette de doc soldée.
3.3 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 — restore what was shared with that identity and drain its inbox. Requires a live session.
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.