@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
|
@@ -29,42 +29,77 @@ export interface MiniappEngine {
|
|
|
29
29
|
|
|
30
30
|
let engine: MiniappEngine | null = null
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Kill and re-spawn a miniapp's background JSContext from its installed bundle
|
|
34
|
+
* (dev: the dev server's fresh build). Resolves false when it isn't installed
|
|
35
|
+
* or the spawn failed.
|
|
36
|
+
*/
|
|
37
|
+
async function respawnBackground(router: JSRouter, uiRouter: UIRouter, packageName: string): Promise<boolean> {
|
|
38
|
+
// Re-resolve the freshly built bundle (dev: HTTP off the dev server;
|
|
39
|
+
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
40
|
+
// the declared permissions + manifest across the respawn — omitting them
|
|
41
|
+
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
42
|
+
// per-call dispatch checks would start rejecting after a background
|
|
43
|
+
// hot-reload.
|
|
44
|
+
const resolved = await miniappLauncher.resolveBundle(packageName)
|
|
45
|
+
if (!resolved) {
|
|
46
|
+
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
50
|
+
// which is idempotent and would no-op an already-registered context.
|
|
51
|
+
await router.unregister(packageName)
|
|
52
|
+
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
53
|
+
permissions: resolved.declaredPermissions,
|
|
54
|
+
installedManifest: resolved.installedManifest,
|
|
55
|
+
})
|
|
56
|
+
if (!ok) {
|
|
57
|
+
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
61
|
+
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
62
|
+
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
63
|
+
// instead of being dropped while unbound.
|
|
64
|
+
uiRouter.notifyReopen(packageName)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
32
68
|
function wireDevServerRespawnBackground(router: JSRouter, uiRouter: UIRouter): void {
|
|
33
69
|
devServerBridge.onRespawnBackground(async (packageName) => {
|
|
34
70
|
try {
|
|
35
|
-
|
|
36
|
-
// released: file:// snapshot) via the launcher's shared recipe. Carries
|
|
37
|
-
// the declared permissions + manifest across the respawn — omitting them
|
|
38
|
-
// would respawn the JSContext with no permissions, so SUBSCRIBE gates and
|
|
39
|
-
// per-call dispatch checks would start rejecting after a background
|
|
40
|
-
// hot-reload.
|
|
41
|
-
const resolved = await miniappLauncher.resolveBundle(packageName)
|
|
42
|
-
if (!resolved) {
|
|
43
|
-
console.warn(`JSRuntime: respawn-bg could not resolve bundle for ${packageName}`)
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
// Force a respawn (kill + spawn) rather than launcher.ensureRunning,
|
|
47
|
-
// which is idempotent and would no-op an already-registered context.
|
|
48
|
-
await router.unregister(packageName)
|
|
49
|
-
const ok = await router.spawnAndRegister(packageName, resolved.bgSource, {
|
|
50
|
-
permissions: resolved.declaredPermissions,
|
|
51
|
-
installedManifest: resolved.installedManifest,
|
|
52
|
-
})
|
|
53
|
-
if (!ok) {
|
|
54
|
-
console.warn(`JSRuntime: respawn-bg failed for ${packageName}`)
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
// The respawned JSContext is a fresh MiniappSession with ui.bound false.
|
|
58
|
-
// The mounted WebView won't re-fire glassly.ready() (it's latched), so
|
|
59
|
-
// re-announce it so RPC replies (glassly.request) reach the UI again
|
|
60
|
-
// instead of being dropped while unbound.
|
|
61
|
-
uiRouter.notifyReopen(packageName)
|
|
71
|
+
await respawnBackground(router, uiRouter, packageName)
|
|
62
72
|
} catch (e) {
|
|
63
73
|
console.warn(`JSRuntime: respawn-bg threw for ${packageName}:`, e)
|
|
64
74
|
}
|
|
65
75
|
})
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
/** Packages with a live background JSContext. */
|
|
79
|
+
export function runningMiniappPackages(): string[] {
|
|
80
|
+
return engine?.router.registeredPackages() ?? []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Tear down a miniapp's background JSContext; a no-op when it isn't running. */
|
|
84
|
+
export async function stopMiniapp(packageName: string): Promise<void> {
|
|
85
|
+
await engine?.router.unregister(packageName)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Start (or restart) a miniapp's background so it reads its storage afresh (an
|
|
90
|
+
* app state restore). Resolves false when the engine isn't up or the spawn failed.
|
|
91
|
+
*/
|
|
92
|
+
export async function respawnMiniapp(packageName: string): Promise<boolean> {
|
|
93
|
+
const current = engine
|
|
94
|
+
if (!current) return false
|
|
95
|
+
try {
|
|
96
|
+
return await respawnBackground(current.router, current.uiRouter, packageName)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn(`JSRuntime: respawn threw for ${packageName}:`, e)
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
68
103
|
/**
|
|
69
104
|
* Construct (once) and return the JSRuntime engine singletons. Starts the router's
|
|
70
105
|
* message pump on first call. Subsequent calls return the same instances.
|
|
@@ -16,6 +16,7 @@ import CrustModule from "@glassly/crust"
|
|
|
16
16
|
import {resolveMapboxAccessToken, resolveMapboxRenderToken, userMapboxApiKey} from "./MapboxTokenService"
|
|
17
17
|
import {decodePolyline, parseDurationSeconds} from "./navigation/routesApiCodec"
|
|
18
18
|
import {resolveStepRoads} from "./navigation/roadNameResolver"
|
|
19
|
+
import {BgTimer} from "../utils/timers"
|
|
19
20
|
|
|
20
21
|
const LOG_TAG = "NAV_SERVICE"
|
|
21
22
|
|
|
@@ -394,7 +395,7 @@ class NavigationService {
|
|
|
394
395
|
sessionToken: string,
|
|
395
396
|
): Promise<{
|
|
396
397
|
ok: boolean
|
|
397
|
-
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string}>
|
|
398
|
+
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string; distanceMeters?: number}>
|
|
398
399
|
error?: string
|
|
399
400
|
}> {
|
|
400
401
|
return placeAutocompleteViaSearchBox(query, near, sessionToken)
|
|
@@ -506,6 +507,19 @@ export default navigationService
|
|
|
506
507
|
const MAPBOX_DIRECTIONS_BASE = "https://api.mapbox.com/directions/v5/mapbox"
|
|
507
508
|
const MAPBOX_GEOCODE_REVERSE = "https://api.mapbox.com/search/geocode/v6/reverse"
|
|
508
509
|
const MAPBOX_SEARCHBOX_SUGGEST = "https://api.mapbox.com/search/searchbox/v1/suggest"
|
|
510
|
+
|
|
511
|
+
/** Place search sits in front of a voice command; a stalled call must fail over, not hang. */
|
|
512
|
+
const PLACE_SEARCH_TIMEOUT_MS = 6_000
|
|
513
|
+
|
|
514
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
515
|
+
const controller = new AbortController()
|
|
516
|
+
const timer = BgTimer.setTimeout(() => controller.abort(), timeoutMs)
|
|
517
|
+
try {
|
|
518
|
+
return await fetch(url, {method: "GET", signal: controller.signal})
|
|
519
|
+
} finally {
|
|
520
|
+
BgTimer.clearTimeout(timer)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
509
523
|
const MAPBOX_SEARCHBOX_RETRIEVE = "https://api.mapbox.com/search/searchbox/v1/retrieve"
|
|
510
524
|
|
|
511
525
|
/**
|
|
@@ -818,7 +832,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
818
832
|
sessionToken: string,
|
|
819
833
|
): Promise<{
|
|
820
834
|
ok: boolean
|
|
821
|
-
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string}>
|
|
835
|
+
suggestions?: Array<{placeId: string; mainText: string; secondaryText: string; distanceMeters?: number}>
|
|
822
836
|
error?: string
|
|
823
837
|
}> {
|
|
824
838
|
if (!query.trim()) return {ok: true, suggestions: []}
|
|
@@ -837,11 +851,14 @@ async function placeAutocompleteViaSearchBox(
|
|
|
837
851
|
types: "poi,address,place,street",
|
|
838
852
|
limit: "10",
|
|
839
853
|
})
|
|
840
|
-
// Mapbox wants lng,lat for proximity bias.
|
|
841
|
-
if (near)
|
|
854
|
+
// Mapbox wants lng,lat for proximity bias; `origin` adds per-suggestion distance.
|
|
855
|
+
if (near) {
|
|
856
|
+
params.set("proximity", `${near.lng},${near.lat}`)
|
|
857
|
+
params.set("origin", `${near.lng},${near.lat}`)
|
|
858
|
+
}
|
|
842
859
|
|
|
843
860
|
try {
|
|
844
|
-
const res = await
|
|
861
|
+
const res = await fetchWithTimeout(`${MAPBOX_SEARCHBOX_SUGGEST}?${params.toString()}`, PLACE_SEARCH_TIMEOUT_MS)
|
|
845
862
|
if (!res.ok) return {ok: false, error: `Search Box suggest ${res.status}`}
|
|
846
863
|
const json = (await res.json()) as {
|
|
847
864
|
suggestions?: Array<{
|
|
@@ -849,6 +866,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
849
866
|
name?: string
|
|
850
867
|
place_formatted?: string
|
|
851
868
|
full_address?: string
|
|
869
|
+
distance?: number
|
|
852
870
|
}>
|
|
853
871
|
}
|
|
854
872
|
const suggestions = (json.suggestions ?? [])
|
|
@@ -857,6 +875,7 @@ async function placeAutocompleteViaSearchBox(
|
|
|
857
875
|
placeId: s.mapbox_id,
|
|
858
876
|
mainText: s.name ?? "",
|
|
859
877
|
secondaryText: s.place_formatted ?? s.full_address ?? "",
|
|
878
|
+
...(typeof s.distance === "number" ? {distanceMeters: s.distance} : {}),
|
|
860
879
|
}))
|
|
861
880
|
return {ok: true, suggestions}
|
|
862
881
|
} catch (err) {
|
|
@@ -890,9 +909,10 @@ async function placeDetailsViaSearchBox(
|
|
|
890
909
|
}
|
|
891
910
|
const params = new URLSearchParams({access_token: token, session_token: sessionToken})
|
|
892
911
|
try {
|
|
893
|
-
const res = await
|
|
894
|
-
|
|
895
|
-
|
|
912
|
+
const res = await fetchWithTimeout(
|
|
913
|
+
`${MAPBOX_SEARCHBOX_RETRIEVE}/${encodeURIComponent(placeId)}?${params.toString()}`,
|
|
914
|
+
PLACE_SEARCH_TIMEOUT_MS,
|
|
915
|
+
)
|
|
896
916
|
if (!res.ok) return {ok: false, error: `Search Box retrieve ${res.status}`}
|
|
897
917
|
const json = (await res.json()) as {
|
|
898
918
|
features?: Array<{
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
|
|
7
|
+
import type {MemoryImportResult} from "@glassly/miniapp"
|
|
8
|
+
import {File, Paths} from "expo-file-system"
|
|
9
|
+
import {Platform} from "react-native"
|
|
10
|
+
import Share from "react-native-share"
|
|
11
|
+
|
|
12
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
13
|
+
import {useAppStatusStore} from "../stores/apps"
|
|
14
|
+
import {storage} from "../utils/storage/storage"
|
|
15
|
+
import appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from "./AppRegistry"
|
|
16
|
+
import {
|
|
17
|
+
AppStateBackupError,
|
|
18
|
+
collectAppStateBackup,
|
|
19
|
+
miniappPrefs,
|
|
20
|
+
miniappsWithStorage,
|
|
21
|
+
parseAppStateBackup,
|
|
22
|
+
restorableSettings,
|
|
23
|
+
restoreMiniappStorage,
|
|
24
|
+
type AppStateBackupBackend,
|
|
25
|
+
} from "./AppStateBackup"
|
|
26
|
+
import {cloudClientService} from "./CloudClientService"
|
|
27
|
+
import {memoryStore} from "./memoryStoreInstance"
|
|
28
|
+
import {respawnMiniapp, runningMiniappPackages, stopMiniapp} from "./MiniappEngine"
|
|
29
|
+
|
|
30
|
+
export interface AppStateExportResult {
|
|
31
|
+
success: boolean
|
|
32
|
+
cancelled?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AppStateRestoreResult {
|
|
36
|
+
/** Settings written. */
|
|
37
|
+
settings: number
|
|
38
|
+
/** Miniapps whose storage was restored. */
|
|
39
|
+
miniapps: number
|
|
40
|
+
memory: MemoryImportResult | null
|
|
41
|
+
/** Installed when the backup was made but not on this phone now. */
|
|
42
|
+
missing: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AppStateRestoreOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Install what the backup had installed and this phone lacks. Runs before
|
|
48
|
+
* the restored miniapps start again, so the dashboard comes back with every
|
|
49
|
+
* widget provider its saved layout names: it drops unknown widgets from the
|
|
50
|
+
* layout the next time it saves it.
|
|
51
|
+
*/
|
|
52
|
+
reinstall?: (packageNames: string[]) => Promise<void>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const PICK_MIME_TYPES =
|
|
56
|
+
Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"]
|
|
57
|
+
|
|
58
|
+
const backend: AppStateBackupBackend = {
|
|
59
|
+
get(key) {
|
|
60
|
+
const result = storage.load<unknown>(key)
|
|
61
|
+
return result.is_ok() ? result.value : null
|
|
62
|
+
},
|
|
63
|
+
set(key, value) {
|
|
64
|
+
const result = storage.save(key, value)
|
|
65
|
+
if (result.is_error()) throw result.error
|
|
66
|
+
},
|
|
67
|
+
keys: () => storage.getAllKeys(),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function exportAppStateFile(): Promise<AppStateExportResult> {
|
|
71
|
+
const userId = await cloudClientService.resolveUserId()
|
|
72
|
+
const installed: Array<{packageName: string; version: string; source?: string}> = []
|
|
73
|
+
for (const app of await appRegistry.getInstalledMiniapps()) {
|
|
74
|
+
const version = await appRegistry.getActiveVersion(app.packageName)
|
|
75
|
+
if (!version) continue
|
|
76
|
+
installed.push({
|
|
77
|
+
packageName: app.packageName,
|
|
78
|
+
version,
|
|
79
|
+
source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
const backup = collectAppStateBackup({
|
|
83
|
+
backend,
|
|
84
|
+
userId,
|
|
85
|
+
settings: Object.values(SETTINGS),
|
|
86
|
+
installed,
|
|
87
|
+
memory: await memoryStore.exportBackup(),
|
|
88
|
+
now: Date.now(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`
|
|
92
|
+
const file = new File(Paths.cache, filename)
|
|
93
|
+
file.writeSync(JSON.stringify(backup))
|
|
94
|
+
try {
|
|
95
|
+
await Share.open({url: file.uri, type: "application/json", filename, title: "Glassly backup"})
|
|
96
|
+
return {success: true}
|
|
97
|
+
} catch (error: any) {
|
|
98
|
+
// react-native-share rejects when the user dismisses the sheet.
|
|
99
|
+
if (error?.message?.includes("User did not share")) return {success: false, cancelled: true}
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolves null when the user cancels the picker. */
|
|
105
|
+
export async function importAppStateFile(opts: AppStateRestoreOptions = {}): Promise<AppStateRestoreResult | null> {
|
|
106
|
+
const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})
|
|
107
|
+
if (picked.canceled) return null
|
|
108
|
+
let data: unknown
|
|
109
|
+
try {
|
|
110
|
+
data = JSON.parse(await picked.result.text())
|
|
111
|
+
} catch {
|
|
112
|
+
throw new AppStateBackupError("Not a Glassly app state backup")
|
|
113
|
+
}
|
|
114
|
+
const backup = parseAppStateBackup(data)
|
|
115
|
+
const userId = await cloudClientService.resolveUserId()
|
|
116
|
+
|
|
117
|
+
// First, so a memory section this build rejects fails the restore before anything is written.
|
|
118
|
+
const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null
|
|
119
|
+
|
|
120
|
+
const {plain, indexed} = restorableSettings(backup, Object.values(SETTINGS))
|
|
121
|
+
const written = await useSettingsStore.getState().setManyLocally(plain)
|
|
122
|
+
if (written.is_error()) throw written.error
|
|
123
|
+
// setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.
|
|
124
|
+
for (const [key, value] of Object.entries(indexed)) backend.set(key, value)
|
|
125
|
+
if (Object.keys(indexed).length > 0) {
|
|
126
|
+
useSettingsStore.setState((state) => ({settings: {...state.settings, ...indexed}}))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A running miniapp holds its old values in memory and could write them back over the
|
|
130
|
+
// restore, so the ones getting storage stop now and start again at the end.
|
|
131
|
+
const running = new Set(runningMiniappPackages())
|
|
132
|
+
const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName))
|
|
133
|
+
for (const packageName of restarting) await stopMiniapp(packageName)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const restored = restoreMiniappStorage(backend, userId, backup)
|
|
137
|
+
|
|
138
|
+
const apps = useAppStatusStore.getState()
|
|
139
|
+
for (const [packageName, record] of Object.entries(backup.miniapps)) {
|
|
140
|
+
const prefs = miniappPrefs(record)
|
|
141
|
+
if (prefs.hidden !== undefined) apps.setHiddenStatus(packageName, prefs.hidden)
|
|
142
|
+
if (prefs.alwaysOn !== undefined) saveAlwaysOnOverride(packageName, prefs.alwaysOn)
|
|
143
|
+
for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {
|
|
144
|
+
saveWidgetExpandChoice(packageName, widgetId, expand)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
appRegistry.markRefreshNeeded()
|
|
148
|
+
await apps.refresh()
|
|
149
|
+
|
|
150
|
+
const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName))
|
|
151
|
+
const missing = Object.entries(backup.miniapps)
|
|
152
|
+
.filter(([packageName, record]) => record.installed && !installedNow.has(packageName))
|
|
153
|
+
.map(([packageName]) => packageName)
|
|
154
|
+
if (missing.length > 0) await opts.reinstall?.(missing)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
settings: Object.keys(plain).length + Object.keys(indexed).length,
|
|
158
|
+
miniapps: restored.packages.length,
|
|
159
|
+
memory,
|
|
160
|
+
missing,
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
for (const packageName of restarting) await respawnMiniapp(packageName)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function localDate(ms: number): string {
|
|
168
|
+
const d = new Date(ms)
|
|
169
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
170
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
171
|
+
}
|