@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.
Files changed (120) hide show
  1. package/.eslintrc.json +36 -0
  2. package/.prettierrc.json +6 -0
  3. package/README.md +4 -0
  4. package/dist/hatchet/clients/admin/admin-client.d.ts +14 -0
  5. package/dist/hatchet/clients/admin/admin-client.js +50 -0
  6. package/dist/hatchet/clients/admin/admin-client.test.d.ts +1 -0
  7. package/dist/hatchet/clients/admin/admin-client.test.js +101 -0
  8. package/dist/hatchet/clients/dispatcher/action-listener.d.ts +24 -0
  9. package/dist/hatchet/clients/dispatcher/action-listener.js +113 -0
  10. package/dist/hatchet/clients/dispatcher/action-listener.test.d.ts +4 -0
  11. package/dist/hatchet/clients/dispatcher/action-listener.test.js +277 -0
  12. package/dist/hatchet/clients/dispatcher/dispatcher-client.d.ts +17 -0
  13. package/dist/hatchet/clients/dispatcher/dispatcher-client.js +46 -0
  14. package/dist/hatchet/clients/dispatcher/dispatcher-client.test.d.ts +1 -0
  15. package/dist/hatchet/clients/dispatcher/dispatcher-client.test.js +99 -0
  16. package/dist/hatchet/clients/event/event-client.d.ts +9 -0
  17. package/dist/hatchet/clients/event/event-client.js +28 -0
  18. package/dist/hatchet/clients/event/event-client.test.d.ts +1 -0
  19. package/dist/hatchet/clients/event/event-client.test.js +60 -0
  20. package/dist/hatchet/clients/hatchet-client/client-config.d.ts +72 -0
  21. package/dist/hatchet/clients/hatchet-client/client-config.js +17 -0
  22. package/dist/hatchet/clients/hatchet-client/hatchet-client.d.ts +26 -0
  23. package/dist/hatchet/clients/hatchet-client/hatchet-client.js +133 -0
  24. package/dist/hatchet/clients/hatchet-client/hatchet-client.test.d.ts +2 -0
  25. package/dist/hatchet/clients/hatchet-client/hatchet-client.test.js +135 -0
  26. package/dist/hatchet/clients/hatchet-client/index.d.ts +2 -0
  27. package/dist/hatchet/clients/hatchet-client/index.js +18 -0
  28. package/dist/hatchet/clients/worker/index.d.ts +1 -0
  29. package/dist/hatchet/clients/worker/index.js +17 -0
  30. package/dist/hatchet/clients/worker/worker.d.ts +31 -0
  31. package/dist/hatchet/clients/worker/worker.js +228 -0
  32. package/dist/hatchet/clients/worker/worker.test.d.ts +1 -0
  33. package/dist/hatchet/clients/worker/worker.test.js +256 -0
  34. package/dist/hatchet/index.d.ts +2 -0
  35. package/dist/hatchet/index.js +4 -0
  36. package/dist/hatchet/sdk.d.ts +2 -0
  37. package/dist/hatchet/sdk.js +4 -0
  38. package/dist/hatchet/step.d.ts +30 -0
  39. package/dist/hatchet/step.js +63 -0
  40. package/dist/hatchet/util/config-loader/config-loader.d.ts +13 -0
  41. package/dist/hatchet/util/config-loader/config-loader.js +85 -0
  42. package/dist/hatchet/util/config-loader/config-loader.test.d.ts +1 -0
  43. package/dist/hatchet/util/config-loader/config-loader.test.js +72 -0
  44. package/dist/hatchet/util/config-loader/index.d.ts +1 -0
  45. package/dist/hatchet/util/config-loader/index.js +17 -0
  46. package/dist/hatchet/util/errors/hatchet-error.d.ts +4 -0
  47. package/dist/hatchet/util/errors/hatchet-error.js +9 -0
  48. package/dist/hatchet/util/hatchet-promise/hatchet-promise.d.ts +6 -0
  49. package/dist/hatchet/util/hatchet-promise/hatchet-promise.js +12 -0
  50. package/dist/hatchet/util/hatchet-promise/hatchet-promise.test.d.ts +1 -0
  51. package/dist/hatchet/util/hatchet-promise/hatchet-promise.test.js +40 -0
  52. package/dist/hatchet/util/logger/index.d.ts +1 -0
  53. package/dist/hatchet/util/logger/index.js +17 -0
  54. package/dist/hatchet/util/logger/logger.d.ts +12 -0
  55. package/dist/hatchet/util/logger/logger.js +37 -0
  56. package/dist/hatchet/util/sleep.d.ts +2 -0
  57. package/dist/hatchet/util/sleep.js +6 -0
  58. package/dist/hatchet/workflow.d.ts +78 -0
  59. package/dist/hatchet/workflow.js +44 -0
  60. package/dist/protoc/dispatcher/dispatcher.d.ts +273 -0
  61. package/dist/protoc/dispatcher/dispatcher.js +918 -0
  62. package/dist/protoc/dispatcher/index.d.ts +1 -0
  63. package/dist/protoc/dispatcher/index.js +17 -0
  64. package/dist/protoc/events/events.d.ts +165 -0
  65. package/dist/protoc/events/events.js +443 -0
  66. package/dist/protoc/google/protobuf/timestamp.d.ts +121 -0
  67. package/dist/protoc/google/protobuf/timestamp.js +110 -0
  68. package/dist/protoc/google/protobuf/wrappers.d.ts +160 -0
  69. package/dist/protoc/google/protobuf/wrappers.js +527 -0
  70. package/dist/protoc/workflows/index.d.ts +1 -0
  71. package/dist/protoc/workflows/index.js +17 -0
  72. package/dist/protoc/workflows/workflows.d.ts +438 -0
  73. package/dist/protoc/workflows/workflows.js +1814 -0
  74. package/examples/dag-worker.ts +55 -0
  75. package/examples/example-event.ts +7 -0
  76. package/examples/simple-worker.ts +39 -0
  77. package/generate-protoc.sh +10 -0
  78. package/hatchet/clients/admin/admin-client.test.ts +116 -0
  79. package/hatchet/clients/admin/admin-client.ts +43 -0
  80. package/hatchet/clients/dispatcher/action-listener.test.ts +270 -0
  81. package/hatchet/clients/dispatcher/action-listener.ts +91 -0
  82. package/hatchet/clients/dispatcher/dispatcher-client.test.ts +111 -0
  83. package/hatchet/clients/dispatcher/dispatcher-client.ts +47 -0
  84. package/hatchet/clients/event/event-client.test.ts +72 -0
  85. package/hatchet/clients/event/event-client.ts +32 -0
  86. package/hatchet/clients/hatchet-client/client-config.ts +22 -0
  87. package/hatchet/clients/hatchet-client/fixtures/.hatchet-invalid.yaml +6 -0
  88. package/hatchet/clients/hatchet-client/fixtures/.hatchet.yaml +8 -0
  89. package/hatchet/clients/hatchet-client/hatchet-client.test.ts +162 -0
  90. package/hatchet/clients/hatchet-client/hatchet-client.ts +136 -0
  91. package/hatchet/clients/hatchet-client/index.ts +2 -0
  92. package/hatchet/clients/worker/index.ts +1 -0
  93. package/hatchet/clients/worker/worker.test.ts +347 -0
  94. package/hatchet/clients/worker/worker.ts +229 -0
  95. package/hatchet/index.ts +3 -0
  96. package/hatchet/sdk.ts +3 -0
  97. package/hatchet/step.ts +48 -0
  98. package/hatchet/util/config-loader/config-loader.test.ts +79 -0
  99. package/hatchet/util/config-loader/config-loader.ts +91 -0
  100. package/hatchet/util/config-loader/fixtures/.hatchet-invalid.yaml +6 -0
  101. package/hatchet/util/config-loader/fixtures/.hatchet.yaml +8 -0
  102. package/hatchet/util/config-loader/index.ts +1 -0
  103. package/hatchet/util/errors/hatchet-error.ts +8 -0
  104. package/hatchet/util/hatchet-promise/hatchet-promise.test.ts +32 -0
  105. package/hatchet/util/hatchet-promise/hatchet-promise.ts +13 -0
  106. package/hatchet/util/logger/index.ts +1 -0
  107. package/hatchet/util/logger/logger.ts +44 -0
  108. package/hatchet/util/sleep.ts +6 -0
  109. package/hatchet/workflow.ts +30 -0
  110. package/jest.config.ts +205 -0
  111. package/package.json +65 -0
  112. package/protoc/dispatcher/dispatcher.ts +1101 -0
  113. package/protoc/dispatcher/index.ts +1 -0
  114. package/protoc/events/events.ts +519 -0
  115. package/protoc/events/index.ts +1 -0
  116. package/protoc/google/protobuf/timestamp.ts +210 -0
  117. package/protoc/google/protobuf/wrappers.ts +657 -0
  118. package/protoc/workflows/index.ts +1 -0
  119. package/protoc/workflows/workflows.ts +2158 -0
  120. package/tsconfig.json +120 -0
@@ -0,0 +1,347 @@
1
+ import { HatchetClient } from '@clients/hatchet-client';
2
+ import { ActionEventType, ActionType, AssignedAction } from '@protoc/dispatcher';
3
+ import { ActionListener } from '@clients/dispatcher/action-listener';
4
+ import { ServerError, Status } from 'nice-grpc-common';
5
+ import { mockListener } from '@clients/dispatcher/action-listener.test';
6
+ import { never } from 'zod';
7
+ import sleep from '@util/sleep';
8
+ import { ChannelCredentials } from 'nice-grpc';
9
+ import { Worker } from './worker';
10
+ import { Context } from '@hatchet/step';
11
+
12
+ type AssignActionMock = AssignedAction | Error;
13
+
14
+ const mockStart: AssignActionMock = {
15
+ tenantId: 'TENANT_ID',
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: JSON.stringify('{"input": {"data": 1}}'),
24
+ };
25
+
26
+ const mockCancel: AssignActionMock = {
27
+ ...mockStart,
28
+ actionType: ActionType.CANCEL_STEP_RUN,
29
+ };
30
+
31
+ describe('Worker', () => {
32
+ let hatchet: HatchetClient;
33
+
34
+ beforeEach(() => {
35
+ hatchet = new HatchetClient(
36
+ {
37
+ token: 'TOKEN',
38
+ log_level: 'OFF',
39
+ host_port: 'HOST_PORT',
40
+ tls_config: {
41
+ cert_file: 'TLS_CERT_FILE',
42
+ key_file: 'TLS_KEY_FILE',
43
+ ca_file: 'TLS_ROOT_CA_FILE',
44
+ server_name: 'TLS_SERVER_NAME',
45
+ },
46
+ },
47
+ {
48
+ credentials: ChannelCredentials.createInsecure(),
49
+ }
50
+ );
51
+ });
52
+
53
+ describe('register_workflow', () => {
54
+ it('should update the registry', async () => {
55
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
56
+ const putWorkflowSpy = jest.spyOn(worker.client.admin, 'put_workflow').mockResolvedValue();
57
+
58
+ const workflow = {
59
+ id: 'workflow1',
60
+ description: 'test',
61
+ on: {
62
+ event: 'user:create',
63
+ },
64
+ steps: [
65
+ {
66
+ name: 'step1',
67
+ run: (ctx: any) => {
68
+ return { test: 'test' };
69
+ },
70
+ },
71
+ ],
72
+ };
73
+
74
+ await worker.register_workflow(workflow);
75
+
76
+ expect(putWorkflowSpy).toHaveBeenCalledTimes(1);
77
+
78
+ expect(worker.action_registry).toEqual({
79
+ [`default:step1`]: workflow.steps[0].run,
80
+ });
81
+ });
82
+ });
83
+
84
+ describe('handle_start_step_run', () => {
85
+ it('should start a step run', async () => {
86
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
87
+
88
+ const putWorkflowSpy = jest.spyOn(worker.client.admin, 'put_workflow').mockResolvedValue();
89
+
90
+ const getActionEventSpy = jest.spyOn(worker, 'get_action_event');
91
+
92
+ const sendActionEventSpy = jest
93
+ .spyOn(worker.client.dispatcher, 'send_action_event')
94
+ .mockResolvedValue({
95
+ tenantId: 'TENANT_ID',
96
+ workerId: 'WORKER_ID',
97
+ });
98
+
99
+ const startSpy = jest.fn().mockReturnValue({ data: 4 });
100
+
101
+ worker.action_registry = {
102
+ [mockStart.actionId]: startSpy,
103
+ };
104
+
105
+ worker.handle_start_step_run(mockStart);
106
+ await sleep(100);
107
+
108
+ expect(startSpy).toHaveBeenCalledTimes(1);
109
+
110
+ expect(getActionEventSpy).toHaveBeenNthCalledWith(
111
+ 2,
112
+ expect.anything(),
113
+ ActionEventType.STEP_EVENT_TYPE_COMPLETED,
114
+ { data: 4 }
115
+ );
116
+ expect(worker.futures[mockStart.stepRunId]).toBeUndefined();
117
+ expect(sendActionEventSpy).toHaveBeenCalledTimes(2);
118
+ });
119
+
120
+ it('should fail gracefully', async () => {
121
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
122
+
123
+ const getActionEventSpy = jest.spyOn(worker, 'get_action_event');
124
+
125
+ const sendActionEventSpy = jest
126
+ .spyOn(worker.client.dispatcher, 'send_action_event')
127
+ .mockResolvedValue({
128
+ tenantId: 'TENANT_ID',
129
+ workerId: 'WORKER_ID',
130
+ });
131
+
132
+ const startSpy = jest.fn().mockRejectedValue(new Error('ERROR'));
133
+
134
+ worker.action_registry = {
135
+ [mockStart.actionId]: startSpy,
136
+ };
137
+
138
+ worker.handle_start_step_run(mockStart);
139
+ await sleep(100);
140
+
141
+ expect(startSpy).toHaveBeenCalledTimes(1);
142
+ expect(getActionEventSpy).toHaveBeenNthCalledWith(
143
+ 2,
144
+ expect.anything(),
145
+ ActionEventType.STEP_EVENT_TYPE_FAILED,
146
+ expect.anything()
147
+ );
148
+ expect(worker.futures[mockStart.stepRunId]).toBeUndefined();
149
+ expect(sendActionEventSpy).toHaveBeenCalledTimes(2);
150
+ });
151
+ });
152
+
153
+ describe('handle_cancel_step_run', () => {
154
+ it('should cancel a step run', () => {
155
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
156
+
157
+ const cancelSpy = jest.fn().mockReturnValue(undefined);
158
+
159
+ worker.futures = {
160
+ [mockCancel.stepRunId]: {
161
+ cancel: cancelSpy,
162
+ } as any,
163
+ };
164
+
165
+ worker.handle_cancel_step_run(mockCancel);
166
+
167
+ expect(cancelSpy).toHaveBeenCalledTimes(1);
168
+ expect(worker.futures[mockCancel.stepRunId]).toBeUndefined();
169
+ });
170
+ });
171
+
172
+ describe('exit_gracefully', () => {
173
+ xit('should call exit_gracefully on SIGTERM', async () => {
174
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
175
+
176
+ // the spy is not working and the test is killing the test process
177
+ const exitSpy = jest.spyOn(worker, 'exit_gracefully').mockImplementationOnce(() => {
178
+ throw new Error('Simulated error');
179
+ });
180
+
181
+ process.emit('SIGTERM', 'SIGTERM');
182
+ expect(exitSpy).toHaveBeenCalledTimes(1);
183
+ });
184
+
185
+ xit('should call exit_gracefully on SIGINT', async () => {
186
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
187
+
188
+ // This is killing the process (as it should) fix the spy at some point
189
+ const exitSpy = jest.spyOn(worker, 'exit_gracefully').mockResolvedValue();
190
+
191
+ process.emit('SIGINT', 'SIGINT');
192
+ expect(exitSpy).toHaveBeenCalledTimes(1);
193
+ });
194
+
195
+ xit('should unregister the listener and exit', async () => {
196
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
197
+
198
+ jest.spyOn(process, 'exit').mockImplementation((number) => {
199
+ throw new Error(`EXIT ${number}`);
200
+ }); // This is killing the process (as it should) fix the spy at some point
201
+
202
+ const mockActionListener = new ActionListener(
203
+ hatchet.dispatcher,
204
+ mockListener([mockStart, mockStart, new ServerError(Status.CANCELLED, 'CANCELLED')]),
205
+ 'WORKER_ID'
206
+ );
207
+
208
+ mockActionListener.unregister = jest.fn().mockResolvedValue(never());
209
+ worker.listener = mockActionListener;
210
+
211
+ expect(async () => {
212
+ await worker.exit_gracefully();
213
+ }).toThrow('EXIT 0');
214
+ expect(mockActionListener.unregister).toHaveBeenCalledTimes(1);
215
+ });
216
+
217
+ it('should exit the process if handle_kill is true', async () => {
218
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
219
+ const exitSpy = jest.spyOn(process, 'exit').mockReturnValue(undefined as never);
220
+ await worker.exit_gracefully();
221
+ expect(exitSpy).toHaveBeenCalledTimes(1);
222
+ });
223
+ });
224
+
225
+ describe('start', () => {
226
+ xit('should get actions and start runs', async () => {
227
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
228
+
229
+ const startSpy = jest.spyOn(worker, 'handle_start_step_run').mockReturnValue();
230
+ const cancelSpy = jest.spyOn(worker, 'handle_cancel_step_run').mockReturnValue();
231
+
232
+ const mockActionListener = new ActionListener(
233
+ hatchet.dispatcher,
234
+ mockListener([mockStart, mockStart, new ServerError(Status.CANCELLED, 'CANCELLED')]),
235
+ 'WORKER_ID'
236
+ );
237
+
238
+ const getActionListenerSpy = jest
239
+ .spyOn(worker.client.dispatcher, 'get_action_listener')
240
+ .mockResolvedValue(mockActionListener);
241
+
242
+ await worker.start();
243
+
244
+ expect(getActionListenerSpy).toHaveBeenCalledTimes(1);
245
+ expect(startSpy).toHaveBeenCalledTimes(2);
246
+ expect(cancelSpy).toHaveBeenCalledTimes(0);
247
+ });
248
+
249
+ it('should get actions and cancel runs', async () => {
250
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
251
+
252
+ const startSpy = jest.spyOn(worker, 'handle_start_step_run').mockReturnValue();
253
+ const cancelSpy = jest.spyOn(worker, 'handle_cancel_step_run').mockReturnValue();
254
+
255
+ const mockActionListner = new ActionListener(
256
+ hatchet.dispatcher,
257
+ mockListener([mockStart, mockCancel, new ServerError(Status.CANCELLED, 'CANCELLED')]),
258
+ 'WORKER_ID'
259
+ );
260
+
261
+ const getActionListenerSpy = jest
262
+ .spyOn(worker.client.dispatcher, 'get_action_listener')
263
+ .mockResolvedValue(mockActionListner);
264
+
265
+ await worker.start();
266
+
267
+ expect(getActionListenerSpy).toHaveBeenCalledTimes(1);
268
+ expect(startSpy).toHaveBeenCalledTimes(1);
269
+ expect(cancelSpy).toHaveBeenCalledTimes(1);
270
+ });
271
+
272
+ it('should retry 5 times to start a worker then throw an error', async () => {
273
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
274
+
275
+ const startSpy = jest.spyOn(worker, 'handle_start_step_run').mockReturnValue();
276
+ const cancelSpy = jest.spyOn(worker, 'handle_cancel_step_run').mockReturnValue();
277
+
278
+ const mockActionListner = new ActionListener(
279
+ hatchet.dispatcher,
280
+ mockListener([mockStart, mockCancel, new ServerError(Status.CANCELLED, 'CANCELLED')]),
281
+ 'WORKER_ID'
282
+ );
283
+
284
+ const getActionListenerSpy = jest
285
+ .spyOn(worker.client.dispatcher, 'get_action_listener')
286
+ .mockImplementationOnce(() => {
287
+ throw new Error('Simulated error');
288
+ })
289
+ .mockImplementationOnce(() => {
290
+ throw new Error('Simulated error');
291
+ })
292
+ .mockImplementationOnce(() => {
293
+ throw new Error('Simulated error');
294
+ })
295
+ .mockImplementationOnce(() => {
296
+ throw new Error('Simulated error');
297
+ })
298
+ .mockImplementationOnce(() => {
299
+ throw new Error('Simulated error');
300
+ })
301
+ .mockImplementationOnce(() => {
302
+ throw new Error('Simulated error');
303
+ });
304
+
305
+ await worker.start();
306
+
307
+ expect(getActionListenerSpy).toHaveBeenCalledTimes(5);
308
+ expect(startSpy).toHaveBeenCalledTimes(0);
309
+ expect(cancelSpy).toHaveBeenCalledTimes(0);
310
+ });
311
+
312
+ it('should successfully run after retrying < 5 times', async () => {
313
+ const worker = new Worker(hatchet, { name: 'WORKER_NAME' });
314
+
315
+ const startSpy = jest.spyOn(worker, 'handle_start_step_run').mockReturnValue();
316
+ const cancelSpy = jest.spyOn(worker, 'handle_cancel_step_run').mockReturnValue();
317
+
318
+ const mockActionLister = new ActionListener(
319
+ hatchet.dispatcher,
320
+ mockListener([mockStart, mockCancel, new ServerError(Status.CANCELLED, 'CANCELLED')]),
321
+ 'WORKER_ID'
322
+ );
323
+
324
+ const getActionListenerSpy = jest
325
+ .spyOn(worker.client.dispatcher, 'get_action_listener')
326
+ .mockImplementationOnce(() => {
327
+ throw new Error('Simulated error');
328
+ })
329
+ .mockImplementationOnce(() => {
330
+ throw new Error('Simulated error');
331
+ })
332
+ .mockImplementationOnce(() => {
333
+ throw new Error('Simulated error');
334
+ })
335
+ .mockImplementationOnce(() => {
336
+ throw new Error('Simulated error');
337
+ })
338
+ .mockImplementationOnce(async () => mockActionLister);
339
+
340
+ await worker.start();
341
+
342
+ expect(getActionListenerSpy).toHaveBeenCalledTimes(5);
343
+ expect(startSpy).toHaveBeenCalledTimes(1);
344
+ expect(cancelSpy).toHaveBeenCalledTimes(1);
345
+ });
346
+ });
347
+ });
@@ -0,0 +1,229 @@
1
+ import { HatchetClient } from '@clients/hatchet-client';
2
+ import HatchetError from '@util/errors/hatchet-error';
3
+ import { Action, ActionListener } from '@clients/dispatcher/action-listener';
4
+ import { ActionEvent, ActionEventType, ActionType } from '@protoc/dispatcher';
5
+ import HatchetPromise from '@util/hatchet-promise/hatchet-promise';
6
+ import { Workflow } from '@hatchet/workflow';
7
+ import { CreateWorkflowStepOpts } from '@protoc/workflows';
8
+ import { Logger } from '@hatchet/util/logger';
9
+ import sleep from '@hatchet/util/sleep';
10
+ import { Context } from '../../step';
11
+
12
+ export type ActionRegistry = Record<Action['actionId'], Function>;
13
+
14
+ export class Worker {
15
+ serviceName = 'default';
16
+ client: HatchetClient;
17
+ name: string;
18
+ killing: boolean;
19
+ handle_kill: boolean;
20
+
21
+ action_registry: ActionRegistry;
22
+ listener: ActionListener | undefined;
23
+ futures: Record<Action['stepRunId'], HatchetPromise<any>> = {};
24
+
25
+ logger: Logger;
26
+
27
+ constructor(client: HatchetClient, options: { name: string; handleKill?: boolean }) {
28
+ this.client = client;
29
+ this.name = options.name;
30
+ this.action_registry = {};
31
+
32
+ process.on('SIGTERM', () => this.exit_gracefully());
33
+ process.on('SIGINT', () => this.exit_gracefully());
34
+
35
+ this.killing = false;
36
+ this.handle_kill = options.handleKill === undefined ? true : options.handleKill;
37
+
38
+ this.logger = new Logger(`Worker/${this.name}`, this.client.config.log_level);
39
+ }
40
+
41
+ async register_workflow(workflow: Workflow, options?: { autoVersion?: boolean }) {
42
+ try {
43
+ await this.client.admin.put_workflow(
44
+ {
45
+ name: workflow.id,
46
+ description: workflow.description,
47
+ version: 'v0.55.0', // FIXME workflow.version,
48
+ eventTriggers: workflow.on.event ? [workflow.on.event] : [],
49
+ cronTriggers: workflow.on.cron ? [workflow.on.cron] : [],
50
+ scheduledTriggers: [],
51
+ jobs: [
52
+ {
53
+ name: 'my-job', // FIXME variable names
54
+ timeout: '60s',
55
+ description: 'my-job',
56
+ steps: workflow.steps.map<CreateWorkflowStepOpts>((step) => ({
57
+ readableId: step.name,
58
+ action: `${this.serviceName}:${step.name}`,
59
+ timeout: '60s',
60
+ inputs: '{}',
61
+ parents: step.parents ?? [],
62
+ })),
63
+ },
64
+ ],
65
+ },
66
+ {
67
+ autoVersion: !options?.autoVersion,
68
+ }
69
+ );
70
+ } catch (e: any) {
71
+ throw new HatchetError(`Could not register workflow: ${e.message}`);
72
+ }
73
+
74
+ this.action_registry = workflow.steps.reduce<ActionRegistry>((acc, step) => {
75
+ acc[`${this.serviceName}:${step.name}`] = step.run;
76
+ return acc;
77
+ }, {});
78
+ }
79
+
80
+ handle_start_step_run(action: Action) {
81
+ const { actionId } = action;
82
+ const context = new Context(action.actionPayload);
83
+
84
+ const step = this.action_registry[actionId];
85
+ if (!step) {
86
+ this.logger.error(`Could not find step '${actionId}'`);
87
+ return;
88
+ }
89
+
90
+ const run = async () => {
91
+ return step(context);
92
+ };
93
+
94
+ const success = (result: any) => {
95
+ this.logger.info(`Step run ${action.stepRunId} succeeded`);
96
+
97
+ try {
98
+ // Send the action event to the dispatcher
99
+ const event = this.get_action_event(
100
+ action,
101
+ ActionEventType.STEP_EVENT_TYPE_COMPLETED,
102
+ result
103
+ );
104
+ this.client.dispatcher.send_action_event(event);
105
+
106
+ // delete the run from the futures
107
+ delete this.futures[action.stepRunId];
108
+ } catch (e: any) {
109
+ this.logger.error(`Could not send action event: ${e.message}`);
110
+ }
111
+ };
112
+
113
+ const failure = (error: any) => {
114
+ this.logger.error(`Step run ${action.stepRunId} failed: ${error.message}`);
115
+
116
+ try {
117
+ // Send the action event to the dispatcher
118
+ const event = this.get_action_event(action, ActionEventType.STEP_EVENT_TYPE_FAILED, error);
119
+ this.client.dispatcher.send_action_event(event);
120
+ // delete the run from the futures
121
+ delete this.futures[action.stepRunId];
122
+ } catch (e: any) {
123
+ this.logger.error(`Could not send action event: ${e.message}`);
124
+ }
125
+ };
126
+
127
+ const future = new HatchetPromise(run().then(success).catch(failure));
128
+ this.futures[action.stepRunId] = future;
129
+
130
+ try {
131
+ // Send the action event to the dispatcher
132
+ const event = this.get_action_event(action, ActionEventType.STEP_EVENT_TYPE_STARTED);
133
+ this.client.dispatcher.send_action_event(event);
134
+ } catch (e: any) {
135
+ this.logger.error(`Could not send action event: ${e.message}`);
136
+ }
137
+ }
138
+
139
+ get_action_event(action: Action, eventType: ActionEventType, payload: any = ''): ActionEvent {
140
+ return {
141
+ workerId: this.name,
142
+ jobId: action.jobId,
143
+ jobRunId: action.jobRunId,
144
+ stepId: action.stepId,
145
+ stepRunId: action.stepRunId,
146
+ actionId: action.actionId,
147
+ eventTimestamp: new Date(),
148
+ eventType,
149
+ eventPayload: JSON.stringify(payload),
150
+ };
151
+ }
152
+
153
+ handle_cancel_step_run(action: Action) {
154
+ const { stepRunId } = action;
155
+ const future = this.futures[stepRunId];
156
+ if (future) {
157
+ future.cancel();
158
+ delete this.futures[stepRunId];
159
+ }
160
+ }
161
+
162
+ async stop() {
163
+ await this.exit_gracefully();
164
+ }
165
+
166
+ async exit_gracefully() {
167
+ this.killing = true;
168
+
169
+ this.logger.info('Starting to exit...');
170
+
171
+ try {
172
+ this.listener?.unregister();
173
+ } catch (e: any) {
174
+ this.logger.error(`Could not unregister listener: ${e.message}`);
175
+ }
176
+
177
+ this.logger.info('Gracefully exiting hatchet worker, running tasks will attempt to finish...');
178
+
179
+ // attempt to wait for futures to finish
180
+ await Promise.all(Object.values(this.futures).map(({ promise }) => promise));
181
+
182
+ if (this.handle_kill) {
183
+ this.logger.info('Exiting hatchet worker...');
184
+ process.exit(0);
185
+ }
186
+ }
187
+
188
+ async start() {
189
+ let retries = 0;
190
+
191
+ while (retries < 5) {
192
+ try {
193
+ this.listener = await this.client.dispatcher.get_action_listener({
194
+ workerName: this.name,
195
+ services: ['default'],
196
+ actions: Object.keys(this.action_registry),
197
+ });
198
+
199
+ const generator = this.listener.actions();
200
+
201
+ this.logger.info(`Worker ${this.name} listening for actions`);
202
+
203
+ for await (const action of generator) {
204
+ this.logger.info(`Worker ${this.name} received action ${action.actionId}`);
205
+
206
+ if (action.actionType === ActionType.START_STEP_RUN) {
207
+ this.handle_start_step_run(action);
208
+ } else if (action.actionType === ActionType.CANCEL_STEP_RUN) {
209
+ this.handle_cancel_step_run(action);
210
+ }
211
+ }
212
+
213
+ break;
214
+ } catch (e: any) {
215
+ this.logger.error(`Could not start worker: ${e.message}`);
216
+ retries += 1;
217
+ const wait = 500;
218
+ this.logger.error(`Could not start worker, retrying in ${500} seconds`);
219
+ await sleep(wait);
220
+ }
221
+ }
222
+
223
+ if (this.killing) return;
224
+
225
+ if (retries > 5) {
226
+ throw new HatchetError('Could not start worker after 5 retries');
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,3 @@
1
+ import { HatchetClient as Hatchet } from '@clients/hatchet-client';
2
+
3
+ export default Hatchet;
package/hatchet/sdk.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { HatchetClient as Hatchet } from '@clients/hatchet-client';
2
+
3
+ export default Hatchet;
@@ -0,0 +1,48 @@
1
+ import HatchetError from '@util/errors/hatchet-error';
2
+ import * as z from 'zod';
3
+
4
+ export const CreateStepSchema = z.object({
5
+ name: z.string(),
6
+ parents: z.array(z.string()).optional(),
7
+ });
8
+
9
+ export type NextStep = { [key: string]: string };
10
+
11
+ interface ContextData<T = unknown> {
12
+ input: T;
13
+ parents: Record<string, any>;
14
+ triggered_by_event: string;
15
+ }
16
+
17
+ export class Context<T = unknown> {
18
+ data: ContextData<T>;
19
+ constructor(payload: string) {
20
+ try {
21
+ this.data = JSON.parse(JSON.parse(payload));
22
+ } catch (e: any) {
23
+ throw new HatchetError(`Could not parse payload: ${e.message}`);
24
+ }
25
+ }
26
+
27
+ stepOutput(step: string): string {
28
+ if (!this.data.parents) {
29
+ throw new HatchetError('Step output not found');
30
+ }
31
+ if (!this.data.parents[step]) {
32
+ throw new HatchetError(`Step output for '${step}' not found`);
33
+ }
34
+ return this.data.parents[step];
35
+ }
36
+
37
+ triggeredByEvent(): boolean {
38
+ return this.data?.triggered_by_event === 'event';
39
+ }
40
+
41
+ workflowInput(): any {
42
+ return this.data?.input || {};
43
+ }
44
+ }
45
+
46
+ export interface CreateStep<T> extends z.infer<typeof CreateStepSchema> {
47
+ run: (ctx: Context) => Promise<NextStep> | NextStep | void;
48
+ }