@alwaysai/device-agent 0.0.13 → 0.0.15

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 (151) hide show
  1. package/lib/application-control/backup.js +3 -3
  2. package/lib/application-control/backup.js.map +1 -1
  3. package/lib/application-control/index.d.ts +4 -4
  4. package/lib/application-control/index.d.ts.map +1 -1
  5. package/lib/application-control/index.js +1 -4
  6. package/lib/application-control/index.js.map +1 -1
  7. package/lib/application-control/install.d.ts +1 -1
  8. package/lib/application-control/install.d.ts.map +1 -1
  9. package/lib/application-control/install.js +41 -54
  10. package/lib/application-control/install.js.map +1 -1
  11. package/lib/application-control/models.d.ts +0 -4
  12. package/lib/application-control/models.d.ts.map +1 -1
  13. package/lib/application-control/models.js +13 -22
  14. package/lib/application-control/models.js.map +1 -1
  15. package/lib/application-control/status.d.ts +0 -6
  16. package/lib/application-control/status.d.ts.map +1 -1
  17. package/lib/application-control/status.js +3 -19
  18. package/lib/application-control/status.js.map +1 -1
  19. package/lib/application-control/utils.d.ts +3 -0
  20. package/lib/application-control/utils.d.ts.map +1 -1
  21. package/lib/application-control/utils.js +50 -21
  22. package/lib/application-control/utils.js.map +1 -1
  23. package/lib/cloud-connection/bootstrap-provision.d.ts +1 -1
  24. package/lib/cloud-connection/bootstrap-provision.d.ts.map +1 -1
  25. package/lib/cloud-connection/bootstrap-provision.js +9 -9
  26. package/lib/cloud-connection/bootstrap-provision.js.map +1 -1
  27. package/lib/cloud-connection/cmd-status.d.ts +8 -0
  28. package/lib/cloud-connection/cmd-status.d.ts.map +1 -0
  29. package/lib/cloud-connection/cmd-status.js +62 -0
  30. package/lib/cloud-connection/cmd-status.js.map +1 -0
  31. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +10 -2
  32. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  33. package/lib/cloud-connection/device-agent-cloud-connection.js +156 -66
  34. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  35. package/lib/cloud-connection/device-agent.d.ts.map +1 -1
  36. package/lib/cloud-connection/device-agent.js +4 -3
  37. package/lib/cloud-connection/device-agent.js.map +1 -1
  38. package/lib/cloud-connection/live-updates-handler.d.ts +10 -18
  39. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  40. package/lib/cloud-connection/live-updates-handler.js +50 -50
  41. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  42. package/lib/cloud-connection/messages.d.ts +3 -1
  43. package/lib/cloud-connection/messages.d.ts.map +1 -1
  44. package/lib/cloud-connection/messages.js +13 -1
  45. package/lib/cloud-connection/messages.js.map +1 -1
  46. package/lib/cloud-connection/passthrough-handler.d.ts +11 -0
  47. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -0
  48. package/lib/cloud-connection/passthrough-handler.js +59 -0
  49. package/lib/cloud-connection/passthrough-handler.js.map +1 -0
  50. package/lib/cloud-connection/publisher.d.ts +1 -0
  51. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  52. package/lib/cloud-connection/publisher.js +14 -0
  53. package/lib/cloud-connection/publisher.js.map +1 -1
  54. package/lib/cloud-connection/shadow-handler.d.ts +2 -3
  55. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  56. package/lib/cloud-connection/shadow-handler.js +18 -4
  57. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  58. package/lib/cloud-connection/shadow-handler.test.d.ts +2 -0
  59. package/lib/cloud-connection/shadow-handler.test.d.ts.map +1 -0
  60. package/lib/cloud-connection/shadow-handler.test.js +321 -0
  61. package/lib/cloud-connection/shadow-handler.test.js.map +1 -0
  62. package/lib/environment.d.ts +1 -0
  63. package/lib/environment.d.ts.map +1 -1
  64. package/lib/environment.js +3 -2
  65. package/lib/environment.js.map +1 -1
  66. package/lib/index.js +2 -2
  67. package/lib/index.js.map +1 -1
  68. package/lib/infrastructure/agent-config.d.ts +15 -48
  69. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  70. package/lib/infrastructure/agent-config.js.map +1 -1
  71. package/lib/infrastructure/agent-config.test.js +0 -6
  72. package/lib/infrastructure/agent-config.test.js.map +1 -1
  73. package/lib/infrastructure/system-id.js +2 -2
  74. package/lib/infrastructure/system-id.js.map +1 -1
  75. package/lib/infrastructure/tokens-and-device-cfg.d.ts.map +1 -1
  76. package/lib/infrastructure/tokens-and-device-cfg.js +5 -9
  77. package/lib/infrastructure/tokens-and-device-cfg.js.map +1 -1
  78. package/lib/local-connection/rabbitmq-connection.d.ts +4 -0
  79. package/lib/local-connection/rabbitmq-connection.d.ts.map +1 -0
  80. package/lib/local-connection/rabbitmq-connection.js +58 -0
  81. package/lib/local-connection/rabbitmq-connection.js.map +1 -0
  82. package/lib/subcommands/app/app.d.ts +4 -3
  83. package/lib/subcommands/app/app.d.ts.map +1 -1
  84. package/lib/subcommands/app/app.js +78 -27
  85. package/lib/subcommands/app/app.js.map +1 -1
  86. package/lib/subcommands/app/index.js +2 -2
  87. package/lib/subcommands/device/clean.js +4 -4
  88. package/lib/subcommands/device/clean.js.map +1 -1
  89. package/lib/subcommands/device/device.d.ts +1 -1
  90. package/lib/subcommands/device/device.d.ts.map +1 -1
  91. package/lib/subcommands/device/device.js +9 -10
  92. package/lib/subcommands/device/device.js.map +1 -1
  93. package/lib/subcommands/index.d.ts +0 -1
  94. package/lib/subcommands/index.d.ts.map +1 -1
  95. package/lib/subcommands/login.d.ts +0 -1
  96. package/lib/subcommands/login.d.ts.map +1 -1
  97. package/lib/subcommands/login.js +1 -9
  98. package/lib/subcommands/login.js.map +1 -1
  99. package/lib/util/directories.d.ts +11 -12
  100. package/lib/util/directories.d.ts.map +1 -1
  101. package/lib/util/directories.js +24 -29
  102. package/lib/util/directories.js.map +1 -1
  103. package/lib/util/fetch-with-timeout.d.ts +4 -0
  104. package/lib/util/fetch-with-timeout.d.ts.map +1 -0
  105. package/lib/util/fetch-with-timeout.js +15 -0
  106. package/lib/util/fetch-with-timeout.js.map +1 -0
  107. package/lib/util/logger.js +1 -0
  108. package/lib/util/logger.js.map +1 -1
  109. package/lib/util/require-logged-in-and-paid-plan.d.ts +2 -0
  110. package/lib/util/require-logged-in-and-paid-plan.d.ts.map +1 -0
  111. package/lib/util/require-logged-in-and-paid-plan.js +18 -0
  112. package/lib/util/require-logged-in-and-paid-plan.js.map +1 -0
  113. package/package.json +20 -32
  114. package/readme.md +100 -89
  115. package/src/application-control/backup.ts +3 -3
  116. package/src/application-control/index.ts +0 -6
  117. package/src/application-control/install.ts +53 -73
  118. package/src/application-control/models.ts +7 -19
  119. package/src/application-control/status.ts +3 -19
  120. package/src/application-control/utils.ts +61 -22
  121. package/src/cloud-connection/bootstrap-provision.ts +13 -10
  122. package/src/cloud-connection/cmd-status.ts +71 -0
  123. package/src/cloud-connection/device-agent-cloud-connection.ts +211 -102
  124. package/src/cloud-connection/device-agent.ts +7 -4
  125. package/src/cloud-connection/live-updates-handler.ts +79 -86
  126. package/src/cloud-connection/messages.ts +22 -1
  127. package/src/cloud-connection/passthrough-handler.ts +67 -0
  128. package/src/cloud-connection/publisher.ts +21 -0
  129. package/src/cloud-connection/shadow-handler.test.ts +361 -0
  130. package/src/cloud-connection/shadow-handler.ts +28 -7
  131. package/src/environment.ts +4 -1
  132. package/src/index.ts +2 -2
  133. package/src/infrastructure/agent-config.test.ts +0 -7
  134. package/src/infrastructure/agent-config.ts +24 -2
  135. package/src/infrastructure/system-id.ts +1 -1
  136. package/src/infrastructure/tokens-and-device-cfg.ts +8 -13
  137. package/src/local-connection/rabbitmq-connection.ts +53 -0
  138. package/src/subcommands/app/app.ts +82 -31
  139. package/src/subcommands/app/index.ts +4 -4
  140. package/src/subcommands/device/clean.ts +4 -4
  141. package/src/subcommands/device/device.ts +13 -13
  142. package/src/subcommands/login.ts +1 -9
  143. package/src/util/directories.ts +31 -29
  144. package/src/util/fetch-with-timeout.ts +18 -0
  145. package/src/util/logger.ts +2 -0
  146. package/src/util/require-logged-in-and-paid-plan.ts +16 -0
  147. package/lib/cloud-connection/app-install-status.d.ts +0 -16
  148. package/lib/cloud-connection/app-install-status.d.ts.map +0 -1
  149. package/lib/cloud-connection/app-install-status.js +0 -53
  150. package/lib/cloud-connection/app-install-status.js.map +0 -1
  151. package/src/cloud-connection/app-install-status.ts +0 -62
@@ -8,13 +8,15 @@ import { getAppLogs } from '../application-control';
8
8
  import { logger } from '../util/logger';
9
9
  import sleep from '../util/sleep';
10
10
  import { Publisher } from './publisher';
11
- import { getAppStateMessage, getDeviceStatsMessage } from './messages';
11
+ import {
12
+ getAppInstallStatusMessage,
13
+ getAppStateMessage,
14
+ getDeviceStatsMessage
15
+ } from './messages';
12
16
  import { AgentMessageTypeValue } from '@alwaysai/device-agent-schemas/lib/constants';
13
- import { AppInstallStatusManager } from './app-install-status';
14
17
 
15
18
  export class LiveUpdatesHandler {
16
19
  private publisher: Publisher;
17
- private appInstallStatusMgr: AppInstallStatusManager;
18
20
 
19
21
  private liveUpdatesTimeout: ReturnType<typeof setTimeout>;
20
22
  private liveUpdatesAlive = {
@@ -32,14 +34,6 @@ export class LiveUpdatesHandler {
32
34
  private appLogStreams = new Set<string>();
33
35
  private appInstallStatuses = new Set<string>();
34
36
 
35
- constructor(
36
- publisher: Publisher,
37
- appInstallStatusMgr: AppInstallStatusManager
38
- ) {
39
- this.publisher = publisher;
40
- this.appInstallStatusMgr = appInstallStatusMgr;
41
- }
42
-
43
37
  private async startAppLogStream(projectId: string) {
44
38
  logger.info(`Starting log stream for ${projectId}`);
45
39
  this.appLogStreams.add(projectId);
@@ -77,50 +71,19 @@ export class LiveUpdatesHandler {
77
71
  });
78
72
  }
79
73
 
80
- // Use arrow function to allow `this` to be accessed
81
- private getAppInstallStatusMessage = async (appReleaseHash: string) => {
82
- const appInstallStatus =
83
- this.appInstallStatusMgr.getStatusPacket(appReleaseHash);
84
- const appInstallStatusMessage = {
85
- messageType: keyMirrors.agentMessageType.app_install_status,
86
- appInstallStatus
87
- };
88
- return appInstallStatusMessage;
89
- };
90
-
91
- public async startPublishingLiveUpdates<T extends any[]>(
92
- messageType: AgentMessageTypeValue,
93
- getMessageData: (...args: T) => Promise<DeviceAgentMessagePayload>,
94
- args: T
95
- ) {
96
- logger.info(`Turned on live updates for ${messageType}`);
97
- // eslint-disable-next-line no-constant-condition
98
- while (true) {
99
- try {
100
- const message = await getMessageData(...args);
101
- this.publisher.publishToClient(message);
102
- } catch (e) {
103
- logger.error(
104
- `Error publishing live updates for ${messageType}: ${e.message}`
105
- );
106
- break;
107
- }
108
- if (!this.continuePublishing(messageType, ...args)) {
109
- logger.info(`Turned off live updates for ${messageType}`);
110
- break;
111
- }
112
- await sleep(this.getLiveUpdatesInterval(messageType));
113
- }
114
- }
115
-
116
- private continuePublishing(flag: AgentMessageTypeValue, data?: any): boolean {
74
+ private continuePublishing(
75
+ flag: AgentMessageTypeValue,
76
+ projectId?: string
77
+ ): boolean {
117
78
  switch (flag) {
118
79
  case keyMirrors.agentMessageType.device_stats:
119
80
  case keyMirrors.agentMessageType.app_state:
120
81
  return this.liveUpdatesAlive[flag];
121
82
  case keyMirrors.agentMessageType.app_install_status: {
122
- const appReleaseHash: string = data;
123
- return this.appInstallStatuses.has(appReleaseHash);
83
+ if (!projectId) {
84
+ throw new Error(`Project ID not provided to continuePublishing!`);
85
+ }
86
+ return this.appInstallStatuses.has(projectId);
124
87
  }
125
88
  default:
126
89
  logger.error(`Unrecognized publishable flag ${flag}`);
@@ -158,28 +121,73 @@ export class LiveUpdatesHandler {
158
121
  }, 600000); // 10 min
159
122
  }
160
123
 
161
- public async update({
162
- deviceStats,
163
- appState,
164
- appLogs,
165
- appInstallStatus
166
- }: {
167
- deviceStats?: boolean;
168
- appState?: boolean;
169
- appLogs?: {
170
- projectId: string;
171
- toggle: boolean;
172
- };
173
- appInstallStatus?: {
174
- appReleaseHash: string;
175
- toggle: boolean;
176
- };
124
+ private async startPublishingLiveUpdates<T extends any[]>(
125
+ messageType: AgentMessageTypeValue,
126
+ getMessageData: (...args: T) => Promise<DeviceAgentMessagePayload>,
127
+ args: T,
128
+ projectId?: string
129
+ ) {
130
+ logger.info(`Turned on live updates for ${messageType}`);
131
+ // eslint-disable-next-line no-constant-condition
132
+ while (true) {
133
+ try {
134
+ const message = await getMessageData(...args);
135
+ this.publisher.publishToClient(message);
136
+ } catch (e) {
137
+ logger.error(
138
+ `Error publishing live updates for ${messageType}: ${e.message}`
139
+ );
140
+ break;
141
+ }
142
+ if (!this.continuePublishing(messageType, projectId)) {
143
+ logger.info(`Turned off live updates for ${messageType}`);
144
+ break;
145
+ }
146
+ await sleep(this.getLiveUpdatesInterval(messageType));
147
+ }
148
+ }
149
+
150
+ /*=================================================================
151
+ Public interface
152
+ =================================================================*/
153
+
154
+ constructor(publisher: Publisher) {
155
+ this.publisher = publisher;
156
+ }
157
+
158
+ public async enableAppInstallStatus(props: {
159
+ projectId: string;
160
+ appReleaseHash: string;
177
161
  }) {
162
+ const { projectId, appReleaseHash } = props;
163
+ this.liveUpdatesAlive.app_install_status = true;
164
+ this.appInstallStatuses.add(projectId);
165
+ // Don't wait for this call to finish since it loops until disabled
166
+ void this.startPublishingLiveUpdates(
167
+ keyMirrors.agentMessageType.app_install_status,
168
+ getAppInstallStatusMessage,
169
+ [keyMirrors.appInstallStatus.in_progress, '', appReleaseHash],
170
+ projectId
171
+ );
172
+ }
173
+
174
+ public async disableAppInstallStatus(props: { projectId: string }) {
175
+ const { projectId } = props;
176
+ this.appInstallStatuses.delete(projectId);
177
+
178
+ if (this.appInstallStatuses.size === 0) {
179
+ this.liveUpdatesAlive.app_install_status = false;
180
+ }
181
+ }
182
+
183
+ public async handleToggles(toggles: LiveUpdatesToggles) {
184
+ const { deviceStats, appState, appLogs } = toggles;
178
185
  this.restartLiveUpdatesTimeout();
179
186
  if (deviceStats !== undefined) {
180
187
  this.liveUpdatesAlive.device_stats = deviceStats;
181
188
  if (deviceStats) {
182
- this.startPublishingLiveUpdates(
189
+ // Don't wait for this call to finish since it loops until disabled
190
+ void this.startPublishingLiveUpdates(
183
191
  keyMirrors.agentMessageType.device_stats,
184
192
  getDeviceStatsMessage,
185
193
  []
@@ -190,7 +198,8 @@ export class LiveUpdatesHandler {
190
198
  if (appState !== undefined) {
191
199
  this.liveUpdatesAlive.app_state = appState;
192
200
  if (appState) {
193
- this.startPublishingLiveUpdates(
201
+ // Don't wait for this call to finish since it loops until disabled
202
+ void this.startPublishingLiveUpdates(
194
203
  keyMirrors.agentMessageType.app_state,
195
204
  getAppStateMessage,
196
205
  []
@@ -200,27 +209,11 @@ export class LiveUpdatesHandler {
200
209
 
201
210
  if (appLogs !== undefined) {
202
211
  if (appLogs.toggle) {
203
- this.startAppLogStream(appLogs.projectId);
212
+ // Don't wait for this call to finish since it loops until disabled
213
+ void this.startAppLogStream(appLogs.projectId);
204
214
  } else {
205
215
  this.appLogStreams.delete(appLogs.projectId);
206
216
  }
207
217
  }
208
-
209
- if (appInstallStatus !== undefined) {
210
- if (appInstallStatus.toggle) {
211
- this.liveUpdatesAlive.app_install_status = appInstallStatus.toggle;
212
- this.appInstallStatuses.add(appInstallStatus.appReleaseHash);
213
- this.startPublishingLiveUpdates(
214
- keyMirrors.agentMessageType.app_install_status,
215
- this.getAppInstallStatusMessage,
216
- [appInstallStatus.appReleaseHash]
217
- );
218
- } else {
219
- this.appInstallStatuses.delete(appInstallStatus.appReleaseHash);
220
- }
221
- if (this.appInstallStatuses.size === 0) {
222
- this.liveUpdatesAlive.app_install_status = false;
223
- }
224
- }
225
218
  }
226
219
  }
@@ -1,4 +1,9 @@
1
- import { AppStatePacket, keyMirrors } from '@alwaysai/device-agent-schemas';
1
+ import {
2
+ AppInstallStatusMessage,
3
+ AppStatePacket,
4
+ keyMirrors
5
+ } from '@alwaysai/device-agent-schemas';
6
+ import { AppInstallStatusValue } from '@alwaysai/device-agent-schemas/lib/constants';
2
7
  import { getAppStatus } from '../application-control';
3
8
  import {
4
9
  getCpuUtil,
@@ -22,6 +27,22 @@ export async function getAppStateMessage() {
22
27
  return appStatePackage;
23
28
  }
24
29
 
30
+ export async function getAppInstallStatusMessage(
31
+ status: AppInstallStatusValue,
32
+ message: string,
33
+ appReleaseHash: string
34
+ ): Promise<AppInstallStatusMessage> {
35
+ const appInstallStatusMessage: AppInstallStatusMessage = {
36
+ messageType: keyMirrors.agentMessageType.app_install_status,
37
+ appInstallStatus: {
38
+ status,
39
+ message,
40
+ appReleaseHash
41
+ }
42
+ };
43
+ return appInstallStatusMessage;
44
+ }
45
+
25
46
  export async function getDeviceStatsMessage() {
26
47
  const cpuUsage = await getCpuUtil();
27
48
  const diskUtil = await getDiskUtil();
@@ -0,0 +1,67 @@
1
+ // eslint-disable-next-line
2
+ const amqp = require('amqplib');
3
+ import { setupRabbitMQContainer } from '../local-connection/rabbitmq-connection';
4
+ import { logger } from '../util/logger';
5
+ import { Publisher } from './publisher';
6
+
7
+ const messageQueue: any[] = [];
8
+ const ackQueue: any[] = [];
9
+
10
+ export class PassthroughHandler {
11
+ public publisher: Publisher;
12
+ public connection;
13
+ public channel;
14
+ public packetQueue;
15
+
16
+ constructor(publisher: Publisher) {
17
+ this.publisher = publisher;
18
+ }
19
+
20
+ public async setup() {
21
+ await setupRabbitMQContainer();
22
+ this.connection = await amqp.connect('amqp://localhost');
23
+ this.channel = await this.connection.createChannel();
24
+ this.channel.prefetch(1); // This ensures we only get one packet at a time! This appears to have prevented throttling
25
+ this.packetQueue = 'edgeiq-analytics-publish';
26
+ await this.channel.assertQueue(this.packetQueue, {
27
+ durable: true
28
+ });
29
+ }
30
+ }
31
+
32
+ function processPublish(passthroughHandler: PassthroughHandler) {
33
+ while (messageQueue.length > 0) {
34
+ const entry = messageQueue.shift();
35
+ const { packet, msg } = entry;
36
+ ackQueue.push(msg);
37
+ // FIXME: put real topic here
38
+ passthroughHandler.publisher.publishToCloudWithAck(packet, (errOrResp) => {
39
+ logger.debug('packet published to cloud?', errOrResp);
40
+ while (ackQueue.length > 0) {
41
+ const msg = ackQueue.shift();
42
+ if (errOrResp === true) {
43
+ passthroughHandler.channel.ack(msg); // acknowledge, allow queue to discard
44
+ } else if (errOrResp === false) {
45
+ passthroughHandler.channel.reject(msg, true); // reject and requeue
46
+ }
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ export async function runChannel(passthroughHandler: PassthroughHandler) {
53
+ logger.debug('Beginning to consume packets');
54
+ passthroughHandler.channel.consume(
55
+ passthroughHandler.packetQueue,
56
+ function (msg) {
57
+ if (msg.content !== undefined) {
58
+ const packet = JSON.parse(msg.content.toString());
59
+ messageQueue.push({ packet, msg });
60
+ processPublish(passthroughHandler);
61
+ }
62
+ },
63
+ {
64
+ noAck: false // When true, RabbitMQ deletes message as soon as it is consumed
65
+ }
66
+ );
67
+ }
@@ -43,6 +43,27 @@ export class Publisher {
43
43
  });
44
44
  }
45
45
 
46
+ public publishToCloudWithAck(
47
+ payload: string,
48
+ ackNackCallback: CallableFunction
49
+ ) {
50
+ const topic = this.toCloudTopic;
51
+ logger.debug('payload received to publishWithAck', payload);
52
+ this.client.publish(topic, payload, { qos: 1 }, (err: any, resp: any) => {
53
+ if (err) {
54
+ logger.error(
55
+ `Error publishing message: \nTopic: ${topic}\nMessage: ${payload}\nError: ${err}`
56
+ );
57
+ ackNackCallback(false);
58
+ } else if (resp) {
59
+ logger.debug(
60
+ `Successfully published message: \nTopic: ${topic}\nMessage: ${payload}`
61
+ );
62
+ ackNackCallback(true);
63
+ }
64
+ });
65
+ }
66
+
46
67
  public publishDeviceAgentPayload(
47
68
  topic: string,
48
69
  payload: DeviceAgentMessagePayload
@@ -0,0 +1,361 @@
1
+ import { AppConfig } from '@alwaysai/app-configuration-schemas';
2
+ import { readAppCfgFile } from '../application-control';
3
+ import { Publisher } from './publisher';
4
+ import { ShadowHandler } from './shadow-handler';
5
+
6
+ jest.mock('../application-control');
7
+ jest.mock('./publisher');
8
+ const mockClient = jest.fn();
9
+ const clientId = 'test-client';
10
+ const projectId1 = 'test-project';
11
+ const projectId2 = 'test-project-2';
12
+
13
+ describe('Test Shadow Handler', () => {
14
+ let publisher: Publisher;
15
+ let shadowHandler: ShadowHandler;
16
+
17
+ beforeEach(() => {
18
+ publisher = new Publisher(mockClient, clientId);
19
+ shadowHandler = new ShadowHandler(clientId, publisher);
20
+ });
21
+
22
+ test.skip('reject buffer payload', async () => {
23
+ //FIXME: Invalid input is silently ignored, need input validation
24
+ expect(async () => {
25
+ await shadowHandler.handleShadowTopic({
26
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
27
+ payload: Buffer.from('test-payload')
28
+ });
29
+ }).toThrow(Error);
30
+ });
31
+
32
+ test('ignore message from self', async () => {
33
+ const ogAppCfg1: AppConfig = {
34
+ scripts: {
35
+ start: 'python app.py'
36
+ },
37
+ models: {
38
+ 'alwaysai/mobilenet_ssd': 2
39
+ }
40
+ };
41
+ jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
42
+
43
+ const appCfg1: AppConfig = {
44
+ scripts: {
45
+ start: 'python app.py'
46
+ },
47
+ models: {
48
+ 'alwaysai/yolo_v3': 4
49
+ }
50
+ };
51
+ const payload = {
52
+ [projectId1]: {
53
+ appConfig: JSON.stringify(appCfg1)
54
+ },
55
+ clientToken: clientId
56
+ };
57
+
58
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
59
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
60
+ payload
61
+ });
62
+ expect(appCfgUpdates.length).toBe(0);
63
+ });
64
+
65
+ test('handle shadow get response with updated models', async () => {
66
+ const ogAppCfg1: AppConfig = {
67
+ scripts: {
68
+ start: 'python app.py'
69
+ },
70
+ models: {
71
+ 'alwaysai/mobilenet_ssd': 3
72
+ }
73
+ };
74
+
75
+ jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
76
+ const appCfg1: AppConfig = {
77
+ scripts: {
78
+ start: 'python app.py'
79
+ },
80
+ models: {
81
+ 'alwaysai/yolo_v3': 4
82
+ }
83
+ };
84
+
85
+ const payload = {
86
+ delta: {
87
+ [projectId1]: {
88
+ appConfig: JSON.stringify(appCfg1)
89
+ }
90
+ }
91
+ };
92
+
93
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
94
+ topic: shadowHandler.shadowTopics.projects.getAccepted,
95
+ payload
96
+ });
97
+ expect(appCfgUpdates.length).toBe(1);
98
+ expect(appCfgUpdates[0]).toEqual({
99
+ projectId: projectId1,
100
+ newAppCfg: appCfg1,
101
+ updatedModels: {
102
+ 'alwaysai/yolo_v3': 4
103
+ }
104
+ });
105
+ });
106
+
107
+ test('handle shadow delta without app config', async () => {
108
+ const ogAppCfg1: AppConfig = {
109
+ scripts: {
110
+ start: 'python app.py'
111
+ },
112
+ models: {
113
+ 'alwaysai/mobilenet_ssd': 2
114
+ }
115
+ };
116
+ jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
117
+
118
+ const payload = {
119
+ [projectId1]: {}
120
+ };
121
+
122
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
123
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
124
+ payload
125
+ });
126
+ expect(appCfgUpdates.length).toBe(0);
127
+ });
128
+
129
+ test('handle shadow delta with updated models', async () => {
130
+ const ogAppCfg1: AppConfig = {
131
+ scripts: {
132
+ start: 'python app.py'
133
+ },
134
+ models: {
135
+ 'alwaysai/mobilenet_ssd': 2
136
+ }
137
+ };
138
+
139
+ jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
140
+ const appCfg1: AppConfig = {
141
+ scripts: {
142
+ start: 'python app.py'
143
+ },
144
+ models: {
145
+ 'alwaysai/mobilenet_ssd': 3,
146
+ 'alwaysai/yolo_v4': 5
147
+ }
148
+ };
149
+
150
+ const payload = {
151
+ [projectId1]: {
152
+ appConfig: JSON.stringify(appCfg1)
153
+ }
154
+ };
155
+
156
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
157
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
158
+ payload
159
+ });
160
+ expect(appCfgUpdates.length).toBe(1);
161
+ expect(appCfgUpdates[0]).toEqual({
162
+ projectId: projectId1,
163
+ newAppCfg: appCfg1,
164
+ updatedModels: {
165
+ 'alwaysai/mobilenet_ssd': 3,
166
+ 'alwaysai/yolo_v4': 5
167
+ }
168
+ });
169
+ });
170
+
171
+ test('handle shadow delta with updated models for two apps', async () => {
172
+ const ogAppCfg1: AppConfig = {
173
+ scripts: {
174
+ start: 'python app.py'
175
+ },
176
+ models: {
177
+ 'alwaysai/mobilenet_ssd': 2
178
+ }
179
+ };
180
+ jest.mocked(readAppCfgFile).mockResolvedValueOnce(ogAppCfg1);
181
+ const ogAppCfg2: AppConfig = {
182
+ scripts: {
183
+ start: 'python app.py'
184
+ },
185
+ models: {
186
+ 'alwaysai/yolo_v4': 5
187
+ }
188
+ };
189
+ jest.mocked(readAppCfgFile).mockResolvedValueOnce(ogAppCfg2);
190
+
191
+ const appCfg1: AppConfig = {
192
+ scripts: {
193
+ start: 'python app.py'
194
+ },
195
+ models: {
196
+ 'alwaysai/mobilenet_ssd': 3,
197
+ 'alwaysai/yolo_v4': 5
198
+ }
199
+ };
200
+ const appCfg2: AppConfig = {
201
+ scripts: {
202
+ start: 'python app.py'
203
+ },
204
+ models: {
205
+ 'alwaysai/yolo_v4': 5,
206
+ 'alwaysai/human_pose': 7
207
+ }
208
+ };
209
+ const payload = {
210
+ [projectId1]: {
211
+ appConfig: JSON.stringify(appCfg1)
212
+ },
213
+ [projectId2]: {
214
+ appConfig: JSON.stringify(appCfg2)
215
+ }
216
+ };
217
+
218
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
219
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
220
+ payload
221
+ });
222
+ expect(appCfgUpdates.length).toBe(2);
223
+ expect(appCfgUpdates[0]).toEqual({
224
+ projectId: projectId1,
225
+ newAppCfg: appCfg1,
226
+ updatedModels: {
227
+ 'alwaysai/mobilenet_ssd': 3,
228
+ 'alwaysai/yolo_v4': 5
229
+ }
230
+ });
231
+ expect(appCfgUpdates[1]).toEqual({
232
+ projectId: projectId2,
233
+ newAppCfg: appCfg2,
234
+ updatedModels: {
235
+ 'alwaysai/human_pose': 7
236
+ }
237
+ });
238
+ });
239
+
240
+ test('handle shadow delta with updated analytics', async () => {
241
+ const ogAppCfg1: AppConfig = {
242
+ scripts: {
243
+ start: 'python app.py'
244
+ },
245
+ models: {}
246
+ };
247
+ jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
248
+
249
+ const appCfg1: AppConfig = {
250
+ scripts: {
251
+ start: 'python app.py'
252
+ },
253
+ models: {},
254
+ analytics: {
255
+ enable_cloud_publish: true
256
+ }
257
+ };
258
+
259
+ const payload = {
260
+ [projectId1]: {
261
+ appConfig: JSON.stringify(appCfg1)
262
+ }
263
+ };
264
+
265
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
266
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
267
+ payload
268
+ });
269
+ expect(appCfgUpdates.length).toBe(1);
270
+ expect(appCfgUpdates[0]).toEqual({
271
+ projectId: projectId1,
272
+ newAppCfg: appCfg1
273
+ });
274
+ });
275
+
276
+ test('handle shadow delta with invalid app config', async () => {
277
+ const ogAppCfg1: AppConfig = {
278
+ scripts: {
279
+ start: 'python app.py'
280
+ },
281
+ models: {
282
+ 'alwaysai/mobilenet_ssd': 2
283
+ }
284
+ };
285
+
286
+ jest.mocked(readAppCfgFile).mockResolvedValue(ogAppCfg1);
287
+ const appCfg1 = {
288
+ scripts: {
289
+ start: 'python app.py'
290
+ },
291
+ models: {
292
+ 'alwaysai/mobilenet_ssd': '3',
293
+ 'alwaysai/yolo_v4': '5'
294
+ }
295
+ };
296
+
297
+ const payload = {
298
+ [projectId1]: {
299
+ appConfig: JSON.stringify(appCfg1)
300
+ }
301
+ };
302
+
303
+ const appCfgUpdates = await shadowHandler.handleShadowTopic({
304
+ topic: shadowHandler.shadowTopics.projects.updateDelta,
305
+ payload
306
+ });
307
+ expect(appCfgUpdates.length).toBe(0);
308
+ });
309
+
310
+ test.skip('publish app state', async () => {
311
+ // FIXME: For some reason publisher is not being called...
312
+ const testAppCfg: AppConfig = {
313
+ scripts: {
314
+ start: ''
315
+ },
316
+ models: {}
317
+ };
318
+ jest.mocked(readAppCfgFile).mockResolvedValue(testAppCfg);
319
+
320
+ await shadowHandler.publishAppState(projectId1);
321
+ expect(jest.mocked(readAppCfgFile)).toBeCalledWith({ projectId1 });
322
+ const packet = {
323
+ state: {
324
+ reported: {
325
+ [projectId1]: { appConfig: JSON.stringify(testAppCfg) }
326
+ }
327
+ },
328
+ clientToken: clientId
329
+ };
330
+ expect(jest.mocked(publisher.publish)).toBeCalledWith(
331
+ shadowHandler.shadowTopics.projects.update,
332
+ JSON.stringify(packet)
333
+ );
334
+ });
335
+
336
+ test('get shadow updates', async () => {
337
+ shadowHandler.getShadowUpdates();
338
+ expect(jest.mocked(publisher.publish)).toBeCalledWith(
339
+ shadowHandler.shadowTopics.projects.get,
340
+ JSON.stringify({
341
+ clientToken: clientId
342
+ })
343
+ );
344
+ });
345
+
346
+ test('delete project shadow', async () => {
347
+ shadowHandler.deleteProjectShadow(projectId1);
348
+ const packet = {
349
+ state: {
350
+ reported: {
351
+ [projectId1]: null
352
+ }
353
+ },
354
+ clientToken: clientId
355
+ };
356
+ expect(jest.mocked(publisher.publish)).toBeCalledWith(
357
+ shadowHandler.shadowTopics.projects.update,
358
+ JSON.stringify(packet)
359
+ );
360
+ });
361
+ });