@alwaysai/device-agent 1.4.0 → 1.5.0

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 (139) hide show
  1. package/lib/application-control/config.js +2 -2
  2. package/lib/application-control/config.js.map +1 -1
  3. package/lib/application-control/install.d.ts.map +1 -1
  4. package/lib/application-control/install.js +1 -0
  5. package/lib/application-control/install.js.map +1 -1
  6. package/lib/application-control/models.d.ts +5 -0
  7. package/lib/application-control/models.d.ts.map +1 -1
  8. package/lib/application-control/models.js +24 -12
  9. package/lib/application-control/models.js.map +1 -1
  10. package/lib/application-control/status.d.ts.map +1 -1
  11. package/lib/application-control/status.js +10 -12
  12. package/lib/application-control/status.js.map +1 -1
  13. package/lib/application-control/utils.js +2 -2
  14. package/lib/application-control/utils.js.map +1 -1
  15. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +2 -2
  16. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  17. package/lib/cloud-connection/device-agent-cloud-connection.js +35 -15
  18. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  19. package/lib/cloud-connection/live-updates-handler.d.ts +3 -2
  20. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  21. package/lib/cloud-connection/live-updates-handler.js +24 -21
  22. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  23. package/lib/cloud-connection/live-updates-handler.test.js +132 -16
  24. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  25. package/lib/cloud-connection/passthrough-handler.d.ts +5 -3
  26. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/passthrough-handler.js +76 -62
  28. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.d.ts +2 -0
  30. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  31. package/lib/cloud-connection/shadow-handler.js +5 -5
  32. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  33. package/lib/cloud-connection/transaction-manager.d.ts +3 -0
  34. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  35. package/lib/cloud-connection/transaction-manager.js +11 -0
  36. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  37. package/lib/cloud-connection/transaction-manager.test.js +102 -0
  38. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  39. package/lib/device-control/device-control.d.ts +10 -2
  40. package/lib/device-control/device-control.d.ts.map +1 -1
  41. package/lib/device-control/device-control.js +86 -10
  42. package/lib/device-control/device-control.js.map +1 -1
  43. package/lib/docker/docker-compose.d.ts +14 -0
  44. package/lib/docker/docker-compose.d.ts.map +1 -0
  45. package/lib/docker/docker-compose.js +56 -0
  46. package/lib/docker/docker-compose.js.map +1 -0
  47. package/lib/index.js +2 -5
  48. package/lib/index.js.map +1 -1
  49. package/lib/infrastructure/agent-config.d.ts +45 -14
  50. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  51. package/lib/infrastructure/agent-config.js +30 -15
  52. package/lib/infrastructure/agent-config.js.map +1 -1
  53. package/lib/infrastructure/agent-config.test.js +3 -0
  54. package/lib/infrastructure/agent-config.test.js.map +1 -1
  55. package/lib/local-connection/rabbitmq-connection.js +11 -11
  56. package/lib/local-connection/rabbitmq-connection.js.map +1 -1
  57. package/lib/secure-tunneling/secure-tunneling.d.ts +14 -22
  58. package/lib/secure-tunneling/secure-tunneling.d.ts.map +1 -1
  59. package/lib/secure-tunneling/secure-tunneling.js +34 -34
  60. package/lib/secure-tunneling/secure-tunneling.js.map +1 -1
  61. package/lib/secure-tunneling/secure-tunneling.test.js +18 -18
  62. package/lib/secure-tunneling/secure-tunneling.test.js.map +1 -1
  63. package/lib/subcommands/device/clean.js +5 -5
  64. package/lib/subcommands/device/clean.js.map +1 -1
  65. package/lib/subcommands/device/get-info.d.ts +2 -0
  66. package/lib/subcommands/device/get-info.d.ts.map +1 -0
  67. package/lib/subcommands/device/get-info.js +36 -0
  68. package/lib/subcommands/device/get-info.js.map +1 -0
  69. package/lib/subcommands/device/index.d.ts.map +1 -1
  70. package/lib/subcommands/device/index.js +11 -2
  71. package/lib/subcommands/device/index.js.map +1 -1
  72. package/lib/subcommands/device/init.d.ts +5 -0
  73. package/lib/subcommands/device/init.d.ts.map +1 -0
  74. package/lib/subcommands/device/{device.js → init.js} +2 -41
  75. package/lib/subcommands/device/init.js.map +1 -0
  76. package/lib/subcommands/device/refresh.d.ts +2 -0
  77. package/lib/subcommands/device/refresh.d.ts.map +1 -0
  78. package/lib/subcommands/device/refresh.js +24 -0
  79. package/lib/subcommands/device/refresh.js.map +1 -0
  80. package/lib/subcommands/device/restart.d.ts +2 -0
  81. package/lib/subcommands/device/restart.d.ts.map +1 -0
  82. package/lib/subcommands/device/restart.js +14 -0
  83. package/lib/subcommands/device/restart.js.map +1 -0
  84. package/lib/util/check-for-updates.d.ts +3 -0
  85. package/lib/util/check-for-updates.d.ts.map +1 -0
  86. package/lib/util/check-for-updates.js +69 -0
  87. package/lib/util/check-for-updates.js.map +1 -0
  88. package/lib/util/file.d.ts +7 -0
  89. package/lib/util/file.d.ts.map +1 -0
  90. package/lib/util/file.js +66 -0
  91. package/lib/util/file.js.map +1 -0
  92. package/lib/util/file.test.d.ts +2 -0
  93. package/lib/util/file.test.d.ts.map +1 -0
  94. package/lib/util/file.test.js +87 -0
  95. package/lib/util/file.test.js.map +1 -0
  96. package/package.json +8 -7
  97. package/readme.md +3 -3
  98. package/src/application-control/config.ts +1 -1
  99. package/src/application-control/install.ts +1 -0
  100. package/src/application-control/models.ts +36 -13
  101. package/src/application-control/status.ts +9 -7
  102. package/src/application-control/utils.ts +1 -1
  103. package/src/cloud-connection/device-agent-cloud-connection.ts +54 -30
  104. package/src/cloud-connection/live-updates-handler.test.ts +161 -20
  105. package/src/cloud-connection/live-updates-handler.ts +30 -26
  106. package/src/cloud-connection/passthrough-handler.ts +98 -76
  107. package/src/cloud-connection/shadow-handler.ts +19 -7
  108. package/src/cloud-connection/transaction-manager.test.ts +124 -0
  109. package/src/cloud-connection/transaction-manager.ts +15 -0
  110. package/src/device-control/device-control.ts +86 -11
  111. package/src/docker/docker-compose.ts +60 -0
  112. package/src/index.ts +2 -6
  113. package/src/infrastructure/agent-config.test.ts +3 -0
  114. package/src/infrastructure/agent-config.ts +38 -40
  115. package/src/local-connection/rabbitmq-connection.ts +8 -8
  116. package/src/secure-tunneling/secure-tunneling.test.ts +26 -26
  117. package/src/secure-tunneling/secure-tunneling.ts +48 -55
  118. package/src/subcommands/device/clean.ts +1 -1
  119. package/src/subcommands/device/get-info.ts +49 -0
  120. package/src/subcommands/device/index.ts +11 -2
  121. package/src/subcommands/device/{device.ts → init.ts} +0 -58
  122. package/src/subcommands/device/refresh.ts +22 -0
  123. package/src/subcommands/device/restart.ts +11 -0
  124. package/src/util/check-for-updates.ts +69 -0
  125. package/src/util/file.test.ts +90 -0
  126. package/src/util/file.ts +76 -0
  127. package/lib/docker/docker-compose-cmd.d.ts +0 -5
  128. package/lib/docker/docker-compose-cmd.d.ts.map +0 -1
  129. package/lib/docker/docker-compose-cmd.js +0 -16
  130. package/lib/docker/docker-compose-cmd.js.map +0 -1
  131. package/lib/subcommands/device/device.d.ts +0 -7
  132. package/lib/subcommands/device/device.d.ts.map +0 -1
  133. package/lib/subcommands/device/device.js.map +0 -1
  134. package/lib/util/safe-rimraf.d.ts +0 -2
  135. package/lib/util/safe-rimraf.d.ts.map +0 -1
  136. package/lib/util/safe-rimraf.js +0 -16
  137. package/lib/util/safe-rimraf.js.map +0 -1
  138. package/src/docker/docker-compose-cmd.ts +0 -15
  139. package/src/util/safe-rimraf.ts +0 -14
@@ -15,7 +15,8 @@ import {
15
15
  buildToClientStatusResponseMessage,
16
16
  StatusResponsePayload,
17
17
  keyMirrors,
18
- validateToDeviceAgentMessage
18
+ validateToDeviceAgentMessage,
19
+ validateSecureTunnelShadowUpdate
19
20
  } from '@alwaysai/device-agent-schemas';
20
21
  import { existsSync } from 'fs';
21
22
  import {
@@ -46,12 +47,14 @@ import { logger } from '../util/logger';
46
47
  import sleep from '../util/sleep';
47
48
  import { bootstrapProvision } from './bootstrap-provision';
48
49
  import { LiveUpdatesHandler } from './live-updates-handler';
49
- import { PassthroughHandler, runChannel } from './passthrough-handler';
50
+ import { PassthroughHandler } from './passthrough-handler';
50
51
  import { Publisher } from './publisher';
51
52
  import { ShadowHandler, ShadowUpdate } from './shadow-handler';
52
53
  import { TransactionManager } from './transaction-manager';
53
54
  import { exec } from 'child_process';
54
55
  import { promisify } from 'util';
56
+ import { pruneModels } from '../application-control/models';
57
+ import { getDeviceAgentVersion } from '../util/check-for-updates';
55
58
 
56
59
  const exec_promise = promisify(exec);
57
60
 
@@ -69,8 +72,6 @@ export class DeviceAgentCloudConnection {
69
72
  private readonly secureTunnelNotifyTopic = `$aws/things/${this.clientId}/tunnels/notify`;
70
73
  private readonly secureTunnelHandler =
71
74
  SecureTunnelHandlerSingleton.getInstance();
72
- // FIXME: Add support for multiple simultaneous project updates
73
- private appCfgUpdateQueue: ShadowUpdate[] = [];
74
75
 
75
76
  private handleAppStateControl = async (
76
77
  payload: AppStateControlPayload
@@ -152,16 +153,18 @@ export class DeviceAgentCloudConnection {
152
153
  };
153
154
 
154
155
  private handleModelsInstallCloudResponsePayload = async (
155
- payload: ModelsInstallResponsePayload
156
+ payload: ModelsInstallResponsePayload,
157
+ txId: string
156
158
  ): Promise<boolean> => {
157
- const update = this.appCfgUpdateQueue.shift();
159
+ const projectId = payload.modelsInstallResponse.projectId;
160
+
161
+ const update = this.txnMgr.getAppCfgUpdateFromTxID(txId);
158
162
  if (update === undefined) {
159
163
  throw new Error(
160
164
  'Unknown error while updating models via application config! No config present for model update.'
161
165
  );
162
166
  }
163
167
  const { appCfgUpdate, envVarUpdate } = update;
164
- const projectId = payload.modelsInstallResponse.projectId;
165
168
  if (appCfgUpdate) {
166
169
  await this.atomicApplicationUpdate(
167
170
  async () =>
@@ -185,6 +188,7 @@ export class DeviceAgentCloudConnection {
185
188
  true
186
189
  );
187
190
  }
191
+
188
192
  return true;
189
193
  };
190
194
 
@@ -305,19 +309,22 @@ export class DeviceAgentCloudConnection {
305
309
  );
306
310
  this.publisher.publishToCloud(message);
307
311
 
308
- this.appCfgUpdateQueue.push(update);
312
+ this.txnMgr.setAppCfgUpdateToTx(txId, update);
313
+
309
314
  return false;
310
315
  }
311
316
 
312
317
  if (appCfgUpdate) {
313
- await this.atomicApplicationUpdate(
314
- async () =>
315
- await updateAppCfg({
316
- projectId,
317
- newAppCfg: appCfgUpdate.newAppCfg
318
- }),
319
- projectId
320
- );
318
+ await this.atomicApplicationUpdate(async () => {
319
+ await pruneModels({
320
+ projectId,
321
+ appCfg: appCfgUpdate.newAppCfg
322
+ });
323
+ await updateAppCfg({
324
+ projectId,
325
+ newAppCfg: appCfgUpdate.newAppCfg
326
+ });
327
+ }, projectId);
321
328
  }
322
329
 
323
330
  if (envVarUpdate) {
@@ -369,6 +376,30 @@ export class DeviceAgentCloudConnection {
369
376
  }
370
377
  }
371
378
 
379
+ public async handleSecureTunnelMessage(payload: any): Promise<void> {
380
+ logger.info(`Received secure tunnel update: ${JSON.stringify(payload)}`);
381
+ const state = payload.state;
382
+ if (!state) {
383
+ logger.debug(`No state found in message: ${JSON.stringify(payload)}`);
384
+ return;
385
+ }
386
+ const valid = validateSecureTunnelShadowUpdate(state);
387
+ if (!valid) {
388
+ logger.error(
389
+ `Error validating message: ${JSON.stringify(
390
+ { payload, errors: validateSecureTunnelShadowUpdate.errors },
391
+ null,
392
+ 2
393
+ )}`
394
+ );
395
+ return;
396
+ }
397
+ const secureTunnelUpdate =
398
+ await this.secureTunnelHandler.syncShadowToDeviceState(payload);
399
+ await this.shadowHandler.updateSecureTunnelShadow(secureTunnelUpdate);
400
+ return;
401
+ }
402
+
372
403
  /*=================================================================
373
404
  Public interface
374
405
  =================================================================*/
@@ -526,7 +557,8 @@ export class DeviceAgentCloudConnection {
526
557
  );
527
558
  }
528
559
  await this.txnMgr.runTransactionStep({
529
- func: () => this.handleModelsInstallCloudResponsePayload(payload),
560
+ func: () =>
561
+ this.handleModelsInstallCloudResponsePayload(payload, txId),
530
562
  projectId,
531
563
  txId,
532
564
  start: false,
@@ -607,10 +639,7 @@ export class DeviceAgentCloudConnection {
607
639
  }
608
640
  }
609
641
 
610
- public async handleMessage(
611
- topic: string,
612
- message: ToDeviceAgentMessage | any
613
- ) {
642
+ public async handleMessage(topic: string, message: any) {
614
643
  logger.debug(
615
644
  `Received message: ${JSON.stringify({ topic, message }, null, 2)}`
616
645
  );
@@ -629,14 +658,7 @@ export class DeviceAgentCloudConnection {
629
658
  } else if (
630
659
  topic === this.shadowHandler.shadowTopics.secureTunnel.updateDelta
631
660
  ) {
632
- logger.info(`Received secure tunnel update: ${message}`);
633
- const reported = await this.secureTunnelHandler.syncShadowToDeviceState(
634
- message
635
- );
636
- this.publisher.publish(
637
- this.shadowHandler.shadowTopics.secureTunnel.update,
638
- JSON.stringify({ state: { reported } })
639
- );
661
+ await this.handleSecureTunnelMessage(message);
640
662
  } else if (
641
663
  topic === this.shadowHandler.shadowTopics.secureTunnel.deleteAccepted
642
664
  ) {
@@ -749,6 +771,9 @@ export class DeviceAgentCloudConnection {
749
771
  }
750
772
 
751
773
  export async function runDeviceAgentCloudInterface() {
774
+ logger.info(
775
+ `Starting alwaysAI Device Agent v${await getDeviceAgentVersion()}`
776
+ );
752
777
  if (cloudModeReady()) {
753
778
  const deviceAgent = new DeviceAgentCloudConnection();
754
779
  await deviceAgent.setupHandlers();
@@ -756,7 +781,6 @@ export async function runDeviceAgentCloudInterface() {
756
781
  const publisher = deviceAgent.publisher;
757
782
  const passthroughHandler = new PassthroughHandler(publisher);
758
783
  await passthroughHandler.setup();
759
- await runChannel(passthroughHandler);
760
784
  }
761
785
  } else if (existsSync(BOOTSTRAP_PRIVATE_KEY_FILE_PATH())) {
762
786
  await bootstrapProvision();
@@ -1,5 +1,12 @@
1
1
  import { LiveUpdatesHandler } from './live-updates-handler';
2
2
  import { Publisher } from './publisher';
3
+ import sleep from '../util/sleep';
4
+ import { getDeviceStatsPayload } from './messages';
5
+
6
+ jest.mock('../util/sleep');
7
+ jest.mock('./messages');
8
+
9
+ jest.mocked(getDeviceStatsPayload).mockResolvedValue({});
3
10
 
4
11
  global.setTimeout = jest.fn() as unknown as typeof setTimeout;
5
12
 
@@ -18,64 +25,163 @@ const testFalseToggles = {
18
25
  appState: false
19
26
  };
20
27
 
21
- const mockClient = jest.fn();
28
+ const mockClient = {
29
+ publish: jest.fn()
30
+ };
22
31
  const clientId = 'test-client';
23
32
  const emptyTxId = '';
24
33
 
25
- // NOTE: this was the way I found to mock private class functions
26
- const mockStartPublishingLiveUpdates = jest.spyOn(
27
- LiveUpdatesHandler.prototype as any,
28
- 'startPublishingLiveUpdates'
29
- );
30
- mockStartPublishingLiveUpdates.mockResolvedValue(null);
31
-
32
34
  describe('Test Live Updates Handler', () => {
33
35
  let liveUpdatesHandler: LiveUpdatesHandler;
34
36
  let publisher: Publisher;
35
37
 
36
38
  beforeEach(() => {
39
+ mockClient.publish = jest.fn();
40
+ jest.mocked(sleep).mockImplementation(async () => {
41
+ return;
42
+ });
37
43
  publisher = new Publisher(mockClient, clientId);
38
44
  liveUpdatesHandler = new LiveUpdatesHandler(publisher, clientId);
39
45
  jest.clearAllMocks();
40
46
  });
41
47
 
48
+ test('enable device stats', async () => {
49
+ mockClient.publish = jest.fn().mockImplementation(async () => {
50
+ liveUpdatesHandler.disableDeviceStatsLiveUpdates();
51
+ });
52
+
53
+ const enable = { deviceStats: true };
54
+ const promises = await liveUpdatesHandler.handleToggles(enable, emptyTxId);
55
+ await Promise.all(promises);
56
+
57
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
58
+ expect(mockClient.publish).toBeCalledTimes(1);
59
+ expect(jest.mocked(sleep)).toBeCalledTimes(1);
60
+ });
61
+
62
+ test('enable app state', async () => {
63
+ mockClient.publish = jest.fn().mockImplementation(async () => {
64
+ liveUpdatesHandler.disableAppStateLiveUpdates();
65
+ });
66
+
67
+ const enable = { appState: true };
68
+ const promises = await liveUpdatesHandler.handleToggles(enable, emptyTxId);
69
+ await Promise.all(promises);
70
+
71
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
72
+ expect(mockClient.publish).toBeCalledTimes(1);
73
+ expect(jest.mocked(sleep)).toBeCalledTimes(1);
74
+ });
75
+
42
76
  test('ignore subsequent enables', async () => {
43
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
44
- // called twice, once for device stats, once for app state
45
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
46
- // restartLiveUpdatesTimeout is always called once when handleToggles is called
77
+ // Block on the first sleep call
78
+ let doneWaiting;
79
+ const waitPromise = new Promise(function (resolve) {
80
+ doneWaiting = resolve;
81
+ });
82
+
83
+ let startSleeping;
84
+ const sleepPromise = new Promise(function (resolve) {
85
+ startSleeping = resolve;
86
+ });
87
+ jest.mocked(sleep).mockImplementation(async () => {
88
+ startSleeping();
89
+ await waitPromise;
90
+ });
91
+
92
+ const enable = { deviceStats: true };
93
+ await liveUpdatesHandler.handleToggles(enable, emptyTxId);
94
+ await sleepPromise;
95
+
47
96
  expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
97
+ expect(mockClient.publish).toBeCalledTimes(1);
98
+ expect(jest.mocked(sleep)).toBeCalledTimes(1);
48
99
 
49
- // Second call -> should not call startPublishingLiveUpdates should not be called
100
+ // Second call
50
101
  jest.clearAllMocks();
51
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
52
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
102
+
103
+ await liveUpdatesHandler.handleToggles(enable, emptyTxId);
104
+
53
105
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
106
+ expect(mockClient.publish).toBeCalledTimes(0);
107
+ expect(jest.mocked(sleep)).toBeCalledTimes(0);
108
+
109
+ // Disable and clean up
110
+ liveUpdatesHandler.disableDeviceStatsLiveUpdates();
111
+ doneWaiting();
54
112
  });
55
113
 
56
114
  test('test disable live updates', async () => {
115
+ let doneWaiting;
116
+ const waitPromise = new Promise(function (resolve) {
117
+ doneWaiting = resolve;
118
+ });
119
+
120
+ let startSleeping;
121
+ const sleepPromise = new Promise(function (resolve) {
122
+ startSleeping = resolve;
123
+ });
124
+
125
+ // Wait for two sleep calls, one for Device Stats and one for App State
126
+ jest
127
+ .mocked(sleep)
128
+ .mockImplementationOnce(async () => {
129
+ await waitPromise;
130
+ })
131
+ .mockImplementationOnce(async () => {
132
+ startSleeping();
133
+ await waitPromise;
134
+ });
135
+
57
136
  // Test calling handleToggles one time, enabling it
58
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
59
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
137
+ await liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
138
+ await sleepPromise;
139
+
60
140
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
141
+ expect(jest.mocked(sleep)).toBeCalledTimes(2);
61
142
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
62
143
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(true);
63
144
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
64
145
 
65
146
  // Check to see that attributes are properly set to false when disabled
66
147
  jest.clearAllMocks();
67
- void liveUpdatesHandler.handleToggles(testFalseToggles, emptyTxId);
68
- expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
148
+ await liveUpdatesHandler.handleToggles(testFalseToggles, emptyTxId);
149
+
69
150
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
151
+ expect(jest.mocked(sleep)).toBeCalledTimes(0);
70
152
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);
71
153
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(false);
72
154
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
155
+
156
+ doneWaiting();
73
157
  });
74
158
 
75
159
  test('timeout turns off live updates', async () => {
76
160
  jest.useFakeTimers({ legacyFakeTimers: true });
161
+ let doneWaiting;
162
+ const waitPromise = new Promise(function (resolve) {
163
+ doneWaiting = resolve;
164
+ });
165
+
166
+ let startSleeping;
167
+ const sleepPromise = new Promise(function (resolve) {
168
+ startSleeping = resolve;
169
+ });
170
+
171
+ // Wait for two sleep calls, one for Device Stats and one for App State
172
+ jest
173
+ .mocked(sleep)
174
+ .mockImplementationOnce(async () => {
175
+ await waitPromise;
176
+ })
177
+ .mockImplementationOnce(async () => {
178
+ startSleeping();
179
+ await waitPromise;
180
+ });
181
+
182
+ await liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
183
+ await sleepPromise;
77
184
 
78
- void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
79
185
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
80
186
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(true);
81
187
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
@@ -85,5 +191,40 @@ describe('Test Live Updates Handler', () => {
85
191
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);
86
192
  expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(false);
87
193
  expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
194
+
195
+ doneWaiting();
196
+ });
197
+
198
+ test("failure doesn't kill publish loop", async () => {
199
+ // Block on the first sleep call
200
+ let doneWaiting;
201
+ const waitPromise = new Promise(function (resolve) {
202
+ doneWaiting = resolve;
203
+ });
204
+
205
+ let startSleeping;
206
+ const sleepPromise = new Promise(function (resolve) {
207
+ startSleeping = resolve;
208
+ });
209
+ jest
210
+ .mocked(sleep)
211
+ .mockImplementationOnce(async () => {
212
+ throw new Error('Test error!');
213
+ })
214
+ .mockImplementationOnce(async () => {
215
+ startSleeping();
216
+ await waitPromise;
217
+ });
218
+
219
+ const enable = { deviceStats: true };
220
+ await liveUpdatesHandler.handleToggles(enable, emptyTxId);
221
+ await sleepPromise;
222
+
223
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
224
+ expect(mockClient.publish).toBeCalledTimes(2);
225
+ expect(jest.mocked(sleep)).toBeCalledTimes(2);
226
+
227
+ liveUpdatesHandler.disableDeviceStatsLiveUpdates();
228
+ doneWaiting();
88
229
  });
89
230
  });
@@ -16,11 +16,10 @@ import sleep from '../util/sleep';
16
16
  import { Publisher } from './publisher';
17
17
  import { getAppStatePayload, getDeviceStatsPayload } from './messages';
18
18
  import { ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS } from '../environment';
19
- import { read } from 'fs';
20
19
 
21
20
  const LIVE_UPDATES_TIMEOUT = ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS
22
21
  ? parseInt(ALWAYSAI_LIVE_UPDATES_TIMEOUT_MS)
23
- : 60000;
22
+ : 80000;
24
23
 
25
24
  export class LiveUpdatesHandler {
26
25
  private publisher: Publisher;
@@ -135,22 +134,11 @@ export class LiveUpdatesHandler {
135
134
  return -1;
136
135
  }
137
136
 
138
- private setLiveUpdates(toggles: LiveStateUpdatesTogglePayload) {
139
- if (toggles.deviceStats !== undefined) {
140
- this.liveUpdatesAlive.device_stats = toggles.deviceStats;
141
- }
142
- if (toggles.appState !== undefined) {
143
- this.liveUpdatesAlive.app_state = toggles.appState;
144
- }
145
- }
146
-
147
137
  private restartLiveUpdatesTimeout() {
148
138
  clearTimeout(this.liveUpdatesTimeout);
149
139
  this.liveUpdatesTimeout = setTimeout(() => {
150
- this.setLiveUpdates({
151
- deviceStats: false,
152
- appState: false
153
- });
140
+ this.disableAppStateLiveUpdates();
141
+ this.disableDeviceStatsLiveUpdates();
154
142
  this.appLogStreams.clear();
155
143
  }, LIVE_UPDATES_TIMEOUT);
156
144
  }
@@ -161,17 +149,17 @@ export class LiveUpdatesHandler {
161
149
  txId: string
162
150
  ) {
163
151
  logger.info(`Turned on live updates for ${messageType}`);
164
- try {
165
- while (this.continuePublishing(messageType, txId)) {
152
+ while (this.continuePublishing(messageType, txId)) {
153
+ try {
166
154
  const message = await getMessage();
167
155
  this.publisher.publishToClient(message, logger.silly);
168
156
 
169
157
  await sleep(this.getLiveUpdatesInterval(messageType));
158
+ } catch (e) {
159
+ logger.error(
160
+ `Error publishing live updates for ${messageType}: ${e.message}`
161
+ );
170
162
  }
171
- } catch (e) {
172
- logger.error(
173
- `Error publishing live updates for ${messageType}: ${e.message}`
174
- );
175
163
  }
176
164
  logger.info(`Turned off live updates for ${messageType}`);
177
165
  }
@@ -188,14 +176,24 @@ export class LiveUpdatesHandler {
188
176
  );
189
177
  }
190
178
 
191
- public getDeviceStatsLiveUpdates() {
179
+ public getDeviceStatsLiveUpdates(): boolean {
192
180
  return this.liveUpdatesAlive.device_stats;
193
181
  }
194
182
 
195
- public getAppStateLiveUpdates() {
183
+ public disableDeviceStatsLiveUpdates() {
184
+ logger.info('Disabled live updates for device_stats');
185
+ this.liveUpdatesAlive.device_stats = false;
186
+ }
187
+
188
+ public getAppStateLiveUpdates(): boolean {
196
189
  return this.liveUpdatesAlive.app_state;
197
190
  }
198
191
 
192
+ public disableAppStateLiveUpdates() {
193
+ logger.info('Disabled live updates for app_state');
194
+ this.liveUpdatesAlive.app_state = false;
195
+ }
196
+
199
197
  public getAppLogsLiveUpdates() {
200
198
  return this.liveUpdatesAlive.app_logs;
201
199
  }
@@ -239,12 +237,14 @@ export class LiveUpdatesHandler {
239
237
  const { deviceStats, appState, appLogs } = toggles;
240
238
  this.restartLiveUpdatesTimeout();
241
239
 
240
+ const promises: Promise<void>[] = [];
241
+
242
242
  if (deviceStats !== undefined) {
243
243
  const currentDeviceStats = this.getDeviceStatsLiveUpdates();
244
244
  this.liveUpdatesAlive.device_stats = deviceStats;
245
245
  if (deviceStats && currentDeviceStats !== true) {
246
246
  // Don't wait for this call to finish since it loops until disabled
247
- void this.startPublishingLiveUpdates(
247
+ const deviceStatsPromise = this.startPublishingLiveUpdates(
248
248
  keyMirrors.toClientMessageType.device_stats,
249
249
  async () => {
250
250
  const payload = await getDeviceStatsPayload();
@@ -252,6 +252,7 @@ export class LiveUpdatesHandler {
252
252
  },
253
253
  txId
254
254
  );
255
+ promises.push(deviceStatsPromise);
255
256
  }
256
257
  }
257
258
 
@@ -260,7 +261,7 @@ export class LiveUpdatesHandler {
260
261
  this.liveUpdatesAlive.app_state = appState;
261
262
  if (appState && currentAppState !== true) {
262
263
  // Don't wait for this call to finish since it loops until disabled
263
- void this.startPublishingLiveUpdates(
264
+ const appStatePromise = this.startPublishingLiveUpdates(
264
265
  keyMirrors.toClientMessageType.app_state,
265
266
  async () => {
266
267
  const payload = await getAppStatePayload();
@@ -268,6 +269,7 @@ export class LiveUpdatesHandler {
268
269
  },
269
270
  txId
270
271
  );
272
+ promises.push(appStatePromise);
271
273
  }
272
274
  }
273
275
 
@@ -275,10 +277,12 @@ export class LiveUpdatesHandler {
275
277
  const currentAppLogs = this.getAppLogsLiveUpdates();
276
278
  if (appLogs.toggle && currentAppLogs !== true) {
277
279
  // Don't wait for this call to finish since it loops until disabled
278
- void this.startAppLogStream(appLogs.projectId, txId);
280
+ const appLogPromise = this.startAppLogStream(appLogs.projectId, txId);
281
+ promises.push(appLogPromise);
279
282
  } else {
280
283
  this.appLogStreams.delete(appLogs.projectId);
281
284
  }
282
285
  }
286
+ return promises;
283
287
  }
284
288
  }