/** * Lifecycle re-exports — SDK-shaped forwarders so the app imports `init` / * `initNg` from `@ng-eventually/polyfill` rather than from `@ng-org/*`. They * delegate to the REAL functions injected at `configure()`. * * ── `init` is not a bare passthrough, and that is the point ──────────────── * The real `init()` hands the page to the broker as its FIRST statement — a top-level * document is navigated to NextGraph's login, carrying `window.location.href` AS IT FINDS * IT. It knows nothing of `?ng-id=`, and it does not come back: everything the application * would have run after that line runs in a document that no longer exists. So an * application that signed in on the next line never signed in at all — the barrier never * showed, the identifier never reached the URL that crossed, and a first-time user landed * on a login with no wallet and no error anywhere. * * The obvious remedy — "call the gate first" — is an ordering rule written in prose, which * every consumer gets to get wrong once; and it deadlocks besides, because signing in used * to include waiting for the session `init()` is what opens. So the invariant is carried * HERE, by composition: this forwarder settles the identity, then delegates. A caller * cannot get the order wrong because a caller no longer takes part in it * (`shared-wallet/access-gate.ts`, {@link settleIdentity}). */ import { getConfig, rememberHandOver } from "../shared-wallet/bootstrap"; import { settleIdentity } from "../shared-wallet/access-gate"; import { captureSession, expectSession } from "../shared-wallet/session"; import { wrappedNg } from "./ng-proxy"; /** * Forwards to the real `@ng-org/web` `init`, once the identifier is in the address bar. * * Awaits {@link settleIdentity} — the session-free half of signing in — and NOT * `ensureIdentity()`, which also awaits the connection work, which awaits `getSession()`, * which resolves only from the session `init()` has not opened yet. That wait is the * deadlock, and avoiding it is what the split in `access-gate.ts` is for. * * A settling failure REJECTS rather than delegating. No shared wallet configured, or no DOM * to ask on, means there is no identifier to hand over — and handing the page to the broker * anyway IS the defect, a navigation the user cannot come back from. It fails at the call * the application made, where the cause is. * * The "not injected" error stays SYNCHRONOUS: it is a wiring mistake rather than a runtime * one, and it threw synchronously before this forwarder had anything to await. * * ── It also LISTENS on the way through, and that is the one argument it touches ── * The real `init` delivers the session by calling its callback with * `{ status: "loggedin", session }` — once, and it is the only channel that ever produces * one (`@ng-org/web` `dist/ngweb.js:113-137`, VERIFIED). Until 2026-08-12 every application * had to catch that event itself and hand the library a thunk unwrapping it, which is a * shape the target never asks anyone to build and which two consumers in a row got wrong. * * So the callback in position 0 is WRAPPED: the wrapper reads the event, keeps the session * (`shared-wallet/session.ts`), and then calls the caller's callback with that same event, * unchanged and un-narrowed. Nothing else about the call moves — the remaining arguments and * the return value pass straight through, and the caller's callback still sees exactly what * the real `init` sent it. It is the one place a wrapper can be, because it is the one place * that knows both what the caller asked and what the SDK will answer. * * A caller that passes NO callback is the same act with nobody listening — upstream accepts * it (`callback: Function | null`, and the call site is guarded). The wrapper still goes in, * so the package gets its session either way, and calls nothing afterwards. */ // @provenance init kind=passthrough level=2 ref=sdk/js/web/src/index.ts:init — arguments and return pass straight through; the callback in position 0 is wrapped to keep the session it carries export function init(...args: any[]): any { const f = getConfig().init; if (!f) throw new Error("[ng-eventually] init() not injected — pass it to configure()"); const [callback, ...rest] = args; const listen = (event: unknown): unknown => { captureSession(event); return typeof callback === "function" ? callback(event) : undefined; }; const delegate = (): unknown => f(listen, ...rest); // Two notes taken on the way in, both about what only this call can know. // // The hand-over, first: this delegation is the whole of it, and a person who reaches the // broker without a wallet comes back to THIS document with it already spent. The barrier // revives there and needs something to hand the page over with — this, exactly as it // would have run the first time (`shared-wallet/access-gate.ts`). rememberHandOver((): void => { // Un-awaited, and it has to be: the promise this call returned was answered on the way // out, so a rejection here has no caller to reach. Reported rather than dropped as an // unhandled rejection — the page is on its way to the broker either way. void Promise.resolve(delegate()).catch((failure: unknown) => { console.error("[ng-eventually] the hand-over to the broker failed", failure); }); }); // And that it RAN. The session arrives through this call and through no other, so // "`init()` has not been called" is the same statement as "no session can ever arrive" — // which is what lets `ensureIdentity()` refuse an impossible wait instead of hanging on it // (`shared-wallet/session.ts`, `shared-wallet/access-gate.ts`). expectSession(); return settleIdentity().then(delegate); } /** * Forwards to the real `@ng-org/orm` `initNg` (ORM signals) - with the `ng` in position 0 * replaced by this package's own wrapped one. * * -- Why the substitution, and why it is invisible to the caller ------------ * `initNg(ngImpl, session)` is how the ORM is told which `ng` to reach the engine * through: it resolves a module-level promise every subscription, every `insertObject` * and every batched patch awaits afterwards. So whatever is handed over HERE is the `ng` * every ORM write leaves by - and an ORM write leaves by `ng.graph_orm_update`, which is * where this package's write guard sits (`ng-proxy.ts`). Handing over the caller's raw * SDK object leaves that guard on a door nothing knocks on. * * It is the same argument `init`'s callback wrapping above rests on, and it lands the same * way: the invariant is carried by COMPOSITION rather than by an instruction a caller gets * to get wrong once. "Pass the package's `ng` to `initNg`" would be an ordering rule in * prose, unenforceable and silent when missed - and it WAS missed, by both consumers in * this repository, which is how the hole was found. * * Nothing is taken from the caller: `wrappedNg()` forwards every one of its members to * the very object `configure()` received, and it is the same object `ng` publishes, so the * ORM now reaches the engine through exactly what the application does. The remaining * arguments and the return value pass straight through. */ // @provenance initNg kind=passthrough level=3 ref=sdk/js/orm/src/core.ts:initNg - upstream's own exported name for `initNgSignals`; the arguments and the return pass through, and the `ng` in position 0 is replaced by this package's wrapped one so the ORM's writes meet the emulated write guard export function initNg(...args: any[]): any { const f = getConfig().initNg; if (!f) throw new Error("[ng-eventually] initNg() not injected — pass it to configure()"); const [, ...rest] = args; return f(wrappedNg(), ...rest); }