@hatchet-dev/typescript-sdk 1.35.1 → 1.37.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 (34) hide show
  1. package/clients/rest/generated/Api.d.ts +85 -1
  2. package/clients/rest/generated/Api.js +30 -0
  3. package/clients/rest/generated/data-contracts.d.ts +11 -0
  4. package/package.json +1 -1
  5. package/protoc/dispatcher/dispatcher.d.ts +4 -0
  6. package/protoc/dispatcher/dispatcher.js +42 -2
  7. package/protoc/v1/streams.d.ts +34 -0
  8. package/protoc/v1/streams.js +279 -5
  9. package/protoc-es/dispatcher/dispatcher_pb.d.ts +12 -0
  10. package/protoc-es/dispatcher/dispatcher_pb.js +1 -1
  11. package/protoc-es/v1/streams_pb.d.ts +79 -0
  12. package/protoc-es/v1/streams_pb.js +12 -2
  13. package/v1/client/duration.js +3 -3
  14. package/v1/client/features/streams.d.ts +22 -0
  15. package/v1/client/features/streams.js +93 -13
  16. package/v1/client/worker/engine-version.d.ts +2 -0
  17. package/v1/client/worker/engine-version.js +7 -0
  18. package/v1/client/worker/worker-internal.d.ts +2 -0
  19. package/v1/client/worker/worker-internal.js +20 -1
  20. package/v1/examples/durable-streams/benchmark.d.ts +34 -0
  21. package/v1/examples/durable-streams/benchmark.js +120 -0
  22. package/v1/examples/e2e-worker.js +52 -49
  23. package/v1/examples/retry_after/run.d.ts +1 -0
  24. package/v1/examples/retry_after/run.js +21 -0
  25. package/v1/examples/retry_after/worker.d.ts +1 -0
  26. package/v1/examples/retry_after/worker.js +24 -0
  27. package/v1/examples/retry_after/workflow.d.ts +9 -0
  28. package/v1/examples/retry_after/workflow.js +40 -0
  29. package/v1/task.d.ts +13 -0
  30. package/v1/task.js +14 -1
  31. package/version.d.ts +1 -1
  32. package/version.js +1 -1
  33. package/v1/examples/durable-streams/publish-latency.d.ts +0 -15
  34. package/v1/examples/durable-streams/publish-latency.js +0 -50
@@ -34,6 +34,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
35
  exports.StreamsClient = void 0;
36
36
  const crypto_1 = require("crypto");
37
+ const axios_1 = require("axios");
37
38
  const nice_grpc_1 = require("nice-grpc");
38
39
  const grpc_helpers_1 = require("../../../util/grpc-helpers");
39
40
  const grpc_error_1 = require("../../../util/grpc-error");
@@ -58,7 +59,27 @@ const permanentSubscribeErrors = new Set([
58
59
  ]);
59
60
  const resubscribeMinDelayMs = 250;
60
61
  const resubscribeMaxDelayMs = 10000;
62
+ // the server's limit for a payload sent over gRPC (MaxStreamMessagePayloadBytes)
63
+ const maxInlinePayloadBytes = 4 * 1024 * 1024 - 5 * 1024;
64
+ // the server's limit for an uploaded payload (MaxStreamUploadedPayloadBytes)
65
+ const maxUploadedPayloadBytes = 64 * 1024 * 1024;
66
+ const payloadRequestAttempts = 3;
67
+ function httpStatus(err) {
68
+ var _a;
69
+ return (0, axios_1.isAxiosError)(err) ? (_a = err.response) === null || _a === void 0 ? void 0 : _a.status : undefined;
70
+ }
71
+ // a payload fetch that got no response, a 5xx or a 429 may succeed later
72
+ function isTemporaryHttpError(err) {
73
+ if (!(0, axios_1.isAxiosError)(err)) {
74
+ return false;
75
+ }
76
+ const status = httpStatus(err);
77
+ return status === undefined || status >= 500 || status === 429;
78
+ }
61
79
  function shouldResubscribe(err, attempt) {
80
+ if (isTemporaryHttpError(err)) {
81
+ return true;
82
+ }
62
83
  const code = (0, grpc_error_1.getGrpcErrorCode)(err);
63
84
  if (code === undefined || permanentSubscribeErrors.has(code)) {
64
85
  return false;
@@ -82,6 +103,7 @@ class StreamsClient {
82
103
  // stored promise always resolves, so one failure doesn't block later calls
83
104
  this.publishChains = new Map();
84
105
  this._config = client.config;
106
+ this._client = client;
85
107
  }
86
108
  get grpc() {
87
109
  if (!this._grpc) {
@@ -90,21 +112,46 @@ class StreamsClient {
90
112
  }
91
113
  return this._grpc;
92
114
  }
93
- publishOrdered(key_1, namespace_1, topic_1, payload_1) {
94
- return __awaiter(this, arguments, void 0, function* (key, namespace, topic, payload, retryGap = true) {
115
+ // retries a 5xx or network failure; a 4xx won't succeed on retry
116
+ static withRetries(request) {
117
+ return __awaiter(this, void 0, void 0, function* () {
118
+ for (let attempt = 1;; attempt += 1) {
119
+ try {
120
+ return yield request();
121
+ }
122
+ catch (err) {
123
+ const status = httpStatus(err);
124
+ if ((status !== undefined && status < 500) || attempt >= payloadRequestAttempts) {
125
+ throw err;
126
+ }
127
+ }
128
+ }
129
+ });
130
+ }
131
+ uploadPayload(payload) {
132
+ return __awaiter(this, void 0, void 0, function* () {
133
+ const res = yield StreamsClient.withRetries(() => this._client.api.v1StreamPayloadUpload(this._config.tenant_id, {
134
+ payload: new Blob([payload]),
135
+ }));
136
+ return res.data.ref;
137
+ });
138
+ }
139
+ fetchPayload(payloadRef, signal) {
140
+ return __awaiter(this, void 0, void 0, function* () {
141
+ const res = yield StreamsClient.withRetries(() => this._client.api.v1StreamPayloadGet(this._config.tenant_id, { ref: payloadRef }, { format: 'arraybuffer', signal }));
142
+ return new Uint8Array(res.data);
143
+ });
144
+ }
145
+ publishOrdered(key_1, namespace_1, topic_1, body_1) {
146
+ return __awaiter(this, arguments, void 0, function* (key, namespace, topic, body, retryGap = true) {
95
147
  let producer = this.producers.get(key);
96
148
  if (!producer) {
97
149
  producer = { producerId: (0, crypto_1.randomUUID)(), seq: 0 };
98
150
  this.producers.set(key, producer);
99
151
  }
100
152
  try {
101
- yield this.grpc.publish({
102
- namespace,
103
- topic,
104
- payload,
105
- producerId: producer.producerId,
106
- producerSeq: producer.seq,
107
- });
153
+ yield this.grpc.publish(Object.assign(Object.assign({ namespace,
154
+ topic }, body), { producerId: producer.producerId, producerSeq: producer.seq }));
108
155
  }
109
156
  catch (err) {
110
157
  // it may still land, and a different payload under its seq would be dropped as a duplicate
@@ -113,7 +160,7 @@ class StreamsClient {
113
160
  }
114
161
  // a gap stored nothing (e.g. the watermark passed cursor retention), so resend as the new producer
115
162
  if (retryGap && (0, grpc_error_1.getGrpcErrorCode)(err) === nice_grpc_1.Status.FAILED_PRECONDITION) {
116
- return this.publishOrdered(key, namespace, topic, payload, false);
163
+ return this.publishOrdered(key, namespace, topic, body, false);
117
164
  }
118
165
  throw err;
119
166
  }
@@ -133,12 +180,41 @@ class StreamsClient {
133
180
  const payload = typeof message === 'string' ? new TextEncoder().encode(message) : message;
134
181
  const namespace = (_a = options === null || options === void 0 ? void 0 : options.namespace) !== null && _a !== void 0 ? _a : '';
135
182
  const key = `${namespace}\u0000${topic}`;
183
+ if (payload.byteLength > maxUploadedPayloadBytes) {
184
+ throw new Error(`payload exceeds maximum size of ${maxUploadedPayloadBytes} bytes`);
185
+ }
186
+ // uploaded before taking its place in the topic's order, so uploads overlap
187
+ const uploaded = payload.byteLength > maxInlinePayloadBytes ? this.uploadPayload(payload) : undefined;
188
+ // awaited in order below; this only keeps an early failure from reporting as unhandled
189
+ uploaded === null || uploaded === void 0 ? void 0 : uploaded.catch(() => { });
136
190
  const previous = (_b = this.publishChains.get(key)) !== null && _b !== void 0 ? _b : Promise.resolve();
137
- const publishPromise = previous.then(() => this.publishOrdered(key, namespace, topic, payload));
191
+ const publishPromise = previous.then(() => __awaiter(this, void 0, void 0, function* () {
192
+ return this.publishOrdered(key, namespace, topic, uploaded ? { payloadRef: yield uploaded } : { payload });
193
+ }));
138
194
  this.publishChains.set(key, publishPromise.catch(() => { }));
139
195
  return publishPromise;
140
196
  });
141
197
  }
198
+ /**
199
+ * Describes topic's retained messages. Rejects with a NOT_FOUND gRPC error
200
+ * for a topic nothing was published to.
201
+ * @param topic - the topic to describe
202
+ * @param options - optional namespace override
203
+ */
204
+ topicMetadata(topic, options) {
205
+ return __awaiter(this, void 0, void 0, function* () {
206
+ var _a;
207
+ const res = yield this.grpc.getTopicMetadata({ namespace: (_a = options === null || options === void 0 ? void 0 : options.namespace) !== null && _a !== void 0 ? _a : '', topic });
208
+ return {
209
+ namespace: res.namespace,
210
+ topic: res.topic,
211
+ tenantId: res.tenantId,
212
+ messageCount: res.messageCount,
213
+ latestCursor: res.latestCursor,
214
+ lastPublishedAt: res.lastPublishedAt,
215
+ };
216
+ });
217
+ }
142
218
  /**
143
219
  * Yields topic's messages after options.cursor, or from the oldest retained.
144
220
  * If the connection drops, it resubscribes from the last message yielded, so
@@ -167,11 +243,15 @@ class StreamsClient {
167
243
  if (msg.hangup) {
168
244
  return yield __await(void 0);
169
245
  }
170
- failures = 0;
171
246
  for (const entry of msg.entries) {
247
+ const payload = entry.payloadRef
248
+ ? yield __await(this.fetchPayload(entry.payloadRef, signal))
249
+ : entry.payload;
172
250
  ({ cursor } = entry);
251
+ // reset only on delivery, so a payload fetch failing on every reconnect still backs off
252
+ failures = 0;
173
253
  yield yield __await({
174
- payload: entry.payload,
254
+ payload,
175
255
  cursor: entry.cursor,
176
256
  createdAt: entry.createdAt,
177
257
  });
@@ -1,5 +1,7 @@
1
1
  export declare const MinEngineVersion: {
2
2
  readonly SLOT_CONFIG: "v0.78.23";
3
3
  readonly DURABLE_EVICTION: "v0.80.0";
4
+ readonly RETRY_AFTER: "v0.111.0";
4
5
  };
6
+ export declare function supportsRetryAfter(engineVersion: string | undefined): boolean;
5
7
  export declare function supportsEviction(engineVersion: string | undefined): boolean;
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MinEngineVersion = void 0;
4
+ exports.supportsRetryAfter = supportsRetryAfter;
4
5
  exports.supportsEviction = supportsEviction;
5
6
  const deprecation_1 = require("./deprecated/deprecation");
6
7
  exports.MinEngineVersion = {
7
8
  SLOT_CONFIG: 'v0.78.23',
8
9
  DURABLE_EVICTION: 'v0.80.0',
10
+ RETRY_AFTER: 'v0.111.0',
9
11
  };
12
+ function supportsRetryAfter(engineVersion) {
13
+ if (!engineVersion)
14
+ return false;
15
+ return !(0, deprecation_1.semverLessThan)(engineVersion, exports.MinEngineVersion.RETRY_AFTER);
16
+ }
10
17
  function supportsEviction(engineVersion) {
11
18
  if (!engineVersion)
12
19
  return false;
@@ -69,6 +69,7 @@ export declare class InternalWorker {
69
69
  * Keep the signature in sync with the instrumentor wrapper.
70
70
  */
71
71
  handleStartStepRun(action: Action): Promise<Error | undefined>;
72
+ private retryAfterMs;
72
73
  getStepActionEvent(action: Action, eventType: StepActionEventType, shouldNotRetry: boolean, payload?: any, retryCount?: number): StepActionEvent;
73
74
  /**
74
75
  * Handles a START_BATCH action: invokes the registered batch task handler once with
@@ -88,6 +89,7 @@ export declare class InternalWorker {
88
89
  getBatchActionEvent(action: Action, eventType: StepActionEventType, items: {
89
90
  taskRunExternalId: string;
90
91
  eventPayload: string;
92
+ retryAfterMs?: number;
91
93
  }[]): BatchActionEvent;
92
94
  getGroupKeyActionEvent(action: Action, eventType: GroupKeyActionEventType, payload?: any): GroupKeyActionEvent;
93
95
  /**
@@ -349,6 +349,7 @@ class InternalWorker {
349
349
  });
350
350
  const failure = (error) => __awaiter(this, void 0, void 0, function* () {
351
351
  const shouldNotRetry = error instanceof task_1.NonRetryableError;
352
+ const retryAfterMs = this.retryAfterMs(error, actionId);
352
353
  try {
353
354
  if (context.cancelled) {
354
355
  return;
@@ -366,6 +367,7 @@ class InternalWorker {
366
367
  message: error === null || error === void 0 ? void 0 : error.message,
367
368
  stack: error === null || error === void 0 ? void 0 : error.stack,
368
369
  }, action.retryCount);
370
+ event.retryAfterMs = retryAfterMs;
369
371
  yield this.client.dispatcher.sendStepActionEvent(event);
370
372
  }
371
373
  catch (e) {
@@ -423,6 +425,18 @@ class InternalWorker {
423
425
  }
424
426
  });
425
427
  }
428
+ retryAfterMs(error, actionId) {
429
+ var _a;
430
+ if (!(error instanceof task_1.RetryAfterError)) {
431
+ return undefined;
432
+ }
433
+ if (!(0, engine_version_1.supportsRetryAfter)(this.engineVersion)) {
434
+ this.logger.warn(`RetryAfterError requires engine >= ${engine_version_1.MinEngineVersion.RETRY_AFTER} ` +
435
+ `(connected: ${(_a = this.engineVersion) !== null && _a !== void 0 ? _a : 'unknown'}). Task ${actionId} threw it, so the engine ` +
436
+ `will handle the failure with the task's retry policy instead of the requested delay.`);
437
+ }
438
+ return error.afterMs;
439
+ }
426
440
  getStepActionEvent(action, eventType, shouldNotRetry, payload = '', retryCount = 0) {
427
441
  return {
428
442
  workerId: this.name,
@@ -518,7 +532,12 @@ class InternalWorker {
518
532
  sendBatchFailureForAll(action, memberIds, error) {
519
533
  return __awaiter(this, void 0, void 0, function* () {
520
534
  const payload = JSON.stringify({ message: error === null || error === void 0 ? void 0 : error.message, stack: error === null || error === void 0 ? void 0 : error.stack });
521
- const items = memberIds.map((id) => ({ taskRunExternalId: id, eventPayload: payload }));
535
+ const retryAfterMs = this.retryAfterMs(error, action.actionId);
536
+ const items = memberIds.map((id) => ({
537
+ taskRunExternalId: id,
538
+ eventPayload: payload,
539
+ retryAfterMs,
540
+ }));
522
541
  try {
523
542
  yield this.client.dispatcher.sendBatchActionEvent(this.getBatchActionEvent(action, dispatcher_1.StepActionEventType.STEP_EVENT_TYPE_FAILED, items));
524
543
  }
@@ -0,0 +1,34 @@
1
+ import { HatchetClient } from '../..';
2
+ import { StreamEvent } from '../../client/features/streams';
3
+ export declare const MiB: number;
4
+ export declare const maxInlinePayloadBytes: number;
5
+ export declare const describeBenchmark: jest.Describe;
6
+ export type LatencyStats = {
7
+ count: number;
8
+ avgMs: number;
9
+ p50Ms: number;
10
+ p99Ms: number;
11
+ maxMs: number;
12
+ };
13
+ export declare function latencyStats(durationsMs: number[]): LatencyStats;
14
+ export declare function formatLatency(stats: LatencyStats, digits?: number): string;
15
+ export declare function formatSize(bytes: number): string;
16
+ export type PublishLatencyStats = LatencyStats & {
17
+ throughputPerSec: number;
18
+ };
19
+ /**
20
+ * Times `count` sequential publishes, after `warmupCount` unmeasured ones
21
+ * absorb connection setup and topic creation.
22
+ */
23
+ export declare function measurePublishLatency(hatchet: HatchetClient, topic: string, payload: Uint8Array | string, count: number, warmupCount?: number): Promise<PublishLatencyStats>;
24
+ export declare function formatPublishLatency(stats: PublishLatencyStats): string;
25
+ export type LiveSubscription = {
26
+ /** resolves with the next message delivered after the call */
27
+ nextEvent: () => Promise<StreamEvent>;
28
+ close: () => Promise<void>;
29
+ };
30
+ /**
31
+ * Subscribes to topic and resolves once the subscription is live, so a
32
+ * benchmark's timings don't include subscribe setup.
33
+ */
34
+ export declare function openLiveSubscription(hatchet: HatchetClient, topic: string): Promise<LiveSubscription>;
@@ -0,0 +1,120 @@
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
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
12
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
13
+ var m = o[Symbol.asyncIterator], i;
14
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
15
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.describeBenchmark = exports.maxInlinePayloadBytes = exports.MiB = void 0;
20
+ exports.latencyStats = latencyStats;
21
+ exports.formatLatency = formatLatency;
22
+ exports.formatSize = formatSize;
23
+ exports.measurePublishLatency = measurePublishLatency;
24
+ exports.formatPublishLatency = formatPublishLatency;
25
+ exports.openLiveSubscription = openLiveSubscription;
26
+ const perf_hooks_1 = require("perf_hooks");
27
+ exports.MiB = 1024 * 1024;
28
+ // the server's per-message gRPC limit (MaxStreamMessagePayloadBytes); larger payloads are uploaded
29
+ exports.maxInlinePayloadBytes = 4 * exports.MiB - 5 * 1024;
30
+ // benchmarks run only when asked for (test_durable_streams.sh --bench)
31
+ exports.describeBenchmark = process.env.HATCHET_E2E_BENCHMARKS ? describe : describe.skip;
32
+ function percentile(sorted, p) {
33
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
34
+ }
35
+ function latencyStats(durationsMs) {
36
+ const sorted = [...durationsMs].sort((a, b) => a - b);
37
+ return {
38
+ count: durationsMs.length,
39
+ avgMs: durationsMs.reduce((sum, d) => sum + d, 0) / durationsMs.length,
40
+ p50Ms: percentile(sorted, 0.5),
41
+ p99Ms: percentile(sorted, 0.99),
42
+ maxMs: sorted[sorted.length - 1],
43
+ };
44
+ }
45
+ function formatLatency(stats, digits = 2) {
46
+ const fmt = (ms) => ms.toFixed(digits);
47
+ return `avg=${fmt(stats.avgMs)}ms p50=${fmt(stats.p50Ms)}ms p99=${fmt(stats.p99Ms)}ms max=${fmt(stats.maxMs)}ms`;
48
+ }
49
+ function formatSize(bytes) {
50
+ if (bytes % exports.MiB === 0)
51
+ return `${bytes / exports.MiB}MiB`;
52
+ if (bytes % 1024 === 0)
53
+ return `${bytes / 1024}KiB`;
54
+ return `${bytes}B`;
55
+ }
56
+ /**
57
+ * Times `count` sequential publishes, after `warmupCount` unmeasured ones
58
+ * absorb connection setup and topic creation.
59
+ */
60
+ function measurePublishLatency(hatchet_1, topic_1, payload_1, count_1) {
61
+ return __awaiter(this, arguments, void 0, function* (hatchet, topic, payload, count, warmupCount = 5) {
62
+ for (let i = 0; i < warmupCount; i += 1) {
63
+ yield hatchet.streams.publish(topic, payload);
64
+ }
65
+ const durationsMs = [];
66
+ const start = perf_hooks_1.performance.now();
67
+ for (let i = 0; i < count; i += 1) {
68
+ const before = perf_hooks_1.performance.now();
69
+ yield hatchet.streams.publish(topic, payload);
70
+ durationsMs.push(perf_hooks_1.performance.now() - before);
71
+ }
72
+ const totalMs = perf_hooks_1.performance.now() - start;
73
+ return Object.assign(Object.assign({}, latencyStats(durationsMs)), { throughputPerSec: (count / totalMs) * 1000 });
74
+ });
75
+ }
76
+ function formatPublishLatency(stats) {
77
+ return `${formatLatency(stats)} throughput=${stats.throughputPerSec.toFixed(2)}/s`;
78
+ }
79
+ /**
80
+ * Subscribes to topic and resolves once the subscription is live, so a
81
+ * benchmark's timings don't include subscribe setup.
82
+ */
83
+ function openLiveSubscription(hatchet, topic) {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ const abort = new AbortController();
86
+ let onEvent;
87
+ const consumer = (() => __awaiter(this, void 0, void 0, function* () {
88
+ var _a, e_1, _b, _c;
89
+ try {
90
+ for (var _d = true, _e = __asyncValues(hatchet.streams.events(topic, { signal: abort.signal })), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
91
+ _c = _f.value;
92
+ _d = false;
93
+ const ev = _c;
94
+ onEvent === null || onEvent === void 0 ? void 0 : onEvent(ev);
95
+ }
96
+ }
97
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
98
+ finally {
99
+ try {
100
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
101
+ }
102
+ finally { if (e_1) throw e_1.error; }
103
+ }
104
+ }))();
105
+ const nextEvent = () => new Promise((resolve) => {
106
+ onEvent = resolve;
107
+ });
108
+ // the subscription is live once a first message comes through
109
+ const ready = nextEvent();
110
+ yield hatchet.streams.publish(topic, 'ready');
111
+ yield ready;
112
+ return {
113
+ nextEvent,
114
+ close: () => __awaiter(this, void 0, void 0, function* () {
115
+ abort.abort();
116
+ yield consumer;
117
+ }),
118
+ };
119
+ });
120
+ }
@@ -35,21 +35,22 @@ const workflow_14 = require("./durable_callback_ordering/workflow");
35
35
  const workflow_15 = require("./durable_sleep/workflow");
36
36
  const workflow_16 = require("./logger/workflow");
37
37
  const workflow_17 = require("./non_retryable/workflow");
38
- const workflow_18 = require("./on_failure/workflow");
39
- const workflow_19 = require("./idempotency/workflow");
40
- const workflow_20 = require("./on_event/workflow");
41
- const workflow_21 = require("./return_exceptions/workflow");
42
- const workflow_22 = require("./run_details/workflow");
38
+ const workflow_18 = require("./retry_after/workflow");
39
+ const workflow_19 = require("./on_failure/workflow");
40
+ const workflow_20 = require("./idempotency/workflow");
41
+ const workflow_21 = require("./on_event/workflow");
42
+ const workflow_22 = require("./return_exceptions/workflow");
43
+ const workflow_23 = require("./run_details/workflow");
43
44
  const e2e_workflows_1 = require("./simple/e2e-workflows");
44
- const workflow_23 = require("./batch_assign/workflow");
45
- const workflow_24 = require("./streaming/workflow");
46
- const workflow_25 = require("./subscribe_to_stream/workflow");
47
- const workflow_26 = require("./timeout/workflow");
48
- const workflow_27 = require("./webhooks/workflow");
49
- const workflow_28 = require("./child_index/workflow");
50
- const workflow_29 = require("./support_agent/workflow");
51
- const workflow_30 = require("./welcome_email/workflow");
52
- const workflow_31 = require("./pdf_pipeline/workflow");
45
+ const workflow_24 = require("./batch_assign/workflow");
46
+ const workflow_25 = require("./streaming/workflow");
47
+ const workflow_26 = require("./subscribe_to_stream/workflow");
48
+ const workflow_27 = require("./timeout/workflow");
49
+ const workflow_28 = require("./webhooks/workflow");
50
+ const workflow_29 = require("./child_index/workflow");
51
+ const workflow_30 = require("./support_agent/workflow");
52
+ const workflow_31 = require("./welcome_email/workflow");
53
+ const workflow_32 = require("./pdf_pipeline/workflow");
53
54
  const workflows = [
54
55
  workflow_1.bulkChild,
55
56
  workflow_1.bulkParentWorkflow,
@@ -101,43 +102,45 @@ const workflows = [
101
102
  workflow_13.evictableChildBulkSpawn,
102
103
  (0, workflow_16.createLoggingWorkflow)(hatchet_client_1.hatchet),
103
104
  workflow_17.nonRetryableWorkflow,
104
- workflow_18.failureWorkflow,
105
- workflow_19.idempotentTask,
106
- workflow_19.idempotentTaskShortWindow,
107
- workflow_20.lower,
108
- workflow_21.returnExceptionsTask,
109
- workflow_22.runDetailTestWorkflow,
105
+ workflow_18.retryAfterUpstreamDelay,
106
+ workflow_18.retryAfterExponentialBackoff,
107
+ workflow_19.failureWorkflow,
108
+ workflow_20.idempotentTask,
109
+ workflow_20.idempotentTaskShortWindow,
110
+ workflow_21.lower,
111
+ workflow_22.returnExceptionsTask,
112
+ workflow_23.runDetailTestWorkflow,
110
113
  e2e_workflows_1.helloWorld,
111
114
  e2e_workflows_1.helloWorldDurable,
112
- workflow_24.streamingTask,
113
- workflow_25.dagStream,
114
- workflow_25.longStream,
115
- workflow_26.timeoutTask,
116
- workflow_26.refreshTimeoutTask,
117
- workflow_27.webhookWorkflow,
118
- workflow_28.childIndexChild,
119
- workflow_28.childIndexParent,
120
- workflow_28.scenarioTask,
121
- workflow_28.orchestratorTask,
122
- workflow_29.supportAgent,
123
- workflow_29.triageTicket,
124
- workflow_29.generateReply,
125
- workflow_29.escalateTicket,
126
- workflow_30.welcomeEmail,
127
- workflow_31.pdfPipeline,
128
- workflow_23.batchSimple,
129
- workflow_23.batchKeyed,
130
- workflow_23.batchKeyedFailable,
131
- workflow_23.batchKeyedInterval,
132
- workflow_23.batchLarge,
133
- workflow_23.batchSingle,
134
- workflow_23.batchOrdered,
135
- workflow_23.batchBroadcast,
136
- workflow_23.batchCancel,
137
- workflow_23.child,
138
- workflow_23.childBatch,
139
- workflow_23.batchChildSpawn,
140
- workflow_23.batchChildBatchSpawn,
115
+ workflow_25.streamingTask,
116
+ workflow_26.dagStream,
117
+ workflow_26.longStream,
118
+ workflow_27.timeoutTask,
119
+ workflow_27.refreshTimeoutTask,
120
+ workflow_28.webhookWorkflow,
121
+ workflow_29.childIndexChild,
122
+ workflow_29.childIndexParent,
123
+ workflow_29.scenarioTask,
124
+ workflow_29.orchestratorTask,
125
+ workflow_30.supportAgent,
126
+ workflow_30.triageTicket,
127
+ workflow_30.generateReply,
128
+ workflow_30.escalateTicket,
129
+ workflow_31.welcomeEmail,
130
+ workflow_32.pdfPipeline,
131
+ workflow_24.batchSimple,
132
+ workflow_24.batchKeyed,
133
+ workflow_24.batchKeyedFailable,
134
+ workflow_24.batchKeyedInterval,
135
+ workflow_24.batchLarge,
136
+ workflow_24.batchSingle,
137
+ workflow_24.batchOrdered,
138
+ workflow_24.batchBroadcast,
139
+ workflow_24.batchCancel,
140
+ workflow_24.child,
141
+ workflow_24.childBatch,
142
+ workflow_24.batchChildSpawn,
143
+ workflow_24.batchChildBatchSpawn,
141
144
  ];
142
145
  function main() {
143
146
  return __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
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 workflow_1 = require("./workflow");
13
+ function main() {
14
+ return __awaiter(this, void 0, void 0, function* () {
15
+ const res = yield workflow_1.retryAfterUpstreamDelay.run({ failingAttempts: 2 });
16
+ console.log(res);
17
+ });
18
+ }
19
+ if (require.main === module) {
20
+ main();
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
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 hatchet_client_1 = require("../hatchet-client");
13
+ const workflow_1 = require("./workflow");
14
+ function main() {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ const worker = yield hatchet_client_1.hatchet.worker('retry-after-worker', {
17
+ workflows: [workflow_1.retryAfterUpstreamDelay, workflow_1.retryAfterExponentialBackoff],
18
+ });
19
+ yield worker.start();
20
+ });
21
+ }
22
+ if (require.main === module) {
23
+ main();
24
+ }
@@ -0,0 +1,9 @@
1
+ export type UpstreamInput = {
2
+ failingAttempts: number;
3
+ };
4
+ export declare const retryAfterUpstreamDelay: import("../..").TaskWorkflowDeclaration<UpstreamInput, {
5
+ attempt: number;
6
+ }, {}, {}, {}, {}>;
7
+ export declare const retryAfterExponentialBackoff: import("../..").TaskWorkflowDeclaration<UpstreamInput, {
8
+ attempt: number;
9
+ }, {}, {}, {}, {}>;
@@ -0,0 +1,40 @@
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
+ exports.retryAfterExponentialBackoff = exports.retryAfterUpstreamDelay = void 0;
13
+ const task_1 = require("../../task");
14
+ const hatchet_client_1 = require("../hatchet-client");
15
+ // > Retry after an upstream-provided delay
16
+ exports.retryAfterUpstreamDelay = hatchet_client_1.hatchet.task({
17
+ name: 'retry-after-upstream-delay',
18
+ retries: 5,
19
+ fn: (input, ctx) => __awaiter(void 0, void 0, void 0, function* () {
20
+ if (ctx.retryCount() < input.failingAttempts) {
21
+ throw new task_1.RetryAfterError('upstream rate limited', { after: '2s' });
22
+ }
23
+ return { attempt: ctx.retryCount() };
24
+ }),
25
+ });
26
+ // !!
27
+ // > Exponential backoff with full jitter
28
+ exports.retryAfterExponentialBackoff = hatchet_client_1.hatchet.task({
29
+ name: 'retry-after-exponential-backoff',
30
+ retries: 3,
31
+ fn: (input, ctx) => __awaiter(void 0, void 0, void 0, function* () {
32
+ if (ctx.retryCount() < input.failingAttempts) {
33
+ throw new task_1.RetryAfterError('upstream unavailable', {
34
+ after: Math.random() * Math.min(2 ** ctx.retryCount(), 60) * 1000,
35
+ });
36
+ }
37
+ return { attempt: ctx.retryCount() };
38
+ }),
39
+ });
40
+ // !!