@mentra/engine 3.1.0-dev.9 → 3.1.0-dev.91
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/README.md +23 -5
- package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
- package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
- package/build/bluetooth-sdk/ota-transport.js +2 -0
- package/build/bluetooth-sdk/ota-transport.js.map +1 -0
- package/build/engine.d.ts +5 -2
- package/build/engine.d.ts.map +1 -1
- package/build/facades/ota.d.ts +11 -6
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +9 -5
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +4 -0
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +81 -7
- package/build/facades/pairing.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +8 -2
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +256 -354
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/index.d.ts +1 -0
- package/build/react/index.d.ts.map +1 -1
- package/build/react/index.js +1 -0
- package/build/react/index.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +84 -0
- package/build/react/useMentraLiveOta.d.ts.map +1 -0
- package/build/react/useMentraLiveOta.js +627 -0
- package/build/react/useMentraLiveOta.js.map +1 -0
- package/build/services/BlobStore.d.ts +2 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +63 -10
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +15 -2
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +3 -3
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts.map +1 -1
- package/build/services/HotspotOtaTransport.js +8 -7
- package/build/services/HotspotOtaTransport.js.map +1 -1
- package/build/services/OtaAutoChain.d.ts +9 -2
- package/build/services/OtaAutoChain.d.ts.map +1 -1
- package/build/services/OtaAutoChain.js +10 -2
- package/build/services/OtaAutoChain.js.map +1 -1
- package/build/services/OtaInstallCoordinator.d.ts +26 -3
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +35 -13
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +1 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +4 -1
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +21 -5
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +13 -12
- package/build/services/asg/localNetworkTransport.js.map +1 -1
- package/build/services/glassesClockSync.js +1 -1
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +4 -0
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +4 -0
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaReleaseVersion.d.ts +10 -0
- package/build/services/otaReleaseVersion.d.ts.map +1 -0
- package/build/services/otaReleaseVersion.js +14 -0
- package/build/services/otaReleaseVersion.js.map +1 -0
- package/build/stores/appVisibility.d.ts +3 -0
- package/build/stores/appVisibility.d.ts.map +1 -0
- package/build/stores/appVisibility.js +3 -0
- package/build/stores/appVisibility.js.map +1 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +7 -1
- package/build/stores/apps.js.map +1 -1
- package/build/stores/core.d.ts +3 -3
- package/build/stores/core.d.ts.map +1 -1
- package/build/stores/core.js.map +1 -1
- package/build/stores/glasses.d.ts +10 -4
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js.map +1 -1
- package/package.json +13 -6
- package/src/bluetooth-sdk/ota-transport.ts +1 -0
- package/src/facades/ota.ts +19 -6
- package/src/facades/pairing.ts +88 -12
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +2 -0
- package/src/react/MentraLiveOtaFlow.tsx +378 -480
- package/src/react/index.ts +14 -0
- package/src/react/useMentraLiveOta.ts +793 -0
- package/src/services/BlobStore.ts +57 -10
- package/src/services/GlassesStatusProjection.ts +7 -6
- package/src/services/HotspotOtaTransport.ts +8 -7
- package/src/services/OtaAutoChain.ts +26 -2
- package/src/services/OtaInstallCoordinator.ts +37 -13
- package/src/services/OtaService.ts +1 -2
- package/src/services/OtaUpdateCheckService.ts +26 -8
- package/src/services/asg/localNetworkTransport.ts +20 -19
- package/src/services/glassesClockSync.ts +1 -1
- package/src/services/otaInstallPolicy.ts +4 -0
- package/src/services/otaReleaseVersion.ts +22 -0
- package/src/stores/appVisibility.ts +3 -0
- package/src/stores/apps.ts +7 -1
- package/src/stores/core.ts +4 -4
- package/src/stores/glasses.ts +13 -6
|
@@ -40,8 +40,15 @@ const LOG_TAG = "LocalMiniappRuntime"
|
|
|
40
40
|
/** Per-app blob quota: 1 GB. */
|
|
41
41
|
export const BLOB_QUOTA_BYTES = 1024 * 1024 * 1024
|
|
42
42
|
const ROOT_DIR_NAME = "mentra_blobs"
|
|
43
|
-
/** Cache dir root holding short-lived, properly-named copies handed to the OS share sheet (one
|
|
43
|
+
/** Cache dir root holding short-lived, properly-named copies handed to the OS share sheet (one subdir per stored blob). */
|
|
44
44
|
const SHARE_DIR_NAME = "mentra_blob_share"
|
|
45
|
+
/**
|
|
46
|
+
* Keep prepared share handoffs alive long enough for recipient apps to read
|
|
47
|
+
* them asynchronously (for example, an email composer attaching the file).
|
|
48
|
+
* Stale copies are collected before the next share; the OS may evict cache
|
|
49
|
+
* files sooner under storage pressure.
|
|
50
|
+
*/
|
|
51
|
+
const SHARE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
45
52
|
const META_KEY_ROOT = "mentraos_blobmeta_"
|
|
46
53
|
/** Cap md5 computation so we don't block the JS thread hashing a huge file. */
|
|
47
54
|
const MD5_MAX_BYTES = 50 * 1024 * 1024
|
|
@@ -598,6 +605,32 @@ export class BlobStore {
|
|
|
598
605
|
|
|
599
606
|
// ---- share ---------------------------------------------------------------
|
|
600
607
|
|
|
608
|
+
/** Best-effort GC for prepared share handoffs retained in the OS cache. */
|
|
609
|
+
private cleanupShareCache(now = Date.now()): void {
|
|
610
|
+
try {
|
|
611
|
+
const root = new Directory(Paths.cache, SHARE_DIR_NAME)
|
|
612
|
+
if (!root.exists) return
|
|
613
|
+
const cutoff = now - SHARE_CACHE_MAX_AGE_MS
|
|
614
|
+
for (const entry of root.list()) {
|
|
615
|
+
if (!(entry instanceof Directory)) continue
|
|
616
|
+
try {
|
|
617
|
+
const info = entry.info()
|
|
618
|
+
// modificationTime is available on both platforms; creationTime can
|
|
619
|
+
// be absent on Android versions before API 26. The directory id
|
|
620
|
+
// begins with Date.now().toString(36), which is a final fallback.
|
|
621
|
+
const encoded = entry.name.split("-", 1)[0]
|
|
622
|
+
const encodedTime = /^[0-9a-z]+$/.test(encoded) ? Number.parseInt(encoded, 36) : Number.NaN
|
|
623
|
+
const timestamp = info.modificationTime ?? info.creationTime ?? encodedTime
|
|
624
|
+
if (Number.isFinite(timestamp) && timestamp < cutoff) entry.delete()
|
|
625
|
+
} catch {
|
|
626
|
+
// Keep scanning if one cache entry disappears or is unreadable.
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
// Cache cleanup must never prevent a share.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
601
634
|
async handleShare(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
|
|
602
635
|
const key = payload.key as string
|
|
603
636
|
const meta = this.readMeta(packageName, key)
|
|
@@ -615,23 +648,33 @@ export class BlobStore {
|
|
|
615
648
|
// applies to base64/data payloads, not file:// URLs), so sharing `file.uri`
|
|
616
649
|
// directly would hand the recipient a machine-named file. Copy to a temp
|
|
617
650
|
// file that carries the real display name + extension, share that, then
|
|
618
|
-
// clean it up.
|
|
651
|
+
// clean it up after the recipient has had time to consume it.
|
|
619
652
|
const shareName = shareFileName(meta)
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
653
|
+
// Key the cache directory by the immutable stored filename. Repeated or
|
|
654
|
+
// concurrent shares of the same blob can safely reference the same bytes,
|
|
655
|
+
// avoiding an extra full-sized cache copy for every share. Different blobs
|
|
656
|
+
// still get isolated directories even when they use the same display name.
|
|
657
|
+
// The file keeps `shareName` as its basename so the OS share sheet shows the
|
|
658
|
+
// right name + extension.
|
|
624
659
|
let tempDir: Directory | null = null
|
|
660
|
+
let cacheReady = false
|
|
625
661
|
try {
|
|
626
|
-
|
|
627
|
-
tempDir.
|
|
662
|
+
this.cleanupShareCache()
|
|
663
|
+
tempDir = new Directory(Paths.cache, SHARE_DIR_NAME, sanitizeSegment(meta.fileName))
|
|
664
|
+
if (!tempDir.exists) tempDir.create({intermediates: true})
|
|
628
665
|
const temp = new File(tempDir, shareName)
|
|
629
|
-
|
|
666
|
+
if (!temp.exists || temp.size !== meta.bytes) {
|
|
667
|
+
if (temp.exists) temp.delete()
|
|
668
|
+
file.copy(temp)
|
|
669
|
+
}
|
|
670
|
+
cacheReady = true
|
|
630
671
|
await Share.open({
|
|
631
672
|
url: temp.uri,
|
|
632
673
|
type: meta.mimeType || OCTET,
|
|
633
674
|
filename: shareName,
|
|
634
675
|
})
|
|
676
|
+
// Share.open resolves once the OS accepts the handoff. Recipient apps may
|
|
677
|
+
// still read the URI asynchronously, so retain this cache copy for GC.
|
|
635
678
|
this.hooks.sendResult(packageName, requestId, true, {success: true})
|
|
636
679
|
} catch (error: any) {
|
|
637
680
|
if (error?.message?.includes("User did not share")) {
|
|
@@ -641,7 +684,11 @@ export class BlobStore {
|
|
|
641
684
|
this.hooks.sendResult(packageName, requestId, true, {success: false})
|
|
642
685
|
}
|
|
643
686
|
} finally {
|
|
644
|
-
if
|
|
687
|
+
// Once a reusable cache entry is ready, retain it even if this particular
|
|
688
|
+
// sheet is cancelled or fails: another concurrent/recent share may still
|
|
689
|
+
// be reading the same file. Only remove an entry whose preparation did
|
|
690
|
+
// not finish; ready entries are collected after the retention window.
|
|
691
|
+
if (tempDir && !cacheReady) {
|
|
645
692
|
try {
|
|
646
693
|
if (tempDir.exists) tempDir.delete()
|
|
647
694
|
} catch {
|
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Started by `engine.start()`. Idempotent.
|
|
13
13
|
*/
|
|
14
|
-
import BluetoothSdk from "@mentra/bluetooth-sdk
|
|
15
|
-
import type {GlassesStatus} from "@mentra/bluetooth-sdk/internal"
|
|
14
|
+
import BluetoothSdk, {type PublicGlassesStatus} from "@mentra/bluetooth-sdk"
|
|
16
15
|
import {useCoreStore} from "../stores/core"
|
|
17
16
|
import {useGlassesStore} from "../stores/glasses"
|
|
18
17
|
|
|
19
18
|
let unsubs: Array<() => void> = []
|
|
20
19
|
let projectionRunId = 0
|
|
21
|
-
let glassesStatusForwarder: ((status: Partial<
|
|
20
|
+
let glassesStatusForwarder: ((status: Partial<PublicGlassesStatus>) => void) | null = null
|
|
22
21
|
let hydrationPromise: Promise<void> | null = null
|
|
23
22
|
|
|
24
|
-
export function startGlassesStatusProjection(
|
|
23
|
+
export function startGlassesStatusProjection(
|
|
24
|
+
forwarder?: (status: Partial<PublicGlassesStatus>) => void,
|
|
25
|
+
): Promise<void> {
|
|
25
26
|
if (forwarder) glassesStatusForwarder = forwarder
|
|
26
27
|
if (unsubs.length) return hydrationPromise ?? Promise.resolve()
|
|
27
28
|
|
|
@@ -49,7 +50,7 @@ export function startGlassesStatusProjection(forwarder?: (status: Partial<Glasse
|
|
|
49
50
|
|
|
50
51
|
// Bluetooth-adapter status -> core store.
|
|
51
52
|
unsubs.push(
|
|
52
|
-
BluetoothSdk.
|
|
53
|
+
BluetoothSdk.subscribeBluetoothStatus((changed) => {
|
|
53
54
|
bluetoothEventSeen = true
|
|
54
55
|
useCoreStore.getState().setCoreInfo(changed)
|
|
55
56
|
}),
|
|
@@ -58,7 +59,7 @@ export function startGlassesStatusProjection(forwarder?: (status: Partial<Glasse
|
|
|
58
59
|
// Glasses status -> glasses store (+ optional full-runtime forwarding to local
|
|
59
60
|
// miniapps; clear any stale OTA-available flag on disconnect).
|
|
60
61
|
unsubs.push(
|
|
61
|
-
BluetoothSdk.
|
|
62
|
+
BluetoothSdk.subscribeGlassesStatus((changed) => {
|
|
62
63
|
glassesEventSeen = true
|
|
63
64
|
useGlassesStore.getState().setGlassesInfo(changed)
|
|
64
65
|
glassesStatusForwarder?.(changed)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import BluetoothSdk, {isEnabledHotspotStatus
|
|
1
|
+
import BluetoothSdk, {isEnabledHotspotStatus} from "@mentra/bluetooth-sdk"
|
|
2
|
+
import {otaServer} from "@mentra/bluetooth-sdk/ota-transport"
|
|
2
3
|
// One coordinator selects the native Android or iOS staging implementation at runtime.
|
|
3
4
|
// eslint-disable-next-line react-native/split-platform-components
|
|
4
5
|
import {PermissionsAndroid, Platform} from "react-native"
|
|
@@ -83,7 +84,7 @@ class HotspotOtaTransport {
|
|
|
83
84
|
let localAddress = scopedAddress
|
|
84
85
|
if (Platform.OS === "ios") {
|
|
85
86
|
try {
|
|
86
|
-
localAddress = await
|
|
87
|
+
localAddress = await otaServer.waitForWifiAddress(hotspot.localIp, 15_000)
|
|
87
88
|
} catch (error) {
|
|
88
89
|
throw new HotspotOtaTransportError(
|
|
89
90
|
"hotspot_join_failed",
|
|
@@ -94,10 +95,10 @@ class HotspotOtaTransport {
|
|
|
94
95
|
const artifactPaths = Object.fromEntries(this.prepared.map((artifact) => [artifact.sha256, artifact.filePath]))
|
|
95
96
|
// Start exactly one native listener to learn its selected port, then atomically replace
|
|
96
97
|
// the placeholder with the immutable rewritten manifest before ota_start is sent.
|
|
97
|
-
const server = await
|
|
98
|
+
const server = await otaServer.start("{}", artifactPaths, localAddress)
|
|
98
99
|
this.serverStarted = true
|
|
99
100
|
const manifest = rewriteManifestForLocalServer(checkResult.manifestBody, this.prepared, server.baseUrl)
|
|
100
|
-
const published = await
|
|
101
|
+
const published = await otaServer.start(manifest, artifactPaths, server.host)
|
|
101
102
|
if (published.manifestUrl !== server.manifestUrl) {
|
|
102
103
|
throw new Error("Local OTA server endpoint changed while publishing the manifest")
|
|
103
104
|
}
|
|
@@ -141,11 +142,11 @@ class HotspotOtaTransport {
|
|
|
141
142
|
destination: string,
|
|
142
143
|
onProgress?: (bytesWritten: number, contentLength: number) => void,
|
|
143
144
|
): Promise<{statusCode: number}> => {
|
|
144
|
-
const subscription =
|
|
145
|
+
const subscription = otaServer.onArtifactDownloadProgress((event) => {
|
|
145
146
|
if (event.destination === destination) onProgress?.(event.bytesWritten, event.contentLength)
|
|
146
147
|
})
|
|
147
148
|
try {
|
|
148
|
-
return await
|
|
149
|
+
return await otaServer.downloadArtifact(entry.url, destination)
|
|
149
150
|
} finally {
|
|
150
151
|
subscription.remove()
|
|
151
152
|
}
|
|
@@ -164,7 +165,7 @@ class HotspotOtaTransport {
|
|
|
164
165
|
console.warn("[OTA_PROGRESS] glasses hotspot shutdown was not confirmed after bounded retries")
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
if (this.serverStarted) await
|
|
168
|
+
if (this.serverStarted) await otaServer.stop().catch(() => {})
|
|
168
169
|
if (this.localNetworkConnected) await localNetworkTransport.disconnect().catch(() => {})
|
|
169
170
|
await cleanupArtifacts().catch(() => {})
|
|
170
171
|
this.prepared = []
|
|
@@ -6,10 +6,18 @@ export const OTA_AUTO_CHAIN_RECONNECT_TIMEOUT_MS = 120_000
|
|
|
6
6
|
type OtaAutoChainSession = {
|
|
7
7
|
approvedDowngrade: boolean
|
|
8
8
|
passCount: number
|
|
9
|
+
releaseRange: OtaAutoChainReleaseRange
|
|
9
10
|
reconnectDeadline: number | null
|
|
10
11
|
seenFingerprints: Set<string>
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export type OtaAutoChainReleaseRange = {
|
|
15
|
+
fromVersion: string | null
|
|
16
|
+
toVersion: string | null
|
|
17
|
+
/** Exact coordinated release identity for the OTA pin, absent for legacy manifests. */
|
|
18
|
+
releaseVersion?: string | null
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export type OtaAutoChainAdvanceResult =
|
|
14
22
|
| {advance: true; passCount: number}
|
|
15
23
|
| {advance: false; reason: "inactive" | "duplicate" | "max_passes" | "downgrade_not_approved"}
|
|
@@ -42,10 +50,15 @@ export function otaAutoChainFingerprint(result: OtaCheckCurrentGlassesResult): s
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/** Start a chain after the user explicitly approves its first update pass. */
|
|
45
|
-
export function beginOtaAutoChain(
|
|
53
|
+
export function beginOtaAutoChain(
|
|
54
|
+
initialFingerprint: string,
|
|
55
|
+
approvedDowngrade: boolean,
|
|
56
|
+
releaseRange: OtaAutoChainReleaseRange,
|
|
57
|
+
): void {
|
|
46
58
|
session = {
|
|
47
59
|
approvedDowngrade,
|
|
48
60
|
passCount: 1,
|
|
61
|
+
releaseRange: {...releaseRange},
|
|
49
62
|
reconnectDeadline: null,
|
|
50
63
|
seenFingerprints: new Set([initialFingerprint]),
|
|
51
64
|
}
|
|
@@ -55,6 +68,10 @@ export function isOtaAutoChainActive(): boolean {
|
|
|
55
68
|
return session !== null
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
export function otaAutoChainReleaseRange(): OtaAutoChainReleaseRange | null {
|
|
72
|
+
return session ? {...session.releaseRange} : null
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
/** End the current chain. Safe to call when no chain is active. */
|
|
59
76
|
export function stopOtaAutoChain(): void {
|
|
60
77
|
session = null
|
|
@@ -80,7 +97,12 @@ export function clearOtaAutoChainReconnectWait(): void {
|
|
|
80
97
|
* Admit the next pass of an active chain. Repeated offers, unexpectedly
|
|
81
98
|
* destructive downgrades, and runaway chains fail closed and end automation.
|
|
82
99
|
*/
|
|
83
|
-
export function tryAdvanceOtaAutoChain(
|
|
100
|
+
export function tryAdvanceOtaAutoChain(
|
|
101
|
+
fingerprint: string,
|
|
102
|
+
isDowngrade: boolean,
|
|
103
|
+
targetVersion: string | null,
|
|
104
|
+
releaseVersion: string | null = null,
|
|
105
|
+
): OtaAutoChainAdvanceResult {
|
|
84
106
|
if (!session) {
|
|
85
107
|
return {advance: false, reason: "inactive"}
|
|
86
108
|
}
|
|
@@ -102,5 +124,7 @@ export function tryAdvanceOtaAutoChain(fingerprint: string, isDowngrade: boolean
|
|
|
102
124
|
|
|
103
125
|
session.seenFingerprints.add(fingerprint)
|
|
104
126
|
session.passCount += 1
|
|
127
|
+
if (targetVersion) session.releaseRange.toVersion = targetVersion
|
|
128
|
+
if (releaseVersion) session.releaseRange.releaseVersion = releaseVersion
|
|
105
129
|
return {advance: true, passCount: session.passCount}
|
|
106
130
|
}
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
* state written mid-pass queues a follow-up pass — the same way a setState
|
|
18
18
|
* during React effects scheduled a re-render after the current effect batch.
|
|
19
19
|
*/
|
|
20
|
-
import BluetoothSdk from "@mentra/bluetooth-sdk
|
|
21
|
-
import type {OtaProgress, OtaStatus} from "@mentra/bluetooth-sdk"
|
|
20
|
+
import BluetoothSdk, {type OtaProgress, type OtaStatus} from "@mentra/bluetooth-sdk"
|
|
22
21
|
import GlobalEventEmitter from "../utils/GlobalEventEmitter"
|
|
23
22
|
import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
|
|
24
23
|
import {resolveOtaManifestUrl} from "./otaManifestUrl"
|
|
@@ -27,10 +26,12 @@ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
|
|
|
27
26
|
import {deriveDisplayState, type DisplayState} from "./otaDisplayState"
|
|
28
27
|
import {
|
|
29
28
|
BES_CONTINUE_LOCKOUT_MS,
|
|
29
|
+
BES_RESTART_TIMEOUT_MS,
|
|
30
30
|
DOWNLOAD_STUCK_TIMEOUT_MS,
|
|
31
31
|
GLOBAL_OTA_TIMEOUT_MS,
|
|
32
32
|
LEGACY_APK_COMPLETION_SETTLE_MS,
|
|
33
33
|
LEGACY_BES_CONTINUE_LOCKOUT_MS,
|
|
34
|
+
LEGACY_BES_RESTART_TIMEOUT_MS,
|
|
34
35
|
LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS,
|
|
35
36
|
LEGACY_GLOBAL_OTA_TIMEOUT_MS,
|
|
36
37
|
LEGACY_MTK_INSTALL_TIMEOUT_MS,
|
|
@@ -170,6 +171,8 @@ export interface OtaInstallSnapshot {
|
|
|
170
171
|
/** Pre-ota_start phone staging/join state for a hotspot attempt. */
|
|
171
172
|
hotspotPhase: HotspotOtaPhase
|
|
172
173
|
hotspotArtifactPercent: number | null
|
|
174
|
+
/** Transport selected from the checked glasses capabilities and Wi-Fi state. */
|
|
175
|
+
transport: "wifi" | "hotspot"
|
|
173
176
|
}
|
|
174
177
|
|
|
175
178
|
type OtaStartOutcome = "pending" | "acknowledged" | "rejected"
|
|
@@ -244,6 +247,7 @@ class OtaInstallCoordinator {
|
|
|
244
247
|
private pingInterval: ReturnType<typeof setInterval> | null = null
|
|
245
248
|
private queryReplyTimeout: ReturnType<typeof setTimeout> | null = null
|
|
246
249
|
private continueLockoutTimer: ReturnType<typeof setTimeout> | null = null
|
|
250
|
+
private restartRecoveryTimeout: ReturnType<typeof setTimeout> | null = null
|
|
247
251
|
private legacyApkSettleTimer: ReturnType<typeof setTimeout> | null = null
|
|
248
252
|
private mtkStallDetectTimer: ReturnType<typeof setTimeout> | null = null
|
|
249
253
|
private mtkSimTickTimer: ReturnType<typeof setInterval> | null = null
|
|
@@ -407,7 +411,7 @@ class OtaInstallCoordinator {
|
|
|
407
411
|
if (this.otaStartOwnership?.outcome === "pending") {
|
|
408
412
|
void this.sendOtaStartWithWatchdogs()
|
|
409
413
|
} else {
|
|
410
|
-
void BluetoothSdk.
|
|
414
|
+
void BluetoothSdk.queryOtaStatus().catch(() => {})
|
|
411
415
|
this.armQueryReplyFallback("retry")
|
|
412
416
|
}
|
|
413
417
|
}
|
|
@@ -449,16 +453,17 @@ class OtaInstallCoordinator {
|
|
|
449
453
|
* stays in the screen): clear the selected update, and after an APK step
|
|
450
454
|
* clear the stale build number so the next check re-reads version_info.
|
|
451
455
|
*/
|
|
452
|
-
finish(): void {
|
|
456
|
+
async finish(): Promise<void> {
|
|
453
457
|
if (this.otaStartOwnership?.outcome !== "pending") {
|
|
454
458
|
this.otaStartOwnership = null
|
|
455
459
|
}
|
|
456
460
|
useGlassesStore.getState().setOtaUpdateAvailable(null)
|
|
457
461
|
if (this.apkStepSeen) {
|
|
458
|
-
BluetoothSdk.updateGlasses({buildNumber: ""})
|
|
459
462
|
useGlassesStore.getState().setGlassesInfo({buildNumber: ""})
|
|
460
463
|
}
|
|
461
|
-
|
|
464
|
+
// The post-install check needs internet again. Do not let callers leave the
|
|
465
|
+
// terminal screen while Android still owns the no-internet hotspot route.
|
|
466
|
+
await this.teardownHotspotTransport()
|
|
462
467
|
}
|
|
463
468
|
|
|
464
469
|
/**
|
|
@@ -467,8 +472,8 @@ class OtaInstallCoordinator {
|
|
|
467
472
|
* deriveDisplayState doesn't resurrect the stale install when the host
|
|
468
473
|
* navigates away and back through /ota routes.
|
|
469
474
|
*/
|
|
470
|
-
discard(): void {
|
|
471
|
-
this.finish()
|
|
475
|
+
async discard(): Promise<void> {
|
|
476
|
+
await this.finish()
|
|
472
477
|
const store = useGlassesStore.getState()
|
|
473
478
|
store.setOtaStatus(null)
|
|
474
479
|
store.setOtaProgress(null)
|
|
@@ -505,6 +510,7 @@ class OtaInstallCoordinator {
|
|
|
505
510
|
versionChangePhase: this.deriveVersionChangePhase(connected),
|
|
506
511
|
hotspotPhase: this.hotspotPhase,
|
|
507
512
|
hotspotArtifactPercent: this.hotspotArtifactPercent,
|
|
513
|
+
transport: this.selectedTransport,
|
|
508
514
|
}
|
|
509
515
|
}
|
|
510
516
|
|
|
@@ -997,12 +1003,22 @@ class OtaInstallCoordinator {
|
|
|
997
1003
|
// (15s unified, 35s for legacy-shaped sessions — WP 8C-f).
|
|
998
1004
|
if (displayStateChanged && displayState === "restarting") {
|
|
999
1005
|
this.clearContinueLockoutTimer()
|
|
1006
|
+
this.clearRestartRecoveryTimeout()
|
|
1000
1007
|
const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS
|
|
1008
|
+
const restartTimeoutMs = legacySession ? LEGACY_BES_RESTART_TIMEOUT_MS : BES_RESTART_TIMEOUT_MS
|
|
1001
1009
|
this.setContinueButtonDisabled(true)
|
|
1002
1010
|
this.continueLockoutTimer = setTimeout(() => {
|
|
1003
1011
|
this.continueLockoutTimer = null
|
|
1004
1012
|
this.setContinueButtonDisabled(false)
|
|
1005
1013
|
}, lockoutMs)
|
|
1014
|
+
this.restartRecoveryTimeout = setTimeout(() => {
|
|
1015
|
+
this.restartRecoveryTimeout = null
|
|
1016
|
+
if (this.computeDisplayStateNow() !== "restarting") return
|
|
1017
|
+
console.log(`[OTA_PROGRESS] watchdog: glasses did not finish restarting in ${restartTimeoutMs}ms`)
|
|
1018
|
+
this.setErrorMsg(OtaProgressMessages.restartTimeout)
|
|
1019
|
+
}, restartTimeoutMs)
|
|
1020
|
+
} else if (displayStateChanged) {
|
|
1021
|
+
this.clearRestartRecoveryTimeout()
|
|
1006
1022
|
}
|
|
1007
1023
|
|
|
1008
1024
|
// Legacy MTK install stall simulation (WP 8C-e): display-only. The simulation
|
|
@@ -1065,7 +1081,7 @@ class OtaInstallCoordinator {
|
|
|
1065
1081
|
if (this.otaStartOwnership?.outcome === "pending") {
|
|
1066
1082
|
void this.sendOtaStartWithWatchdogs()
|
|
1067
1083
|
}
|
|
1068
|
-
void BluetoothSdk.
|
|
1084
|
+
void BluetoothSdk.queryOtaStatus().catch(() => {})
|
|
1069
1085
|
this.armQueryReplyFallback("reconnect")
|
|
1070
1086
|
return true
|
|
1071
1087
|
}
|
|
@@ -1109,7 +1125,7 @@ class OtaInstallCoordinator {
|
|
|
1109
1125
|
if (noSessionYet) {
|
|
1110
1126
|
if (!isIdleStatus && this.otaStartOwnership?.outcome === "acknowledged") {
|
|
1111
1127
|
console.log("[OTA_PROGRESS] initial mount, acknowledged session has no cached status — reconciling")
|
|
1112
|
-
void BluetoothSdk.
|
|
1128
|
+
void BluetoothSdk.queryOtaStatus().catch(() => {})
|
|
1113
1129
|
this.armQueryReplyFallback("initial-mount")
|
|
1114
1130
|
return
|
|
1115
1131
|
}
|
|
@@ -1125,7 +1141,7 @@ class OtaInstallCoordinator {
|
|
|
1125
1141
|
void this.sendOtaStartWithWatchdogs()
|
|
1126
1142
|
} else {
|
|
1127
1143
|
console.log("[OTA_PROGRESS] initial mount, session exists, sending ota_query_status")
|
|
1128
|
-
void BluetoothSdk.
|
|
1144
|
+
void BluetoothSdk.queryOtaStatus().catch(() => {})
|
|
1129
1145
|
this.armQueryReplyFallback("initial-mount")
|
|
1130
1146
|
}
|
|
1131
1147
|
}
|
|
@@ -1154,7 +1170,7 @@ class OtaInstallCoordinator {
|
|
|
1154
1170
|
this.clearProgressTimeout()
|
|
1155
1171
|
this.onFirstActivity()
|
|
1156
1172
|
this.onFirstNonZeroProgress()
|
|
1157
|
-
void BluetoothSdk.
|
|
1173
|
+
void BluetoothSdk.queryOtaStatus().catch(() => {})
|
|
1158
1174
|
useGlassesStore.getState().setMtkUpdatedThisSession(true)
|
|
1159
1175
|
}
|
|
1160
1176
|
|
|
@@ -1345,7 +1361,7 @@ class OtaInstallCoordinator {
|
|
|
1345
1361
|
const s = useGlassesStore.getState().otaStatus
|
|
1346
1362
|
if (s?.stepType === "apk" && s.phase === "install" && s.status === "in_progress") {
|
|
1347
1363
|
this.apkInstallPollInFlight = true
|
|
1348
|
-
void BluetoothSdk.
|
|
1364
|
+
void BluetoothSdk.queryOtaStatus()
|
|
1349
1365
|
.catch(() => {})
|
|
1350
1366
|
.finally(() => {
|
|
1351
1367
|
this.apkInstallPollInFlight = false
|
|
@@ -1665,6 +1681,13 @@ class OtaInstallCoordinator {
|
|
|
1665
1681
|
}
|
|
1666
1682
|
}
|
|
1667
1683
|
|
|
1684
|
+
private clearRestartRecoveryTimeout(): void {
|
|
1685
|
+
if (this.restartRecoveryTimeout) {
|
|
1686
|
+
clearTimeout(this.restartRecoveryTimeout)
|
|
1687
|
+
this.restartRecoveryTimeout = null
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1668
1691
|
private clearLegacyApkSettleTimer(): void {
|
|
1669
1692
|
if (this.legacyApkSettleTimer) {
|
|
1670
1693
|
clearTimeout(this.legacyApkSettleTimer)
|
|
@@ -1703,6 +1726,7 @@ class OtaInstallCoordinator {
|
|
|
1703
1726
|
this.clearPingInterval()
|
|
1704
1727
|
this.clearLegacyApkSettleTimer()
|
|
1705
1728
|
this.clearMtkSimulationTimers()
|
|
1729
|
+
this.clearRestartRecoveryTimeout()
|
|
1706
1730
|
}
|
|
1707
1731
|
}
|
|
1708
1732
|
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Started by `engine.start()`. Idempotent.
|
|
12
12
|
*/
|
|
13
|
-
import BluetoothSdk from "@mentra/bluetooth-sdk
|
|
14
|
-
import type {OtaStatus} from "@mentra/bluetooth-sdk/internal"
|
|
13
|
+
import BluetoothSdk, {type OtaStatus} from "@mentra/bluetooth-sdk"
|
|
15
14
|
import GlobalEventEmitter from "../utils/GlobalEventEmitter"
|
|
16
15
|
import {useGlassesStore} from "../stores/glasses"
|
|
17
16
|
import {handleOtaClockSkewFromGlasses} from "./glassesClockSync"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import BluetoothSdk from "@mentra/bluetooth-sdk
|
|
2
|
-
import
|
|
1
|
+
import BluetoothSdk, {type OtaUpdateInfo} from "@mentra/bluetooth-sdk"
|
|
2
|
+
import {ENGINE_RELEASE_METADATA} from "../generated/releaseMetadata"
|
|
3
3
|
import {getGlassesSystemTimeMs, isGlassesConnected, useGlassesStore, waitForGlassesState} from "../stores/glasses"
|
|
4
4
|
import {maybeFixGlassesClockFromVersionInfo} from "./glassesClockSync"
|
|
5
5
|
import {hasConfiguredModernOtaManifestPin, resolveOtaManifestUrl} from "./otaManifestUrl"
|
|
6
|
+
import {resolveOtaReleaseVersion} from "./otaReleaseVersion"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Phone-side mirror of ASG's `DOWNGRADE_FLOOR_VERSION_CODE`. Set to the versionCode of the first
|
|
@@ -38,6 +39,7 @@ export interface BesFirmware {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export interface VersionJson {
|
|
42
|
+
releaseVersion?: string
|
|
41
43
|
apps?: {
|
|
42
44
|
[packageName: string]: VersionInfo
|
|
43
45
|
}
|
|
@@ -66,6 +68,8 @@ export interface OtaCheckResult {
|
|
|
66
68
|
* instead of racing it with a second fetch of the same URL.
|
|
67
69
|
*/
|
|
68
70
|
manifestBody: string | null
|
|
71
|
+
/** Coordinated release identity for the selected OTA pin. */
|
|
72
|
+
releaseVersion: string | null
|
|
69
73
|
/**
|
|
70
74
|
* Why the check failed, when it did. "network" is retryable (connection/server
|
|
71
75
|
* trouble); "pin_unavailable" is permanent for this app build (manifest 404/gone,
|
|
@@ -106,6 +110,7 @@ function emptyCheckResult(skippedReason?: OtaCheckSkippedReason): OtaCheckCurren
|
|
|
106
110
|
besVersion: null,
|
|
107
111
|
isApkDowngrade: false,
|
|
108
112
|
manifestBody: null,
|
|
113
|
+
releaseVersion: null,
|
|
109
114
|
updateInfo: null,
|
|
110
115
|
isRequired: true,
|
|
111
116
|
skippedReason,
|
|
@@ -122,7 +127,7 @@ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResul
|
|
|
122
127
|
try {
|
|
123
128
|
const response = await fetch(url)
|
|
124
129
|
if (!response.ok) {
|
|
125
|
-
console.
|
|
130
|
+
console.warn("Failed to fetch version info:", response.status)
|
|
126
131
|
// 4xx: the manifest does not exist (or is gone) — permanent for this app
|
|
127
132
|
// build, retrying cannot help. Everything else is transient server/network
|
|
128
133
|
// trouble and retryable.
|
|
@@ -134,11 +139,11 @@ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResul
|
|
|
134
139
|
try {
|
|
135
140
|
return {json: await response.json()}
|
|
136
141
|
} catch (parseError) {
|
|
137
|
-
console.
|
|
142
|
+
console.warn("OTA: manifest is not valid JSON:", parseError)
|
|
138
143
|
return {json: null, failureReason: "pin_unavailable"}
|
|
139
144
|
}
|
|
140
145
|
} catch (error) {
|
|
141
|
-
console.
|
|
146
|
+
console.warn("OTA: Error fetching version info:", error)
|
|
142
147
|
return {json: null, failureReason: "network"}
|
|
143
148
|
}
|
|
144
149
|
}
|
|
@@ -298,6 +303,7 @@ export async function checkForOtaUpdate(
|
|
|
298
303
|
besVersion: null,
|
|
299
304
|
isApkDowngrade: false,
|
|
300
305
|
manifestBody: null,
|
|
306
|
+
releaseVersion: null,
|
|
301
307
|
checkFailureReason: fetched.failureReason ?? "network",
|
|
302
308
|
}
|
|
303
309
|
}
|
|
@@ -329,6 +335,7 @@ export async function checkForOtaUpdate(
|
|
|
329
335
|
besVersion: null,
|
|
330
336
|
isApkDowngrade: false,
|
|
331
337
|
manifestBody: null,
|
|
338
|
+
releaseVersion: null,
|
|
332
339
|
checkFailureReason: "pin_unavailable",
|
|
333
340
|
}
|
|
334
341
|
}
|
|
@@ -367,6 +374,12 @@ export async function checkForOtaUpdate(
|
|
|
367
374
|
besVersion: versionJson?.bes_firmware?.version || null,
|
|
368
375
|
isApkDowngrade: apkDirection === "downgrade",
|
|
369
376
|
manifestBody: versionJson ? JSON.stringify(versionJson) : null,
|
|
377
|
+
releaseVersion: resolveOtaReleaseVersion({
|
|
378
|
+
manifestReleaseVersion: versionJson.releaseVersion,
|
|
379
|
+
manifestUrl: otaVersionUrl,
|
|
380
|
+
packagedManifestUrl: ENGINE_RELEASE_METADATA.otaManifestUrl,
|
|
381
|
+
packagedReleaseIdentity: ENGINE_RELEASE_METADATA.releaseIdentity,
|
|
382
|
+
}),
|
|
370
383
|
}
|
|
371
384
|
} catch (error) {
|
|
372
385
|
console.error("Error checking for OTA update:", error)
|
|
@@ -379,6 +392,7 @@ export async function checkForOtaUpdate(
|
|
|
379
392
|
besVersion: null,
|
|
380
393
|
isApkDowngrade: false,
|
|
381
394
|
manifestBody: null,
|
|
395
|
+
releaseVersion: null,
|
|
382
396
|
checkFailureReason: "network",
|
|
383
397
|
}
|
|
384
398
|
}
|
|
@@ -406,9 +420,13 @@ export async function checkCurrentGlassesForUpdate(
|
|
|
406
420
|
}
|
|
407
421
|
|
|
408
422
|
if (refreshVersionInfo) {
|
|
409
|
-
void BluetoothSdk.requestVersionInfo()
|
|
410
|
-
|
|
411
|
-
|
|
423
|
+
void BluetoothSdk.requestVersionInfo()
|
|
424
|
+
.then((versionInfo) => {
|
|
425
|
+
useGlassesStore.getState().setGlassesInfo(versionInfo)
|
|
426
|
+
})
|
|
427
|
+
.catch((error) => {
|
|
428
|
+
console.warn("OTA: Failed to request version_info from glasses:", error)
|
|
429
|
+
})
|
|
412
430
|
}
|
|
413
431
|
|
|
414
432
|
let buildNumber = useGlassesStore.getState().buildNumber
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as RNFS from "@dr.pogodin/react-native-fs"
|
|
2
|
-
import {
|
|
2
|
+
import {otaLocalNetwork, type OtaLocalNetworkDownloadProgress} from "@mentra/bluetooth-sdk/ota-transport"
|
|
3
3
|
import {Buffer} from "buffer"
|
|
4
4
|
import {Platform} from "react-native"
|
|
5
5
|
import WifiManager from "react-native-wifi-reborn"
|
|
@@ -17,7 +17,7 @@ export function shouldUseScopedLocalNetwork(os: string, moduleAvailable: boolean
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
function supportsScopedConnection(): boolean {
|
|
20
|
-
return shouldUseScopedLocalNetwork(Platform.OS,
|
|
20
|
+
return shouldUseScopedLocalNetwork(Platform.OS, otaLocalNetwork.isAvailable())
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function normalizeHeaders(headers?: HeadersInit): Record<string, string> {
|
|
@@ -48,25 +48,25 @@ export const localNetworkTransport = {
|
|
|
48
48
|
await WifiManager.connectToProtectedSSID(ssid, password, false, false)
|
|
49
49
|
return undefined
|
|
50
50
|
}
|
|
51
|
-
const result = await
|
|
51
|
+
const result = await otaLocalNetwork.connect(ssid, password)
|
|
52
52
|
scopedConnectionActive = true
|
|
53
53
|
return result.localAddress
|
|
54
54
|
},
|
|
55
55
|
|
|
56
56
|
async disconnect(): Promise<void> {
|
|
57
57
|
if (supportsScopedConnection()) {
|
|
58
|
-
await
|
|
58
|
+
await otaLocalNetwork.disconnect().catch(() => {})
|
|
59
59
|
}
|
|
60
60
|
scopedConnectionActive = false
|
|
61
61
|
nativeJobs.clear()
|
|
62
62
|
},
|
|
63
63
|
|
|
64
64
|
async fetch(url: string | URL | Request, init?: RequestInit, timeoutMs = 30_000): Promise<Response> {
|
|
65
|
-
if (!scopedConnectionActive || !
|
|
65
|
+
if (!scopedConnectionActive || !otaLocalNetwork.isAvailable() || typeof url !== "string") {
|
|
66
66
|
return globalThis.fetch(url, init)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const nativeModule =
|
|
69
|
+
const nativeModule = otaLocalNetwork
|
|
70
70
|
const id = requestId("request")
|
|
71
71
|
const signal = init?.signal
|
|
72
72
|
const cancel = () => void nativeModule.cancel(id)
|
|
@@ -92,13 +92,13 @@ export const localNetworkTransport = {
|
|
|
92
92
|
},
|
|
93
93
|
|
|
94
94
|
downloadFile(options: DownloadOptions): DownloadHandle {
|
|
95
|
-
if (!scopedConnectionActive || !
|
|
95
|
+
if (!scopedConnectionActive || !otaLocalNetwork.isAvailable()) return RNFS.downloadFile(options)
|
|
96
96
|
|
|
97
97
|
const jobId = nextJobId++
|
|
98
98
|
const id = requestId("download")
|
|
99
99
|
let began = false
|
|
100
100
|
nativeJobs.set(jobId, id)
|
|
101
|
-
const subscription =
|
|
101
|
+
const subscription = otaLocalNetwork.onDownloadProgress((event: OtaLocalNetworkDownloadProgress) => {
|
|
102
102
|
if (event.requestId !== id) return
|
|
103
103
|
if (!began) {
|
|
104
104
|
began = true
|
|
@@ -116,14 +116,15 @@ export const localNetworkTransport = {
|
|
|
116
116
|
})
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
-
const promise =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
const promise = otaLocalNetwork
|
|
120
|
+
.download(
|
|
121
|
+
id,
|
|
122
|
+
options.fromUrl,
|
|
123
|
+
options.toFile,
|
|
124
|
+
options.headers || {},
|
|
125
|
+
options.connectionTimeout || 30_000,
|
|
126
|
+
options.readTimeout || 30_000,
|
|
127
|
+
)
|
|
127
128
|
.then((result) => {
|
|
128
129
|
if (!began) {
|
|
129
130
|
options.begin?.({
|
|
@@ -154,16 +155,16 @@ export const localNetworkTransport = {
|
|
|
154
155
|
|
|
155
156
|
stopDownload(jobId: number): void {
|
|
156
157
|
const nativeId = nativeJobs.get(jobId)
|
|
157
|
-
if (nativeId &&
|
|
158
|
+
if (nativeId && otaLocalNetwork.isAvailable()) {
|
|
158
159
|
cancelledNativeJobs.add(jobId)
|
|
159
|
-
void
|
|
160
|
+
void otaLocalNetwork.cancel(nativeId)
|
|
160
161
|
return
|
|
161
162
|
}
|
|
162
163
|
RNFS.stopDownload(jobId)
|
|
163
164
|
},
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
otaLocalNetwork.onNetworkLost(() => {
|
|
167
168
|
scopedConnectionActive = false
|
|
168
169
|
nativeJobs.clear()
|
|
169
170
|
})
|