@glassly/engine 0.3.7 → 0.3.8-dev.977

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 (176) hide show
  1. package/README.md +4 -0
  2. package/build/engine.d.ts +8 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -4
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/backup.d.ts +14 -0
  7. package/build/facades/backup.d.ts.map +1 -0
  8. package/build/facades/backup.js +22 -0
  9. package/build/facades/backup.js.map +1 -0
  10. package/build/facades/glasses.d.ts +4 -4
  11. package/build/facades/glasses.d.ts.map +1 -1
  12. package/build/facades/glasses.js +4 -4
  13. package/build/facades/glasses.js.map +1 -1
  14. package/build/generated/releaseMetadata.js +4 -4
  15. package/build/generated/releaseMetadata.js.map +1 -1
  16. package/build/index.d.ts +1 -0
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js.map +1 -1
  19. package/build/services/AppRegistry.d.ts +2 -1
  20. package/build/services/AppRegistry.d.ts.map +1 -1
  21. package/build/services/AppRegistry.js +38 -8
  22. package/build/services/AppRegistry.js.map +1 -1
  23. package/build/services/AppStateBackup.d.ts +98 -0
  24. package/build/services/AppStateBackup.d.ts.map +1 -0
  25. package/build/services/AppStateBackup.js +224 -0
  26. package/build/services/AppStateBackup.js.map +1 -0
  27. package/build/services/BlobStore.d.ts.map +1 -1
  28. package/build/services/BlobStore.js +5 -12
  29. package/build/services/BlobStore.js.map +1 -1
  30. package/build/services/CloudClientService.d.ts +1 -0
  31. package/build/services/CloudClientService.d.ts.map +1 -1
  32. package/build/services/CloudClientService.js +34 -2
  33. package/build/services/CloudClientService.js.map +1 -1
  34. package/build/services/DisplayProcessor.d.ts +2 -0
  35. package/build/services/DisplayProcessor.d.ts.map +1 -1
  36. package/build/services/DisplayProcessor.js +15 -1
  37. package/build/services/DisplayProcessor.js.map +1 -1
  38. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  39. package/build/services/GlassesTouchInput.js +4 -2
  40. package/build/services/GlassesTouchInput.js.map +1 -1
  41. package/build/services/JSCrashloopReportService.d.ts.map +1 -1
  42. package/build/services/JSCrashloopReportService.js +5 -0
  43. package/build/services/JSCrashloopReportService.js.map +1 -1
  44. package/build/services/JSRouter.d.ts +11 -3
  45. package/build/services/JSRouter.d.ts.map +1 -1
  46. package/build/services/JSRouter.js +36 -6
  47. package/build/services/JSRouter.js.map +1 -1
  48. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  49. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  50. package/build/services/LocalMiniappRuntime.js +54 -20
  51. package/build/services/LocalMiniappRuntime.js.map +1 -1
  52. package/build/services/MiniappEngine.d.ts +9 -0
  53. package/build/services/MiniappEngine.d.ts.map +1 -1
  54. package/build/services/MiniappEngine.js +60 -27
  55. package/build/services/MiniappEngine.js.map +1 -1
  56. package/build/services/MiniappLauncher.d.ts +2 -0
  57. package/build/services/MiniappLauncher.d.ts.map +1 -1
  58. package/build/services/MiniappLauncher.js +6 -0
  59. package/build/services/MiniappLauncher.js.map +1 -1
  60. package/build/services/NavigationService.d.ts +1 -0
  61. package/build/services/NavigationService.d.ts.map +1 -1
  62. package/build/services/NavigationService.js +20 -6
  63. package/build/services/NavigationService.js.map +1 -1
  64. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  65. package/build/services/PhoneCalendarService.js +8 -0
  66. package/build/services/PhoneCalendarService.js.map +1 -1
  67. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  68. package/build/services/PhoneNotificationsSync.js +7 -3
  69. package/build/services/PhoneNotificationsSync.js.map +1 -1
  70. package/build/services/SceneRenderer.js +1 -1
  71. package/build/services/SceneRenderer.js.map +1 -1
  72. package/build/services/appStateBackupFile.d.ts +32 -0
  73. package/build/services/appStateBackupFile.d.ts.map +1 -0
  74. package/build/services/appStateBackupFile.js +135 -0
  75. package/build/services/appStateBackupFile.js.map +1 -0
  76. package/build/services/controllers.d.ts.map +1 -1
  77. package/build/services/controllers.js +11 -3
  78. package/build/services/controllers.js.map +1 -1
  79. package/build/services/effectiveCapabilities.js +3 -0
  80. package/build/services/effectiveCapabilities.js.map +1 -1
  81. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  82. package/build/services/pages/pageEligibility.js +8 -1
  83. package/build/services/pages/pageEligibility.js.map +1 -1
  84. package/build/services/settingsBridge.d.ts.map +1 -1
  85. package/build/services/settingsBridge.js +1 -0
  86. package/build/services/settingsBridge.js.map +1 -1
  87. package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
  88. package/build/services/watch/WatchLink.d.ts.map +1 -0
  89. package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
  90. package/build/services/watch/WatchLink.js.map +1 -0
  91. package/build/stores/apps.d.ts.map +1 -1
  92. package/build/stores/apps.js +14 -2
  93. package/build/stores/apps.js.map +1 -1
  94. package/build/stores/settings.d.ts.map +1 -1
  95. package/build/stores/settings.js +53 -3
  96. package/build/stores/settings.js.map +1 -1
  97. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  98. package/build/types/capabilities/even-realities-g1.js +3 -0
  99. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  100. package/build/types/capabilities/memomind-one.d.ts +3 -0
  101. package/build/types/capabilities/memomind-one.d.ts.map +1 -0
  102. package/build/types/capabilities/memomind-one.js +41 -0
  103. package/build/types/capabilities/memomind-one.js.map +1 -0
  104. package/build/types/enums.d.ts +6 -2
  105. package/build/types/enums.d.ts.map +1 -1
  106. package/build/types/enums.js +4 -0
  107. package/build/types/enums.js.map +1 -1
  108. package/build/types/hardware.d.ts +3 -1
  109. package/build/types/hardware.d.ts.map +1 -1
  110. package/build/types/hardware.js +3 -1
  111. package/build/types/hardware.js.map +1 -1
  112. package/build/utils/display/index.d.ts +2 -1
  113. package/build/utils/display/index.d.ts.map +1 -1
  114. package/build/utils/display/index.js +3 -2
  115. package/build/utils/display/index.js.map +1 -1
  116. package/build/utils/display/profiles/index.d.ts +2 -1
  117. package/build/utils/display/profiles/index.d.ts.map +1 -1
  118. package/build/utils/display/profiles/index.js +3 -2
  119. package/build/utils/display/profiles/index.js.map +1 -1
  120. package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
  121. package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
  122. package/build/utils/display/profiles/rayneo-io.js +1 -1
  123. package/build/utils/display/profiles/rayneo-io.js.map +1 -1
  124. package/build/utils/display/scene/index.d.ts +1 -1
  125. package/build/utils/display/scene/index.d.ts.map +1 -1
  126. package/build/utils/display/scene/index.js +1 -1
  127. package/build/utils/display/scene/index.js.map +1 -1
  128. package/build/utils/display/scene/process.d.ts +6 -0
  129. package/build/utils/display/scene/process.d.ts.map +1 -1
  130. package/build/utils/display/scene/process.js +22 -7
  131. package/build/utils/display/scene/process.js.map +1 -1
  132. package/build/utils/miniappGlobals.d.ts +13 -17
  133. package/build/utils/miniappGlobals.d.ts.map +1 -1
  134. package/build/utils/miniappGlobals.js +24 -56
  135. package/build/utils/miniappGlobals.js.map +1 -1
  136. package/package.json +7 -7
  137. package/src/engine.ts +7 -4
  138. package/src/facades/backup.ts +28 -0
  139. package/src/facades/glasses.ts +5 -5
  140. package/src/generated/releaseMetadata.ts +4 -4
  141. package/src/index.ts +1 -0
  142. package/src/services/AppRegistry.ts +36 -9
  143. package/src/services/AppStateBackup.ts +283 -0
  144. package/src/services/BlobStore.ts +5 -11
  145. package/src/services/CloudClientService.ts +35 -4
  146. package/src/services/DisplayProcessor.ts +20 -8
  147. package/src/services/GlassesTouchInput.ts +6 -2
  148. package/src/services/JSCrashloopReportService.ts +5 -0
  149. package/src/services/JSRouter.ts +37 -6
  150. package/src/services/LocalMiniappRuntime.ts +55 -20
  151. package/src/services/MiniappEngine.ts +62 -27
  152. package/src/services/MiniappLauncher.ts +7 -0
  153. package/src/services/NavigationService.ts +28 -8
  154. package/src/services/PhoneCalendarService.ts +7 -0
  155. package/src/services/PhoneNotificationsSync.ts +8 -3
  156. package/src/services/SceneRenderer.ts +1 -1
  157. package/src/services/appStateBackupFile.ts +171 -0
  158. package/src/services/controllers.ts +12 -3
  159. package/src/services/effectiveCapabilities.ts +3 -0
  160. package/src/services/pages/pageEligibility.ts +9 -1
  161. package/src/services/settingsBridge.ts +1 -0
  162. package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
  163. package/src/stores/apps.ts +13 -2
  164. package/src/stores/settings.ts +60 -7
  165. package/src/types/capabilities/even-realities-g1.ts +4 -0
  166. package/src/types/capabilities/memomind-one.ts +42 -0
  167. package/src/types/enums.ts +4 -0
  168. package/src/types/hardware.ts +14 -1
  169. package/src/utils/display/index.ts +4 -2
  170. package/src/utils/display/profiles/index.ts +9 -2
  171. package/src/utils/display/profiles/rayneo-io.ts +6 -1
  172. package/src/utils/display/scene/index.ts +1 -1
  173. package/src/utils/display/scene/process.ts +22 -7
  174. package/src/utils/miniappGlobals.ts +32 -64
  175. package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
  176. package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
@@ -658,7 +658,8 @@ class AppRegistry {
658
658
  }
659
659
  }
660
660
 
661
- public async getActiveVersion(packageName: string): Promise<string> {
661
+ /** Undefined when no bundle is on disk (offline built-ins, HTTP-direct dev apps). */
662
+ public async getActiveVersion(packageName: string): Promise<string | undefined> {
662
663
  let versions = this.getInstalledVersions(packageName)
663
664
  // Treat MMKV as a hint, not authority. A stored version may have been
664
665
  // GC'd off disk without this pointer being updated.
@@ -677,8 +678,10 @@ class AppRegistry {
677
678
  }
678
679
  versions = versions.filter((v) => semver.valid(v))
679
680
  versions.sort((a, b) => semver.rcompare(a, b))
680
- this.setActiveVersion(packageName, versions[0])
681
- return versions[0]
681
+ const latest = versions[0] as string | undefined
682
+ // Never persist undefined: MMKV.set throws on it.
683
+ if (latest) this.setActiveVersion(packageName, latest)
684
+ return latest
682
685
  }
683
686
 
684
687
  public setActiveVersion(packageName: string, version: string): Result<void, Error> {
@@ -727,7 +730,13 @@ class AppRegistry {
727
730
  const installed = diskApps.filter(
728
731
  (app) => !offlinePackages.has(app.packageName) && !devPackages.has(app.packageName),
729
732
  )
730
- return [...installed, ...dev, ...offline]
733
+ // The last switcher-card capture comes back from disk, so a relaunched
734
+ // app's card is not blank until it is minimized again.
735
+ const withScreenshot = (app: ClientApp): ClientApp => ({
736
+ ...app,
737
+ screenshot: app.screenshot ?? getLocalAppScreenshot(app.packageName),
738
+ })
739
+ return [...installed.map(withScreenshot), ...dev.map(withScreenshot), ...offline]
731
740
  }
732
741
 
733
742
  public async getInstalledMiniapps(): Promise<ClientApp[]> {
@@ -747,6 +756,8 @@ class AppRegistry {
747
756
  const out: ClientApp[] = []
748
757
  for (const lmaInfo of installedInfo) {
749
758
  const versionString = await this.getActiveVersion(lmaInfo.packageName)
759
+ // A package dir with no usable bundle (e.g. only a half-written version) has nothing to run.
760
+ if (!versionString) continue
750
761
  const versionInfo = lmaInfo.versions[versionString]
751
762
 
752
763
  const manifest = this.getMiniappManifest(lmaInfo.packageName, versionString) as {
@@ -963,6 +974,19 @@ export function saveWidgetExpandChoice(packageName: string, widgetId: string, ex
963
974
  storage.save(`${packageName}_widget_expand_${widgetId}`, expandOnTap)
964
975
  }
965
976
 
977
+ /**
978
+ * Where a screenshot captured under an earlier install now lives: iOS gives
979
+ * every install a new container, so a stored absolute file URI goes stale
980
+ * while the file itself is still in the documents directory.
981
+ */
982
+ function rerootDocumentUri(uri: string): string | null {
983
+ const marker = "/Documents/"
984
+ const at = uri.indexOf(marker)
985
+ if (at < 0) return null
986
+ const rerooted = new File(Paths.document, uri.slice(at + marker.length)).uri
987
+ return rerooted === uri ? null : rerooted
988
+ }
989
+
966
990
  export function getLocalAppScreenshot(packageName: string): string | undefined {
967
991
  const key = `${packageName}_screenshot`
968
992
  const res = storage.load<string>(key)
@@ -971,14 +995,17 @@ export function getLocalAppScreenshot(packageName: string): string | undefined {
971
995
  const uri = res.value
972
996
  if (uri.startsWith("file://")) {
973
997
  try {
974
- if (!new File(uri).exists) {
975
- storage.remove(key)
976
- return undefined
998
+ if (new File(uri).exists) return uri
999
+ const rerooted = rerootDocumentUri(uri)
1000
+ if (rerooted && new File(rerooted).exists) {
1001
+ storage.save(key, rerooted)
1002
+ return rerooted
977
1003
  }
978
1004
  } catch {
979
- storage.remove(key)
980
- return undefined
1005
+ // fall through: the stored uri is unusable
981
1006
  }
1007
+ storage.remove(key)
1008
+ return undefined
982
1009
  }
983
1010
 
984
1011
  return uri
@@ -0,0 +1,283 @@
1
+ /**
2
+ * App state backups (Settings > Profile > Backup): one JSON file carrying the
3
+ * system settings, every miniapp's SimpleStorage, the installed miniapp list
4
+ * with each app's home flags, and the memory timeline.
5
+ *
6
+ * It exists so a logout or reinstall doesn't mean signing back into every
7
+ * miniapp, which is why the file holds live credentials (miniapp tokens,
8
+ * provider API keys) as plain JSON. Pairing identity, cloud endpoints, and
9
+ * consent/login state stay out: they belong to the install and the account,
10
+ * not to the user's preferences.
11
+ *
12
+ * Storage keys are written without their user segment, so a backup restores
13
+ * into whatever namespace the phone has now (account or `local_`). Restore
14
+ * overwrites key by key and leaves keys the backup doesn't carry alone.
15
+ *
16
+ * Pure: the MMKV view, settings registry, and memory snapshot are passed in by
17
+ * appStateBackupFile.ts.
18
+ */
19
+
20
+ import type {MemoryBackup} from "@glassly/miniapp"
21
+
22
+ /** Key-value surface (MMKV in production, values already JSON-decoded). */
23
+ export interface AppStateBackupBackend {
24
+ get(key: string): unknown | null
25
+ set(key: string, value: unknown): void
26
+ keys(): string[]
27
+ }
28
+
29
+ /** The fields of a SETTINGS descriptor that decide whether it is backed up. */
30
+ export interface BackupSettingDescriptor {
31
+ key: string
32
+ persist: boolean
33
+ writable: boolean
34
+ nativeAuthoritative?: true
35
+ resetOnBuildEnvChange?: true
36
+ indexer?: (key: string) => string
37
+ }
38
+
39
+ export interface AppStateBackupMiniapp {
40
+ /** Set when the miniapp was installed at export. */
41
+ installed?: {version: string; source?: string}
42
+ /** SimpleStorage key → stored value. */
43
+ storage: Record<string, unknown>
44
+ /** Home flags by suffix: `hidden`, `always_on`, `widget_expand_<widgetId>`. */
45
+ prefs: Record<string, unknown>
46
+ }
47
+
48
+ export interface AppStateBackup {
49
+ format: "glassly-app-state"
50
+ version: 1
51
+ /** Epoch ms the backup was written. */
52
+ exportedAt: number
53
+ /** Stored setting values; indexed settings keep their `key:index` storage key. */
54
+ settings: Record<string, unknown>
55
+ miniapps: Record<string, AppStateBackupMiniapp>
56
+ memory: MemoryBackup | null
57
+ }
58
+
59
+ /** Thrown for a file that isn't an app state backup this build can read. */
60
+ export class AppStateBackupError extends Error {
61
+ constructor(message: string) {
62
+ super(message)
63
+ this.name = "AppStateBackupError"
64
+ }
65
+ }
66
+
67
+ const STORAGE_ROOT = "localstorage_"
68
+ const PREF_SUFFIXES = ["hidden", "always_on"] as const
69
+ const WIDGET_EXPAND_PREFIX = "widget_expand_"
70
+ // Same rule as the miniapp.json schema; keeps a hand-edited file from writing keys no app reads.
71
+ const PACKAGE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/
72
+
73
+ /**
74
+ * Persisted, writable settings that are still not the user's to carry between
75
+ * installs: account and consent state, the paired devices, and caches.
76
+ */
77
+ const EXCLUDED_SETTINGS = new Set([
78
+ "auth_email",
79
+ "disable_cloud",
80
+ "ai_data_consent_version",
81
+ "ai_data_consent_agreed",
82
+ "login_skipped",
83
+ "has_paired_glasses",
84
+ "keyboard_name",
85
+ "cached_required_version",
86
+ "offline_camera_running",
87
+ "offline_translation_running",
88
+ ])
89
+
90
+ export function isBackupSetting(setting: BackupSettingDescriptor): boolean {
91
+ return (
92
+ setting.persist &&
93
+ setting.writable &&
94
+ !setting.nativeAuthoritative &&
95
+ !setting.resetOnBuildEnvChange &&
96
+ !EXCLUDED_SETTINGS.has(setting.key)
97
+ )
98
+ }
99
+
100
+ export interface CollectAppStateInput {
101
+ backend: AppStateBackupBackend
102
+ userId: string
103
+ settings: BackupSettingDescriptor[]
104
+ installed: Array<{packageName: string; version: string; source?: string}>
105
+ memory: MemoryBackup | null
106
+ now: number
107
+ }
108
+
109
+ export function collectAppStateBackup(input: CollectAppStateInput): AppStateBackup {
110
+ const {backend, settings} = input
111
+ const keys = backend.keys()
112
+
113
+ const settingValues: Record<string, unknown> = {}
114
+ for (const setting of settings) {
115
+ if (!isBackupSetting(setting)) continue
116
+ if (setting.indexer) {
117
+ for (const key of keys) {
118
+ if (!key.startsWith(`${setting.key}:`)) continue
119
+ const value = backend.get(key)
120
+ if (value != null) settingValues[key] = value
121
+ }
122
+ continue
123
+ }
124
+ const value = backend.get(setting.key)
125
+ if (value != null) settingValues[setting.key] = value
126
+ }
127
+
128
+ const miniapps: Record<string, AppStateBackupMiniapp> = {}
129
+ const entry = (packageName: string) => (miniapps[packageName] ??= {storage: {}, prefs: {}})
130
+
131
+ for (const app of input.installed) {
132
+ const record = entry(app.packageName)
133
+ record.installed = app.source ? {version: app.version, source: app.source} : {version: app.version}
134
+ for (const suffix of PREF_SUFFIXES) {
135
+ const value = backend.get(`${app.packageName}_${suffix}`)
136
+ if (value != null) record.prefs[suffix] = value
137
+ }
138
+ const expandPrefix = `${app.packageName}_${WIDGET_EXPAND_PREFIX}`
139
+ for (const key of keys) {
140
+ if (!key.startsWith(expandPrefix)) continue
141
+ const value = backend.get(key)
142
+ if (value != null) record.prefs[key.slice(app.packageName.length + 1)] = value
143
+ }
144
+ }
145
+
146
+ // Longest first, so a package whose name extends another's still claims its own keys.
147
+ const known = input.installed.map((app) => app.packageName).sort((a, b) => b.length - a.length)
148
+ const prefix = `${STORAGE_ROOT}${input.userId}_`
149
+ for (const key of keys) {
150
+ if (!key.startsWith(prefix)) continue
151
+ const split = splitStorageKey(key.slice(prefix.length), known)
152
+ if (!split) continue
153
+ const value = backend.get(key)
154
+ if (value != null) entry(split.packageName).storage[split.key] = value
155
+ }
156
+
157
+ return {
158
+ format: "glassly-app-state",
159
+ version: 1,
160
+ exportedAt: input.now,
161
+ settings: settingValues,
162
+ miniapps,
163
+ memory: input.memory,
164
+ }
165
+ }
166
+
167
+ /** `{packageName}_{key}` → its parts. Package names may contain `_`, so known names win over the first `_`. */
168
+ function splitStorageKey(rest: string, known: string[]): {packageName: string; key: string} | null {
169
+ const packageName = known.find((name) => rest.startsWith(`${name}_`)) ?? rest.slice(0, rest.indexOf("_"))
170
+ const key = rest.slice(packageName.length + 1)
171
+ if (!packageName || !key) return null
172
+ return {packageName, key}
173
+ }
174
+
175
+ export function parseAppStateBackup(data: unknown): AppStateBackup {
176
+ if (!isRecord(data) || data.format !== "glassly-app-state") {
177
+ throw new AppStateBackupError("Not a Glassly app state backup")
178
+ }
179
+ if (typeof data.version !== "number" || data.version > 1) {
180
+ throw new AppStateBackupError(`Unsupported app state backup version ${String(data.version)}`)
181
+ }
182
+
183
+ const settings = isRecord(data.settings) ? data.settings : {}
184
+ const miniapps: Record<string, AppStateBackupMiniapp> = {}
185
+ if (isRecord(data.miniapps)) {
186
+ for (const [packageName, raw] of Object.entries(data.miniapps)) {
187
+ if (!PACKAGE_NAME_RE.test(packageName) || !isRecord(raw)) continue
188
+ const record: AppStateBackupMiniapp = {
189
+ storage: isRecord(raw.storage) ? raw.storage : {},
190
+ prefs: isRecord(raw.prefs) ? raw.prefs : {},
191
+ }
192
+ if (isRecord(raw.installed) && typeof raw.installed.version === "string") {
193
+ record.installed = {version: raw.installed.version}
194
+ if (typeof raw.installed.source === "string") record.installed.source = raw.installed.source
195
+ }
196
+ miniapps[packageName] = record
197
+ }
198
+ }
199
+
200
+ let memory: MemoryBackup | null = null
201
+ if (data.memory != null) {
202
+ if (!isRecord(data.memory) || data.memory.format !== "glassly-memory") {
203
+ throw new AppStateBackupError("The backup's memory section is not a memory backup")
204
+ }
205
+ memory = data.memory as unknown as MemoryBackup
206
+ }
207
+
208
+ return {format: "glassly-app-state", version: 1, exportedAt: Number(data.exportedAt) || 0, settings, miniapps, memory}
209
+ }
210
+
211
+ /**
212
+ * The backup's settings this build will restore, split by how they're written:
213
+ * `plain` through the settings store, `indexed` (`key:index`) straight to storage.
214
+ * Keys this build doesn't know, or doesn't back up, are dropped.
215
+ */
216
+ export function restorableSettings(
217
+ backup: AppStateBackup,
218
+ settings: BackupSettingDescriptor[],
219
+ ): {plain: Record<string, unknown>; indexed: Record<string, unknown>} {
220
+ const byKey = new Map(settings.map((setting) => [setting.key, setting]))
221
+ const plain: Record<string, unknown> = {}
222
+ const indexed: Record<string, unknown> = {}
223
+ for (const [key, value] of Object.entries(backup.settings)) {
224
+ if (value == null) continue
225
+ const colon = key.indexOf(":")
226
+ const setting = byKey.get(colon < 0 ? key : key.slice(0, colon))
227
+ if (!setting || !isBackupSetting(setting)) continue
228
+ if (colon < 0 && !setting.indexer) plain[key] = value
229
+ else if (colon > 0 && setting.indexer) indexed[key] = value
230
+ }
231
+ return {plain, indexed}
232
+ }
233
+
234
+ /** Miniapps the backup has storage for. */
235
+ export function miniappsWithStorage(backup: AppStateBackup): string[] {
236
+ return Object.entries(backup.miniapps)
237
+ .filter(([, record]) => Object.entries(record.storage).some(([key, value]) => key && value != null))
238
+ .map(([packageName]) => packageName)
239
+ }
240
+
241
+ /** Write every miniapp's storage into `userId`'s namespace. Returns the packages that got keys. */
242
+ export function restoreMiniappStorage(
243
+ backend: AppStateBackupBackend,
244
+ userId: string,
245
+ backup: AppStateBackup,
246
+ ): {packages: string[]; keys: number} {
247
+ const packages: string[] = []
248
+ let keys = 0
249
+ for (const [packageName, record] of Object.entries(backup.miniapps)) {
250
+ let wrote = 0
251
+ for (const [key, value] of Object.entries(record.storage)) {
252
+ if (!key || value == null) continue
253
+ backend.set(`${STORAGE_ROOT}${userId}_${packageName}_${key}`, value)
254
+ wrote++
255
+ }
256
+ if (wrote > 0) packages.push(packageName)
257
+ keys += wrote
258
+ }
259
+ return {packages, keys}
260
+ }
261
+
262
+ export interface MiniappPrefs {
263
+ hidden?: boolean
264
+ alwaysOn?: boolean
265
+ widgetExpand: Record<string, boolean>
266
+ }
267
+
268
+ /** A miniapp's home flags from the backup, with anything that isn't a boolean dropped. */
269
+ export function miniappPrefs(record: AppStateBackupMiniapp): MiniappPrefs {
270
+ const prefs: MiniappPrefs = {widgetExpand: {}}
271
+ if (typeof record.prefs.hidden === "boolean") prefs.hidden = record.prefs.hidden
272
+ if (typeof record.prefs.always_on === "boolean") prefs.alwaysOn = record.prefs.always_on
273
+ for (const [key, value] of Object.entries(record.prefs)) {
274
+ if (!key.startsWith(WIDGET_EXPAND_PREFIX) || typeof value !== "boolean") continue
275
+ const widgetId = key.slice(WIDGET_EXPAND_PREFIX.length)
276
+ if (widgetId) prefs.widgetExpand[widgetId] = value
277
+ }
278
+ return prefs
279
+ }
280
+
281
+ function isRecord(value: unknown): value is Record<string, unknown> {
282
+ return value != null && typeof value === "object" && !Array.isArray(value)
283
+ }
@@ -504,20 +504,14 @@ export class BlobStore {
504
504
  const key = (typeof payload.key === "string" && payload.key) || this.makeId()
505
505
  const mimeFilter = payload.mimeType as string | undefined
506
506
 
507
- let picked: File | File[] | null = null
508
- try {
509
- picked = await File.pickFileAsync(undefined, mimeFilter)
510
- } catch (err) {
511
- // Picker dismissed / cancelled (or unavailable) → resolve to null, not an error.
512
- console.log(`${LOG_TAG}: blob import picker dismissed`, err)
513
- this.hooks.sendResult(packageName, requestId, true, null)
514
- return
515
- }
516
- const file = Array.isArray(picked) ? picked[0] : picked
517
- if (!file) {
507
+ // The options form resolves {canceled, result} and never throws; the deprecated
508
+ // (initialUri, mimeType) form resolves a bare File only when a mime type is passed.
509
+ const picked = await File.pickFileAsync(mimeFilter ? {mimeTypes: mimeFilter} : {})
510
+ if (picked.canceled) {
518
511
  this.hooks.sendResult(packageName, requestId, true, null)
519
512
  return
520
513
  }
514
+ const file = picked.result
521
515
  try {
522
516
  const size = file.size ?? 0
523
517
  if (this.overQuota(packageName, key, size)) {
@@ -21,7 +21,7 @@ import type {
21
21
  StoreListing,
22
22
  } from "@glassly/cloud-client/react-native"
23
23
  import type {SubjectTokenType} from "@glassly/cloud-client"
24
- import {Platform} from "react-native"
24
+ import {AppState, Platform} from "react-native"
25
25
  import type {AudioSubscription, QuotaStatusPayload, TranscriptionData, TranslationData} from "@glassly/cloud-protocol"
26
26
 
27
27
  import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
@@ -70,6 +70,8 @@ const LOCAL_AUTH_PORT = 3002
70
70
 
71
71
  let client: CloudClient | null = null
72
72
  let connected = false
73
+ /** The cloud closed us for a newer socket; the client stays down until reconnect(). */
74
+ let superseded = false
73
75
  let persistentFailureTimer: ReturnType<typeof BgTimer.setTimeout> | null = null
74
76
  let persistentFailureNotified = false
75
77
  let audioSubscriptions: AudioSubscription[] = []
@@ -84,6 +86,7 @@ let authStateUnsubscribe: (() => void) | null = null
84
86
  let localDevRuntimeToken: {runtimeUrl: string; token: string; expiresAtMs: number} | null = null
85
87
  let lc3FrameSizeUnsubscribe: (() => void) | null = null
86
88
  let disableCloudUnsubscribe: (() => void) | null = null
89
+ let foregroundUnsubscribe: (() => void) | null = null
87
90
  let coreTokenSyncPromise: Promise<string> | null = null
88
91
  /**
89
92
  * A device-local namespace (`local_…`) whose data still needs to be adopted
@@ -511,6 +514,26 @@ function stopLc3FrameSizeWatcher(): void {
511
514
  lc3FrameSizeUnsubscribe = null
512
515
  }
513
516
 
517
+ /**
518
+ * Reclaim a superseded session only on foreground: the user picked this phone
519
+ * up, and a reclaim on any other signal would fight the newer socket.
520
+ */
521
+ function startForegroundWatcher(): void {
522
+ if (foregroundUnsubscribe) return
523
+
524
+ const subscription = AppState.addEventListener("change", (state) => {
525
+ if (state !== "active" || !superseded || !client) return
526
+ console.log(`${LOG_TAG}: foregrounded after supersede; reconnecting runtime`)
527
+ cloudClientService.reconnect()
528
+ })
529
+ foregroundUnsubscribe = () => subscription.remove()
530
+ }
531
+
532
+ function stopForegroundWatcher(): void {
533
+ foregroundUnsubscribe?.()
534
+ foregroundUnsubscribe = null
535
+ }
536
+
514
537
  /**
515
538
  * Apply a mid-session flip of the Privacy toggle without a restart: turning it
516
539
  * on tears the live client down, turning it back off reconnects. Installed by
@@ -596,6 +619,7 @@ function construct(): void {
596
619
  c.runtime.onConnected(() => {
597
620
  if (c !== client) return
598
621
  connected = true
622
+ superseded = false
599
623
  console.log(`${LOG_TAG}: runtime connected`)
600
624
  console.log(`${LOG_TAG}: debug: ws-session-debug connected`)
601
625
  // Cloud is up — cancel any pending persistent-failure alarm and re-arm for the
@@ -613,10 +637,9 @@ function construct(): void {
613
637
  c.runtime.onDisconnected((info) => {
614
638
  if (c !== client) return
615
639
  connected = false
640
+ superseded = /superseded by newer session/i.test(info.reason)
616
641
  console.log(`${LOG_TAG}: runtime disconnected (${info.reason})`)
617
- console.log(
618
- `${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${/superseded by newer session/i.test(info.reason)}`,
619
- )
642
+ console.log(`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${superseded}`)
620
643
  // Arm the persistent-failure alarm once; if we're still down when it fires, raise
621
644
  // the notification. A reconnect within the window cancels it (onConnected above).
622
645
  if (!persistentFailureTimer && !persistentFailureNotified) {
@@ -659,6 +682,7 @@ function construct(): void {
659
682
  )
660
683
 
661
684
  startLc3FrameSizeWatcher()
685
+ startForegroundWatcher()
662
686
  }
663
687
 
664
688
  /**
@@ -720,6 +744,7 @@ export const cloudClientService = {
720
744
  client = null
721
745
  localDevRuntimeToken = null
722
746
  connected = false
747
+ superseded = false
723
748
  resetRuntimeStatus()
724
749
  if (wasConnected) notifyConnectionListeners(false)
725
750
 
@@ -835,11 +860,13 @@ export const cloudClientService = {
835
860
  console.warn(`${LOG_TAG}: stop close() failed: ${(err as Error)?.message ?? err}`)
836
861
  }
837
862
  stopLc3FrameSizeWatcher()
863
+ stopForegroundWatcher()
838
864
  stopAuthWatch()
839
865
  clearRuntimeEventSubscriptions()
840
866
  clearPersistentFailureAlarm()
841
867
  const wasConnected = connected
842
868
  client = null
869
+ superseded = false
843
870
  // stop() is part of the logout lifecycle and runs before the host emits
844
871
  // SIGNED_OUT. Remove the persisted namespace owner here so a later account
845
872
  // can never inherit it. A force-stop/process kill does not call stop(), so
@@ -855,6 +882,10 @@ export const cloudClientService = {
855
882
  shutdown(): void {
856
883
  this.stop()
857
884
  stopDisableCloudWatcher()
885
+ // The next engine.start() is configured afresh; an override left by a
886
+ // reconnect() in this process would otherwise outrank the new config
887
+ // and keep the client on the previous backend.
888
+ endpointsOverride = null
858
889
  },
859
890
 
860
891
  /** Device-side managed photo: presign now, deliver bytes, await ready. */
@@ -20,6 +20,8 @@ import {
20
20
  Z100_PROFILE,
21
21
  NEX_PROFILE,
22
22
  RAYNEO_IO_PROFILE,
23
+ MEMOMIND_ONE_PROFILE,
24
+ RAYNEO_IO_CFW_PROFILE,
23
25
  TextMeasurer,
24
26
  TextWrapper,
25
27
  ColumnComposer,
@@ -42,12 +44,7 @@ import {isGlassesConnected} from "./GlassesReadiness"
42
44
  * Display event types that we process
43
45
  */
44
46
  export type DisplayLayoutType =
45
- | "text_wall"
46
- | "text_line"
47
- | "text_rows"
48
- | "reference_card"
49
- | "double_text_wall"
50
- | "bitmap_view"
47
+ "text_wall" | "text_line" | "text_rows" | "reference_card" | "double_text_wall" | "bitmap_view"
51
48
 
52
49
  /**
53
50
  * Raw display event from the cloud/WebSocket
@@ -208,6 +205,16 @@ const DEVICE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
208
205
  [DeviceTypes.NEX]: NEX_PROFILE,
209
206
  [DeviceTypes.MACH1]: Z100_PROFILE, // Mach1 uses same hardware as Vuzix Z100
210
207
  [DeviceTypes.RAYNEO_IO]: RAYNEO_IO_PROFILE,
208
+ [DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
209
+ }
210
+
211
+ /**
212
+ * Scene text profiles for models whose Glassly CFW draws text with a font of
213
+ * its own, used while the CFW publishes a scene canvas.
214
+ */
215
+ const CFW_SCENE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
216
+ [DeviceTypes.RAYNEO_IO]: RAYNEO_IO_CFW_PROFILE,
217
+ [DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
211
218
  }
212
219
 
213
220
  // =============================================================================
@@ -275,8 +282,7 @@ export class DisplayProcessor {
275
282
  // Seed the device profile from the saved default wearable, read off the engine
276
283
  // settings store directly (was a host settings hook).
277
284
  const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
278
- | string
279
- | undefined
285
+ string | undefined
280
286
  if (defaultWearable) {
281
287
  this.setDeviceModel(defaultWearable)
282
288
  console.log(`DISPLAY_PROCESSOR: Initialized DisplayProcessor with default wearable: ${defaultWearable}`)
@@ -380,6 +386,12 @@ export class DisplayProcessor {
380
386
  return this.profile
381
387
  }
382
388
 
389
+ /** Profile scene text is wrapped with: the CFW's own while its scene page is up, else the model's. */
390
+ public getSceneProfile(): DisplayProfile {
391
+ const cfw = CFW_SCENE_PROFILES[this.deviceModel]
392
+ return cfw && (useGlassesStore.getState().sceneCanvasWidth ?? 0) > 0 ? cfw : this.profile
393
+ }
394
+
383
395
  // ===========================================================================
384
396
  // Display Event Processing
385
397
  // ===========================================================================
@@ -110,7 +110,7 @@ function routeTouchEvent(event: TouchEvent): void {
110
110
  const doubleTap = gesture === "double_tap"
111
111
  // The Neural Band's middle finger (index 1) shares the tap names. Either
112
112
  // finger leaves the app on screen; the firmware dashboard is the index finger's.
113
- const osDoubleTap = doubleTap && event.index !== 1
113
+ const osDoubleTap = doubleTap && !(event.source === 3 && event.index === 1)
114
114
  const osHeadUp = gesture === "head_up"
115
115
 
116
116
  if (doubleTap) {
@@ -156,7 +156,11 @@ function routeTouchEvent(event: TouchEvent): void {
156
156
  // pagesEligible: connected Glassly CFW glasses, whose firmware leaves the
157
157
  // dashboard to the phone; other firmware opens its own on a lift.
158
158
  const opensNativeDashboard = osDoubleTap || (osHeadUp && pagesEligible())
159
- if (opensNativeDashboard && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
159
+ if (
160
+ opensNativeDashboard &&
161
+ !nativeNavigationService.isActive &&
162
+ isGlassesConnected(useGlassesStore.getState().connection)
163
+ ) {
160
164
  useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
161
165
  void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
162
166
  console.error("DeviceEventRouter: showDashboard failed:", err)
@@ -19,6 +19,11 @@ export async function submitJSCrashloopReport(params: {
19
19
  }): Promise<AutomaticReportSubmissionStatus> {
20
20
  const {packageName, reason, lastLogLines = []} = params
21
21
  const app = useAppStatusStore.getState().apps.find((a) => a.packageName === packageName)
22
+ // A dev miniapp's crash is its developer's to fix; JSRouter forwards it to their dev server.
23
+ if (app?.isMiniappDev) {
24
+ console.log(`[${LOG_TAG}] Not reporting a dev miniapp crashloop:`, packageName)
25
+ return {status: "skipped", reason: "dev miniapp"}
26
+ }
22
27
  const appName = app?.name ?? packageName
23
28
  const result = await submitAutomaticReport({
24
29
  kind: "automatic",