@hatchet-dev/typescript-sdk 0.0.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/.eslintrc.json +36 -0
- package/.prettierrc.json +6 -0
- package/README.md +4 -0
- package/dist/hatchet/clients/admin/admin-client.d.ts +14 -0
- package/dist/hatchet/clients/admin/admin-client.js +50 -0
- package/dist/hatchet/clients/admin/admin-client.test.d.ts +1 -0
- package/dist/hatchet/clients/admin/admin-client.test.js +101 -0
- package/dist/hatchet/clients/dispatcher/action-listener.d.ts +24 -0
- package/dist/hatchet/clients/dispatcher/action-listener.js +113 -0
- package/dist/hatchet/clients/dispatcher/action-listener.test.d.ts +4 -0
- package/dist/hatchet/clients/dispatcher/action-listener.test.js +277 -0
- package/dist/hatchet/clients/dispatcher/dispatcher-client.d.ts +17 -0
- package/dist/hatchet/clients/dispatcher/dispatcher-client.js +46 -0
- package/dist/hatchet/clients/dispatcher/dispatcher-client.test.d.ts +1 -0
- package/dist/hatchet/clients/dispatcher/dispatcher-client.test.js +99 -0
- package/dist/hatchet/clients/event/event-client.d.ts +9 -0
- package/dist/hatchet/clients/event/event-client.js +28 -0
- package/dist/hatchet/clients/event/event-client.test.d.ts +1 -0
- package/dist/hatchet/clients/event/event-client.test.js +60 -0
- package/dist/hatchet/clients/hatchet-client/client-config.d.ts +72 -0
- package/dist/hatchet/clients/hatchet-client/client-config.js +17 -0
- package/dist/hatchet/clients/hatchet-client/hatchet-client.d.ts +26 -0
- package/dist/hatchet/clients/hatchet-client/hatchet-client.js +133 -0
- package/dist/hatchet/clients/hatchet-client/hatchet-client.test.d.ts +2 -0
- package/dist/hatchet/clients/hatchet-client/hatchet-client.test.js +135 -0
- package/dist/hatchet/clients/hatchet-client/index.d.ts +2 -0
- package/dist/hatchet/clients/hatchet-client/index.js +18 -0
- package/dist/hatchet/clients/worker/index.d.ts +1 -0
- package/dist/hatchet/clients/worker/index.js +17 -0
- package/dist/hatchet/clients/worker/worker.d.ts +31 -0
- package/dist/hatchet/clients/worker/worker.js +228 -0
- package/dist/hatchet/clients/worker/worker.test.d.ts +1 -0
- package/dist/hatchet/clients/worker/worker.test.js +256 -0
- package/dist/hatchet/index.d.ts +2 -0
- package/dist/hatchet/index.js +4 -0
- package/dist/hatchet/sdk.d.ts +2 -0
- package/dist/hatchet/sdk.js +4 -0
- package/dist/hatchet/step.d.ts +30 -0
- package/dist/hatchet/step.js +63 -0
- package/dist/hatchet/util/config-loader/config-loader.d.ts +13 -0
- package/dist/hatchet/util/config-loader/config-loader.js +85 -0
- package/dist/hatchet/util/config-loader/config-loader.test.d.ts +1 -0
- package/dist/hatchet/util/config-loader/config-loader.test.js +72 -0
- package/dist/hatchet/util/config-loader/index.d.ts +1 -0
- package/dist/hatchet/util/config-loader/index.js +17 -0
- package/dist/hatchet/util/errors/hatchet-error.d.ts +4 -0
- package/dist/hatchet/util/errors/hatchet-error.js +9 -0
- package/dist/hatchet/util/hatchet-promise/hatchet-promise.d.ts +6 -0
- package/dist/hatchet/util/hatchet-promise/hatchet-promise.js +12 -0
- package/dist/hatchet/util/hatchet-promise/hatchet-promise.test.d.ts +1 -0
- package/dist/hatchet/util/hatchet-promise/hatchet-promise.test.js +40 -0
- package/dist/hatchet/util/logger/index.d.ts +1 -0
- package/dist/hatchet/util/logger/index.js +17 -0
- package/dist/hatchet/util/logger/logger.d.ts +12 -0
- package/dist/hatchet/util/logger/logger.js +37 -0
- package/dist/hatchet/util/sleep.d.ts +2 -0
- package/dist/hatchet/util/sleep.js +6 -0
- package/dist/hatchet/workflow.d.ts +78 -0
- package/dist/hatchet/workflow.js +44 -0
- package/dist/protoc/dispatcher/dispatcher.d.ts +273 -0
- package/dist/protoc/dispatcher/dispatcher.js +918 -0
- package/dist/protoc/dispatcher/index.d.ts +1 -0
- package/dist/protoc/dispatcher/index.js +17 -0
- package/dist/protoc/events/events.d.ts +165 -0
- package/dist/protoc/events/events.js +443 -0
- package/dist/protoc/google/protobuf/timestamp.d.ts +121 -0
- package/dist/protoc/google/protobuf/timestamp.js +110 -0
- package/dist/protoc/google/protobuf/wrappers.d.ts +160 -0
- package/dist/protoc/google/protobuf/wrappers.js +527 -0
- package/dist/protoc/workflows/index.d.ts +1 -0
- package/dist/protoc/workflows/index.js +17 -0
- package/dist/protoc/workflows/workflows.d.ts +438 -0
- package/dist/protoc/workflows/workflows.js +1814 -0
- package/examples/dag-worker.ts +55 -0
- package/examples/example-event.ts +7 -0
- package/examples/simple-worker.ts +39 -0
- package/generate-protoc.sh +10 -0
- package/hatchet/clients/admin/admin-client.test.ts +116 -0
- package/hatchet/clients/admin/admin-client.ts +43 -0
- package/hatchet/clients/dispatcher/action-listener.test.ts +270 -0
- package/hatchet/clients/dispatcher/action-listener.ts +91 -0
- package/hatchet/clients/dispatcher/dispatcher-client.test.ts +111 -0
- package/hatchet/clients/dispatcher/dispatcher-client.ts +47 -0
- package/hatchet/clients/event/event-client.test.ts +72 -0
- package/hatchet/clients/event/event-client.ts +32 -0
- package/hatchet/clients/hatchet-client/client-config.ts +22 -0
- package/hatchet/clients/hatchet-client/fixtures/.hatchet-invalid.yaml +6 -0
- package/hatchet/clients/hatchet-client/fixtures/.hatchet.yaml +8 -0
- package/hatchet/clients/hatchet-client/hatchet-client.test.ts +162 -0
- package/hatchet/clients/hatchet-client/hatchet-client.ts +136 -0
- package/hatchet/clients/hatchet-client/index.ts +2 -0
- package/hatchet/clients/worker/index.ts +1 -0
- package/hatchet/clients/worker/worker.test.ts +347 -0
- package/hatchet/clients/worker/worker.ts +229 -0
- package/hatchet/index.ts +3 -0
- package/hatchet/sdk.ts +3 -0
- package/hatchet/step.ts +48 -0
- package/hatchet/util/config-loader/config-loader.test.ts +79 -0
- package/hatchet/util/config-loader/config-loader.ts +91 -0
- package/hatchet/util/config-loader/fixtures/.hatchet-invalid.yaml +6 -0
- package/hatchet/util/config-loader/fixtures/.hatchet.yaml +8 -0
- package/hatchet/util/config-loader/index.ts +1 -0
- package/hatchet/util/errors/hatchet-error.ts +8 -0
- package/hatchet/util/hatchet-promise/hatchet-promise.test.ts +32 -0
- package/hatchet/util/hatchet-promise/hatchet-promise.ts +13 -0
- package/hatchet/util/logger/index.ts +1 -0
- package/hatchet/util/logger/logger.ts +44 -0
- package/hatchet/util/sleep.ts +6 -0
- package/hatchet/workflow.ts +30 -0
- package/jest.config.ts +205 -0
- package/package.json +65 -0
- package/protoc/dispatcher/dispatcher.ts +1101 -0
- package/protoc/dispatcher/index.ts +1 -0
- package/protoc/events/events.ts +519 -0
- package/protoc/events/index.ts +1 -0
- package/protoc/google/protobuf/timestamp.ts +210 -0
- package/protoc/google/protobuf/wrappers.ts +657 -0
- package/protoc/workflows/index.ts +1 -0
- package/protoc/workflows/workflows.ts +2158 -0
- package/tsconfig.json +120 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Hatchet from '@hatchet/sdk';
|
|
2
|
+
import { Workflow } from '@hatchet/workflow';
|
|
3
|
+
|
|
4
|
+
const hatchet = Hatchet.init({
|
|
5
|
+
log_level: 'OFF',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const sleep = (ms: number) =>
|
|
9
|
+
new Promise((resolve) => {
|
|
10
|
+
setTimeout(resolve, ms);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const workflow: Workflow = {
|
|
14
|
+
id: 'dag-example',
|
|
15
|
+
description: 'test',
|
|
16
|
+
on: {
|
|
17
|
+
event: 'user:create',
|
|
18
|
+
},
|
|
19
|
+
steps: [
|
|
20
|
+
{
|
|
21
|
+
name: 'dag-step1',
|
|
22
|
+
run: async (ctx) => {
|
|
23
|
+
console.log('executed step1!');
|
|
24
|
+
await sleep(5000);
|
|
25
|
+
return { step1: 'step1' };
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'dag-step2',
|
|
30
|
+
parents: ['dag-step1'],
|
|
31
|
+
run: (ctx) => {
|
|
32
|
+
console.log('executed step2!');
|
|
33
|
+
return { step2: 'step2' };
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'dag-step3',
|
|
38
|
+
parents: ['dag-step1', 'dag-step2'],
|
|
39
|
+
run: (ctx) => {
|
|
40
|
+
console.log('executed step3!');
|
|
41
|
+
return { step3: 'step3' };
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'dag-step4',
|
|
46
|
+
parents: ['dag-step1', 'dag-step3'],
|
|
47
|
+
run: (ctx) => {
|
|
48
|
+
console.log('executed step4!');
|
|
49
|
+
return { step4: 'step4' };
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
hatchet.run(workflow);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Hatchet from '@hatchet/sdk';
|
|
2
|
+
import { Workflow } from '@hatchet/workflow';
|
|
3
|
+
|
|
4
|
+
const hatchet = Hatchet.init();
|
|
5
|
+
|
|
6
|
+
const sleep = (ms: number) =>
|
|
7
|
+
new Promise((resolve) => {
|
|
8
|
+
setTimeout(resolve, ms);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const workflow: Workflow = {
|
|
12
|
+
id: 'example',
|
|
13
|
+
description: 'test',
|
|
14
|
+
on: {
|
|
15
|
+
event: 'user:create',
|
|
16
|
+
},
|
|
17
|
+
steps: [
|
|
18
|
+
{
|
|
19
|
+
name: 'step1',
|
|
20
|
+
run: async (ctx) => {
|
|
21
|
+
console.log('starting step1 with the following input', ctx.workflowInput());
|
|
22
|
+
console.log('waiting 5 seconds...');
|
|
23
|
+
await sleep(5000);
|
|
24
|
+
console.log('executed step1!');
|
|
25
|
+
return { step1: 'step1 results!' };
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'step2',
|
|
30
|
+
parents: ['step1'],
|
|
31
|
+
run: (ctx) => {
|
|
32
|
+
console.log('executed step2 after step1 returned ', ctx.stepOutput('step1'));
|
|
33
|
+
return { step2: 'step2 results!' };
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
hatchet.run(workflow);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Directory to write generated code to (.js and .d.ts files)
|
|
2
|
+
OUT_DIR="./protoc"
|
|
3
|
+
|
|
4
|
+
# Generate code
|
|
5
|
+
./node_modules/.bin/grpc_tools_node_protoc \
|
|
6
|
+
--plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto \
|
|
7
|
+
--ts_proto_out=$OUT_DIR \
|
|
8
|
+
--ts_proto_opt=outputServices=nice-grpc,outputServices=generic-definitions,useExactTypes=false \
|
|
9
|
+
--proto_path=../api-contracts \
|
|
10
|
+
../api-contracts/**/*.proto
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { CreateWorkflowVersionOpts, WorkflowVersion } from '@protoc/workflows';
|
|
2
|
+
import { AdminClient } from './admin-client';
|
|
3
|
+
import { mockChannel, mockFactory } from '../hatchet-client/hatchet-client.test';
|
|
4
|
+
|
|
5
|
+
describe('AdminClient', () => {
|
|
6
|
+
let client: AdminClient;
|
|
7
|
+
|
|
8
|
+
it('should create a client', () => {
|
|
9
|
+
const x = new AdminClient(
|
|
10
|
+
{
|
|
11
|
+
token: 'TOKEN',
|
|
12
|
+
host_port: 'HOST_PORT',
|
|
13
|
+
tls_config: {
|
|
14
|
+
cert_file: 'TLS_CERT_FILE',
|
|
15
|
+
key_file: 'TLS_KEY_FILE',
|
|
16
|
+
ca_file: 'TLS_ROOT_CA_FILE',
|
|
17
|
+
server_name: 'TLS_SERVER_NAME',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
mockChannel,
|
|
21
|
+
mockFactory
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(x).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
client = new AdminClient(
|
|
29
|
+
{
|
|
30
|
+
token: 'TOKEN',
|
|
31
|
+
host_port: 'HOST_PORT',
|
|
32
|
+
tls_config: {
|
|
33
|
+
cert_file: 'TLS_CERT_FILE',
|
|
34
|
+
key_file: 'TLS_KEY_FILE',
|
|
35
|
+
ca_file: 'TLS_ROOT_CA_FILE',
|
|
36
|
+
server_name: 'TLS_SERVER_NAME',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
mockChannel,
|
|
40
|
+
mockFactory
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('put_workflow', () => {
|
|
45
|
+
it('should throw an error if no version and not auto version', async () => {
|
|
46
|
+
const workflow: CreateWorkflowVersionOpts = {
|
|
47
|
+
name: 'workflow1',
|
|
48
|
+
version: '',
|
|
49
|
+
description: 'description1',
|
|
50
|
+
eventTriggers: [],
|
|
51
|
+
cronTriggers: [],
|
|
52
|
+
scheduledTriggers: [],
|
|
53
|
+
jobs: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
expect(() => client.put_workflow(workflow, { autoVersion: false })).rejects.toThrow(
|
|
57
|
+
'PutWorkflow error: workflow version is required, or use autoVersion'
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should attempt to put the workflow', async () => {
|
|
62
|
+
const workflow: CreateWorkflowVersionOpts = {
|
|
63
|
+
name: 'workflow1',
|
|
64
|
+
version: 'v0.0.1',
|
|
65
|
+
description: 'description1',
|
|
66
|
+
eventTriggers: [],
|
|
67
|
+
cronTriggers: [],
|
|
68
|
+
scheduledTriggers: [],
|
|
69
|
+
jobs: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const putSpy = jest.spyOn(client.client, 'putWorkflow').mockResolvedValue({
|
|
73
|
+
id: 'workflow1',
|
|
74
|
+
version: 'v0.1.0',
|
|
75
|
+
order: 1,
|
|
76
|
+
workflowId: 'workflow1',
|
|
77
|
+
jobs: [],
|
|
78
|
+
createdAt: undefined,
|
|
79
|
+
updatedAt: undefined,
|
|
80
|
+
triggers: undefined,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await client.put_workflow(workflow);
|
|
84
|
+
|
|
85
|
+
expect(putSpy).toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('schedule_workflow', () => {
|
|
90
|
+
it('should schedule a workflow', () => {
|
|
91
|
+
const res: WorkflowVersion = {
|
|
92
|
+
id: 'string',
|
|
93
|
+
version: 'v0.0.1',
|
|
94
|
+
order: 1,
|
|
95
|
+
workflowId: 'string',
|
|
96
|
+
jobs: [],
|
|
97
|
+
createdAt: undefined,
|
|
98
|
+
updatedAt: undefined,
|
|
99
|
+
triggers: undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const spy = jest.spyOn(client.client, 'scheduleWorkflow').mockResolvedValue(res);
|
|
103
|
+
|
|
104
|
+
const now = new Date();
|
|
105
|
+
|
|
106
|
+
client.schedule_workflow('workflowId', {
|
|
107
|
+
schedules: [now],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(spy).toHaveBeenCalledWith({
|
|
111
|
+
workflowId: 'workflowId',
|
|
112
|
+
schedules: [now],
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Channel, ClientFactory } from 'nice-grpc';
|
|
2
|
+
import {
|
|
3
|
+
CreateWorkflowVersionOpts,
|
|
4
|
+
WorkflowServiceClient,
|
|
5
|
+
WorkflowServiceDefinition,
|
|
6
|
+
} from '@protoc/workflows';
|
|
7
|
+
import HatchetError from '@util/errors/hatchet-error';
|
|
8
|
+
import { ClientConfig } from '@clients/hatchet-client/client-config';
|
|
9
|
+
|
|
10
|
+
export class AdminClient {
|
|
11
|
+
config: ClientConfig;
|
|
12
|
+
client: WorkflowServiceClient;
|
|
13
|
+
|
|
14
|
+
constructor(config: ClientConfig, channel: Channel, factory: ClientFactory) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.client = factory.create(WorkflowServiceDefinition, channel);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async put_workflow(workflow: CreateWorkflowVersionOpts, options?: { autoVersion?: boolean }) {
|
|
20
|
+
if (workflow.version === '' && !options?.autoVersion) {
|
|
21
|
+
throw new HatchetError('PutWorkflow error: workflow version is required, or use autoVersion');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await this.client.putWorkflow({
|
|
26
|
+
opts: workflow,
|
|
27
|
+
});
|
|
28
|
+
} catch (e: any) {
|
|
29
|
+
throw new HatchetError(e.message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
schedule_workflow(workflowId: string, options?: { schedules?: Date[] }) {
|
|
34
|
+
try {
|
|
35
|
+
this.client.scheduleWorkflow({
|
|
36
|
+
workflowId,
|
|
37
|
+
schedules: options?.schedules,
|
|
38
|
+
});
|
|
39
|
+
} catch (e: any) {
|
|
40
|
+
throw new HatchetError(e.message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { ActionType, AssignedAction } from '@protoc/dispatcher';
|
|
2
|
+
import sleep from '@util/sleep';
|
|
3
|
+
import { ServerError, Status } from 'nice-grpc-common';
|
|
4
|
+
import { DispatcherClient } from './dispatcher-client';
|
|
5
|
+
import { ActionListener } from './action-listener';
|
|
6
|
+
import { mockChannel, mockFactory } from '../hatchet-client/hatchet-client.test';
|
|
7
|
+
|
|
8
|
+
let dispatcher: DispatcherClient;
|
|
9
|
+
|
|
10
|
+
type AssignActionMock = AssignedAction | Error;
|
|
11
|
+
|
|
12
|
+
// Mock data for AssignedAction
|
|
13
|
+
const mockAssignedActions: AssignActionMock[] = [
|
|
14
|
+
{
|
|
15
|
+
tenantId: 'tenant1',
|
|
16
|
+
jobId: 'job1',
|
|
17
|
+
jobName: 'Job One',
|
|
18
|
+
jobRunId: 'run1',
|
|
19
|
+
stepId: 'step1',
|
|
20
|
+
stepRunId: 'runStep1',
|
|
21
|
+
actionId: 'action1',
|
|
22
|
+
actionType: ActionType.START_STEP_RUN,
|
|
23
|
+
actionPayload: 'payload1',
|
|
24
|
+
},
|
|
25
|
+
// ... Add more mock AssignedAction objects as needed
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Mock implementation of the listener
|
|
29
|
+
export const mockListener = (fixture: AssignActionMock[]) =>
|
|
30
|
+
(async function* gen() {
|
|
31
|
+
for (const action of fixture) {
|
|
32
|
+
// Simulate asynchronous behavior
|
|
33
|
+
await sleep(100);
|
|
34
|
+
|
|
35
|
+
if (action instanceof Error) {
|
|
36
|
+
throw action;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
yield action;
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
|
|
43
|
+
describe('ActionListener', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
dispatcher = new DispatcherClient(
|
|
46
|
+
{
|
|
47
|
+
token: 'TOKEN',
|
|
48
|
+
|
|
49
|
+
host_port: 'HOST_PORT',
|
|
50
|
+
log_level: 'OFF',
|
|
51
|
+
tls_config: {
|
|
52
|
+
cert_file: 'TLS_CERT_FILE',
|
|
53
|
+
key_file: 'TLS_KEY_FILE',
|
|
54
|
+
ca_file: 'TLS_ROOT_CA_FILE',
|
|
55
|
+
server_name: 'TLS_SERVER_NAME',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
mockChannel,
|
|
59
|
+
mockFactory
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should create a client', async () => {
|
|
64
|
+
const listener = new ActionListener(dispatcher, mockListener(mockAssignedActions), 'WORKER_ID');
|
|
65
|
+
expect(listener).toBeDefined();
|
|
66
|
+
expect(listener.workerId).toEqual('WORKER_ID');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('actions', () => {
|
|
70
|
+
it('it should "yield" actions', async () => {
|
|
71
|
+
const listener = new ActionListener(
|
|
72
|
+
dispatcher,
|
|
73
|
+
mockListener([...mockAssignedActions, new ServerError(Status.CANCELLED, 'CANCELLED')]),
|
|
74
|
+
'WORKER_ID'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const actions = listener.actions();
|
|
78
|
+
const res = [];
|
|
79
|
+
for await (const action of actions) {
|
|
80
|
+
res.push(action);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
expect(res[0]).toEqual({
|
|
84
|
+
tenantId: 'tenant1',
|
|
85
|
+
jobId: 'job1',
|
|
86
|
+
jobName: 'Job One',
|
|
87
|
+
jobRunId: 'run1',
|
|
88
|
+
stepId: 'step1',
|
|
89
|
+
stepRunId: 'runStep1',
|
|
90
|
+
actionId: 'action1',
|
|
91
|
+
actionType: ActionType.START_STEP_RUN,
|
|
92
|
+
actionPayload: 'payload1',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('it should break on grpc CANCELLED', async () => {
|
|
97
|
+
const listener = new ActionListener(
|
|
98
|
+
dispatcher,
|
|
99
|
+
mockListener([...mockAssignedActions, new ServerError(Status.CANCELLED, 'CANCELLED')]),
|
|
100
|
+
'WORKER_ID'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const actions = listener.actions();
|
|
104
|
+
const retrySpy = jest.spyOn(listener, 'retrySubscribe').mockResolvedValue(undefined);
|
|
105
|
+
|
|
106
|
+
const res = [];
|
|
107
|
+
for await (const action of actions) {
|
|
108
|
+
res.push(action);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expect(res.length).toEqual(1);
|
|
112
|
+
expect(retrySpy).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('it should break on unknown error', async () => {
|
|
116
|
+
const listener = new ActionListener(
|
|
117
|
+
dispatcher,
|
|
118
|
+
mockListener([...mockAssignedActions, new Error('Simulated error')]),
|
|
119
|
+
'WORKER_ID'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const actions = listener.actions();
|
|
123
|
+
const retrySpy = jest.spyOn(listener, 'retrySubscribe').mockResolvedValue(undefined);
|
|
124
|
+
|
|
125
|
+
const res = [];
|
|
126
|
+
for await (const action of actions) {
|
|
127
|
+
res.push(action);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(res.length).toEqual(1);
|
|
131
|
+
expect(retrySpy).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('it should attempt to re-establish connection on grpc UNAVAILABLE', async () => {
|
|
135
|
+
const listener = new ActionListener(
|
|
136
|
+
dispatcher,
|
|
137
|
+
mockListener([...mockAssignedActions, new ServerError(Status.UNAVAILABLE, 'UNAVAILABLE')]),
|
|
138
|
+
'WORKER_ID'
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const retrySpy = jest.spyOn(listener, 'retrySubscribe').mockResolvedValue(undefined);
|
|
142
|
+
|
|
143
|
+
const actions = listener.actions();
|
|
144
|
+
|
|
145
|
+
const res = [];
|
|
146
|
+
for await (const action of actions) {
|
|
147
|
+
res.push(action);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
expect(res.length).toEqual(1);
|
|
151
|
+
expect(retrySpy).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('retry_subscribe', () => {
|
|
156
|
+
it('should exit after successful connection', async () => {
|
|
157
|
+
const listener = new ActionListener(
|
|
158
|
+
dispatcher,
|
|
159
|
+
mockListener(mockAssignedActions),
|
|
160
|
+
'WORKER_ID'
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Mock the listener to throw an error on the first call
|
|
164
|
+
const listenSpy = jest
|
|
165
|
+
.spyOn(listener.client, 'listen')
|
|
166
|
+
.mockReturnValue(mockListener(mockAssignedActions));
|
|
167
|
+
|
|
168
|
+
await listener.retrySubscribe();
|
|
169
|
+
|
|
170
|
+
expect(listenSpy).toHaveBeenCalledTimes(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should retry until success', async () => {
|
|
174
|
+
const listener = new ActionListener(
|
|
175
|
+
dispatcher,
|
|
176
|
+
mockListener(mockAssignedActions),
|
|
177
|
+
'WORKER_ID'
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Mock the listener to throw an error on the first call
|
|
181
|
+
const listenSpy = jest
|
|
182
|
+
.spyOn(listener.client, 'listen')
|
|
183
|
+
.mockImplementationOnce(() => {
|
|
184
|
+
throw new Error('Simulated error');
|
|
185
|
+
})
|
|
186
|
+
.mockImplementationOnce(() => mockListener(mockAssignedActions));
|
|
187
|
+
|
|
188
|
+
await expect(async () => {
|
|
189
|
+
await listener.retrySubscribe();
|
|
190
|
+
}).not.toThrow();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should not throw an error if successful', async () => {
|
|
194
|
+
const listener = new ActionListener(
|
|
195
|
+
dispatcher,
|
|
196
|
+
mockListener(mockAssignedActions),
|
|
197
|
+
'WORKER_ID'
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Mock the listener to throw an error on the first call
|
|
201
|
+
const listenSpy = jest
|
|
202
|
+
.spyOn(listener.client, 'listen')
|
|
203
|
+
.mockImplementationOnce(() => {
|
|
204
|
+
throw new Error('Simulated error');
|
|
205
|
+
})
|
|
206
|
+
.mockImplementationOnce(() => mockListener(mockAssignedActions));
|
|
207
|
+
|
|
208
|
+
await listener.retrySubscribe();
|
|
209
|
+
expect(listenSpy).toHaveBeenCalledTimes(2);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should retry at most COUNT times and throw an error', async () => {
|
|
213
|
+
const listener = new ActionListener(
|
|
214
|
+
dispatcher,
|
|
215
|
+
mockListener(mockAssignedActions),
|
|
216
|
+
'WORKER_ID'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Mock the listener to throw an error on the first call
|
|
220
|
+
const listenSpy = jest
|
|
221
|
+
.spyOn(listener.client, 'listen')
|
|
222
|
+
.mockImplementationOnce(() => {
|
|
223
|
+
throw new Error('Simulated error');
|
|
224
|
+
})
|
|
225
|
+
.mockImplementationOnce(() => {
|
|
226
|
+
throw new Error('Simulated error');
|
|
227
|
+
})
|
|
228
|
+
.mockImplementationOnce(() => {
|
|
229
|
+
throw new Error('Simulated error');
|
|
230
|
+
})
|
|
231
|
+
.mockImplementationOnce(() => {
|
|
232
|
+
throw new Error('Simulated error');
|
|
233
|
+
})
|
|
234
|
+
.mockImplementationOnce(() => {
|
|
235
|
+
throw new Error('Simulated error');
|
|
236
|
+
})
|
|
237
|
+
.mockImplementationOnce(() => {
|
|
238
|
+
throw new Error('Simulated error');
|
|
239
|
+
})
|
|
240
|
+
.mockImplementationOnce(() => mockListener(mockAssignedActions));
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await listener.retrySubscribe();
|
|
244
|
+
expect(listenSpy).toHaveBeenCalledTimes(5);
|
|
245
|
+
} catch (e: any) {
|
|
246
|
+
expect(e.message).toEqual(`Could not subscribe to the worker after 5 retries`);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('unregister', () => {
|
|
252
|
+
it('should unsubscribe itself', async () => {
|
|
253
|
+
const listener = new ActionListener(
|
|
254
|
+
dispatcher,
|
|
255
|
+
mockListener(mockAssignedActions),
|
|
256
|
+
'WORKER_ID'
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const unsubscribeSpy = jest.spyOn(listener.client, 'unsubscribe').mockResolvedValue({
|
|
260
|
+
tenantId: 'TENANT_ID',
|
|
261
|
+
workerId: 'WORKER_ID',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const res = await listener.unregister();
|
|
265
|
+
|
|
266
|
+
expect(unsubscribeSpy).toHaveBeenCalled();
|
|
267
|
+
expect(res.workerId).toEqual('WORKER_ID');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DispatcherClient as PbDispatcherClient, AssignedAction } from '@protoc/dispatcher';
|
|
2
|
+
|
|
3
|
+
import { Status } from 'nice-grpc';
|
|
4
|
+
import { ClientConfig } from '@clients/hatchet-client/client-config';
|
|
5
|
+
import sleep from '@util/sleep';
|
|
6
|
+
import HatchetError from '@util/errors/hatchet-error';
|
|
7
|
+
import { DispatcherClient } from './dispatcher-client';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ACTION_LISTENER_RETRY_INTERVAL = 5; // seconds
|
|
10
|
+
const DEFAULT_ACTION_LISTENER_RETRY_COUNT = 5;
|
|
11
|
+
|
|
12
|
+
export interface Action {
|
|
13
|
+
tenantId: string;
|
|
14
|
+
jobId: string;
|
|
15
|
+
jobName: string;
|
|
16
|
+
jobRunId: string;
|
|
17
|
+
stepId: string;
|
|
18
|
+
stepRunId: string;
|
|
19
|
+
actionId: string;
|
|
20
|
+
actionType: number;
|
|
21
|
+
actionPayload: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ActionListener {
|
|
25
|
+
config: ClientConfig;
|
|
26
|
+
client: PbDispatcherClient;
|
|
27
|
+
listener: AsyncIterable<AssignedAction>;
|
|
28
|
+
workerId: string;
|
|
29
|
+
|
|
30
|
+
constructor(client: DispatcherClient, listener: AsyncIterable<AssignedAction>, workerId: string) {
|
|
31
|
+
this.config = client.config;
|
|
32
|
+
this.client = client.client;
|
|
33
|
+
this.listener = listener;
|
|
34
|
+
this.workerId = workerId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
actions = () =>
|
|
38
|
+
(async function* gen(client: ActionListener) {
|
|
39
|
+
while (true) {
|
|
40
|
+
try {
|
|
41
|
+
for await (const assignedAction of client.listener) {
|
|
42
|
+
const action: Action = {
|
|
43
|
+
...assignedAction,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
yield action;
|
|
47
|
+
}
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
if (e.code === Status.CANCELLED) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (e.code === Status.UNAVAILABLE) {
|
|
53
|
+
client.retrySubscribe();
|
|
54
|
+
}
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})(this);
|
|
59
|
+
|
|
60
|
+
async retrySubscribe() {
|
|
61
|
+
let retries = 0;
|
|
62
|
+
|
|
63
|
+
while (retries < DEFAULT_ACTION_LISTENER_RETRY_COUNT) {
|
|
64
|
+
try {
|
|
65
|
+
await sleep(DEFAULT_ACTION_LISTENER_RETRY_INTERVAL);
|
|
66
|
+
|
|
67
|
+
this.listener = this.client.listen({
|
|
68
|
+
workerId: this.workerId,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return;
|
|
72
|
+
} catch (e: any) {
|
|
73
|
+
retries += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new HatchetError(
|
|
78
|
+
`Could not subscribe to the worker after ${DEFAULT_ACTION_LISTENER_RETRY_COUNT} retries`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async unregister() {
|
|
83
|
+
try {
|
|
84
|
+
return this.client.unsubscribe({
|
|
85
|
+
workerId: this.workerId,
|
|
86
|
+
});
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
throw new HatchetError(`Failed to unsubscribe: ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|