@mentra/engine 0.1.0-beta.0 → 0.1.0-dev.1

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.
Files changed (355) hide show
  1. package/README.md +42 -1
  2. package/build/engine.d.ts +11 -3
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -2
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts.map +1 -1
  7. package/build/facades/dev.js +9 -5
  8. package/build/facades/dev.js.map +1 -1
  9. package/build/facades/gallery.js +1 -1
  10. package/build/facades/gallery.js.map +1 -1
  11. package/build/facades/glasses.d.ts +1 -0
  12. package/build/facades/glasses.d.ts.map +1 -1
  13. package/build/facades/glasses.js +19 -4
  14. package/build/facades/glasses.js.map +1 -1
  15. package/build/facades/glassesSettings.d.ts.map +1 -1
  16. package/build/facades/glassesSettings.js +1 -0
  17. package/build/facades/glassesSettings.js.map +1 -1
  18. package/build/facades/miniapps.d.ts +4 -0
  19. package/build/facades/miniapps.d.ts.map +1 -1
  20. package/build/facades/miniapps.js +5 -0
  21. package/build/facades/miniapps.js.map +1 -1
  22. package/build/facades/ota.d.ts +11 -2
  23. package/build/facades/ota.d.ts.map +1 -1
  24. package/build/facades/ota.js +19 -4
  25. package/build/facades/ota.js.map +1 -1
  26. package/build/facades/pairing.d.ts +4 -0
  27. package/build/facades/pairing.d.ts.map +1 -1
  28. package/build/facades/pairing.js +10 -8
  29. package/build/facades/pairing.js.map +1 -1
  30. package/build/facades/phoneNotifications.d.ts +0 -4
  31. package/build/facades/phoneNotifications.d.ts.map +1 -1
  32. package/build/facades/phoneNotifications.js +4 -14
  33. package/build/facades/phoneNotifications.js.map +1 -1
  34. package/build/index.d.ts +6 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +4 -2
  37. package/build/index.js.map +1 -1
  38. package/build/internal.d.ts +2 -2
  39. package/build/internal.d.ts.map +1 -1
  40. package/build/internal.js +1 -1
  41. package/build/internal.js.map +1 -1
  42. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  43. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  44. package/build/react/MentraLiveOtaFlow.js +528 -0
  45. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  46. package/build/react/index.d.ts +2 -0
  47. package/build/react/index.d.ts.map +1 -0
  48. package/build/react/index.js +2 -0
  49. package/build/react/index.js.map +1 -0
  50. package/build/react/useEngineSnapshot.d.ts +2 -0
  51. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  52. package/build/react/useEngineSnapshot.js +14 -0
  53. package/build/react/useEngineSnapshot.js.map +1 -0
  54. package/build/runtime/bootstrap.d.ts +24 -1
  55. package/build/runtime/bootstrap.d.ts.map +1 -1
  56. package/build/runtime/bootstrap.js +17 -0
  57. package/build/runtime/bootstrap.js.map +1 -1
  58. package/build/runtime/scanQrSeam.d.ts +7 -0
  59. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  60. package/build/runtime/scanQrSeam.js +16 -0
  61. package/build/runtime/scanQrSeam.js.map +1 -0
  62. package/build/services/AppRegistry.d.ts +18 -8
  63. package/build/services/AppRegistry.d.ts.map +1 -1
  64. package/build/services/AppRegistry.js +44 -45
  65. package/build/services/AppRegistry.js.map +1 -1
  66. package/build/services/AudioCloudUplink.d.ts +9 -3
  67. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  68. package/build/services/AudioCloudUplink.js +9 -3
  69. package/build/services/AudioCloudUplink.js.map +1 -1
  70. package/build/services/AudioPlaybackService.d.ts +9 -1
  71. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  72. package/build/services/AudioPlaybackService.js +77 -32
  73. package/build/services/AudioPlaybackService.js.map +1 -1
  74. package/build/services/BlobStore.d.ts +2 -1
  75. package/build/services/BlobStore.d.ts.map +1 -1
  76. package/build/services/BlobStore.js +17 -9
  77. package/build/services/BlobStore.js.map +1 -1
  78. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  79. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  80. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  81. package/build/services/CloudClientService.d.ts +15 -0
  82. package/build/services/CloudClientService.d.ts.map +1 -1
  83. package/build/services/CloudClientService.js +75 -0
  84. package/build/services/CloudClientService.js.map +1 -1
  85. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  86. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  87. package/build/services/ForegroundLocationPermission.js +12 -0
  88. package/build/services/ForegroundLocationPermission.js.map +1 -0
  89. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  90. package/build/services/GlassesSettingsSync.js +9 -3
  91. package/build/services/GlassesSettingsSync.js.map +1 -1
  92. package/build/services/GlassesStatusProjection.d.ts +2 -1
  93. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  94. package/build/services/GlassesStatusProjection.js +15 -8
  95. package/build/services/GlassesStatusProjection.js.map +1 -1
  96. package/build/services/HotspotOtaTransport.d.ts +29 -0
  97. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  98. package/build/services/HotspotOtaTransport.js +146 -0
  99. package/build/services/HotspotOtaTransport.js.map +1 -0
  100. package/build/services/HotspotShutdown.d.ts +17 -0
  101. package/build/services/HotspotShutdown.d.ts.map +1 -0
  102. package/build/services/HotspotShutdown.js +29 -0
  103. package/build/services/HotspotShutdown.js.map +1 -0
  104. package/build/services/LocalDisplayManager.d.ts +15 -0
  105. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  106. package/build/services/LocalDisplayManager.js +106 -20
  107. package/build/services/LocalDisplayManager.js.map +1 -1
  108. package/build/services/LocalMiniappRuntime.d.ts +67 -1
  109. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  110. package/build/services/LocalMiniappRuntime.js +486 -126
  111. package/build/services/LocalMiniappRuntime.js.map +1 -1
  112. package/build/services/LocalMiniappStorage.d.ts +32 -0
  113. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  114. package/build/services/LocalMiniappStorage.js +66 -0
  115. package/build/services/LocalMiniappStorage.js.map +1 -0
  116. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  117. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  118. package/build/services/LocalMiniappUserIdentity.js +43 -0
  119. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  120. package/build/services/MicStateCoordinator.d.ts +42 -1
  121. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  122. package/build/services/MicStateCoordinator.js +141 -3
  123. package/build/services/MicStateCoordinator.js.map +1 -1
  124. package/build/services/MiniappLauncher.d.ts.map +1 -1
  125. package/build/services/MiniappLauncher.js +23 -3
  126. package/build/services/MiniappLauncher.js.map +1 -1
  127. package/build/services/MiniappLiveness.d.ts +12 -0
  128. package/build/services/MiniappLiveness.d.ts.map +1 -0
  129. package/build/services/MiniappLiveness.js +16 -0
  130. package/build/services/MiniappLiveness.js.map +1 -0
  131. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  132. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  133. package/build/services/OtaArtifactDownloader.js +226 -0
  134. package/build/services/OtaArtifactDownloader.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +34 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +83 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +574 -130
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +8 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +142 -22
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  155. package/build/services/PhoneCalendarService.js +22 -1
  156. package/build/services/PhoneCalendarService.js.map +1 -1
  157. package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
  158. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  159. package/build/services/PhoneCameraFovCoordinator.js +14 -0
  160. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  161. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  162. package/build/services/PhoneNotificationsSync.js +12 -10
  163. package/build/services/PhoneNotificationsSync.js.map +1 -1
  164. package/build/services/PhonePhotoCoordinator.d.ts +10 -3
  165. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  166. package/build/services/PhonePhotoCoordinator.js +21 -3
  167. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  168. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  169. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  170. package/build/services/PhoneStreamCoordinator.js +125 -13
  171. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  172. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  173. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  174. package/build/services/PhoneVideoCoordinator.js +10 -0
  175. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  176. package/build/services/SceneRenderer.d.ts.map +1 -1
  177. package/build/services/SceneRenderer.js +15 -0
  178. package/build/services/SceneRenderer.js.map +1 -1
  179. package/build/services/SupportProfileSync.d.ts +22 -0
  180. package/build/services/SupportProfileSync.d.ts.map +1 -0
  181. package/build/services/SupportProfileSync.js +213 -0
  182. package/build/services/SupportProfileSync.js.map +1 -0
  183. package/build/services/TtsTextSanitizer.d.ts +5 -0
  184. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  185. package/build/services/TtsTextSanitizer.js +61 -0
  186. package/build/services/TtsTextSanitizer.js.map +1 -0
  187. package/build/services/WifiAdbRequest.d.ts +17 -0
  188. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  189. package/build/services/WifiAdbRequest.js +35 -0
  190. package/build/services/WifiAdbRequest.js.map +1 -0
  191. package/build/services/asg/asgCameraApi.d.ts +3 -0
  192. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  193. package/build/services/asg/asgCameraApi.js +72 -2
  194. package/build/services/asg/asgCameraApi.js.map +1 -1
  195. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  196. package/build/services/asg/gallerySyncService.js +26 -14
  197. package/build/services/asg/gallerySyncService.js.map +1 -1
  198. package/build/services/asg/localNetworkTransport.d.ts +2 -2
  199. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  200. package/build/services/asg/localNetworkTransport.js +6 -6
  201. package/build/services/asg/localNetworkTransport.js.map +1 -1
  202. package/build/services/audioPlaybackAssets.d.ts +8 -0
  203. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  204. package/build/services/audioPlaybackAssets.js +7 -0
  205. package/build/services/audioPlaybackAssets.js.map +1 -0
  206. package/build/services/glassesClockSync.js +2 -2
  207. package/build/services/glassesClockSync.js.map +1 -1
  208. package/build/services/manifestActions.d.ts +7 -0
  209. package/build/services/manifestActions.d.ts.map +1 -0
  210. package/build/services/manifestActions.js +25 -0
  211. package/build/services/manifestActions.js.map +1 -0
  212. package/build/services/otaDisplayState.d.ts +22 -11
  213. package/build/services/otaDisplayState.d.ts.map +1 -1
  214. package/build/services/otaDisplayState.js +36 -14
  215. package/build/services/otaDisplayState.js.map +1 -1
  216. package/build/services/otaInstallPolicy.d.ts +15 -3
  217. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  218. package/build/services/otaInstallPolicy.js +24 -3
  219. package/build/services/otaInstallPolicy.js.map +1 -1
  220. package/build/services/otaManifestUrl.d.ts +2 -0
  221. package/build/services/otaManifestUrl.d.ts.map +1 -0
  222. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  223. package/build/services/otaManifestUrl.js.map +1 -0
  224. package/build/services/slimStreamStatus.d.ts +11 -0
  225. package/build/services/slimStreamStatus.d.ts.map +1 -1
  226. package/build/services/slimStreamStatus.js +14 -4
  227. package/build/services/slimStreamStatus.js.map +1 -1
  228. package/build/stores/apps.d.ts.map +1 -1
  229. package/build/stores/apps.js +11 -0
  230. package/build/stores/apps.js.map +1 -1
  231. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  232. package/build/stores/bluetoothSettingKeys.js +2 -0
  233. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  234. package/build/stores/gallerySync.d.ts +2 -0
  235. package/build/stores/gallerySync.d.ts.map +1 -1
  236. package/build/stores/gallerySync.js +2 -0
  237. package/build/stores/gallerySync.js.map +1 -1
  238. package/build/stores/glasses.d.ts.map +1 -1
  239. package/build/stores/glasses.js +3 -0
  240. package/build/stores/glasses.js.map +1 -1
  241. package/build/stores/settings.d.ts +17 -0
  242. package/build/stores/settings.d.ts.map +1 -1
  243. package/build/stores/settings.js +152 -19
  244. package/build/stores/settings.js.map +1 -1
  245. package/build/types/applet.d.ts +2 -0
  246. package/build/types/applet.d.ts.map +1 -1
  247. package/build/types/applet.js.map +1 -1
  248. package/build/types/enums.d.ts +1 -0
  249. package/build/types/enums.d.ts.map +1 -1
  250. package/build/types/enums.js +1 -0
  251. package/build/types/enums.js.map +1 -1
  252. package/build/types/hardware.d.ts +1 -0
  253. package/build/types/hardware.d.ts.map +1 -1
  254. package/build/types/hardware.js +14 -0
  255. package/build/types/hardware.js.map +1 -1
  256. package/build/utils/configuredDevHost.d.ts +6 -0
  257. package/build/utils/configuredDevHost.d.ts.map +1 -0
  258. package/build/utils/configuredDevHost.js +19 -0
  259. package/build/utils/configuredDevHost.js.map +1 -0
  260. package/build/utils/devMiniappLaunch.d.ts +19 -8
  261. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  262. package/build/utils/devMiniappLaunch.js +144 -21
  263. package/build/utils/devMiniappLaunch.js.map +1 -1
  264. package/build/utils/diagnosticContext.d.ts.map +1 -1
  265. package/build/utils/diagnosticContext.js +15 -17
  266. package/build/utils/diagnosticContext.js.map +1 -1
  267. package/build/utils/hardware/hardware.d.ts.map +1 -1
  268. package/build/utils/hardware/hardware.js +7 -3
  269. package/build/utils/hardware/hardware.js.map +1 -1
  270. package/build/utils/miniappDiagnostics.d.ts +64 -0
  271. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  272. package/build/utils/miniappDiagnostics.js +173 -0
  273. package/build/utils/miniappDiagnostics.js.map +1 -0
  274. package/build/utils/timers/timers.d.ts.map +1 -1
  275. package/build/utils/timers/timers.js +55 -18
  276. package/build/utils/timers/timers.js.map +1 -1
  277. package/package.json +27 -18
  278. package/src/assets/silence.wav +0 -0
  279. package/src/engine.ts +7 -2
  280. package/src/facades/dev.ts +9 -5
  281. package/src/facades/gallery.ts +1 -1
  282. package/src/facades/glasses.ts +17 -4
  283. package/src/facades/glassesSettings.ts +5 -0
  284. package/src/facades/miniapps.ts +7 -0
  285. package/src/facades/ota.ts +19 -4
  286. package/src/facades/pairing.ts +11 -7
  287. package/src/facades/phoneNotifications.ts +4 -13
  288. package/src/index.ts +26 -2
  289. package/src/internal.ts +2 -1
  290. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  291. package/src/react/index.ts +7 -0
  292. package/src/react/useEngineSnapshot.ts +16 -0
  293. package/src/runtime/bootstrap.ts +33 -1
  294. package/src/runtime/scanQrSeam.ts +20 -0
  295. package/src/services/AppRegistry.ts +72 -48
  296. package/src/services/AudioCloudUplink.ts +9 -3
  297. package/src/services/AudioPlaybackService.ts +94 -34
  298. package/src/services/BlobStore.ts +22 -9
  299. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  300. package/src/services/CloudClientService.ts +74 -0
  301. package/src/services/ForegroundLocationPermission.ts +24 -0
  302. package/src/services/GlassesSettingsSync.ts +9 -3
  303. package/src/services/GlassesStatusProjection.ts +16 -8
  304. package/src/services/HotspotOtaTransport.ts +184 -0
  305. package/src/services/HotspotShutdown.ts +41 -0
  306. package/src/services/LocalDisplayManager.ts +112 -21
  307. package/src/services/LocalMiniappRuntime.ts +576 -130
  308. package/src/services/LocalMiniappStorage.ts +82 -0
  309. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  310. package/src/services/MicStateCoordinator.ts +169 -4
  311. package/src/services/MiniappLauncher.ts +30 -3
  312. package/src/services/MiniappLiveness.ts +24 -0
  313. package/src/services/OtaArtifactDownloader.ts +312 -0
  314. package/src/services/OtaAutoChain.ts +106 -0
  315. package/src/services/OtaErrorMapping.ts +80 -0
  316. package/src/services/OtaInstallCoordinator.ts +645 -143
  317. package/src/services/OtaService.ts +11 -5
  318. package/src/services/OtaUpdateCheckService.ts +180 -20
  319. package/src/services/PhoneCalendarService.ts +27 -4
  320. package/src/services/PhoneCameraFovCoordinator.ts +15 -0
  321. package/src/services/PhoneNotificationsSync.ts +12 -10
  322. package/src/services/PhonePhotoCoordinator.ts +33 -7
  323. package/src/services/PhoneStreamCoordinator.ts +139 -25
  324. package/src/services/PhoneVideoCoordinator.ts +11 -0
  325. package/src/services/SceneRenderer.ts +16 -0
  326. package/src/services/SupportProfileSync.ts +218 -0
  327. package/src/services/TtsTextSanitizer.ts +66 -0
  328. package/src/services/WifiAdbRequest.ts +53 -0
  329. package/src/services/asg/asgCameraApi.ts +99 -8
  330. package/src/services/asg/gallerySyncService.ts +28 -19
  331. package/src/services/asg/localNetworkTransport.ts +7 -11
  332. package/src/services/audioPlaybackAssets.ts +8 -0
  333. package/src/services/glassesClockSync.ts +2 -2
  334. package/src/services/manifestActions.ts +30 -0
  335. package/src/services/otaDisplayState.ts +57 -16
  336. package/src/services/otaInstallPolicy.ts +29 -3
  337. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  338. package/src/services/slimStreamStatus.ts +16 -5
  339. package/src/stores/apps.ts +12 -0
  340. package/src/stores/bluetoothSettingKeys.ts +2 -0
  341. package/src/stores/gallerySync.ts +4 -0
  342. package/src/stores/glasses.ts +3 -0
  343. package/src/stores/settings.ts +174 -21
  344. package/src/types/applet.ts +2 -0
  345. package/src/types/enums.ts +2 -1
  346. package/src/types/hardware.ts +17 -0
  347. package/src/utils/configuredDevHost.ts +17 -0
  348. package/src/utils/devMiniappLaunch.ts +151 -23
  349. package/src/utils/diagnosticContext.ts +17 -17
  350. package/src/utils/hardware/hardware.ts +7 -9
  351. package/src/utils/miniappDiagnostics.ts +226 -0
  352. package/src/utils/timers/timers.ts +57 -21
  353. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  354. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  355. package/build/services/asgOtaVersionUrl.js.map +0 -1
@@ -12,12 +12,27 @@
12
12
  * get it in the same round trip — no second fetch.
13
13
  *
14
14
  * Pre-flighting at the call site keeps /applet/local a pure mount route.
15
+ *
16
+ * When the laptop's Wi-Fi IP changes, the URL stored from the last QR scan
17
+ * goes stale. We then retry alternate hosts (Metro / EXPO_PUBLIC_LOCAL_MINIAPP_HOST,
18
+ * plus an mDNS name captured at scan time) and persist the working URL so
19
+ * the next launch doesn't need a re-scan.
15
20
  */
16
21
 
22
+ import {configuredDevHost} from "./configuredDevHost"
17
23
  import {storage} from "./storage/storage"
18
24
 
19
25
  const REACHABILITY_TIMEOUT_MS = 1500
20
26
 
27
+ // A single dropped DNS lookup or momentary Wi-Fi blip (AP roam, DHCP renewal,
28
+ // phone hopping to cellular for a beat) looks identical to a dead dev server —
29
+ // both throw out of fetch(). Retrying once absorbs that transient case instead
30
+ // of bouncing straight to the offline screen. We do NOT retry a real HTTP
31
+ // response (res.ok === false): that's the server telling us something
32
+ // definitive, not a network-layer hiccup.
33
+ const REACHABILITY_MAX_ATTEMPTS = 2
34
+ const REACHABILITY_RETRY_DELAY_MS = 400
35
+
21
36
  export type DevManifest = {
22
37
  packageName?: string
23
38
  name?: string
@@ -29,40 +44,153 @@ export type DevManifest = {
29
44
  }
30
45
 
31
46
  export type DevLaunchResult =
32
- | {decision: "live"; manifest: DevManifest}
33
- | {decision: "offline"; manifest: null}
47
+ | {decision: "live"; manifest: DevManifest; resolvedUrl: string}
48
+ | {decision: "offline"; manifest: null; resolvedUrl?: undefined}
49
+
50
+ export type DecideDevLaunchOptions = {
51
+ /** Extra hostnames / IPs to try if `devUrl` is unreachable (no scheme/port). */
52
+ alternateHosts?: string[]
53
+ }
54
+
55
+ function delay(ms: number): Promise<void> {
56
+ return new Promise((resolve) => setTimeout(resolve, ms))
57
+ }
58
+
59
+ /** Single GET <devUrl>/miniapp.json attempt with a hard timeout. */
60
+ async function fetchManifestOnce(devUrl: string): Promise<DevManifest | null> {
61
+ const controller = new AbortController()
62
+ const timer = setTimeout(() => controller.abort(), REACHABILITY_TIMEOUT_MS)
63
+ try {
64
+ const res = await fetch(`${devUrl.replace(/\/$/, "")}/miniapp.json`, {
65
+ method: "GET",
66
+ signal: controller.signal,
67
+ })
68
+ if (!res.ok) return null
69
+ return (await res.json()) as DevManifest
70
+ } finally {
71
+ clearTimeout(timer)
72
+ }
73
+ }
74
+
75
+ function rewriteHost(devUrl: string, host: string): string | null {
76
+ const trimmed = host.trim()
77
+ if (!trimmed) return null
78
+ try {
79
+ const url = new URL(devUrl)
80
+ if (url.hostname === trimmed) return null
81
+ url.hostname = trimmed
82
+ return url.toString().replace(/\/$/, "")
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Point an http(s) icon at the host that actually answered. Cached dev icons are
90
+ * local `file://` paths and third-party icons live on unrelated hosts, so only
91
+ * icons still served by the stale dev host are rewritten.
92
+ */
93
+ function rewriteIconHost(iconUrl: string, staleDevUrl: string, resolvedDevUrl: string): string | null {
94
+ try {
95
+ const icon = new URL(iconUrl)
96
+ if (icon.protocol !== "http:" && icon.protocol !== "https:") return null
97
+ const stale = new URL(staleDevUrl)
98
+ const resolved = new URL(resolvedDevUrl)
99
+ // Compare host (hostname+port) so an icon on the same hostname but a
100
+ // different service port is left alone.
101
+ if (icon.host !== stale.host) return null
102
+ icon.host = resolved.host
103
+ return icon.toString()
104
+ } catch {
105
+ return null
106
+ }
107
+ }
108
+
109
+ function collectAlternateHosts(packageName: string, extra?: string[]): string[] {
110
+ const hosts: string[] = []
111
+ const push = (h: string | undefined | null) => {
112
+ const v = h?.trim()
113
+ if (v && !hosts.includes(v)) hosts.push(v)
114
+ }
115
+
116
+ push(configuredDevHost())
117
+
118
+ if (packageName) {
119
+ const mdns = storage.load<string>(`${packageName}_dev_mdns`)
120
+ if (mdns.is_ok()) push(mdns.value)
121
+ }
122
+
123
+ for (const h of extra ?? []) push(h)
124
+ return hosts
125
+ }
126
+
127
+ async function probeUrl(devUrl: string): Promise<DevManifest | null | "network-error"> {
128
+ for (let attempt = 1; attempt <= REACHABILITY_MAX_ATTEMPTS; attempt++) {
129
+ try {
130
+ const manifest = await fetchManifestOnce(devUrl)
131
+ // Definitive HTTP failure — do not retry this URL further.
132
+ if (!manifest) return null
133
+ return manifest
134
+ } catch {
135
+ if (attempt < REACHABILITY_MAX_ATTEMPTS) {
136
+ await delay(REACHABILITY_RETRY_DELAY_MS)
137
+ continue
138
+ }
139
+ return "network-error"
140
+ }
141
+ }
142
+ return "network-error"
143
+ }
34
144
 
35
145
  /**
36
- * GET <devUrl>/miniapp.json with a hard timeout. Returns the parsed
146
+ * GET <devUrl>/miniapp.json with a hard timeout, retrying once on a
147
+ * network-layer failure (DNS/connection/abort) before trying alternate
148
+ * hosts (Metro laptop IP, mDNS name from the QR). Returns the parsed
37
149
  * manifest on success ("live") or null ("offline").
38
150
  *
39
- * Side effect: on success, writes <packageName>_dev_last_reachable so
40
- * the dev-offline screen can show "Last reached: N min ago" the next
41
- * time the user lands there.
42
- *
43
- * The fetch doubles as the reachability probe AND the manifest source —
44
- * one request per launch attempt instead of two.
151
+ * Side effects on success:
152
+ * - writes <packageName>_dev_last_reachable
153
+ * - if an alternate host worked, rewrites <packageName>_dev_url so the
154
+ * next launch skips the stale IP
45
155
  */
46
156
  export async function decideDevLaunchRoute(
47
157
  packageName: string,
48
158
  devUrl: string,
159
+ options?: DecideDevLaunchOptions,
49
160
  ): Promise<DevLaunchResult> {
50
- const controller = new AbortController()
51
- const timer = setTimeout(() => controller.abort(), REACHABILITY_TIMEOUT_MS)
52
- try {
53
- const res = await fetch(`${devUrl.replace(/\/$/, "")}/miniapp.json`, {
54
- method: "GET",
55
- signal: controller.signal,
56
- })
57
- if (!res.ok) return {decision: "offline", manifest: null}
58
- const manifest = (await res.json()) as DevManifest
161
+ const candidates: string[] = [devUrl]
162
+ for (const host of collectAlternateHosts(packageName, options?.alternateHosts)) {
163
+ const rewritten = rewriteHost(devUrl, host)
164
+ if (rewritten && !candidates.includes(rewritten)) candidates.push(rewritten)
165
+ }
166
+
167
+ for (const candidate of candidates) {
168
+ const result = await probeUrl(candidate)
169
+ if (result === "network-error" || result === null) continue
170
+
59
171
  if (packageName) {
60
172
  storage.save(`${packageName}_dev_last_reachable`, Date.now())
173
+ if (candidate !== devUrl) {
174
+ storage.save(`${packageName}_dev_url`, candidate)
175
+ // Keep _dev_meta.devUrl in sync when present so home-tile reads match.
176
+ const metaRes = storage.load<string>(`${packageName}_dev_meta`)
177
+ if (metaRes.is_ok()) {
178
+ try {
179
+ const meta = JSON.parse(metaRes.value) as {devUrl?: string; iconUrl?: string}
180
+ meta.devUrl = candidate
181
+ if (typeof meta.iconUrl === "string") {
182
+ const rewritten = rewriteIconHost(meta.iconUrl, devUrl, candidate)
183
+ if (rewritten) meta.iconUrl = rewritten
184
+ }
185
+ storage.save(`${packageName}_dev_meta`, JSON.stringify(meta))
186
+ } catch {
187
+ /* ignore malformed meta */
188
+ }
189
+ }
190
+ }
61
191
  }
62
- return {decision: "live", manifest}
63
- } catch {
64
- return {decision: "offline", manifest: null}
65
- } finally {
66
- clearTimeout(timer)
192
+ return {decision: "live", manifest: result, resolvedUrl: candidate}
67
193
  }
194
+
195
+ return {decision: "offline", manifest: null}
68
196
  }
@@ -4,11 +4,14 @@ import * as Location from "expo-location"
4
4
  import {Platform} from "react-native"
5
5
  import type {ReportContext} from "@mentra/cloud-client"
6
6
 
7
- import {useAppStatusStore} from "../stores/apps"
7
+ import appRegistry from "../services/AppRegistry"
8
+ import localMiniappRuntime from "../services/LocalMiniappRuntime"
9
+ import {getLastOpenTime, useAppStatusStore} from "../stores/apps"
8
10
  import {useConnectionStore} from "../stores/connection"
9
11
  import {useCoreStore} from "../stores/core"
10
12
  import {useGlassesStore} from "../stores/glasses"
11
13
  import {SETTINGS, useSettingsStore} from "../stores/settings"
14
+ import {buildMiniappDiagnosticContext} from "./miniappDiagnostics"
12
15
 
13
16
  const SENSITIVE_SETTINGS_KEYS = ["core_token", "auth_token", "auth_email"] as const
14
17
  const SENSITIVE_GLASSES_KEYS = ["hotspotPassword"] as const
@@ -108,17 +111,17 @@ export async function collectDiagnosticContext(extra?: Partial<ReportContext>):
108
111
 
109
112
  const offlineMode = await useSettingsStore.getState().getSetting(SETTINGS.offline_mode.key)
110
113
  const defaultWearable = await useSettingsStore.getState().getSetting(SETTINGS.default_wearable.key)
111
- const apps = appletState.apps.map((app) => ({
112
- packageName: app.packageName,
113
- name: app.name,
114
- running: app.running,
115
- loading: app.loading,
116
- healthy: app.healthy,
117
- hidden: app.hidden,
118
- type: app.type,
119
- offline: app.offline,
120
- local: app.local,
121
- }))
114
+ const miniappRuntime = localMiniappRuntime.getDiagnosticSnapshot()
115
+ const miniapps = buildMiniappDiagnosticContext({
116
+ apps: appletState.apps,
117
+ foregroundedPackage: appletState.foregroundedPackage,
118
+ runtime: miniappRuntime,
119
+ getLastOpenedAtMs: (app) => getLastOpenTime(app.packageName),
120
+ getManifest: (app) =>
121
+ app.local && app.version ? appRegistry.getMiniappManifest(app.packageName, app.version) : undefined,
122
+ getReleaseIdentity: (app) =>
123
+ app.local && app.version ? appRegistry.getReleaseIdentity(app.packageName, app.version) : undefined,
124
+ })
122
125
 
123
126
  return {
124
127
  app: {
@@ -139,12 +142,9 @@ export async function collectDiagnosticContext(extra?: Partial<ReportContext>):
139
142
  runtime: {
140
143
  core: coreState,
141
144
  connection: connectionState,
145
+ miniapps: miniappRuntime,
142
146
  },
143
- apps: {
144
- apps,
145
- installed: apps.map((app) => app.packageName),
146
- running: apps.filter((app) => app.running).map((app) => app.packageName),
147
- },
147
+ apps: miniapps,
148
148
  settings: {
149
149
  ...filteredSettings,
150
150
  offlineMode: !!offlineMode,
@@ -1,11 +1,4 @@
1
- import {
2
- Capabilities,
3
- HardwareRequirement,
4
- HardwareType,
5
- HardwareRequirementLevel,
6
- DeviceTypes,
7
- } from "../../types"
8
- import {simulatedGlasses} from "../../types"
1
+ import {Capabilities, DeviceTypes, HardwareRequirement, HardwareRequirementLevel, HardwareType, simulatedGlasses} from "../../types"
9
2
 
10
3
  /**
11
4
  * Result of a hardware compatibility check
@@ -86,7 +79,12 @@ export class HardwareCompatibility {
86
79
  return capabilities.hasDisplay
87
80
 
88
81
  case HardwareType.MICROPHONE:
89
- return capabilities.hasMicrophone
82
+ // MentraOS always provides an audio-input path. When the connected
83
+ // glasses do not have an onboard microphone, the engine falls back to
84
+ // the phone microphone. Keep `capabilities.hasMicrophone` reserved for
85
+ // physical-device/source selection; app compatibility is about whether
86
+ // the requested input is available to the miniapp.
87
+ return true
90
88
 
91
89
  case HardwareType.SPEAKER:
92
90
  return capabilities.hasSpeaker
@@ -0,0 +1,226 @@
1
+ import type {ClientApp} from "../types/applet"
2
+
3
+ const MAX_DIAGNOSTIC_STRING_LENGTH = 512
4
+ const MAX_DIAGNOSTIC_LIST_ITEMS = 100
5
+ const MAX_RECENT_MINIAPPS = 20
6
+
7
+ export interface MiniappManifestSnapshot {
8
+ packageName: string
9
+ name?: string
10
+ version?: string
11
+ sdkVersion?: string
12
+ minHostVersion?: string
13
+ type?: string
14
+ entry?: {
15
+ background?: string
16
+ ui?: string
17
+ }
18
+ permissions: Array<{type: string; required?: boolean}>
19
+ hardwareRequirements: Array<{type: string; level: string}>
20
+ actionIds: string[]
21
+ }
22
+
23
+ export interface MiniappRuntimeAppSnapshot {
24
+ packageName: string
25
+ handshakeComplete: boolean
26
+ subscriptions: string[]
27
+ lastMessageAgeMs: number
28
+ requestedLocationRate: string | null
29
+ hasActiveSpeakerStream: boolean
30
+ manifest?: MiniappManifestSnapshot
31
+ }
32
+
33
+ export interface MiniappRuntimeDiagnosticSnapshot {
34
+ connectedApps: MiniappRuntimeAppSnapshot[]
35
+ subscriptionsByStream: Record<string, string[]>
36
+ resources: {
37
+ display: Record<string, unknown>
38
+ camera: Record<string, unknown>
39
+ video: Record<string, unknown>
40
+ stream: Record<string, unknown>
41
+ cameraFov: Record<string, unknown>
42
+ }
43
+ }
44
+
45
+ type ManifestFallback = {
46
+ packageName: string
47
+ name?: string
48
+ version?: string
49
+ type?: string
50
+ }
51
+
52
+ function boundedString(value: unknown): string | undefined {
53
+ if (typeof value !== "string") return undefined
54
+ const trimmed = value.trim()
55
+ if (!trimmed) return undefined
56
+ return trimmed.slice(0, MAX_DIAGNOSTIC_STRING_LENGTH)
57
+ }
58
+
59
+ function recordValue(value: unknown): Record<string, unknown> | null {
60
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : null
61
+ }
62
+
63
+ function diagnosticPermissions(value: unknown): Array<{type: string; required?: boolean}> {
64
+ if (!Array.isArray(value)) return []
65
+ const permissions: Array<{type: string; required?: boolean}> = []
66
+ for (const raw of value) {
67
+ if (typeof raw === "string") {
68
+ const type = boundedString(raw)
69
+ if (type) permissions.push({type})
70
+ continue
71
+ }
72
+ const permission = recordValue(raw)
73
+ const type = boundedString(permission?.type)
74
+ if (!type) continue
75
+ permissions.push({
76
+ type,
77
+ ...(typeof permission?.required === "boolean" ? {required: permission.required} : {}),
78
+ })
79
+ }
80
+ return permissions.slice(0, MAX_DIAGNOSTIC_LIST_ITEMS)
81
+ }
82
+
83
+ function diagnosticHardwareRequirements(value: unknown): Array<{type: string; level: string}> {
84
+ if (!Array.isArray(value)) return []
85
+ const requirements: Array<{type: string; level: string}> = []
86
+ for (const raw of value) {
87
+ const requirement = recordValue(raw)
88
+ const type = boundedString(requirement?.type)
89
+ const level = boundedString(requirement?.level)
90
+ if (type && level) requirements.push({type, level})
91
+ }
92
+ return requirements.slice(0, MAX_DIAGNOSTIC_LIST_ITEMS)
93
+ }
94
+
95
+ function diagnosticActionIds(value: unknown): string[] {
96
+ if (!Array.isArray(value)) return []
97
+ return value
98
+ .map((raw) => boundedString(typeof raw === "string" ? raw : recordValue(raw)?.id))
99
+ .filter((id): id is string => id !== undefined)
100
+ .sort()
101
+ .slice(0, MAX_DIAGNOSTIC_LIST_ITEMS)
102
+ }
103
+
104
+ /**
105
+ * Keep the routing/build fields from miniapp.json while excluding arbitrary
106
+ * free-form manifest content from reports. Descriptions, URLs, and unknown
107
+ * extension fields are intentionally omitted.
108
+ */
109
+ export function buildMiniappManifestSnapshot(raw: unknown, fallback: ManifestFallback): MiniappManifestSnapshot {
110
+ const manifest = recordValue(raw)
111
+ const entry = recordValue(manifest?.entry)
112
+ const packageName = boundedString(manifest?.packageName) ?? boundedString(fallback.packageName) ?? "unknown"
113
+ const name = boundedString(manifest?.name) ?? boundedString(fallback.name)
114
+ const version = boundedString(manifest?.version) ?? boundedString(fallback.version)
115
+ const type = boundedString(manifest?.type) ?? boundedString(fallback.type)
116
+ const background = boundedString(entry?.background)
117
+ const ui = boundedString(entry?.ui)
118
+
119
+ return {
120
+ packageName,
121
+ ...(name ? {name} : {}),
122
+ ...(version ? {version} : {}),
123
+ ...(boundedString(manifest?.sdkVersion) ? {sdkVersion: boundedString(manifest?.sdkVersion)} : {}),
124
+ ...(boundedString(manifest?.minHostVersion) ? {minHostVersion: boundedString(manifest?.minHostVersion)} : {}),
125
+ ...(type ? {type} : {}),
126
+ ...(background || ui ? {entry: {...(background ? {background} : {}), ...(ui ? {ui} : {})}} : {}),
127
+ permissions: diagnosticPermissions(manifest?.permissions),
128
+ hardwareRequirements: diagnosticHardwareRequirements(manifest?.hardwareRequirements),
129
+ actionIds: diagnosticActionIds(manifest?.actions ?? manifest?.actionIds),
130
+ }
131
+ }
132
+
133
+ function executionMode(app: ClientApp): "built_in" | "dev_server" | "dev_snapshot" | "installed_bundle" {
134
+ if (app.offline) return "built_in"
135
+ if (app.isMiniappDev && app.devUrl && !app.version) return "dev_server"
136
+ if (app.isMiniappDev) return "dev_snapshot"
137
+ return "installed_bundle"
138
+ }
139
+
140
+ function safeReleaseIdentity(value: unknown): Record<string, string> | undefined {
141
+ const identity = recordValue(value)
142
+ if (!identity) return undefined
143
+ const out: Record<string, string> = {}
144
+ for (const key of ["source", "releaseId", "bundleSha256", "channel"] as const) {
145
+ const field = boundedString(identity[key])
146
+ if (field) out[key] = field
147
+ }
148
+ return Object.keys(out).length > 0 ? out : undefined
149
+ }
150
+
151
+ export function buildMiniappDiagnosticContext(input: {
152
+ apps: ClientApp[]
153
+ foregroundedPackage: string | null
154
+ runtime: MiniappRuntimeDiagnosticSnapshot
155
+ getLastOpenedAtMs: (app: ClientApp) => number
156
+ getManifest: (app: ClientApp) => unknown
157
+ getReleaseIdentity: (app: ClientApp) => unknown
158
+ }): Record<string, unknown> {
159
+ const runtimeByPackage = new Map(input.runtime.connectedApps.map((app) => [app.packageName, app]))
160
+ const appSnapshots = input.apps.map((app) => {
161
+ const lastOpenedAtMs = input.getLastOpenedAtMs(app)
162
+ const packageName = boundedString(app.packageName) ?? "unknown"
163
+ return {
164
+ packageName,
165
+ name: boundedString(app.name) ?? packageName,
166
+ version: boundedString(app.version) ?? null,
167
+ executionMode: executionMode(app),
168
+ running: app.running,
169
+ foregrounded: app.packageName === input.foregroundedPackage,
170
+ loading: app.loading,
171
+ healthy: app.healthy,
172
+ hidden: app.hidden,
173
+ type: app.type,
174
+ offline: app.offline,
175
+ local: app.local,
176
+ lastOpenedAtMs: Number.isFinite(lastOpenedAtMs) ? lastOpenedAtMs : 0,
177
+ }
178
+ })
179
+
180
+ const recentlyInteracted = [...appSnapshots]
181
+ .filter((app) => app.lastOpenedAtMs > 0)
182
+ .sort((a, b) => b.lastOpenedAtMs - a.lastOpenedAtMs)
183
+ .slice(0, MAX_RECENT_MINIAPPS)
184
+ .map((app) => ({packageName: app.packageName, lastOpenedAtMs: app.lastOpenedAtMs}))
185
+
186
+ const runningMiniapps = input.apps
187
+ .filter((app) => app.running)
188
+ .map((app) => {
189
+ const runtime = runtimeByPackage.get(app.packageName)
190
+ const releaseIdentity = safeReleaseIdentity(input.getReleaseIdentity(app))
191
+ const packageName = boundedString(app.packageName) ?? "unknown"
192
+ const manifest = buildMiniappManifestSnapshot(input.getManifest(app) ?? runtime?.manifest, {
193
+ packageName,
194
+ name: app.name,
195
+ version: app.version,
196
+ type: app.type,
197
+ })
198
+ return {
199
+ packageName,
200
+ version: boundedString(app.version) ?? manifest.version ?? null,
201
+ executionMode: executionMode(app),
202
+ foregrounded: app.packageName === input.foregroundedPackage,
203
+ manifest,
204
+ ...(releaseIdentity ? {releaseIdentity} : {}),
205
+ runtime: runtime
206
+ ? {
207
+ handshakeComplete: runtime.handshakeComplete,
208
+ subscriptions: runtime.subscriptions,
209
+ lastMessageAgeMs: runtime.lastMessageAgeMs,
210
+ requestedLocationRate: runtime.requestedLocationRate,
211
+ hasActiveSpeakerStream: runtime.hasActiveSpeakerStream,
212
+ }
213
+ : null,
214
+ }
215
+ })
216
+
217
+ return {
218
+ apps: appSnapshots,
219
+ installed: appSnapshots.map((app) => app.packageName),
220
+ running: runningMiniapps.map((app) => app.packageName),
221
+ foregroundedPackage: boundedString(input.foregroundedPackage) ?? null,
222
+ mostRecentlyInteractedPackage: recentlyInteracted[0]?.packageName ?? null,
223
+ recentlyInteracted,
224
+ runningMiniapps,
225
+ }
226
+ }
@@ -1,6 +1,28 @@
1
- // until https://github.com/tconns/react-native-nitro-bg-timer/issues/2 is resolved, we need to use this class to disable this package on iOS:
2
-
3
- import {Platform} from "react-native"
1
+ // BgTimer — background-safe timers for the engine.
2
+ //
3
+ // Everything timing-critical in the engine runs through this class: the
4
+ // miniapp liveness watchdog, display durationMs expiries, boot windows,
5
+ // auth-token refresh. On Android, React Native PAUSES plain JS timers while
6
+ // the app is backgrounded, so those features only keep working in the
7
+ // background when the native implementation (react-native-nitro-bg-timer)
8
+ // is active.
9
+ //
10
+ // Who actually gets native timers:
11
+ // - Android, dev AND release: always attempted — dev must match production
12
+ // background behavior. There is no opt-in/opt-out env var. If the native
13
+ // module is unavailable (a dev client whose binary predates it, or a
14
+ // broken release binary), the failure is LOUD: a console.error with the
15
+ // remediation, plus a blocking dev alert. The JS-timer fallback still
16
+ // engages so foreground behavior keeps working, but backgrounded
17
+ // behavior is broken until the native rebuild — the error says exactly
18
+ // that. (A silent dev fallback previously cost a full debugging day of
19
+ // phantom "zombie miniapp" bugs; see OS-1790.)
20
+ // - iOS: never, dev or prod — the package is disabled pending
21
+ // https://github.com/tconns/react-native-nitro-bg-timer/issues/2. iOS
22
+ // suspends the whole process in background anyway, so background timing
23
+ // there is handled by native per-device queues, not JS timers.
24
+
25
+ import {Alert, Platform} from "react-native"
4
26
 
5
27
  type NitroTimerApi = {
6
28
  setInterval: (callback: () => void, delay: number) => number
@@ -11,14 +33,36 @@ type NitroTimerApi = {
11
33
 
12
34
  let nitroTimer: NitroTimerApi | null | undefined
13
35
  let nitroDisabled = false
14
- let warnedUnavailable = false
15
-
16
- function warnUnavailable(reason: string) {
17
- if (warnedUnavailable) {
36
+ let failedLoud = false
37
+
38
+ /**
39
+ * The native background timer is unavailable — say so ONCE, loudly, with the
40
+ * remediation. The JS-timer fallback keeps foreground behavior working, but
41
+ * everything timing-driven freezes while the app is backgrounded, which
42
+ * presents as phantom miniapp bugs (frozen captions, missed wake words,
43
+ * watchdog kills on resume). A quiet warning here has already cost a full
44
+ * debugging day; never downgrade this to console.warn.
45
+ */
46
+ function failUnavailable(reason: string) {
47
+ if (failedLoud) {
18
48
  return
19
49
  }
20
- warnedUnavailable = true
21
- console.warn(`BgTimer: ${reason}, using JS timers`)
50
+ failedLoud = true
51
+ const message =
52
+ `BgTimer: native background timer unavailable (${reason}). ` +
53
+ "Backgrounded behavior IS BROKEN until you rebuild the Android app: run `bun android`. " +
54
+ "Engine timers and local miniapps (captions, wake words) freeze whenever the app is not foregrounded."
55
+ console.error(message)
56
+ if (__DEV__) {
57
+ // A stale dev client may ALSO show the library's own require-time red box
58
+ // (bridgeless LogBox surfaces it even though we catch below) — this alert
59
+ // is the actionable one: it names the fix.
60
+ try {
61
+ Alert.alert("Background timers unavailable", message)
62
+ } catch {
63
+ /* Alert not available (tests / headless) — the console.error stands. */
64
+ }
65
+ }
22
66
  }
23
67
 
24
68
  function getNitroTimer(): NitroTimerApi | null {
@@ -26,15 +70,6 @@ function getNitroTimer(): NitroTimerApi | null {
26
70
  return null
27
71
  }
28
72
 
29
- // react-native-nitro-bg-timer calls createHybridObject() at require() time. On dev
30
- // builds with a stale native binary that throws NPE, React Native still surfaces a
31
- // red LogBox error even when the exception is caught. Skip nitro in __DEV__ unless
32
- // explicitly enabled after a native rebuild (bun android).
33
- if (__DEV__ && process.env.EXPO_PUBLIC_USE_NITRO_BG_TIMER !== "true") {
34
- warnUnavailable("nitro bg-timer disabled in dev (set EXPO_PUBLIC_USE_NITRO_BG_TIMER=true after native rebuild)")
35
- return null
36
- }
37
-
38
73
  if (nitroTimer !== undefined) {
39
74
  return nitroTimer
40
75
  }
@@ -43,7 +78,7 @@ function getNitroTimer(): NitroTimerApi | null {
43
78
  const {isRuntimeAlive} = require("react-native-nitro-modules") as {isRuntimeAlive?: () => boolean}
44
79
  if (typeof isRuntimeAlive === "function" && !isRuntimeAlive()) {
45
80
  nitroTimer = null
46
- warnUnavailable("nitro runtime is not alive")
81
+ failUnavailable("nitro runtime is not alive")
47
82
  return nitroTimer
48
83
  }
49
84
 
@@ -62,7 +97,7 @@ function getNitroTimer(): NitroTimerApi | null {
62
97
  }
63
98
 
64
99
  if (!nitroTimer) {
65
- warnUnavailable("react-native-nitro-bg-timer unavailable")
100
+ failUnavailable("react-native-nitro-bg-timer failed to load")
66
101
  }
67
102
  return nitroTimer
68
103
  }
@@ -77,7 +112,8 @@ function withNitro<T>(run: (api: NitroTimerApi) => T, fallback: () => T): T {
77
112
  } catch (error) {
78
113
  nitroDisabled = true
79
114
  nitroTimer = null
80
- console.warn("BgTimer: nitro timer failed, using JS timers", error)
115
+ failedLoud = false // a mid-session failure is new information — re-announce
116
+ failUnavailable(`nitro timer call threw: ${String(error)}`)
81
117
  return fallback()
82
118
  }
83
119
  }
@@ -1,2 +0,0 @@
1
- export declare function getAsgOtaVersionUrl(glassesUrl?: string | null, glassesBuildNumber?: string | null): string;
2
- //# sourceMappingURL=asgOtaVersionUrl.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"asgOtaVersionUrl.d.ts","sourceRoot":"","sources":["../../src/services/asgOtaVersionUrl.ts"],"names":[],"mappings":"AAsBA,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAkB1G"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"asgOtaVersionUrl.js","sourceRoot":"","sources":["../../src/services/asgOtaVersionUrl.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAE,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AAE7D,MAAM,oBAAoB,GAAG,oDAAoD,CAAA;AAEjF,SAAS,wBAAwB,CAAC,kBAAkC;IAClE,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,kBAAkB,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;IACjE,4EAA4E;IAC5E,kCAAkC;IAClC,OAAO,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,EAAE,CAAA;AACzD,CAAC;AAED,SAAS,2BAA2B;IAClC,sEAAsE;IACtE,4DAA4D;IAC5D,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrE,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,KAAK,GAAG,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;IAClF,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7D,OAAO,OAAO,IAAI,IAAI,CAAA;AACxB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAA0B,EAAE,kBAAkC;IAChG,MAAM,SAAS,GAAG,UAAU,EAAE,IAAI,EAAE,CAAA;IACpC,IAAI,wBAAwB,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACjD,yEAAyE;QACzE,6EAA6E;QAC7E,OAAO,SAAS,IAAI,oBAAoB,CAAA;IAC1C,CAAC;IAED,MAAM,cAAc,GAAG,2BAA2B,EAAE,CAAA;IACpD,IAAI,cAAc,EAAE,CAAC;QACnB,OAAO,cAAc,CAAA;IACvB,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,+BAA+B,EAAE,IAAI,EAAE,CAAA;IAClE,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAA;IACf,CAAC;IACD,OAAO,SAAS,IAAI,oBAAoB,CAAA;AAC1C,CAAC","sourcesContent":["import {SETTINGS, useSettingsStore} from \"../stores/settings\"\n\nconst OTA_VERSION_URL_PROD = \"https://ota.mentraglass.com/prod_live_version.json\"\n\nfunction isLegacyAsgOtaStartBuild(glassesBuildNumber?: string | null): boolean {\n const buildNumber = Number.parseInt(glassesBuildNumber ?? \"\", 10)\n // ASG builds before 39 ignore ota_start.ota_version_url, so compare against\n // the URL they will actually use.\n return Number.isFinite(buildNumber) && buildNumber < 39\n}\n\nfunction getOtaVersionUrlDevOverride(): string | null {\n // Super mode only: a wrong OTA manifest can brick glasses, so a saved\n // override is inert unless super mode is currently enabled.\n if (!useSettingsStore.getState().getSetting(SETTINGS.super_mode.key)) {\n return null\n }\n const value = useSettingsStore.getState().getSetting(SETTINGS.ota_version_url.key)\n const trimmed = typeof value === \"string\" ? value.trim() : \"\"\n return trimmed || null\n}\n\nexport function getAsgOtaVersionUrl(glassesUrl?: string | null, glassesBuildNumber?: string | null): string {\n const deviceUrl = glassesUrl?.trim()\n if (isLegacyAsgOtaStartBuild(glassesBuildNumber)) {\n // Legacy glasses ignore ota_start.ota_version_url and install from their\n // compiled default, so the developer override does not apply to them either.\n return deviceUrl || OTA_VERSION_URL_PROD\n }\n\n const devOverrideUrl = getOtaVersionUrlDevOverride()\n if (devOverrideUrl) {\n return devOverrideUrl\n }\n\n const envUrl = process.env.EXPO_PUBLIC_ASG_OTA_VERSION_URL?.trim()\n if (envUrl) {\n return envUrl\n }\n return deviceUrl || OTA_VERSION_URL_PROD\n}\n"]}