@hatchet-dev/typescript-sdk 1.30.0 → 1.31.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.
Files changed (32) hide show
  1. package/clients/dispatcher/action-listener.d.ts +1 -1
  2. package/clients/dispatcher/action-listener.js +6 -0
  3. package/clients/listeners/durable-listener/durable-listener-client.d.ts +6 -1
  4. package/clients/listeners/durable-listener/durable-listener-client.js +124 -37
  5. package/clients/rest/generated/Api.d.ts +6 -1
  6. package/clients/rest/generated/Api.js +1 -1
  7. package/clients/rest/generated/data-contracts.d.ts +9 -1
  8. package/clients/rest/generated/data-contracts.js +1 -0
  9. package/package.json +1 -1
  10. package/protoc/v1/workflows.d.ts +15 -0
  11. package/protoc/v1/workflows.js +62 -2
  12. package/v1/client/worker/context.d.ts +2 -0
  13. package/v1/client/worker/context.js +26 -8
  14. package/v1/client/worker/eviction/eviction-manager.d.ts +1 -1
  15. package/v1/client/worker/eviction/eviction-manager.js +12 -5
  16. package/v1/client/worker/worker-internal.d.ts +15 -0
  17. package/v1/client/worker/worker-internal.js +66 -29
  18. package/v1/declaration.d.ts +3 -1
  19. package/v1/embedded.d.ts +36 -2
  20. package/v1/embedded.js +35 -8
  21. package/v1/examples/concurrency_dynamic/workflow.d.ts +10 -0
  22. package/v1/examples/concurrency_dynamic/workflow.js +32 -0
  23. package/v1/examples/concurrency_shared/workflow.d.ts +17 -0
  24. package/v1/examples/concurrency_shared/workflow.js +70 -0
  25. package/v1/examples/durable_callback_ordering/workflow.d.ts +42 -0
  26. package/v1/examples/durable_callback_ordering/workflow.js +95 -0
  27. package/v1/examples/durable_eviction/workflow.d.ts +5 -0
  28. package/v1/examples/durable_eviction/workflow.js +15 -1
  29. package/v1/examples/e2e-worker.js +97 -88
  30. package/v1/task.d.ts +24 -3
  31. package/version.d.ts +1 -1
  32. package/version.js +1 -1
@@ -7,7 +7,7 @@ declare enum ListenStrategy {
7
7
  LISTEN_STRATEGY_V1 = 1,
8
8
  LISTEN_STRATEGY_V2 = 2
9
9
  }
10
- export type ActionKey = `${string}/${number}`;
10
+ export type ActionKey = `${string}/${number}` | `${string}/${number}/${number}`;
11
11
  export type Action = AssignedAction & {
12
12
  readonly key: ActionKey;
13
13
  };
@@ -90,6 +90,12 @@ function createAction(assignedAction) {
90
90
  const action = assignedAction;
91
91
  Object.defineProperty(action, 'key', {
92
92
  get() {
93
+ // Durable task invocations each get a distinct key so a restored
94
+ // invocation never collides with the one it replaces in contexts,
95
+ // futures, or eviction state.
96
+ if (this.durableTaskInvocationCount !== undefined) {
97
+ return `${this.taskRunExternalId}/${this.retryCount}/${this.durableTaskInvocationCount}`;
98
+ }
93
99
  return `${this.taskRunExternalId}/${this.retryCount}`;
94
100
  },
95
101
  enumerable: true,
@@ -65,7 +65,7 @@ export declare class DurableListenerClient {
65
65
  private _requestNotify;
66
66
  private _pendingEventAcks;
67
67
  private _pendingCallbacks;
68
- private _bufferedCompletions;
68
+ private _orderedCompletions;
69
69
  private _pendingEvictionAcks;
70
70
  private _receiveAbort;
71
71
  private _statusInterval;
@@ -90,6 +90,11 @@ export declare class DurableListenerClient {
90
90
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: RunChildrenEvent): Promise<DurableTaskEventRunAck>;
91
91
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: WaitForEvent): Promise<DurableTaskEventWaitForAck>;
92
92
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: MemoEvent): Promise<DurableTaskEventMemoAck>;
93
+ private _drainsInProgress;
94
+ private _drainRerunRequests;
95
+ private _drainOrderedCompletions;
96
+ private _drainLoop;
97
+ consumeCallbackWithoutBlocking(durableTaskExternalId: string, invocationCount: number, branchId: number, nodeId: number): void;
93
98
  waitForCallback(durableTaskExternalId: string, invocationCount: number, branchId: number, nodeId: number, opts?: {
94
99
  signal?: AbortSignal;
95
100
  }): Promise<DurableTaskEventLogEntryResult>;
@@ -38,6 +38,7 @@ const abort_controller_x_1 = require("abort-controller-x");
38
38
  const hatchet_error_1 = require("../../../util/errors/hatchet-error");
39
39
  const dispatcher_1 = require("../../../protoc/v1/dispatcher");
40
40
  const non_determinism_error_1 = require("../../../util/errors/non-determinism-error");
41
+ const task_run_terminated_error_1 = require("../../../util/errors/task-run-terminated-error");
41
42
  const abort_error_1 = require("../../../util/abort-error");
42
43
  const sleep_1 = __importDefault(require("../../../util/sleep"));
43
44
  const listener_severity_1 = require("../../dispatcher/listener-severity");
@@ -110,6 +111,9 @@ function eventLogEntryResultFromProto(proto) {
110
111
  function ackKey(taskExtId, invocationCount) {
111
112
  return `${taskExtId}:${invocationCount}`;
112
113
  }
114
+ function completionOrderKey(taskExtId, invocationCount) {
115
+ return `${taskExtId}:${invocationCount}`;
116
+ }
113
117
  function callbackKey(taskExtId, invocationCount, branchId, nodeId) {
114
118
  return `${taskExtId}:${invocationCount}:${branchId}:${nodeId}`;
115
119
  }
@@ -131,13 +135,15 @@ class DurableListenerClient {
131
135
  this._requestQueue = [];
132
136
  this._pendingEventAcks = new Map();
133
137
  this._pendingCallbacks = new Map();
134
- // Completions that arrived before waitForCallback() registered a deferred
135
- // in _pendingCallbacks. This happens when the server delivers an
136
- // entryCompleted between the event ack and the waitForCallback call
137
- // (e.g. an already-satisfied sleep delivered via polling).
138
- this._bufferedCompletions = new TTLMap(10000);
138
+ // Completions held in server delivery order until their waiters can
139
+ // consume them without overtaking an earlier-delivered completion.
140
+ // The TTL only garbage-collects queues for invocations that died
141
+ // without consuming everything; the server stall-evicts long before.
142
+ this._orderedCompletions = new TTLMap(300000);
139
143
  this._pendingEvictionAcks = new Map();
140
144
  this._consecutiveFailures = 0;
145
+ this._drainsInProgress = new Set();
146
+ this._drainRerunRequests = new Set();
141
147
  this.config = config;
142
148
  this.client = factory.create(dispatcher_1.V1DispatcherDefinition, channel);
143
149
  this.logger = config.logger(`DurableListener`, config.log_level);
@@ -185,7 +191,7 @@ class DurableListenerClient {
185
191
  this._receiveAbort.abort();
186
192
  }
187
193
  this._failPendingAcks(new Error('DurableListener stopped'));
188
- this._bufferedCompletions.destroy();
194
+ this._orderedCompletions.destroy();
189
195
  });
190
196
  }
191
197
  _connect() {
@@ -332,10 +338,10 @@ class DurableListenerClient {
332
338
  d.reject(exc);
333
339
  }
334
340
  this._pendingCallbacks.clear();
335
- this._bufferedCompletions.clear();
341
+ this._orderedCompletions.clear();
336
342
  }
337
343
  _handleResponse(response) {
338
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2;
344
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
339
345
  if (response.registerWorker) {
340
346
  // registration acknowledged
341
347
  }
@@ -396,14 +402,28 @@ class DurableListenerClient {
396
402
  const { ref } = completed;
397
403
  const key = callbackKey((_o = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _o !== void 0 ? _o : '', (_p = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _p !== void 0 ? _p : 0, (_q = ref === null || ref === void 0 ? void 0 : ref.branchId) !== null && _q !== void 0 ? _q : 0, (_r = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _r !== void 0 ? _r : 0);
398
404
  const result = eventLogEntryResultFromProto(completed);
399
- const pending = this._pendingCallbacks.get(key);
400
- if (pending) {
401
- pending.resolve(result);
402
- this._pendingCallbacks.delete(key);
403
- }
404
- else {
405
- this._bufferedCompletions.set(key, result);
405
+ 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);
406
+ const queue = (_u = this._orderedCompletions.get(orderKey)) !== null && _u !== void 0 ? _u : {
407
+ pending: [],
408
+ delivered: new Set(),
409
+ };
410
+ if (!queue.delivered.has(key)) {
411
+ queue.delivered.add(key);
412
+ queue.pending.push({ key, result });
413
+ }
414
+ else if (queue.pending.every((entry) => entry.key !== key)) {
415
+ // Re-delivery of a completion that already drained (reconnect,
416
+ // worker-status re-send, or a repeated wait on a node deduped by
417
+ // child key). Its satisfied order was released before anything
418
+ // still queued, so handing it straight to a waiter keeps order.
419
+ const redelivered = this._pendingCallbacks.get(key);
420
+ if (redelivered) {
421
+ this._pendingCallbacks.delete(key);
422
+ redelivered.resolve(result);
423
+ }
406
424
  }
425
+ this._orderedCompletions.set(orderKey, queue);
426
+ this._drainOrderedCompletions(orderKey);
407
427
  }
408
428
  else if (response.evictionAck) {
409
429
  const ack = response.evictionAck;
@@ -418,34 +438,36 @@ class DurableListenerClient {
418
438
  const evict = response.serverEvict;
419
439
  this.logger.info(`received server eviction notification for task ${evict.durableTaskExternalId} ` +
420
440
  `invocation ${evict.invocationCount}: ${evict.reason}`);
421
- this.cleanupTaskState(evict.durableTaskExternalId, evict.invocationCount);
441
+ // onServerEvict aborts the run first so waiters settle as aborted
442
+ // (eviction) rather than with cleanup's generic rejection.
422
443
  if (this.onServerEvict) {
423
444
  this.onServerEvict(evict.durableTaskExternalId, evict.invocationCount);
424
445
  }
446
+ this.cleanupTaskState(evict.durableTaskExternalId, evict.invocationCount);
425
447
  }
426
448
  else if (response.error) {
427
449
  const { error } = response;
428
450
  const { ref } = error;
429
451
  let exc;
430
452
  if (error.errorType === dispatcher_1.DurableTaskErrorType.DURABLE_TASK_ERROR_TYPE_NONDETERMINISM) {
431
- exc = new non_determinism_error_1.NonDeterminismError((_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, (_u = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _u !== void 0 ? _u : 0, error.errorMessage);
453
+ exc = new non_determinism_error_1.NonDeterminismError((_v = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _v !== void 0 ? _v : '', (_w = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _w !== void 0 ? _w : 0, (_x = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _x !== void 0 ? _x : 0, error.errorMessage);
432
454
  }
433
455
  else {
434
456
  exc = new Error(`Unspecified durable task error: ${error.errorMessage} (type: ${error.errorType})`);
435
457
  }
436
- const eAckKey = ackKey((_v = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _v !== void 0 ? _v : '', (_w = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _w !== void 0 ? _w : 0);
458
+ const eAckKey = ackKey((_y = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _y !== void 0 ? _y : '', (_z = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _z !== void 0 ? _z : 0);
437
459
  const pendingAck = this._pendingEventAcks.get(eAckKey);
438
460
  if (pendingAck) {
439
461
  pendingAck.reject(exc);
440
462
  this._pendingEventAcks.delete(eAckKey);
441
463
  }
442
- const eCbKey = callbackKey((_x = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _x !== void 0 ? _x : '', (_y = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _y !== void 0 ? _y : 0, (_z = ref === null || ref === void 0 ? void 0 : ref.branchId) !== null && _z !== void 0 ? _z : 0, (_0 = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _0 !== void 0 ? _0 : 0);
464
+ const eCbKey = callbackKey((_0 = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _0 !== void 0 ? _0 : '', (_1 = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _1 !== void 0 ? _1 : 0, (_2 = ref === null || ref === void 0 ? void 0 : ref.branchId) !== null && _2 !== void 0 ? _2 : 0, (_3 = ref === null || ref === void 0 ? void 0 : ref.nodeId) !== null && _3 !== void 0 ? _3 : 0);
443
465
  const pendingCb = this._pendingCallbacks.get(eCbKey);
444
466
  if (pendingCb) {
445
467
  pendingCb.reject(exc);
446
468
  this._pendingCallbacks.delete(eCbKey);
447
469
  }
448
- const eEvKey = evictionKey((_1 = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _1 !== void 0 ? _1 : '', (_2 = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _2 !== void 0 ? _2 : 0);
470
+ const eEvKey = evictionKey((_4 = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _4 !== void 0 ? _4 : '', (_5 = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _5 !== void 0 ? _5 : 0);
449
471
  const pendingEv = this._pendingEvictionAcks.get(eEvKey);
450
472
  if (pendingEv) {
451
473
  pendingEv.reject(exc);
@@ -498,26 +520,87 @@ class DurableListenerClient {
498
520
  return d.promise;
499
521
  });
500
522
  }
523
+ // Hands queued completions to their waiters strictly in delivery order.
524
+ // Stops at the first completion whose waiter has not registered yet:
525
+ // releasing a later completion first would resume its coroutine ahead of
526
+ // the recorded order and diverge the re-emitted event sequence.
527
+ _drainOrderedCompletions(orderKey) {
528
+ if (this._drainsInProgress.has(orderKey)) {
529
+ this._drainRerunRequests.add(orderKey);
530
+ return;
531
+ }
532
+ this._drainsInProgress.add(orderKey);
533
+ void this._drainLoop(orderKey).finally(() => {
534
+ this._drainsInProgress.delete(orderKey);
535
+ if (this._drainRerunRequests.delete(orderKey)) {
536
+ this._drainOrderedCompletions(orderKey);
537
+ }
538
+ });
539
+ }
540
+ _drainLoop(orderKey) {
541
+ return __awaiter(this, void 0, void 0, function* () {
542
+ let released = false;
543
+ while (true) {
544
+ if (released) {
545
+ // A macrotask boundary between releases lets the just-resumed waiter
546
+ // run to its next suspension point (emitting its next durable event)
547
+ // before the following completion is released. Resolving several
548
+ // deferreds back-to-back would instead resume them in the order their
549
+ // continuations happened to be attached, not in release order.
550
+ yield new Promise((resolve) => {
551
+ setImmediate(resolve);
552
+ });
553
+ }
554
+ const queue = this._orderedCompletions.get(orderKey);
555
+ if (!queue || queue.pending.length === 0)
556
+ return;
557
+ const [head] = queue.pending;
558
+ const waiter = this._pendingCallbacks.get(head.key);
559
+ if (!waiter)
560
+ return;
561
+ queue.pending.shift();
562
+ this._pendingCallbacks.delete(head.key);
563
+ waiter.resolve(head.result);
564
+ released = true;
565
+ }
566
+ });
567
+ }
568
+ // A memo's value arrives in its ack and its completion is never awaited,
569
+ // but the server still delivers that completion on replay, in recorded
570
+ // order. Register a waiter that nothing blocks on so the drain can hand the
571
+ // completion through instead of stalling the queue at it forever.
572
+ consumeCallbackWithoutBlocking(durableTaskExternalId, invocationCount, branchId, nodeId) {
573
+ const key = callbackKey(durableTaskExternalId, invocationCount, branchId, nodeId);
574
+ if (this._pendingCallbacks.has(key))
575
+ return;
576
+ const d = deferred();
577
+ d.promise.catch(() => { });
578
+ this._pendingCallbacks.set(key, d);
579
+ this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
580
+ }
501
581
  waitForCallback(durableTaskExternalId, invocationCount, branchId, nodeId, opts) {
502
582
  return __awaiter(this, void 0, void 0, function* () {
503
- const key = callbackKey(durableTaskExternalId, invocationCount, branchId, nodeId);
504
- const early = this._bufferedCompletions.get(key);
505
- if (early) {
506
- this._bufferedCompletions.delete(key);
507
- return early;
583
+ const signal = opts === null || opts === void 0 ? void 0 : opts.signal;
584
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
585
+ return Promise.reject((0, abort_error_1.createAbortError)('Operation cancelled by AbortSignal'));
508
586
  }
509
- if (!this._pendingCallbacks.has(key)) {
510
- this._pendingCallbacks.set(key, deferred());
511
- this._pollWorkerStatus();
587
+ const key = callbackKey(durableTaskExternalId, invocationCount, branchId, nodeId);
588
+ let d = this._pendingCallbacks.get(key);
589
+ if (!d) {
590
+ d = deferred();
591
+ // A deferred can outlive its waiters (eviction aborts the caller while
592
+ // the entry stays registered); a later cleanupTaskState rejection must
593
+ // not crash the process as an unhandled rejection.
594
+ d.promise.catch(() => { });
595
+ this._pendingCallbacks.set(key, d);
596
+ this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
597
+ if (this._pendingCallbacks.has(key)) {
598
+ this._pollWorkerStatus();
599
+ }
512
600
  }
513
- const d = this._pendingCallbacks.get(key);
514
- const signal = opts === null || opts === void 0 ? void 0 : opts.signal;
515
601
  if (!signal) {
516
602
  return d.promise;
517
603
  }
518
- if (signal.aborted) {
519
- return Promise.reject((0, abort_error_1.createAbortError)('Operation cancelled by AbortSignal'));
520
- }
521
604
  return new Promise((resolve, reject) => {
522
605
  let settled = false;
523
606
  const onAbort = () => {
@@ -544,24 +627,28 @@ class DurableListenerClient {
544
627
  });
545
628
  }
546
629
  cleanupTaskState(durableTaskExternalId, invocationCount) {
630
+ // Rejecting with TaskRunTerminatedError marks any coroutine still blocked
631
+ // on this invocation's state as evicted rather than failed, matching
632
+ // Python's future cancellation on cleanup.
633
+ const evicted = () => new task_run_terminated_error_1.TaskRunTerminatedError('evicted', 'task state cleaned up');
547
634
  for (const [k, d] of this._pendingCallbacks) {
548
635
  const parts = k.split(':');
549
636
  if (parts[0] === durableTaskExternalId && parseInt(parts[1], 10) <= invocationCount) {
550
- d.reject(new Error('task state cleaned up'));
637
+ d.reject(evicted());
551
638
  this._pendingCallbacks.delete(k);
552
639
  }
553
640
  }
554
641
  for (const [k, d] of this._pendingEventAcks) {
555
642
  const parts = k.split(':');
556
643
  if (parts[0] === durableTaskExternalId && parseInt(parts[1], 10) <= invocationCount) {
557
- d.reject(new Error('task state cleaned up'));
644
+ d.reject(evicted());
558
645
  this._pendingEventAcks.delete(k);
559
646
  }
560
647
  }
561
- for (const k of this._bufferedCompletions.keys()) {
648
+ for (const k of this._orderedCompletions.keys()) {
562
649
  const parts = k.split(':');
563
650
  if (parts[0] === durableTaskExternalId && parseInt(parts[1], 10) <= invocationCount) {
564
- this._bufferedCompletions.delete(k);
651
+ this._orderedCompletions.delete(k);
565
652
  }
566
653
  }
567
654
  }
@@ -350,7 +350,10 @@ export declare class Api<SecurityDataType = unknown> extends HttpClient<Security
350
350
  * @request GET:/api/v1/stable/workflow-runs/{v1-workflow-run}
351
351
  * @secure
352
352
  */
353
- v1WorkflowRunGet: (v1WorkflowRun: string, params?: RequestParams) => Promise<import("axios").AxiosResponse<V1WorkflowRunDetails, any, {}>>;
353
+ v1WorkflowRunGet: (v1WorkflowRun: string, query?: {
354
+ /** Whether to include the DAG orchestrator's task events, which are hidden by default */
355
+ includeOrchestratorEvents?: boolean;
356
+ }, params?: RequestParams) => Promise<import("axios").AxiosResponse<V1WorkflowRunDetails, any, {}>>;
354
357
  /**
355
358
  * @description Get the status of a workflow run.
356
359
  *
@@ -2024,6 +2027,8 @@ export declare class Api<SecurityDataType = unknown> extends HttpClient<Security
2024
2027
  statuses?: WorkerStatus[];
2025
2028
  /** Filter by worker labels */
2026
2029
  labels?: string[];
2030
+ /** Whether to include engine-managed operator workers, which are hidden by default */
2031
+ includeOperators?: boolean;
2027
2032
  }, params?: RequestParams) => Promise<import("axios").AxiosResponse<WorkerList, any, {}>>;
2028
2033
  /**
2029
2034
  * @description Update a worker
@@ -175,7 +175,7 @@ class Api extends http_client_1.HttpClient {
175
175
  * @request GET:/api/v1/stable/workflow-runs/{v1-workflow-run}
176
176
  * @secure
177
177
  */
178
- this.v1WorkflowRunGet = (v1WorkflowRun, params = {}) => this.request(Object.assign({ path: `/api/v1/stable/workflow-runs/${v1WorkflowRun}`, method: 'GET', secure: true, format: 'json' }, params));
178
+ this.v1WorkflowRunGet = (v1WorkflowRun, query, params = {}) => this.request(Object.assign({ path: `/api/v1/stable/workflow-runs/${v1WorkflowRun}`, method: 'GET', query: query, secure: true, format: 'json' }, params));
179
179
  /**
180
180
  * @description Get the status of a workflow run.
181
181
  *
@@ -25,7 +25,8 @@ export declare enum PullRequestState {
25
25
  export declare enum FeatureFlagId {
26
26
  TenantLogWorkflowFilterEnabled = "tenant-log-workflow-filter-enabled",
27
27
  TraceMinimapEnabled = "trace-minimap-enabled",
28
- OrganizationSsoEnabled = "organization-sso-enabled"
28
+ OrganizationSsoEnabled = "organization-sso-enabled",
29
+ OperatorDetailsEnabled = "operator-details-enabled"
29
30
  }
30
31
  export declare enum WebhookWorkerRequestMethod {
31
32
  GET = "GET",
@@ -2328,6 +2329,13 @@ export interface Worker {
2328
2329
  */
2329
2330
  webhookId?: string;
2330
2331
  runtimeInfo?: WorkerRuntimeInfo;
2332
+ /**
2333
+ * The id of the operator that owns this worker, if it is an engine-managed operator worker.
2334
+ * @format uuid
2335
+ */
2336
+ operatorId?: string;
2337
+ /** The number of durable task runs owned by this operator that are currently evicted while waiting on durable events. Evicted runs hold no slots. Only set for operator workers. */
2338
+ evictedDurableTaskCount?: number;
2331
2339
  }
2332
2340
  export interface WorkerList {
2333
2341
  pagination?: PaginationResponse;
@@ -46,6 +46,7 @@ var FeatureFlagId;
46
46
  FeatureFlagId["TenantLogWorkflowFilterEnabled"] = "tenant-log-workflow-filter-enabled";
47
47
  FeatureFlagId["TraceMinimapEnabled"] = "trace-minimap-enabled";
48
48
  FeatureFlagId["OrganizationSsoEnabled"] = "organization-sso-enabled";
49
+ FeatureFlagId["OperatorDetailsEnabled"] = "operator-details-enabled";
49
50
  })(FeatureFlagId || (exports.FeatureFlagId = FeatureFlagId = {}));
50
51
  var WebhookWorkerRequestMethod;
51
52
  (function (WebhookWorkerRequestMethod) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchet-dev/typescript-sdk",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "description": "Background task orchestration & visibility for developers",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
@@ -170,6 +170,15 @@ export interface DefaultFilter {
170
170
  /** (optional) the payload for the filter, if any. A JSON object as a string. */
171
171
  payload?: Uint8Array | undefined;
172
172
  }
173
+ /**
174
+ * Concurrency declares one entry in a concurrency chain. Entries are processed in array
175
+ * order, so a tenant-scoped entry may come before or after a workflow-scoped one.
176
+ *
177
+ * A tenant-scoped entry (is_tenant_scoped = true) defines (or updates in place) a strategy
178
+ * shared across workflows, keyed by name: every task declaring the same name consumes the
179
+ * same concurrency limit. Registrations whose chains order the same tenant-scoped
180
+ * strategies inconsistently are rejected, since inconsistent orders can deadlock.
181
+ */
173
182
  export interface Concurrency {
174
183
  /** (required) the expression to use for concurrency */
175
184
  expression: string;
@@ -177,6 +186,12 @@ export interface Concurrency {
177
186
  maxRuns?: number | undefined;
178
187
  /** (optional) the strategy to use when the concurrency limit is reached, default CANCEL_IN_PROGRESS */
179
188
  limitStrategy?: ConcurrencyLimitStrategy | undefined;
189
+ /** (required when is_tenant_scoped) the strategy name; unique per tenant for tenant-scoped strategies */
190
+ name?: string | undefined;
191
+ /** (optional) when true, the entry is a tenant-scoped strategy shared across workflows, default false */
192
+ isTenantScoped?: boolean | undefined;
193
+ /** (optional) CEL expression over task input returning the max runs for that task's concurrency group; the group's effective limit is the value from its most recently created task. Overrides max_runs per group; a non-integer or negative result fails the task, and 0 holds the group until a newer task raises the limit */
194
+ maxRunsExpression?: string | undefined;
180
195
  }
181
196
  export interface TaskBatchConfig {
182
197
  /** (required) maximum items per batch */
@@ -1794,7 +1794,14 @@ exports.DefaultFilter = {
1794
1794
  },
1795
1795
  };
1796
1796
  function createBaseConcurrency() {
1797
- return { expression: '', maxRuns: undefined, limitStrategy: undefined };
1797
+ return {
1798
+ expression: '',
1799
+ maxRuns: undefined,
1800
+ limitStrategy: undefined,
1801
+ name: undefined,
1802
+ isTenantScoped: undefined,
1803
+ maxRunsExpression: undefined,
1804
+ };
1798
1805
  }
1799
1806
  exports.Concurrency = {
1800
1807
  encode(message, writer = new wire_1.BinaryWriter()) {
@@ -1807,6 +1814,15 @@ exports.Concurrency = {
1807
1814
  if (message.limitStrategy !== undefined) {
1808
1815
  writer.uint32(24).int32(message.limitStrategy);
1809
1816
  }
1817
+ if (message.name !== undefined) {
1818
+ writer.uint32(34).string(message.name);
1819
+ }
1820
+ if (message.isTenantScoped !== undefined) {
1821
+ writer.uint32(40).bool(message.isTenantScoped);
1822
+ }
1823
+ if (message.maxRunsExpression !== undefined) {
1824
+ writer.uint32(50).string(message.maxRunsExpression);
1825
+ }
1810
1826
  return writer;
1811
1827
  },
1812
1828
  decode(input, length) {
@@ -1837,6 +1853,27 @@ exports.Concurrency = {
1837
1853
  message.limitStrategy = reader.int32();
1838
1854
  continue;
1839
1855
  }
1856
+ case 4: {
1857
+ if (tag !== 34) {
1858
+ break;
1859
+ }
1860
+ message.name = reader.string();
1861
+ continue;
1862
+ }
1863
+ case 5: {
1864
+ if (tag !== 40) {
1865
+ break;
1866
+ }
1867
+ message.isTenantScoped = reader.bool();
1868
+ continue;
1869
+ }
1870
+ case 6: {
1871
+ if (tag !== 50) {
1872
+ break;
1873
+ }
1874
+ message.maxRunsExpression = reader.string();
1875
+ continue;
1876
+ }
1840
1877
  }
1841
1878
  if ((tag & 7) === 4 || tag === 0) {
1842
1879
  break;
@@ -1858,6 +1895,17 @@ exports.Concurrency = {
1858
1895
  : isSet(object.limit_strategy)
1859
1896
  ? concurrencyLimitStrategyFromJSON(object.limit_strategy)
1860
1897
  : undefined,
1898
+ name: isSet(object.name) ? globalThis.String(object.name) : undefined,
1899
+ isTenantScoped: isSet(object.isTenantScoped)
1900
+ ? globalThis.Boolean(object.isTenantScoped)
1901
+ : isSet(object.is_tenant_scoped)
1902
+ ? globalThis.Boolean(object.is_tenant_scoped)
1903
+ : undefined,
1904
+ maxRunsExpression: isSet(object.maxRunsExpression)
1905
+ ? globalThis.String(object.maxRunsExpression)
1906
+ : isSet(object.max_runs_expression)
1907
+ ? globalThis.String(object.max_runs_expression)
1908
+ : undefined,
1861
1909
  };
1862
1910
  },
1863
1911
  toJSON(message) {
@@ -1871,17 +1919,29 @@ exports.Concurrency = {
1871
1919
  if (message.limitStrategy !== undefined) {
1872
1920
  obj.limitStrategy = concurrencyLimitStrategyToJSON(message.limitStrategy);
1873
1921
  }
1922
+ if (message.name !== undefined) {
1923
+ obj.name = message.name;
1924
+ }
1925
+ if (message.isTenantScoped !== undefined) {
1926
+ obj.isTenantScoped = message.isTenantScoped;
1927
+ }
1928
+ if (message.maxRunsExpression !== undefined) {
1929
+ obj.maxRunsExpression = message.maxRunsExpression;
1930
+ }
1874
1931
  return obj;
1875
1932
  },
1876
1933
  create(base) {
1877
1934
  return exports.Concurrency.fromPartial(base !== null && base !== void 0 ? base : {});
1878
1935
  },
1879
1936
  fromPartial(object) {
1880
- var _a, _b, _c;
1937
+ var _a, _b, _c, _d, _e, _f;
1881
1938
  const message = createBaseConcurrency();
1882
1939
  message.expression = (_a = object.expression) !== null && _a !== void 0 ? _a : '';
1883
1940
  message.maxRuns = (_b = object.maxRuns) !== null && _b !== void 0 ? _b : undefined;
1884
1941
  message.limitStrategy = (_c = object.limitStrategy) !== null && _c !== void 0 ? _c : undefined;
1942
+ message.name = (_d = object.name) !== null && _d !== void 0 ? _d : undefined;
1943
+ message.isTenantScoped = (_e = object.isTenantScoped) !== null && _e !== void 0 ? _e : undefined;
1944
+ message.maxRunsExpression = (_f = object.maxRunsExpression) !== null && _f !== void 0 ? _f : undefined;
1885
1945
  return message;
1886
1946
  },
1887
1947
  };
@@ -381,6 +381,8 @@ export declare class DurableContext<T, K = {}> extends Context<T, K> {
381
381
  private _evictionManager;
382
382
  private _engineVersion;
383
383
  private _waitKey;
384
+ private _sendEventLock;
385
+ private _serializeSendEvent;
384
386
  constructor(action: Action, v1: HatchetClient, worker: InternalWorker, durableListener: DurableListenerClient, evictionManager?: DurableEvictionManager, engineVersion?: string);
385
387
  get supportsEviction(): boolean;
386
388
  get durableListener(): DurableListenerClient;
@@ -692,9 +692,27 @@ exports.Context = Context;
692
692
  * It extends the Context class and includes additional methods for durable execution like sleepFor and waitFor.
693
693
  */
694
694
  class DurableContext extends Context {
695
+ _serializeSendEvent(send) {
696
+ // Re-check cancellation when the queued send actually executes: once this
697
+ // invocation is aborted (evicted), a queued send must not reach the
698
+ // server, where it could race the eviction and append to the event log
699
+ // out of recorded order. Python gets this for free from task
700
+ // cancellation killing coroutines queued on the send lock.
701
+ const runSend = () => {
702
+ this.throwIfCancelled();
703
+ return send();
704
+ };
705
+ const result = this._sendEventLock.then(runSend, runSend);
706
+ this._sendEventLock = result.then(() => undefined, () => undefined);
707
+ return result;
708
+ }
695
709
  constructor(action, v1, worker, durableListener, evictionManager, engineVersion) {
696
710
  super(action, v1, worker);
697
711
  this._waitKey = 0;
712
+ // Serializes sendEvent calls from concurrent coroutines in this invocation.
713
+ // The listener keys pending acks by (task, invocation), so overlapping sends
714
+ // would overwrite each other's ack and cross-wire branch/node assignments.
715
+ this._sendEventLock = Promise.resolve();
698
716
  this._durableListener = durableListener;
699
717
  this._evictionManager = evictionManager;
700
718
  this._engineVersion = engineVersion;
@@ -767,14 +785,14 @@ class DurableContext extends Context {
767
785
  }
768
786
  const rendered = (0, conditions_1.Render)(condition_1.Action.CREATE, conditions);
769
787
  const pbConditions = (0, transformer_1.conditionsToPb)(rendered, this.v1.config.namespace);
770
- const ack = yield this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
788
+ const ack = yield this._serializeSendEvent(() => this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
771
789
  kind: 'waitFor',
772
790
  waitForConditions: {
773
791
  sleepConditions: pbConditions.sleepConditions,
774
792
  userEventConditions: pbConditions.userEventConditions,
775
793
  },
776
794
  label,
777
- });
795
+ }));
778
796
  const resourceId = rendered
779
797
  .map((c) => c.base.readableDataKey)
780
798
  .filter(Boolean)
@@ -787,9 +805,8 @@ class DurableContext extends Context {
787
805
  }
788
806
  waitForEvent(key, expression, payloadSchema, scope, lookbackWindow, label) {
789
807
  return __awaiter(this, void 0, void 0, function* () {
790
- const now = yield this.now();
791
808
  const considerEventsSince = lookbackWindow
792
- ? new Date(now.getTime() - (0, duration_1.durationToMs)(lookbackWindow)).toISOString()
809
+ ? new Date((yield this.now()).getTime() - (0, duration_1.durationToMs)(lookbackWindow)).toISOString()
793
810
  : undefined;
794
811
  const res = yield this.waitFor({
795
812
  eventKey: key,
@@ -918,10 +935,10 @@ class DurableContext extends Context {
918
935
  const { triggerOpts } = this._buildTriggerOpts(child.workflow, child.input, child.options);
919
936
  return triggerOpts;
920
937
  });
921
- const ack = yield this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
938
+ const ack = yield this._serializeSendEvent(() => this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
922
939
  kind: 'runChildren',
923
940
  triggerOpts: triggerOptsList,
924
- });
941
+ }));
925
942
  const results = yield Promise.all(ack.runEntries.map((entry) => this.withEvictionWait('runChild', `workflow:bulk-child`, () => __awaiter(this, void 0, void 0, function* () {
926
943
  const result = yield this._durableListener.waitForCallback(this.action.taskRunExternalId, this.invocationCount, entry.branchId, entry.nodeId, { signal: this.abortController.signal });
927
944
  if (result.isFailure) {
@@ -946,10 +963,11 @@ class DurableContext extends Context {
946
963
  return fn();
947
964
  }
948
965
  const memoKey = computeMemoKey(this.action.taskRunExternalId, deps);
949
- const ack = yield this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
966
+ const ack = yield this._serializeSendEvent(() => this._durableListener.sendEvent(this.action.taskRunExternalId, this.invocationCount, {
950
967
  kind: 'memo',
951
968
  memoKey,
952
- });
969
+ }));
970
+ this._durableListener.consumeCallbackWithoutBlocking(this.action.taskRunExternalId, this.invocationCount, ack.branchId, ack.nodeId);
953
971
  if (ack.memoAlreadyExisted && ack.memoResultPayload && ack.memoResultPayload.length > 0) {
954
972
  const serialized = new TextDecoder().decode(ack.memoResultPayload);
955
973
  return JSON.parse(serialized);
@@ -21,7 +21,7 @@ export declare class DurableEvictionManager {
21
21
  private _ticking;
22
22
  constructor(opts: {
23
23
  durableSlots: number;
24
- cancelLocal: (key: ActionKey) => void;
24
+ cancelLocal: (key: ActionKey, invocationCount: number) => void;
25
25
  requestEvictionWithAck: (key: ActionKey, rec: DurableRunRecord) => Promise<void>;
26
26
  config?: DurableEvictionConfig;
27
27
  cache?: DurableEvictionCache;