@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.
- package/lib/application-control/environment-variables.d.ts +1 -0
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/application-control/environment-variables.js +22 -20
- package/lib/application-control/environment-variables.js.map +1 -1
- package/lib/application-control/environment-variables.test.js +37 -2
- package/lib/application-control/environment-variables.test.js.map +1 -1
- package/lib/application-control/install.js +1 -1
- package/lib/application-control/install.js.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +2 -2
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +116 -99
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +30 -25
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/live-updates-handler.test.js +15 -0
- package/lib/cloud-connection/live-updates-handler.test.js.map +1 -1
- package/lib/cloud-connection/messages.d.ts +1 -3
- package/lib/cloud-connection/messages.d.ts.map +1 -1
- package/lib/cloud-connection/messages.js +1 -9
- package/lib/cloud-connection/messages.js.map +1 -1
- package/lib/cloud-connection/publisher.d.ts +1 -0
- package/lib/cloud-connection/publisher.d.ts.map +1 -1
- package/lib/cloud-connection/publisher.js +3 -0
- package/lib/cloud-connection/publisher.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +10 -3
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.test.js +79 -28
- package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +26 -6
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +103 -22
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +179 -13
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/subcommands/app/analytics.d.ts +10 -0
- package/lib/subcommands/app/analytics.d.ts.map +1 -0
- package/lib/subcommands/app/analytics.js +83 -0
- package/lib/subcommands/app/analytics.js.map +1 -0
- package/lib/subcommands/app/index.d.ts.map +1 -1
- package/lib/subcommands/app/index.js +3 -1
- package/lib/subcommands/app/index.js.map +1 -1
- package/lib/subcommands/app/models.d.ts +0 -5
- package/lib/subcommands/app/models.d.ts.map +1 -1
- package/lib/subcommands/app/models.js +11 -47
- package/lib/subcommands/app/models.js.map +1 -1
- package/lib/subcommands/app/status.d.ts +1 -0
- package/lib/subcommands/app/status.d.ts.map +1 -1
- package/lib/subcommands/app/status.js +14 -3
- package/lib/subcommands/app/status.js.map +1 -1
- package/lib/subcommands/app/version.d.ts +2 -1
- package/lib/subcommands/app/version.d.ts.map +1 -1
- package/lib/subcommands/app/version.js +16 -3
- package/lib/subcommands/app/version.js.map +1 -1
- package/lib/util/parsing.d.ts +2 -0
- package/lib/util/parsing.d.ts.map +1 -0
- package/lib/util/parsing.js +17 -0
- package/lib/util/parsing.js.map +1 -0
- package/package.json +4 -6
- package/readme.md +146 -92
- package/src/application-control/environment-variables.test.ts +43 -3
- package/src/application-control/environment-variables.ts +29 -19
- package/src/application-control/install.ts +1 -1
- package/src/cloud-connection/device-agent-cloud-connection.ts +155 -141
- package/src/cloud-connection/live-updates-handler.test.ts +20 -0
- package/src/cloud-connection/live-updates-handler.ts +45 -52
- package/src/cloud-connection/messages.ts +1 -14
- package/src/cloud-connection/publisher.ts +4 -0
- package/src/cloud-connection/shadow-handler.test.ts +88 -28
- package/src/cloud-connection/shadow-handler.ts +13 -3
- package/src/cloud-connection/transaction-manager.test.ts +193 -18
- package/src/cloud-connection/transaction-manager.ts +174 -26
- package/src/subcommands/app/analytics.ts +99 -0
- package/src/subcommands/app/index.ts +4 -3
- package/src/subcommands/app/models.ts +13 -49
- package/src/subcommands/app/status.ts +20 -3
- package/src/subcommands/app/version.ts +19 -4
- package/src/util/parsing.ts +11 -0
- package/lib/cloud-connection/cmd-status.d.ts +0 -8
- package/lib/cloud-connection/cmd-status.d.ts.map +0 -1
- package/lib/cloud-connection/cmd-status.js +0 -62
- package/lib/cloud-connection/cmd-status.js.map +0 -1
- package/lib/cloud-connection/message-builder.d.ts +0 -7
- package/lib/cloud-connection/message-builder.d.ts.map +0 -1
- package/lib/cloud-connection/message-builder.js +0 -63
- package/lib/cloud-connection/message-builder.js.map +0 -1
- package/src/cloud-connection/cmd-status.ts +0 -71
- package/src/cloud-connection/message-builder.ts +0 -117
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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('
|
|
41
|
+
test('Start a new transaction', async () => {
|
|
18
42
|
const txId = generateTxId();
|
|
19
43
|
const projectId = generateRandomProjectId();
|
|
20
|
-
txnMgr.
|
|
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('
|
|
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.
|
|
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
|
|
120
|
+
test('Attempt to start an ongoing transaction, results in failure', async () => {
|
|
41
121
|
const txId = generateTxId();
|
|
42
122
|
const projectId = generateRandomProjectId();
|
|
43
|
-
txnMgr.
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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.
|
|
56
|
-
|
|
57
|
-
|
|
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('
|
|
197
|
+
test('Handle error in step function', async () => {
|
|
65
198
|
const txId = generateTxId();
|
|
66
199
|
const projectId = generateRandomProjectId();
|
|
67
|
-
txnMgr.
|
|
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
|
|
3
|
-
private
|
|
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
|
-
|
|
6
|
-
this.
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
throw new
|
|
14
|
-
`
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
return this.
|
|
159
|
+
public isOngoingTransaction(txId: string): boolean {
|
|
160
|
+
return txId in this.detailsByTx;
|
|
36
161
|
}
|
|
37
162
|
|
|
38
|
-
public
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
version: 3,
|
|
62
|
-
timestamp: 0,
|
|
62
|
+
const packet = {
|
|
63
63
|
state: {
|
|
64
|
-
|
|
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:
|
|
70
|
+
clientToken: 'client'
|
|
113
71
|
};
|
|
114
|
-
|
|
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
|
}
|