@alwaysai/device-agent 0.1.0 → 0.1.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 (96) hide show
  1. package/lib/application-control/config.d.ts +0 -1
  2. package/lib/application-control/config.d.ts.map +1 -1
  3. package/lib/application-control/config.js +15 -29
  4. package/lib/application-control/config.js.map +1 -1
  5. package/lib/application-control/environment-variables.d.ts +7 -3
  6. package/lib/application-control/environment-variables.d.ts.map +1 -1
  7. package/lib/application-control/environment-variables.js +71 -35
  8. package/lib/application-control/environment-variables.js.map +1 -1
  9. package/lib/application-control/environment-variables.test.d.ts +2 -0
  10. package/lib/application-control/environment-variables.test.d.ts.map +1 -0
  11. package/lib/application-control/environment-variables.test.js +163 -0
  12. package/lib/application-control/environment-variables.test.js.map +1 -0
  13. package/lib/application-control/index.d.ts +3 -3
  14. package/lib/application-control/index.d.ts.map +1 -1
  15. package/lib/application-control/index.js +1 -3
  16. package/lib/application-control/index.js.map +1 -1
  17. package/lib/application-control/models.d.ts +0 -1
  18. package/lib/application-control/models.d.ts.map +1 -1
  19. package/lib/application-control/models.js +12 -26
  20. package/lib/application-control/models.js.map +1 -1
  21. package/lib/application-control/status.d.ts +3 -0
  22. package/lib/application-control/status.d.ts.map +1 -1
  23. package/lib/application-control/status.js +19 -1
  24. package/lib/application-control/status.js.map +1 -1
  25. package/lib/application-control/utils.d.ts.map +1 -1
  26. package/lib/application-control/utils.js +2 -2
  27. package/lib/application-control/utils.js.map +1 -1
  28. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +6 -3
  29. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  30. package/lib/cloud-connection/device-agent-cloud-connection.js +201 -151
  31. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  32. package/lib/cloud-connection/live-updates-handler.d.ts +3 -0
  33. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  34. package/lib/cloud-connection/live-updates-handler.js +23 -7
  35. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  36. package/lib/cloud-connection/live-updates-handler.test.d.ts +2 -0
  37. package/lib/cloud-connection/live-updates-handler.test.d.ts.map +1 -0
  38. package/lib/cloud-connection/live-updates-handler.test.js +57 -0
  39. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -0
  40. package/lib/cloud-connection/shadow-handler.d.ts +11 -3
  41. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  42. package/lib/cloud-connection/shadow-handler.js +22 -7
  43. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  44. package/lib/cloud-connection/shadow-handler.test.js +313 -228
  45. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  46. package/lib/cloud-connection/shadow.js +1 -1
  47. package/lib/cloud-connection/shadow.js.map +1 -1
  48. package/lib/environment.d.ts +1 -0
  49. package/lib/environment.d.ts.map +1 -1
  50. package/lib/environment.js +2 -1
  51. package/lib/environment.js.map +1 -1
  52. package/lib/infrastructure/agent-config.d.ts +3 -1
  53. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  54. package/lib/subcommands/app/env-vars.d.ts +1 -1
  55. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  56. package/lib/subcommands/app/env-vars.js +32 -5
  57. package/lib/subcommands/app/env-vars.js.map +1 -1
  58. package/lib/subcommands/app/index.d.ts.map +1 -1
  59. package/lib/subcommands/app/index.js +4 -1
  60. package/lib/subcommands/app/index.js.map +1 -1
  61. package/lib/subcommands/app/models.d.ts.map +1 -1
  62. package/lib/subcommands/app/models.js +6 -1
  63. package/lib/subcommands/app/models.js.map +1 -1
  64. package/lib/subcommands/app/shadow.d.ts +7 -0
  65. package/lib/subcommands/app/shadow.d.ts.map +1 -0
  66. package/lib/subcommands/app/shadow.js +48 -0
  67. package/lib/subcommands/app/shadow.js.map +1 -0
  68. package/lib/subcommands/app/version.js +2 -2
  69. package/lib/subcommands/app/version.js.map +1 -1
  70. package/lib/util/cloud-mode-ready.d.ts +2 -0
  71. package/lib/util/cloud-mode-ready.d.ts.map +1 -0
  72. package/lib/util/cloud-mode-ready.js +22 -0
  73. package/lib/util/cloud-mode-ready.js.map +1 -0
  74. package/package.json +1 -1
  75. package/readme.md +2 -2
  76. package/src/application-control/config.ts +30 -31
  77. package/src/application-control/environment-variables.test.ts +171 -0
  78. package/src/application-control/environment-variables.ts +102 -43
  79. package/src/application-control/index.ts +3 -9
  80. package/src/application-control/models.ts +14 -29
  81. package/src/application-control/status.ts +20 -0
  82. package/src/application-control/utils.ts +4 -2
  83. package/src/cloud-connection/device-agent-cloud-connection.ts +220 -155
  84. package/src/cloud-connection/live-updates-handler.test.ts +68 -0
  85. package/src/cloud-connection/live-updates-handler.ts +30 -7
  86. package/src/cloud-connection/shadow-handler.test.ts +329 -239
  87. package/src/cloud-connection/shadow-handler.ts +38 -12
  88. package/src/cloud-connection/shadow.ts +1 -1
  89. package/src/environment.ts +2 -0
  90. package/src/infrastructure/agent-config.ts +1 -1
  91. package/src/subcommands/app/env-vars.ts +38 -8
  92. package/src/subcommands/app/index.ts +4 -1
  93. package/src/subcommands/app/models.ts +10 -1
  94. package/src/subcommands/app/shadow.ts +48 -0
  95. package/src/subcommands/app/version.ts +2 -2
  96. package/src/util/cloud-mode-ready.ts +23 -0
@@ -17,10 +17,12 @@ import {
17
17
  LiveUpdatesToggleMessage,
18
18
  SignedUrlsRequestMessage,
19
19
  ClientMessage,
20
- getDeviceTopic
20
+ getDeviceTopic,
21
+ AppInstallCloudResponseMessage
21
22
  } from '@alwaysai/device-agent-schemas';
22
23
  import { getDeviceUuid } from '../util/get-device-id';
23
24
  import { logger } from '../util/logger';
25
+ import { cloudModeReady } from '../util/cloud-mode-ready';
24
26
  import { AgentConfigFile } from '../infrastructure/agent-config';
25
27
  import {
26
28
  startApp,
@@ -29,17 +31,19 @@ import {
29
31
  updateModelsWithPresignedUrls,
30
32
  installApp,
31
33
  uninstallApp,
32
- updateAppCfg
34
+ updateAppCfg,
35
+ setEnv
33
36
  } from '../application-control';
34
- import { AppConfigUpdate, ShadowHandler, ShadowTopics } from './shadow-handler';
37
+ import { ShadowHandler, ShadowTopics, ShadowUpdate } from './shadow-handler';
35
38
  import { Publisher } from './publisher';
36
39
  import { LiveUpdatesHandler } from './live-updates-handler';
37
40
  import { bootstrapProvision } from './bootstrap-provision';
38
- import { AppConfig } from '@alwaysai/app-configuration-schemas';
39
41
  import { CmdStatusManager } from './cmd-status';
40
42
  import { PassthroughHandler, runChannel } from './passthrough-handler';
41
43
  import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
42
44
  import { getAppInstallStatusMessage } from './messages';
45
+ import { ModelsInstallCloudResponseMessage } from '@alwaysai/device-agent-schemas/lib/schemas/client/application-action-schema';
46
+ import sleep from '../util/sleep';
43
47
 
44
48
  export class DeviceAgentCloudConnection {
45
49
  private shadowHandler: ShadowHandler;
@@ -52,9 +56,11 @@ export class DeviceAgentCloudConnection {
52
56
  private host = getIoTCoreEndpointUrl();
53
57
  private readonly toDeviceTopic = getDeviceTopic(this.clientId);
54
58
  // FIXME: Add support for multiple simultaneous project updates
55
- private newAppCfgQueue: AppConfig[] = [];
59
+ private appCfgUpdateQueue: ShadowUpdate[] = [];
56
60
 
57
- private async handleAppStateControl(payload: AppStateControlPacket) {
61
+ private handleAppStateControl = async (
62
+ payload: AppStateControlPacket
63
+ ): Promise<boolean> => {
58
64
  const { baseCommand, projectId } = payload;
59
65
  switch (baseCommand) {
60
66
  case keyMirrors.appStateControl.start:
@@ -67,29 +73,27 @@ export class DeviceAgentCloudConnection {
67
73
  await restartApp({ projectId });
68
74
  break;
69
75
  }
70
- }
76
+ return true;
77
+ };
71
78
 
72
- private async handleAppVersionControl(payload: AppVersionControlPacket) {
79
+ private handleAppVersionControl = async (
80
+ payload: AppVersionControlPacket
81
+ ): Promise<boolean> => {
73
82
  switch (payload.baseCommand) {
74
83
  case keyMirrors.appVersionControl.install: {
75
84
  const { projectId, appReleaseHash } = payload;
76
- await this.cmdStatusMgr.start(projectId);
77
- await this.liveUpdatesHandler.enableAppInstallStatus({
78
- projectId,
79
- appReleaseHash
80
- });
81
85
 
82
86
  const signedUrlsRequest = { projectId, appReleaseHash };
83
87
  await this.publishCloudRequest({
84
88
  messageType: keyMirrors.agentMessageType.signed_urls_request,
85
89
  signedUrlsRequest
86
90
  });
87
- break;
91
+ return false;
88
92
  }
89
93
  case keyMirrors.appVersionControl.uninstall: {
90
94
  const { projectId } = payload;
91
95
  await this.atomicApplicationUninstall(projectId);
92
- break;
96
+ return true;
93
97
  }
94
98
  default:
95
99
  logger.warn(
@@ -99,14 +103,13 @@ export class DeviceAgentCloudConnection {
99
103
  2
100
104
  )}`
101
105
  );
106
+ return true;
102
107
  }
103
- }
104
-
105
- private handleDeviceCommand = async (packet: any) => {
106
- // TODO
107
108
  };
108
109
 
109
- private async handleAgentCommand(message: LiveUpdatesToggleMessage) {
110
+ private handleAgentCommand = async (
111
+ message: LiveUpdatesToggleMessage
112
+ ): Promise<void> => {
110
113
  switch (message.messageType) {
111
114
  case keyMirrors.clientMessageType.live_state_updates:
112
115
  await this.liveUpdatesHandler.handleToggles(message.liveUpdatesToggles);
@@ -116,7 +119,63 @@ export class DeviceAgentCloudConnection {
116
119
  `Invalid agent action message type from message '${message}'`
117
120
  );
118
121
  }
119
- }
122
+ };
123
+
124
+ private handleAppInstallCloudResponse = async (
125
+ payload: AppInstallCloudResponseMessage
126
+ ): Promise<boolean> => {
127
+ const {
128
+ projectId,
129
+ appReleaseHash,
130
+ appInstallPayload,
131
+ modelsInstallPayload
132
+ } = payload.appInstallCloudResponse;
133
+ const signedUrlsPayload = {
134
+ appInstallPayload,
135
+ modelsInstallPayload
136
+ };
137
+ await this.atomicApplicationUpdate(
138
+ installApp,
139
+ [{ projectId, appReleaseHash, signedUrlsPayload }],
140
+ projectId
141
+ );
142
+ return true;
143
+ };
144
+
145
+ private handleModelsInstallCloudResponse = async (
146
+ payload: ModelsInstallCloudResponseMessage
147
+ ): Promise<boolean> => {
148
+ const update = this.appCfgUpdateQueue.shift();
149
+ if (update === undefined) {
150
+ throw new Error(
151
+ 'Unknown error while updating models via application config! No config present for model update.'
152
+ );
153
+ }
154
+ const { appCfgUpdate, envVarUpdate } = update;
155
+ const projectId = payload.modelsInstallCloudResponse.projectId;
156
+ if (appCfgUpdate) {
157
+ await this.atomicApplicationUpdate(
158
+ updateModelsWithPresignedUrls,
159
+ [
160
+ {
161
+ projectId,
162
+ modelInstallPayloads: payload.modelsInstallCloudResponse.newModels,
163
+ newAppCfg: appCfgUpdate.newAppCfg
164
+ }
165
+ ],
166
+ projectId
167
+ );
168
+ }
169
+
170
+ if (envVarUpdate) {
171
+ await this.atomicApplicationUpdate(
172
+ setEnv,
173
+ [{ projectId, envVars: envVarUpdate.envVars }],
174
+ projectId
175
+ );
176
+ }
177
+ return true;
178
+ };
120
179
 
121
180
  private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
122
181
  this.publisher.publishToCloud(payload);
@@ -128,45 +187,12 @@ export class DeviceAgentCloudConnection {
128
187
  }
129
188
 
130
189
  private async atomicApplicationUninstall(projectId: string) {
131
- const appReleaseHash = await AgentConfigFile().getAppVersion({
132
- projectId
133
- });
134
- await this.cmdStatusMgr.start(projectId);
135
- await this.liveUpdatesHandler.enableAppInstallStatus({
136
- projectId,
137
- appReleaseHash
138
- });
139
190
  try {
140
191
  await uninstallApp({ projectId });
141
192
  this.shadowHandler.clearAppConfig(projectId);
142
-
143
- await this.cmdStatusMgr.stop(projectId);
144
- await this.liveUpdatesHandler.disableAppInstallStatus({
145
- projectId
146
- });
147
- // Send final status message
148
- this.publisher.publishToClient(
149
- await getAppInstallStatusMessage(
150
- keyMirrors.appInstallStatus.success,
151
- '',
152
- appReleaseHash
153
- )
154
- );
155
193
  } catch (e) {
156
194
  logger.error(`Failed to uninstall ${projectId}: ${e.message}`);
157
- const message: string = e.message;
158
- await this.cmdStatusMgr.stop(projectId);
159
- await this.liveUpdatesHandler.disableAppInstallStatus({
160
- projectId
161
- });
162
- // Send final status message
163
- this.publisher.publishToClient(
164
- await getAppInstallStatusMessage(
165
- keyMirrors.appInstallStatus.failure,
166
- message,
167
- appReleaseHash
168
- )
169
- );
195
+ throw e;
170
196
  }
171
197
  }
172
198
 
@@ -174,37 +200,14 @@ export class DeviceAgentCloudConnection {
174
200
  private async atomicApplicationUpdate<T extends any[], R extends any>(
175
201
  func: (...args: T) => R,
176
202
  args: T,
177
- projectId: string,
178
- appReleaseHash: string
203
+ projectId: string
179
204
  ) {
180
- await this.cmdStatusMgr.start(projectId);
181
- await this.liveUpdatesHandler.enableAppInstallStatus({
182
- projectId,
183
- appReleaseHash
184
- });
185
-
186
- // Install the app and models
187
205
  try {
188
206
  const out: R = await func(...args);
189
- await this.cmdStatusMgr.stop(projectId);
190
- await this.liveUpdatesHandler.disableAppInstallStatus({
191
- projectId
192
- });
193
- // Send final status message
194
- this.publisher.publishToClient(
195
- await getAppInstallStatusMessage(
196
- keyMirrors.appInstallStatus.success,
197
- '',
198
- appReleaseHash
199
- )
200
- );
201
-
202
- // update app config shadow for project
203
- await this.shadowHandler.publishAppConfig(projectId);
207
+ await this.shadowHandler.updateProjectShadow(projectId);
204
208
  return out;
205
209
  } catch (e) {
206
- logger.error(`Failed to install ${projectId}: ${e.message}`);
207
- const message: string = e.message;
210
+ logger.error(`Failed to update ${projectId}:\n${e.message}\n${e.stack}`);
208
211
 
209
212
  // uninstall the failed app to put system back in good state
210
213
  // TODO: Replace this with rollback
@@ -212,7 +215,37 @@ export class DeviceAgentCloudConnection {
212
215
  await uninstallApp({ projectId });
213
216
  } finally {
214
217
  this.shadowHandler.clearAppConfig(projectId);
218
+ }
219
+ throw e;
220
+ }
221
+ }
215
222
 
223
+ // eslint-disable-next-line
224
+ private async atomicCmd<T extends any[]>(props: {
225
+ func: (...args: T) => Promise<boolean>;
226
+ args: T;
227
+ projectId: string;
228
+ appReleaseHash?: string;
229
+ }) {
230
+ const { func, args, projectId } = props;
231
+ let appReleaseHash = props.appReleaseHash;
232
+ if (appReleaseHash === undefined) {
233
+ try {
234
+ appReleaseHash = await AgentConfigFile().getAppVersion({
235
+ projectId
236
+ });
237
+ } catch (e) {
238
+ appReleaseHash = '';
239
+ }
240
+ }
241
+ try {
242
+ await this.cmdStatusMgr.start(projectId);
243
+ await this.liveUpdatesHandler.enableAppInstallStatus({
244
+ projectId,
245
+ appReleaseHash
246
+ });
247
+ const completed = await func(...args);
248
+ if (completed) {
216
249
  await this.cmdStatusMgr.stop(projectId);
217
250
  await this.liveUpdatesHandler.disableAppInstallStatus({
218
251
  projectId
@@ -220,30 +253,48 @@ export class DeviceAgentCloudConnection {
220
253
  // Send final status message
221
254
  this.publisher.publishToClient(
222
255
  await getAppInstallStatusMessage(
223
- keyMirrors.appInstallStatus.failure,
224
- message,
256
+ keyMirrors.appInstallStatus.success,
257
+ '',
225
258
  appReleaseHash
226
259
  )
227
260
  );
228
261
  }
229
- }
230
- }
262
+ return completed;
263
+ } catch (e) {
264
+ logger.error(
265
+ `Failed to execute cmd for ${projectId}:\n${e.message}\n${e.stack}`
266
+ );
267
+ const message: string = e.message;
231
268
 
232
- private async handleAppConfigUpdates(appCfgUpdates: AppConfigUpdate[]) {
233
- for (const appConfigUpdate of appCfgUpdates) {
234
- const { projectId, newAppCfg, updatedModels } = appConfigUpdate;
235
- const appReleaseHash = await AgentConfigFile().getAppVersion({
269
+ // uninstall the failed app to put system back in good state
270
+ await this.cmdStatusMgr.stop(projectId);
271
+ await this.liveUpdatesHandler.disableAppInstallStatus({
236
272
  projectId
237
273
  });
238
- await this.cmdStatusMgr.start(projectId);
239
- await this.liveUpdatesHandler.enableAppInstallStatus({
240
- projectId,
241
- appReleaseHash
242
- });
274
+ // Send final status message
275
+ this.publisher.publishToClient(
276
+ await getAppInstallStatusMessage(
277
+ keyMirrors.appInstallStatus.failure,
278
+ message,
279
+ appReleaseHash
280
+ )
281
+ );
282
+ }
283
+ }
243
284
 
244
- if (updatedModels && Object.keys(updatedModels).length) {
245
- // Publish request for model urls
246
- this.newAppCfgQueue.push(newAppCfg);
285
+ private handleAppConfigUpdates = async (
286
+ updates: ShadowUpdate[]
287
+ ): Promise<boolean> => {
288
+ for (const update of updates) {
289
+ const { projectId, appCfgUpdate, envVarUpdate } = update;
290
+
291
+ if (
292
+ appCfgUpdate &&
293
+ appCfgUpdate.updatedModels &&
294
+ Object.keys(appCfgUpdate.updatedModels).length
295
+ ) {
296
+ // When there are model updates request signed URLs and wait to apply config changes
297
+ const { updatedModels } = appCfgUpdate;
247
298
 
248
299
  logger.debug(
249
300
  `Requesting presigned urls from cloud for model versions: ${JSON.stringify(
@@ -257,28 +308,34 @@ export class DeviceAgentCloudConnection {
257
308
  models: updatedModels
258
309
  }
259
310
  });
260
- } else {
261
- // FIXME: do we need to send this up to the cloud?
262
- // should it be something other than appReleaseHash?
263
- const appReleaseHash = await AgentConfigFile().getAppVersion({
264
- projectId
265
- });
266
311
 
312
+ this.appCfgUpdateQueue.push(update);
313
+ return false;
314
+ }
315
+
316
+ if (appCfgUpdate) {
267
317
  await this.atomicApplicationUpdate(
268
318
  updateAppCfg,
269
319
  [
270
320
  {
271
321
  projectId,
272
- appReleaseHash,
273
- newAppCfg
322
+ newAppCfg: appCfgUpdate.newAppCfg
274
323
  }
275
324
  ],
276
- projectId,
277
- appReleaseHash
325
+ projectId
326
+ );
327
+ }
328
+
329
+ if (envVarUpdate) {
330
+ await this.atomicApplicationUpdate(
331
+ setEnv,
332
+ [{ projectId, envVars: envVarUpdate.envVars }],
333
+ projectId
278
334
  );
279
335
  }
280
336
  }
281
- }
337
+ return true;
338
+ };
282
339
 
283
340
  /*=================================================================
284
341
  Public interface
@@ -323,6 +380,10 @@ export class DeviceAgentCloudConnection {
323
380
  return this.cmdStatusMgr.isCmdInProgress(projectId);
324
381
  }
325
382
 
383
+ public async updateProjectShadow(projectId: string) {
384
+ await this.shadowHandler.updateProjectShadow(projectId);
385
+ }
386
+
326
387
  public async handleClientMessage({
327
388
  topic,
328
389
  message
@@ -339,70 +400,58 @@ export class DeviceAgentCloudConnection {
339
400
  2
340
401
  )}`
341
402
  );
403
+ // TODO: Send generic error response
342
404
  return;
343
405
  }
344
406
  const payload = message.payload;
345
407
  switch (payload.messageType) {
346
408
  case keyMirrors.clientMessageType.app_state_control: {
347
- await this.handleAppStateControl(payload.appStateControl);
409
+ const projectId = payload.appStateControl.projectId;
410
+ await this.atomicCmd({
411
+ func: this.handleAppStateControl,
412
+ args: [payload.appStateControl],
413
+ projectId
414
+ });
348
415
  break;
349
416
  }
350
417
  case keyMirrors.clientMessageType.app_version_control: {
351
- await this.handleAppVersionControl(payload.appVersionControl);
418
+ const projectId = payload.appVersionControl.projectId;
419
+ const appReleaseHash =
420
+ payload.appVersionControl.baseCommand ===
421
+ keyMirrors.appVersionControl.install
422
+ ? payload.appVersionControl.appReleaseHash
423
+ : undefined;
424
+ await this.atomicCmd({
425
+ func: this.handleAppVersionControl,
426
+ args: [payload.appVersionControl],
427
+ projectId,
428
+ appReleaseHash
429
+ });
352
430
  break;
353
431
  }
354
432
  case keyMirrors.clientMessageType.live_state_updates: {
433
+ // TODO: Send response?
355
434
  await this.handleAgentCommand(payload);
356
435
  break;
357
436
  }
358
437
  case keyMirrors.clientMessageType.app_install_cloud_response: {
359
- const {
360
- projectId,
361
- appReleaseHash,
362
- appInstallPayload,
363
- modelsInstallPayload
364
- } = payload.appInstallCloudResponse;
365
- const signedUrlsPayload = {
366
- appInstallPayload,
367
- modelsInstallPayload
368
- };
369
- await this.atomicApplicationUpdate(
370
- installApp,
371
- [{ projectId, appReleaseHash, signedUrlsPayload }],
372
- projectId,
373
- appReleaseHash
374
- );
438
+ const projectId = payload.appInstallCloudResponse.projectId;
439
+ await this.atomicCmd({
440
+ func: this.handleAppInstallCloudResponse,
441
+ args: [payload],
442
+ projectId
443
+ });
375
444
 
376
445
  break;
377
446
  }
378
447
  case keyMirrors.clientMessageType.models_install_cloud_response: {
379
- const { projectId, newModels } = payload.modelsInstallCloudResponse;
380
- const appReleaseHash = await AgentConfigFile().getAppVersion({
448
+ const { projectId } = payload.modelsInstallCloudResponse;
449
+ await this.atomicCmd({
450
+ func: this.handleModelsInstallCloudResponse,
451
+ args: [payload],
381
452
  projectId
382
453
  });
383
454
 
384
- const newAppCfg = this.newAppCfgQueue.shift();
385
- if (newAppCfg === undefined) {
386
- logger.error(
387
- 'Unknown error while updating models via application config! No config present for model update.'
388
- );
389
- return;
390
- }
391
-
392
- await this.atomicApplicationUpdate(
393
- updateModelsWithPresignedUrls,
394
- [
395
- {
396
- projectId,
397
- modelInstallPayloads: newModels,
398
- newAppCfg,
399
- appReleaseHash
400
- }
401
- ],
402
- projectId,
403
- appReleaseHash
404
- );
405
-
406
455
  break;
407
456
  }
408
457
  default:
@@ -423,12 +472,25 @@ export class DeviceAgentCloudConnection {
423
472
  switch (topic) {
424
473
  case this.shadowHandler.shadowTopics.projects.getAccepted:
425
474
  case this.shadowHandler.shadowTopics.projects.updateDelta: {
426
- const appConfigUpdates = await this.shadowHandler.handleShadowTopic({
475
+ const shadowUpdates = await this.shadowHandler.handleShadowTopic({
427
476
  topic,
428
477
  payload: message.state,
429
478
  clientToken: message.clientToken
430
479
  });
431
- await this.handleAppConfigUpdates(appConfigUpdates);
480
+ if (shadowUpdates.length) {
481
+ // FIXME: Take project ID of first shadow update. Most likely there will only be one update
482
+ // so this should be sufficient for now.
483
+ const projectId = shadowUpdates[0].projectId;
484
+ try {
485
+ await this.atomicCmd({
486
+ func: this.handleAppConfigUpdates,
487
+ args: [shadowUpdates],
488
+ projectId
489
+ });
490
+ } catch (e) {
491
+ logger.error(`Error handling shadow message: ${e.message}`);
492
+ }
493
+ }
432
494
  break;
433
495
  }
434
496
  case this.shadowHandler.shadowTopics.projects.getRejected:
@@ -483,14 +545,17 @@ export class DeviceAgentCloudConnection {
483
545
  });
484
546
  }
485
547
 
486
- public stop() {
548
+ public async stop() {
549
+ // FIXME: This method is currently only used by the CLI, and shadow messages
550
+ // can be lost since we aren't waiting for responses so sleep for a short
551
+ // time to receive them
552
+ await sleep(1000);
487
553
  this.device.end();
488
554
  }
489
555
  }
490
556
 
491
557
  export async function runDeviceAgentCloudInterface() {
492
- // FIXME: Check for KeyPath as well
493
- if (existsSync(DEVICE_CERTIFICATE_FILE_PATH)) {
558
+ if (cloudModeReady()) {
494
559
  const deviceAgent = new DeviceAgentCloudConnection();
495
560
  await deviceAgent.setupHandlers();
496
561
  if (ALWAYSAI_ANALYTICS_PASSTHROUGH === true) {
@@ -0,0 +1,68 @@
1
+ import { LiveUpdatesHandler } from './live-updates-handler';
2
+ import { Publisher } from './publisher';
3
+
4
+ const testTrueToggles = {
5
+ deviceStats: true,
6
+ appState: true
7
+ };
8
+
9
+ const testFalseToggles = {
10
+ deviceStats: false,
11
+ appState: false
12
+ };
13
+
14
+ const mockClient = jest.fn();
15
+ const clientId = 'test-client';
16
+
17
+ jest.spyOn(global, 'setTimeout');
18
+
19
+ // NOTE: this was the way I found to mock private class functions
20
+ const mockStartPublishingLiveUpdates = jest.spyOn(
21
+ LiveUpdatesHandler.prototype as any,
22
+ 'startPublishingLiveUpdates'
23
+ );
24
+ mockStartPublishingLiveUpdates.mockResolvedValue(null);
25
+
26
+ describe('Test Live Updates Handler', () => {
27
+ let liveUpdatesHandler: LiveUpdatesHandler;
28
+ let publisher: Publisher;
29
+
30
+ beforeEach(() => {
31
+ publisher = new Publisher(mockClient, clientId);
32
+ liveUpdatesHandler = new LiveUpdatesHandler(publisher);
33
+ jest.clearAllMocks();
34
+ });
35
+
36
+ test('ignore subsequent enables', async () => {
37
+ void liveUpdatesHandler.handleToggles(testTrueToggles);
38
+ // called twice, once for device stats, once for app state
39
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
40
+ // restartLiveUpdatesTimeout is always called once when handleToggles is called
41
+ expect(jest.mocked(setTimeout)).toBeCalledTimes(1);
42
+
43
+ // Second call -> should not call startPublishingLiveUpdates should not be called
44
+ jest.clearAllMocks();
45
+ void liveUpdatesHandler.handleToggles(testTrueToggles);
46
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
47
+ expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
48
+ });
49
+
50
+ test('test disable live updates', async () => {
51
+ // Test calling handleToggles one time, enabling it
52
+ void liveUpdatesHandler.handleToggles(testTrueToggles);
53
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
54
+ expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
55
+ expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
56
+ expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(true);
57
+ expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
58
+
59
+ // Check to see that attributes are properly set to false when disabled
60
+ jest.clearAllMocks();
61
+ void liveUpdatesHandler.handleToggles(testFalseToggles);
62
+ expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
63
+ expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
64
+ expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);
65
+ expect(liveUpdatesHandler.getAppStateLiveUpdates()).toBe(false);
66
+ expect(liveUpdatesHandler.getAppLogsLiveUpdates()).toBe(false);
67
+ });
68
+ });