@mentra/engine 3.2.0-dev.200 → 3.2.0-dev.208
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/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +3 -7
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +7 -20
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +16 -99
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/asg/galleryNotices.d.ts +1 -1
- package/build/services/asg/galleryNotices.d.ts.map +1 -1
- package/build/services/asg/galleryNotices.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts +14 -0
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +232 -96
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +16 -0
- package/build/services/slimStreamStatus.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/services/DeviceEventRouter.ts +5 -9
- package/src/services/PhoneStreamCoordinator.ts +23 -128
- package/src/services/asg/galleryNotices.ts +1 -0
- package/src/services/asg/gallerySyncService.ts +260 -112
- package/src/services/slimStreamStatus.ts +10 -1
- package/build/services/StreamLifecycleController.d.ts +0 -85
- package/build/services/StreamLifecycleController.d.ts.map +0 -1
- package/build/services/StreamLifecycleController.js +0 -173
- package/build/services/StreamLifecycleController.js.map +0 -1
- package/src/services/StreamLifecycleController.ts +0 -243
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drives stream liveliness checks via a keep-alive heartbeat:
|
|
3
|
-
* `setActive(true)` starts the timer; each tick sends a keep-alive with a
|
|
4
|
-
* fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.
|
|
5
|
-
*
|
|
6
|
-
* Phone-owned successor to the retired cloud stream lifecycle controller.
|
|
7
|
-
* It uses the local `LifecycleLogger` interface because pino is not a phone
|
|
8
|
-
* dependency.
|
|
9
|
-
*
|
|
10
|
-
* Timers MUST go through BgTimer. React Native pauses plain `setInterval`
|
|
11
|
-
* while MentraOS is backgrounded; glasses WHIP then auto-stops after 60s
|
|
12
|
-
* without `keep_stream_alive`, and Mentra Call recovers by restarting ingest.
|
|
13
|
-
*/
|
|
14
|
-
function productionTimers() {
|
|
15
|
-
// Lazy so unit tests that inject timers never parse react-native.
|
|
16
|
-
const { BgTimer } = require("../utils/timers");
|
|
17
|
-
return BgTimer;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Keep-alive heartbeat. Activate to start ticks; each tick sends keep-alive
|
|
21
|
-
* with a fresh ackId and arms a timeout. `maxMissedAcks` consecutive misses
|
|
22
|
-
* fire `onTimeout`, and the caller is expected to tear the stream down.
|
|
23
|
-
*/
|
|
24
|
-
export class StreamLifecycleController {
|
|
25
|
-
callbacks;
|
|
26
|
-
keepAliveTimer;
|
|
27
|
-
pendingAcks = new Map();
|
|
28
|
-
missedAcks = 0;
|
|
29
|
-
lastActivityMs;
|
|
30
|
-
active = false;
|
|
31
|
-
disposed = false;
|
|
32
|
-
logger;
|
|
33
|
-
streamId;
|
|
34
|
-
keepAliveIntervalMs;
|
|
35
|
-
ackTimeoutMs;
|
|
36
|
-
maxMissedAcks;
|
|
37
|
-
shouldSendKeepAlive;
|
|
38
|
-
now;
|
|
39
|
-
timers;
|
|
40
|
-
constructor(options, callbacks) {
|
|
41
|
-
this.callbacks = callbacks;
|
|
42
|
-
this.logger = options.logger.child({ component: "StreamLifecycle" });
|
|
43
|
-
this.streamId = options.streamId;
|
|
44
|
-
this.keepAliveIntervalMs = options.keepAliveIntervalMs;
|
|
45
|
-
this.ackTimeoutMs = options.ackTimeoutMs;
|
|
46
|
-
this.maxMissedAcks = options.maxMissedAcks;
|
|
47
|
-
this.shouldSendKeepAlive = options.shouldSendKeepAlive;
|
|
48
|
-
this.now = options.now ?? (() => Date.now());
|
|
49
|
-
this.timers = options.timers ?? productionTimers();
|
|
50
|
-
this.lastActivityMs = this.now();
|
|
51
|
-
}
|
|
52
|
-
setActive(active) {
|
|
53
|
-
if (this.disposed || this.active === active)
|
|
54
|
-
return;
|
|
55
|
-
this.logger.debug({ streamId: this.streamId, active }, "Updating lifecycle active state");
|
|
56
|
-
this.active = active;
|
|
57
|
-
if (active) {
|
|
58
|
-
this.startTimer();
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
this.stopTimer();
|
|
62
|
-
this.clearPendingAcks();
|
|
63
|
-
this.missedAcks = 0;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
recordActivity() {
|
|
67
|
-
this.lastActivityMs = this.now();
|
|
68
|
-
this.missedAcks = 0;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Send one keep-alive immediately instead of waiting for the next interval.
|
|
72
|
-
* Used when the BLE link comes back after a suspension: the glasses
|
|
73
|
-
* publisher's 60s watchdog has been running the whole time, so the first
|
|
74
|
-
* heartbeat after resume must not wait up to another full interval.
|
|
75
|
-
*/
|
|
76
|
-
tickNow() {
|
|
77
|
-
if (this.disposed || !this.active)
|
|
78
|
-
return;
|
|
79
|
-
void this.tick();
|
|
80
|
-
}
|
|
81
|
-
handleAck(ackId) {
|
|
82
|
-
if (this.disposed)
|
|
83
|
-
return;
|
|
84
|
-
const ackInfo = this.pendingAcks.get(ackId);
|
|
85
|
-
if (!ackInfo) {
|
|
86
|
-
this.logger.warn({ streamId: this.streamId, ackId }, "Received unknown keep-alive ACK");
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
this.timers.clearTimeout(ackInfo.timeout);
|
|
90
|
-
this.pendingAcks.delete(ackId);
|
|
91
|
-
this.recordActivity();
|
|
92
|
-
this.callbacks.onKeepAliveAcked?.(ackId, this.now() - ackInfo.sentAt);
|
|
93
|
-
}
|
|
94
|
-
dispose() {
|
|
95
|
-
if (this.disposed)
|
|
96
|
-
return;
|
|
97
|
-
this.disposed = true;
|
|
98
|
-
this.stopTimer();
|
|
99
|
-
this.clearPendingAcks();
|
|
100
|
-
this.logger.debug({ streamId: this.streamId }, "Lifecycle disposed");
|
|
101
|
-
}
|
|
102
|
-
getLastActivityMs() {
|
|
103
|
-
return this.lastActivityMs;
|
|
104
|
-
}
|
|
105
|
-
startTimer() {
|
|
106
|
-
if (this.keepAliveTimer)
|
|
107
|
-
return;
|
|
108
|
-
this.keepAliveTimer = this.timers.setInterval(() => {
|
|
109
|
-
void this.tick();
|
|
110
|
-
}, this.keepAliveIntervalMs);
|
|
111
|
-
this.logger.debug({ streamId: this.streamId }, "Keep-alive timer started");
|
|
112
|
-
}
|
|
113
|
-
stopTimer() {
|
|
114
|
-
if (this.keepAliveTimer) {
|
|
115
|
-
this.timers.clearInterval(this.keepAliveTimer);
|
|
116
|
-
this.keepAliveTimer = undefined;
|
|
117
|
-
this.logger.debug({ streamId: this.streamId }, "Keep-alive timer stopped");
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async tick() {
|
|
121
|
-
if (this.disposed || !this.active)
|
|
122
|
-
return;
|
|
123
|
-
if (this.shouldSendKeepAlive && !this.shouldSendKeepAlive()) {
|
|
124
|
-
this.logger.warn({ streamId: this.streamId }, "Skipping keep-alive send because transport is unavailable");
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const ackId = this.createAckId();
|
|
128
|
-
const sentAt = this.now();
|
|
129
|
-
const timeout = this.timers.setTimeout(() => {
|
|
130
|
-
this.onAckTimeout(ackId, sentAt);
|
|
131
|
-
}, this.ackTimeoutMs);
|
|
132
|
-
this.pendingAcks.set(ackId, { sentAt, timeout });
|
|
133
|
-
this.callbacks.onKeepAliveSent?.(ackId);
|
|
134
|
-
try {
|
|
135
|
-
await this.callbacks.sendKeepAlive(ackId);
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
this.logger.error({ streamId: this.streamId, ackId, error }, "Error sending keep-alive");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
onAckTimeout(ackId, sentAt) {
|
|
142
|
-
if (this.disposed)
|
|
143
|
-
return;
|
|
144
|
-
// Race guard: handleAck may have processed the ACK between the timeout
|
|
145
|
-
// being scheduled and this callback running. In that case the ackId is
|
|
146
|
-
// no longer pending — counting it as missed would falsely escalate.
|
|
147
|
-
if (!this.pendingAcks.has(ackId))
|
|
148
|
-
return;
|
|
149
|
-
this.pendingAcks.delete(ackId);
|
|
150
|
-
this.missedAcks += 1;
|
|
151
|
-
const ageMs = this.now() - sentAt;
|
|
152
|
-
this.logger.warn({ streamId: this.streamId, ackId, missedAcks: this.missedAcks, ageMs }, "Keep-alive ACK timeout");
|
|
153
|
-
this.callbacks.onKeepAliveMissed?.(ackId, ageMs, this.missedAcks);
|
|
154
|
-
if (this.missedAcks >= this.maxMissedAcks) {
|
|
155
|
-
this.logger.error({
|
|
156
|
-
streamId: this.streamId,
|
|
157
|
-
missedAcks: this.missedAcks,
|
|
158
|
-
maxMissedAcks: this.maxMissedAcks,
|
|
159
|
-
}, "Maximum missed ACKs reached; triggering timeout");
|
|
160
|
-
void this.callbacks.onTimeout();
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
clearPendingAcks() {
|
|
164
|
-
for (const { timeout } of this.pendingAcks.values()) {
|
|
165
|
-
this.timers.clearTimeout(timeout);
|
|
166
|
-
}
|
|
167
|
-
this.pendingAcks.clear();
|
|
168
|
-
}
|
|
169
|
-
createAckId() {
|
|
170
|
-
return `a${this.now().toString(36).slice(-5)}`;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
//# sourceMappingURL=StreamLifecycleController.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StreamLifecycleController.js","sourceRoot":"","sources":["../../src/services/StreamLifecycleController.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Drives stream liveliness checks via a keep-alive heartbeat:\n * `setActive(true)` starts the timer; each tick sends a keep-alive with a\n * fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.\n *\n * Phone-owned successor to the retired cloud stream lifecycle controller.\n * It uses the local `LifecycleLogger` interface because pino is not a phone\n * dependency.\n *\n * Timers MUST go through BgTimer. React Native pauses plain `setInterval`\n * while MentraOS is backgrounded; glasses WHIP then auto-stops after 60s\n * without `keep_stream_alive`, and Mentra Call recovers by restarting ingest.\n */\n\nexport interface LifecycleLogger {\n child(bindings: Record<string, unknown>): LifecycleLogger\n debug(...args: unknown[]): void\n warn(...args: unknown[]): void\n error(...args: unknown[]): void\n}\n\ninterface StreamLifecycleCallbacks {\n sendKeepAlive: (ackId: string) => Promise<void> | void\n onTimeout: () => Promise<void> | void\n onKeepAliveSent?: (ackId: string) => void\n onKeepAliveAcked?: (ackId: string, ageMs: number) => void\n onKeepAliveMissed?: (ackId: string, ageMs: number, missedCount: number) => void\n}\n\nexport interface StreamLifecycleTimerApi {\n setInterval: (callback: () => void, delay: number) => number\n clearInterval: (intervalId: number) => void\n setTimeout: (callback: () => void, delay: number) => number\n clearTimeout: (timeoutId: number) => void\n}\n\nexport interface StreamLifecycleOptions {\n logger: LifecycleLogger\n streamId: string\n keepAliveIntervalMs: number\n ackTimeoutMs: number\n maxMissedAcks: number\n shouldSendKeepAlive?: () => boolean\n now?: () => number\n timers?: StreamLifecycleTimerApi\n}\n\ninterface PendingAckInfo {\n sentAt: number\n timeout: number\n}\n\nfunction productionTimers(): StreamLifecycleTimerApi {\n // Lazy so unit tests that inject timers never parse react-native.\n const {BgTimer} = require(\"../utils/timers\") as {BgTimer: StreamLifecycleTimerApi}\n return BgTimer\n}\n\n/**\n * Keep-alive heartbeat. Activate to start ticks; each tick sends keep-alive\n * with a fresh ackId and arms a timeout. `maxMissedAcks` consecutive misses\n * fire `onTimeout`, and the caller is expected to tear the stream down.\n */\nexport class StreamLifecycleController {\n private keepAliveTimer?: number\n private pendingAcks: Map<string, PendingAckInfo> = new Map()\n private missedAcks = 0\n private lastActivityMs: number\n private active = false\n private disposed = false\n\n private readonly logger: LifecycleLogger\n private readonly streamId: string\n private readonly keepAliveIntervalMs: number\n private readonly ackTimeoutMs: number\n private readonly maxMissedAcks: number\n private readonly shouldSendKeepAlive?: () => boolean\n private readonly now: () => number\n private readonly timers: StreamLifecycleTimerApi\n\n constructor(\n options: StreamLifecycleOptions,\n private readonly callbacks: StreamLifecycleCallbacks,\n ) {\n this.logger = options.logger.child({component: \"StreamLifecycle\"})\n this.streamId = options.streamId\n this.keepAliveIntervalMs = options.keepAliveIntervalMs\n this.ackTimeoutMs = options.ackTimeoutMs\n this.maxMissedAcks = options.maxMissedAcks\n this.shouldSendKeepAlive = options.shouldSendKeepAlive\n this.now = options.now ?? (() => Date.now())\n this.timers = options.timers ?? productionTimers()\n this.lastActivityMs = this.now()\n }\n\n setActive(active: boolean): void {\n if (this.disposed || this.active === active) return\n\n this.logger.debug({streamId: this.streamId, active}, \"Updating lifecycle active state\")\n\n this.active = active\n if (active) {\n this.startTimer()\n } else {\n this.stopTimer()\n this.clearPendingAcks()\n this.missedAcks = 0\n }\n }\n\n recordActivity(): void {\n this.lastActivityMs = this.now()\n this.missedAcks = 0\n }\n\n /**\n * Send one keep-alive immediately instead of waiting for the next interval.\n * Used when the BLE link comes back after a suspension: the glasses\n * publisher's 60s watchdog has been running the whole time, so the first\n * heartbeat after resume must not wait up to another full interval.\n */\n tickNow(): void {\n if (this.disposed || !this.active) return\n void this.tick()\n }\n\n handleAck(ackId: string): void {\n if (this.disposed) return\n\n const ackInfo = this.pendingAcks.get(ackId)\n if (!ackInfo) {\n this.logger.warn({streamId: this.streamId, ackId}, \"Received unknown keep-alive ACK\")\n return\n }\n\n this.timers.clearTimeout(ackInfo.timeout)\n this.pendingAcks.delete(ackId)\n this.recordActivity()\n\n this.callbacks.onKeepAliveAcked?.(ackId, this.now() - ackInfo.sentAt)\n }\n\n dispose(): void {\n if (this.disposed) return\n\n this.disposed = true\n this.stopTimer()\n this.clearPendingAcks()\n this.logger.debug({streamId: this.streamId}, \"Lifecycle disposed\")\n }\n\n getLastActivityMs(): number {\n return this.lastActivityMs\n }\n\n private startTimer(): void {\n if (this.keepAliveTimer) return\n this.keepAliveTimer = this.timers.setInterval(() => {\n void this.tick()\n }, this.keepAliveIntervalMs)\n this.logger.debug({streamId: this.streamId}, \"Keep-alive timer started\")\n }\n\n private stopTimer(): void {\n if (this.keepAliveTimer) {\n this.timers.clearInterval(this.keepAliveTimer)\n this.keepAliveTimer = undefined\n this.logger.debug({streamId: this.streamId}, \"Keep-alive timer stopped\")\n }\n }\n\n private async tick(): Promise<void> {\n if (this.disposed || !this.active) return\n\n if (this.shouldSendKeepAlive && !this.shouldSendKeepAlive()) {\n this.logger.warn(\n {streamId: this.streamId},\n \"Skipping keep-alive send because transport is unavailable\",\n )\n return\n }\n\n const ackId = this.createAckId()\n const sentAt = this.now()\n\n const timeout = this.timers.setTimeout(() => {\n this.onAckTimeout(ackId, sentAt)\n }, this.ackTimeoutMs)\n\n this.pendingAcks.set(ackId, {sentAt, timeout})\n this.callbacks.onKeepAliveSent?.(ackId)\n\n try {\n await this.callbacks.sendKeepAlive(ackId)\n } catch (error) {\n this.logger.error({streamId: this.streamId, ackId, error}, \"Error sending keep-alive\")\n }\n }\n\n private onAckTimeout(ackId: string, sentAt: number): void {\n if (this.disposed) return\n\n // Race guard: handleAck may have processed the ACK between the timeout\n // being scheduled and this callback running. In that case the ackId is\n // no longer pending — counting it as missed would falsely escalate.\n if (!this.pendingAcks.has(ackId)) return\n\n this.pendingAcks.delete(ackId)\n this.missedAcks += 1\n const ageMs = this.now() - sentAt\n\n this.logger.warn(\n {streamId: this.streamId, ackId, missedAcks: this.missedAcks, ageMs},\n \"Keep-alive ACK timeout\",\n )\n\n this.callbacks.onKeepAliveMissed?.(ackId, ageMs, this.missedAcks)\n\n if (this.missedAcks >= this.maxMissedAcks) {\n this.logger.error(\n {\n streamId: this.streamId,\n missedAcks: this.missedAcks,\n maxMissedAcks: this.maxMissedAcks,\n },\n \"Maximum missed ACKs reached; triggering timeout\",\n )\n\n void this.callbacks.onTimeout()\n }\n }\n\n private clearPendingAcks(): void {\n for (const {timeout} of this.pendingAcks.values()) {\n this.timers.clearTimeout(timeout)\n }\n this.pendingAcks.clear()\n }\n\n private createAckId(): string {\n return `a${this.now().toString(36).slice(-5)}`\n }\n}\n"]}
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drives stream liveliness checks via a keep-alive heartbeat:
|
|
3
|
-
* `setActive(true)` starts the timer; each tick sends a keep-alive with a
|
|
4
|
-
* fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.
|
|
5
|
-
*
|
|
6
|
-
* Phone-owned successor to the retired cloud stream lifecycle controller.
|
|
7
|
-
* It uses the local `LifecycleLogger` interface because pino is not a phone
|
|
8
|
-
* dependency.
|
|
9
|
-
*
|
|
10
|
-
* Timers MUST go through BgTimer. React Native pauses plain `setInterval`
|
|
11
|
-
* while MentraOS is backgrounded; glasses WHIP then auto-stops after 60s
|
|
12
|
-
* without `keep_stream_alive`, and Mentra Call recovers by restarting ingest.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export interface LifecycleLogger {
|
|
16
|
-
child(bindings: Record<string, unknown>): LifecycleLogger
|
|
17
|
-
debug(...args: unknown[]): void
|
|
18
|
-
warn(...args: unknown[]): void
|
|
19
|
-
error(...args: unknown[]): void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface StreamLifecycleCallbacks {
|
|
23
|
-
sendKeepAlive: (ackId: string) => Promise<void> | void
|
|
24
|
-
onTimeout: () => Promise<void> | void
|
|
25
|
-
onKeepAliveSent?: (ackId: string) => void
|
|
26
|
-
onKeepAliveAcked?: (ackId: string, ageMs: number) => void
|
|
27
|
-
onKeepAliveMissed?: (ackId: string, ageMs: number, missedCount: number) => void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface StreamLifecycleTimerApi {
|
|
31
|
-
setInterval: (callback: () => void, delay: number) => number
|
|
32
|
-
clearInterval: (intervalId: number) => void
|
|
33
|
-
setTimeout: (callback: () => void, delay: number) => number
|
|
34
|
-
clearTimeout: (timeoutId: number) => void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface StreamLifecycleOptions {
|
|
38
|
-
logger: LifecycleLogger
|
|
39
|
-
streamId: string
|
|
40
|
-
keepAliveIntervalMs: number
|
|
41
|
-
ackTimeoutMs: number
|
|
42
|
-
maxMissedAcks: number
|
|
43
|
-
shouldSendKeepAlive?: () => boolean
|
|
44
|
-
now?: () => number
|
|
45
|
-
timers?: StreamLifecycleTimerApi
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface PendingAckInfo {
|
|
49
|
-
sentAt: number
|
|
50
|
-
timeout: number
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function productionTimers(): StreamLifecycleTimerApi {
|
|
54
|
-
// Lazy so unit tests that inject timers never parse react-native.
|
|
55
|
-
const {BgTimer} = require("../utils/timers") as {BgTimer: StreamLifecycleTimerApi}
|
|
56
|
-
return BgTimer
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Keep-alive heartbeat. Activate to start ticks; each tick sends keep-alive
|
|
61
|
-
* with a fresh ackId and arms a timeout. `maxMissedAcks` consecutive misses
|
|
62
|
-
* fire `onTimeout`, and the caller is expected to tear the stream down.
|
|
63
|
-
*/
|
|
64
|
-
export class StreamLifecycleController {
|
|
65
|
-
private keepAliveTimer?: number
|
|
66
|
-
private pendingAcks: Map<string, PendingAckInfo> = new Map()
|
|
67
|
-
private missedAcks = 0
|
|
68
|
-
private lastActivityMs: number
|
|
69
|
-
private active = false
|
|
70
|
-
private disposed = false
|
|
71
|
-
|
|
72
|
-
private readonly logger: LifecycleLogger
|
|
73
|
-
private readonly streamId: string
|
|
74
|
-
private readonly keepAliveIntervalMs: number
|
|
75
|
-
private readonly ackTimeoutMs: number
|
|
76
|
-
private readonly maxMissedAcks: number
|
|
77
|
-
private readonly shouldSendKeepAlive?: () => boolean
|
|
78
|
-
private readonly now: () => number
|
|
79
|
-
private readonly timers: StreamLifecycleTimerApi
|
|
80
|
-
|
|
81
|
-
constructor(
|
|
82
|
-
options: StreamLifecycleOptions,
|
|
83
|
-
private readonly callbacks: StreamLifecycleCallbacks,
|
|
84
|
-
) {
|
|
85
|
-
this.logger = options.logger.child({component: "StreamLifecycle"})
|
|
86
|
-
this.streamId = options.streamId
|
|
87
|
-
this.keepAliveIntervalMs = options.keepAliveIntervalMs
|
|
88
|
-
this.ackTimeoutMs = options.ackTimeoutMs
|
|
89
|
-
this.maxMissedAcks = options.maxMissedAcks
|
|
90
|
-
this.shouldSendKeepAlive = options.shouldSendKeepAlive
|
|
91
|
-
this.now = options.now ?? (() => Date.now())
|
|
92
|
-
this.timers = options.timers ?? productionTimers()
|
|
93
|
-
this.lastActivityMs = this.now()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
setActive(active: boolean): void {
|
|
97
|
-
if (this.disposed || this.active === active) return
|
|
98
|
-
|
|
99
|
-
this.logger.debug({streamId: this.streamId, active}, "Updating lifecycle active state")
|
|
100
|
-
|
|
101
|
-
this.active = active
|
|
102
|
-
if (active) {
|
|
103
|
-
this.startTimer()
|
|
104
|
-
} else {
|
|
105
|
-
this.stopTimer()
|
|
106
|
-
this.clearPendingAcks()
|
|
107
|
-
this.missedAcks = 0
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
recordActivity(): void {
|
|
112
|
-
this.lastActivityMs = this.now()
|
|
113
|
-
this.missedAcks = 0
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Send one keep-alive immediately instead of waiting for the next interval.
|
|
118
|
-
* Used when the BLE link comes back after a suspension: the glasses
|
|
119
|
-
* publisher's 60s watchdog has been running the whole time, so the first
|
|
120
|
-
* heartbeat after resume must not wait up to another full interval.
|
|
121
|
-
*/
|
|
122
|
-
tickNow(): void {
|
|
123
|
-
if (this.disposed || !this.active) return
|
|
124
|
-
void this.tick()
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
handleAck(ackId: string): void {
|
|
128
|
-
if (this.disposed) return
|
|
129
|
-
|
|
130
|
-
const ackInfo = this.pendingAcks.get(ackId)
|
|
131
|
-
if (!ackInfo) {
|
|
132
|
-
this.logger.warn({streamId: this.streamId, ackId}, "Received unknown keep-alive ACK")
|
|
133
|
-
return
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
this.timers.clearTimeout(ackInfo.timeout)
|
|
137
|
-
this.pendingAcks.delete(ackId)
|
|
138
|
-
this.recordActivity()
|
|
139
|
-
|
|
140
|
-
this.callbacks.onKeepAliveAcked?.(ackId, this.now() - ackInfo.sentAt)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
dispose(): void {
|
|
144
|
-
if (this.disposed) return
|
|
145
|
-
|
|
146
|
-
this.disposed = true
|
|
147
|
-
this.stopTimer()
|
|
148
|
-
this.clearPendingAcks()
|
|
149
|
-
this.logger.debug({streamId: this.streamId}, "Lifecycle disposed")
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
getLastActivityMs(): number {
|
|
153
|
-
return this.lastActivityMs
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private startTimer(): void {
|
|
157
|
-
if (this.keepAliveTimer) return
|
|
158
|
-
this.keepAliveTimer = this.timers.setInterval(() => {
|
|
159
|
-
void this.tick()
|
|
160
|
-
}, this.keepAliveIntervalMs)
|
|
161
|
-
this.logger.debug({streamId: this.streamId}, "Keep-alive timer started")
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private stopTimer(): void {
|
|
165
|
-
if (this.keepAliveTimer) {
|
|
166
|
-
this.timers.clearInterval(this.keepAliveTimer)
|
|
167
|
-
this.keepAliveTimer = undefined
|
|
168
|
-
this.logger.debug({streamId: this.streamId}, "Keep-alive timer stopped")
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private async tick(): Promise<void> {
|
|
173
|
-
if (this.disposed || !this.active) return
|
|
174
|
-
|
|
175
|
-
if (this.shouldSendKeepAlive && !this.shouldSendKeepAlive()) {
|
|
176
|
-
this.logger.warn(
|
|
177
|
-
{streamId: this.streamId},
|
|
178
|
-
"Skipping keep-alive send because transport is unavailable",
|
|
179
|
-
)
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const ackId = this.createAckId()
|
|
184
|
-
const sentAt = this.now()
|
|
185
|
-
|
|
186
|
-
const timeout = this.timers.setTimeout(() => {
|
|
187
|
-
this.onAckTimeout(ackId, sentAt)
|
|
188
|
-
}, this.ackTimeoutMs)
|
|
189
|
-
|
|
190
|
-
this.pendingAcks.set(ackId, {sentAt, timeout})
|
|
191
|
-
this.callbacks.onKeepAliveSent?.(ackId)
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await this.callbacks.sendKeepAlive(ackId)
|
|
195
|
-
} catch (error) {
|
|
196
|
-
this.logger.error({streamId: this.streamId, ackId, error}, "Error sending keep-alive")
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private onAckTimeout(ackId: string, sentAt: number): void {
|
|
201
|
-
if (this.disposed) return
|
|
202
|
-
|
|
203
|
-
// Race guard: handleAck may have processed the ACK between the timeout
|
|
204
|
-
// being scheduled and this callback running. In that case the ackId is
|
|
205
|
-
// no longer pending — counting it as missed would falsely escalate.
|
|
206
|
-
if (!this.pendingAcks.has(ackId)) return
|
|
207
|
-
|
|
208
|
-
this.pendingAcks.delete(ackId)
|
|
209
|
-
this.missedAcks += 1
|
|
210
|
-
const ageMs = this.now() - sentAt
|
|
211
|
-
|
|
212
|
-
this.logger.warn(
|
|
213
|
-
{streamId: this.streamId, ackId, missedAcks: this.missedAcks, ageMs},
|
|
214
|
-
"Keep-alive ACK timeout",
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
this.callbacks.onKeepAliveMissed?.(ackId, ageMs, this.missedAcks)
|
|
218
|
-
|
|
219
|
-
if (this.missedAcks >= this.maxMissedAcks) {
|
|
220
|
-
this.logger.error(
|
|
221
|
-
{
|
|
222
|
-
streamId: this.streamId,
|
|
223
|
-
missedAcks: this.missedAcks,
|
|
224
|
-
maxMissedAcks: this.maxMissedAcks,
|
|
225
|
-
},
|
|
226
|
-
"Maximum missed ACKs reached; triggering timeout",
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
void this.callbacks.onTimeout()
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private clearPendingAcks(): void {
|
|
234
|
-
for (const {timeout} of this.pendingAcks.values()) {
|
|
235
|
-
this.timers.clearTimeout(timeout)
|
|
236
|
-
}
|
|
237
|
-
this.pendingAcks.clear()
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
private createAckId(): string {
|
|
241
|
-
return `a${this.now().toString(36).slice(-5)}`
|
|
242
|
-
}
|
|
243
|
-
}
|