@mentra/engine 3.2.0-dev.120 → 3.2.0-dev.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,9 +70,11 @@ declare class MiniappLauncher {
70
70
  * package. Handles both dev (HTTP off the running dev server) and released
71
71
  * (file:// from the installed snapshot). Reads disk/network/storage; does
72
72
  * NOT spawn. Returns null when the bundle can't be resolved (dev server
73
- * unreachable, missing entry, no installed version).
73
+ * unreachable with no on-disk snapshot, missing entry, no installed version).
74
74
  */
75
75
  resolveBundle(packageName: string, hints?: LaunchHints): Promise<ResolvedBundle | null>;
76
+ private resolveLiveHttp;
77
+ private resolveInstalledBundle;
76
78
  /**
77
79
  * Ensure a miniapp's background JS context is spawned. Idempotent — if the
78
80
  * context is already alive this is a no-op spawn-wise (it still resolves the
@@ -1 +1 @@
1
- {"version":3,"file":"MiniappLauncher.d.ts","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAQH,OAA4B,EAAC,KAAK,wBAAwB,EAAC,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAEpD,UAAU,YAAY;IACpB,oEAAoE;IACpE,MAAM,EAAE,cAAc,CAAA;CACvB;AAED,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,cAAM,eAAe;IACnB,OAAO,CAAC,IAAI,CAA4B;IAExC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IAEpE,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAInC,OAAO,CAAC,aAAa;IAOrB,yEAAyE;IACzE,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;;;OAMG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IA8G7F;;;;;;;;;;OAUG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAwBpF,iFAAiF;YACnE,KAAK;IAiCnB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB7C;;;;OAIG;IACG,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,gBAAgB,SAAS,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzG,mDAAmD;IAC7C,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9C,OAAO,CAAC,YAAY;IAKpB,4EAA4E;IAC5E,OAAO,CAAC,cAAc;CASvB;AAED,eAAO,MAAM,eAAe,iBAAwB,CAAA"}
1
+ {"version":3,"file":"MiniappLauncher.d.ts","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAQH,OAA4B,EAAC,KAAK,wBAAwB,EAAC,MAAM,uBAAuB,CAAA;AACxF,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAA;AAEpD,UAAU,YAAY;IACpB,oEAAoE;IACpE,MAAM,EAAE,cAAc,CAAA;CACvB;AAED,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,mBAAmB,EAAE,MAAM,EAAE,CAAA;IAC7B,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,cAAM,eAAe;IACnB,OAAO,CAAC,IAAI,CAA4B;IAExC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2C;IAEpE,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAInC,OAAO,CAAC,aAAa;IAOrB,yEAAyE;IACzE,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAIvC;;;;;;OAMG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;YA0B/E,eAAe;YAsDf,sBAAsB;IAqDpC;;;;;;;;;;OAUG;IACG,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAwBpF,iFAAiF;YACnE,KAAK;IAiCnB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB7C;;;;OAIG;IACG,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,gBAAgB,SAAS,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzG,mDAAmD;IAC7C,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB9C,OAAO,CAAC,YAAY;IAKpB,4EAA4E;IAC5E,OAAO,CAAC,cAAc;CASvB;AAED,eAAO,MAAM,eAAe,iBAAwB,CAAA"}
@@ -23,7 +23,7 @@
23
23
  * resolved UI entry so the host can mount its `<WebView>`.
24
24
  */
25
25
  import { File } from "expo-file-system";
26
- import { decideDevLaunchRoute } from "../utils/devMiniappLaunch";
26
+ import { resolveDevBundleSource } from "../utils/devMiniappSnapshot";
27
27
  import { storage } from "../utils/storage/storage";
28
28
  import appRegistry, { getLocalAppRunningState, saveLocalAppRunningState } from "./AppRegistry";
29
29
  import devServerBridge from "./DevServerBridge";
@@ -57,66 +57,81 @@ class MiniappLauncher {
57
57
  * package. Handles both dev (HTTP off the running dev server) and released
58
58
  * (file:// from the installed snapshot). Reads disk/network/storage; does
59
59
  * NOT spawn. Returns null when the bundle can't be resolved (dev server
60
- * unreachable, missing entry, no installed version).
60
+ * unreachable with no on-disk snapshot, missing entry, no installed version).
61
61
  */
62
62
  async resolveBundle(packageName, hints) {
63
63
  const devUrl = hints?.devUrl ?? this.storedDevUrl(packageName);
64
- // --- Dev: load directly off the local dev server over HTTP. ---
64
+ // --- Dev: live HTTP, then the last on-disk snapshot if the laptop is gone. ---
65
65
  if (devUrl) {
66
- const route = await decideDevLaunchRoute(packageName, devUrl);
67
- if (route.decision === "offline" || !route.manifest)
68
- return null;
69
- const manifest = route.manifest;
70
- const entry = manifest.entry;
71
- if (!entry?.background)
72
- return null;
73
- // Use the host that actually answered — may differ from the stored IP
74
- // after a laptop Wi-Fi change (mDNS / Metro failover inside decideDevLaunchRoute).
75
- const base = route.resolvedUrl.replace(/\/$/, "");
76
- // entry.* are bundle-root paths (dist/ stripped); the dev server serves
77
- // files relative to cwd, so prepend dist/.
78
- const bgUrl = `${base}/dist/${entry.background.replace(/^\.?\/+/, "")}`;
79
- const uiUri = entry.ui ? `${base}/dist/${entry.ui.replace(/^\.?\/+/, "")}` : null;
80
- const perms = manifest.permissions;
81
- const declaredPermissions = (perms ?? [])
82
- .map((p) => (typeof p === "string" ? p : p?.type))
83
- .filter((t) => typeof t === "string");
84
- const installedManifest = {
85
- packageName: typeof manifest.packageName === "string" ? manifest.packageName : packageName,
86
- name: manifest.name,
87
- version: typeof manifest.version === "string" ? manifest.version : undefined,
88
- sdkVersion: typeof manifest.sdkVersion === "string" ? manifest.sdkVersion : undefined,
89
- minHostVersion: typeof manifest.minHostVersion === "string" ? manifest.minHostVersion : undefined,
90
- type: typeof manifest.type === "string" ? manifest.type : undefined,
91
- entry: manifest.entry,
92
- permissions: manifest.permissions,
93
- hardwareRequirements: manifest.hardwareRequirements,
94
- actions: manifest.actions,
95
- };
96
- let bgSource;
97
- try {
98
- const res = await fetch(bgUrl);
99
- if (!res.ok)
100
- return null;
101
- bgSource = await res.text();
66
+ const source = await resolveDevBundleSource(packageName, devUrl);
67
+ if (source.kind === "live") {
68
+ const live = await this.resolveLiveHttp(packageName, source.resolvedUrl, source.manifest, hints);
69
+ if (live)
70
+ return live;
71
+ // Live probe succeeded but the entry fetch failed — still try disk.
102
72
  }
103
- catch {
104
- return null;
73
+ const snapshotVersion = source.kind === "snapshot" ? source.version : appRegistry.getLatestDevSnapshotVersion(packageName);
74
+ if (snapshotVersion) {
75
+ const snapshot = await this.resolveInstalledBundle(packageName, snapshotVersion);
76
+ if (snapshot)
77
+ return snapshot;
105
78
  }
106
- return {
107
- bgSource,
108
- uiUri,
109
- uiBaseDir: uiUri ? uiUri.replace(/\/[^/]+$/, "/") : null,
110
- declaredPermissions,
111
- installedManifest,
112
- devUrl: route.resolvedUrl,
113
- devPort: this.resolveDevPort(hints?.devPort, packageName),
114
- };
79
+ return null;
115
80
  }
116
81
  // --- Released: resolve from the installed file:// snapshot. ---
117
82
  const version = hints?.version ?? (await appRegistry.getActiveVersion(packageName));
118
83
  if (!version)
119
84
  return null;
85
+ return this.resolveInstalledBundle(packageName, version);
86
+ }
87
+ async resolveLiveHttp(packageName, resolvedUrl, manifest, hints) {
88
+ const entry = manifest.entry;
89
+ if (!entry?.background)
90
+ return null;
91
+ // Use the host that actually answered — may differ from the stored IP
92
+ // after a laptop Wi-Fi change (mDNS / Metro failover inside decideDevLaunchRoute).
93
+ const base = resolvedUrl.replace(/\/$/, "");
94
+ // entry.* are bundle-root paths (dist/ stripped); the dev server serves
95
+ // files relative to cwd, so prepend dist/.
96
+ const bgUrl = `${base}/dist/${entry.background.replace(/^\.?\/+/, "")}`;
97
+ const uiUri = entry.ui ? `${base}/dist/${entry.ui.replace(/^\.?\/+/, "")}` : null;
98
+ const perms = manifest.permissions;
99
+ const declaredPermissions = (perms ?? [])
100
+ .map((p) => (typeof p === "string" ? p : p?.type))
101
+ .filter((t) => typeof t === "string");
102
+ const installedManifest = {
103
+ packageName: typeof manifest.packageName === "string" ? manifest.packageName : packageName,
104
+ name: typeof manifest.name === "string" ? manifest.name : undefined,
105
+ version: typeof manifest.version === "string" ? manifest.version : undefined,
106
+ sdkVersion: typeof manifest.sdkVersion === "string" ? manifest.sdkVersion : undefined,
107
+ minHostVersion: typeof manifest.minHostVersion === "string" ? manifest.minHostVersion : undefined,
108
+ type: typeof manifest.type === "string" ? manifest.type : undefined,
109
+ entry: manifest.entry,
110
+ permissions: manifest.permissions,
111
+ hardwareRequirements: manifest.hardwareRequirements,
112
+ actions: manifest.actions,
113
+ };
114
+ let bgSource;
115
+ try {
116
+ const res = await fetch(bgUrl);
117
+ if (!res.ok)
118
+ return null;
119
+ bgSource = await res.text();
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ return {
125
+ bgSource,
126
+ uiUri,
127
+ uiBaseDir: uiUri ? uiUri.replace(/\/[^/]+$/, "/") : null,
128
+ declaredPermissions,
129
+ installedManifest,
130
+ devUrl: resolvedUrl,
131
+ devPort: this.resolveDevPort(hints?.devPort, packageName),
132
+ };
133
+ }
134
+ async resolveInstalledBundle(packageName, version) {
120
135
  const entryPaths = appRegistry.getMiniappEntryPaths(packageName, version);
121
136
  if (!entryPaths?.background)
122
137
  return null;
@@ -151,6 +166,7 @@ class MiniappLauncher {
151
166
  uiBaseDir: entryPaths.ui ? entryPaths.ui.replace(/\/[^/]+$/, "/") : null,
152
167
  declaredPermissions,
153
168
  installedManifest,
169
+ // Snapshot fallback is file:// — do not wire the sidecar; the laptop is gone.
154
170
  devUrl: null,
155
171
  devPort: null,
156
172
  };
@@ -1 +1 @@
1
- {"version":3,"file":"MiniappLauncher.js","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MiniappLauncher — headless launch/teardown of a local miniapp's background\n * JS context.\n *\n * This is the one piece of \"run a miniapp\" that used to live inside the\n * `LocalMiniappView` React component (resolve the bundle → read the manifest →\n * `spawnAndRegister`), triggered only when the Compositor mounted that\n * component on foreground. Pulling it into the runtime means:\n *\n * - `start()` can actually spawn an app even when nothing foregrounds it\n * (e.g. a system app launching another miniapp via `session.miniapps`).\n * - the action broker can wake a stopped target headlessly — no UI at all.\n * - the dev hot-reload respawn path and the WebView mount path share one\n * resolve+spawn recipe instead of three copies.\n *\n * The launcher is **headless**: no React, no RN components. The engine-owned\n * MiniappEngine hands it the {@link MentraJSRouter} instance once during\n * engine construction. OEMs embedding the runtime drive miniapp lifecycle\n * through the engine facade and stores, not a separate launcher configure API.\n *\n * Boundary: the launcher owns the **background** context. WebView *rendering*\n * (the UI layer) stays with the host — the launcher just hands back the\n * resolved UI entry so the host can mount its `<WebView>`.\n */\n\nimport {File} from \"expo-file-system\"\n\nimport {decideDevLaunchRoute} from \"../utils/devMiniappLaunch\"\nimport {storage} from \"../utils/storage/storage\"\nimport appRegistry, {getLocalAppRunningState, saveLocalAppRunningState} from \"./AppRegistry\"\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime, {type InstalledMiniappManifest} from \"./LocalMiniappRuntime\"\nimport type {MentraJSRouter} from \"./MentraJSRouter\"\n\ninterface LauncherDeps {\n /** The host-constructed router (needs the native Crust binding). */\n router: MentraJSRouter\n}\n\n/** Hints the host may pass from a view's props to avoid re-deriving them. */\nexport interface LaunchHints {\n devUrl?: string\n version?: string\n devPort?: string\n}\n\n/** Everything the launcher resolved for a package — bg source + UI entry. */\nexport interface ResolvedBundle {\n bgSource: string\n uiUri: string | null\n uiBaseDir: string | null\n declaredPermissions: string[]\n installedManifest?: InstalledMiniappManifest\n /** Set for dev miniapps (HTTP off the dev server); null for released. */\n devUrl: string | null\n devPort: number | null\n}\n\n/** What the host needs back to render the UI layer. */\nexport interface LaunchResult {\n uiUri: string | null\n uiBaseDir: string | null\n}\n\nclass MiniappLauncher {\n private deps: LauncherDeps | null = null\n\n /**\n * In-flight launches keyed by packageName. Both apps.ts start() and the\n * WebView mount call ensureRunning() before the first spawnAndRegister\n * resolves — without this, neither sees the package in registeredPackages()\n * yet and both spawn the same context (a clobber/double-spawn). Concurrent\n * callers share the same promise.\n */\n private readonly inFlight = new Map<string, Promise<LaunchResult>>()\n\n /** Wire the router in. Called once from MiniappEngine construction. */\n configure(deps: LauncherDeps): void {\n this.deps = deps\n }\n\n private requireRouter(): MentraJSRouter {\n if (!this.deps) {\n throw new Error(\"MiniappLauncher not configured — ensureMiniappEngine() has not run\")\n }\n return this.deps.router\n }\n\n /** True iff the package's background JS context is currently spawned. */\n isRunning(packageName: string): boolean {\n return this.deps?.router.registeredPackages().includes(packageName) ?? false\n }\n\n /**\n * Resolve the background JS source + UI entry + declared permissions for a\n * package. Handles both dev (HTTP off the running dev server) and released\n * (file:// from the installed snapshot). Reads disk/network/storage; does\n * NOT spawn. Returns null when the bundle can't be resolved (dev server\n * unreachable, missing entry, no installed version).\n */\n async resolveBundle(packageName: string, hints?: LaunchHints): Promise<ResolvedBundle | null> {\n const devUrl = hints?.devUrl ?? this.storedDevUrl(packageName)\n\n // --- Dev: load directly off the local dev server over HTTP. ---\n if (devUrl) {\n const route = await decideDevLaunchRoute(packageName, devUrl)\n if (route.decision === \"offline\" || !route.manifest) return null\n const manifest = route.manifest\n const entry = manifest.entry as {background?: string; ui?: string} | undefined\n if (!entry?.background) return null\n\n // Use the host that actually answered — may differ from the stored IP\n // after a laptop Wi-Fi change (mDNS / Metro failover inside decideDevLaunchRoute).\n const base = route.resolvedUrl.replace(/\\/$/, \"\")\n // entry.* are bundle-root paths (dist/ stripped); the dev server serves\n // files relative to cwd, so prepend dist/.\n const bgUrl = `${base}/dist/${entry.background.replace(/^\\.?\\/+/, \"\")}`\n const uiUri = entry.ui ? `${base}/dist/${entry.ui.replace(/^\\.?\\/+/, \"\")}` : null\n\n const perms = manifest.permissions as Array<{type?: string} | string> | undefined\n const declaredPermissions = (perms ?? [])\n .map((p) => (typeof p === \"string\" ? p : p?.type))\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest = {\n packageName: typeof manifest.packageName === \"string\" ? manifest.packageName : packageName,\n name: manifest.name,\n version: typeof manifest.version === \"string\" ? manifest.version : undefined,\n sdkVersion: typeof manifest.sdkVersion === \"string\" ? manifest.sdkVersion : undefined,\n minHostVersion: typeof manifest.minHostVersion === \"string\" ? manifest.minHostVersion : undefined,\n type: typeof manifest.type === \"string\" ? manifest.type : undefined,\n entry: manifest.entry as InstalledMiniappManifest[\"entry\"],\n permissions: manifest.permissions as InstalledMiniappManifest[\"permissions\"],\n hardwareRequirements: manifest.hardwareRequirements as InstalledMiniappManifest[\"hardwareRequirements\"],\n actions: manifest.actions as InstalledMiniappManifest[\"actions\"],\n }\n\n let bgSource: string\n try {\n const res = await fetch(bgUrl)\n if (!res.ok) return null\n bgSource = await res.text()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri,\n uiBaseDir: uiUri ? uiUri.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: route.resolvedUrl,\n devPort: this.resolveDevPort(hints?.devPort, packageName),\n }\n }\n\n // --- Released: resolve from the installed file:// snapshot. ---\n const version = hints?.version ?? (await appRegistry.getActiveVersion(packageName))\n if (!version) return null\n const entryPaths = appRegistry.getMiniappEntryPaths(packageName, version)\n if (!entryPaths?.background) return null\n\n const manifest = appRegistry.getMiniappManifest(packageName, version) as {\n packageName?: string\n name?: string\n version?: string\n sdkVersion?: string\n minHostVersion?: string\n type?: string\n entry?: {background?: string; ui?: string}\n permissions?: Array<{type: string; required?: boolean; description?: string}>\n hardwareRequirements?: Array<{type: string; level: string; description?: string}>\n actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>\n } | null\n const declaredPermissions = (manifest?.permissions ?? [])\n .map((p) => p.type)\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest | undefined = manifest\n ? {\n packageName: manifest.packageName ?? packageName,\n name: manifest.name,\n version: manifest.version ?? version,\n sdkVersion: manifest.sdkVersion,\n minHostVersion: manifest.minHostVersion,\n type: manifest.type,\n entry: manifest.entry,\n permissions: manifest.permissions,\n hardwareRequirements: manifest.hardwareRequirements,\n actions: manifest.actions,\n }\n : undefined\n\n let bgSource: string\n try {\n bgSource = new File(entryPaths.background).textSync()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri: entryPaths.ui ?? null,\n uiBaseDir: entryPaths.ui ? entryPaths.ui.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: null,\n devPort: null,\n }\n }\n\n /**\n * Ensure a miniapp's background JS context is spawned. Idempotent — if the\n * context is already alive this is a no-op spawn-wise (it still resolves the\n * UI entry so a re-mounting host gets its `<WebView>` source back).\n *\n * Resolves once the context is SPAWNED. It does NOT block on the context's\n * CONNECT handshake — the host's WebView mount shouldn't wait on background\n * JS. Callers that need the context to be connected and ready to receive\n * messages (the action broker) follow up with\n * {@link LocalMiniappRuntime.waitForConnect}.\n */\n async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n\n // Already spawned: best-effort resolve for the UI entry, never throw —\n // a headless caller that doesn't need UI gets {null, null} fast even if\n // the dev server has since dropped.\n if (router.registeredPackages().includes(packageName)) {\n const existing = await this.resolveBundle(packageName, hints).catch(() => null)\n return {uiUri: existing?.uiUri ?? null, uiBaseDir: existing?.uiBaseDir ?? null}\n }\n\n // Coalesce concurrent launches of the same package onto one promise.\n const pending = this.inFlight.get(packageName)\n if (pending) return pending\n\n const launch = this.spawn(packageName, hints)\n this.inFlight.set(packageName, launch)\n try {\n return await launch\n } finally {\n this.inFlight.delete(packageName)\n }\n }\n\n /** Resolve the bundle and spawn the context. Serialized via {@link inFlight}. */\n private async spawn(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n const resolved = await this.resolveBundle(packageName, hints)\n if (!resolved) {\n throw new Error(`MiniappLauncher: cannot resolve bundle for ${packageName}`)\n }\n\n // Re-check after the async resolve: a different path may have spawned it\n // while we were fetching/reading the bundle.\n if (!router.registeredPackages().includes(packageName)) {\n const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {\n permissions: resolved.declaredPermissions,\n installedManifest: resolved.installedManifest,\n })\n if (!ok) {\n throw new Error(`MiniappLauncher: spawn failed for ${packageName}`)\n }\n\n // Dev: wire the hot-reload + log-forwarding sidecar. No-op in prod\n // (devServerBridge silently drops when no sidecar is up).\n if (resolved.devUrl && resolved.devPort != null) {\n devServerBridge.connect(packageName, resolved.devUrl, resolved.devPort)\n } else if (resolved.devUrl) {\n // Dev miniapp with no resolvable port — it still launches, but hot\n // reload + log forwarding won't connect. Surface it instead of failing\n // silently (the old inline path hard-failed with \"no dev port\").\n console.warn(`MiniappLauncher: ${packageName} has no dev port — hot reload + log forwarding disabled`)\n }\n }\n\n return {uiUri: resolved.uiUri, uiBaseDir: resolved.uiBaseDir}\n }\n\n /**\n * Re-spawn local miniapps that were running when the app was last killed.\n *\n * Cloud apps run on a remote server: the cloud persists which ones are\n * running and resurrects them when the phone reconnects, so a host-app\n * restart brings them back on its own. Local (phone-hosted) miniapps run in\n * the phone's own JS engine — a host-process kill tears down their JSContext\n * and there is no server to resurrect them, so without this they silently\n * stay stopped on the next launch even though the user left them running.\n *\n * Each local miniapp persists its running flag to disk on start/stop\n * (`saveLocalAppRunningState`, via the applet's `onStart`/`onStop`). We read\n * those flags back and re-spawn the background context headlessly (no\n * foreground, no navigation) — the spawn registers the package so the home\n * tray/switcher project it as running, exactly as a normal start would. The\n * WebView, if any, re-attaches lazily when the user opens the app.\n *\n * Idempotent and best-effort: skips already-spawned contexts, and clears the\n * persisted flag for any app whose bundle can no longer be resolved\n * (uninstalled, or dev server gone) so a dead entry doesn't retry every boot.\n * Not compatibility-gated: a previously-running background app shouldn't be\n * dropped just because glasses are momentarily disconnected at boot — it\n * resumes when they reconnect, same as mid-session.\n */\n async autostartLocalMiniapps(): Promise<void> {\n const apps = await appRegistry.getInstalledMiniapps()\n for (const app of apps) {\n // Only phone-hosted miniapps have a JSContext to re-spawn. Offline\n // built-ins (`local:false, offline:true`) restore their native running\n // state elsewhere and have no bundle to launch.\n if (!app.local) continue\n if (!getLocalAppRunningState(app.packageName)) continue\n if (this.isRunning(app.packageName)) continue\n try {\n await this.ensureRunning(app.packageName)\n } catch (e) {\n console.warn(`MiniappLauncher: autostart failed for ${app.packageName} — clearing stale running flag`, e)\n saveLocalAppRunningState(app.packageName, false)\n }\n }\n }\n\n /**\n * Ensure the context is running AND has completed its CONNECT handshake.\n * Used by the action broker, which must not deliver to a context that hasn't\n * come up yet. Throws on spawn failure or connect timeout.\n */\n async ensureConnected(packageName: string, connectTimeoutMs = 10_000, hints?: LaunchHints): Promise<void> {\n await this.ensureRunning(packageName, hints)\n await localMiniappRuntime.waitForConnect(packageName, connectTimeoutMs)\n }\n\n /** Tear down a miniapp's background JS context. */\n async stop(packageName: string): Promise<void> {\n // If a launch is in flight, let it finish first. Otherwise unregister()\n // no-ops on the not-yet-registered package and the pending spawn then\n // re-registers the context — leaving it running despite the stop.\n const pending = this.inFlight.get(packageName)\n if (pending) {\n try {\n await pending\n } catch {\n // Launch failed — nothing got registered, so nothing to tear down.\n }\n }\n await this.deps?.router.unregister(packageName)\n }\n\n // --- internals -----------------------------------------------------------\n\n private storedDevUrl(packageName: string): string | undefined {\n const r = storage.load<string>(`${packageName}_dev_url`)\n return r.is_ok() ? r.value : undefined\n }\n\n /** resolveDevPort — moved verbatim from the old LocalMiniappView helper. */\n private resolveDevPort(searchParam: string | undefined, packageName: string): number | null {\n if (searchParam) {\n const n = parseInt(searchParam, 10)\n if (Number.isFinite(n)) return n\n }\n const stored = storage.load<number>(`${packageName}_dev_port`)\n if (stored.is_ok()) return stored.value\n return null\n }\n}\n\nexport const miniappLauncher = new MiniappLauncher()\n"]}
1
+ {"version":3,"file":"MiniappLauncher.js","sourceRoot":"","sources":["../../src/services/MiniappLauncher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MiniappLauncher — headless launch/teardown of a local miniapp's background\n * JS context.\n *\n * This is the one piece of \"run a miniapp\" that used to live inside the\n * `LocalMiniappView` React component (resolve the bundle → read the manifest →\n * `spawnAndRegister`), triggered only when the Compositor mounted that\n * component on foreground. Pulling it into the runtime means:\n *\n * - `start()` can actually spawn an app even when nothing foregrounds it\n * (e.g. a system app launching another miniapp via `session.miniapps`).\n * - the action broker can wake a stopped target headlessly — no UI at all.\n * - the dev hot-reload respawn path and the WebView mount path share one\n * resolve+spawn recipe instead of three copies.\n *\n * The launcher is **headless**: no React, no RN components. The engine-owned\n * MiniappEngine hands it the {@link MentraJSRouter} instance once during\n * engine construction. OEMs embedding the runtime drive miniapp lifecycle\n * through the engine facade and stores, not a separate launcher configure API.\n *\n * Boundary: the launcher owns the **background** context. WebView *rendering*\n * (the UI layer) stays with the host — the launcher just hands back the\n * resolved UI entry so the host can mount its `<WebView>`.\n */\n\nimport {File} from \"expo-file-system\"\n\nimport {resolveDevBundleSource} from \"../utils/devMiniappSnapshot\"\nimport {storage} from \"../utils/storage/storage\"\nimport appRegistry, {getLocalAppRunningState, saveLocalAppRunningState} from \"./AppRegistry\"\nimport devServerBridge from \"./DevServerBridge\"\nimport localMiniappRuntime, {type InstalledMiniappManifest} from \"./LocalMiniappRuntime\"\nimport type {MentraJSRouter} from \"./MentraJSRouter\"\n\ninterface LauncherDeps {\n /** The host-constructed router (needs the native Crust binding). */\n router: MentraJSRouter\n}\n\n/** Hints the host may pass from a view's props to avoid re-deriving them. */\nexport interface LaunchHints {\n devUrl?: string\n version?: string\n devPort?: string\n}\n\n/** Everything the launcher resolved for a package — bg source + UI entry. */\nexport interface ResolvedBundle {\n bgSource: string\n uiUri: string | null\n uiBaseDir: string | null\n declaredPermissions: string[]\n installedManifest?: InstalledMiniappManifest\n /** Set for dev miniapps (HTTP off the dev server); null for released. */\n devUrl: string | null\n devPort: number | null\n}\n\n/** What the host needs back to render the UI layer. */\nexport interface LaunchResult {\n uiUri: string | null\n uiBaseDir: string | null\n}\n\nclass MiniappLauncher {\n private deps: LauncherDeps | null = null\n\n /**\n * In-flight launches keyed by packageName. Both apps.ts start() and the\n * WebView mount call ensureRunning() before the first spawnAndRegister\n * resolves — without this, neither sees the package in registeredPackages()\n * yet and both spawn the same context (a clobber/double-spawn). Concurrent\n * callers share the same promise.\n */\n private readonly inFlight = new Map<string, Promise<LaunchResult>>()\n\n /** Wire the router in. Called once from MiniappEngine construction. */\n configure(deps: LauncherDeps): void {\n this.deps = deps\n }\n\n private requireRouter(): MentraJSRouter {\n if (!this.deps) {\n throw new Error(\"MiniappLauncher not configured — ensureMiniappEngine() has not run\")\n }\n return this.deps.router\n }\n\n /** True iff the package's background JS context is currently spawned. */\n isRunning(packageName: string): boolean {\n return this.deps?.router.registeredPackages().includes(packageName) ?? false\n }\n\n /**\n * Resolve the background JS source + UI entry + declared permissions for a\n * package. Handles both dev (HTTP off the running dev server) and released\n * (file:// from the installed snapshot). Reads disk/network/storage; does\n * NOT spawn. Returns null when the bundle can't be resolved (dev server\n * unreachable with no on-disk snapshot, missing entry, no installed version).\n */\n async resolveBundle(packageName: string, hints?: LaunchHints): Promise<ResolvedBundle | null> {\n const devUrl = hints?.devUrl ?? this.storedDevUrl(packageName)\n\n // --- Dev: live HTTP, then the last on-disk snapshot if the laptop is gone. ---\n if (devUrl) {\n const source = await resolveDevBundleSource(packageName, devUrl)\n if (source.kind === \"live\") {\n const live = await this.resolveLiveHttp(packageName, source.resolvedUrl, source.manifest, hints)\n if (live) return live\n // Live probe succeeded but the entry fetch failed — still try disk.\n }\n const snapshotVersion =\n source.kind === \"snapshot\" ? source.version : appRegistry.getLatestDevSnapshotVersion(packageName)\n if (snapshotVersion) {\n const snapshot = await this.resolveInstalledBundle(packageName, snapshotVersion)\n if (snapshot) return snapshot\n }\n return null\n }\n\n // --- Released: resolve from the installed file:// snapshot. ---\n const version = hints?.version ?? (await appRegistry.getActiveVersion(packageName))\n if (!version) return null\n return this.resolveInstalledBundle(packageName, version)\n }\n\n private async resolveLiveHttp(\n packageName: string,\n resolvedUrl: string,\n manifest: {entry?: unknown; permissions?: unknown; [key: string]: unknown},\n hints?: LaunchHints,\n ): Promise<ResolvedBundle | null> {\n const entry = manifest.entry as {background?: string; ui?: string} | undefined\n if (!entry?.background) return null\n\n // Use the host that actually answered — may differ from the stored IP\n // after a laptop Wi-Fi change (mDNS / Metro failover inside decideDevLaunchRoute).\n const base = resolvedUrl.replace(/\\/$/, \"\")\n // entry.* are bundle-root paths (dist/ stripped); the dev server serves\n // files relative to cwd, so prepend dist/.\n const bgUrl = `${base}/dist/${entry.background.replace(/^\\.?\\/+/, \"\")}`\n const uiUri = entry.ui ? `${base}/dist/${entry.ui.replace(/^\\.?\\/+/, \"\")}` : null\n\n const perms = manifest.permissions as Array<{type?: string} | string> | undefined\n const declaredPermissions = (perms ?? [])\n .map((p) => (typeof p === \"string\" ? p : p?.type))\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest = {\n packageName: typeof manifest.packageName === \"string\" ? manifest.packageName : packageName,\n name: typeof manifest.name === \"string\" ? manifest.name : undefined,\n version: typeof manifest.version === \"string\" ? manifest.version : undefined,\n sdkVersion: typeof manifest.sdkVersion === \"string\" ? manifest.sdkVersion : undefined,\n minHostVersion: typeof manifest.minHostVersion === \"string\" ? manifest.minHostVersion : undefined,\n type: typeof manifest.type === \"string\" ? manifest.type : undefined,\n entry: manifest.entry as InstalledMiniappManifest[\"entry\"],\n permissions: manifest.permissions as InstalledMiniappManifest[\"permissions\"],\n hardwareRequirements: manifest.hardwareRequirements as InstalledMiniappManifest[\"hardwareRequirements\"],\n actions: manifest.actions as InstalledMiniappManifest[\"actions\"],\n }\n\n let bgSource: string\n try {\n const res = await fetch(bgUrl)\n if (!res.ok) return null\n bgSource = await res.text()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri,\n uiBaseDir: uiUri ? uiUri.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n devUrl: resolvedUrl,\n devPort: this.resolveDevPort(hints?.devPort, packageName),\n }\n }\n\n private async resolveInstalledBundle(packageName: string, version: string): Promise<ResolvedBundle | null> {\n const entryPaths = appRegistry.getMiniappEntryPaths(packageName, version)\n if (!entryPaths?.background) return null\n\n const manifest = appRegistry.getMiniappManifest(packageName, version) as {\n packageName?: string\n name?: string\n version?: string\n sdkVersion?: string\n minHostVersion?: string\n type?: string\n entry?: {background?: string; ui?: string}\n permissions?: Array<{type: string; required?: boolean; description?: string}>\n hardwareRequirements?: Array<{type: string; level: string; description?: string}>\n actions?: Array<{id?: unknown; description?: unknown; parameters?: unknown; outputSchema?: unknown}>\n } | null\n const declaredPermissions = (manifest?.permissions ?? [])\n .map((p) => p.type)\n .filter((t): t is string => typeof t === \"string\")\n const installedManifest: InstalledMiniappManifest | undefined = manifest\n ? {\n packageName: manifest.packageName ?? packageName,\n name: manifest.name,\n version: manifest.version ?? version,\n sdkVersion: manifest.sdkVersion,\n minHostVersion: manifest.minHostVersion,\n type: manifest.type,\n entry: manifest.entry,\n permissions: manifest.permissions,\n hardwareRequirements: manifest.hardwareRequirements,\n actions: manifest.actions,\n }\n : undefined\n\n let bgSource: string\n try {\n bgSource = new File(entryPaths.background).textSync()\n } catch {\n return null\n }\n\n return {\n bgSource,\n uiUri: entryPaths.ui ?? null,\n uiBaseDir: entryPaths.ui ? entryPaths.ui.replace(/\\/[^/]+$/, \"/\") : null,\n declaredPermissions,\n installedManifest,\n // Snapshot fallback is file:// — do not wire the sidecar; the laptop is gone.\n devUrl: null,\n devPort: null,\n }\n }\n\n /**\n * Ensure a miniapp's background JS context is spawned. Idempotent — if the\n * context is already alive this is a no-op spawn-wise (it still resolves the\n * UI entry so a re-mounting host gets its `<WebView>` source back).\n *\n * Resolves once the context is SPAWNED. It does NOT block on the context's\n * CONNECT handshake — the host's WebView mount shouldn't wait on background\n * JS. Callers that need the context to be connected and ready to receive\n * messages (the action broker) follow up with\n * {@link LocalMiniappRuntime.waitForConnect}.\n */\n async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n\n // Already spawned: best-effort resolve for the UI entry, never throw —\n // a headless caller that doesn't need UI gets {null, null} fast even if\n // the dev server has since dropped.\n if (router.registeredPackages().includes(packageName)) {\n const existing = await this.resolveBundle(packageName, hints).catch(() => null)\n return {uiUri: existing?.uiUri ?? null, uiBaseDir: existing?.uiBaseDir ?? null}\n }\n\n // Coalesce concurrent launches of the same package onto one promise.\n const pending = this.inFlight.get(packageName)\n if (pending) return pending\n\n const launch = this.spawn(packageName, hints)\n this.inFlight.set(packageName, launch)\n try {\n return await launch\n } finally {\n this.inFlight.delete(packageName)\n }\n }\n\n /** Resolve the bundle and spawn the context. Serialized via {@link inFlight}. */\n private async spawn(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {\n const router = this.requireRouter()\n const resolved = await this.resolveBundle(packageName, hints)\n if (!resolved) {\n throw new Error(`MiniappLauncher: cannot resolve bundle for ${packageName}`)\n }\n\n // Re-check after the async resolve: a different path may have spawned it\n // while we were fetching/reading the bundle.\n if (!router.registeredPackages().includes(packageName)) {\n const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {\n permissions: resolved.declaredPermissions,\n installedManifest: resolved.installedManifest,\n })\n if (!ok) {\n throw new Error(`MiniappLauncher: spawn failed for ${packageName}`)\n }\n\n // Dev: wire the hot-reload + log-forwarding sidecar. No-op in prod\n // (devServerBridge silently drops when no sidecar is up).\n if (resolved.devUrl && resolved.devPort != null) {\n devServerBridge.connect(packageName, resolved.devUrl, resolved.devPort)\n } else if (resolved.devUrl) {\n // Dev miniapp with no resolvable port — it still launches, but hot\n // reload + log forwarding won't connect. Surface it instead of failing\n // silently (the old inline path hard-failed with \"no dev port\").\n console.warn(`MiniappLauncher: ${packageName} has no dev port — hot reload + log forwarding disabled`)\n }\n }\n\n return {uiUri: resolved.uiUri, uiBaseDir: resolved.uiBaseDir}\n }\n\n /**\n * Re-spawn local miniapps that were running when the app was last killed.\n *\n * Cloud apps run on a remote server: the cloud persists which ones are\n * running and resurrects them when the phone reconnects, so a host-app\n * restart brings them back on its own. Local (phone-hosted) miniapps run in\n * the phone's own JS engine — a host-process kill tears down their JSContext\n * and there is no server to resurrect them, so without this they silently\n * stay stopped on the next launch even though the user left them running.\n *\n * Each local miniapp persists its running flag to disk on start/stop\n * (`saveLocalAppRunningState`, via the applet's `onStart`/`onStop`). We read\n * those flags back and re-spawn the background context headlessly (no\n * foreground, no navigation) — the spawn registers the package so the home\n * tray/switcher project it as running, exactly as a normal start would. The\n * WebView, if any, re-attaches lazily when the user opens the app.\n *\n * Idempotent and best-effort: skips already-spawned contexts, and clears the\n * persisted flag for any app whose bundle can no longer be resolved\n * (uninstalled, or dev server gone) so a dead entry doesn't retry every boot.\n * Not compatibility-gated: a previously-running background app shouldn't be\n * dropped just because glasses are momentarily disconnected at boot — it\n * resumes when they reconnect, same as mid-session.\n */\n async autostartLocalMiniapps(): Promise<void> {\n const apps = await appRegistry.getInstalledMiniapps()\n for (const app of apps) {\n // Only phone-hosted miniapps have a JSContext to re-spawn. Offline\n // built-ins (`local:false, offline:true`) restore their native running\n // state elsewhere and have no bundle to launch.\n if (!app.local) continue\n if (!getLocalAppRunningState(app.packageName)) continue\n if (this.isRunning(app.packageName)) continue\n try {\n await this.ensureRunning(app.packageName)\n } catch (e) {\n console.warn(`MiniappLauncher: autostart failed for ${app.packageName} — clearing stale running flag`, e)\n saveLocalAppRunningState(app.packageName, false)\n }\n }\n }\n\n /**\n * Ensure the context is running AND has completed its CONNECT handshake.\n * Used by the action broker, which must not deliver to a context that hasn't\n * come up yet. Throws on spawn failure or connect timeout.\n */\n async ensureConnected(packageName: string, connectTimeoutMs = 10_000, hints?: LaunchHints): Promise<void> {\n await this.ensureRunning(packageName, hints)\n await localMiniappRuntime.waitForConnect(packageName, connectTimeoutMs)\n }\n\n /** Tear down a miniapp's background JS context. */\n async stop(packageName: string): Promise<void> {\n // If a launch is in flight, let it finish first. Otherwise unregister()\n // no-ops on the not-yet-registered package and the pending spawn then\n // re-registers the context — leaving it running despite the stop.\n const pending = this.inFlight.get(packageName)\n if (pending) {\n try {\n await pending\n } catch {\n // Launch failed — nothing got registered, so nothing to tear down.\n }\n }\n await this.deps?.router.unregister(packageName)\n }\n\n // --- internals -----------------------------------------------------------\n\n private storedDevUrl(packageName: string): string | undefined {\n const r = storage.load<string>(`${packageName}_dev_url`)\n return r.is_ok() ? r.value : undefined\n }\n\n /** resolveDevPort — moved verbatim from the old LocalMiniappView helper. */\n private resolveDevPort(searchParam: string | undefined, packageName: string): number | null {\n if (searchParam) {\n const n = parseInt(searchParam, 10)\n if (Number.isFinite(n)) return n\n }\n const stored = storage.load<number>(`${packageName}_dev_port`)\n if (stored.is_ok()) return stored.value\n return null\n }\n}\n\nexport const miniappLauncher = new MiniappLauncher()\n"]}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Dev miniapp snapshot — keep a local copy of the last live-dev bundle so
3
+ * walking away from the laptop still opens the miniapp while Mentra App is
4
+ * running (and after, as long as the on-disk `dev-*` snapshot remains).
5
+ *
6
+ * Live launches load HTTP off `mentra-miniapp dev` for hot reload. After a
7
+ * successful reachability probe we fire-and-forget `${devUrl}/bundle.zip`
8
+ * into `lmas/<pkg>/dev-<ms>/`. When the laptop is unreachable, launch
9
+ * routes and MiniappLauncher fall back to that snapshot instead of the
10
+ * "Dev server offline" dead-end.
11
+ */
12
+ import { type DecideDevLaunchOptions, type DevManifest } from "./devMiniappLaunch";
13
+ export type DevOpenDecision = {
14
+ decision: "live";
15
+ manifest: DevManifest;
16
+ resolvedUrl: string;
17
+ } | {
18
+ decision: "cached";
19
+ } | {
20
+ decision: "offline";
21
+ };
22
+ export type DevBundleSource = {
23
+ kind: "live";
24
+ resolvedUrl: string;
25
+ manifest: DevManifest;
26
+ } | {
27
+ kind: "snapshot";
28
+ version: string;
29
+ } | {
30
+ kind: "none";
31
+ };
32
+ /** Candidate zip URLs: same-origin `/bundle.zip`, then the sidecar path. */
33
+ export declare function snapshotCandidateUrls(baseUrl: string, sidecarPort?: number | null): string[];
34
+ /**
35
+ * Download the current live-dev zip into `lmas/<pkg>/dev-<ms>/` and keep
36
+ * only the newest snapshot. Coalesces concurrent calls per package.
37
+ */
38
+ export declare function queueDevSnapshot(packageName: string, baseUrl: string, sidecarPort?: number | null): void;
39
+ /** Reachability first; if the laptop is down, open the last local snapshot. */
40
+ export declare function decideDevOpenRoute(packageName: string, devUrl: string, options?: DecideDevLaunchOptions): Promise<DevOpenDecision>;
41
+ /**
42
+ * Same live-vs-snapshot choice MiniappLauncher uses to resolve a bundle.
43
+ * Live also kicks a background snapshot so the next offline open has a copy.
44
+ */
45
+ export declare function resolveDevBundleSource(packageName: string, devUrl: string, options?: DecideDevLaunchOptions): Promise<DevBundleSource>;
46
+ //# sourceMappingURL=devMiniappSnapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devMiniappSnapshot.d.ts","sourceRoot":"","sources":["../../src/utils/devMiniappSnapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAEL,KAAK,sBAAsB,EAE3B,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,eAAe,GACvB;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,GAC9D;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAC,GACpB;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAC,CAAA;AAEzB,MAAM,MAAM,eAAe,GACvB;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAC,GAC1D;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACnC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AAIlB,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAc5F;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAOxG;AAkBD,+EAA+E;AAC/E,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAU1B;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAS1B"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Dev miniapp snapshot — keep a local copy of the last live-dev bundle so
3
+ * walking away from the laptop still opens the miniapp while Mentra App is
4
+ * running (and after, as long as the on-disk `dev-*` snapshot remains).
5
+ *
6
+ * Live launches load HTTP off `mentra-miniapp dev` for hot reload. After a
7
+ * successful reachability probe we fire-and-forget `${devUrl}/bundle.zip`
8
+ * into `lmas/<pkg>/dev-<ms>/`. When the laptop is unreachable, launch
9
+ * routes and MiniappLauncher fall back to that snapshot instead of the
10
+ * "Dev server offline" dead-end.
11
+ */
12
+ import appRegistry from "../services/AppRegistry";
13
+ import { storage } from "./storage/storage";
14
+ import { decideDevLaunchRoute, } from "./devMiniappLaunch";
15
+ const snapshotInFlight = new Map();
16
+ /** Candidate zip URLs: same-origin `/bundle.zip`, then the sidecar path. */
17
+ export function snapshotCandidateUrls(baseUrl, sidecarPort) {
18
+ const trimmed = baseUrl.replace(/\/$/, "");
19
+ const urls = [`${trimmed}/bundle.zip`];
20
+ try {
21
+ const url = new URL(trimmed);
22
+ const userPort = Number(url.port) || (url.protocol === "https:" ? 443 : 80);
23
+ const sidecar = sidecarPort && sidecarPort > 0 ? sidecarPort : userPort + 1;
24
+ url.port = String(sidecar);
25
+ const sidecarZip = `${url.origin}/__mentra_dev/bundle.zip`;
26
+ if (!urls.includes(sidecarZip))
27
+ urls.push(sidecarZip);
28
+ }
29
+ catch {
30
+ /* ignore malformed base */
31
+ }
32
+ return urls;
33
+ }
34
+ function storedSidecarPort(packageName) {
35
+ const stored = storage.load(`${packageName}_dev_port`);
36
+ return stored.is_ok() && Number.isFinite(stored.value) ? stored.value : undefined;
37
+ }
38
+ /**
39
+ * Download the current live-dev zip into `lmas/<pkg>/dev-<ms>/` and keep
40
+ * only the newest snapshot. Coalesces concurrent calls per package.
41
+ */
42
+ export function queueDevSnapshot(packageName, baseUrl, sidecarPort) {
43
+ if (!packageName || !baseUrl)
44
+ return;
45
+ if (snapshotInFlight.has(packageName))
46
+ return;
47
+ const promise = installDevSnapshot(packageName, baseUrl, sidecarPort ?? storedSidecarPort(packageName)).finally(() => {
48
+ snapshotInFlight.delete(packageName);
49
+ });
50
+ snapshotInFlight.set(packageName, promise);
51
+ }
52
+ async function installDevSnapshot(packageName, baseUrl, sidecarPort) {
53
+ let lastError;
54
+ for (const url of snapshotCandidateUrls(baseUrl, sidecarPort)) {
55
+ const res = await appRegistry.installFromUrl(url, {
56
+ versionOverride: `dev-${Date.now()}`,
57
+ releaseIdentity: { source: "dev_snapshot" },
58
+ });
59
+ if (res.is_ok()) {
60
+ appRegistry.gcDevVersions(packageName, 1);
61
+ return;
62
+ }
63
+ lastError = res.error;
64
+ }
65
+ console.warn(`Dev snapshot failed for ${packageName}:`, lastError);
66
+ }
67
+ /** Reachability first; if the laptop is down, open the last local snapshot. */
68
+ export async function decideDevOpenRoute(packageName, devUrl, options) {
69
+ const route = await decideDevLaunchRoute(packageName, devUrl, options);
70
+ if (route.decision === "live") {
71
+ queueDevSnapshot(packageName, route.resolvedUrl);
72
+ return route;
73
+ }
74
+ if (packageName && appRegistry.hasDevSnapshot(packageName)) {
75
+ return { decision: "cached" };
76
+ }
77
+ return { decision: "offline" };
78
+ }
79
+ /**
80
+ * Same live-vs-snapshot choice MiniappLauncher uses to resolve a bundle.
81
+ * Live also kicks a background snapshot so the next offline open has a copy.
82
+ */
83
+ export async function resolveDevBundleSource(packageName, devUrl, options) {
84
+ const route = await decideDevLaunchRoute(packageName, devUrl, options);
85
+ if (route.decision === "live" && route.manifest) {
86
+ queueDevSnapshot(packageName, route.resolvedUrl);
87
+ return { kind: "live", resolvedUrl: route.resolvedUrl, manifest: route.manifest };
88
+ }
89
+ const version = appRegistry.getLatestDevSnapshotVersion(packageName);
90
+ if (version)
91
+ return { kind: "snapshot", version };
92
+ return { kind: "none" };
93
+ }
94
+ //# sourceMappingURL=devMiniappSnapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devMiniappSnapshot.js","sourceRoot":"","sources":["../../src/utils/devMiniappSnapshot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Dev miniapp snapshot — keep a local copy of the last live-dev bundle so\n * walking away from the laptop still opens the miniapp while Mentra App is\n * running (and after, as long as the on-disk `dev-*` snapshot remains).\n *\n * Live launches load HTTP off `mentra-miniapp dev` for hot reload. After a\n * successful reachability probe we fire-and-forget `${devUrl}/bundle.zip`\n * into `lmas/<pkg>/dev-<ms>/`. When the laptop is unreachable, launch\n * routes and MiniappLauncher fall back to that snapshot instead of the\n * \"Dev server offline\" dead-end.\n */\n\nimport appRegistry from \"../services/AppRegistry\"\nimport {storage} from \"./storage/storage\"\nimport {\n decideDevLaunchRoute,\n type DecideDevLaunchOptions,\n type DevLaunchResult,\n type DevManifest,\n} from \"./devMiniappLaunch\"\n\nexport type DevOpenDecision =\n | {decision: \"live\"; manifest: DevManifest; resolvedUrl: string}\n | {decision: \"cached\"}\n | {decision: \"offline\"}\n\nexport type DevBundleSource =\n | {kind: \"live\"; resolvedUrl: string; manifest: DevManifest}\n | {kind: \"snapshot\"; version: string}\n | {kind: \"none\"}\n\nconst snapshotInFlight = new Map<string, Promise<void>>()\n\n/** Candidate zip URLs: same-origin `/bundle.zip`, then the sidecar path. */\nexport function snapshotCandidateUrls(baseUrl: string, sidecarPort?: number | null): string[] {\n const trimmed = baseUrl.replace(/\\/$/, \"\")\n const urls = [`${trimmed}/bundle.zip`]\n try {\n const url = new URL(trimmed)\n const userPort = Number(url.port) || (url.protocol === \"https:\" ? 443 : 80)\n const sidecar = sidecarPort && sidecarPort > 0 ? sidecarPort : userPort + 1\n url.port = String(sidecar)\n const sidecarZip = `${url.origin}/__mentra_dev/bundle.zip`\n if (!urls.includes(sidecarZip)) urls.push(sidecarZip)\n } catch {\n /* ignore malformed base */\n }\n return urls\n}\n\nfunction storedSidecarPort(packageName: string): number | undefined {\n const stored = storage.load<number>(`${packageName}_dev_port`)\n return stored.is_ok() && Number.isFinite(stored.value) ? stored.value : undefined\n}\n\n/**\n * Download the current live-dev zip into `lmas/<pkg>/dev-<ms>/` and keep\n * only the newest snapshot. Coalesces concurrent calls per package.\n */\nexport function queueDevSnapshot(packageName: string, baseUrl: string, sidecarPort?: number | null): void {\n if (!packageName || !baseUrl) return\n if (snapshotInFlight.has(packageName)) return\n const promise = installDevSnapshot(packageName, baseUrl, sidecarPort ?? storedSidecarPort(packageName)).finally(() => {\n snapshotInFlight.delete(packageName)\n })\n snapshotInFlight.set(packageName, promise)\n}\n\nasync function installDevSnapshot(packageName: string, baseUrl: string, sidecarPort?: number | null): Promise<void> {\n let lastError: unknown\n for (const url of snapshotCandidateUrls(baseUrl, sidecarPort)) {\n const res = await appRegistry.installFromUrl(url, {\n versionOverride: `dev-${Date.now()}`,\n releaseIdentity: {source: \"dev_snapshot\"},\n })\n if (res.is_ok()) {\n appRegistry.gcDevVersions(packageName, 1)\n return\n }\n lastError = res.error\n }\n console.warn(`Dev snapshot failed for ${packageName}:`, lastError)\n}\n\n/** Reachability first; if the laptop is down, open the last local snapshot. */\nexport async function decideDevOpenRoute(\n packageName: string,\n devUrl: string,\n options?: DecideDevLaunchOptions,\n): Promise<DevOpenDecision> {\n const route = await decideDevLaunchRoute(packageName, devUrl, options)\n if (route.decision === \"live\") {\n queueDevSnapshot(packageName, route.resolvedUrl)\n return route\n }\n if (packageName && appRegistry.hasDevSnapshot(packageName)) {\n return {decision: \"cached\"}\n }\n return {decision: \"offline\"}\n}\n\n/**\n * Same live-vs-snapshot choice MiniappLauncher uses to resolve a bundle.\n * Live also kicks a background snapshot so the next offline open has a copy.\n */\nexport async function resolveDevBundleSource(\n packageName: string,\n devUrl: string,\n options?: DecideDevLaunchOptions,\n): Promise<DevBundleSource> {\n const route: DevLaunchResult = await decideDevLaunchRoute(packageName, devUrl, options)\n if (route.decision === \"live\" && route.manifest) {\n queueDevSnapshot(packageName, route.resolvedUrl)\n return {kind: \"live\", resolvedUrl: route.resolvedUrl, manifest: route.manifest}\n }\n const version = appRegistry.getLatestDevSnapshotVersion(packageName)\n if (version) return {kind: \"snapshot\", version}\n return {kind: \"none\"}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/engine",
3
- "version": "3.2.0-dev.120",
3
+ "version": "3.2.0-dev.121",
4
4
  "description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
5
5
  "main": "build/index.js",
6
6
  "react-native": "src/index.ts",
@@ -97,12 +97,12 @@
97
97
  "registry": "https://registry.npmjs.org/"
98
98
  },
99
99
  "dependencies": {
100
- "@mentra/acs-meeting": "3.2.0-dev.120",
101
- "@mentra/bluetooth-sdk": "3.2.0-dev.120",
102
- "@mentra/cloud-client": "3.2.0-dev.120",
103
- "@mentra/cloud-protocol": "3.2.0-dev.120",
104
- "@mentra/crust": "3.2.0-dev.120",
105
- "@mentra/miniapp": "3.2.0-dev.120",
100
+ "@mentra/acs-meeting": "3.2.0-dev.121",
101
+ "@mentra/bluetooth-sdk": "3.2.0-dev.121",
102
+ "@mentra/cloud-client": "3.2.0-dev.121",
103
+ "@mentra/cloud-protocol": "3.2.0-dev.121",
104
+ "@mentra/crust": "3.2.0-dev.121",
105
+ "@mentra/miniapp": "3.2.0-dev.121",
106
106
  "buffer": "^6.0.3",
107
107
  "events": "^3.3.0",
108
108
  "react-native-marked": "8.1.1",
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "3.2.0",
15
- "releaseIdentity": "3.2.0-dev.120",
16
- "releaseSetId": "mentra-3.2.0-dev.120",
17
- "sourceCommit": "1b8b39b87dc44799c88f414f088629df0f013850",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.120.json",
19
- "otaManifestSha256": "4a5e13392f0d395a33867331fed19e9e5980526bc1b7e78f6ba2711c9e9ae1df"
15
+ "releaseIdentity": "3.2.0-dev.121",
16
+ "releaseSetId": "mentra-3.2.0-dev.121",
17
+ "sourceCommit": "4b4ff7a0a5c3c73f79ccc99e2464f75c23472cf0",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.121.json",
19
+ "otaManifestSha256": "2e8470ba4809eb5c1ea2d250772ab8cdbb542805526c3900d6d18d906b29bbb2"
20
20
  })
package/src/index.ts CHANGED
@@ -182,6 +182,10 @@ export {
182
182
  type DevLaunchResult,
183
183
  type DevManifest,
184
184
  } from "./utils/devMiniappLaunch"
185
+ export {
186
+ decideDevOpenRoute,
187
+ type DevOpenDecision,
188
+ } from "./utils/devMiniappSnapshot"
185
189
  export {HardwareCompatibility, type CompatibilityResult} from "./utils/hardware"
186
190
  export {BgTimer, throttle, debounce} from "./utils/timers"
187
191
 
@@ -13,6 +13,7 @@
13
13
  * - uninstall(packageName, version?) remove one or all versions
14
14
  * - getInstalledMiniapps() ClientApp[] derived from disk
15
15
  * - getActiveVersion(packageName) active version string for a package
16
+ * - getLatestDevSnapshotVersion(pkg) newest on-disk `dev-*` snapshot
16
17
  * - getBundleDir / getMiniappManifest filesystem helpers used by hosts
17
18
  * - subscribe(fn) register a refresh listener
18
19
  */
@@ -675,6 +676,28 @@ class AppRegistry {
675
676
  return storage.save(`${packageName}_active_version`, version)
676
677
  }
677
678
 
679
+ /**
680
+ * Newest `dev-*` snapshot that still has a resolvable UI or background
681
+ * entry. Semver store installs are ignored — a live-dev tile must not
682
+ * silently fall back to a released store bundle when the laptop drops.
683
+ */
684
+ public getLatestDevSnapshotVersion(packageName: string): string | null {
685
+ const versions = this.getInstalledVersions(packageName)
686
+ .filter((v) => v.startsWith("dev-"))
687
+ .sort()
688
+ .reverse()
689
+ for (const version of versions) {
690
+ const paths = this.getMiniappEntryPaths(packageName, version)
691
+ if (paths?.background || paths?.ui) return version
692
+ }
693
+ return null
694
+ }
695
+
696
+ /** True iff {@link getLatestDevSnapshotVersion} finds a usable snapshot. */
697
+ public hasDevSnapshot(packageName: string): boolean {
698
+ return this.getLatestDevSnapshotVersion(packageName) != null
699
+ }
700
+
678
701
  public getMetadata(packageName: string, version: string): InstalledInfo {
679
702
  try {
680
703
  const lmaDir = new Directory(Paths.document, "lmas", packageName, version)