@alwaysai/device-agent 2.0.0 → 2.0.2-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.
- package/lib/application-control/environment-variables.d.ts +4 -0
- package/lib/application-control/environment-variables.d.ts.map +1 -1
- package/lib/application-control/environment-variables.js +17 -13
- package/lib/application-control/environment-variables.js.map +1 -1
- package/lib/application-control/install.d.ts +4 -1
- package/lib/application-control/install.d.ts.map +1 -1
- package/lib/application-control/install.js +16 -1
- package/lib/application-control/install.js.map +1 -1
- package/lib/application-control/utils.d.ts.map +1 -1
- package/lib/application-control/utils.js +13 -0
- package/lib/application-control/utils.js.map +1 -1
- package/lib/cloud-connection/base-message-handler.d.ts +27 -0
- package/lib/cloud-connection/base-message-handler.d.ts.map +1 -0
- package/lib/cloud-connection/base-message-handler.js +72 -0
- package/lib/cloud-connection/base-message-handler.js.map +1 -0
- package/lib/cloud-connection/connection-manager.d.ts +20 -0
- package/lib/cloud-connection/connection-manager.d.ts.map +1 -0
- package/lib/cloud-connection/connection-manager.js +164 -0
- package/lib/cloud-connection/connection-manager.js.map +1 -0
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts +7 -23
- package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
- package/lib/cloud-connection/device-agent-cloud-connection.js +49 -517
- package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
- package/lib/cloud-connection/device-agent-message-handler.d.ts +22 -0
- package/lib/cloud-connection/device-agent-message-handler.d.ts.map +1 -0
- package/lib/cloud-connection/device-agent-message-handler.js +357 -0
- package/lib/cloud-connection/device-agent-message-handler.js.map +1 -0
- package/lib/cloud-connection/live-updates-handler.d.ts +1 -0
- package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
- package/lib/cloud-connection/live-updates-handler.js +13 -10
- package/lib/cloud-connection/live-updates-handler.js.map +1 -1
- package/lib/cloud-connection/message-dispatcher.d.ts +10 -0
- package/lib/cloud-connection/message-dispatcher.d.ts.map +1 -0
- package/lib/cloud-connection/message-dispatcher.js +27 -0
- package/lib/cloud-connection/message-dispatcher.js.map +1 -0
- package/lib/cloud-connection/publisher.d.ts +3 -2
- package/lib/cloud-connection/publisher.d.ts.map +1 -1
- package/lib/cloud-connection/publisher.js +8 -4
- package/lib/cloud-connection/publisher.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.d.ts +7 -0
- package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
- package/lib/cloud-connection/shadow-handler.js +77 -1
- package/lib/cloud-connection/shadow-handler.js.map +1 -1
- package/lib/cloud-connection/shadow-handler.test.js +5 -2
- package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.d.ts +9 -4
- package/lib/cloud-connection/transaction-manager.d.ts.map +1 -1
- package/lib/cloud-connection/transaction-manager.js +22 -11
- package/lib/cloud-connection/transaction-manager.js.map +1 -1
- package/lib/cloud-connection/transaction-manager.test.js +43 -14
- package/lib/cloud-connection/transaction-manager.test.js.map +1 -1
- package/lib/jobs/job-handler.d.ts +23 -0
- package/lib/jobs/job-handler.d.ts.map +1 -0
- package/lib/jobs/job-handler.js +131 -0
- package/lib/jobs/job-handler.js.map +1 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts +8 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.d.ts.map +1 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.js +42 -0
- package/lib/secure-tunneling/secure-tunnel-message-handler.js.map +1 -0
- package/lib/subcommands/app/version.d.ts +2 -0
- package/lib/subcommands/app/version.d.ts.map +1 -1
- package/lib/subcommands/app/version.js +14 -2
- package/lib/subcommands/app/version.js.map +1 -1
- package/package.json +2 -2
- package/src/application-control/environment-variables.ts +31 -21
- package/src/application-control/install.ts +24 -3
- package/src/application-control/utils.ts +13 -0
- package/src/cloud-connection/base-message-handler.ts +118 -0
- package/src/cloud-connection/connection-manager.ts +196 -0
- package/src/cloud-connection/device-agent-cloud-connection.ts +104 -817
- package/src/cloud-connection/device-agent-message-handler.ts +642 -0
- package/src/cloud-connection/live-updates-handler.ts +26 -18
- package/src/cloud-connection/message-dispatcher.ts +33 -0
- package/src/cloud-connection/publisher.ts +28 -23
- package/src/cloud-connection/shadow-handler.test.ts +6 -2
- package/src/cloud-connection/shadow-handler.ts +129 -1
- package/src/cloud-connection/transaction-manager.test.ts +55 -24
- package/src/cloud-connection/transaction-manager.ts +42 -31
- package/src/jobs/job-handler.ts +146 -0
- package/src/secure-tunneling/secure-tunnel-message-handler.ts +56 -0
- package/src/subcommands/app/version.ts +20 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { logger } from '../util/logger';
|
|
2
|
+
|
|
3
|
+
export interface MessageHandler<T = any> {
|
|
4
|
+
handle(payload: T, topic?: string): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class MessageDispatcher<T = any> {
|
|
8
|
+
private handlers: Map<string, MessageHandler<T>> = new Map();
|
|
9
|
+
|
|
10
|
+
public registerHandler(topic: string, handler: MessageHandler): void {
|
|
11
|
+
this.handlers.set(topic, handler);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public dispatch(topic: string, payload: T): void {
|
|
15
|
+
const handler = this.handlers.get(topic);
|
|
16
|
+
if (handler) {
|
|
17
|
+
handler.handle(payload, topic);
|
|
18
|
+
} else {
|
|
19
|
+
this.handleUnknownMessage(topic, payload);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle unknown or unregistered message topic
|
|
24
|
+
private handleUnknownMessage(topic: string, payload: T): void {
|
|
25
|
+
logger.error(
|
|
26
|
+
`No handler found for topic/type: ${topic} for message ${JSON.stringify(
|
|
27
|
+
payload,
|
|
28
|
+
null,
|
|
29
|
+
2
|
|
30
|
+
)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -6,15 +6,16 @@ import {
|
|
|
6
6
|
getToCloudTopic
|
|
7
7
|
} from '@alwaysai/device-agent-schemas';
|
|
8
8
|
import * as winston from 'winston';
|
|
9
|
+
import { ConnectionManager } from './connection-manager';
|
|
9
10
|
|
|
10
11
|
export class Publisher {
|
|
11
|
-
private
|
|
12
|
+
private connectionManager: ConnectionManager;
|
|
12
13
|
private clientId: string;
|
|
13
14
|
private readonly toClientTopic: string;
|
|
14
15
|
private readonly toCloudTopic: string;
|
|
15
16
|
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
17
|
+
constructor(connectionManager: ConnectionManager, clientId: string) {
|
|
18
|
+
this.connectionManager = connectionManager;
|
|
18
19
|
this.clientId = clientId;
|
|
19
20
|
this.toClientTopic = getToClientTopic(this.clientId);
|
|
20
21
|
this.toCloudTopic = getToCloudTopic(this.clientId);
|
|
@@ -34,13 +35,15 @@ export class Publisher {
|
|
|
34
35
|
2
|
|
35
36
|
)}`
|
|
36
37
|
);
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
this.connectionManager
|
|
39
|
+
.getIoTDevice()
|
|
40
|
+
.publish(topic, payload, (err: any) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
logger.error(
|
|
43
|
+
`Error publishing message: \nTopic: ${topic}\nMessage: ${payload}\nError: ${err}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
public publishToCloudWithAck(
|
|
@@ -48,19 +51,21 @@ export class Publisher {
|
|
|
48
51
|
ackNackCallback: CallableFunction
|
|
49
52
|
) {
|
|
50
53
|
const topic = this.toCloudTopic;
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
this.connectionManager
|
|
55
|
+
.getIoTDevice()
|
|
56
|
+
.publish(topic, payload, { qos: 1 }, (err: any, resp: any) => {
|
|
57
|
+
if (err) {
|
|
58
|
+
logger.error(
|
|
59
|
+
`Error publishing message: \nTopic: ${topic}\nMessage: ${payload}\nError: ${err}`
|
|
60
|
+
);
|
|
61
|
+
ackNackCallback(false);
|
|
62
|
+
} else if (resp) {
|
|
63
|
+
logger.debug(
|
|
64
|
+
`Successfully published message: \nTopic: ${topic}\nMessage: ${payload}`
|
|
65
|
+
);
|
|
66
|
+
ackNackCallback(true);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
public publishDeviceAgentMessage(
|
|
@@ -5,10 +5,14 @@ import { ShadowHandler } from './shadow-handler';
|
|
|
5
5
|
import { Logger } from 'winston';
|
|
6
6
|
import { logger } from '../util/logger';
|
|
7
7
|
import { getShadowTopic } from '@alwaysai/device-agent-schemas';
|
|
8
|
+
import { ConnectionManager } from './connection-manager';
|
|
8
9
|
|
|
9
10
|
jest.mock('../application-control');
|
|
10
11
|
jest.mock('./publisher');
|
|
11
|
-
|
|
12
|
+
jest.mock('./connection-manager');
|
|
13
|
+
const mockConnectionManager = {
|
|
14
|
+
getIotDevice: jest.fn()
|
|
15
|
+
} as any as ConnectionManager;
|
|
12
16
|
const clientId = 'test-client';
|
|
13
17
|
const projectId1 = 'test-project';
|
|
14
18
|
const projectId2 = 'test-project-2';
|
|
@@ -18,7 +22,7 @@ describe('Test Shadow Handler', () => {
|
|
|
18
22
|
let shadowHandler: ShadowHandler;
|
|
19
23
|
|
|
20
24
|
beforeEach(() => {
|
|
21
|
-
publisher = new Publisher(
|
|
25
|
+
publisher = new Publisher(mockConnectionManager, clientId);
|
|
22
26
|
shadowHandler = new ShadowHandler(clientId, publisher);
|
|
23
27
|
});
|
|
24
28
|
|
|
@@ -3,13 +3,17 @@ import {
|
|
|
3
3
|
validateAppConfig
|
|
4
4
|
} from '@alwaysai/app-configuration-schemas';
|
|
5
5
|
import {
|
|
6
|
+
buildSignedUrlsRequestMessage,
|
|
7
|
+
buildToClientStatusResponseMessage,
|
|
6
8
|
buildUpdateShadowMessage,
|
|
7
9
|
EnvVars,
|
|
8
10
|
generateTxId,
|
|
9
11
|
getShadowTopic,
|
|
10
12
|
getUpdateDeltaStateFromMessage,
|
|
13
|
+
keyMirrors,
|
|
11
14
|
ProjectShadowUpdate,
|
|
12
15
|
SecureTunnelShadowUpdate,
|
|
16
|
+
SignedUrlsRequestPayload,
|
|
13
17
|
validateEnvVarSchemaShadowUpdate,
|
|
14
18
|
validateProjectShadowUpdate
|
|
15
19
|
} from '@alwaysai/device-agent-schemas';
|
|
@@ -19,11 +23,19 @@ import {
|
|
|
19
23
|
ShadowProjectsUpdateAll
|
|
20
24
|
} from '@alwaysai/device-agent-schemas/lib/shadow-schema';
|
|
21
25
|
import { stringifyError } from 'alwaysai/lib/util';
|
|
22
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
getAllEnvs,
|
|
28
|
+
readAppCfgFile,
|
|
29
|
+
setEnv,
|
|
30
|
+
updateAppCfg
|
|
31
|
+
} from '../application-control';
|
|
23
32
|
import { getSystemInformation } from '../device-control/device-control';
|
|
24
33
|
import { logger } from '../util/logger';
|
|
25
34
|
import { Publisher } from './publisher';
|
|
26
35
|
import { AppConfigModels, getAppCfgModelsDiff } from './shadow';
|
|
36
|
+
import { pruneModels } from '../application-control/models';
|
|
37
|
+
import { MessageHandler } from './message-dispatcher';
|
|
38
|
+
import { BaseHandler } from './base-message-handler';
|
|
27
39
|
|
|
28
40
|
export type AppConfigUpdate = {
|
|
29
41
|
newAppCfg: AppConfig;
|
|
@@ -334,6 +346,10 @@ export class ShadowHandler {
|
|
|
334
346
|
return [];
|
|
335
347
|
}
|
|
336
348
|
|
|
349
|
+
public async initShadows() {
|
|
350
|
+
await this.updateSystemInfoShadow();
|
|
351
|
+
}
|
|
352
|
+
|
|
337
353
|
public async updateSystemInfoShadow() {
|
|
338
354
|
const systemInfo = await getSystemInformation();
|
|
339
355
|
this.publisher.publish(
|
|
@@ -440,3 +456,115 @@ export class ShadowHandler {
|
|
|
440
456
|
);
|
|
441
457
|
}
|
|
442
458
|
}
|
|
459
|
+
|
|
460
|
+
export class ProjectShadowMessageHandler
|
|
461
|
+
extends BaseHandler
|
|
462
|
+
implements MessageHandler<any>
|
|
463
|
+
{
|
|
464
|
+
public async handle(message: any, topic: string): Promise<void> {
|
|
465
|
+
const shadowUpdates = await this.shadowHandler.handleProjectShadow({
|
|
466
|
+
topic,
|
|
467
|
+
payload: message,
|
|
468
|
+
clientToken: message.clientToken
|
|
469
|
+
});
|
|
470
|
+
if (shadowUpdates.length) {
|
|
471
|
+
const shadowUpdatePromises: Promise<void>[] = [];
|
|
472
|
+
for (const shadowUpdate of shadowUpdates) {
|
|
473
|
+
const projectId = shadowUpdate.projectId;
|
|
474
|
+
const txId = shadowUpdate.txId;
|
|
475
|
+
shadowUpdatePromises.push(
|
|
476
|
+
this.txnMgr
|
|
477
|
+
.runTransactionStep({
|
|
478
|
+
func: () =>
|
|
479
|
+
this.handleProjectShadowConfigUpdate(shadowUpdate, txId),
|
|
480
|
+
projectId,
|
|
481
|
+
txId,
|
|
482
|
+
start: true,
|
|
483
|
+
liveUpdatesPublishFn: async () =>
|
|
484
|
+
this.publisher.publishToClient(
|
|
485
|
+
buildToClientStatusResponseMessage(
|
|
486
|
+
this.clientId,
|
|
487
|
+
{ status: keyMirrors.statusResponse.in_progress },
|
|
488
|
+
txId
|
|
489
|
+
),
|
|
490
|
+
logger.silly
|
|
491
|
+
),
|
|
492
|
+
stepName: topic
|
|
493
|
+
})
|
|
494
|
+
.catch((e) => {
|
|
495
|
+
logger.error(
|
|
496
|
+
`There was an issue updating project shadow config for ${projectId}!\n${stringifyError(
|
|
497
|
+
e
|
|
498
|
+
)}`
|
|
499
|
+
);
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
await Promise.all(shadowUpdatePromises);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private handleProjectShadowConfigUpdate = async (
|
|
509
|
+
update: ShadowUpdate,
|
|
510
|
+
txId: string
|
|
511
|
+
): Promise<boolean> => {
|
|
512
|
+
const { projectId, appCfgUpdate, envVarUpdate } = update;
|
|
513
|
+
|
|
514
|
+
if (
|
|
515
|
+
appCfgUpdate?.updatedModels &&
|
|
516
|
+
Object.keys(appCfgUpdate.updatedModels).length
|
|
517
|
+
) {
|
|
518
|
+
// When there are model updates request signed URLs and wait to apply config changes
|
|
519
|
+
const { updatedModels } = appCfgUpdate;
|
|
520
|
+
|
|
521
|
+
logger.debug(
|
|
522
|
+
`Requesting presigned urls from cloud for model versions: ${JSON.stringify(
|
|
523
|
+
updatedModels
|
|
524
|
+
)}`
|
|
525
|
+
);
|
|
526
|
+
const modelsOnlyUrlsRequestPayload: SignedUrlsRequestPayload = {
|
|
527
|
+
modelsOnlyUrlsRequest: {
|
|
528
|
+
projectId,
|
|
529
|
+
models: updatedModels
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const message = buildSignedUrlsRequestMessage(
|
|
533
|
+
this.clientId,
|
|
534
|
+
modelsOnlyUrlsRequestPayload,
|
|
535
|
+
txId
|
|
536
|
+
);
|
|
537
|
+
this.publisher.publishToCloud(message);
|
|
538
|
+
|
|
539
|
+
this.txnMgr.setAppCfgUpdateToTx(txId, update);
|
|
540
|
+
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (appCfgUpdate) {
|
|
545
|
+
await this.atomicApplicationUpdate(async () => {
|
|
546
|
+
await pruneModels({
|
|
547
|
+
projectId,
|
|
548
|
+
appCfg: appCfgUpdate.newAppCfg
|
|
549
|
+
});
|
|
550
|
+
await updateAppCfg({
|
|
551
|
+
projectId,
|
|
552
|
+
newAppCfg: appCfgUpdate.newAppCfg
|
|
553
|
+
});
|
|
554
|
+
}, projectId);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (envVarUpdate) {
|
|
558
|
+
await this.atomicApplicationUpdate(
|
|
559
|
+
async () =>
|
|
560
|
+
await this.shadowHandler.updateProjectEnvVars({
|
|
561
|
+
projectId,
|
|
562
|
+
envVars: envVarUpdate.envVars
|
|
563
|
+
}),
|
|
564
|
+
projectId,
|
|
565
|
+
true
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return true;
|
|
569
|
+
};
|
|
570
|
+
}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ToClientStatusResponseMessage,
|
|
3
|
-
generateTxId,
|
|
4
|
-
keyMirrors
|
|
5
|
-
} from '@alwaysai/device-agent-schemas';
|
|
1
|
+
import { EnvVars, generateTxId } from '@alwaysai/device-agent-schemas';
|
|
6
2
|
import { TransactionManager } from './transaction-manager';
|
|
7
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
8
|
-
import { Publisher } from './publisher';
|
|
9
4
|
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
10
5
|
import { AppConfigUpdate, ShadowUpdate } from './shadow-handler';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
publish: jest.fn()
|
|
14
|
-
};
|
|
15
|
-
const clientId = 'test-client';
|
|
6
|
+
import { AppContent } from './device-agent-message-handler';
|
|
7
|
+
import { AppConfig } from '@alwaysai/app-configuration-schemas';
|
|
16
8
|
|
|
17
9
|
const mockLiveUpdatesHandler = {
|
|
18
10
|
enable: jest.fn(),
|
|
@@ -21,7 +13,6 @@ const mockLiveUpdatesHandler = {
|
|
|
21
13
|
|
|
22
14
|
describe('Test Transaction Manager', () => {
|
|
23
15
|
let txnMgr: TransactionManager;
|
|
24
|
-
let publisher: Publisher;
|
|
25
16
|
const func_complete: () => Promise<boolean> = jest
|
|
26
17
|
.fn()
|
|
27
18
|
.mockResolvedValue(true);
|
|
@@ -30,8 +21,7 @@ describe('Test Transaction Manager', () => {
|
|
|
30
21
|
.mockResolvedValue(false);
|
|
31
22
|
|
|
32
23
|
beforeEach(() => {
|
|
33
|
-
|
|
34
|
-
txnMgr = new TransactionManager(publisher, mockLiveUpdatesHandler);
|
|
24
|
+
txnMgr = new TransactionManager(mockLiveUpdatesHandler);
|
|
35
25
|
jest.clearAllMocks();
|
|
36
26
|
});
|
|
37
27
|
|
|
@@ -58,19 +48,23 @@ describe('Test Transaction Manager', () => {
|
|
|
58
48
|
test('Start a new transaction which completes in one step', async () => {
|
|
59
49
|
const txId = generateTxId();
|
|
60
50
|
const projectId = generateRandomProjectId();
|
|
51
|
+
|
|
52
|
+
const successFn = jest.fn();
|
|
53
|
+
const errorFn = jest.fn();
|
|
54
|
+
|
|
61
55
|
await txnMgr.runTransactionStep({
|
|
62
56
|
func: func_complete,
|
|
63
57
|
projectId,
|
|
64
58
|
txId,
|
|
65
|
-
start: true
|
|
59
|
+
start: true,
|
|
60
|
+
successFn,
|
|
61
|
+
errorFn
|
|
66
62
|
});
|
|
67
63
|
expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
|
|
68
64
|
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
|
|
69
65
|
expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
expect(msg.payload.status).toBe(keyMirrors.statusResponse.success);
|
|
66
|
+
expect(successFn).toBeCalledTimes(1);
|
|
67
|
+
expect(errorFn).toBeCalledTimes(0);
|
|
74
68
|
});
|
|
75
69
|
|
|
76
70
|
test('Start a new transaction and complete in second step', async () => {
|
|
@@ -198,6 +192,10 @@ describe('Test Transaction Manager', () => {
|
|
|
198
192
|
test('Handle error in step function', async () => {
|
|
199
193
|
const txId = generateTxId();
|
|
200
194
|
const projectId = generateRandomProjectId();
|
|
195
|
+
|
|
196
|
+
const successFn = jest.fn();
|
|
197
|
+
const errorFn = jest.fn();
|
|
198
|
+
|
|
201
199
|
await txnMgr.runTransactionStep({
|
|
202
200
|
func: jest.fn().mockImplementation(() => {
|
|
203
201
|
throw new Error('Test error!');
|
|
@@ -205,15 +203,15 @@ describe('Test Transaction Manager', () => {
|
|
|
205
203
|
projectId,
|
|
206
204
|
txId,
|
|
207
205
|
start: true,
|
|
208
|
-
stepName: 'step1'
|
|
206
|
+
stepName: 'step1',
|
|
207
|
+
successFn,
|
|
208
|
+
errorFn
|
|
209
209
|
});
|
|
210
210
|
expect(txnMgr.isOngoingTransaction(txId)).toBe(false);
|
|
211
211
|
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(false);
|
|
212
212
|
expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
expect(msg.payload.status).toBe(keyMirrors.statusResponse.failure);
|
|
213
|
+
expect(successFn).toBeCalledTimes(0);
|
|
214
|
+
expect(errorFn).toBeCalledTimes(1);
|
|
217
215
|
});
|
|
218
216
|
|
|
219
217
|
test('Complete ongoing transaction', async () => {
|
|
@@ -369,4 +367,37 @@ describe('Test Transaction Manager', () => {
|
|
|
369
367
|
expect(txnMgr.isAnyOngoingTransaction()).toBe(false);
|
|
370
368
|
expect(txnMgr.getAppCfgUpdateFromTxID(txId)).toEqual(undefined);
|
|
371
369
|
});
|
|
370
|
+
|
|
371
|
+
test('store appContent', async () => {
|
|
372
|
+
const txId = generateTxId();
|
|
373
|
+
const projectId = generateRandomProjectId();
|
|
374
|
+
const appCfg: AppConfig = {
|
|
375
|
+
models: {},
|
|
376
|
+
scripts: { start: 'python app.py' }
|
|
377
|
+
};
|
|
378
|
+
const envVars: EnvVars = { alwaysai: { TEST: '1' } };
|
|
379
|
+
const cfgUpdate: AppContent = {
|
|
380
|
+
projectId: projectId,
|
|
381
|
+
appCfg: appCfg,
|
|
382
|
+
envVars: envVars
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
await txnMgr.runTransactionStep({
|
|
386
|
+
func: func_incomplete,
|
|
387
|
+
projectId,
|
|
388
|
+
txId,
|
|
389
|
+
start: true
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(txnMgr.getAppContentFromTxId(txId)).toEqual(undefined);
|
|
393
|
+
|
|
394
|
+
txnMgr.setAppContentToTx(txId, cfgUpdate);
|
|
395
|
+
|
|
396
|
+
expect(txnMgr.getProjectFromTransaction(txId)).toEqual(projectId);
|
|
397
|
+
expect(txnMgr.getTransactionFromProject(projectId)).toEqual(txId);
|
|
398
|
+
expect(txnMgr.isOngoingTransaction(txId)).toBe(true);
|
|
399
|
+
expect(txnMgr.isOngoingTransactionForProjectID(projectId)).toBe(true);
|
|
400
|
+
expect(txnMgr.isAnyOngoingTransaction()).toBe(true);
|
|
401
|
+
expect(txnMgr.getAppContentFromTxId(txId)).toEqual(cfgUpdate);
|
|
402
|
+
});
|
|
372
403
|
});
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
keyMirrors
|
|
5
5
|
} from '@alwaysai/device-agent-schemas';
|
|
6
6
|
import { LiveUpdatesHandler } from './live-updates-handler';
|
|
7
|
-
import { Publisher } from './publisher';
|
|
8
7
|
import { logger } from '../util/logger';
|
|
9
8
|
import { keyMirror, stringifyError } from 'alwaysai/lib/util';
|
|
10
9
|
import { CodedError } from '@carnesen/coded-error';
|
|
11
10
|
import { ShadowUpdate } from './shadow-handler';
|
|
11
|
+
import { AppContent } from './device-agent-message-handler';
|
|
12
12
|
|
|
13
13
|
interface TransactionDetails {
|
|
14
14
|
txId: string;
|
|
@@ -18,14 +18,17 @@ interface TransactionDetails {
|
|
|
18
18
|
update?: string;
|
|
19
19
|
stop?: string;
|
|
20
20
|
appCfgUpdate?: ShadowUpdate;
|
|
21
|
+
appContent?: AppContent;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
export type ErrorFunction = (txid: string, message: string) => void;
|
|
25
|
+
|
|
26
|
+
export type SuccessFunction = (txId: string) => void;
|
|
27
|
+
|
|
23
28
|
export class TransactionManager {
|
|
24
29
|
private detailsByTx: Record<string, TransactionDetails> = {};
|
|
25
30
|
private detailsByProject: Record<string, TransactionDetails> = {};
|
|
26
|
-
private liveUpdatesHandler: LiveUpdatesHandler;
|
|
27
|
-
private publisher: Publisher;
|
|
28
|
-
|
|
31
|
+
private readonly liveUpdatesHandler: LiveUpdatesHandler;
|
|
29
32
|
private async startTransaction(
|
|
30
33
|
txId: string,
|
|
31
34
|
projectId: string,
|
|
@@ -91,10 +94,9 @@ export class TransactionManager {
|
|
|
91
94
|
logger.info(`Updated transaction:\n${JSON.stringify(txDetails, null, 2)}`);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
constructor(
|
|
97
|
+
constructor(liveUpdatesHandler: LiveUpdatesHandler) {
|
|
95
98
|
this.detailsByTx = {};
|
|
96
99
|
this.detailsByProject = {};
|
|
97
|
-
this.publisher = publisher;
|
|
98
100
|
this.liveUpdatesHandler = liveUpdatesHandler;
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -111,9 +113,19 @@ export class TransactionManager {
|
|
|
111
113
|
start: boolean;
|
|
112
114
|
liveUpdatesPublishFn?: () => Promise<void>;
|
|
113
115
|
stepName?: string;
|
|
116
|
+
errorFn?: ErrorFunction;
|
|
117
|
+
successFn?: SuccessFunction;
|
|
114
118
|
}) {
|
|
115
|
-
const {
|
|
116
|
-
|
|
119
|
+
const {
|
|
120
|
+
func,
|
|
121
|
+
projectId,
|
|
122
|
+
txId,
|
|
123
|
+
start,
|
|
124
|
+
liveUpdatesPublishFn,
|
|
125
|
+
stepName,
|
|
126
|
+
errorFn,
|
|
127
|
+
successFn
|
|
128
|
+
} = props;
|
|
117
129
|
if (start) {
|
|
118
130
|
await this.startTransaction(
|
|
119
131
|
txId,
|
|
@@ -127,31 +139,20 @@ export class TransactionManager {
|
|
|
127
139
|
try {
|
|
128
140
|
const completed = await func();
|
|
129
141
|
if (completed) {
|
|
130
|
-
this.completeTransaction(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{ status: keyMirrors.statusResponse.success },
|
|
135
|
-
txId
|
|
136
|
-
)
|
|
137
|
-
);
|
|
142
|
+
this.completeTransaction(txId);
|
|
143
|
+
if (successFn) {
|
|
144
|
+
successFn(txId);
|
|
145
|
+
}
|
|
138
146
|
}
|
|
139
147
|
} catch (e) {
|
|
140
148
|
logger.error(
|
|
141
149
|
`Failed to execute cmd for ${projectId}!\n${stringifyError(e)}`
|
|
142
150
|
);
|
|
143
151
|
|
|
144
|
-
this.completeTransaction(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
{
|
|
149
|
-
status: keyMirrors.statusResponse.failure,
|
|
150
|
-
message: e.message
|
|
151
|
-
},
|
|
152
|
-
txId
|
|
153
|
-
)
|
|
154
|
-
);
|
|
152
|
+
this.completeTransaction(txId);
|
|
153
|
+
if (errorFn) {
|
|
154
|
+
errorFn(txId, e.message);
|
|
155
|
+
}
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -181,6 +182,10 @@ export class TransactionManager {
|
|
|
181
182
|
return this.detailsByTx[txId]?.appCfgUpdate;
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
public getAppContentFromTxId(txId: string): AppContent | undefined {
|
|
186
|
+
return this.detailsByTx[txId]?.appContent;
|
|
187
|
+
}
|
|
188
|
+
|
|
184
189
|
public setAppCfgUpdateToTx(txId: string, appCfgUpdate: ShadowUpdate) {
|
|
185
190
|
if (this.isOngoingTransaction(txId)) {
|
|
186
191
|
this.detailsByTx[txId].appCfgUpdate = appCfgUpdate;
|
|
@@ -190,6 +195,16 @@ export class TransactionManager {
|
|
|
190
195
|
);
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
public setAppContentToTx(txId: string, appContent: AppContent) {
|
|
199
|
+
if (this.isOngoingTransaction(txId)) {
|
|
200
|
+
logger.debug(`${txId}: Setting AppContent:${JSON.stringify(appContent)}`);
|
|
201
|
+
this.detailsByTx[txId].appContent = appContent;
|
|
202
|
+
} else
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Could not set appCfgUpdate, the transaction ${txId} does not exist.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
193
208
|
public completeTransaction(
|
|
194
209
|
txId: string,
|
|
195
210
|
messageToPublish?: ToClientStatusResponseMessage
|
|
@@ -212,9 +227,5 @@ export class TransactionManager {
|
|
|
212
227
|
keyMirrors.toClientMessageType.status_response,
|
|
213
228
|
txId
|
|
214
229
|
);
|
|
215
|
-
|
|
216
|
-
if (messageToPublish) {
|
|
217
|
-
this.publisher.publishToClient(messageToPublish);
|
|
218
|
-
}
|
|
219
230
|
}
|
|
220
231
|
}
|