entry.ts

3.3 KB · raw

1// src/mobile/entry.ts — the mobile WebView's single entrypoint (god document §4.3).2//3// Order is load-bearing: the chrome shim must install `globalThis.chrome`4// before any other module evaluates, and the brain boots before the UI5// mounts so `bridge.sendMessage`'s first call has a handler waiting.6//7// Found on a real device (emulator run, 2026-07-01): a plain static8// `import "./chrome-shim"` followed by other static imports does NOT9// guarantee this ordering once bundled. ES module *evaluation* order follows10// the import graph, not source-line order within one file — and Rollup11// inlined chrome-shim.ts (small, single-use) directly into this chunk's own12// body, positioned AFTER this chunk's own `import` statements for13// background/entry's heavier dependencies (which get hoisted regardless of14// where the `import "../background/entry"` line sits in the source). Net15// effect: those dependencies evaluated and threw `chrome is not defined`16// before chrome-shim's inlined code ever ran.17//18// The fix: keep `./chrome-shim` as the ONLY static import in this file, and19// load everything else via dynamic `import()` — a dynamic import is a plain20// expression evaluated in this module's own top-level synchronous flow, not21// a hoisted declaration, so it genuinely cannot start until chrome-shim's22// static import has fully finished (chrome-shim has no dependencies of its23// own to delay it).24import "./chrome-shim";2526async function boot(): Promise<void> {27  // `background/entry.ts` self-invokes `start()` at its own module bottom28  // (verified against source — diverges from the god document's illustrative29  // sketch, which shows calling `start()` again explicitly; doing that would30  // run session-restore, setupAutoLock, and setupClipboardClearAlarm a second31  // time). Importing it for the side effect is sufficient.32  await import("../background/entry");3334  // `popup/index.tsx` self-mounts `<App/>` and, on native platforms, also35  // calls `setupMessageHandler()` again — harmless, since that registration36  // is idempotent (see chrome-shim.ts's dispatch comment for why running it37  // twice doesn't invoke `handleMessage` twice).38  await import("../popup/index");3940  const { ensureSettingsLoaded, isUnlocked, lock } = await import("../background/vault");41  const { isRecoveryConfirmPending } = await import("../background/recovery-pending");42  const { denyAllPendingPasskeyRequests } = await import("../background/passkey");43  const { nativePlugin } = await import("../shared/native");4445  // Lock-on-close, mobile-shaped (§4.4 point 1): entry.ts's real lock-on-close46  // listens for the popup's `chrome.runtime.onConnect` port disconnecting —47  // there is no such port here, so peach-android's NativeBridge pushes48  // foreground/background over the bridge instead. Same intent (mirrors49  // entry.ts's onDisconnect body exactly), native trigger.50  nativePlugin<{ onLifecycle(cb: (s: "foreground" | "background") => void): void }>("PeachNative")?.onLifecycle(51    (state) => {52      if (state !== "background") return;53      void (async () => {54        await denyAllPendingPasskeyRequests();55        const settings = await ensureSettingsLoaded();56        if (!isUnlocked() || settings.lockOnClose !== true) return;57        if (await isRecoveryConfirmPending()) return;58        lock();59      })();60    }61  );62}6364void boot();