fix: régler l'identité n'a plus le droit de réclamer une session

Le partage était cassé : Bob n'ouvrait pas le document qu'Alice venait de lui
partager, sans erreur, juste « (illisible) ». Régression introduite en scindant
ensureIdentity().

Chaîne observée aux sondes, pas déduite : settleIdentity() appelait
setCurrentUser, qui déclenche startConnect(), qui va chercher la session via le
thunk getSession de l'application. Or l'exemple appelle init() DEPUIS
L'EXÉCUTEUR qui construit sessionReady — le thunk ne peut donc pas répondre, par
construction. Il lève, resolveAccount rend null, l'exécution est abandonnée sans
restauration ni drainage, mais s'est déjà enregistrée « en vol ». Le
ensureIdentity() suivant rejoint cette exécution morte et se résout sans avoir
rien fait.

Avant la scission, rien n'appelait setCurrentUser pendant l'évaluation du
module : la session existait, l'exécution était saine, et la rejoindre était
sans danger. C'était bien une affaire de moment.

bootstrap.ts scinde le setter : adoptCurrentUser enregistre qui agit,
setCurrentUser reste « enregistrer + connecter » pour tous les autres appelants.
La moitié sans session ne réclame donc plus de session, et se connecter
redevient l'affaire du seul ensureIdentity(), attendu, là où une session existe.

Ce que ça bloque, tracé avant de livrer : une application qui appellerait init()
sans jamais appeler ensureIdentity() n'aurait plus de restauration en arrière-
plan. Aucun appelant de ce genre n'existe, et avant la scission init() était un
passthrough nu qui ne déclenchait rien — c'est une répartition rétablie, pas un
comportement retiré.

Reste connu, non corrigé : connectedUser mémorise toujours une exécution
abandonnée. Le piège est documenté sur setCurrentUser.
This commit is contained in:
Sylvain Duchesne
2026-08-11 19:03:58 +02:00
parent 3547de202c
commit 3be8da2178
6 changed files with 220 additions and 40 deletions
@@ -209,24 +209,55 @@ export function resetStoreRegistry(): void {
}
/**
* Set the current identity id — who the SDK is reading/writing as. In the target
* this is the wallet user established at wallet-import time; here the consumer
* relays that id through this call so the read filter and the inbox `from` know
* who is acting. Passing `null` clears it (no identity yet, e.g. during startup).
* Record who is acting — and touch nothing else. Answers whether it CHANGED, which is
* what decides whether there is any connecting to do.
*
* Split out of {@link setCurrentUser} so that the two things it did — naming the identity
* and reaching for the session on its behalf — can be asked for separately. Only the
* session-FREE half of signing in uses this one ({@link ./access-gate}.settleIdentity),
* and that is the whole of why it exists: see the warning on {@link setCurrentUser}.
*
* @internal Never published. A consumer names its identity through the access gate.
*/
export function setCurrentUser(id: PrincipalId | null): void {
export function adoptCurrentUser(id: PrincipalId | null): boolean {
const changed = currentUser !== id;
currentUser = id;
return changed;
}
/**
* Set the current identity id — who the SDK is reading/writing as — **and connect it**.
* In the target this is the wallet user established at wallet-import time; here the
* consumer relays that id through this call so the read filter and the inbox `from` know
* who is acting. Passing `null` clears it (no identity yet, e.g. during startup).
*
* ── This call REACHES THE SESSION, and a caller must know it ──────────────
* "Fire-and-forget" says nothing about whether the session is touched — only about who
* waits. The fired work asks `resolveAccount` on its first line, which awaits the
* consumer's `getSession` thunk (`connect.ts` → `account-registry.ts:280`). So this
* setter is unusable at any moment the session cannot yet answer, and calling it there is
* not merely wasteful — it **poisons** the work: the thunk throws, `resolveAccount`
* returns null, the run abandons before restoring anything, and it is that abandoned run
* that the next `connectedUser()` JOINS instead of doing the work (`connect.ts:56`).
* Nothing anywhere throws; a user simply cannot read what was shared with it.
*
* That is not hypothetical — it shipped. The reference application calls the polyfill's
* `init()` from inside the executor that is still building its own `sessionReady`
* promise, so the thunk could not answer, by construction. Whoever settles an identity
* before a session can exist wants {@link adoptCurrentUser} and an awaited
* `connectedUser()` later.
*
* Gated on the registry being configured, and that is not a test convenience: an
* identity set before the registry is wired has nothing to restore and no inbox to
* reach. The consumer's real sequence is `configureStoreRegistry` then `setCurrentUser`;
* anything else can call `connectedUser()` explicitly.
*/
export function setCurrentUser(id: PrincipalId | null): void {
const changed = adoptCurrentUser(id);
// Connecting a user is what triggers inbox processing — the library's job, not
// the app's. Fire-and-forget: this setter is synchronous and every consumer calls
// it from synchronous code, so the work announces itself through the cap
// registry's change signal instead of making callers await. See `connect.ts`.
//
// Gated on the registry being configured, and that is not a test convenience: an
// identity set before the session resolves has nothing to restore and no inbox to
// reach, so firing would be I/O that can only fail. The consumer's real sequence
// is `configureStoreRegistry` then `setCurrentUser`; anything else can call
// `connectedUser()` explicitly.
if (changed && id !== null && registryDeps !== null) startConnect();
}