fix: trois coûts qui revenaient à l'appelant reviennent au paquet

Le contrat faisait porter à l'application trois choses qui sont des artefacts de
notre implémentation, pas de la cible.

Le rechargement de page. Au retour depuis le cache du navigateur, la barrière se
rechargeait pour rejouer init() — et détruisait au passage l'état de
l'application, qui ne pouvait ni s'y opposer ni nettoyer avant. Le paquet
détenait pourtant ce qu'il fallait : la fonction init injectée et le callback de
l'appelant. Il enregistre désormais sa délégation, ranime sa barrière au retour
— champ conservé, bouton réactivé — et redélègue à la confirmation. Rien hors de
la barrière n'est touché. Vérifié dans le bundle amont : en page de tête, init
navigue à chaque appel, sa garde « une seule fois » ne portant que sur la
branche iframe.

L'ordre d'appel silencieux. ensureIdentity() attendu avant init() ne se
résolvait jamais, sans erreur. Le paquet possédant la session, il distingue
maintenant les deux cas sans délai ni heuristique : session pas encore arrivée →
il attend ; init jamais appelé → elle n'arrivera pas, il lève en nommant l'appel
à faire d'abord.

Et la clause qui annonçait la barrière était rangée dans les exigences de
déploiement, alors qu'une application n'y peut rien. Elle passe dans les
garanties, avec ce qui la remplace : la page n'est jamais rechargée.

Il reste deux lignes d'exigences : servir le fichier de portefeuille, et appeler
init avant d'attendre l'identité — ce qui échoue désormais bruyamment.
This commit is contained in:
Sylvain Duchesne
2026-08-13 09:49:24 +02:00
parent f77317c4d1
commit 55714d0a23
10 changed files with 458 additions and 76 deletions
@@ -33,6 +33,17 @@ import type { RegistrySession } from "./account-registry";
let current: RegistrySession | null = null;
let announce!: (s: RegistrySession) => void;
let arrival = openArrival();
/**
* Has `init()` been called — is a session on its way at all?
*
* {@link sharedWalletSession} WAITS, and waiting is only right while a session can still
* arrive. Before `init()` has been delegated to, nothing in the system will ever open one:
* the wait is unbounded and answerless, which is the worst shape a failure can take — no
* error, no timeout, the application simply stops where it awaited. The polyfill's `init()`
* notes itself here as it goes through (`surface/lifecycle.ts`), and that one fact separates
* "not yet" from "never" with no timeout, no race and no guessing.
*/
let expected = false;
function openArrival(): Promise<RegistrySession> {
return new Promise<RegistrySession>((resolve) => {
@@ -52,6 +63,28 @@ export function sharedWalletSession(): Promise<RegistrySession> {
return current !== null ? Promise.resolve(current) : arrival;
}
/**
* Note that `init()` has been called — a session is on its way. See {@link expected}.
*
* @internal Called by the polyfill's `init()`, and by nothing else: it is a statement about
* that call having happened, not a switch anyone may set.
*/
export function expectSession(): void {
expected = true;
}
/**
* Can a session still arrive through this holder — because one is already here, or because
* `init()` has been called and will deliver one?
*
* A caller that awaits when this answers `false` waits forever. It is deliberately not a
* refusal by itself: the holder's job is to answer, and who should refuse is whoever is
* about to wait ({@link ./access-gate}.ensureIdentity).
*/
export function sessionIsComing(): boolean {
return current !== null || expected;
}
/**
* Read a lifecycle event, and keep the session if it carries one.
*
@@ -111,4 +144,7 @@ export function captureSession(event: unknown): boolean {
export function resetSharedWalletSession(): void {
current = null;
arrival = openArrival();
// Including the expectation: it was set by an `init()` on the config being discarded, and
// a suite that reset and then awaited must be told the truth about the NEXT one.
expected = false;
}