@alwaysai/device-agent 1.3.0 → 1.3.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 (89) hide show
  1. package/lib/application-control/environment-variables.d.ts +1 -0
  2. package/lib/application-control/environment-variables.d.ts.map +1 -1
  3. package/lib/application-control/environment-variables.js +22 -20
  4. package/lib/application-control/environment-variables.js.map +1 -1
  5. package/lib/application-control/environment-variables.test.js +37 -2
  6. package/lib/application-control/environment-variables.test.js.map +1 -1
  7. package/lib/application-control/install.js +1 -1
  8. package/lib/application-control/install.js.map +1 -1
  9. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +2 -2
  10. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  11. package/lib/cloud-connection/device-agent-cloud-connection.js +116 -99
  12. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  13. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  14. package/lib/cloud-connection/live-updates-handler.js +30 -25
  15. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  16. package/lib/cloud-connection/live-updates-handler.test.js +15 -0
  17. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
  18. package/lib/cloud-connection/messages.d.ts +1 -3
  19. package/lib/cloud-connection/messages.d.ts.map +1 -1
  20. package/lib/cloud-connection/messages.js +1 -9
  21. package/lib/cloud-connection/messages.js.map +1 -1
  22. package/lib/cloud-connection/publisher.d.ts +1 -0
  23. package/lib/cloud-connection/publisher.d.ts.map +1 -1
  24. package/lib/cloud-connection/publisher.js +3 -0
  25. package/lib/cloud-connection/publisher.js.map +1 -1
  26. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  27. package/lib/cloud-connection/shadow-handler.js +10 -3
  28. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  29. package/lib/cloud-connection/shadow-handler.test.js +79 -28
  30. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  31. package/lib/cloud-connection/transaction-manager.d.ts +26 -6
  32. package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
  33. package/lib/cloud-connection/transaction-manager.js +103 -22
  34. package/lib/cloud-connection/transaction-manager.js.map +1 -1
  35. package/lib/cloud-connection/transaction-manager.test.js +179 -13
  36. package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
  37. package/lib/subcommands/app/analytics.d.ts +10 -0
  38. package/lib/subcommands/app/analytics.d.ts.map +1 -0
  39. package/lib/subcommands/app/analytics.js +83 -0
  40. package/lib/subcommands/app/analytics.js.map +1 -0
  41. package/lib/subcommands/app/index.d.ts.map +1 -1
  42. package/lib/subcommands/app/index.js +3 -1
  43. package/lib/subcommands/app/index.js.map +1 -1
  44. package/lib/subcommands/app/models.d.ts +0 -5
  45. package/lib/subcommands/app/models.d.ts.map +1 -1
  46. package/lib/subcommands/app/models.js +11 -47
  47. package/lib/subcommands/app/models.js.map +1 -1
  48. package/lib/subcommands/app/status.d.ts +1 -0
  49. package/lib/subcommands/app/status.d.ts.map +1 -1
  50. package/lib/subcommands/app/status.js +14 -3
  51. package/lib/subcommands/app/status.js.map +1 -1
  52. package/lib/subcommands/app/version.d.ts +2 -1
  53. package/lib/subcommands/app/version.d.ts.map +1 -1
  54. package/lib/subcommands/app/version.js +16 -3
  55. package/lib/subcommands/app/version.js.map +1 -1
  56. package/lib/util/parsing.d.ts +2 -0
  57. package/lib/util/parsing.d.ts.map +1 -0
  58. package/lib/util/parsing.js +17 -0
  59. package/lib/util/parsing.js.map +1 -0
  60. package/package.json +4 -6
  61. package/readme.md +146 -92
  62. package/src/application-control/environment-variables.test.ts +43 -3
  63. package/src/application-control/environment-variables.ts +29 -19
  64. package/src/application-control/install.ts +1 -1
  65. package/src/cloud-connection/device-agent-cloud-connection.ts +155 -141
  66. package/src/cloud-connection/live-updates-handler.test.ts +20 -0
  67. package/src/cloud-connection/live-updates-handler.ts +45 -52
  68. package/src/cloud-connection/messages.ts +1 -14
  69. package/src/cloud-connection/publisher.ts +4 -0
  70. package/src/cloud-connection/shadow-handler.test.ts +88 -28
  71. package/src/cloud-connection/shadow-handler.ts +13 -3
  72. package/src/cloud-connection/transaction-manager.test.ts +193 -18
  73. package/src/cloud-connection/transaction-manager.ts +174 -26
  74. package/src/subcommands/app/analytics.ts +99 -0
  75. package/src/subcommands/app/index.ts +4 -3
  76. package/src/subcommands/app/models.ts +13 -49
  77. package/src/subcommands/app/status.ts +20 -3
  78. package/src/subcommands/app/version.ts +19 -4
  79. package/src/util/parsing.ts +11 -0
  80. package/lib/cloud-connection/cmd-status.d.ts +0 -8
  81. package/lib/cloud-connection/cmd-status.d.ts.map +0 -1
  82. package/lib/cloud-connection/cmd-status.js +0 -62
  83. package/lib/cloud-connection/cmd-status.js.map +0 -1
  84. package/lib/cloud-connection/message-builder.d.ts +0 -7
  85. package/lib/cloud-connection/message-builder.d.ts.map +0 -1
  86. package/lib/cloud-connection/message-builder.js +0 -63
  87. package/lib/cloud-connection/message-builder.js.map +0 -1
  88. package/src/cloud-connection/cmd-status.ts +0 -71
  89. package/src/cloud-connection/message-builder.ts +0 -117
@@ -1,12 +1,36 @@
1
- import { generateTxId } from '@alwaysai/device-agent-schemas';
1
+ import {
2
+ ToClientStatusResponseMessage,
3
+ generateTxId,
4
+ keyMirrors
5
+ } from '@alwaysai/device-agent-schemas';
2
6
  import { TransactionManager } from './transaction-manager';
3
7
  import { v4 as uuidv4 } from 'uuid';
8
+ import { Publisher } from './publisher';
9
+ import { LiveUpdatesHandler } from './live-updates-handler';
10
+
11
+ const mockClient = {
12
+ publish: jest.fn()
13
+ };
14
+ const clientId = 'test-client';
15
+
16
+ const mockLiveUpdatesHandler = {
17
+ enableTransactionStatus: jest.fn(),
18
+ disableTransactionStatus: jest.fn()
19
+ } as any as LiveUpdatesHandler;
4
20
 
5
21
  describe('Test Transaction Manager', () => {
6
22
  let txnMgr: TransactionManager;
23
+ let publisher: Publisher;
24
+ const func_complete: () => Promise<boolean> = jest
25
+ .fn()
26
+ .mockResolvedValue(true);
27
+ const func_incomplete: () => Promise<boolean> = jest
28
+ .fn()
29
+ .mockResolvedValue(false);
7
30
 
8
31
  beforeEach(() => {
9
- txnMgr = new TransactionManager();
32
+ publisher = new Publisher(mockClient, clientId);
33
+ txnMgr = new TransactionManager(publisher, mockLiveUpdatesHandler);
10
34
  jest.clearAllMocks();
11
35
  });
12
36
 
@@ -14,60 +38,211 @@ describe('Test Transaction Manager', () => {
14
38
  return uuidv4();
15
39
  }
16
40
 
17
- test('Add a new transaction', async () => {
41
+ test('Start a new transaction', async () => {
18
42
  const txId = generateTxId();
19
43
  const projectId = generateRandomProjectId();
20
- txnMgr.addTransaction(txId, projectId);
44
+ await txnMgr.runTransactionStep({
45
+ func: func_incomplete,
46
+ projectId,
47
+ txId,
48
+ start: true
49
+ });
21
50
  expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
22
51
  expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
52
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
53
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
54
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
55
+ });
56
+
57
+ test('Start a new transaction which completes in one step', async () => {
58
+ const txId = generateTxId();
59
+ const projectId = generateRandomProjectId();
60
+ await txnMgr.runTransactionStep({
61
+ func: func_complete,
62
+ projectId,
63
+ txId,
64
+ start: true
65
+ });
66
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
67
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
68
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
69
+ const msg: ToClientStatusResponseMessage = JSON.parse(
70
+ mockClient.publish.mock.calls[0][1]
71
+ );
72
+ expect(msg.payload.status).toBe(keyMirrors.statusResponse.success);
73
+ });
74
+
75
+ test('Start a new transaction and complete in second step', async () => {
76
+ const txId = generateTxId();
77
+ const projectId = generateRandomProjectId();
78
+ await txnMgr.runTransactionStep({
79
+ func: func_incomplete,
80
+ projectId,
81
+ txId,
82
+ start: true,
83
+ stepName: 'step1'
84
+ });
85
+ await txnMgr.runTransactionStep({
86
+ func: func_complete,
87
+ projectId,
88
+ txId,
89
+ start: false,
90
+ stepName: 'step2'
91
+ });
92
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
93
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
94
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
23
95
  });
24
96
 
25
- test('Add multiple transactions to different projects', async () => {
97
+ test('Start multiple transactions for different projects', async () => {
26
98
  const numTransactions = 3;
27
99
  const projectTxIdList: any = [];
28
100
  for (let i = 0; i < numTransactions; i++) {
29
101
  const txId = generateTxId();
30
102
  const projectId = generateRandomProjectId();
31
- txnMgr.addTransaction(txId, projectId);
103
+ await txnMgr.runTransactionStep({
104
+ func: func_incomplete,
105
+ projectId,
106
+ txId,
107
+ start: true
108
+ });
32
109
  projectTxIdList.push({ projectId, txId });
33
110
  }
34
111
  projectTxIdList.forEach(({ txId, projectId }) => {
35
112
  expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
36
113
  expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
114
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
115
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
116
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
37
117
  });
38
118
  });
39
119
 
40
- test('Attempt to add existing transaction to a project, results in failure', async () => {
120
+ test('Attempt to start an ongoing transaction, results in failure', async () => {
41
121
  const txId = generateTxId();
42
122
  const projectId = generateRandomProjectId();
43
- txnMgr.addTransaction(txId, projectId);
44
- expect(() => txnMgr.addTransaction(txId, projectId)).toThrow(
45
- `Transaction ID ${txId} already exists for this projectId ${projectId}!`
46
- );
123
+ await txnMgr.runTransactionStep({
124
+ func: func_incomplete,
125
+ projectId,
126
+ txId,
127
+ start: true,
128
+ stepName: 'step1'
129
+ });
130
+ try {
131
+ await txnMgr.runTransactionStep({
132
+ func: func_incomplete,
133
+ projectId,
134
+ txId,
135
+ start: true,
136
+ stepName: 'step2'
137
+ });
138
+ throw new Error('Expected starting transaction to fail!');
139
+ } catch (e) {
140
+ console.log(e);
141
+ expect(e.code).toBe(txnMgr.Errors.TRANSACTION_ONGOING);
142
+ }
47
143
  expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
48
144
  expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
49
145
  });
50
146
 
51
- test('Attempt to add a transaction to a project with an ongoing transaction, results in failure', async () => {
147
+ test('Attempt to continue a transaction that is not ongoing', async () => {
148
+ const txId = generateTxId();
149
+ const projectId = generateRandomProjectId();
150
+ try {
151
+ await txnMgr.runTransactionStep({
152
+ func: func_incomplete,
153
+ projectId,
154
+ txId,
155
+ start: false,
156
+ stepName: 'step1'
157
+ });
158
+ throw new Error('Expected continue transaction to fail!');
159
+ } catch (e) {
160
+ console.log(e);
161
+ expect(e.code).toBe(txnMgr.Errors.TRANSACTION_NOT_ONGOING);
162
+ }
163
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
164
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
165
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
166
+ });
167
+
168
+ test('Attempt to start a transaction for a project with an ongoing transaction, results in failure', async () => {
52
169
  const txId = generateTxId();
53
170
  const txId2 = generateTxId();
54
171
  const projectId = generateRandomProjectId();
55
- txnMgr.addTransaction(txId, projectId);
56
- expect(() => txnMgr.addTransaction(txId2, projectId)).toThrow(
57
- `This Project ID ${projectId} already has an ongoing transaction!`
58
- );
172
+ await txnMgr.runTransactionStep({
173
+ func: func_incomplete,
174
+ projectId,
175
+ txId,
176
+ start: true,
177
+ stepName: 'step1'
178
+ });
179
+ try {
180
+ await txnMgr.runTransactionStep({
181
+ func: func_incomplete,
182
+ projectId,
183
+ txId: txId2,
184
+ start: true,
185
+ stepName: 'step2'
186
+ });
187
+ throw new Error('Expected start transaction to fail!');
188
+ } catch (e) {
189
+ console.log(e);
190
+ expect(e.code).toBe(txnMgr.Errors.PROJECT_ONGOING);
191
+ }
59
192
  expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
60
193
  expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
61
194
  expect(txnMgr.getProjectFromTransaction(txId2)).toBeUndefined();
62
195
  });
63
196
 
64
- test('Test remove transaction from queue', async () => {
197
+ test('Handle error in step function', async () => {
65
198
  const txId = generateTxId();
66
199
  const projectId = generateRandomProjectId();
67
- txnMgr.addTransaction(txId, projectId);
200
+ await txnMgr.runTransactionStep({
201
+ func: jest.fn().mockImplementation(() => {
202
+ throw new Error('Test error!');
203
+ }),
204
+ projectId,
205
+ txId,
206
+ start: true,
207
+ stepName: 'step1'
208
+ });
209
+ expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
210
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
211
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
212
+ const msg: ToClientStatusResponseMessage = JSON.parse(
213
+ mockClient.publish.mock.calls[0][1]
214
+ );
215
+ expect(msg.payload.status).toBe(keyMirrors.statusResponse.failure);
216
+ });
217
+
218
+ test('Complete ongoing transaction', async () => {
219
+ const txId = generateTxId();
220
+ const projectId = generateRandomProjectId();
221
+ await txnMgr.runTransactionStep({
222
+ func: func_incomplete,
223
+ projectId,
224
+ txId,
225
+ start: true,
226
+ stepName: 'step1'
227
+ });
68
228
  txnMgr.completeTransaction(txId);
69
229
 
70
230
  expect(txnMgr.getTransactionFromProject(projectId)).toBeUndefined();
71
231
  expect(txnMgr.getProjectFromTransaction(txId)).toBeUndefined();
232
+ expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
233
+ expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
234
+ });
235
+
236
+ test('Test remove non-existing transaction from queue', async () => {
237
+ const txId = generateTxId();
238
+ try {
239
+ txnMgr.completeTransaction(txId);
240
+ throw new Error('Expected completTransaction to fail!');
241
+ } catch (e) {
242
+ console.log(e);
243
+ expect(e.code).toBe(txnMgr.Errors.TRANSACTION_NOT_ONGOING);
244
+ }
245
+
246
+ expect(txnMgr.getProjectFromTransaction(txId)).toBeUndefined();
72
247
  });
73
248
  });
@@ -1,43 +1,191 @@
1
+ import {
2
+ StatusResponsePayload,
3
+ buildToClientStatusResponseMessage,
4
+ keyMirrors
5
+ } from '@alwaysai/device-agent-schemas';
6
+ import { LiveUpdatesHandler } from './live-updates-handler';
7
+ import { Publisher } from './publisher';
8
+ import { logger } from '../util/logger';
9
+ import { keyMirror } from 'alwaysai/lib/util';
10
+ import { CodedError } from '@carnesen/coded-error';
11
+
12
+ interface TransactionDetails {
13
+ txId: string;
14
+ projectId: string;
15
+ stepName?: string;
16
+ start: string;
17
+ update?: string;
18
+ stop?: string;
19
+ }
20
+
1
21
  export class TransactionManager {
2
- private txToProject: Record<string, string> = {};
3
- private projectToTx: Record<string, string> = {};
22
+ private detailsByTx: Record<string, TransactionDetails> = {};
23
+ private detailsByProject: Record<string, TransactionDetails> = {};
24
+ private liveUpdatesHandler: LiveUpdatesHandler;
25
+ private publisher: Publisher;
26
+
27
+ private startTransaction(
28
+ txId: string,
29
+ projectId: string,
30
+ stepName?: string
31
+ ): void {
32
+ // Check if the transaction already exists
33
+ if (this.detailsByTx[txId]) {
34
+ const txnDetails = this.detailsByTx[txId];
35
+ throw new CodedError(
36
+ `Transaction ${txId} already ongoing!\n${JSON.stringify(
37
+ txnDetails,
38
+ null,
39
+ 2
40
+ )}`,
41
+ this.Errors.TRANSACTION_ONGOING
42
+ );
43
+ }
4
44
 
5
- constructor() {
6
- this.txToProject = {};
7
- this.projectToTx = {};
45
+ // Check if there is any ongoing transactions for project
46
+ if (this.detailsByProject[projectId]) {
47
+ const txnDetails = this.detailsByProject[projectId];
48
+ throw new CodedError(
49
+ `Project ${projectId} already has an ongoing transaction!\n${JSON.stringify(
50
+ txnDetails,
51
+ null,
52
+ 2
53
+ )}`,
54
+ this.Errors.PROJECT_ONGOING
55
+ );
56
+ }
57
+
58
+ // Map the Transaction ID with Project ID
59
+ const txDetails = {
60
+ txId,
61
+ projectId,
62
+ stepName,
63
+ start: new Date().toLocaleString()
64
+ };
65
+ this.detailsByTx[txId] = txDetails;
66
+ this.detailsByProject[projectId] = txDetails;
67
+ logger.info(`Started transaction:\n${JSON.stringify(txDetails, null, 2)}`);
68
+ // send live updates
69
+ void this.liveUpdatesHandler.enableTransactionStatus({
70
+ txId
71
+ });
8
72
  }
9
73
 
10
- public addTransaction(txId: string, projectId: string) {
11
- // Check if the Transaction already exists
12
- if (this.txToProject[txId]) {
13
- throw new Error(
14
- `Transaction ID ${txId} already exists for this projectId ${projectId}!`
74
+ private updateTransaction(txId: string, stepName?: string): void {
75
+ const txDetails = this.detailsByTx[txId];
76
+ if (!txDetails) {
77
+ throw new CodedError(
78
+ `Cannot update transaction ${txId} since it doesn't exist!`,
79
+ this.Errors.TRANSACTION_NOT_ONGOING
15
80
  );
81
+ }
82
+ txDetails.stepName = stepName;
83
+ txDetails.update = new Date().toLocaleString();
84
+ logger.info(`Updated transaction:\n${JSON.stringify(txDetails, null, 2)}`);
85
+ }
86
+
87
+ constructor(publisher: Publisher, liveUpdatesHandler: LiveUpdatesHandler) {
88
+ this.detailsByTx = {};
89
+ this.detailsByProject = {};
90
+ this.publisher = publisher;
91
+ this.liveUpdatesHandler = liveUpdatesHandler;
92
+ }
93
+
94
+ public Errors = keyMirror({
95
+ TRANSACTION_ONGOING: null,
96
+ PROJECT_ONGOING: null,
97
+ TRANSACTION_NOT_ONGOING: null
98
+ });
99
+
100
+ public async runTransactionStep(props: {
101
+ func: () => Promise<boolean>;
102
+ projectId: string;
103
+ txId: string;
104
+ start: boolean;
105
+ stepName?: string;
106
+ }) {
107
+ const { func, projectId, txId, start, stepName } = props;
108
+ if (start) {
109
+ this.startTransaction(txId, projectId, stepName);
16
110
  } else {
17
- // Check if there is any ongoing Transactions for Project ID
18
- if (this.projectToTx[projectId]) {
19
- throw new Error(
20
- `This Project ID ${projectId} already has an ongoing transaction!`
111
+ this.updateTransaction(txId, stepName);
112
+ }
113
+ try {
114
+ const completed = await func();
115
+ if (completed) {
116
+ this.completeTransaction(txId);
117
+ const successStatusResponsePayload: StatusResponsePayload = {
118
+ status: keyMirrors.statusResponse.success
119
+ };
120
+ // Send final status message
121
+ const message = buildToClientStatusResponseMessage(
122
+ this.publisher.getClientId(),
123
+ successStatusResponsePayload,
124
+ txId
21
125
  );
22
- } else {
23
- // Map the Transaction ID with Project ID
24
- this.txToProject[txId] = projectId;
25
- this.projectToTx[projectId] = txId;
126
+ this.publisher.publishToClient(message);
26
127
  }
128
+ } catch (e) {
129
+ const message: string = e.message;
130
+ logger.error(
131
+ `Failed to execute cmd for ${projectId}:\n${message}\n${e.stack}`
132
+ );
133
+
134
+ this.completeTransaction(txId);
135
+
136
+ const failureStatusResponsePayload: StatusResponsePayload = {
137
+ status: keyMirrors.statusResponse.failure,
138
+ message
139
+ };
140
+ // Send final status message
141
+ const failureStatusResponseMessage = buildToClientStatusResponseMessage(
142
+ this.publisher.getClientId(),
143
+ failureStatusResponsePayload,
144
+ txId
145
+ );
146
+ this.publisher.publishToClient(failureStatusResponseMessage);
27
147
  }
28
148
  }
29
149
 
30
- public getTransactionFromProject(projectId: string) {
31
- return this.projectToTx[projectId];
150
+ public getTransactionFromProject(projectId: string): string | undefined {
151
+ const txnDetails = this.detailsByProject[projectId];
152
+ return txnDetails?.txId;
153
+ }
154
+
155
+ public isOngoingTransactionForProjectID(projectId: string): boolean {
156
+ return projectId in this.detailsByProject;
32
157
  }
33
158
 
34
- public getProjectFromTransaction(txId: string) {
35
- return this.txToProject[txId];
159
+ public isOngoingTransaction(txId: string): boolean {
160
+ return txId in this.detailsByTx;
36
161
  }
37
162
 
38
- public completeTransaction(txId: string) {
39
- const projectId = this.txToProject[txId];
40
- delete this.txToProject[txId];
41
- delete this.projectToTx[projectId];
163
+ public isAnyOngoingTransaction(): boolean {
164
+ return Object.keys(this.detailsByTx).length > 0;
165
+ }
166
+
167
+ public getProjectFromTransaction(txId: string): string | undefined {
168
+ const txnDetails = this.detailsByTx[txId];
169
+ return txnDetails?.projectId;
170
+ }
171
+
172
+ public completeTransaction(txId: string): void {
173
+ const txDetails = this.detailsByTx[txId];
174
+ if (txDetails === undefined) {
175
+ throw new CodedError(
176
+ `Cannot complete transaction ${txId} since it doesn't exist!`,
177
+ this.Errors.TRANSACTION_NOT_ONGOING
178
+ );
179
+ }
180
+ txDetails.stop = new Date().toLocaleString();
181
+ logger.info(
182
+ `Completed transaction:\n${JSON.stringify(txDetails, null, 2)}`
183
+ );
184
+ delete this.detailsByTx[txId];
185
+ delete this.detailsByProject[txDetails.projectId];
186
+
187
+ void this.liveUpdatesHandler.disableTransactionStatus({
188
+ txId
189
+ });
42
190
  }
43
191
  }
@@ -0,0 +1,99 @@
1
+ import {
2
+ CliFlagInput,
3
+ CliLeaf,
4
+ CliNumberInput,
5
+ CliStringInput
6
+ } from '@alwaysai/alwayscli';
7
+ import { readAppCfgFile } from '../../application-control';
8
+ import { DeviceAgentCloudConnection } from '../../cloud-connection/device-agent-cloud-connection';
9
+ import sleep from '../../util/sleep';
10
+ import { logger } from '../../util/logger';
11
+ import { assign, merge } from 'lodash';
12
+
13
+ export const getAnalyticsCfgCliLeaf = CliLeaf({
14
+ name: 'get-analytics-cfg',
15
+ description: 'Get analytics configuration for an application',
16
+ namedInputs: {
17
+ project: CliStringInput({
18
+ description: 'Project Id',
19
+ required: true
20
+ })
21
+ },
22
+ async action(_, opts) {
23
+ const { project } = opts;
24
+ const appCfg = await readAppCfgFile({ projectId: project });
25
+ if (appCfg.analytics !== undefined) {
26
+ console.log(JSON.stringify(appCfg.analytics, null, 2));
27
+ } else {
28
+ console.log('No analytics configuration for app!');
29
+ }
30
+ }
31
+ });
32
+
33
+ export const setAnalyticsCfgCliLeaf = CliLeaf({
34
+ name: 'set-analytics-cfg',
35
+ description:
36
+ 'Set analytics configuration for an application. Note that this resets the config so all desired options must be set',
37
+ namedInputs: {
38
+ project: CliStringInput({
39
+ description: 'Project Id',
40
+ required: true
41
+ }),
42
+ 'enable-cloud-publish': CliFlagInput({
43
+ description: 'Enable publishing analytics to cloud'
44
+ }),
45
+ 'enable-file-publish': CliFlagInput({
46
+ description: 'Enable publishing analytics to file'
47
+ }),
48
+ 'file-size-bytes': CliNumberInput({
49
+ description: 'Set the max file size in bytes for analytics file writing',
50
+ required: false
51
+ })
52
+ },
53
+ async action(
54
+ _,
55
+ {
56
+ project,
57
+ 'enable-cloud-publish': enableCLoudPublish,
58
+ 'enable-file-publish': enableFilePublish,
59
+ 'file-size-bytes': fileSizeBytes
60
+ }
61
+ ) {
62
+ const deviceAgent = new DeviceAgentCloudConnection();
63
+ await deviceAgent.setupHandlers();
64
+
65
+ const newAppCfg = {
66
+ analytics: {
67
+ enable_cloud_publish: enableCLoudPublish,
68
+ enable_file_publish: enableFilePublish,
69
+ file_size_bytes: fileSizeBytes
70
+ }
71
+ };
72
+ const existingAppCfg = await readAppCfgFile({ projectId: project });
73
+ const appCfg = assign(existingAppCfg, merge(existingAppCfg, newAppCfg));
74
+
75
+ // Update the shadow as a client
76
+ const topic = deviceAgent.getShadowTopics().projects.update;
77
+ const packet = {
78
+ state: {
79
+ desired: {
80
+ [project]: {
81
+ appConfig: JSON.stringify(appCfg) // Pack app config as string as dictated by schema
82
+ }
83
+ }
84
+ },
85
+ clientToken: 'client'
86
+ };
87
+ logger.debug(
88
+ `Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
89
+ );
90
+ deviceAgent.publisher.publish(topic, JSON.stringify(packet));
91
+ // Sleep for extra time to ensure time for shadow response
92
+ await sleep(10000);
93
+
94
+ while (deviceAgent.isCmdInProgress(project)) {
95
+ await sleep(1000);
96
+ }
97
+ await deviceAgent.stop();
98
+ }
99
+ });
@@ -5,8 +5,7 @@ import {
5
5
  addModelCliLeaf,
6
6
  removeModelCliLeaf,
7
7
  replaceModelsCliLeaf,
8
- updateModelsCliLeaf,
9
- installModelCliLeaf
8
+ updateModelsCliLeaf
10
9
  } from './models';
11
10
  import {
12
11
  getAppStatusCliLeaf,
@@ -22,6 +21,7 @@ import {
22
21
  rollbackAppCliLeaf
23
22
  } from './version';
24
23
  import { getShadowCliLeaf, updateShadowCliLeaf } from './shadow';
24
+ import { getAnalyticsCfgCliLeaf, setAnalyticsCfgCliLeaf } from './analytics';
25
25
 
26
26
  export const appCliBranch = CliBranch({
27
27
  name: 'app',
@@ -40,10 +40,11 @@ export const appCliBranch = CliBranch({
40
40
  addModelCliLeaf,
41
41
  removeModelCliLeaf,
42
42
  replaceModelsCliLeaf,
43
- installModelCliLeaf,
44
43
  updateModelsCliLeaf,
45
44
  getAllEnvsCliLeaf,
46
45
  setEnvCliLeaf,
46
+ getAnalyticsCfgCliLeaf,
47
+ setAnalyticsCfgCliLeaf,
47
48
  getShadowCliLeaf,
48
49
  updateShadowCliLeaf
49
50
  ]
@@ -13,6 +13,7 @@ import {
13
13
  } from '../../application-control';
14
14
  import { DeviceAgentCloudConnection } from '../../cloud-connection/device-agent-cloud-connection';
15
15
  import sleep from '../../util/sleep';
16
+ import { logger } from '../../util/logger';
16
17
 
17
18
  export const showAppModelsCliLeaf = CliLeaf({
18
19
  name: 'show-models',
@@ -52,66 +53,29 @@ export const addModelCliLeaf = CliLeaf({
52
53
  const deviceAgent = new DeviceAgentCloudConnection();
53
54
  await deviceAgent.setupHandlers();
54
55
 
55
- const topic = deviceAgent.getShadowTopics().projects.updateDelta;
56
+ // Update the shadow as a client
57
+ const topic = deviceAgent.getShadowTopics().projects.update;
56
58
 
57
59
  const newAppCfg = await readAppCfgFile({ projectId: project });
58
60
  newAppCfg.models[model] = Number(version);
59
61
 
60
- const message = {
61
- version: 3,
62
- timestamp: 0,
62
+ const packet = {
63
63
  state: {
64
- [project]: {
65
- appConfig: JSON.stringify(newAppCfg)
66
- }
67
- },
68
- clientToken: 'not-self'
69
- };
70
-
71
- await deviceAgent.handleMessage(topic, message);
72
- while (deviceAgent.isCmdInProgress(project)) {
73
- await sleep(1000);
74
- }
75
- await deviceAgent.stop();
76
- }
77
- });
78
-
79
- export const installModelCliLeaf = CliLeaf({
80
- name: 'install-model',
81
- description: 'Install an alwaysAI model to a project',
82
- namedInputs: {
83
- project: CliStringInput({
84
- description: 'Project ID',
85
- required: true
86
- }),
87
- modelName: CliStringInput({
88
- description: 'Model Name',
89
- required: true
90
- }),
91
- modelVersion: CliNumberInput({
92
- description: 'Model Version',
93
- required: true
94
- })
95
- },
96
- async action(_, opts) {
97
- const { project, modelName, modelVersion } = opts;
98
- const deviceAgent = new DeviceAgentCloudConnection();
99
- await deviceAgent.setupHandlers();
100
- const topic = deviceAgent.getShadowTopics().projects.getAccepted;
101
- const newAppCfg = await readAppCfgFile({ projectId: project });
102
- newAppCfg['models'][modelName] = modelVersion;
103
-
104
- const message = {
105
- state: {
106
- delta: {
64
+ desired: {
107
65
  [project]: {
108
66
  appConfig: JSON.stringify(newAppCfg)
109
67
  }
110
68
  }
111
69
  },
112
- clientToken: deviceAgent.getClientId()
70
+ clientToken: 'client'
113
71
  };
114
- await deviceAgent.handleMessage(topic, message);
72
+ logger.debug(
73
+ `Publishing message:\n${JSON.stringify({ topic, packet }, null, 2)}`
74
+ );
75
+ deviceAgent.publisher.publish(topic, JSON.stringify(packet));
76
+ // Sleep for extra time to ensure time for shadow response
77
+ await sleep(10000);
78
+
115
79
  while (deviceAgent.isCmdInProgress(project)) {
116
80
  await sleep(1000);
117
81
  }