refactor(api): le bootstrap redescend de quatre appels à un

L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :

- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
  injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
  qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
  `getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
  passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
  sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
  exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
  identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
  le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
  commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
  sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
  le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
  la session EST la connexion — aucune application n'attend un second appel.

Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.

Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.

180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
This commit is contained in:
Sylvain Duchesne
2026-08-07 12:06:15 +02:00
parent b98fcaa77d
commit 0455a408b6
26 changed files with 205 additions and 114 deletions
+20 -8
View File
@@ -78,16 +78,28 @@ export type { NG } from "@ng-org/web";
* Inject the real SDK, and tell the library about the shared wallet. Upstream nothing
* is injected — an application imports the SDK and opens its own wallet — so this call
* is the shape of that absence. `docs/api-contract.md` § 1.
*
* **It is the ONLY call here**, and keeping it that way is the design target: an
* application's bootstrap should be one line to delete, not four.
*/
export { configure, configureStoreRegistry, setCurrentUser } from "./shared-wallet/bootstrap";
export type { EventuallyConfig, StoreRegistryDeps } from "./shared-wallet/bootstrap";
export { configure } from "./shared-wallet/bootstrap";
export type { EventuallyConfig } from "./shared-wallet/bootstrap";
export type { RegistrySession } from "./shared-wallet/account-registry";
/**
* Await the connection work `setCurrentUser` fires: restore what was shared with this
* user, and drain its inboxes. An application need not call it — the work runs anyway —
* but it may want to know it has finished. Upstream this is the session opening.
*/
export { connectedUser } from "./emulated-verifier/connect";
// --- what this block deliberately does NOT contain --------------------------
//
// Three calls were published here and removed on 2026-08-07, when the count had drifted
// to four against a target of two. Each removal is a thing an application no longer does:
//
// - `configureStoreRegistry` — folded into `configure`. Two bootstrap calls existed
// because the library has two internals, which is not a reason a caller should pay.
// - `setCurrentUser` — the access gate sets the identity (`ensureIdentity`, below).
// An application naming its own identity is the gesture that inverts the model, and
// it must not have a published call to reach for. The e2e harness plays several
// identities on one page and reaches it by its internal path, which is what a
// harness is allowed to do and an application is not.
// - `connectedUser` — `ensureIdentity` awaits it. Upstream, opening the session IS the
// connection; no application awaits a second call, so ours should not either.
// ── the access gate — polyfill-era in substance, one line in the app ────────
// One call before the app renders. It shows a technical barrier only while the shared
+23 -2
View File
@@ -45,6 +45,7 @@ import {
getStoreRegistryDeps,
setCurrentUser,
} from "./bootstrap";
import { connectedUser } from "../emulated-verifier/connect";
/**
* Normalize an identifier the SAME way the shim keys accounts on.
@@ -221,12 +222,12 @@ function askForIdentity(cfg: SharedWalletConfig): Promise<string> {
* the URL, and a plain reload finds it in storage.
*/
export async function ensureIdentity(): Promise<void> {
if (getCurrentUser() !== null) return;
if (getCurrentUser() !== null) return connected();
const known = storedIdentity();
if (known) {
setCurrentUser(known);
return;
return connected();
}
const cfg = getConfig().sharedWallet;
@@ -249,4 +250,24 @@ export async function ensureIdentity(): Promise<void> {
const normalized = normalizeIdentity(chosen);
rememberIdentity(normalized);
setCurrentUser(normalized);
return connected();
}
/**
* Wait for the connection work `setCurrentUser` fires — restoring what others shared
* with this user, draining its inboxes — before this call resolves.
*
* **Not a convenience: a correctness fix, found by the applicative e2e.** Setting an
* identity FIRES that work and does not wait for it. An application that rendered on
* `ensureIdentity()` alone could read a note someone had just shared with it as
* unreadable — which looks like a permission problem and is a timing one, in the one
* place where the difference is invisible (nothing throws; a read is simply empty).
*
* Doing it here rather than exposing `connectedUser()` is the point: the awaited thing
* has NO counterpart upstream — there, opening the session IS the connection, and no
* application awaits a second call. So the polyfill absorbs it, and an application's
* bootstrap keeps the shape it will still have after migration.
*/
async function connected(): Promise<void> {
await connectedUser();
}
+40 -8
View File
@@ -51,17 +51,40 @@ export interface StoreRegistryDeps {
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
}
/**
* Everything the polyfill needs, in ONE call.
*
* It used to take two — `configure` for the SDK injection, `configureStoreRegistry` for
* the session — because the two belonged to different internals. That is a reason the
* library has, not one an application should pay for: from a caller's side both are
* "here is what you need to run", and two bootstrap calls is one more thing to delete
* at migration than there needs to be. Merged 2026-08-07; the registry's own wiring
* function stays internal.
*/
export interface EventuallyConfig {
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
ng: NgLike;
/** The REAL `@ng-org/orm` `useShape`. */
useShape: UseShapeLike;
/**
* Resolve the wallet session. Shared-wallet only: upstream the session IS the user, so
* there is nothing to inject — an application opens its wallet and the SDK knows.
* A thunk, so it may be given before the session exists.
*/
getSession?: () => Promise<RegistrySession>;
/** Normalize an identity id for shim keying. Default: trim. */
normalizeId?: (id: string) => string;
/**
* POINTER micro-guard budget — see {@link StoreRegistryDeps.pointerGuard}. Left unset
* → a single read, which keeps the synchronous unit fakes fast.
*/
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
/**
* The shared wallet this deployment hands out, and what the access gate needs to do
* it (`shared-wallet/access-gate.ts`). Absent → no gate; the caller sets the identity
* itself. Disappears with the gate: upstream a user opens their own wallet.
*/
sharedWallet?: SharedWalletConfig;
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
ng: NgLike;
/** The REAL `@ng-org/orm` `useShape`. */
useShape: UseShapeLike;
/** Initial current user; may also be set later via {@link setCurrentUser}. */
currentUser?: PrincipalId;
/**
@@ -114,6 +137,16 @@ export function configure(c: EventuallyConfig): void {
cfg = c;
currentUser = c.currentUser ?? null;
setAccessLog(c.debugAccessLog ?? false);
// The session wiring is part of the same act — see {@link EventuallyConfig}. Omitted
// only by unit suites that never touch the registry; those get the same
// "must be configured" error they got before, from `getStoreRegistryDeps`.
if (c.getSession) {
configureStoreRegistry({
getSession: c.getSession,
...(c.normalizeId ? { normalizeId: c.normalizeId } : {}),
...(c.pointerGuard ? { pointerGuard: c.pointerGuard } : {}),
});
}
}
/** @internal — used by the SDK-shaped wrappers to reach the injected real SDK. */
@@ -130,10 +163,9 @@ export function resetConfig(): void {
}
/**
* Wire the storeRegistry's consumer-injected dependencies (session + identity-id
* normalization). Must be called before any storeRegistry.* use. Separate from
* {@link configure} because it's storeRegistry-specific and, like the shim,
* disappears at migration.
* Wire the storeRegistry's dependencies. INTERNAL since 2026-08-07: an application
* passes these to {@link configure}, which calls this. Still exported for the library's
* own suites, which wire the registry alone.
*/
export function configureStoreRegistry(deps: StoreRegistryDeps): void {
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,