@alwaysai/device-agent 0.2.0 → 1.0.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 (84) hide show
  1. package/LICENSE +12 -0
  2. package/lib/application-control/config.d.ts.map +1 -1
  3. package/lib/application-control/config.js +6 -1
  4. package/lib/application-control/config.js.map +1 -1
  5. package/lib/application-control/index.d.ts +2 -2
  6. package/lib/application-control/index.d.ts.map +1 -1
  7. package/lib/application-control/index.js +2 -2
  8. package/lib/application-control/index.js.map +1 -1
  9. package/lib/application-control/install.d.ts +2 -2
  10. package/lib/application-control/install.d.ts.map +1 -1
  11. package/lib/application-control/install.js +10 -0
  12. package/lib/application-control/install.js.map +1 -1
  13. package/lib/application-control/status.d.ts +3 -3
  14. package/lib/application-control/status.d.ts.map +1 -1
  15. package/lib/application-control/status.js +4 -4
  16. package/lib/application-control/status.js.map +1 -1
  17. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +7 -7
  18. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  19. package/lib/cloud-connection/device-agent-cloud-connection.js +158 -100
  20. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  21. package/lib/cloud-connection/live-updates-handler.d.ts +9 -9
  22. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  23. package/lib/cloud-connection/live-updates-handler.js +45 -42
  24. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  25. package/lib/cloud-connection/live-updates-handler.test.js +6 -5
  26. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  27. package/lib/cloud-connection/message-builder.d.ts +7 -0
  28. package/lib/cloud-connection/message-builder.d.ts.map +1 -0
  29. package/lib/cloud-connection/message-builder.js +63 -0
  30. package/lib/cloud-connection/message-builder.js.map +1 -0
  31. package/lib/cloud-connection/messages.d.ts +5 -15
  32. package/lib/cloud-connection/messages.d.ts.map +1 -1
  33. package/lib/cloud-connection/messages.js +22 -31
  34. package/lib/cloud-connection/messages.js.map +1 -1
  35. package/lib/cloud-connection/publisher.d.ts +4 -5
  36. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  37. package/lib/cloud-connection/publisher.js +12 -21
  38. package/lib/cloud-connection/publisher.js.map +1 -1
  39. package/lib/cloud-connection/transaction-queue.d.ts +12 -0
  40. package/lib/cloud-connection/transaction-queue.d.ts.map +1 -0
  41. package/lib/cloud-connection/transaction-queue.js +38 -0
  42. package/lib/cloud-connection/transaction-queue.js.map +1 -0
  43. package/lib/cloud-connection/transaction-queue.test.d.ts +2 -0
  44. package/lib/cloud-connection/transaction-queue.test.d.ts.map +1 -0
  45. package/lib/cloud-connection/transaction-queue.test.js +46 -0
  46. package/lib/cloud-connection/transaction-queue.test.js.map +1 -0
  47. package/lib/local-connection/rabbitmq-connection.d.ts.map +1 -1
  48. package/lib/local-connection/rabbitmq-connection.js +5 -1
  49. package/lib/local-connection/rabbitmq-connection.js.map +1 -1
  50. package/lib/subcommands/app/index.d.ts.map +1 -1
  51. package/lib/subcommands/app/index.js +1 -0
  52. package/lib/subcommands/app/index.js.map +1 -1
  53. package/lib/subcommands/app/models.d.ts +5 -0
  54. package/lib/subcommands/app/models.d.ts.map +1 -1
  55. package/lib/subcommands/app/models.js +42 -1
  56. package/lib/subcommands/app/models.js.map +1 -1
  57. package/lib/subcommands/app/status.js +1 -1
  58. package/lib/subcommands/app/status.js.map +1 -1
  59. package/lib/subcommands/app/version.d.ts.map +1 -1
  60. package/lib/subcommands/app/version.js +9 -11
  61. package/lib/subcommands/app/version.js.map +1 -1
  62. package/lib/util/logger.d.ts.map +1 -1
  63. package/lib/util/logger.js +3 -1
  64. package/lib/util/logger.js.map +1 -1
  65. package/package.json +5 -4
  66. package/readme.md +30 -1
  67. package/src/application-control/config.ts +5 -1
  68. package/src/application-control/index.ts +2 -2
  69. package/src/application-control/install.ts +17 -4
  70. package/src/application-control/status.ts +9 -8
  71. package/src/cloud-connection/device-agent-cloud-connection.ts +225 -132
  72. package/src/cloud-connection/live-updates-handler.test.ts +6 -5
  73. package/src/cloud-connection/live-updates-handler.ts +90 -64
  74. package/src/cloud-connection/message-builder.ts +117 -0
  75. package/src/cloud-connection/messages.ts +27 -35
  76. package/src/cloud-connection/publisher.ts +17 -30
  77. package/src/cloud-connection/transaction-queue.test.ts +55 -0
  78. package/src/cloud-connection/transaction-queue.ts +40 -0
  79. package/src/local-connection/rabbitmq-connection.ts +5 -1
  80. package/src/subcommands/app/index.ts +3 -1
  81. package/src/subcommands/app/models.ts +44 -0
  82. package/src/subcommands/app/status.ts +2 -2
  83. package/src/subcommands/app/version.ts +16 -14
  84. package/src/util/logger.ts +5 -1
@@ -11,14 +11,17 @@ import {
11
11
  } from '../util/directories';
12
12
  import {
13
13
  keyMirrors,
14
- validateClientMessage,
15
- AppStateControlPacket,
16
- AppVersionControlPacket,
17
- LiveUpdatesToggleMessage,
18
- SignedUrlsRequestMessage,
19
- ClientMessage,
20
- getDeviceTopic,
21
- AppInstallCloudResponseMessage
14
+ validateToClientMessage,
15
+ SignedUrlsRequestPayload,
16
+ getToDeviceTopic,
17
+ AppInstallResponsePayload,
18
+ validateToDeviceAgentMessage,
19
+ ToDeviceAgentMessage,
20
+ ToCloudMessage,
21
+ AppStateControlPayload,
22
+ AppVersionControlInstallPayload,
23
+ AppVersionControlUninstallPayload,
24
+ ToClientMessage
22
25
  } from '@alwaysai/device-agent-schemas';
23
26
  import { getDeviceUuid } from '../util/get-device-id';
24
27
  import { logger } from '../util/logger';
@@ -42,26 +45,34 @@ import { bootstrapProvision } from './bootstrap-provision';
42
45
  import { CmdStatusManager } from './cmd-status';
43
46
  import { PassthroughHandler, runChannel } from './passthrough-handler';
44
47
  import { ALWAYSAI_ANALYTICS_PASSTHROUGH } from '../environment';
45
- import { getAppInstallStatusMessage } from './messages';
46
- import { ModelsInstallCloudResponseMessage } from '@alwaysai/device-agent-schemas/lib/schemas/client/application-action-schema';
48
+ import { getStatusResponsePayload } from './messages';
49
+ import { ModelsInstallResponsePayload } from '@alwaysai/device-agent-schemas';
47
50
  import sleep from '../util/sleep';
51
+ import { createAppBackup, rollbackApp } from '../application-control/backup';
52
+ import { TransactionQueue } from './transaction-queue';
53
+ import {
54
+ buildSignedUrlsRequestMessage,
55
+ buildStatusResponseMessage
56
+ } from './message-builder';
57
+ import { generateTxId } from '@alwaysai/device-agent-schemas';
48
58
 
49
59
  export class DeviceAgentCloudConnection {
50
60
  private shadowHandler: ShadowHandler;
51
61
  public publisher: Publisher;
52
62
  private cmdStatusMgr: CmdStatusManager;
53
63
  private liveUpdatesHandler: LiveUpdatesHandler;
64
+ private txnQueue: TransactionQueue;
54
65
  private device = awsIot.device;
55
66
 
56
67
  private clientId = getDeviceUuid();
57
68
  private host = getIoTCoreEndpointUrl();
58
- private readonly toDeviceTopic = getDeviceTopic(this.clientId);
69
+ private readonly toDeviceTopic = getToDeviceTopic(this.clientId);
59
70
  private readonly secureTunnelNotifyTopic = `$aws/things/${this.clientId}/tunnels/notify`;
60
71
  // FIXME: Add support for multiple simultaneous project updates
61
72
  private appCfgUpdateQueue: ShadowUpdate[] = [];
62
73
 
63
74
  private handleAppStateControl = async (
64
- payload: AppStateControlPacket
75
+ payload: AppStateControlPayload
65
76
  ): Promise<boolean> => {
66
77
  const { baseCommand, projectId } = payload;
67
78
  switch (baseCommand) {
@@ -79,17 +90,26 @@ export class DeviceAgentCloudConnection {
79
90
  };
80
91
 
81
92
  private handleAppVersionControl = async (
82
- payload: AppVersionControlPacket
93
+ payload:
94
+ | AppVersionControlInstallPayload
95
+ | AppVersionControlUninstallPayload,
96
+ txId: string
83
97
  ): Promise<boolean> => {
84
98
  switch (payload.baseCommand) {
85
99
  case keyMirrors.appVersionControl.install: {
86
100
  const { projectId, appReleaseHash } = payload;
87
101
 
88
- const signedUrlsRequest = { projectId, appReleaseHash };
89
- await this.publishCloudRequest({
90
- messageType: keyMirrors.agentMessageType.signed_urls_request,
91
- signedUrlsRequest
92
- });
102
+ const signedUrlsRequestPayload: SignedUrlsRequestPayload = {
103
+ signedUrlsRequest: {
104
+ projectId,
105
+ appReleaseHash
106
+ }
107
+ };
108
+ const message = await buildSignedUrlsRequestMessage(
109
+ signedUrlsRequestPayload,
110
+ txId
111
+ );
112
+ await this.publishCloudRequest(message);
93
113
  return false;
94
114
  }
95
115
  case keyMirrors.appVersionControl.uninstall: {
@@ -109,29 +129,15 @@ export class DeviceAgentCloudConnection {
109
129
  }
110
130
  };
111
131
 
112
- private handleAgentCommand = async (
113
- message: LiveUpdatesToggleMessage
114
- ): Promise<void> => {
115
- switch (message.messageType) {
116
- case keyMirrors.clientMessageType.live_state_updates:
117
- await this.liveUpdatesHandler.handleToggles(message.liveUpdatesToggles);
118
- break;
119
- default:
120
- logger.error(
121
- `Invalid agent action message type from message '${message}'`
122
- );
123
- }
124
- };
125
-
126
- private handleAppInstallCloudResponse = async (
127
- payload: AppInstallCloudResponseMessage
132
+ private handleAppInstallCloudResponsePayload = async (
133
+ payload: AppInstallResponsePayload
128
134
  ): Promise<boolean> => {
129
135
  const {
130
136
  projectId,
131
137
  appReleaseHash,
132
138
  appInstallPayload,
133
139
  modelsInstallPayload
134
- } = payload.appInstallCloudResponse;
140
+ } = payload.appInstallResponse;
135
141
  const signedUrlsPayload = {
136
142
  appInstallPayload,
137
143
  modelsInstallPayload
@@ -144,8 +150,8 @@ export class DeviceAgentCloudConnection {
144
150
  return true;
145
151
  };
146
152
 
147
- private handleModelsInstallCloudResponse = async (
148
- payload: ModelsInstallCloudResponseMessage
153
+ private handleModelsInstallCloudResponsePayload = async (
154
+ payload: ModelsInstallResponsePayload
149
155
  ): Promise<boolean> => {
150
156
  const update = this.appCfgUpdateQueue.shift();
151
157
  if (update === undefined) {
@@ -154,14 +160,14 @@ export class DeviceAgentCloudConnection {
154
160
  );
155
161
  }
156
162
  const { appCfgUpdate, envVarUpdate } = update;
157
- const projectId = payload.modelsInstallCloudResponse.projectId;
163
+ const projectId = payload.modelsInstallResponse.projectId;
158
164
  if (appCfgUpdate) {
159
165
  await this.atomicApplicationUpdate(
160
166
  updateModelsWithPresignedUrls,
161
167
  [
162
168
  {
163
169
  projectId,
164
- modelInstallPayloads: payload.modelsInstallCloudResponse.newModels,
170
+ modelInstallPayloads: payload.modelsInstallResponse.newModels,
165
171
  newAppCfg: appCfgUpdate.newAppCfg
166
172
  }
167
173
  ],
@@ -179,8 +185,8 @@ export class DeviceAgentCloudConnection {
179
185
  return true;
180
186
  };
181
187
 
182
- private async publishCloudRequest(payload: SignedUrlsRequestMessage) {
183
- this.publisher.publishToCloud(payload);
188
+ private async publishCloudRequest(message: ToCloudMessage) {
189
+ this.publisher.publishToCloud(message);
184
190
  }
185
191
 
186
192
  private subscribe(topic: string) {
@@ -204,21 +210,48 @@ export class DeviceAgentCloudConnection {
204
210
  args: T,
205
211
  projectId: string
206
212
  ) {
213
+ // First try to create a backup, so that there is one available if something goes wrong in the next try:catch.
214
+ if (await AgentConfigFile().isAppPresent({ projectId })) {
215
+ try {
216
+ await createAppBackup({ projectId });
217
+ } catch (e) {
218
+ logger.error(
219
+ `Could not create a backup for the project: ${projectId}:\n${e.message}\n${e.stack}`
220
+ );
221
+ }
222
+ }
223
+
207
224
  try {
208
225
  const out: R = await func(...args);
209
226
  await this.shadowHandler.updateProjectShadow(projectId);
210
227
  return out;
211
- } catch (e) {
212
- logger.error(`Failed to update ${projectId}:\n${e.message}\n${e.stack}`);
213
-
214
- // uninstall the failed app to put system back in good state
215
- // TODO: Replace this with rollback
228
+ } catch (errorAppUpdate) {
229
+ logger.error(
230
+ `Failed to update ${projectId}:\n${JSON.stringify(errorAppUpdate)}}`
231
+ );
232
+ // If something goes wrong, first try to rollback
216
233
  try {
217
- await uninstallApp({ projectId });
218
- } finally {
219
- this.shadowHandler.clearAppConfig(projectId);
234
+ await rollbackApp({ projectId });
235
+ logger.error(
236
+ `Application update failed, rolled back to previous version: ${errorAppUpdate}`
237
+ );
238
+ } catch (errorRollbackApp) {
239
+ // and if that fails, uninstall the app as a last resort.
240
+ try {
241
+ await uninstallApp({ projectId });
242
+ } finally {
243
+ this.shadowHandler.clearAppConfig(projectId);
244
+ }
245
+ logger.error(
246
+ `Application update failed, rolled back to previous version: ${errorAppUpdate}`
247
+ );
248
+ throw new Error(
249
+ `Application update and rollback failed, uninstalled the application: ${errorAppUpdate}`
250
+ );
220
251
  }
221
- throw e;
252
+ throw new Error(
253
+ `Application update failed, rolled the application back: ${errorAppUpdate}`
254
+ );
222
255
  }
223
256
  }
224
257
 
@@ -227,40 +260,30 @@ export class DeviceAgentCloudConnection {
227
260
  func: (...args: T) => Promise<boolean>;
228
261
  args: T;
229
262
  projectId: string;
230
- appReleaseHash?: string;
231
- }) {
232
- const { func, args, projectId } = props;
233
- let appReleaseHash = props.appReleaseHash;
234
- if (appReleaseHash === undefined) {
235
- try {
236
- appReleaseHash = await AgentConfigFile().getAppVersion({
237
- projectId
238
- });
239
- } catch (e) {
240
- logger.warn(`Unable to get app release hash for ${projectId}!`);
241
- appReleaseHash = '';
242
- }
243
- }
263
+ txId: string;
264
+ }): Promise<boolean> {
265
+ const { func, args, projectId, txId } = props;
244
266
  try {
245
267
  await this.cmdStatusMgr.start(projectId);
246
- await this.liveUpdatesHandler.enableAppInstallStatus({
247
- projectId,
248
- appReleaseHash
268
+ await this.liveUpdatesHandler.enableTransactionStatus({
269
+ txId
249
270
  });
250
271
  const completed = await func(...args);
251
272
  if (completed) {
252
273
  await this.cmdStatusMgr.stop(projectId);
253
- await this.liveUpdatesHandler.disableAppInstallStatus({
254
- projectId
274
+ await this.liveUpdatesHandler.disableTransactionStatus({
275
+ txId
255
276
  });
277
+ const sucessStatusResponsePayload = await getStatusResponsePayload(
278
+ keyMirrors.statusResponse.success,
279
+ ''
280
+ );
256
281
  // Send final status message
257
- this.publisher.publishToClient(
258
- await getAppInstallStatusMessage(
259
- keyMirrors.appInstallStatus.success,
260
- '',
261
- appReleaseHash
262
- )
282
+ const message = await buildStatusResponseMessage(
283
+ sucessStatusResponsePayload,
284
+ txId
263
285
  );
286
+ this.publisher.publishToClient(message);
264
287
  }
265
288
  return completed;
266
289
  } catch (e) {
@@ -271,22 +294,26 @@ export class DeviceAgentCloudConnection {
271
294
 
272
295
  // uninstall the failed app to put system back in good state
273
296
  await this.cmdStatusMgr.stop(projectId);
274
- await this.liveUpdatesHandler.disableAppInstallStatus({
275
- projectId
297
+ await this.liveUpdatesHandler.disableTransactionStatus({
298
+ txId
276
299
  });
300
+ const failureStatusResponsePayload = await getStatusResponsePayload(
301
+ keyMirrors.statusResponse.failure,
302
+ message
303
+ );
277
304
  // Send final status message
278
- this.publisher.publishToClient(
279
- await getAppInstallStatusMessage(
280
- keyMirrors.appInstallStatus.failure,
281
- message,
282
- appReleaseHash
283
- )
305
+ const failureStatusResponseMessage = await buildStatusResponseMessage(
306
+ failureStatusResponsePayload,
307
+ txId
284
308
  );
309
+ this.publisher.publishToClient(failureStatusResponseMessage);
310
+ return true;
285
311
  }
286
312
  }
287
313
 
288
314
  private handleAppConfigUpdates = async (
289
- updates: ShadowUpdate[]
315
+ updates: ShadowUpdate[],
316
+ txId: string
290
317
  ): Promise<boolean> => {
291
318
  for (const update of updates) {
292
319
  const { projectId, appCfgUpdate, envVarUpdate } = update;
@@ -304,13 +331,17 @@ export class DeviceAgentCloudConnection {
304
331
  updatedModels
305
332
  )}`
306
333
  );
307
- this.publisher.publishToCloud({
308
- messageType: keyMirrors.agentMessageType.signed_urls_request,
334
+ const modelsOnlyUrlsRequestPayload: SignedUrlsRequestPayload = {
309
335
  modelsOnlyUrlsRequest: {
310
336
  projectId,
311
337
  models: updatedModels
312
338
  }
313
- });
339
+ };
340
+ const message = await buildSignedUrlsRequestMessage(
341
+ modelsOnlyUrlsRequestPayload,
342
+ txId
343
+ );
344
+ this.publisher.publishToCloud(message);
314
345
 
315
346
  this.appCfgUpdateQueue.push(update);
316
347
  return false;
@@ -357,7 +388,11 @@ export class DeviceAgentCloudConnection {
357
388
  this.publisher = new Publisher(this.device, this.clientId);
358
389
  this.shadowHandler = new ShadowHandler(this.clientId, this.publisher);
359
390
  this.cmdStatusMgr = new CmdStatusManager();
360
- this.liveUpdatesHandler = new LiveUpdatesHandler(this.publisher);
391
+ this.liveUpdatesHandler = new LiveUpdatesHandler(
392
+ this.publisher,
393
+ this.clientId
394
+ );
395
+ this.txnQueue = new TransactionQueue();
361
396
 
362
397
  this.subscribe(this.toDeviceTopic);
363
398
  this.subscribe(this.secureTunnelNotifyTopic);
@@ -388,18 +423,18 @@ export class DeviceAgentCloudConnection {
388
423
  await this.shadowHandler.updateProjectShadow(projectId);
389
424
  }
390
425
 
391
- public async handleClientMessage({
426
+ public async handleDeviceAgentMessage({
392
427
  topic,
393
428
  message
394
429
  }: {
395
430
  topic: string;
396
- message: ClientMessage;
431
+ message: ToDeviceAgentMessage;
397
432
  }) {
398
- const valid = validateClientMessage(message);
433
+ const valid = validateToDeviceAgentMessage(message);
399
434
  if (!valid) {
400
435
  logger.error(
401
436
  `Error validating message: ${JSON.stringify(
402
- { topic, message, errors: validateClientMessage.errors },
437
+ { topic, message, errors: validateToDeviceAgentMessage.errors },
403
438
  null,
404
439
  2
405
440
  )}`
@@ -407,64 +442,109 @@ export class DeviceAgentCloudConnection {
407
442
  // TODO: Send generic error response
408
443
  return;
409
444
  }
410
- const payload = message.payload;
411
- switch (payload.messageType) {
412
- case keyMirrors.clientMessageType.app_state_control: {
413
- const projectId = payload.appStateControl.projectId;
414
- await this.atomicCmd({
415
- func: this.handleAppStateControl,
416
- args: [payload.appStateControl],
417
- projectId
418
- });
445
+ const txId = message.txId;
446
+ switch (message.messageType) {
447
+ case keyMirrors.toDeviceAgentMessageType.app_state_control: {
448
+ // txId sent from cloud, just need to continue it
449
+ const payload = message.payload;
450
+ const projectId = payload.projectId;
451
+
452
+ try {
453
+ this.txnQueue.addTxIdToQueue(txId, projectId);
454
+ const completed = await this.atomicCmd({
455
+ func: this.handleAppStateControl,
456
+ args: [message.payload],
457
+ projectId,
458
+ txId
459
+ });
460
+ if (completed) {
461
+ this.txnQueue.completeTxn(txId);
462
+ }
463
+ } catch (e) {
464
+ logger.error(
465
+ `Error processing application state control request: ${e}!`
466
+ );
467
+ }
468
+
419
469
  break;
420
470
  }
421
- case keyMirrors.clientMessageType.app_version_control: {
422
- const projectId = payload.appVersionControl.projectId;
471
+ case keyMirrors.toDeviceAgentMessageType.app_version_control: {
472
+ // txId sent from cloud, just need to continue it
473
+ const payload = message.payload;
474
+ const projectId = payload.projectId;
423
475
  const appReleaseHash =
424
- payload.appVersionControl.baseCommand ===
425
- keyMirrors.appVersionControl.install
426
- ? payload.appVersionControl.appReleaseHash
476
+ payload.baseCommand === keyMirrors.appVersionControl.install
477
+ ? payload.appReleaseHash
427
478
  : undefined;
428
- await this.atomicCmd({
429
- func: this.handleAppVersionControl,
430
- args: [payload.appVersionControl],
431
- projectId,
432
- appReleaseHash
433
- });
479
+ try {
480
+ this.txnQueue.addTxIdToQueue(txId, projectId);
481
+ const completed = await this.atomicCmd({
482
+ func: this.handleAppVersionControl,
483
+ args: [payload, txId],
484
+ projectId,
485
+ txId
486
+ });
487
+ if (completed) {
488
+ this.txnQueue.completeTxn(txId);
489
+ }
490
+ } catch (e) {
491
+ logger.error(`Error processing application install request: ${e}!`);
492
+ }
493
+
434
494
  break;
435
495
  }
436
- case keyMirrors.clientMessageType.live_state_updates: {
496
+ case keyMirrors.toDeviceAgentMessageType.live_state_updates: {
497
+ const payload = message.payload;
437
498
  // TODO: Send response?
438
- await this.handleAgentCommand(payload);
499
+ await this.liveUpdatesHandler.handleToggles(payload, txId);
439
500
  break;
440
501
  }
441
- case keyMirrors.clientMessageType.app_install_cloud_response: {
442
- const { projectId, appReleaseHash } = payload.appInstallCloudResponse;
443
- await this.atomicCmd({
444
- func: this.handleAppInstallCloudResponse,
502
+ case keyMirrors.toDeviceAgentMessageType.app_install_response: {
503
+ const payload = message.payload;
504
+ const { projectId, appReleaseHash } = payload.appInstallResponse;
505
+ if (txId !== this.txnQueue.getCurrentTxId()) {
506
+ throw new Error(
507
+ `App install response received a message for a transaction ID ${txId} that is not currently underway (${this.txnQueue.getCurrentTxId()})!`
508
+ );
509
+ }
510
+ const completed = await this.atomicCmd({
511
+ func: this.handleAppInstallCloudResponsePayload,
445
512
  args: [payload],
446
513
  projectId,
447
- appReleaseHash
514
+ txId
448
515
  });
516
+ if (completed) {
517
+ this.txnQueue.completeTxn(txId);
518
+ }
449
519
 
450
520
  break;
451
521
  }
452
- case keyMirrors.clientMessageType.models_install_cloud_response: {
522
+ case keyMirrors.toDeviceAgentMessageType.models_install_response: {
453
523
  // This message doesn't have appReleaseHash in it's payload, but
454
524
  // atomicCmd should be able to read it from the installed app
455
- const { projectId } = payload.modelsInstallCloudResponse;
456
- await this.atomicCmd({
457
- func: this.handleModelsInstallCloudResponse,
525
+ const payload = message.payload;
526
+ const { projectId } = payload.modelsInstallResponse;
527
+ if (txId !== this.txnQueue.getCurrentTxId()) {
528
+ throw new Error(
529
+ `Model install response received a message for a transaction ID ${txId} that is not currently underway (${this.txnQueue.getCurrentTxId()})!`
530
+ );
531
+ }
532
+ const completed = await this.atomicCmd({
533
+ func: this.handleModelsInstallCloudResponsePayload,
458
534
  args: [payload],
459
- projectId
535
+ projectId,
536
+ txId
460
537
  });
538
+ if (completed) {
539
+ this.txnQueue.completeTxn(txId);
540
+ }
461
541
 
462
542
  break;
463
543
  }
464
544
  default:
465
545
  logger.error(
466
546
  `Invalid client message: '${JSON.stringify(
467
- { topic, message },
547
+ { topic, message, txId },
468
548
  null,
469
549
  2
470
550
  )}'`
@@ -472,10 +552,14 @@ export class DeviceAgentCloudConnection {
472
552
  }
473
553
  }
474
554
 
475
- public async handleMessage(topic: string, message: ClientMessage | any) {
555
+ public async handleMessage(
556
+ topic: string,
557
+ message: ToDeviceAgentMessage | any
558
+ ) {
476
559
  logger.debug(
477
560
  `Received message: ${JSON.stringify({ topic, message }, null, 2)}`
478
561
  );
562
+ const txId = message.txId || generateTxId(); // txId should be passed in for DeviceAgentMessage messages but shadow updates won't have this
479
563
  switch (topic) {
480
564
  case this.shadowHandler.shadowTopics.projects.getAccepted:
481
565
  case this.shadowHandler.shadowTopics.projects.updateDelta: {
@@ -489,11 +573,20 @@ export class DeviceAgentCloudConnection {
489
573
  // so this should be sufficient for now.
490
574
  const projectId = shadowUpdates[0].projectId;
491
575
  try {
492
- await this.atomicCmd({
493
- func: this.handleAppConfigUpdates,
494
- args: [shadowUpdates],
495
- projectId
496
- });
576
+ try {
577
+ this.txnQueue.addTxIdToQueue(txId, projectId);
578
+ const completed = await this.atomicCmd({
579
+ func: this.handleAppConfigUpdates,
580
+ args: [shadowUpdates, txId],
581
+ projectId,
582
+ txId
583
+ });
584
+ if (completed) {
585
+ this.txnQueue.completeTxn(txId);
586
+ }
587
+ } catch (e) {
588
+ logger.error(`Error processing model update request: ${e}!`);
589
+ }
497
590
  } catch (e) {
498
591
  logger.error(`Error handling shadow message: ${e.message}`);
499
592
  }
@@ -506,7 +599,7 @@ export class DeviceAgentCloudConnection {
506
599
  // Not handling these for now
507
600
  break;
508
601
  case this.toDeviceTopic:
509
- await this.handleClientMessage({
602
+ await this.handleDeviceAgentMessage({
510
603
  topic,
511
604
  message
512
605
  });
@@ -13,6 +13,7 @@ const testFalseToggles = {
13
13
 
14
14
  const mockClient = jest.fn();
15
15
  const clientId = 'test-client';
16
+ const emptyTxId = '';
16
17
 
17
18
  jest.spyOn(global, 'setTimeout');
18
19
 
@@ -29,12 +30,12 @@ describe('Test Live Updates Handler', () => {
29
30
 
30
31
  beforeEach(() => {
31
32
  publisher = new Publisher(mockClient, clientId);
32
- liveUpdatesHandler = new LiveUpdatesHandler(publisher);
33
+ liveUpdatesHandler = new LiveUpdatesHandler(publisher, clientId);
33
34
  jest.clearAllMocks();
34
35
  });
35
36
 
36
37
  test('ignore subsequent enables', async () => {
37
- void liveUpdatesHandler.handleToggles(testTrueToggles);
38
+ void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
38
39
  // called twice, once for device stats, once for app state
39
40
  expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
40
41
  // restartLiveUpdatesTimeout is always called once when handleToggles is called
@@ -42,14 +43,14 @@ describe('Test Live Updates Handler', () => {
42
43
 
43
44
  // Second call -> should not call startPublishingLiveUpdates should not be called
44
45
  jest.clearAllMocks();
45
- void liveUpdatesHandler.handleToggles(testTrueToggles);
46
+ void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
46
47
  expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
47
48
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
48
49
  });
49
50
 
50
51
  test('test disable live updates', async () => {
51
52
  // Test calling handleToggles one time, enabling it
52
- void liveUpdatesHandler.handleToggles(testTrueToggles);
53
+ void liveUpdatesHandler.handleToggles(testTrueToggles, emptyTxId);
53
54
  expect(mockStartPublishingLiveUpdates).toBeCalledTimes(2);
54
55
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
55
56
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(true);
@@ -58,7 +59,7 @@ describe('Test Live Updates Handler', () => {
58
59
 
59
60
  // Check to see that attributes are properly set to false when disabled
60
61
  jest.clearAllMocks();
61
- void liveUpdatesHandler.handleToggles(testFalseToggles);
62
+ void liveUpdatesHandler.handleToggles(testFalseToggles, emptyTxId);
62
63
  expect(mockStartPublishingLiveUpdates).toBeCalledTimes(0);
63
64
  expect(jest.mocked(setTimeout)).toHaveBeenCalledTimes(1);
64
65
  expect(liveUpdatesHandler.getDeviceStatsLiveUpdates()).toBe(false);