@glassly/engine 0.3.7-dev.888 → 0.3.7-dev.891
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.
- package/build/engine.d.ts +6 -0
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +3 -0
- package/build/engine.js.map +1 -1
- package/build/facades/backup.d.ts +14 -0
- package/build/facades/backup.d.ts.map +1 -0
- package/build/facades/backup.js +22 -0
- package/build/facades/backup.js.map +1 -0
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppStateBackup.d.ts +98 -0
- package/build/services/AppStateBackup.d.ts.map +1 -0
- package/build/services/AppStateBackup.js +224 -0
- package/build/services/AppStateBackup.js.map +1 -0
- package/build/services/CloudClientService.d.ts +1 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +18 -4
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappEngine.d.ts +9 -0
- package/build/services/MiniappEngine.d.ts.map +1 -1
- package/build/services/MiniappEngine.js +60 -27
- package/build/services/MiniappEngine.js.map +1 -1
- package/build/services/NavigationService.d.ts +1 -0
- package/build/services/NavigationService.d.ts.map +1 -1
- package/build/services/NavigationService.js +20 -6
- package/build/services/NavigationService.js.map +1 -1
- package/build/services/appStateBackupFile.d.ts +32 -0
- package/build/services/appStateBackupFile.d.ts.map +1 -0
- package/build/services/appStateBackupFile.js +135 -0
- package/build/services/appStateBackupFile.js.map +1 -0
- package/package.json +7 -7
- package/src/engine.ts +3 -0
- package/src/facades/backup.ts +28 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +1 -0
- package/src/services/AppStateBackup.ts +283 -0
- package/src/services/LocalMiniappRuntime.ts +21 -4
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/NavigationService.ts +28 -8
- package/src/services/appStateBackupFile.ts +171 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App state backup files: the share-sheet export and file-picker restore
|
|
3
|
+
* behind Settings > Profile > Backup. What goes in the file is decided in
|
|
4
|
+
* AppStateBackup.ts; this side reads and writes the live stores.
|
|
5
|
+
*/
|
|
6
|
+
import { File, Paths } from "expo-file-system";
|
|
7
|
+
import { Platform } from "react-native";
|
|
8
|
+
import Share from "react-native-share";
|
|
9
|
+
import { SETTINGS, useSettingsStore } from "../stores/settings";
|
|
10
|
+
import { useAppStatusStore } from "../stores/apps";
|
|
11
|
+
import { storage } from "../utils/storage/storage";
|
|
12
|
+
import appRegistry, { saveAlwaysOnOverride, saveWidgetExpandChoice } from "./AppRegistry";
|
|
13
|
+
import { AppStateBackupError, collectAppStateBackup, miniappPrefs, miniappsWithStorage, parseAppStateBackup, restorableSettings, restoreMiniappStorage, } from "./AppStateBackup";
|
|
14
|
+
import { cloudClientService } from "./CloudClientService";
|
|
15
|
+
import { memoryStore } from "./memoryStoreInstance";
|
|
16
|
+
import { respawnMiniapp, runningMiniappPackages, stopMiniapp } from "./MiniappEngine";
|
|
17
|
+
const PICK_MIME_TYPES = Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"];
|
|
18
|
+
const backend = {
|
|
19
|
+
get(key) {
|
|
20
|
+
const result = storage.load(key);
|
|
21
|
+
return result.is_ok() ? result.value : null;
|
|
22
|
+
},
|
|
23
|
+
set(key, value) {
|
|
24
|
+
const result = storage.save(key, value);
|
|
25
|
+
if (result.is_error())
|
|
26
|
+
throw result.error;
|
|
27
|
+
},
|
|
28
|
+
keys: () => storage.getAllKeys(),
|
|
29
|
+
};
|
|
30
|
+
export async function exportAppStateFile() {
|
|
31
|
+
const userId = await cloudClientService.resolveUserId();
|
|
32
|
+
const installed = [];
|
|
33
|
+
for (const app of await appRegistry.getInstalledMiniapps()) {
|
|
34
|
+
const version = await appRegistry.getActiveVersion(app.packageName);
|
|
35
|
+
if (!version)
|
|
36
|
+
continue;
|
|
37
|
+
installed.push({
|
|
38
|
+
packageName: app.packageName,
|
|
39
|
+
version,
|
|
40
|
+
source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const backup = collectAppStateBackup({
|
|
44
|
+
backend,
|
|
45
|
+
userId,
|
|
46
|
+
settings: Object.values(SETTINGS),
|
|
47
|
+
installed,
|
|
48
|
+
memory: await memoryStore.exportBackup(),
|
|
49
|
+
now: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`;
|
|
52
|
+
const file = new File(Paths.cache, filename);
|
|
53
|
+
file.writeSync(JSON.stringify(backup));
|
|
54
|
+
try {
|
|
55
|
+
await Share.open({ url: file.uri, type: "application/json", filename, title: "Glassly backup" });
|
|
56
|
+
return { success: true };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// react-native-share rejects when the user dismisses the sheet.
|
|
60
|
+
if (error?.message?.includes("User did not share"))
|
|
61
|
+
return { success: false, cancelled: true };
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Resolves null when the user cancels the picker. */
|
|
66
|
+
export async function importAppStateFile(opts = {}) {
|
|
67
|
+
const picked = await File.pickFileAsync({ mimeTypes: PICK_MIME_TYPES });
|
|
68
|
+
if (picked.canceled)
|
|
69
|
+
return null;
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(await picked.result.text());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new AppStateBackupError("Not a Glassly app state backup");
|
|
76
|
+
}
|
|
77
|
+
const backup = parseAppStateBackup(data);
|
|
78
|
+
const userId = await cloudClientService.resolveUserId();
|
|
79
|
+
// First, so a memory section this build rejects fails the restore before anything is written.
|
|
80
|
+
const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null;
|
|
81
|
+
const { plain, indexed } = restorableSettings(backup, Object.values(SETTINGS));
|
|
82
|
+
const written = await useSettingsStore.getState().setManyLocally(plain);
|
|
83
|
+
if (written.is_error())
|
|
84
|
+
throw written.error;
|
|
85
|
+
// setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.
|
|
86
|
+
for (const [key, value] of Object.entries(indexed))
|
|
87
|
+
backend.set(key, value);
|
|
88
|
+
if (Object.keys(indexed).length > 0) {
|
|
89
|
+
useSettingsStore.setState((state) => ({ settings: { ...state.settings, ...indexed } }));
|
|
90
|
+
}
|
|
91
|
+
// A running miniapp holds its old values in memory and could write them back over the
|
|
92
|
+
// restore, so the ones getting storage stop now and start again at the end.
|
|
93
|
+
const running = new Set(runningMiniappPackages());
|
|
94
|
+
const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName));
|
|
95
|
+
for (const packageName of restarting)
|
|
96
|
+
await stopMiniapp(packageName);
|
|
97
|
+
try {
|
|
98
|
+
const restored = restoreMiniappStorage(backend, userId, backup);
|
|
99
|
+
const apps = useAppStatusStore.getState();
|
|
100
|
+
for (const [packageName, record] of Object.entries(backup.miniapps)) {
|
|
101
|
+
const prefs = miniappPrefs(record);
|
|
102
|
+
if (prefs.hidden !== undefined)
|
|
103
|
+
apps.setHiddenStatus(packageName, prefs.hidden);
|
|
104
|
+
if (prefs.alwaysOn !== undefined)
|
|
105
|
+
saveAlwaysOnOverride(packageName, prefs.alwaysOn);
|
|
106
|
+
for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {
|
|
107
|
+
saveWidgetExpandChoice(packageName, widgetId, expand);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
appRegistry.markRefreshNeeded();
|
|
111
|
+
await apps.refresh();
|
|
112
|
+
const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName));
|
|
113
|
+
const missing = Object.entries(backup.miniapps)
|
|
114
|
+
.filter(([packageName, record]) => record.installed && !installedNow.has(packageName))
|
|
115
|
+
.map(([packageName]) => packageName);
|
|
116
|
+
if (missing.length > 0)
|
|
117
|
+
await opts.reinstall?.(missing);
|
|
118
|
+
return {
|
|
119
|
+
settings: Object.keys(plain).length + Object.keys(indexed).length,
|
|
120
|
+
miniapps: restored.packages.length,
|
|
121
|
+
memory,
|
|
122
|
+
missing,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
for (const packageName of restarting)
|
|
127
|
+
await respawnMiniapp(packageName);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function localDate(ms) {
|
|
131
|
+
const d = new Date(ms);
|
|
132
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
133
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=appStateBackupFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appStateBackupFile.js","sourceRoot":"","sources":["../../src/services/appStateBackupFile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * App state backup files: the share-sheet export and file-picker restore\n * behind Settings > Profile > Backup. What goes in the file is decided in\n * AppStateBackup.ts; this side reads and writes the live stores.\n */\n\nimport type {MemoryImportResult} from \"@glassly/miniapp\"\nimport {File, Paths} from \"expo-file-system\"\nimport {Platform} from \"react-native\"\nimport Share from \"react-native-share\"\n\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {storage} from \"../utils/storage/storage\"\nimport appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from \"./AppRegistry\"\nimport {\n AppStateBackupError,\n collectAppStateBackup,\n miniappPrefs,\n miniappsWithStorage,\n parseAppStateBackup,\n restorableSettings,\n restoreMiniappStorage,\n type AppStateBackupBackend,\n} from \"./AppStateBackup\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport {memoryStore} from \"./memoryStoreInstance\"\nimport {respawnMiniapp, runningMiniappPackages, stopMiniapp} from \"./MiniappEngine\"\n\nexport interface AppStateExportResult {\n success: boolean\n cancelled?: boolean\n}\n\nexport interface AppStateRestoreResult {\n /** Settings written. */\n settings: number\n /** Miniapps whose storage was restored. */\n miniapps: number\n memory: MemoryImportResult | null\n /** Installed when the backup was made but not on this phone now. */\n missing: string[]\n}\n\nexport interface AppStateRestoreOptions {\n /**\n * Install what the backup had installed and this phone lacks. Runs before\n * the restored miniapps start again, so the dashboard comes back with every\n * widget provider its saved layout names: it drops unknown widgets from the\n * layout the next time it saves it.\n */\n reinstall?: (packageNames: string[]) => Promise<void>\n}\n\nconst PICK_MIME_TYPES =\n Platform.OS === \"android\" ? [\"application/json\", \"application/octet-stream\"] : [\"application/json\"]\n\nconst backend: AppStateBackupBackend = {\n get(key) {\n const result = storage.load<unknown>(key)\n return result.is_ok() ? result.value : null\n },\n set(key, value) {\n const result = storage.save(key, value)\n if (result.is_error()) throw result.error\n },\n keys: () => storage.getAllKeys(),\n}\n\nexport async function exportAppStateFile(): Promise<AppStateExportResult> {\n const userId = await cloudClientService.resolveUserId()\n const installed: Array<{packageName: string; version: string; source?: string}> = []\n for (const app of await appRegistry.getInstalledMiniapps()) {\n const version = await appRegistry.getActiveVersion(app.packageName)\n if (!version) continue\n installed.push({\n packageName: app.packageName,\n version,\n source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,\n })\n }\n const backup = collectAppStateBackup({\n backend,\n userId,\n settings: Object.values(SETTINGS),\n installed,\n memory: await memoryStore.exportBackup(),\n now: Date.now(),\n })\n\n const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`\n const file = new File(Paths.cache, filename)\n file.writeSync(JSON.stringify(backup))\n try {\n await Share.open({url: file.uri, type: \"application/json\", filename, title: \"Glassly backup\"})\n return {success: true}\n } catch (error: any) {\n // react-native-share rejects when the user dismisses the sheet.\n if (error?.message?.includes(\"User did not share\")) return {success: false, cancelled: true}\n throw error\n }\n}\n\n/** Resolves null when the user cancels the picker. */\nexport async function importAppStateFile(opts: AppStateRestoreOptions = {}): Promise<AppStateRestoreResult | null> {\n const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})\n if (picked.canceled) return null\n let data: unknown\n try {\n data = JSON.parse(await picked.result.text())\n } catch {\n throw new AppStateBackupError(\"Not a Glassly app state backup\")\n }\n const backup = parseAppStateBackup(data)\n const userId = await cloudClientService.resolveUserId()\n\n // First, so a memory section this build rejects fails the restore before anything is written.\n const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null\n\n const {plain, indexed} = restorableSettings(backup, Object.values(SETTINGS))\n const written = await useSettingsStore.getState().setManyLocally(plain)\n if (written.is_error()) throw written.error\n // setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.\n for (const [key, value] of Object.entries(indexed)) backend.set(key, value)\n if (Object.keys(indexed).length > 0) {\n useSettingsStore.setState((state) => ({settings: {...state.settings, ...indexed}}))\n }\n\n // A running miniapp holds its old values in memory and could write them back over the\n // restore, so the ones getting storage stop now and start again at the end.\n const running = new Set(runningMiniappPackages())\n const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName))\n for (const packageName of restarting) await stopMiniapp(packageName)\n\n try {\n const restored = restoreMiniappStorage(backend, userId, backup)\n\n const apps = useAppStatusStore.getState()\n for (const [packageName, record] of Object.entries(backup.miniapps)) {\n const prefs = miniappPrefs(record)\n if (prefs.hidden !== undefined) apps.setHiddenStatus(packageName, prefs.hidden)\n if (prefs.alwaysOn !== undefined) saveAlwaysOnOverride(packageName, prefs.alwaysOn)\n for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {\n saveWidgetExpandChoice(packageName, widgetId, expand)\n }\n }\n appRegistry.markRefreshNeeded()\n await apps.refresh()\n\n const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName))\n const missing = Object.entries(backup.miniapps)\n .filter(([packageName, record]) => record.installed && !installedNow.has(packageName))\n .map(([packageName]) => packageName)\n if (missing.length > 0) await opts.reinstall?.(missing)\n\n return {\n settings: Object.keys(plain).length + Object.keys(indexed).length,\n miniapps: restored.packages.length,\n memory,\n missing,\n }\n } finally {\n for (const packageName of restarting) await respawnMiniapp(packageName)\n }\n}\n\nfunction localDate(ms: number): string {\n const d = new Date(ms)\n const pad = (n: number) => String(n).padStart(2, \"0\")\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.7-dev.
|
|
3
|
+
"version": "0.3.7-dev.891",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.7-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.3.7-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.7-dev.
|
|
103
|
-
"@glassly/crust": "0.3.7-dev.
|
|
104
|
-
"@glassly/miniapp": "0.3.7-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.7-dev.891",
|
|
101
|
+
"@glassly/cloud-client": "0.3.7-dev.891",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.7-dev.891",
|
|
103
|
+
"@glassly/crust": "0.3.7-dev.891",
|
|
104
|
+
"@glassly/miniapp": "0.3.7-dev.891",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.3.2",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.7-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.3.7-dev.891",
|
|
111
111
|
"@huggingface/jinja": "^0.5.9",
|
|
112
112
|
"jsonrepair": "^3.12.0"
|
|
113
113
|
},
|
package/src/engine.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {permissions} from "./facades/permissions"
|
|
|
49
49
|
import {notifications} from "./facades/notifications"
|
|
50
50
|
import {maps} from "./facades/maps"
|
|
51
51
|
import {memory} from "./facades/memory"
|
|
52
|
+
import {backup} from "./facades/backup"
|
|
52
53
|
import {speakers} from "./facades/speakers"
|
|
53
54
|
import {providerKeys} from "./facades/providerKeys"
|
|
54
55
|
import {logBuffer} from "./utils/devLogging"
|
|
@@ -232,6 +233,8 @@ export const engine = {
|
|
|
232
233
|
maps,
|
|
233
234
|
/** Shared user-scoped memory timeline (Settings > Memory + session.memory). */
|
|
234
235
|
memory,
|
|
236
|
+
/** Whole-app backup file: settings, miniapp storage, installed list, memory (Settings > Profile). */
|
|
237
|
+
backup,
|
|
235
238
|
/** Device-local voice profiles for on-device speaker ID (Settings > Speakers). */
|
|
236
239
|
speakers,
|
|
237
240
|
/** AI/voice provider key validation — user-supplied keys (Settings > API Keys). */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* backup facade — `engine.backup`: the whole-app backup behind Settings >
|
|
3
|
+
* Profile > Backup. One JSON file carries system settings, every miniapp's
|
|
4
|
+
* storage (so their sign-ins survive a logout or reinstall), the installed
|
|
5
|
+
* miniapp list, and the memory timeline.
|
|
6
|
+
*/
|
|
7
|
+
import {AppStateBackupError} from "../services/AppStateBackup"
|
|
8
|
+
import {
|
|
9
|
+
exportAppStateFile,
|
|
10
|
+
importAppStateFile,
|
|
11
|
+
type AppStateExportResult,
|
|
12
|
+
type AppStateRestoreOptions,
|
|
13
|
+
type AppStateRestoreResult,
|
|
14
|
+
} from "../services/appStateBackupFile"
|
|
15
|
+
import {MemoryBackupError} from "../services/MemoryStore"
|
|
16
|
+
|
|
17
|
+
export const backup = {
|
|
18
|
+
/** Share the app state as a JSON backup file. */
|
|
19
|
+
exportFile: (): Promise<AppStateExportResult> => exportAppStateFile(),
|
|
20
|
+
/**
|
|
21
|
+
* Pick a backup file and restore it: backup values overwrite per key, and
|
|
22
|
+
* running miniapps whose storage changed restart after `opts.reinstall` has
|
|
23
|
+
* installed the ones this phone lacks. Resolves null on cancel.
|
|
24
|
+
*/
|
|
25
|
+
importFile: (opts?: AppStateRestoreOptions): Promise<AppStateRestoreResult | null> => importAppStateFile(opts),
|
|
26
|
+
/** True for a file that isn't a backup this build can read. */
|
|
27
|
+
isBackupError: (err: unknown): boolean => err instanceof AppStateBackupError || err instanceof MemoryBackupError,
|
|
28
|
+
}
|
|
@@ -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": "0.3.7",
|
|
15
|
-
"releaseIdentity": "0.3.7-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.7-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.7-dev.891",
|
|
16
|
+
"releaseSetId": "glassly-0.3.7-dev.891",
|
|
17
|
+
"sourceCommit": "64f5b454fbb7786a307c1fe678c63f6f13f571a3",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export {DEFAULT_LOCAL_LLM_MODEL, isLocalLlmModelId} from "./services/LocalLlmMod
|
|
|
69
69
|
// session shapes are canonical in @glassly/miniapp (the session.memory
|
|
70
70
|
// surface); the stats read model is engine-side.
|
|
71
71
|
export type {MemoryStats} from "./services/MemoryStore"
|
|
72
|
+
export type {AppStateExportResult, AppStateRestoreOptions, AppStateRestoreResult} from "./services/appStateBackupFile"
|
|
72
73
|
export type {SpeakerProfile} from "./services/SpeakerProfileStore"
|
|
73
74
|
export type {EnrollOutcome} from "./facades/speakers"
|
|
74
75
|
export {MENTRA_LIVE_SETTING_KEYS, getBluetoothSettingKeysForDevice} from "./stores/bluetoothSettingKeys"
|
|
@@ -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
|
+
}
|
|
@@ -304,6 +304,20 @@ function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | nul
|
|
|
304
304
|
})
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/** Bounds the cold-GPS wait for a one-shot poll; on a stall, any older fix beats an error. */
|
|
308
|
+
const FRESH_LOCATION_TIMEOUT_MS = 8_000
|
|
309
|
+
|
|
310
|
+
async function freshLocationOrStale(): Promise<Location.LocationObject> {
|
|
311
|
+
const fresh = await withTimeout(
|
|
312
|
+
Location.getCurrentPositionAsync({accuracy: Location.Accuracy.Low}),
|
|
313
|
+
FRESH_LOCATION_TIMEOUT_MS,
|
|
314
|
+
)
|
|
315
|
+
if (fresh) return fresh
|
|
316
|
+
const stale = await Location.getLastKnownPositionAsync()
|
|
317
|
+
if (stale) return stale
|
|
318
|
+
throw new Error("No location fix available")
|
|
319
|
+
}
|
|
320
|
+
|
|
307
321
|
const LOG_TAG = "LOCAL_MINIAPP"
|
|
308
322
|
const DIAGNOSTIC_MAX_LIST_ITEMS = 100
|
|
309
323
|
const DIAGNOSTIC_MAX_STRING_LENGTH = 512
|
|
@@ -3450,7 +3464,7 @@ class LocalMiniappRuntime {
|
|
|
3450
3464
|
const locationStartedAt = Date.now()
|
|
3451
3465
|
const cached = await Location.getLastKnownPositionAsync({maxAge: 60_000})
|
|
3452
3466
|
console.log(`${LOG_TAG}: location poll cache hit=${cached !== null} elapsed=${Date.now() - locationStartedAt}ms`)
|
|
3453
|
-
const location = cached ?? (await
|
|
3467
|
+
const location = cached ?? (await freshLocationOrStale())
|
|
3454
3468
|
console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`)
|
|
3455
3469
|
|
|
3456
3470
|
this.sendResult(packageName, requestId, true, {
|
|
@@ -6071,13 +6085,16 @@ class LocalMiniappRuntime {
|
|
|
6071
6085
|
private async startInteropApp(packageName: string): Promise<boolean> {
|
|
6072
6086
|
const app = this.interopApps().find((a) => a.packageName === packageName)
|
|
6073
6087
|
if (!app) return false
|
|
6088
|
+
// session.miniapps.start comes from the glasses (the dashboard's Apps menu,
|
|
6089
|
+
// an Assist voice command), unlike a headless action wake, which carries no source.
|
|
6090
|
+
const launchSource = "glasses" as const
|
|
6074
6091
|
// Running only for its widget: start() promotes it to a normal run, as opening it on the phone does.
|
|
6075
|
-
if (app.widgetOnly) return useAppStatusStore.getState().start(app, {skipNavigation: true})
|
|
6092
|
+
if (app.widgetOnly) return useAppStatusStore.getState().start(app, {skipNavigation: true, launchSource})
|
|
6076
6093
|
if (app.local) {
|
|
6077
|
-
await miniappLauncher.ensureConnected(packageName)
|
|
6094
|
+
await miniappLauncher.ensureConnected(packageName, undefined, {launchSource})
|
|
6078
6095
|
return true
|
|
6079
6096
|
}
|
|
6080
|
-
return useAppStatusStore.getState().start(app, {skipNavigation: true})
|
|
6097
|
+
return useAppStatusStore.getState().start(app, {skipNavigation: true, launchSource})
|
|
6081
6098
|
}
|
|
6082
6099
|
|
|
6083
6100
|
private stopInteropApp(packageName: string): Promise<void> {
|