@mentra/sdk 2.1.13 → 2.1.14
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/dist/app/session/events.d.ts +35 -23
- package/dist/app/session/events.d.ts.map +1 -1
- package/dist/app/session/events.js +46 -38
- package/dist/app/session/index.d.ts +13 -13
- package/dist/app/session/index.d.ts.map +1 -1
- package/dist/app/session/index.js +237 -204
- package/dist/index.d.ts +28 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -1
- package/dist/types/message-types.d.ts +1 -0
- package/dist/types/message-types.d.ts.map +1 -1
- package/dist/types/message-types.js +6 -5
- package/dist/types/messages/cloud-to-app.d.ts +15 -5
- package/dist/types/messages/cloud-to-app.d.ts.map +1 -1
- package/dist/types/messages/cloud-to-app.js +4 -0
- package/dist/types/streams.d.ts +9 -3
- package/dist/types/streams.d.ts.map +1 -1
- package/dist/types/streams.js +68 -19
- package/package.json +1 -1
@@ -26,11 +26,11 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
26
26
|
const cloud_to_app_1 = require("../../types/messages/cloud-to-app");
|
27
27
|
// List of event types that should never be subscribed to as streams
|
28
28
|
const APP_TO_APP_EVENT_TYPES = [
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
"app_message_received",
|
30
|
+
"app_user_joined",
|
31
|
+
"app_user_left",
|
32
|
+
"app_room_updated",
|
33
|
+
"app_direct_message_response",
|
34
34
|
];
|
35
35
|
/**
|
36
36
|
* 🚀 App Session Implementation
|
@@ -95,18 +95,21 @@ class AppSession {
|
|
95
95
|
autoReconnect: true, // Enable auto-reconnection by default for better reliability
|
96
96
|
maxReconnectAttempts: 3, // Default to 3 reconnection attempts for better resilience
|
97
97
|
reconnectDelay: 1000, // Start with 1 second delay (uses exponential backoff)
|
98
|
-
...config
|
98
|
+
...config,
|
99
99
|
};
|
100
100
|
this.appServer = this.config.appServer;
|
101
|
-
this.logger = this.appServer.logger.child({
|
101
|
+
this.logger = this.appServer.logger.child({
|
102
|
+
userId: this.config.userId,
|
103
|
+
service: "app-session",
|
104
|
+
});
|
102
105
|
this.userId = this.config.userId;
|
103
106
|
// Make sure the URL is correctly formatted to prevent double protocol issues
|
104
107
|
if (this.config.mentraOSWebsocketUrl) {
|
105
108
|
try {
|
106
109
|
const url = new URL(this.config.mentraOSWebsocketUrl);
|
107
|
-
if (![
|
110
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
108
111
|
// Fix URLs with incorrect protocol (e.g., 'ws://http://host')
|
109
|
-
const fixedUrl = this.config.mentraOSWebsocketUrl.replace(/^ws:\/\/http:\/\//,
|
112
|
+
const fixedUrl = this.config.mentraOSWebsocketUrl.replace(/^ws:\/\/http:\/\//, "ws://");
|
110
113
|
this.config.mentraOSWebsocketUrl = fixedUrl;
|
111
114
|
this.logger.warn(`⚠️ [${this.config.packageName}] Fixed malformed WebSocket URL: ${fixedUrl}`);
|
112
115
|
}
|
@@ -123,7 +126,7 @@ class AppSession {
|
|
123
126
|
if (this.config.mentraOSWebsocketUrl) {
|
124
127
|
try {
|
125
128
|
const url = new URL(this.config.mentraOSWebsocketUrl);
|
126
|
-
if (![
|
129
|
+
if (!["ws:", "wss:"].includes(url.protocol)) {
|
127
130
|
this.logger.error({ config: this.config }, `⚠️ [${this.config.packageName}] Invalid WebSocket URL protocol: ${url.protocol}. Should be ws: or wss:`);
|
128
131
|
}
|
129
132
|
}
|
@@ -156,14 +159,14 @@ class AppSession {
|
|
156
159
|
});
|
157
160
|
// Initialize dashboard API with this session instance
|
158
161
|
// Import DashboardManager dynamically to avoid circular dependency
|
159
|
-
const { DashboardManager } = require(
|
162
|
+
const { DashboardManager } = require("./dashboard");
|
160
163
|
this.dashboard = new DashboardManager(this, this.send.bind(this));
|
161
164
|
// Initialize camera module with session reference
|
162
|
-
this.camera = new camera_1.CameraModule(this.config.packageName, this.sessionId ||
|
163
|
-
this.logger.child({ module:
|
165
|
+
this.camera = new camera_1.CameraModule(this.config.packageName, this.sessionId || "unknown-session-id", this.send.bind(this), this, // Pass session reference
|
166
|
+
this.logger.child({ module: "camera" }));
|
164
167
|
// Initialize audio module with session reference
|
165
|
-
this.audio = new audio_1.AudioManager(this.config.packageName, this.sessionId ||
|
166
|
-
this.logger.child({ module:
|
168
|
+
this.audio = new audio_1.AudioManager(this.config.packageName, this.sessionId || "unknown-session-id", this.send.bind(this), this, // Pass session reference
|
169
|
+
this.logger.child({ module: "audio" }));
|
167
170
|
this.location = new location_1.LocationManager(this, this.send.bind(this));
|
168
171
|
}
|
169
172
|
/**
|
@@ -171,7 +174,7 @@ class AppSession {
|
|
171
174
|
* @returns The current session ID or 'unknown-session-id' if not connected
|
172
175
|
*/
|
173
176
|
getSessionId() {
|
174
|
-
return this.sessionId ||
|
177
|
+
return this.sessionId || "unknown-session-id";
|
175
178
|
}
|
176
179
|
/**
|
177
180
|
* Get the package name for this App
|
@@ -197,8 +200,8 @@ class AppSession {
|
|
197
200
|
* @throws Error if language code is invalid
|
198
201
|
* @deprecated Use session.events.onTranscriptionForLanguage() instead
|
199
202
|
*/
|
200
|
-
onTranscriptionForLanguage(language, handler) {
|
201
|
-
return this.events.onTranscriptionForLanguage(language, handler);
|
203
|
+
onTranscriptionForLanguage(language, handler, disableLanguageIdentification = false) {
|
204
|
+
return this.events.onTranscriptionForLanguage(language, handler, disableLanguageIdentification);
|
202
205
|
}
|
203
206
|
/**
|
204
207
|
* 🌐 Listen for speech translation events for a specific language pair
|
@@ -278,7 +281,7 @@ class AppSession {
|
|
278
281
|
subscribe(sub) {
|
279
282
|
let type;
|
280
283
|
let rate;
|
281
|
-
if (typeof sub ===
|
284
|
+
if (typeof sub === "string") {
|
282
285
|
type = sub;
|
283
286
|
}
|
284
287
|
else {
|
@@ -304,7 +307,7 @@ class AppSession {
|
|
304
307
|
*/
|
305
308
|
unsubscribe(sub) {
|
306
309
|
let type;
|
307
|
-
if (typeof sub ===
|
310
|
+
if (typeof sub === "string") {
|
308
311
|
type = sub;
|
309
312
|
}
|
310
313
|
else {
|
@@ -340,7 +343,7 @@ class AppSession {
|
|
340
343
|
this.sessionId = sessionId;
|
341
344
|
// Configure settings API client with the WebSocket URL and session ID
|
342
345
|
// This allows settings to be fetched from the correct server
|
343
|
-
this.settings.configureApiClient(this.config.packageName, this.config.mentraOSWebsocketUrl ||
|
346
|
+
this.settings.configureApiClient(this.config.packageName, this.config.mentraOSWebsocketUrl || "", sessionId);
|
344
347
|
// Update the sessionId in the camera module
|
345
348
|
if (this.camera) {
|
346
349
|
this.camera.updateSessionId(sessionId);
|
@@ -354,15 +357,16 @@ class AppSession {
|
|
354
357
|
// Clear previous resources if reconnecting
|
355
358
|
if (this.ws) {
|
356
359
|
// Don't call full dispose() as that would clear subscriptions
|
357
|
-
if (this.ws.readyState !== 3) {
|
360
|
+
if (this.ws.readyState !== 3) {
|
361
|
+
// 3 = CLOSED
|
358
362
|
this.ws.close();
|
359
363
|
}
|
360
364
|
this.ws = null;
|
361
365
|
}
|
362
366
|
// Validate WebSocket URL before attempting connection
|
363
367
|
if (!this.config.mentraOSWebsocketUrl) {
|
364
|
-
this.logger.error(
|
365
|
-
reject(new Error(
|
368
|
+
this.logger.error("WebSocket URL is missing or undefined");
|
369
|
+
reject(new Error("WebSocket URL is required"));
|
366
370
|
return;
|
367
371
|
}
|
368
372
|
// Add debug logging for connection attempts
|
@@ -371,18 +375,19 @@ class AppSession {
|
|
371
375
|
this.ws = new ws_1.WebSocket(this.config.mentraOSWebsocketUrl);
|
372
376
|
// Track WebSocket for automatic cleanup
|
373
377
|
this.resources.track(() => {
|
374
|
-
if (this.ws && this.ws.readyState !== 3) {
|
378
|
+
if (this.ws && this.ws.readyState !== 3) {
|
379
|
+
// 3 = CLOSED
|
375
380
|
this.ws.close();
|
376
381
|
}
|
377
382
|
});
|
378
|
-
this.ws.on(
|
383
|
+
this.ws.on("open", () => {
|
379
384
|
try {
|
380
385
|
this.sendConnectionInit();
|
381
386
|
}
|
382
387
|
catch (error) {
|
383
|
-
this.logger.error(error,
|
388
|
+
this.logger.error(error, "Error during connection initialization");
|
384
389
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
385
|
-
this.events.emit(
|
390
|
+
this.events.emit("error", new Error(`Connection initialization failed: ${errorMessage}`));
|
386
391
|
reject(error);
|
387
392
|
}
|
388
393
|
});
|
@@ -394,7 +399,7 @@ class AppSession {
|
|
394
399
|
try {
|
395
400
|
// Validate buffer before processing
|
396
401
|
if (data.length === 0) {
|
397
|
-
this.events.emit(
|
402
|
+
this.events.emit("error", new Error("Received empty binary data"));
|
398
403
|
return;
|
399
404
|
}
|
400
405
|
// Convert Node.js Buffer to ArrayBuffer safely
|
@@ -403,15 +408,15 @@ class AppSession {
|
|
403
408
|
const audioChunk = {
|
404
409
|
type: types_1.StreamType.AUDIO_CHUNK,
|
405
410
|
arrayBuffer: arrayBuf,
|
406
|
-
timestamp: new Date() // Ensure timestamp is present
|
411
|
+
timestamp: new Date(), // Ensure timestamp is present
|
407
412
|
};
|
408
413
|
this.handleMessage(audioChunk);
|
409
414
|
return;
|
410
415
|
}
|
411
416
|
catch (error) {
|
412
|
-
this.logger.error(error,
|
417
|
+
this.logger.error(error, "Error processing binary message:");
|
413
418
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
414
|
-
this.events.emit(
|
419
|
+
this.events.emit("error", new Error(`Failed to process binary message: ${errorMessage}`));
|
415
420
|
return;
|
416
421
|
}
|
417
422
|
}
|
@@ -423,59 +428,61 @@ class AppSession {
|
|
423
428
|
try {
|
424
429
|
// Convert string data to JSON safely
|
425
430
|
let jsonData;
|
426
|
-
if (typeof data ===
|
431
|
+
if (typeof data === "string") {
|
427
432
|
jsonData = data;
|
428
433
|
}
|
429
434
|
else if (Buffer.isBuffer(data)) {
|
430
|
-
jsonData = data.toString(
|
435
|
+
jsonData = data.toString("utf8");
|
431
436
|
}
|
432
437
|
else {
|
433
|
-
throw new Error(
|
438
|
+
throw new Error("Unknown message format");
|
434
439
|
}
|
435
440
|
// Validate JSON before parsing
|
436
|
-
if (!jsonData || jsonData.trim() ===
|
437
|
-
this.events.emit(
|
441
|
+
if (!jsonData || jsonData.trim() === "") {
|
442
|
+
this.events.emit("error", new Error("Received empty JSON message"));
|
438
443
|
return;
|
439
444
|
}
|
440
445
|
// Parse JSON with error handling
|
441
446
|
const message = JSON.parse(jsonData);
|
442
447
|
// Basic schema validation
|
443
|
-
if (!message ||
|
444
|
-
|
448
|
+
if (!message ||
|
449
|
+
typeof message !== "object" ||
|
450
|
+
!("type" in message)) {
|
451
|
+
this.events.emit("error", new Error("Malformed message: missing type property"));
|
445
452
|
return;
|
446
453
|
}
|
447
454
|
// Process the validated message
|
448
455
|
this.handleMessage(message);
|
449
456
|
}
|
450
457
|
catch (error) {
|
451
|
-
this.logger.error(error,
|
458
|
+
this.logger.error(error, "JSON parsing error");
|
452
459
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
453
|
-
this.events.emit(
|
460
|
+
this.events.emit("error", new Error(`Failed to parse JSON message: ${errorMessage}`));
|
454
461
|
}
|
455
462
|
}
|
456
463
|
catch (error) {
|
457
464
|
// Final catch - should never reach here if individual handlers work correctly
|
458
|
-
this.logger.error({ error },
|
465
|
+
this.logger.error({ error }, "Unhandled message processing error");
|
459
466
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
460
|
-
this.events.emit(
|
467
|
+
this.events.emit("error", new Error(`Unhandled message error: ${errorMessage}`));
|
461
468
|
}
|
462
469
|
};
|
463
|
-
this.ws.on(
|
470
|
+
this.ws.on("message", messageHandler);
|
464
471
|
// Track event handler removal for automatic cleanup
|
465
472
|
this.resources.track(() => {
|
466
473
|
if (this.ws) {
|
467
|
-
this.ws.off(
|
474
|
+
this.ws.off("message", messageHandler);
|
468
475
|
}
|
469
476
|
});
|
470
477
|
// Connection closure handler
|
471
478
|
const closeHandler = (code, reason) => {
|
472
|
-
const reasonStr = reason ? `: ${reason}` :
|
479
|
+
const reasonStr = reason ? `: ${reason}` : "";
|
473
480
|
const closeInfo = `Connection closed (code: ${code})${reasonStr}`;
|
474
481
|
// Emit the disconnected event with structured data for better handling
|
475
|
-
this.events.emit(
|
482
|
+
this.events.emit("disconnected", {
|
476
483
|
message: closeInfo,
|
477
484
|
code: code,
|
478
|
-
reason: reason ||
|
485
|
+
reason: reason || "",
|
479
486
|
wasClean: code === 1000 || code === 1001,
|
480
487
|
});
|
481
488
|
// Only attempt reconnection for abnormal closures
|
@@ -483,8 +490,8 @@ class AppSession {
|
|
483
490
|
// 1000 (Normal Closure) and 1001 (Going Away) are normal
|
484
491
|
// 1002-1015 are abnormal, and reason "App stopped" means intentional closure
|
485
492
|
// 1008 usually when the userSession no longer exists on server. i.e user disconnected from cloud.
|
486
|
-
const isNormalClosure =
|
487
|
-
const isManualStop = reason && reason.includes(
|
493
|
+
const isNormalClosure = code === 1000 || code === 1001 || code === 1008;
|
494
|
+
const isManualStop = reason && reason.includes("App stopped");
|
488
495
|
// Log closure details for diagnostics
|
489
496
|
this.logger.debug(`🔌 [${this.config.packageName}] WebSocket closed with code ${code}${reasonStr}`);
|
490
497
|
this.logger.debug(`🔌 [${this.config.packageName}] isNormalClosure: ${isNormalClosure}, isManualStop: ${isManualStop}`);
|
@@ -496,27 +503,27 @@ class AppSession {
|
|
496
503
|
this.logger.debug(`🔌 [${this.config.packageName}] Normal closure detected, not attempting reconnection`);
|
497
504
|
}
|
498
505
|
};
|
499
|
-
this.ws.on(
|
506
|
+
this.ws.on("close", closeHandler);
|
500
507
|
// Track event handler removal
|
501
508
|
this.resources.track(() => {
|
502
509
|
if (this.ws) {
|
503
|
-
this.ws.off(
|
510
|
+
this.ws.off("close", closeHandler);
|
504
511
|
}
|
505
512
|
});
|
506
513
|
// Connection error handler
|
507
514
|
const errorHandler = (error) => {
|
508
|
-
this.logger.error(error,
|
509
|
-
this.events.emit(
|
515
|
+
this.logger.error(error, "WebSocket error");
|
516
|
+
this.events.emit("error", error);
|
510
517
|
};
|
511
518
|
// Enhanced error handler with detailed logging
|
512
|
-
this.ws.on(
|
519
|
+
this.ws.on("error", (error) => {
|
513
520
|
this.logger.error(error, `⛔️⛔️⛔️ [${this.config.packageName}] WebSocket connection error: ${error.message}`);
|
514
521
|
// Try to provide more context
|
515
|
-
const errMsg = error.message ||
|
516
|
-
if (errMsg.includes(
|
522
|
+
const errMsg = error.message || "";
|
523
|
+
if (errMsg.includes("ECONNREFUSED")) {
|
517
524
|
this.logger.error(`⛔️⛔️⛔️ [${this.config.packageName}] Connection refused - Check if the server is running at the specified URL`);
|
518
525
|
}
|
519
|
-
else if (errMsg.includes(
|
526
|
+
else if (errMsg.includes("ETIMEDOUT")) {
|
520
527
|
this.logger.error(`⛔️⛔️⛔️ [${this.config.packageName}] Connection timed out - Check network connectivity and firewall rules`);
|
521
528
|
}
|
522
529
|
errorHandler(error);
|
@@ -524,7 +531,7 @@ class AppSession {
|
|
524
531
|
// Track event handler removal
|
525
532
|
this.resources.track(() => {
|
526
533
|
if (this.ws) {
|
527
|
-
this.ws.off(
|
534
|
+
this.ws.off("error", errorHandler);
|
528
535
|
}
|
529
536
|
});
|
530
537
|
// Set up connection success handler
|
@@ -538,10 +545,10 @@ class AppSession {
|
|
538
545
|
this.logger.error({
|
539
546
|
config: this.config,
|
540
547
|
sessionId: this.sessionId,
|
541
|
-
timeoutMs
|
548
|
+
timeoutMs,
|
542
549
|
}, `⏱️⏱️⏱️ [${this.config.packageName}] Connection timeout after ${timeoutMs}ms`);
|
543
|
-
this.events.emit(
|
544
|
-
reject(new Error(
|
550
|
+
this.events.emit("error", new Error(`Connection timeout after ${timeoutMs}ms`));
|
551
|
+
reject(new Error("Connection timeout"));
|
545
552
|
}, timeoutMs);
|
546
553
|
// Clear timeout on successful connection
|
547
554
|
const timeoutCleanup = this.events.onConnected(() => {
|
@@ -552,7 +559,7 @@ class AppSession {
|
|
552
559
|
this.resources.track(timeoutCleanup);
|
553
560
|
}
|
554
561
|
catch (error) {
|
555
|
-
this.logger.error(error,
|
562
|
+
this.logger.error(error, "Connection setup error");
|
556
563
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
557
564
|
reject(new Error(`Failed to setup connection: ${errorMessage}`));
|
558
565
|
}
|
@@ -621,7 +628,7 @@ class AppSession {
|
|
621
628
|
const newSubscriptions = this.subscriptionSettingsHandler(this.settingsData);
|
622
629
|
// Update all subscriptions at once
|
623
630
|
this.subscriptions.clear();
|
624
|
-
newSubscriptions.forEach(subscription => {
|
631
|
+
newSubscriptions.forEach((subscription) => {
|
625
632
|
this.subscriptions.add(subscription);
|
626
633
|
});
|
627
634
|
// Send subscription update to cloud if connected
|
@@ -630,9 +637,9 @@ class AppSession {
|
|
630
637
|
}
|
631
638
|
}
|
632
639
|
catch (error) {
|
633
|
-
this.logger.error(error,
|
640
|
+
this.logger.error(error, "Error updating subscriptions from settings");
|
634
641
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
635
|
-
this.events.emit(
|
642
|
+
this.events.emit("error", new Error(`Failed to update subscriptions: ${errorMessage}`));
|
636
643
|
}
|
637
644
|
}
|
638
645
|
/**
|
@@ -645,7 +652,7 @@ class AppSession {
|
|
645
652
|
// Update the settings manager with the new settings
|
646
653
|
this.settings.updateSettings(newSettings);
|
647
654
|
// Emit update event for backwards compatibility
|
648
|
-
this.events.emit(
|
655
|
+
this.events.emit("settings_update", this.settingsData);
|
649
656
|
// Check if we should update subscriptions
|
650
657
|
if (this.shouldUpdateSubscriptionsOnSettingsChange) {
|
651
658
|
this.updateSubscriptionsFromSettings();
|
@@ -665,7 +672,7 @@ class AppSession {
|
|
665
672
|
return parsedConfig;
|
666
673
|
}
|
667
674
|
else {
|
668
|
-
throw new Error(
|
675
|
+
throw new Error("Invalid App configuration format");
|
669
676
|
}
|
670
677
|
}
|
671
678
|
catch (error) {
|
@@ -695,11 +702,11 @@ class AppSession {
|
|
695
702
|
}
|
696
703
|
static convertToHttps(rawUrl) {
|
697
704
|
if (!rawUrl)
|
698
|
-
return
|
705
|
+
return "";
|
699
706
|
// Remove ws:// or wss://
|
700
|
-
let url = rawUrl.replace(/^wss?:\/\//,
|
707
|
+
let url = rawUrl.replace(/^wss?:\/\//, "");
|
701
708
|
// Remove trailing /app-ws
|
702
|
-
url = url.replace(/\/app-ws$/,
|
709
|
+
url = url.replace(/\/app-ws$/, "");
|
703
710
|
// Prepend https://
|
704
711
|
return `https://${url}`;
|
705
712
|
}
|
@@ -710,13 +717,13 @@ class AppSession {
|
|
710
717
|
*/
|
711
718
|
getDefaultSettings() {
|
712
719
|
if (!this.appConfig) {
|
713
|
-
throw new Error(
|
720
|
+
throw new Error("App configuration not loaded. Call loadConfigFromJson first.");
|
714
721
|
}
|
715
722
|
return this.appConfig.settings
|
716
|
-
.filter((s) => s.type !==
|
723
|
+
.filter((s) => s.type !== "group")
|
717
724
|
.map((s) => ({
|
718
725
|
...s,
|
719
|
-
value: s.defaultValue // Set value to defaultValue
|
726
|
+
value: s.defaultValue, // Set value to defaultValue
|
720
727
|
}));
|
721
728
|
}
|
722
729
|
/**
|
@@ -727,7 +734,7 @@ class AppSession {
|
|
727
734
|
getSettingSchema(key) {
|
728
735
|
if (!this.appConfig)
|
729
736
|
return undefined;
|
730
|
-
const setting = this.appConfig.settings.find((s) => s.type !==
|
737
|
+
const setting = this.appConfig.settings.find((s) => s.type !== "group" && "key" in s && s.key === key);
|
731
738
|
return setting;
|
732
739
|
}
|
733
740
|
// =====================================
|
@@ -740,7 +747,7 @@ class AppSession {
|
|
740
747
|
try {
|
741
748
|
// Validate message before processing
|
742
749
|
if (!this.validateMessage(message)) {
|
743
|
-
this.events.emit(
|
750
|
+
this.events.emit("error", new Error("Invalid message format received"));
|
744
751
|
return;
|
745
752
|
}
|
746
753
|
// Handle binary data (audio or video)
|
@@ -764,7 +771,7 @@ class AppSession {
|
|
764
771
|
this.settingsData = this.getDefaultSettings();
|
765
772
|
}
|
766
773
|
catch (error) {
|
767
|
-
this.logger.warn(
|
774
|
+
this.logger.warn("Failed to load default settings from config:", error);
|
768
775
|
}
|
769
776
|
}
|
770
777
|
// Update the settings manager with the new settings
|
@@ -787,18 +794,20 @@ class AppSession {
|
|
787
794
|
this.logger.debug(`[AppSession] No capabilities provided in CONNECTION_ACK`);
|
788
795
|
}
|
789
796
|
// Emit connected event with settings
|
790
|
-
this.events.emit(
|
797
|
+
this.events.emit("connected", this.settingsData);
|
791
798
|
// Update subscriptions (normal flow)
|
792
799
|
this.updateSubscriptions();
|
793
800
|
// If settings-based subscriptions are enabled, update those too
|
794
|
-
if (this.shouldUpdateSubscriptionsOnSettingsChange &&
|
801
|
+
if (this.shouldUpdateSubscriptionsOnSettingsChange &&
|
802
|
+
this.settingsData.length > 0) {
|
795
803
|
this.updateSubscriptionsFromSettings();
|
796
804
|
}
|
797
805
|
}
|
798
|
-
else if ((0, types_1.isAppConnectionError)(message) ||
|
806
|
+
else if ((0, types_1.isAppConnectionError)(message) ||
|
807
|
+
message.type === "connection_error") {
|
799
808
|
// Handle both App-specific connection_error and standard connection_error
|
800
|
-
const errorMessage = message.message ||
|
801
|
-
this.events.emit(
|
809
|
+
const errorMessage = message.message || "Unknown connection error";
|
810
|
+
this.events.emit("error", new Error(errorMessage));
|
802
811
|
}
|
803
812
|
else if (message.type === types_1.StreamType.AUDIO_CHUNK) {
|
804
813
|
if (this.subscriptions.has(types_1.StreamType.AUDIO_CHUNK)) {
|
@@ -808,19 +817,18 @@ class AppSession {
|
|
808
817
|
}
|
809
818
|
else if ((0, types_1.isDataStream)(message)) {
|
810
819
|
// Ensure streamType exists before emitting the event
|
811
|
-
|
812
|
-
if (message.streamType ===
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
}
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
}
|
820
|
+
const messageStreamType = message.streamType;
|
821
|
+
// if (message.streamType === StreamType.TRANSCRIPTION) {
|
822
|
+
// const transcriptionData = message.data as TranscriptionData;
|
823
|
+
// if (transcriptionData.transcribeLanguage) {
|
824
|
+
// messageStreamType = createTranscriptionStream(transcriptionData.transcribeLanguage) as ExtendedStreamType;
|
825
|
+
// }
|
826
|
+
// } else if (message.streamType === StreamType.TRANSLATION) {
|
827
|
+
// const translationData = message.data as TranslationData;
|
828
|
+
// if (translationData.transcribeLanguage && translationData.translateLanguage) {
|
829
|
+
// messageStreamType = createTranslationStream(translationData.transcribeLanguage, translationData.translateLanguage) as ExtendedStreamType;
|
830
|
+
// }
|
831
|
+
// }
|
824
832
|
if (messageStreamType && this.subscriptions.has(messageStreamType)) {
|
825
833
|
const sanitizedData = this.sanitizeEventData(messageStreamType, message.data);
|
826
834
|
this.events.emit(messageStreamType, sanitizedData);
|
@@ -850,16 +858,16 @@ class AppSession {
|
|
850
858
|
// Update the settings manager with the new settings
|
851
859
|
const changes = this.settings.updateSettings(this.settingsData);
|
852
860
|
// Emit settings update event (for backwards compatibility)
|
853
|
-
this.events.emit(
|
861
|
+
this.events.emit("settings_update", this.settingsData);
|
854
862
|
// --- MentraOS settings update logic ---
|
855
863
|
// If the message.settings looks like MentraOS settings (object with known keys), update mentraosSettings
|
856
|
-
if (message.settings && typeof message.settings ===
|
864
|
+
if (message.settings && typeof message.settings === "object") {
|
857
865
|
this.settings.updateMentraosSettings(message.settings);
|
858
866
|
}
|
859
867
|
// Check if we should update subscriptions
|
860
868
|
if (this.shouldUpdateSubscriptionsOnSettingsChange) {
|
861
869
|
// Check if any subscription trigger settings changed
|
862
|
-
const shouldUpdateSubs = this.subscriptionUpdateTriggers.some(key => {
|
870
|
+
const shouldUpdateSubs = this.subscriptionUpdateTriggers.some((key) => {
|
863
871
|
return key in changes;
|
864
872
|
});
|
865
873
|
if (shouldUpdateSubs) {
|
@@ -867,11 +875,23 @@ class AppSession {
|
|
867
875
|
}
|
868
876
|
}
|
869
877
|
}
|
878
|
+
else if ((0, types_1.isCapabilitiesUpdate)(message)) {
|
879
|
+
// Update device capabilities
|
880
|
+
const capabilitiesMessage = message;
|
881
|
+
this.capabilities = capabilitiesMessage.capabilities;
|
882
|
+
this.logger.info(capabilitiesMessage.capabilities, `[AppSession] Capabilities updated for model: ${capabilitiesMessage.modelName}`);
|
883
|
+
// Emit capabilities update event for applications to handle
|
884
|
+
this.events.emit("capabilities_update", {
|
885
|
+
capabilities: capabilitiesMessage.capabilities,
|
886
|
+
modelName: capabilitiesMessage.modelName,
|
887
|
+
timestamp: capabilitiesMessage.timestamp,
|
888
|
+
});
|
889
|
+
}
|
870
890
|
else if ((0, types_1.isAppStopped)(message)) {
|
871
|
-
const reason = message.reason ||
|
891
|
+
const reason = message.reason || "unknown";
|
872
892
|
const displayReason = `App stopped: ${reason}`;
|
873
893
|
// Emit disconnected event with clean closure info to prevent reconnection attempts
|
874
|
-
this.events.emit(
|
894
|
+
this.events.emit("disconnected", {
|
875
895
|
message: displayReason,
|
876
896
|
code: 1000, // Normal closure code
|
877
897
|
reason: displayReason,
|
@@ -884,14 +904,14 @@ class AppSession {
|
|
884
904
|
else if ((0, types_1.isDashboardModeChanged)(message)) {
|
885
905
|
try {
|
886
906
|
// Use proper type
|
887
|
-
const mode = message.mode ||
|
907
|
+
const mode = message.mode || "none";
|
888
908
|
// Update dashboard state in the API
|
889
|
-
if (this.dashboard &&
|
909
|
+
if (this.dashboard && "content" in this.dashboard) {
|
890
910
|
this.dashboard.content.setCurrentMode(mode);
|
891
911
|
}
|
892
912
|
}
|
893
913
|
catch (error) {
|
894
|
-
this.logger.error(error,
|
914
|
+
this.logger.error(error, "Error handling dashboard mode change");
|
895
915
|
}
|
896
916
|
}
|
897
917
|
// Handle always-on dashboard state changes
|
@@ -900,75 +920,78 @@ class AppSession {
|
|
900
920
|
// Use proper type
|
901
921
|
const enabled = !!message.enabled;
|
902
922
|
// Update dashboard state in the API
|
903
|
-
if (this.dashboard &&
|
923
|
+
if (this.dashboard && "content" in this.dashboard) {
|
904
924
|
this.dashboard.content.setAlwaysOnEnabled(enabled);
|
905
925
|
}
|
906
926
|
}
|
907
927
|
catch (error) {
|
908
|
-
this.logger.error(error,
|
928
|
+
this.logger.error(error, "Error handling dashboard always-on change");
|
909
929
|
}
|
910
930
|
}
|
911
931
|
// Handle custom messages
|
912
932
|
else if (message.type === types_1.CloudToAppMessageType.CUSTOM_MESSAGE) {
|
913
|
-
this.events.emit(
|
933
|
+
this.events.emit("custom_message", message);
|
914
934
|
return;
|
915
935
|
}
|
916
936
|
// Handle App-to-App communication messages
|
917
|
-
else if (message.type ===
|
918
|
-
this.appEvents.emit(
|
937
|
+
else if (message.type === "app_message_received") {
|
938
|
+
this.appEvents.emit("app_message_received", message);
|
919
939
|
}
|
920
|
-
else if (message.type ===
|
921
|
-
this.appEvents.emit(
|
940
|
+
else if (message.type === "app_user_joined") {
|
941
|
+
this.appEvents.emit("app_user_joined", message);
|
922
942
|
}
|
923
|
-
else if (message.type ===
|
924
|
-
this.appEvents.emit(
|
943
|
+
else if (message.type === "app_user_left") {
|
944
|
+
this.appEvents.emit("app_user_left", message);
|
925
945
|
}
|
926
|
-
else if (message.type ===
|
927
|
-
this.appEvents.emit(
|
946
|
+
else if (message.type === "app_room_updated") {
|
947
|
+
this.appEvents.emit("app_room_updated", message);
|
928
948
|
}
|
929
|
-
else if (message.type ===
|
949
|
+
else if (message.type === "app_direct_message_response") {
|
930
950
|
const response = message;
|
931
|
-
if (response.messageId &&
|
951
|
+
if (response.messageId &&
|
952
|
+
this.pendingDirectMessages.has(response.messageId)) {
|
932
953
|
const { resolve } = this.pendingDirectMessages.get(response.messageId);
|
933
954
|
resolve(response.success);
|
934
955
|
this.pendingDirectMessages.delete(response.messageId);
|
935
956
|
}
|
936
957
|
}
|
937
|
-
else if (message.type ===
|
958
|
+
else if (message.type === "augmentos_settings_update") {
|
938
959
|
const mentraosMsg = message;
|
939
|
-
if (mentraosMsg.settings &&
|
960
|
+
if (mentraosMsg.settings &&
|
961
|
+
typeof mentraosMsg.settings === "object") {
|
940
962
|
this.settings.updateMentraosSettings(mentraosMsg.settings);
|
941
963
|
}
|
942
964
|
}
|
943
965
|
// Handle 'connection_error' as a specific case if cloud sends this string literal
|
944
|
-
else if (message.type ===
|
966
|
+
else if (message.type === "connection_error") {
|
945
967
|
// Treat 'connection_error' (string literal) like AppConnectionError
|
946
968
|
// This handles cases where the cloud might send the type as a direct string
|
947
969
|
// instead of the enum's 'tpa_connection_error' value.
|
948
|
-
const errorMessage = message.message ||
|
970
|
+
const errorMessage = message.message ||
|
971
|
+
"Unknown connection error (type: connection_error)";
|
949
972
|
this.logger.warn(`Received 'connection_error' type directly. Consider aligning cloud to send 'tpa_connection_error'. Message: ${errorMessage}`);
|
950
|
-
this.events.emit(
|
973
|
+
this.events.emit("error", new Error(errorMessage));
|
951
974
|
}
|
952
|
-
else if (message.type ===
|
975
|
+
else if (message.type === "permission_error") {
|
953
976
|
// Handle permission errors from cloud
|
954
|
-
this.logger.warn(
|
977
|
+
this.logger.warn("Permission error received:", {
|
955
978
|
message: message.message,
|
956
979
|
details: message.details,
|
957
980
|
detailsCount: message.details?.length || 0,
|
958
|
-
rejectedStreams: message.details?.map(d => d.stream) || []
|
981
|
+
rejectedStreams: message.details?.map((d) => d.stream) || [],
|
959
982
|
});
|
960
983
|
// Emit permission error event for application handling
|
961
|
-
this.events.emit(
|
984
|
+
this.events.emit("permission_error", {
|
962
985
|
message: message.message,
|
963
986
|
details: message.details,
|
964
|
-
timestamp: message.timestamp
|
987
|
+
timestamp: message.timestamp,
|
965
988
|
});
|
966
989
|
// Optionally emit individual permission denied events for each stream
|
967
|
-
message.details?.forEach(detail => {
|
968
|
-
this.events.emit(
|
990
|
+
message.details?.forEach((detail) => {
|
991
|
+
this.events.emit("permission_denied", {
|
969
992
|
stream: detail.stream,
|
970
993
|
requiredPermission: detail.requiredPermission,
|
971
|
-
message: detail.message
|
994
|
+
message: detail.message,
|
972
995
|
});
|
973
996
|
});
|
974
997
|
}
|
@@ -981,26 +1004,28 @@ class AppSession {
|
|
981
1004
|
else if ((0, cloud_to_app_1.isPhotoResponse)(message)) {
|
982
1005
|
// Legacy photo response handling - now photos come directly via webhook
|
983
1006
|
// This branch can be removed in the future as all photos now go through /photo-upload
|
984
|
-
this.logger.warn(
|
1007
|
+
this.logger.warn("Received legacy photo response - photos should now come via /photo-upload webhook");
|
985
1008
|
}
|
986
1009
|
// Handle unrecognized message types gracefully
|
987
1010
|
else {
|
988
1011
|
this.logger.warn(`Unrecognized message type: ${message.type}`);
|
989
|
-
this.events.emit(
|
1012
|
+
this.events.emit("error", new Error(`Unrecognized message type: ${message.type}`));
|
990
1013
|
}
|
991
1014
|
}
|
992
1015
|
catch (processingError) {
|
993
1016
|
// Catch any errors during message processing to prevent App crashes
|
994
|
-
this.logger.error(processingError,
|
995
|
-
const errorMessage = processingError instanceof Error
|
996
|
-
|
1017
|
+
this.logger.error(processingError, "Error processing message:");
|
1018
|
+
const errorMessage = processingError instanceof Error
|
1019
|
+
? processingError.message
|
1020
|
+
: String(processingError);
|
1021
|
+
this.events.emit("error", new Error(`Error processing message: ${errorMessage}`));
|
997
1022
|
}
|
998
1023
|
}
|
999
1024
|
catch (error) {
|
1000
1025
|
// Final safety net to ensure the App doesn't crash on any unexpected errors
|
1001
|
-
this.logger.error(error,
|
1026
|
+
this.logger.error(error, "Unexpected error in message handler");
|
1002
1027
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
1003
|
-
this.events.emit(
|
1028
|
+
this.events.emit("error", new Error(`Unexpected error in message handler: ${errorMessage}`));
|
1004
1029
|
}
|
1005
1030
|
}
|
1006
1031
|
/**
|
@@ -1018,7 +1043,7 @@ class AppSession {
|
|
1018
1043
|
return false;
|
1019
1044
|
}
|
1020
1045
|
// Check if message has a type property
|
1021
|
-
if (!(
|
1046
|
+
if (!("type" in message)) {
|
1022
1047
|
return false;
|
1023
1048
|
}
|
1024
1049
|
// All other message types should be objects with a type property
|
@@ -1036,7 +1061,7 @@ class AppSession {
|
|
1036
1061
|
}
|
1037
1062
|
// Validate buffer has content before processing
|
1038
1063
|
if (!buffer || buffer.byteLength === 0) {
|
1039
|
-
this.events.emit(
|
1064
|
+
this.events.emit("error", new Error("Received empty binary message"));
|
1040
1065
|
return;
|
1041
1066
|
}
|
1042
1067
|
// Create a safety wrapped audio chunk with proper defaults
|
@@ -1044,15 +1069,15 @@ class AppSession {
|
|
1044
1069
|
type: types_1.StreamType.AUDIO_CHUNK,
|
1045
1070
|
timestamp: new Date(),
|
1046
1071
|
arrayBuffer: buffer,
|
1047
|
-
sampleRate: 16000 // Default sample rate
|
1072
|
+
sampleRate: 16000, // Default sample rate
|
1048
1073
|
};
|
1049
1074
|
// Emit to subscribers
|
1050
1075
|
this.events.emit(types_1.StreamType.AUDIO_CHUNK, audioChunk);
|
1051
1076
|
}
|
1052
1077
|
catch (error) {
|
1053
|
-
this.logger.error(error,
|
1078
|
+
this.logger.error(error, "Error processing binary message");
|
1054
1079
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
1055
|
-
this.events.emit(
|
1080
|
+
this.events.emit("error", new Error(`Error processing binary message: ${errorMessage}`));
|
1056
1081
|
}
|
1057
1082
|
}
|
1058
1083
|
/**
|
@@ -1071,12 +1096,12 @@ class AppSession {
|
|
1071
1096
|
switch (streamType) {
|
1072
1097
|
case types_1.StreamType.TRANSCRIPTION:
|
1073
1098
|
// Ensure text field exists and is a string
|
1074
|
-
if (typeof data.text !==
|
1099
|
+
if (typeof data.text !== "string") {
|
1075
1100
|
return {
|
1076
|
-
text:
|
1101
|
+
text: "",
|
1077
1102
|
isFinal: true,
|
1078
1103
|
startTime: Date.now(),
|
1079
|
-
endTime: Date.now()
|
1104
|
+
endTime: Date.now(),
|
1080
1105
|
};
|
1081
1106
|
}
|
1082
1107
|
break;
|
@@ -1084,15 +1109,19 @@ class AppSession {
|
|
1084
1109
|
// Ensure position data has required numeric fields
|
1085
1110
|
// Handle HeadPosition - Note the property position instead of x,y,z
|
1086
1111
|
const pos = data;
|
1087
|
-
if (typeof pos?.position !==
|
1088
|
-
return { position:
|
1112
|
+
if (typeof pos?.position !== "string") {
|
1113
|
+
return { position: "up", timestamp: new Date() };
|
1089
1114
|
}
|
1090
1115
|
break;
|
1091
1116
|
case types_1.StreamType.BUTTON_PRESS:
|
1092
1117
|
// Ensure button type is valid
|
1093
1118
|
const btn = data;
|
1094
1119
|
if (!btn.buttonId || !btn.pressType) {
|
1095
|
-
return {
|
1120
|
+
return {
|
1121
|
+
buttonId: "unknown",
|
1122
|
+
pressType: "short",
|
1123
|
+
timestamp: new Date(),
|
1124
|
+
};
|
1096
1125
|
}
|
1097
1126
|
break;
|
1098
1127
|
}
|
@@ -1113,7 +1142,7 @@ class AppSession {
|
|
1113
1142
|
sessionId: this.sessionId,
|
1114
1143
|
packageName: this.config.packageName,
|
1115
1144
|
apiKey: this.config.apiKey,
|
1116
|
-
timestamp: new Date()
|
1145
|
+
timestamp: new Date(),
|
1117
1146
|
};
|
1118
1147
|
this.send(message);
|
1119
1148
|
}
|
@@ -1123,10 +1152,10 @@ class AppSession {
|
|
1123
1152
|
updateSubscriptions() {
|
1124
1153
|
this.logger.info(`[AppSession] updateSubscriptions: sending subscriptions to cloud:`, Array.from(this.subscriptions));
|
1125
1154
|
// [MODIFIED] builds the array of SubscriptionRequest objects to send to the cloud
|
1126
|
-
const subscriptionPayload = Array.from(this.subscriptions).map(stream => {
|
1155
|
+
const subscriptionPayload = Array.from(this.subscriptions).map((stream) => {
|
1127
1156
|
const rate = this.streamRates.get(stream);
|
1128
1157
|
if (rate && stream === types_1.StreamType.LOCATION_STREAM) {
|
1129
|
-
return { stream:
|
1158
|
+
return { stream: "location_stream", rate: rate };
|
1130
1159
|
}
|
1131
1160
|
return stream;
|
1132
1161
|
});
|
@@ -1135,7 +1164,7 @@ class AppSession {
|
|
1135
1164
|
packageName: this.config.packageName,
|
1136
1165
|
subscriptions: subscriptionPayload, // [MODIFIED]
|
1137
1166
|
sessionId: this.sessionId,
|
1138
|
-
timestamp: new Date()
|
1167
|
+
timestamp: new Date(),
|
1139
1168
|
};
|
1140
1169
|
this.send(message);
|
1141
1170
|
}
|
@@ -1145,7 +1174,7 @@ class AppSession {
|
|
1145
1174
|
async handleReconnection() {
|
1146
1175
|
// Check if reconnection is allowed
|
1147
1176
|
if (!this.config.autoReconnect || !this.sessionId) {
|
1148
|
-
this.logger.debug(`🔄 Reconnection skipped: autoReconnect=${this.config.autoReconnect}, sessionId=${this.sessionId ?
|
1177
|
+
this.logger.debug(`🔄 Reconnection skipped: autoReconnect=${this.config.autoReconnect}, sessionId=${this.sessionId ? "valid" : "invalid"}`);
|
1149
1178
|
return;
|
1150
1179
|
}
|
1151
1180
|
// Check if we've exceeded the maximum attempts
|
@@ -1153,12 +1182,12 @@ class AppSession {
|
|
1153
1182
|
if (this.reconnectAttempts >= maxAttempts) {
|
1154
1183
|
this.logger.info(`🔄 Maximum reconnection attempts (${maxAttempts}) reached, giving up`);
|
1155
1184
|
// Emit a permanent disconnection event to trigger onStop in the App server
|
1156
|
-
this.events.emit(
|
1185
|
+
this.events.emit("disconnected", {
|
1157
1186
|
message: `Connection permanently lost after ${maxAttempts} failed reconnection attempts`,
|
1158
1187
|
code: 4000, // Custom code for max reconnection attempts exhausted
|
1159
|
-
reason:
|
1188
|
+
reason: "Maximum reconnection attempts exceeded",
|
1160
1189
|
wasClean: false,
|
1161
|
-
permanent: true // Flag this as a permanent disconnection
|
1190
|
+
permanent: true, // Flag this as a permanent disconnection
|
1162
1191
|
});
|
1163
1192
|
return;
|
1164
1193
|
}
|
@@ -1168,7 +1197,7 @@ class AppSession {
|
|
1168
1197
|
this.reconnectAttempts++;
|
1169
1198
|
this.logger.debug(`🔄 [${this.config.packageName}] Reconnection attempt ${this.reconnectAttempts}/${maxAttempts} in ${delay}ms`);
|
1170
1199
|
// Use the resource tracker for the timeout
|
1171
|
-
await new Promise(resolve => {
|
1200
|
+
await new Promise((resolve) => {
|
1172
1201
|
this.resources.setTimeout(() => resolve(), delay);
|
1173
1202
|
});
|
1174
1203
|
try {
|
@@ -1180,17 +1209,17 @@ class AppSession {
|
|
1180
1209
|
catch (error) {
|
1181
1210
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
1182
1211
|
this.logger.error(error, `❌ [${this.config.packageName}] Reconnection failed for user ${this.userId}`);
|
1183
|
-
this.events.emit(
|
1212
|
+
this.events.emit("error", new Error(`Reconnection failed: ${errorMessage}`));
|
1184
1213
|
// Check if this was the last attempt
|
1185
1214
|
if (this.reconnectAttempts >= maxAttempts) {
|
1186
1215
|
this.logger.debug(`🔄 [${this.config.packageName}] Final reconnection attempt failed, emitting permanent disconnection`);
|
1187
1216
|
// Emit permanent disconnection event after the last failed attempt
|
1188
|
-
this.events.emit(
|
1217
|
+
this.events.emit("disconnected", {
|
1189
1218
|
message: `Connection permanently lost after ${maxAttempts} failed reconnection attempts`,
|
1190
1219
|
code: 4000, // Custom code for max reconnection attempts exhausted
|
1191
|
-
reason:
|
1220
|
+
reason: "Maximum reconnection attempts exceeded",
|
1192
1221
|
wasClean: false,
|
1193
|
-
permanent: true // Flag this as a permanent disconnection
|
1222
|
+
permanent: true, // Flag this as a permanent disconnection
|
1194
1223
|
});
|
1195
1224
|
}
|
1196
1225
|
}
|
@@ -1203,27 +1232,27 @@ class AppSession {
|
|
1203
1232
|
try {
|
1204
1233
|
// Verify WebSocket connection is valid
|
1205
1234
|
if (!this.ws) {
|
1206
|
-
throw new Error(
|
1235
|
+
throw new Error("WebSocket connection not established");
|
1207
1236
|
}
|
1208
1237
|
if (this.ws.readyState !== 1) {
|
1209
1238
|
const stateMap = {
|
1210
|
-
0:
|
1211
|
-
1:
|
1212
|
-
2:
|
1213
|
-
3:
|
1239
|
+
0: "CONNECTING",
|
1240
|
+
1: "OPEN",
|
1241
|
+
2: "CLOSING",
|
1242
|
+
3: "CLOSED",
|
1214
1243
|
};
|
1215
|
-
const stateName = stateMap[this.ws.readyState] ||
|
1244
|
+
const stateName = stateMap[this.ws.readyState] || "UNKNOWN";
|
1216
1245
|
throw new Error(`WebSocket not connected (current state: ${stateName})`);
|
1217
1246
|
}
|
1218
1247
|
// Validate message before sending
|
1219
|
-
if (!message || typeof message !==
|
1220
|
-
throw new Error(
|
1248
|
+
if (!message || typeof message !== "object") {
|
1249
|
+
throw new Error("Invalid message: must be an object");
|
1221
1250
|
}
|
1222
|
-
if (!(
|
1251
|
+
if (!("type" in message)) {
|
1223
1252
|
throw new Error('Invalid message: missing "type" property');
|
1224
1253
|
}
|
1225
1254
|
// Ensure message format is consistent
|
1226
|
-
if (!(
|
1255
|
+
if (!("timestamp" in message) || !(message.timestamp instanceof Date)) {
|
1227
1256
|
message.timestamp = new Date();
|
1228
1257
|
}
|
1229
1258
|
// Try to send with error handling
|
@@ -1238,13 +1267,13 @@ class AppSession {
|
|
1238
1267
|
}
|
1239
1268
|
catch (error) {
|
1240
1269
|
// Log the error and emit an event so App developers are aware
|
1241
|
-
this.logger.error(error,
|
1270
|
+
this.logger.error(error, "Message send error");
|
1242
1271
|
// Ensure we always emit an Error object
|
1243
1272
|
if (error instanceof Error) {
|
1244
|
-
this.events.emit(
|
1273
|
+
this.events.emit("error", error);
|
1245
1274
|
}
|
1246
1275
|
else {
|
1247
|
-
this.events.emit(
|
1276
|
+
this.events.emit("error", new Error(String(error)));
|
1248
1277
|
}
|
1249
1278
|
// Re-throw to maintain the original function behavior
|
1250
1279
|
throw error;
|
@@ -1257,11 +1286,13 @@ class AppSession {
|
|
1257
1286
|
async getInstructions() {
|
1258
1287
|
try {
|
1259
1288
|
const baseUrl = this.getServerUrl();
|
1260
|
-
const response = await axios_1.default.get(`${baseUrl}/api/instructions`, {
|
1289
|
+
const response = await axios_1.default.get(`${baseUrl}/api/instructions`, {
|
1290
|
+
params: { userId: this.userId },
|
1291
|
+
});
|
1261
1292
|
return response.data.instructions || null;
|
1262
1293
|
}
|
1263
1294
|
catch (err) {
|
1264
|
-
this.logger.error(
|
1295
|
+
this.logger.error("Error fetching instructions from backend:", err);
|
1265
1296
|
return null;
|
1266
1297
|
}
|
1267
1298
|
}
|
@@ -1276,26 +1307,26 @@ class AppSession {
|
|
1276
1307
|
async discoverAppUsers(domain, includeProfiles = false) {
|
1277
1308
|
// Use the domain argument as the base URL if provided
|
1278
1309
|
if (!domain) {
|
1279
|
-
throw new Error(
|
1310
|
+
throw new Error("Domain (API base URL) is required for user discovery");
|
1280
1311
|
}
|
1281
1312
|
const url = `${domain}/api/app-communication/discover-users`;
|
1282
1313
|
// Use the user's core token for authentication
|
1283
1314
|
const appApiKey = this.config.apiKey; // This may need to be updated if you store the core token elsewhere
|
1284
1315
|
if (!appApiKey) {
|
1285
|
-
throw new Error(
|
1316
|
+
throw new Error("Core token (apiKey) is required for user discovery");
|
1286
1317
|
}
|
1287
1318
|
const body = {
|
1288
1319
|
packageName: this.config.packageName,
|
1289
1320
|
userId: this.userId,
|
1290
|
-
includeUserProfiles: includeProfiles
|
1321
|
+
includeUserProfiles: includeProfiles,
|
1291
1322
|
};
|
1292
1323
|
const response = await (0, node_fetch_1.default)(url, {
|
1293
|
-
method:
|
1324
|
+
method: "POST",
|
1294
1325
|
headers: {
|
1295
|
-
|
1296
|
-
|
1326
|
+
Authorization: `Bearer ${appApiKey}`,
|
1327
|
+
"Content-Type": "application/json",
|
1297
1328
|
},
|
1298
|
-
body: JSON.stringify(body)
|
1329
|
+
body: JSON.stringify(body),
|
1299
1330
|
});
|
1300
1331
|
if (!response.ok) {
|
1301
1332
|
const errorText = await response.text();
|
@@ -1310,11 +1341,11 @@ class AppSession {
|
|
1310
1341
|
*/
|
1311
1342
|
async isUserActive(userId) {
|
1312
1343
|
try {
|
1313
|
-
const userList = await this.discoverAppUsers(
|
1344
|
+
const userList = await this.discoverAppUsers("", false);
|
1314
1345
|
return userList.users.some((user) => user.userId === userId);
|
1315
1346
|
}
|
1316
1347
|
catch (error) {
|
1317
|
-
this.logger.error({ error, userId },
|
1348
|
+
this.logger.error({ error, userId }, "Error checking if user is active");
|
1318
1349
|
return false;
|
1319
1350
|
}
|
1320
1351
|
}
|
@@ -1328,7 +1359,7 @@ class AppSession {
|
|
1328
1359
|
return userList.totalUsers;
|
1329
1360
|
}
|
1330
1361
|
catch (error) {
|
1331
|
-
this.logger.error(error,
|
1362
|
+
this.logger.error(error, "Error getting user count");
|
1332
1363
|
return 0;
|
1333
1364
|
}
|
1334
1365
|
}
|
@@ -1342,13 +1373,13 @@ class AppSession {
|
|
1342
1373
|
try {
|
1343
1374
|
const messageId = this.generateMessageId();
|
1344
1375
|
const message = {
|
1345
|
-
type:
|
1376
|
+
type: "app_broadcast_message",
|
1346
1377
|
packageName: this.config.packageName,
|
1347
1378
|
sessionId: this.sessionId,
|
1348
1379
|
payload,
|
1349
1380
|
messageId,
|
1350
1381
|
senderUserId: this.userId,
|
1351
|
-
timestamp: new Date()
|
1382
|
+
timestamp: new Date(),
|
1352
1383
|
};
|
1353
1384
|
this.send(message);
|
1354
1385
|
}
|
@@ -1370,21 +1401,23 @@ class AppSession {
|
|
1370
1401
|
// Store promise resolver
|
1371
1402
|
this.pendingDirectMessages.set(messageId, { resolve, reject });
|
1372
1403
|
const message = {
|
1373
|
-
type:
|
1404
|
+
type: "app_direct_message",
|
1374
1405
|
packageName: this.config.packageName,
|
1375
1406
|
sessionId: this.sessionId,
|
1376
1407
|
targetUserId,
|
1377
1408
|
payload,
|
1378
1409
|
messageId,
|
1379
1410
|
senderUserId: this.userId,
|
1380
|
-
timestamp: new Date()
|
1411
|
+
timestamp: new Date(),
|
1381
1412
|
};
|
1382
1413
|
this.send(message);
|
1383
1414
|
// Set timeout to avoid hanging promises
|
1384
1415
|
const timeoutMs = 15000; // 15 seconds
|
1385
1416
|
this.resources.setTimeout(() => {
|
1386
1417
|
if (this.pendingDirectMessages.has(messageId)) {
|
1387
|
-
this.pendingDirectMessages
|
1418
|
+
this.pendingDirectMessages
|
1419
|
+
.get(messageId)
|
1420
|
+
.reject(new Error("Direct message timed out"));
|
1388
1421
|
this.pendingDirectMessages.delete(messageId);
|
1389
1422
|
}
|
1390
1423
|
}, timeoutMs);
|
@@ -1404,12 +1437,12 @@ class AppSession {
|
|
1404
1437
|
async joinAppRoom(roomId, roomConfig) {
|
1405
1438
|
try {
|
1406
1439
|
const message = {
|
1407
|
-
type:
|
1440
|
+
type: "app_room_join",
|
1408
1441
|
packageName: this.config.packageName,
|
1409
1442
|
sessionId: this.sessionId,
|
1410
1443
|
roomId,
|
1411
1444
|
roomConfig,
|
1412
|
-
timestamp: new Date()
|
1445
|
+
timestamp: new Date(),
|
1413
1446
|
};
|
1414
1447
|
this.send(message);
|
1415
1448
|
}
|
@@ -1426,11 +1459,11 @@ class AppSession {
|
|
1426
1459
|
async leaveAppRoom(roomId) {
|
1427
1460
|
try {
|
1428
1461
|
const message = {
|
1429
|
-
type:
|
1462
|
+
type: "app_room_leave",
|
1430
1463
|
packageName: this.config.packageName,
|
1431
1464
|
sessionId: this.sessionId,
|
1432
1465
|
roomId,
|
1433
|
-
timestamp: new Date()
|
1466
|
+
timestamp: new Date(),
|
1434
1467
|
};
|
1435
1468
|
this.send(message);
|
1436
1469
|
}
|
@@ -1445,8 +1478,8 @@ class AppSession {
|
|
1445
1478
|
* @returns Cleanup function to remove the handler
|
1446
1479
|
*/
|
1447
1480
|
onAppMessage(handler) {
|
1448
|
-
this.appEvents.on(
|
1449
|
-
return () => this.appEvents.off(
|
1481
|
+
this.appEvents.on("app_message_received", handler);
|
1482
|
+
return () => this.appEvents.off("app_message_received", handler);
|
1450
1483
|
}
|
1451
1484
|
/**
|
1452
1485
|
* 👋 Listen for user join events
|
@@ -1454,8 +1487,8 @@ class AppSession {
|
|
1454
1487
|
* @returns Cleanup function to remove the handler
|
1455
1488
|
*/
|
1456
1489
|
onAppUserJoined(handler) {
|
1457
|
-
this.appEvents.on(
|
1458
|
-
return () => this.appEvents.off(
|
1490
|
+
this.appEvents.on("app_user_joined", handler);
|
1491
|
+
return () => this.appEvents.off("app_user_joined", handler);
|
1459
1492
|
}
|
1460
1493
|
/**
|
1461
1494
|
* 🚪 Listen for user leave events
|
@@ -1463,8 +1496,8 @@ class AppSession {
|
|
1463
1496
|
* @returns Cleanup function to remove the handler
|
1464
1497
|
*/
|
1465
1498
|
onAppUserLeft(handler) {
|
1466
|
-
this.appEvents.on(
|
1467
|
-
return () => this.appEvents.off(
|
1499
|
+
this.appEvents.on("app_user_left", handler);
|
1500
|
+
return () => this.appEvents.off("app_user_left", handler);
|
1468
1501
|
}
|
1469
1502
|
/**
|
1470
1503
|
* 🏠 Listen for room update events
|
@@ -1472,8 +1505,8 @@ class AppSession {
|
|
1472
1505
|
* @returns Cleanup function to remove the handler
|
1473
1506
|
*/
|
1474
1507
|
onAppRoomUpdated(handler) {
|
1475
|
-
this.appEvents.on(
|
1476
|
-
return () => this.appEvents.off(
|
1508
|
+
this.appEvents.on("app_room_updated", handler);
|
1509
|
+
return () => this.appEvents.off("app_room_updated", handler);
|
1477
1510
|
}
|
1478
1511
|
/**
|
1479
1512
|
* 🔧 Generate unique message ID
|
@@ -1501,8 +1534,8 @@ class TpaSession extends AppSession {
|
|
1501
1534
|
constructor(config) {
|
1502
1535
|
super(config);
|
1503
1536
|
// Emit a deprecation warning to help developers migrate
|
1504
|
-
console.warn(
|
1505
|
-
|
1537
|
+
console.warn("⚠️ DEPRECATION WARNING: TpaSession is deprecated and will be removed in a future version. " +
|
1538
|
+
"Please use AppSession instead. " +
|
1506
1539
|
'Simply replace "TpaSession" with "AppSession" in your code.');
|
1507
1540
|
}
|
1508
1541
|
}
|