/** * The wallet session, held by the PACKAGE — never assembled by an application. * * ── Why this is not the application's business ──────────────────────────── * Upstream a session is RETURNED: `session_start` hands one back, and `init()`'s callback * delivers `{ status: "loggedin", session }` (`@ng-org/web` `dist/ngweb.js:124`, VERIFIED — * the callback is invoked once, with the `Session` the broker just opened). Nowhere does an * application build a session out of raw fields. * * It did here, and only here: every consumer had to wrap `init()` in a promise, catch that * event, and hand the library back a thunk unwrapping `session_id` / `private_store_id` / * `protected_store_id` / `public_store_id`. Identical plumbing in every application, with * nothing to migrate to — and it went wrong twice in the reference integration alone (a * deadlock, and a share that silently reached nobody). So the package captures the event * itself (`surface/lifecycle.ts`) and keeps the session here. * * ── What survives migration ─────────────────────────────────────────────── * NO COUNTERPART as a module: upstream nothing relays the session, because the SDK holds * the one the wallet opened. What survives is the application-side gesture this removes — * which is none at all. */ import type { RegistrySession } from "./account-registry"; /** * The session as it stands, and a promise for the first one to arrive. * * Both, because the two questions differ: a caller after the fact wants the CURRENT session * (a reconnection opens a new one, and reads must route through it), while a caller during * startup has to wait for the first. Answering the first question with a settled promise * would pin the very first session forever. */ 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 { return new Promise((resolve) => { announce = resolve; }); } /** * The session this package holds — the current one, or the first to arrive. * * It WAITS rather than refusing: before `init()` has been delegated to, no session can * exist and nothing else will make one. A thunk that threw there is what shipped the * silent-abandon defect (`emulated-verifier/connect.ts` swallows the throw, the run * abandons, and the caller that joins it resolves having restored nothing). */ export function sharedWalletSession(): Promise { 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. * * Answers whether it did, so a caller can tell "the session landed" from "some other * event went by" without reaching in. Every event that is not a `loggedin` carrying a * session is ignored — the callback is a general lifecycle channel, and inventing a * session out of a partial event would be worse than having none. * * ── The session id is RELAYED, never rebuilt — and that is load-bearing ──── * Upstream declares it `string | number` (`Session`, `index.d.ts:266`) and the broker * returns a NUMBER; the chain below here now types it the same way and hands it to `ng.*`, * whose binding takes it as-is. So this reads the field and passes it on untouched. It is * not a detail: normalizing it to a string was written here first, and the applicative e2e * refused every call in the batch with `Deserialization error of session_id JsValue("1")` * — the wasm side deserializes the id by its own type, and a stringified number is not it. * * The chain used to narrow it to `string`, which made the value the SDK hands an application * unpassable through this library. Widened to upstream's own declared type on 2026-08-14, at * the capture and along every hop that relays it; the id is still only ever RELAYED. */ export function captureSession(event: unknown): boolean { if (typeof event !== "object" || event === null) return false; const { status, session } = event as { status?: unknown; session?: unknown }; if (status !== "loggedin") return false; if (typeof session !== "object" || session === null) return false; const { session_id: sessionId, private_store_id: privateStoreId, protected_store_id: protectedStoreId, public_store_id: publicStoreId, } = session as { session_id?: string | number; private_store_id?: string; protected_store_id?: string; public_store_id?: string; }; // An event missing either anchor is not a session; the id is checked for PRESENCE only, // since its runtime type is the broker's to choose and ours to relay. if (sessionId === undefined || sessionId === null) return false; if (typeof privateStoreId !== "string") return false; current = { sessionId, privateStoreId, ...(typeof protectedStoreId === "string" ? { protectedStoreId } : {}), ...(typeof publicStoreId === "string" ? { publicStoreId } : {}), }; announce(current); return true; } /** * Forget the captured session (a fresh `configure()`, or a test). * * The pending promise is REPLACED rather than left resolved: a suite that reset and then * awaited again must wait for the next session, not be handed the previous one's. */ 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; }