@hatchet-dev/typescript-sdk 1.34.0 → 1.35.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.
@@ -28,5 +28,6 @@ export declare class ActionListener {
28
28
  getListenStrategy(): Promise<ListenStrategy>;
29
29
  incrementRetries(): Promise<void>;
30
30
  getListenClient(): Promise<AsyncIterable<AssignedAction>>;
31
+ stopStream(): void;
31
32
  unregister(): Promise<import("../../protoc/dispatcher").WorkerUnsubscribeResponse>;
32
33
  }
@@ -225,14 +225,19 @@ class ActionListener {
225
225
  }
226
226
  });
227
227
  }
228
+ // Leaves the heartbeat running: tasks that are already in flight still need
229
+ // the engine to see this worker as alive until they finish.
230
+ stopStream() {
231
+ this.done = true;
232
+ // Abort the gRPC stream to immediately cancel the generator
233
+ if (this.abortController) {
234
+ this.abortController.abort('Worker stopping');
235
+ }
236
+ }
228
237
  unregister() {
229
238
  return __awaiter(this, void 0, void 0, function* () {
230
- this.done = true;
239
+ this.stopStream();
231
240
  this.heartbeat.stop();
232
- // Abort the gRPC stream to immediately cancel the generator
233
- if (this.abortController) {
234
- this.abortController.abort('Worker stopping');
235
- }
236
241
  try {
237
242
  return yield this.client.unsubscribe({
238
243
  workerId: this.workerId,
@@ -18,6 +18,9 @@ export declare class DurableListenerClient {
18
18
  private _pendingCallbacks;
19
19
  private _orderedCompletions;
20
20
  private _pendingEvictionAcks;
21
+ private _newlyWaitingKeys;
22
+ private _waitingSince;
23
+ private _workerStatusSendScheduled;
21
24
  private _receiveAbort;
22
25
  private _statusInterval;
23
26
  private _startLock;
@@ -34,13 +37,20 @@ export declare class DurableListenerClient {
34
37
  private _requestIterator;
35
38
  private _enqueueRequest;
36
39
  private _startStatusPolling;
37
- private _pollWorkerStatus;
40
+ private _registerWaitingKey;
41
+ private _scheduleWorkerStatusSend;
42
+ private _enqueueWorkerStatusForNewlyWaitingKeys;
43
+ private _enqueueWorkerStatusForLongPendingWaiters;
44
+ private _enqueueFullWorkerStatus;
45
+ private _enqueueWorkerStatusFor;
38
46
  private _failPendingAcks;
39
47
  private _failAllPending;
40
48
  private _handleResponse;
41
49
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: RunChildrenEvent): Promise<DurableTaskEventRunAck>;
42
50
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: WaitForEvent): Promise<DurableTaskEventWaitForAck>;
43
51
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: MemoEvent): Promise<DurableTaskEventMemoAck>;
52
+ private _sendRequestAndAwaitAck;
53
+ private _sendRunChildrenEvent;
44
54
  private _drainsInProgress;
45
55
  private _drainRerunRequests;
46
56
  private _drainOrderedCompletions;
@@ -94,7 +94,10 @@ class TTLMap {
94
94
  }
95
95
  const DEFAULT_RECONNECT_INTERVAL = 3000;
96
96
  const EVICTION_ACK_TIMEOUT_MS = 30000;
97
- const WORKER_STATUS_POLL_INTERVAL_MS = 1000;
97
+ const WORKER_STATUS_RESEND_INTERVAL_MS = 5000;
98
+ const WORKER_STATUS_RESEND_MIN_PENDING_MS = 2000;
99
+ const WORKER_STATUS_MAX_ENTRIES_PER_REQUEST = 10000;
100
+ const RUN_CHILDREN_MAX_CHILDREN_PER_REQUEST = 100;
98
101
  function eventLogEntryResultFromProto(proto) {
99
102
  var _a, _b, _c, _d;
100
103
  let payload;
@@ -109,6 +112,15 @@ function eventLogEntryResultFromProto(proto) {
109
112
  errorMessage: proto.errorMessage,
110
113
  };
111
114
  }
115
+ function awaitedCompletedEntryFromCallbackKey(key) {
116
+ const [durableTaskExternalId, invocationCount, branchId, nodeId] = key.split(':');
117
+ return {
118
+ durableTaskExternalId,
119
+ invocationCount: parseInt(invocationCount, 10),
120
+ branchId: parseInt(branchId, 10),
121
+ nodeId: parseInt(nodeId, 10),
122
+ };
123
+ }
112
124
  function ackKey(taskExtId, invocationCount) {
113
125
  return `${taskExtId}:${invocationCount}`;
114
126
  }
@@ -142,6 +154,9 @@ class DurableListenerClient {
142
154
  // without consuming everything; the server stall-evicts long before.
143
155
  this._orderedCompletions = new TTLMap(300000);
144
156
  this._pendingEvictionAcks = new Map();
157
+ this._newlyWaitingKeys = new Set();
158
+ this._waitingSince = new Map();
159
+ this._workerStatusSendScheduled = false;
145
160
  this._consecutiveFailures = 0;
146
161
  this._drainsInProgress = new Set();
147
162
  this._drainRerunRequests = new Set();
@@ -205,7 +220,7 @@ class DurableListenerClient {
205
220
  this._enqueueRequest({
206
221
  registerWorker: { workerId: this._workerId },
207
222
  });
208
- this._pollWorkerStatus();
223
+ this._enqueueFullWorkerStatus();
209
224
  void this._streamLoop();
210
225
  this.logger.info('durable event listener connected');
211
226
  });
@@ -300,28 +315,57 @@ class DurableListenerClient {
300
315
  clearInterval(this._statusInterval);
301
316
  }
302
317
  this._statusInterval = setInterval(() => {
303
- this._pollWorkerStatus();
304
- }, WORKER_STATUS_POLL_INTERVAL_MS);
318
+ this._enqueueWorkerStatusForLongPendingWaiters();
319
+ }, WORKER_STATUS_RESEND_INTERVAL_MS);
305
320
  }
306
- _pollWorkerStatus() {
307
- if (!this._workerId || this._pendingCallbacks.size === 0)
321
+ _registerWaitingKey(key) {
322
+ this._waitingSince.set(key, Date.now());
323
+ this._newlyWaitingKeys.add(key);
324
+ }
325
+ // Waiters registered in the same turn of the event loop share one request,
326
+ // instead of each sending its own.
327
+ _scheduleWorkerStatusSend() {
328
+ if (this._workerStatusSendScheduled)
308
329
  return;
309
- const waitingEntries = [];
310
- for (const key of this._pendingCallbacks.keys()) {
311
- const parts = key.split(':');
312
- waitingEntries.push({
313
- durableTaskExternalId: parts[0],
314
- invocationCount: parseInt(parts[1], 10),
315
- branchId: parseInt(parts[2], 10),
316
- nodeId: parseInt(parts[3], 10),
330
+ this._workerStatusSendScheduled = true;
331
+ setImmediate(() => this._enqueueWorkerStatusForNewlyWaitingKeys());
332
+ }
333
+ _enqueueWorkerStatusForNewlyWaitingKeys() {
334
+ this._workerStatusSendScheduled = false;
335
+ const keys = [...this._newlyWaitingKeys].filter((key) => this._pendingCallbacks.has(key));
336
+ this._newlyWaitingKeys.clear();
337
+ this._enqueueWorkerStatusFor(keys);
338
+ }
339
+ _enqueueWorkerStatusForLongPendingWaiters() {
340
+ const registeredBefore = Date.now() - WORKER_STATUS_RESEND_MIN_PENDING_MS;
341
+ const keys = [];
342
+ for (const [key, waitingSince] of this._waitingSince) {
343
+ if (!this._pendingCallbacks.has(key)) {
344
+ this._waitingSince.delete(key);
345
+ }
346
+ else if (waitingSince <= registeredBefore) {
347
+ keys.push(key);
348
+ }
349
+ }
350
+ this._enqueueWorkerStatusFor(keys);
351
+ }
352
+ _enqueueFullWorkerStatus() {
353
+ this._newlyWaitingKeys.clear();
354
+ this._enqueueWorkerStatusFor([...this._pendingCallbacks.keys()]);
355
+ }
356
+ _enqueueWorkerStatusFor(keys, maxEntriesPerRequest = WORKER_STATUS_MAX_ENTRIES_PER_REQUEST) {
357
+ if (!this._workerId)
358
+ return;
359
+ for (let start = 0; start < keys.length; start += maxEntriesPerRequest) {
360
+ this._enqueueRequest({
361
+ workerStatus: {
362
+ workerId: this._workerId,
363
+ waitingEntries: keys
364
+ .slice(start, start + maxEntriesPerRequest)
365
+ .map(awaitedCompletedEntryFromCallbackKey),
366
+ },
317
367
  });
318
368
  }
319
- this._enqueueRequest({
320
- workerStatus: {
321
- workerId: this._workerId,
322
- waitingEntries,
323
- },
324
- });
325
369
  }
326
370
  _failPendingAcks(exc) {
327
371
  for (const d of this._pendingEventAcks.values()) {
@@ -406,13 +450,15 @@ class DurableListenerClient {
406
450
  const orderKey = completionOrderKey((_s = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _s !== void 0 ? _s : '', (_t = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _t !== void 0 ? _t : 0);
407
451
  const queue = (_u = this._orderedCompletions.get(orderKey)) !== null && _u !== void 0 ? _u : {
408
452
  pending: [],
453
+ pendingKeys: new Set(),
409
454
  delivered: new Set(),
410
455
  };
411
456
  if (!queue.delivered.has(key)) {
412
457
  queue.delivered.add(key);
458
+ queue.pendingKeys.add(key);
413
459
  queue.pending.push({ key, result });
414
460
  }
415
- else if (queue.pending.every((entry) => entry.key !== key)) {
461
+ else if (!queue.pendingKeys.has(key)) {
416
462
  // Re-delivery of a completion that already drained (reconnect,
417
463
  // worker-status re-send, or a repeated wait on a node deduped by
418
464
  // child key). Its satisfied order was released before anything
@@ -478,20 +524,10 @@ class DurableListenerClient {
478
524
  }
479
525
  sendEvent(durableTaskExternalId, invocationCount, event) {
480
526
  return __awaiter(this, void 0, void 0, function* () {
481
- const key = ackKey(durableTaskExternalId, invocationCount);
482
- const d = deferred();
483
- this._pendingEventAcks.set(key, d);
484
527
  let request;
485
528
  switch (event.kind) {
486
- case 'runChildren': {
487
- const triggerRunsReq = {
488
- invocationCount,
489
- durableTaskExternalId,
490
- triggerOpts: event.triggerOpts,
491
- };
492
- request = { triggerRuns: triggerRunsReq };
493
- break;
494
- }
529
+ case 'runChildren':
530
+ return this._sendRunChildrenEvent(durableTaskExternalId, invocationCount, event);
495
531
  case 'waitFor': {
496
532
  const waitForReq = {
497
533
  invocationCount,
@@ -517,8 +553,33 @@ class DurableListenerClient {
517
553
  throw new Error(`Unknown durable task send event: ${_}`);
518
554
  }
519
555
  }
520
- this._enqueueRequest(request);
521
- return d.promise;
556
+ return this._sendRequestAndAwaitAck(durableTaskExternalId, invocationCount, request);
557
+ });
558
+ }
559
+ _sendRequestAndAwaitAck(durableTaskExternalId, invocationCount, request) {
560
+ const d = deferred();
561
+ this._pendingEventAcks.set(ackKey(durableTaskExternalId, invocationCount), d);
562
+ this._enqueueRequest(request);
563
+ return d.promise;
564
+ }
565
+ _sendRunChildrenEvent(durableTaskExternalId, invocationCount, event) {
566
+ return __awaiter(this, void 0, void 0, function* () {
567
+ const runEntries = [];
568
+ for (let start = 0; start < event.triggerOpts.length; start += RUN_CHILDREN_MAX_CHILDREN_PER_REQUEST) {
569
+ const triggerRunsReq = {
570
+ invocationCount,
571
+ durableTaskExternalId,
572
+ triggerOpts: event.triggerOpts.slice(start, start + RUN_CHILDREN_MAX_CHILDREN_PER_REQUEST),
573
+ };
574
+ const ack = yield this._sendRequestAndAwaitAck(durableTaskExternalId, invocationCount, {
575
+ triggerRuns: triggerRunsReq,
576
+ });
577
+ if (ack.ackType !== 'run') {
578
+ throw new Error(`Expected a run ack for a trigger runs request, got ${ack.ackType}`);
579
+ }
580
+ runEntries.push(...ack.runEntries);
581
+ }
582
+ return { ackType: 'run', invocationCount, durableTaskExternalId, runEntries };
522
583
  });
523
584
  }
524
585
  // Hands queued completions to their waiters strictly in delivery order.
@@ -560,6 +621,7 @@ class DurableListenerClient {
560
621
  if (!waiter)
561
622
  return;
562
623
  queue.pending.shift();
624
+ queue.pendingKeys.delete(head.key);
563
625
  this._pendingCallbacks.delete(head.key);
564
626
  waiter.resolve(head.result);
565
627
  released = true;
@@ -577,6 +639,7 @@ class DurableListenerClient {
577
639
  const d = deferred();
578
640
  d.promise.catch(() => { });
579
641
  this._pendingCallbacks.set(key, d);
642
+ this._registerWaitingKey(key);
580
643
  this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
581
644
  }
582
645
  waitForCallback(durableTaskExternalId, invocationCount, branchId, nodeId, opts) {
@@ -594,9 +657,10 @@ class DurableListenerClient {
594
657
  // not crash the process as an unhandled rejection.
595
658
  d.promise.catch(() => { });
596
659
  this._pendingCallbacks.set(key, d);
660
+ this._registerWaitingKey(key);
597
661
  this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
598
662
  if (this._pendingCallbacks.has(key)) {
599
- this._pollWorkerStatus();
663
+ this._scheduleWorkerStatusSend();
600
664
  }
601
665
  }
602
666
  if (!signal) {
@@ -656,6 +720,10 @@ class DurableListenerClient {
656
720
  sendEvictInvocation(durableTaskExternalId, invocationCount, reason) {
657
721
  return __awaiter(this, void 0, void 0, function* () {
658
722
  const key = evictionKey(durableTaskExternalId, invocationCount);
723
+ const pending = this._pendingEvictionAcks.get(key);
724
+ if (pending) {
725
+ return pending.promise;
726
+ }
659
727
  const d = deferred();
660
728
  this._pendingEvictionAcks.set(key, d);
661
729
  const req = {
@@ -664,15 +732,17 @@ class DurableListenerClient {
664
732
  reason,
665
733
  };
666
734
  this._enqueueRequest({ evictInvocation: req });
667
- const timeout = (0, sleep_1.default)(EVICTION_ACK_TIMEOUT_MS).then(() => {
668
- throw new Error(`Eviction ack timed out after ${EVICTION_ACK_TIMEOUT_MS}ms for task ${durableTaskExternalId} invocation ${invocationCount}`);
669
- });
735
+ const timeout = setTimeout(() => {
736
+ d.reject(new Error(`Eviction ack timed out after ${EVICTION_ACK_TIMEOUT_MS}ms for task ${durableTaskExternalId} invocation ${invocationCount}`));
737
+ }, EVICTION_ACK_TIMEOUT_MS);
670
738
  try {
671
- yield Promise.race([d.promise, timeout]);
739
+ yield d.promise;
672
740
  }
673
- catch (err) {
674
- this._pendingEvictionAcks.delete(key);
675
- throw err;
741
+ finally {
742
+ clearTimeout(timeout);
743
+ if (this._pendingEvictionAcks.get(key) === d) {
744
+ this._pendingEvictionAcks.delete(key);
745
+ }
676
746
  }
677
747
  });
678
748
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchet-dev/typescript-sdk",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -1,6 +1,17 @@
1
1
  import { BaseWorkflowDeclaration, WorkflowDefinition } from '../..';
2
2
  import type { LegacyWorkflow } from '../../../legacy/legacy-transformer';
3
+ import { WorkflowPauseScheduledCronRunQueueBehavior } from '../../../clients/rest/generated/data-contracts';
3
4
  import { HatchetClient } from '../client';
5
+ import { Duration } from '../duration';
6
+ export { WorkflowPauseScheduledCronRunQueueBehavior };
7
+ export type PauseWorkflowOpts = {
8
+ /** How long runs stay queued while the workflow is paused before they are dropped. */
9
+ queueTTL: Duration;
10
+ /** The behavior of cron runs triggered while the workflow is paused. Defaults to QUEUE. */
11
+ pausedWorkflowCronRunQueueBehavior?: WorkflowPauseScheduledCronRunQueueBehavior;
12
+ /** The behavior of scheduled runs triggered while the workflow is paused. Defaults to QUEUE. */
13
+ pausedWorkflowScheduledRunQueueBehavior?: WorkflowPauseScheduledCronRunQueueBehavior;
14
+ };
4
15
  export declare const workflowNameString: (workflow: string | WorkflowDefinition | BaseWorkflowDeclaration<any, any> | LegacyWorkflow) => string;
5
16
  /**
6
17
  * The workflows client is a client for managing workflows programmatically within Hatchet.
@@ -39,4 +50,18 @@ export declare class WorkflowsClient {
39
50
  * @returns A promise that resolves to the deleted workflow.
40
51
  */
41
52
  delete(workflow: string | BaseWorkflowDeclaration<any, any> | LegacyWorkflow): Promise<void>;
53
+ /**
54
+ * Pause a workflow. While paused, new runs of the workflow are queued but not started.
55
+ * @param workflow - The workflow name, ID, or object.
56
+ * @param opts - The options for the pause operation.
57
+ * @returns A promise that resolves to the updated workflow.
58
+ */
59
+ pause(workflow: string | BaseWorkflowDeclaration<any, any> | LegacyWorkflow, opts: PauseWorkflowOpts): Promise<import("../../../clients/rest/generated/data-contracts").Workflow>;
60
+ /**
61
+ * Unpause a workflow.
62
+ * @param workflow - The workflow name, ID, or object.
63
+ * @returns A promise that resolves to the updated workflow.
64
+ */
65
+ unpause(workflow: string | BaseWorkflowDeclaration<any, any> | LegacyWorkflow): Promise<import("../../../clients/rest/generated/data-contracts").Workflow>;
66
+ private update;
42
67
  }
@@ -9,9 +9,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.WorkflowsClient = exports.workflowNameString = void 0;
12
+ exports.WorkflowsClient = exports.workflowNameString = exports.WorkflowPauseScheduledCronRunQueueBehavior = void 0;
13
13
  const legacy_transformer_1 = require("../../../legacy/legacy-transformer");
14
14
  const uuid_1 = require("../../../util/uuid");
15
+ const data_contracts_1 = require("../../../clients/rest/generated/data-contracts");
16
+ Object.defineProperty(exports, "WorkflowPauseScheduledCronRunQueueBehavior", { enumerable: true, get: function () { return data_contracts_1.WorkflowPauseScheduledCronRunQueueBehavior; } });
17
+ const duration_1 = require("../duration");
15
18
  const workflowNameString = (workflow) => {
16
19
  if (typeof workflow === 'string') {
17
20
  return workflow;
@@ -148,5 +151,50 @@ class WorkflowsClient {
148
151
  }
149
152
  });
150
153
  }
154
+ /**
155
+ * Pause a workflow. While paused, new runs of the workflow are queued but not started.
156
+ * @param workflow - The workflow name, ID, or object.
157
+ * @param opts - The options for the pause operation.
158
+ * @returns A promise that resolves to the updated workflow.
159
+ */
160
+ pause(workflow, opts) {
161
+ return __awaiter(this, void 0, void 0, function* () {
162
+ var _a, _b;
163
+ return this.update(workflow, {
164
+ pause: {
165
+ action: 'pause',
166
+ pausedWorkflowQueueTTL: (0, duration_1.durationToString)(opts.queueTTL),
167
+ pausedWorkflowCronRunQueueBehavior: (_a = opts.pausedWorkflowCronRunQueueBehavior) !== null && _a !== void 0 ? _a : data_contracts_1.WorkflowPauseScheduledCronRunQueueBehavior.QUEUE,
168
+ pausedWorkflowScheduledRunQueueBehavior: (_b = opts.pausedWorkflowScheduledRunQueueBehavior) !== null && _b !== void 0 ? _b : data_contracts_1.WorkflowPauseScheduledCronRunQueueBehavior.QUEUE,
169
+ },
170
+ });
171
+ });
172
+ }
173
+ /**
174
+ * Unpause a workflow.
175
+ * @param workflow - The workflow name, ID, or object.
176
+ * @returns A promise that resolves to the updated workflow.
177
+ */
178
+ unpause(workflow) {
179
+ return __awaiter(this, void 0, void 0, function* () {
180
+ return this.update(workflow, { pause: { action: 'unpause' } });
181
+ });
182
+ }
183
+ update(workflow, request) {
184
+ return __awaiter(this, void 0, void 0, function* () {
185
+ const name = (0, exports.workflowNameString)(workflow);
186
+ const workflowId = yield this.getWorkflowIdFromName(name);
187
+ try {
188
+ const { data } = yield this.api.workflowUpdate(workflowId, request);
189
+ this.workflowCache.delete(name);
190
+ this.workflowCache.delete(data.name);
191
+ return data;
192
+ }
193
+ catch (error) {
194
+ this.workflowCache.delete(name);
195
+ throw error;
196
+ }
197
+ });
198
+ }
151
199
  }
152
200
  exports.WorkflowsClient = WorkflowsClient;
@@ -45,6 +45,7 @@ export declare class InternalWorker {
45
45
  enableHealthServer: boolean;
46
46
  private healthServer;
47
47
  private status;
48
+ private shutdownPromise;
48
49
  constructor(client: HatchetClient, options: {
49
50
  name: string;
50
51
  handleKill?: boolean;
@@ -96,6 +97,7 @@ export declare class InternalWorker {
96
97
  handleCancelStepRun(action: Action): Promise<void>;
97
98
  stop(): Promise<void>;
98
99
  exitGracefully(handleKill: boolean): Promise<void>;
100
+ private shutdown;
99
101
  /**
100
102
  * Creates an action listener by registering the worker with the dispatcher.
101
103
  * Override in subclasses to change registration behavior (e.g. legacy engines).
@@ -628,6 +628,18 @@ class InternalWorker {
628
628
  exitGracefully(handleKill) {
629
629
  return __awaiter(this, void 0, void 0, function* () {
630
630
  var _a;
631
+ // Signals and an explicit stop must await the same eviction and listener teardown.
632
+ (_a = this.shutdownPromise) !== null && _a !== void 0 ? _a : (this.shutdownPromise = this.shutdown());
633
+ yield this.shutdownPromise;
634
+ if (handleKill) {
635
+ this.logger.info('Exiting hatchet worker...');
636
+ process.exit(0);
637
+ }
638
+ });
639
+ }
640
+ shutdown() {
641
+ return __awaiter(this, void 0, void 0, function* () {
642
+ var _a, _b;
631
643
  this.killing = true;
632
644
  this.setStatus(health_server_1.workerStatus.UNHEALTHY);
633
645
  this.logger.info('Starting to exit...');
@@ -658,16 +670,19 @@ class InternalWorker {
658
670
  catch (e) {
659
671
  this.logger.error(`Could not stop durable listener: ${e.message}`);
660
672
  }
673
+ (_a = this.listener) === null || _a === void 0 ? void 0 : _a.stopStream();
674
+ this.logger.info('Gracefully exiting hatchet worker, running tasks will attempt to finish...');
675
+ // attempt to wait for futures to finish
676
+ yield Promise.all(Object.values(this.futures).map(({ promise }) => promise));
677
+ this.logger.info('Successfully finished pending tasks.');
678
+ // Unregistering stops the heartbeat, so it has to wait until the running tasks are done:
679
+ // otherwise the engine sees a dead worker and reassigns tasks that are about to complete.
661
680
  try {
662
- yield ((_a = this.listener) === null || _a === void 0 ? void 0 : _a.unregister());
681
+ yield ((_b = this.listener) === null || _b === void 0 ? void 0 : _b.unregister());
663
682
  }
664
683
  catch (e) {
665
684
  this.logger.error(`Could not unregister listener: ${e.message}`);
666
685
  }
667
- this.logger.info('Gracefully exiting hatchet worker, running tasks will attempt to finish...');
668
- // attempt to wait for futures to finish
669
- yield Promise.all(Object.values(this.futures).map(({ promise }) => promise));
670
- this.logger.info('Successfully finished pending tasks.');
671
686
  if (this.healthServer) {
672
687
  try {
673
688
  yield this.healthServer.stop();
@@ -676,10 +691,6 @@ class InternalWorker {
676
691
  this.logger.error(`Could not stop health server: ${e.message}`);
677
692
  }
678
693
  }
679
- if (handleKill) {
680
- this.logger.info('Exiting hatchet worker...');
681
- process.exit(0);
682
- }
683
694
  });
684
695
  }
685
696
  /**
@@ -700,10 +711,18 @@ class InternalWorker {
700
711
  start() {
701
712
  return __awaiter(this, void 0, void 0, function* () {
702
713
  var _a, e_1, _b, _c;
714
+ if (this.killing) {
715
+ return;
716
+ }
703
717
  this.setStatus(health_server_1.workerStatus.STARTING);
704
718
  if (this.healthServer) {
705
719
  try {
706
720
  yield this.healthServer.start();
721
+ if (this.killing) {
722
+ yield this.shutdownPromise;
723
+ yield this.healthServer.stop();
724
+ return;
725
+ }
707
726
  }
708
727
  catch (e) {
709
728
  this.logger.error(`Could not start health server: ${e.message}`);
@@ -713,11 +732,17 @@ class InternalWorker {
713
732
  }
714
733
  // ensure all workflows are registered
715
734
  yield Promise.all(this.registeredWorkflowPromises);
716
- if (Object.keys(this.action_registry).length === 0) {
735
+ if (this.killing || Object.keys(this.action_registry).length === 0) {
717
736
  return;
718
737
  }
719
738
  try {
720
- this.listener = yield this.createListener();
739
+ const listener = yield this.createListener();
740
+ if (this.killing) {
741
+ // Shutdown cannot clean up a listener that registration has not returned yet.
742
+ yield listener.unregister();
743
+ return;
744
+ }
745
+ this.listener = listener;
721
746
  this.workerId = this.listener.workerId;
722
747
  this.setStatus(health_server_1.workerStatus.HEALTHY);
723
748
  const generator = this.listener.actions();
@@ -12,6 +12,7 @@ import { IHatchetClient } from './client/client.interface';
12
12
  import { CreateWorkflowTaskOpts, CreateOnFailureTaskOpts, TaskFn, CreateWorkflowDurableTaskOpts, CreateBaseTaskOpts, CreateOnSuccessTaskOpts, Concurrency, DurableTaskFn, WorkerLabelComparator, IdempotencyConfig, BatchTaskConfig, BatchTaskFn } from './task';
13
13
  import { Duration } from './client/duration';
14
14
  import { MetricsClient } from './client/features/metrics';
15
+ import type { PauseWorkflowOpts } from './client/features/workflows';
15
16
  import { InputType, OutputType, UnknownInputType, JsonObject, Resolved } from './types';
16
17
  import { Context, DurableContext } from './client/worker/context';
17
18
  import { EvictionPolicy } from './client/worker/eviction/eviction-policy';
@@ -374,6 +375,19 @@ export declare class BaseWorkflowDeclaration<I extends InputType = UnknownInputT
374
375
  * @throws Error if the workflow is not bound to a Hatchet client.
375
376
  */
376
377
  get(): Promise<import("../clients/rest/generated/data-contracts").Workflow>;
378
+ /**
379
+ * Pause the workflow. While paused, new runs of the workflow are queued but not started.
380
+ * @param opts - The options for the pause operation.
381
+ * @returns A promise that resolves to the updated workflow.
382
+ * @throws Error if the workflow is not bound to a Hatchet client.
383
+ */
384
+ pause(opts: PauseWorkflowOpts): Promise<import("../clients/rest/generated/data-contracts").Workflow>;
385
+ /**
386
+ * Unpause the workflow.
387
+ * @returns A promise that resolves to the updated workflow.
388
+ * @throws Error if the workflow is not bound to a Hatchet client.
389
+ */
390
+ unpause(): Promise<import("../clients/rest/generated/data-contracts").Workflow>;
377
391
  /**
378
392
  * @deprecated use definition.name instead
379
393
  * @hidden
package/v1/declaration.js CHANGED
@@ -379,27 +379,29 @@ class BaseWorkflowDeclaration {
379
379
  }
380
380
  return this.client.workflows.get(this);
381
381
  }
382
- // // gets the pause state of the workflow
383
- // isPaused() {
384
- // if (!this.client) {
385
- // throw UNBOUND_ERR;
386
- // }
387
- // return this.client.workflows.isPaused(this);
388
- // }
389
- // // pause assignment of workflow
390
- // pause() {
391
- // if (!this.client) {
392
- // throw UNBOUND_ERR;
393
- // }
394
- // return this.client.workflows.pause(this);
395
- // }
396
- // // unpause assignment of workflow
397
- // unpause() {
398
- // if (!this.client) {
399
- // throw UNBOUND_ERR;
400
- // }
401
- // return this.client.workflows.unpause(this);
402
- // }
382
+ /**
383
+ * Pause the workflow. While paused, new runs of the workflow are queued but not started.
384
+ * @param opts - The options for the pause operation.
385
+ * @returns A promise that resolves to the updated workflow.
386
+ * @throws Error if the workflow is not bound to a Hatchet client.
387
+ */
388
+ pause(opts) {
389
+ if (!this.client) {
390
+ throw UNBOUND_ERR;
391
+ }
392
+ return this.client.workflows.pause(this, opts);
393
+ }
394
+ /**
395
+ * Unpause the workflow.
396
+ * @returns A promise that resolves to the updated workflow.
397
+ * @throws Error if the workflow is not bound to a Hatchet client.
398
+ */
399
+ unpause() {
400
+ if (!this.client) {
401
+ throw UNBOUND_ERR;
402
+ }
403
+ return this.client.workflows.unpause(this);
404
+ }
403
405
  /**
404
406
  * @deprecated use definition.name instead
405
407
  * @hidden
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const v1_1 = require("../..");
13
+ const hatchet_client_1 = require("../hatchet-client");
14
+ const pausableWorkflow = hatchet_client_1.hatchet.workflow({
15
+ name: 'pausable-workflow',
16
+ });
17
+ function main() {
18
+ return __awaiter(this, void 0, void 0, function* () {
19
+ // > Pause a workflow
20
+ yield pausableWorkflow.pause({
21
+ queueTTL: '24h',
22
+ pausedWorkflowCronRunQueueBehavior: v1_1.WorkflowPauseScheduledCronRunQueueBehavior.DROP,
23
+ pausedWorkflowScheduledRunQueueBehavior: v1_1.WorkflowPauseScheduledCronRunQueueBehavior.QUEUE,
24
+ });
25
+ // !!
26
+ // > Unpause a workflow
27
+ yield pausableWorkflow.unpause();
28
+ // !!
29
+ });
30
+ }
31
+ if (require.main === module) {
32
+ main()
33
+ .catch(console.error)
34
+ .finally(() => process.exit(0));
35
+ }
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const HATCHET_VERSION = "1.34.0";
1
+ export declare const HATCHET_VERSION = "1.35.0";
package/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HATCHET_VERSION = void 0;
4
- exports.HATCHET_VERSION = '1.34.0';
4
+ exports.HATCHET_VERSION = '1.35.0';