@mentra/sdk 2.1.29-beta.2 → 2.1.31-beta.1

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.
Files changed (63) hide show
  1. package/dist/app/server/index.d.ts +2 -1
  2. package/dist/app/server/index.d.ts.map +1 -1
  3. package/dist/app/session/device-state.d.ts +83 -0
  4. package/dist/app/session/device-state.d.ts.map +1 -0
  5. package/dist/app/session/events.d.ts +9 -0
  6. package/dist/app/session/events.d.ts.map +1 -1
  7. package/dist/app/session/index.d.ts +23 -3
  8. package/dist/app/session/index.d.ts.map +1 -1
  9. package/dist/index.d.ts +6 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +251 -38
  12. package/dist/index.js.map +13 -11
  13. package/dist/types/capabilities.d.ts +3 -90
  14. package/dist/types/capabilities.d.ts.map +1 -1
  15. package/dist/types/index.d.ts +1 -1
  16. package/dist/types/index.d.ts.map +1 -1
  17. package/dist/types/message-types.d.ts +3 -1
  18. package/dist/types/message-types.d.ts.map +1 -1
  19. package/dist/types/messages/app-to-cloud.d.ts +15 -1
  20. package/dist/types/messages/app-to-cloud.d.ts.map +1 -1
  21. package/dist/types/messages/cloud-to-app.d.ts +14 -1
  22. package/dist/types/messages/cloud-to-app.d.ts.map +1 -1
  23. package/dist/utils/Observable.d.ts +92 -0
  24. package/dist/utils/Observable.d.ts.map +1 -0
  25. package/node_modules/@mentra/types/README.md +134 -0
  26. package/node_modules/@mentra/types/dist/applet.d.ts +39 -0
  27. package/node_modules/@mentra/types/dist/applet.d.ts.map +1 -0
  28. package/node_modules/@mentra/types/dist/applet.js +5 -0
  29. package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.d.ts +12 -0
  30. package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.d.ts.map +1 -0
  31. package/node_modules/@mentra/types/dist/capabilities/even-realities-g1.js +54 -0
  32. package/node_modules/@mentra/types/dist/capabilities/mentra-live.d.ts +12 -0
  33. package/node_modules/@mentra/types/dist/capabilities/mentra-live.d.ts.map +1 -0
  34. package/node_modules/@mentra/types/dist/capabilities/mentra-live.js +94 -0
  35. package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.d.ts +13 -0
  36. package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.d.ts.map +1 -0
  37. package/node_modules/@mentra/types/dist/capabilities/simulated-glasses.js +67 -0
  38. package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.d.ts +12 -0
  39. package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.d.ts.map +1 -0
  40. package/node_modules/@mentra/types/dist/capabilities/vuzix-z100.js +51 -0
  41. package/node_modules/@mentra/types/dist/cli.d.ts +130 -0
  42. package/node_modules/@mentra/types/dist/cli.d.ts.map +1 -0
  43. package/node_modules/@mentra/types/dist/cli.js +7 -0
  44. package/node_modules/@mentra/types/dist/device.d.ts +32 -0
  45. package/node_modules/@mentra/types/dist/device.d.ts.map +1 -0
  46. package/node_modules/@mentra/types/dist/device.js +6 -0
  47. package/node_modules/@mentra/types/dist/enums.d.ts +34 -0
  48. package/node_modules/@mentra/types/dist/enums.d.ts.map +1 -0
  49. package/node_modules/@mentra/types/dist/enums.js +39 -0
  50. package/node_modules/@mentra/types/dist/hardware.d.ts +141 -0
  51. package/node_modules/@mentra/types/dist/hardware.d.ts.map +1 -0
  52. package/node_modules/@mentra/types/dist/hardware.js +33 -0
  53. package/node_modules/@mentra/types/dist/index.d.ts +18 -0
  54. package/node_modules/@mentra/types/dist/index.d.ts.map +1 -0
  55. package/node_modules/@mentra/types/dist/index.js +25 -0
  56. package/node_modules/@mentra/types/package.json +31 -0
  57. package/package.json +6 -6
  58. package/dist/display-utils/test/ScrollView.test.d.ts +0 -2
  59. package/dist/display-utils/test/ScrollView.test.d.ts.map +0 -1
  60. package/dist/display-utils/test/TextMeasurer.test.d.ts +0 -2
  61. package/dist/display-utils/test/TextMeasurer.test.d.ts.map +0 -1
  62. package/dist/display-utils/test/TextWrapper.test.d.ts +0 -2
  63. package/dist/display-utils/test/TextWrapper.test.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -373,6 +373,7 @@ var init_message_types = __esm(() => {
373
373
  AppToCloudMessageType2["APP_USER_DISCOVERY"] = "app_user_discovery";
374
374
  AppToCloudMessageType2["APP_ROOM_JOIN"] = "app_room_join";
375
375
  AppToCloudMessageType2["APP_ROOM_LEAVE"] = "app_room_leave";
376
+ AppToCloudMessageType2["OWNERSHIP_RELEASE"] = "ownership_release";
376
377
  })(AppToCloudMessageType ||= {});
377
378
  ((CloudToAppMessageType2) => {
378
379
  CloudToAppMessageType2["CONNECTION_ACK"] = "tpa_connection_ack";
@@ -380,6 +381,7 @@ var init_message_types = __esm(() => {
380
381
  CloudToAppMessageType2["APP_STOPPED"] = "app_stopped";
381
382
  CloudToAppMessageType2["SETTINGS_UPDATE"] = "settings_update";
382
383
  CloudToAppMessageType2["CAPABILITIES_UPDATE"] = "capabilities_update";
384
+ CloudToAppMessageType2["DEVICE_STATE_UPDATE"] = "device_state_update";
383
385
  CloudToAppMessageType2["DASHBOARD_MODE_CHANGED"] = "dashboard_mode_changed";
384
386
  CloudToAppMessageType2["DASHBOARD_ALWAYS_ON_CHANGED"] = "dashboard_always_on_changed";
385
387
  CloudToAppMessageType2["DATA_STREAM"] = "data_stream";
@@ -829,6 +831,9 @@ function isRtmpStreamRequest(message) {
829
831
  function isRtmpStreamStopRequest(message) {
830
832
  return message.type === "rtmp_stream_stop" /* RTMP_STREAM_STOP */;
831
833
  }
834
+ function isOwnershipRelease(message) {
835
+ return message.type === "ownership_release" /* OWNERSHIP_RELEASE */;
836
+ }
832
837
  // src/utils/bitmap-utils.ts
833
838
  import * as fs from "fs/promises";
834
839
  import * as path from "path";
@@ -1339,6 +1344,9 @@ function isSettingsUpdate2(message) {
1339
1344
  function isCapabilitiesUpdate(message) {
1340
1345
  return message.type === "capabilities_update" /* CAPABILITIES_UPDATE */;
1341
1346
  }
1347
+ function isDeviceStateUpdate(message) {
1348
+ return message.type === "device_state_update" /* DEVICE_STATE_UPDATE */;
1349
+ }
1342
1350
  function isDataStream(message) {
1343
1351
  return message.type === "data_stream" /* DATA_STREAM */;
1344
1352
  }
@@ -1906,6 +1914,9 @@ class EventManager {
1906
1914
  this.unsubscribe(type);
1907
1915
  }
1908
1916
  }
1917
+ getRegisteredStreams() {
1918
+ return Array.from(this.handlers.keys());
1919
+ }
1909
1920
  emit(event, data) {
1910
1921
  try {
1911
1922
  this.emitter.emit(event, data);
@@ -2195,7 +2206,7 @@ class ApiClient {
2195
2206
  import pino from "pino";
2196
2207
  var BETTERSTACK_SOURCE_TOKEN = process.env.BETTERSTACK_SOURCE_TOKEN;
2197
2208
  var BETTERSTACK_ENDPOINT = process.env.BETTERSTACK_ENDPOINT || "https://s1311181.eu-nbg-2.betterstackdata.com";
2198
- var NODE_ENV = "isaiah";
2209
+ var NODE_ENV = "development";
2199
2210
  var PORTER_APP_NAME = process.env.PORTER_APP_NAME || "cloud-local";
2200
2211
  var LOG_LEVEL = NODE_ENV === "production" ? "info" : "debug";
2201
2212
  var streams2 = [];
@@ -3527,7 +3538,151 @@ class SimpleStorage {
3527
3538
  }
3528
3539
  }
3529
3540
 
3541
+ // src/utils/Observable.ts
3542
+ class Observable {
3543
+ _value;
3544
+ _listeners = new Set;
3545
+ _initialized = false;
3546
+ constructor(initialValue) {
3547
+ this._value = initialValue;
3548
+ }
3549
+ get value() {
3550
+ return this._value;
3551
+ }
3552
+ valueOf() {
3553
+ return this._value;
3554
+ }
3555
+ toString() {
3556
+ return String(this._value);
3557
+ }
3558
+ [Symbol.toPrimitive](hint) {
3559
+ if (hint === "string") {
3560
+ return String(this._value);
3561
+ }
3562
+ return this._value;
3563
+ }
3564
+ onChange(callback) {
3565
+ this._listeners.add(callback);
3566
+ if (this._initialized) {
3567
+ callback(this._value);
3568
+ }
3569
+ return () => this._listeners.delete(callback);
3570
+ }
3571
+ setValue(value) {
3572
+ const isFirstInit = !this._initialized;
3573
+ if (isFirstInit) {
3574
+ this._initialized = true;
3575
+ }
3576
+ if (isFirstInit || this._value !== value) {
3577
+ this._value = value;
3578
+ this._listeners.forEach((cb) => {
3579
+ try {
3580
+ cb(value);
3581
+ } catch (error) {
3582
+ console.error("Error in Observable onChange callback:", error);
3583
+ }
3584
+ });
3585
+ }
3586
+ }
3587
+ get listenerCount() {
3588
+ return this._listeners.size;
3589
+ }
3590
+ }
3591
+
3592
+ // src/app/session/device-state.ts
3593
+ class DeviceState {
3594
+ wifiConnected;
3595
+ wifiSsid;
3596
+ wifiLocalIp;
3597
+ batteryLevel;
3598
+ charging;
3599
+ caseBatteryLevel;
3600
+ caseCharging;
3601
+ caseOpen;
3602
+ caseRemoved;
3603
+ hotspotEnabled;
3604
+ hotspotSsid;
3605
+ connected;
3606
+ modelName;
3607
+ appSession;
3608
+ constructor(appSession) {
3609
+ this.appSession = appSession;
3610
+ this.wifiConnected = new Observable(false);
3611
+ this.wifiSsid = new Observable(null);
3612
+ this.wifiLocalIp = new Observable(null);
3613
+ this.batteryLevel = new Observable(null);
3614
+ this.charging = new Observable(null);
3615
+ this.caseBatteryLevel = new Observable(null);
3616
+ this.caseCharging = new Observable(null);
3617
+ this.caseOpen = new Observable(null);
3618
+ this.caseRemoved = new Observable(null);
3619
+ this.hotspotEnabled = new Observable(null);
3620
+ this.hotspotSsid = new Observable(null);
3621
+ this.connected = new Observable(false);
3622
+ this.modelName = new Observable(null);
3623
+ }
3624
+ updateFromMessage(state) {
3625
+ if (state.connected !== undefined) {
3626
+ this.connected.setValue(state.connected);
3627
+ }
3628
+ if (state.modelName !== undefined) {
3629
+ this.modelName.setValue(state.modelName);
3630
+ }
3631
+ if (state.wifiConnected !== undefined) {
3632
+ this.wifiConnected.setValue(state.wifiConnected);
3633
+ }
3634
+ if (state.wifiSsid !== undefined) {
3635
+ this.wifiSsid.setValue(state.wifiSsid ?? null);
3636
+ }
3637
+ if (state.wifiLocalIp !== undefined) {
3638
+ this.wifiLocalIp.setValue(state.wifiLocalIp ?? null);
3639
+ }
3640
+ if (state.batteryLevel !== undefined) {
3641
+ this.batteryLevel.setValue(state.batteryLevel ?? null);
3642
+ }
3643
+ if (state.charging !== undefined) {
3644
+ this.charging.setValue(state.charging ?? null);
3645
+ }
3646
+ if (state.caseBatteryLevel !== undefined) {
3647
+ this.caseBatteryLevel.setValue(state.caseBatteryLevel ?? null);
3648
+ }
3649
+ if (state.caseCharging !== undefined) {
3650
+ this.caseCharging.setValue(state.caseCharging ?? null);
3651
+ }
3652
+ if (state.caseOpen !== undefined) {
3653
+ this.caseOpen.setValue(state.caseOpen ?? null);
3654
+ }
3655
+ if (state.caseRemoved !== undefined) {
3656
+ this.caseRemoved.setValue(state.caseRemoved ?? null);
3657
+ }
3658
+ if (state.hotspotEnabled !== undefined) {
3659
+ this.hotspotEnabled.setValue(state.hotspotEnabled ?? null);
3660
+ }
3661
+ if (state.hotspotSsid !== undefined) {
3662
+ this.hotspotSsid.setValue(state.hotspotSsid ?? null);
3663
+ }
3664
+ }
3665
+ getSnapshot() {
3666
+ return {
3667
+ connected: this.connected.value,
3668
+ modelName: this.modelName.value ?? undefined,
3669
+ wifiConnected: this.wifiConnected.value,
3670
+ wifiSsid: this.wifiSsid.value ?? undefined,
3671
+ wifiLocalIp: this.wifiLocalIp.value ?? undefined,
3672
+ batteryLevel: this.batteryLevel.value ?? undefined,
3673
+ charging: this.charging.value ?? undefined,
3674
+ caseBatteryLevel: this.caseBatteryLevel.value ?? undefined,
3675
+ caseCharging: this.caseCharging.value ?? undefined,
3676
+ caseOpen: this.caseOpen.value ?? undefined,
3677
+ caseRemoved: this.caseRemoved.value ?? undefined,
3678
+ hotspotEnabled: this.hotspotEnabled.value ?? undefined,
3679
+ hotspotSsid: this.hotspotSsid.value ?? undefined
3680
+ };
3681
+ }
3682
+ }
3683
+
3530
3684
  // src/app/session/index.ts
3685
+ var SDK_SUBSCRIPTION_PATCH = "bug007-fix-v2";
3531
3686
  var APP_TO_APP_EVENT_TYPES = [
3532
3687
  "app_message_received",
3533
3688
  "app_user_joined",
@@ -3541,7 +3696,7 @@ class AppSession {
3541
3696
  ws = null;
3542
3697
  sessionId = null;
3543
3698
  reconnectAttempts = 0;
3544
- subscriptions = new Set;
3699
+ terminated = false;
3545
3700
  streamRates = new Map;
3546
3701
  resources = new ResourceTracker;
3547
3702
  settingsData = [];
@@ -3560,6 +3715,7 @@ class AppSession {
3560
3715
  led;
3561
3716
  audio;
3562
3717
  simpleStorage;
3718
+ device;
3563
3719
  appServer;
3564
3720
  logger;
3565
3721
  userId;
@@ -3609,18 +3765,14 @@ class AppSession {
3609
3765
  this.layouts = new LayoutManager(config.packageName, this.send.bind(this));
3610
3766
  this.settings = new SettingsManager(this.settingsData, this.config.packageName, this.config.mentraOSWebsocketUrl, this.sessionId ?? undefined, async (streams3) => {
3611
3767
  this.logger.debug({ streams: JSON.stringify(streams3) }, `[AppSession] subscribeFn called for streams`);
3612
- streams3.forEach((stream) => {
3613
- if (!this.subscriptions.has(stream)) {
3614
- this.subscriptions.add(stream);
3615
- this.logger.debug(`[AppSession] Auto-subscribed to stream '${stream}' for MentraOS setting.`);
3616
- } else {
3617
- this.logger.debug(`[AppSession] Already subscribed to stream '${stream}'.`);
3618
- }
3619
- });
3620
- this.logger.debug({ subscriptions: JSON.stringify(Array.from(this.subscriptions)) }, `[AppSession] Current subscriptions after subscribeFn`);
3768
+ const currentHandlerStreams = this.events.getRegisteredStreams();
3769
+ this.logger.debug({
3770
+ requestedStreams: JSON.stringify(streams3),
3771
+ currentHandlerStreams: JSON.stringify(currentHandlerStreams)
3772
+ }, `[AppSession] subscribeFn: requested streams vs current handler streams`);
3621
3773
  if (this.ws?.readyState === 1) {
3622
3774
  this.updateSubscriptions();
3623
- this.logger.debug(`[AppSession] Sent updated subscriptions to cloud after auto-subscribing to MentraOS setting.`);
3775
+ this.logger.debug(`[AppSession] Sent updated subscriptions to cloud (derived from handlers).`);
3624
3776
  } else {
3625
3777
  this.logger.debug(`[AppSession] WebSocket not open, will send subscriptions when connected.`);
3626
3778
  }
@@ -3631,6 +3783,7 @@ class AppSession {
3631
3783
  this.led = new LedModule(this, this.config.packageName, this.sessionId || "unknown-session-id", this.logger.child({ module: "led" }));
3632
3784
  this.audio = new AudioManager(this, this.config.packageName, this.sessionId || "unknown-session-id", this.logger.child({ module: "audio" }));
3633
3785
  this.simpleStorage = new SimpleStorage(this);
3786
+ this.device = { state: new DeviceState(this) };
3634
3787
  this.location = new LocationManager(this);
3635
3788
  }
3636
3789
  getSessionId() {
@@ -3697,7 +3850,6 @@ class AppSession {
3697
3850
  this.logger.warn(`[AppSession] Attempted to subscribe to App-to-App event type '${type}', which is not a valid stream. Use the event handler (e.g., onAppMessage) instead.`);
3698
3851
  return;
3699
3852
  }
3700
- this.subscriptions.add(type);
3701
3853
  if (rate) {
3702
3854
  this.streamRates.set(type, rate);
3703
3855
  }
@@ -3716,7 +3868,6 @@ class AppSession {
3716
3868
  this.logger.warn(`[AppSession] Attempted to unsubscribe from App-to-App event type '${type}', which is not a valid stream.`);
3717
3869
  return;
3718
3870
  }
3719
- this.subscriptions.delete(type);
3720
3871
  this.streamRates.delete(type);
3721
3872
  if (this.ws?.readyState === 1) {
3722
3873
  this.updateSubscriptions();
@@ -3840,11 +3991,15 @@ class AppSession {
3840
3991
  const isUserSessionEnded = reason && reason.includes("User session ended");
3841
3992
  this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] WebSocket closed with code ${code}${reasonStr}`);
3842
3993
  this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] isNormalClosure: ${isNormalClosure}, isManualStop: ${isManualStop}, isUserSessionEnded: ${isUserSessionEnded}`);
3843
- if (!isNormalClosure && !isManualStop) {
3994
+ if (isUserSessionEnded) {
3995
+ this.terminated = true;
3996
+ this.logger.info(`\uD83D\uDED1 [${this.config.packageName}] User session ended - marking as terminated, no reconnection allowed`);
3997
+ }
3998
+ if (!isNormalClosure && !isManualStop && !this.terminated) {
3844
3999
  this.logger.warn(`\uD83D\uDD0C [${this.config.packageName}] Abnormal closure detected, attempting reconnection`);
3845
4000
  this.handleReconnection();
3846
4001
  } else {
3847
- this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] Normal closure detected, not attempting reconnection`);
4002
+ this.logger.debug(`\uD83D\uDD0C [${this.config.packageName}] Normal/terminated closure detected, not attempting reconnection (terminated: ${this.terminated})`);
3848
4003
  }
3849
4004
  if (isUserSessionEnded) {
3850
4005
  this.logger.info(`\uD83D\uDED1 [${this.config.packageName}] User session ended - emitting disconnected event with sessionEnded flag`);
@@ -3908,7 +4063,26 @@ class AppSession {
3908
4063
  }
3909
4064
  });
3910
4065
  }
3911
- async disconnect() {
4066
+ async releaseOwnership(reason) {
4067
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
4068
+ this.logger.debug(`[${this.config.packageName}] Cannot release ownership - WebSocket not open`);
4069
+ return;
4070
+ }
4071
+ const message = {
4072
+ type: "ownership_release" /* OWNERSHIP_RELEASE */,
4073
+ packageName: this.config.packageName,
4074
+ sessionId: this.sessionId || "",
4075
+ reason,
4076
+ timestamp: new Date
4077
+ };
4078
+ this.logger.info({ reason, sessionId: this.sessionId }, `\uD83D\uDD04 [${this.config.packageName}] Releasing ownership: ${reason}`);
4079
+ this.send(message);
4080
+ await new Promise((resolve) => setTimeout(resolve, 100));
4081
+ }
4082
+ async disconnect(options) {
4083
+ if (options?.releaseOwnership && options?.reason) {
4084
+ await this.releaseOwnership(options.reason);
4085
+ }
3912
4086
  try {
3913
4087
  await this.simpleStorage.flush();
3914
4088
  console.log("SimpleStorage flushed on disconnect");
@@ -3924,7 +4098,6 @@ class AppSession {
3924
4098
  this.resources.dispose();
3925
4099
  this.ws = null;
3926
4100
  this.sessionId = null;
3927
- this.subscriptions.clear();
3928
4101
  this.reconnectAttempts = 0;
3929
4102
  }
3930
4103
  getSettings() {
@@ -3945,11 +4118,14 @@ class AppSession {
3945
4118
  if (!this.subscriptionSettingsHandler)
3946
4119
  return;
3947
4120
  try {
3948
- const newSubscriptions = this.subscriptionSettingsHandler(this.settingsData);
3949
- this.subscriptions.clear();
3950
- newSubscriptions.forEach((subscription) => {
3951
- this.subscriptions.add(subscription);
3952
- });
4121
+ const settingsSubscriptions = this.subscriptionSettingsHandler(this.settingsData);
4122
+ const handlerStreams = this.events.getRegisteredStreams();
4123
+ if (settingsSubscriptions.length !== handlerStreams.length) {
4124
+ this.logger.warn({
4125
+ settingsSubscriptions: JSON.stringify(settingsSubscriptions),
4126
+ handlerStreams: JSON.stringify(handlerStreams)
4127
+ }, `[AppSession] Settings-based subscriptions (${settingsSubscriptions.length}) differ from handler-based subscriptions (${handlerStreams.length}). ` + `Subscriptions are now derived from handlers. Ensure handlers are registered for desired streams.`);
4128
+ }
3953
4129
  if (this.ws && this.ws.readyState === 1) {
3954
4130
  this.updateSubscriptions();
3955
4131
  }
@@ -4079,6 +4255,8 @@ class AppSession {
4079
4255
  this.logger.debug(`[AppSession] No capabilities provided in CONNECTION_ACK`);
4080
4256
  }
4081
4257
  this.events.emit("connected", this.settingsData);
4258
+ const handlerCount = this.events.getRegisteredStreams().length;
4259
+ this.logger.info({ patch: SDK_SUBSCRIPTION_PATCH, handlerCount }, `[AppSession] \uD83D\uDD27 SDK Patch Active: ${SDK_SUBSCRIPTION_PATCH} - Subscriptions derived from ${handlerCount} handler(s)`);
4082
4260
  this.updateSubscriptions();
4083
4261
  if (this.shouldUpdateSubscriptionsOnSettingsChange && this.settingsData.length > 0) {
4084
4262
  this.updateSubscriptionsFromSettings();
@@ -4087,28 +4265,33 @@ class AppSession {
4087
4265
  const errorMessage = message.message || "Unknown connection error";
4088
4266
  this.events.emit("error", new Error(errorMessage));
4089
4267
  } else if (message.type === "audio_chunk" /* AUDIO_CHUNK */) {
4090
- if (this.subscriptions.has("audio_chunk" /* AUDIO_CHUNK */)) {
4268
+ const hasAudioHandler = this.events.getRegisteredStreams().includes("audio_chunk" /* AUDIO_CHUNK */);
4269
+ if (hasAudioHandler) {
4091
4270
  this.events.emit("audio_chunk" /* AUDIO_CHUNK */, message);
4092
4271
  }
4093
4272
  } else if (isDataStream(message) && message.streamType === "glasses_connection_state" /* GLASSES_CONNECTION_STATE */) {
4094
4273
  this.glassesConnectionState = message.data;
4095
- if (this.subscriptions.has("glasses_connection_state" /* GLASSES_CONNECTION_STATE */)) {
4274
+ const hasGlassesStateHandler = this.events.getRegisteredStreams().includes("glasses_connection_state" /* GLASSES_CONNECTION_STATE */);
4275
+ if (hasGlassesStateHandler) {
4096
4276
  const sanitizedData = this.sanitizeEventData("glasses_connection_state" /* GLASSES_CONNECTION_STATE */, message.data);
4097
4277
  this.events.emit("glasses_connection_state" /* GLASSES_CONNECTION_STATE */, sanitizedData);
4098
4278
  }
4099
4279
  } else if (isDataStream(message)) {
4100
4280
  const messageStreamType = message.streamType;
4101
- if (messageStreamType && this.subscriptions.has(messageStreamType)) {
4281
+ const hasHandler = this.events.getRegisteredStreams().includes(messageStreamType);
4282
+ if (messageStreamType && hasHandler) {
4102
4283
  const sanitizedData = this.sanitizeEventData(messageStreamType, message.data);
4103
4284
  this.events.emit(messageStreamType, sanitizedData);
4104
4285
  }
4105
4286
  } else if (isRtmpStreamStatus2(message)) {
4106
- if (this.subscriptions.has("rtmp_stream_status" /* RTMP_STREAM_STATUS */)) {
4287
+ const hasRtmpHandler = this.events.getRegisteredStreams().includes("rtmp_stream_status" /* RTMP_STREAM_STATUS */);
4288
+ if (hasRtmpHandler) {
4107
4289
  this.events.emit("rtmp_stream_status" /* RTMP_STREAM_STATUS */, message);
4108
4290
  }
4109
4291
  this.camera.updateStreamState(message);
4110
4292
  } else if (isManagedStreamStatus(message)) {
4111
- if (this.subscriptions.has("managed_stream_status" /* MANAGED_STREAM_STATUS */)) {
4293
+ const hasManagedStreamHandler = this.events.getRegisteredStreams().includes("managed_stream_status" /* MANAGED_STREAM_STATUS */);
4294
+ if (hasManagedStreamHandler) {
4112
4295
  this.events.emit("managed_stream_status" /* MANAGED_STREAM_STATUS */, message);
4113
4296
  }
4114
4297
  this.camera.handleManagedStreamStatus(message);
@@ -4139,6 +4322,12 @@ class AppSession {
4139
4322
  modelName: capabilitiesMessage.modelName,
4140
4323
  timestamp: capabilitiesMessage.timestamp
4141
4324
  });
4325
+ } else if (isDeviceStateUpdate(message)) {
4326
+ this.device.state.updateFromMessage(message.state);
4327
+ this.logger.debug({
4328
+ changedFields: Object.keys(message.state),
4329
+ fullSnapshot: message.fullSnapshot
4330
+ }, `[AppSession] Device state updated via WebSocket`);
4142
4331
  } else if (isAppStopped(message)) {
4143
4332
  const reason = message.reason || "unknown";
4144
4333
  const displayReason = `App stopped: ${reason}`;
@@ -4245,7 +4434,8 @@ class AppSession {
4245
4434
  }
4246
4435
  handleBinaryMessage(buffer) {
4247
4436
  try {
4248
- if (!this.subscriptions.has("audio_chunk" /* AUDIO_CHUNK */)) {
4437
+ const hasAudioHandler = this.events.getRegisteredStreams().includes("audio_chunk" /* AUDIO_CHUNK */);
4438
+ if (!hasAudioHandler) {
4249
4439
  return;
4250
4440
  }
4251
4441
  if (!buffer || buffer.byteLength === 0) {
@@ -4318,8 +4508,9 @@ class AppSession {
4318
4508
  this.send(message);
4319
4509
  }
4320
4510
  updateSubscriptions() {
4321
- this.logger.info({ subscriptions: JSON.stringify(Array.from(this.subscriptions)) }, `[AppSession] updateSubscriptions: sending subscriptions to cloud`);
4322
- const subscriptionPayload = Array.from(this.subscriptions).map((stream) => {
4511
+ const derivedSubscriptions = this.events.getRegisteredStreams();
4512
+ this.logger.info({ subscriptions: JSON.stringify(derivedSubscriptions) }, `[AppSession] updateSubscriptions: sending ${derivedSubscriptions.length} subscriptions to cloud (derived from handlers)`);
4513
+ const subscriptionPayload = derivedSubscriptions.map((stream) => {
4323
4514
  const rate = this.streamRates.get(stream);
4324
4515
  if (rate && stream === "location_stream" /* LOCATION_STREAM */) {
4325
4516
  return { stream: "location_stream", rate };
@@ -4336,6 +4527,10 @@ class AppSession {
4336
4527
  this.send(message);
4337
4528
  }
4338
4529
  async handleReconnection() {
4530
+ if (this.terminated) {
4531
+ this.logger.info(`\uD83D\uDD04 Reconnection skipped: session was terminated (User session ended). ` + `If cloud restarts app, onSession will be called with fresh handlers.`);
4532
+ return;
4533
+ }
4339
4534
  if (!this.config.autoReconnect || !this.sessionId) {
4340
4535
  this.logger.debug(`\uD83D\uDD04 Reconnection skipped: autoReconnect=${this.config.autoReconnect}, sessionId=${this.sessionId ? "valid" : "invalid"}`);
4341
4536
  return;
@@ -4415,7 +4610,12 @@ class AppSession {
4415
4610
  throw new Error(`Failed to send message: ${errorMessage}`);
4416
4611
  }
4417
4612
  } catch (error) {
4418
- this.logger.error(error, "Message send error");
4613
+ const isDisconnectError = error instanceof Error && (error.message.includes("WebSocket not connected") || error.message.includes("CLOSED") || error.message.includes("CLOSING"));
4614
+ if (isDisconnectError) {
4615
+ this.logger.debug(error, "Message send skipped - session disconnected");
4616
+ } else {
4617
+ this.logger.error(error, "Message send error");
4618
+ }
4419
4619
  if (error instanceof Error) {
4420
4620
  this.events.emit("error", error);
4421
4621
  } else {
@@ -4968,10 +5168,10 @@ class AppServer {
4968
5168
  });
4969
5169
  });
4970
5170
  }
4971
- stop() {
5171
+ async stop() {
4972
5172
  this.logger.info(`
4973
5173
  \uD83D\uDED1 Shutting down...`);
4974
- this.cleanup();
5174
+ await this.cleanup();
4975
5175
  process.exit(0);
4976
5176
  }
4977
5177
  generateToken(userId, sessionId, secretKey) {
@@ -5175,10 +5375,20 @@ class AppServer {
5175
5375
  process.on("SIGTERM", () => this.stop());
5176
5376
  process.on("SIGINT", () => this.stop());
5177
5377
  }
5178
- cleanup() {
5378
+ async cleanup() {
5179
5379
  for (const [sessionId, session] of this.activeSessions) {
5180
- this.logger.info(`\uD83D\uDC4B Closing session ${sessionId}`);
5181
- session.disconnect();
5380
+ this.logger.info(`\uD83D\uDC4B Closing session ${sessionId} with ownership release`);
5381
+ try {
5382
+ await session.disconnect({
5383
+ releaseOwnership: true,
5384
+ reason: "clean_shutdown"
5385
+ });
5386
+ } catch (error) {
5387
+ this.logger.error(error, `Error during cleanup of session ${sessionId}`);
5388
+ try {
5389
+ await session.disconnect();
5390
+ } catch {}
5391
+ }
5182
5392
  }
5183
5393
  this.activeSessions.clear();
5184
5394
  this.activeSessionsByUserId.clear();
@@ -5332,6 +5542,7 @@ export {
5332
5542
  isPhoneNotificationDismissed,
5333
5543
  isPhoneNotification,
5334
5544
  isPhoneBatteryUpdate,
5545
+ isOwnershipRelease,
5335
5546
  isMicrophoneStateChange,
5336
5547
  isManagedStreamStopRequest,
5337
5548
  isManagedStreamStatus,
@@ -5400,6 +5611,7 @@ export {
5400
5611
  PhotoStage,
5401
5612
  PhotoErrorCode,
5402
5613
  PermissionType,
5614
+ Observable,
5403
5615
  LedModule,
5404
5616
  LayoutType,
5405
5617
  LEGACY_PERMISSION_MAP,
@@ -5408,6 +5620,7 @@ export {
5408
5620
  GlassesToCloudMessageType,
5409
5621
  GIVE_APP_CONTROL_OF_TOOL_RESPONSE,
5410
5622
  EventTypes,
5623
+ DeviceState,
5411
5624
  DashboardMode,
5412
5625
  DashboardMessageTypes,
5413
5626
  ControlActionTypes,
@@ -5425,4 +5638,4 @@ export {
5425
5638
  AnimationUtils
5426
5639
  };
5427
5640
 
5428
- //# debugId=AD30D8A3207C691A64756E2164756E21
5641
+ //# debugId=BBCA9D1CAB7CB76464756E2164756E21