@hatchet-dev/typescript-sdk 1.34.0-alpha.2 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,6 +49,7 @@ TypeScript SDK reference (overview: https://docs.hatchet.run/reference/typescrip
49
49
  - Rate Limits: https://docs.hatchet.run/reference/typescript/feature-clients/ratelimits.md (guide: https://docs.hatchet.run/v1/rate-limits.md)
50
50
  - Runs: https://docs.hatchet.run/reference/typescript/feature-clients/runs.md (guide: https://docs.hatchet.run/v1/running-your-task.md)
51
51
  - Scheduled Runs: https://docs.hatchet.run/reference/typescript/feature-clients/schedules.md (guide: https://docs.hatchet.run/v1/scheduled-runs.md)
52
+ - Streams: https://docs.hatchet.run/reference/typescript/feature-clients/streams.md
52
53
  - Tenant: https://docs.hatchet.run/reference/typescript/feature-clients/tenant.md
53
54
  - Webhooks: https://docs.hatchet.run/reference/typescript/feature-clients/webhooks.md (guide: https://docs.hatchet.run/v1/webhooks.md)
54
55
  - Workers: https://docs.hatchet.run/reference/typescript/feature-clients/workers.md (guide: https://docs.hatchet.run/v1/workers.md)
@@ -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-alpha.2",
3
+ "version": "1.34.1",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -0,0 +1,79 @@
1
+ import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
2
+ import type { CallContext, CallOptions } from 'nice-grpc-common';
3
+ export declare const protobufPackage = "v1";
4
+ export interface PublishStreamMessageRequest {
5
+ namespace: string;
6
+ topic: string;
7
+ payload: Uint8Array;
8
+ producerId: string;
9
+ producerSeq: number;
10
+ }
11
+ export interface PublishStreamMessageResponse {
12
+ }
13
+ export interface SubscribeStreamRequest {
14
+ namespace: string;
15
+ topic: string;
16
+ cursor?: string | undefined;
17
+ }
18
+ export interface StreamMessage {
19
+ entries: StreamEntry[];
20
+ hangup: boolean;
21
+ cursor: string;
22
+ }
23
+ export interface StreamEntry {
24
+ payload: Uint8Array;
25
+ cursor: string;
26
+ createdAt: Date | undefined;
27
+ }
28
+ export declare const PublishStreamMessageRequest: MessageFns<PublishStreamMessageRequest>;
29
+ export declare const PublishStreamMessageResponse: MessageFns<PublishStreamMessageResponse>;
30
+ export declare const SubscribeStreamRequest: MessageFns<SubscribeStreamRequest>;
31
+ export declare const StreamMessage: MessageFns<StreamMessage>;
32
+ export declare const StreamEntry: MessageFns<StreamEntry>;
33
+ export type V1StreamsDefinition = typeof V1StreamsDefinition;
34
+ export declare const V1StreamsDefinition: {
35
+ readonly name: "V1Streams";
36
+ readonly fullName: "v1.V1Streams";
37
+ readonly methods: {
38
+ readonly publish: {
39
+ readonly name: "Publish";
40
+ readonly requestType: typeof PublishStreamMessageRequest;
41
+ readonly requestStream: false;
42
+ readonly responseType: typeof PublishStreamMessageResponse;
43
+ readonly responseStream: false;
44
+ readonly options: {};
45
+ };
46
+ readonly subscribe: {
47
+ readonly name: "Subscribe";
48
+ readonly requestType: typeof SubscribeStreamRequest;
49
+ readonly requestStream: false;
50
+ readonly responseType: typeof StreamMessage;
51
+ readonly responseStream: true;
52
+ readonly options: {};
53
+ };
54
+ };
55
+ };
56
+ export interface V1StreamsServiceImplementation<CallContextExt = {}> {
57
+ publish(request: PublishStreamMessageRequest, context: CallContext & CallContextExt): Promise<DeepPartial<PublishStreamMessageResponse>>;
58
+ subscribe(request: SubscribeStreamRequest, context: CallContext & CallContextExt): ServerStreamingMethodResult<DeepPartial<StreamMessage>>;
59
+ }
60
+ export interface V1StreamsClient<CallOptionsExt = {}> {
61
+ publish(request: DeepPartial<PublishStreamMessageRequest>, options?: CallOptions & CallOptionsExt): Promise<PublishStreamMessageResponse>;
62
+ subscribe(request: DeepPartial<SubscribeStreamRequest>, options?: CallOptions & CallOptionsExt): AsyncIterable<StreamMessage>;
63
+ }
64
+ type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
65
+ export type DeepPartial<T> = T extends Builtin ? T : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends {} ? {
66
+ [K in keyof T]?: DeepPartial<T[K]>;
67
+ } : Partial<T>;
68
+ export type ServerStreamingMethodResult<Response> = {
69
+ [Symbol.asyncIterator](): AsyncIterator<Response, void>;
70
+ };
71
+ export interface MessageFns<T> {
72
+ encode(message: T, writer?: BinaryWriter): BinaryWriter;
73
+ decode(input: BinaryReader | Uint8Array, length?: number): T;
74
+ fromJSON(object: any): T;
75
+ toJSON(message: T): unknown;
76
+ create(base?: DeepPartial<T>): T;
77
+ fromPartial(object: DeepPartial<T>): T;
78
+ }
79
+ export {};