@hatchet-dev/typescript-sdk 1.29.3 → 1.30.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/clients/dispatcher/action-listener.d.ts +1 -1
- package/clients/dispatcher/action-listener.js +6 -0
- package/clients/listeners/durable-listener/durable-listener-client.d.ts +6 -1
- package/clients/listeners/durable-listener/durable-listener-client.js +124 -37
- package/clients/rest/generated/Api.d.ts +6 -1
- package/clients/rest/generated/Api.js +1 -1
- package/clients/rest/generated/data-contracts.d.ts +9 -1
- package/clients/rest/generated/data-contracts.js +1 -0
- package/legacy/workflow.d.ts +4 -4
- package/legacy/workflow.js +1 -1
- package/package.json +1 -1
- package/protoc/v1/workflows.d.ts +17 -0
- package/protoc/v1/workflows.js +74 -2
- package/v1/client/worker/context.d.ts +2 -0
- package/v1/client/worker/context.js +26 -8
- package/v1/client/worker/eviction/eviction-manager.d.ts +1 -1
- package/v1/client/worker/eviction/eviction-manager.js +12 -5
- package/v1/client/worker/worker-internal.d.ts +2 -1
- package/v1/client/worker/worker-internal.js +52 -25
- package/v1/embedded.d.ts +36 -2
- package/v1/embedded.js +35 -8
- package/v1/examples/concurrency_cancel_queued_except_newest/workflow.d.ts +9 -0
- package/v1/examples/concurrency_cancel_queued_except_newest/workflow.js +47 -0
- package/v1/examples/concurrency_cancel_queued_except_oldest/workflow.d.ts +9 -0
- package/v1/examples/concurrency_cancel_queued_except_oldest/workflow.js +47 -0
- package/v1/examples/durable_callback_ordering/workflow.d.ts +42 -0
- package/v1/examples/durable_callback_ordering/workflow.js +95 -0
- package/v1/examples/durable_eviction/workflow.d.ts +5 -0
- package/v1/examples/durable_eviction/workflow.js +15 -1
- package/v1/examples/e2e-worker.js +99 -90
- package/version.d.ts +1 -1
- package/version.js +1 -1
|
@@ -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;
|
|
@@ -53,8 +53,15 @@ class DurableEvictionManager {
|
|
|
53
53
|
markActive(key) {
|
|
54
54
|
this._cache.markActive(key);
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// The invocation count pins the teardown to the invocation that was
|
|
57
|
+
// actually evicted: between requesting eviction and the server's ack, the
|
|
58
|
+
// server may restore and re-dispatch the next invocation, which registers
|
|
59
|
+
// under the same action key and must not be torn down.
|
|
60
|
+
_evictRun(key, invocationCount) {
|
|
61
|
+
const current = this._cache.get(key);
|
|
62
|
+
if (current && current.invocationCount !== invocationCount)
|
|
63
|
+
return;
|
|
64
|
+
this._cancelLocal(key, invocationCount);
|
|
58
65
|
this.unregisterRun(key);
|
|
59
66
|
}
|
|
60
67
|
_tickSafe() {
|
|
@@ -89,7 +96,7 @@ class DurableEvictionManager {
|
|
|
89
96
|
this._logger.debug(`DurableEvictionManager: evicting task_run_external_id=${rec.taskRunExternalId} ` +
|
|
90
97
|
`wait_kind=${rec.waitKind} resource_id=${rec.waitResourceId}`);
|
|
91
98
|
yield this._requestEvictionWithAck(key, rec);
|
|
92
|
-
this._evictRun(key);
|
|
99
|
+
this._evictRun(key, rec.invocationCount);
|
|
93
100
|
}
|
|
94
101
|
});
|
|
95
102
|
}
|
|
@@ -101,7 +108,7 @@ class DurableEvictionManager {
|
|
|
101
108
|
if (rec && rec.invocationCount !== invocationCount)
|
|
102
109
|
return;
|
|
103
110
|
this._logger.info(`DurableEvictionManager: server-initiated eviction for task_run_external_id=${taskRunExternalId} invocation_count=${invocationCount}`);
|
|
104
|
-
this._evictRun(key);
|
|
111
|
+
this._evictRun(key, invocationCount);
|
|
105
112
|
}
|
|
106
113
|
evictAllWaiting() {
|
|
107
114
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -122,7 +129,7 @@ class DurableEvictionManager {
|
|
|
122
129
|
// Always cancel locally even if the server ACK failed, so the
|
|
123
130
|
// future settles and exitGracefully doesn't hang.
|
|
124
131
|
// This will get resolved by the reassignment of the task.
|
|
125
|
-
this._evictRun(rec.key);
|
|
132
|
+
this._evictRun(rec.key, rec.invocationCount);
|
|
126
133
|
evicted++;
|
|
127
134
|
}
|
|
128
135
|
return evicted;
|
|
@@ -5,7 +5,7 @@ import { CreateStepRateLimit } from '../../../protoc/workflows';
|
|
|
5
5
|
import { Logger } from '../../../util/logger';
|
|
6
6
|
import { BaseWorkflowDeclaration, WorkflowDefinition, HatchetClient } from '../..';
|
|
7
7
|
import { TaskBatchConfig } from '../../../protoc/v1/workflows';
|
|
8
|
-
import { CreateWorkflowTaskOpts } from '../../task';
|
|
8
|
+
import { Concurrency, CreateWorkflowTaskOpts } from '../../task';
|
|
9
9
|
import { WorkerLabels } from '../../../clients/dispatcher/dispatcher-client';
|
|
10
10
|
import { Duration } from '../duration';
|
|
11
11
|
import { Context } from './context';
|
|
@@ -113,6 +113,7 @@ export declare function mapSlotRequestsPb(task: {
|
|
|
113
113
|
slotCost?: number;
|
|
114
114
|
}, isDurable: boolean): Record<string, number>;
|
|
115
115
|
export declare function mapRateLimitPb(limits: CreateWorkflowTaskOpts<any, any>['rateLimits']): CreateStepRateLimit[];
|
|
116
|
+
export declare function assertValidConcurrencyArr(concurrency: Concurrency[] | undefined): void;
|
|
116
117
|
export declare function mapBatchConfigPb(batch: CreateWorkflowTaskOpts<any, any>['batch']): TaskBatchConfig | undefined;
|
|
117
118
|
export declare function resolveExecutionTimeout(task: {
|
|
118
119
|
executionTimeout?: Duration;
|
|
@@ -55,6 +55,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
exports.InternalWorker = void 0;
|
|
56
56
|
exports.mapSlotRequestsPb = mapSlotRequestsPb;
|
|
57
57
|
exports.mapRateLimitPb = mapRateLimitPb;
|
|
58
|
+
exports.assertValidConcurrencyArr = assertValidConcurrencyArr;
|
|
58
59
|
exports.mapBatchConfigPb = mapBatchConfigPb;
|
|
59
60
|
exports.resolveExecutionTimeout = resolveExecutionTimeout;
|
|
60
61
|
exports.resolveScheduleTimeout = resolveScheduleTimeout;
|
|
@@ -269,6 +270,8 @@ class InternalWorker {
|
|
|
269
270
|
];
|
|
270
271
|
const concurrencyArr = Array.isArray(concurrency) ? concurrency : [];
|
|
271
272
|
const concurrencySolo = !Array.isArray(concurrency) ? concurrency : undefined;
|
|
273
|
+
assertValidConcurrencyArr(concurrencyArr);
|
|
274
|
+
assertValidConcurrencyArr(concurrencySolo ? [concurrencySolo] : undefined);
|
|
272
275
|
// Convert Zod schema to JSON Schema if provided
|
|
273
276
|
let inputJsonSchema;
|
|
274
277
|
if (workflow.inputValidator) {
|
|
@@ -309,7 +312,7 @@ class InternalWorker {
|
|
|
309
312
|
defaultPriority: workflow.defaultPriority,
|
|
310
313
|
inputJsonSchema,
|
|
311
314
|
tasks: [...workflow._tasks, ...workflow._durableTasks].map((task) => {
|
|
312
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l
|
|
315
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
313
316
|
return ({
|
|
314
317
|
readableId: task.name,
|
|
315
318
|
action: `${workflow.name}:${task.name}`,
|
|
@@ -329,15 +332,20 @@ class InternalWorker {
|
|
|
329
332
|
isDurable: durableTaskSet.has(task),
|
|
330
333
|
slotRequests: mapSlotRequestsPb(task, durableTaskSet.has(task)),
|
|
331
334
|
batch: mapBatchConfigPb(batchOf(task)),
|
|
332
|
-
concurrency:
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
concurrency: (() => {
|
|
336
|
+
var _a;
|
|
337
|
+
const taskConcurrency = task.concurrency
|
|
338
|
+
? Array.isArray(task.concurrency)
|
|
339
|
+
? task.concurrency
|
|
340
|
+
: [task.concurrency]
|
|
341
|
+
: ((_a = workflow.taskDefaults) === null || _a === void 0 ? void 0 : _a.concurrency)
|
|
342
|
+
? Array.isArray(workflow.taskDefaults.concurrency)
|
|
343
|
+
? workflow.taskDefaults.concurrency
|
|
344
|
+
: [workflow.taskDefaults.concurrency]
|
|
345
|
+
: [];
|
|
346
|
+
assertValidConcurrencyArr(taskConcurrency);
|
|
347
|
+
return taskConcurrency;
|
|
348
|
+
})(),
|
|
341
349
|
});
|
|
342
350
|
}),
|
|
343
351
|
concurrency: concurrencySolo,
|
|
@@ -375,28 +383,31 @@ class InternalWorker {
|
|
|
375
383
|
const totalDurableSlots = (_c = (_b = (_a = this.slotConfig) === null || _a === void 0 ? void 0 : _a.durable) !== null && _b !== void 0 ? _b : this.durableSlots) !== null && _c !== void 0 ? _c : 0;
|
|
376
384
|
this.evictionManager = new eviction_manager_1.DurableEvictionManager({
|
|
377
385
|
durableSlots: totalDurableSlots,
|
|
378
|
-
cancelLocal: (key) => {
|
|
386
|
+
cancelLocal: (key, invocationCount) => {
|
|
379
387
|
var _a;
|
|
380
388
|
const err = new task_run_terminated_error_1.TaskRunTerminatedError('evicted');
|
|
381
389
|
const ctx = this.contexts[key];
|
|
390
|
+
// A newer invocation may already be registered under this key (the
|
|
391
|
+
// server restored the run before the eviction ack arrived); it must
|
|
392
|
+
// not be aborted for the old invocation's eviction.
|
|
393
|
+
const ctxMatchesEvictedInvocation = ctx && ((_a = ctx.invocationCount) !== null && _a !== void 0 ? _a : 1) === invocationCount;
|
|
382
394
|
if (ctx) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
// Abort before cleanup: waiters must settle as aborted (eviction),
|
|
396
|
+
// not with cleanup's generic rejection, which would surface as a
|
|
397
|
+
// task failure.
|
|
398
|
+
if (ctxMatchesEvictedInvocation && ctx.abortController) {
|
|
386
399
|
ctx.abortController.abort(err);
|
|
387
400
|
}
|
|
401
|
+
this.client.durableListener.cleanupTaskState(ctx.action.taskRunExternalId, invocationCount);
|
|
388
402
|
}
|
|
389
403
|
const future = this.futures[key];
|
|
390
|
-
if (future) {
|
|
404
|
+
if (future && ctxMatchesEvictedInvocation) {
|
|
391
405
|
future.promise.catch(() => undefined);
|
|
392
406
|
future.cancel(hatchet_promise_1.CancellationReason.EVICTED_BY_WORKER);
|
|
393
407
|
}
|
|
394
408
|
},
|
|
395
409
|
requestEvictionWithAck: (key, rec) => __awaiter(this, void 0, void 0, function* () {
|
|
396
|
-
|
|
397
|
-
const ctx = this.contexts[key];
|
|
398
|
-
const invocationCount = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.invocationCount) !== null && _a !== void 0 ? _a : 1;
|
|
399
|
-
yield this.client.durableListener.sendEvictInvocation(rec.taskRunExternalId, invocationCount, rec.evictionReason);
|
|
410
|
+
yield this.client.durableListener.sendEvictInvocation(rec.taskRunExternalId, rec.invocationCount, rec.evictionReason);
|
|
400
411
|
}),
|
|
401
412
|
logger: this.logger,
|
|
402
413
|
});
|
|
@@ -407,12 +418,16 @@ class InternalWorker {
|
|
|
407
418
|
this.evictionManager.start();
|
|
408
419
|
return this.evictionManager;
|
|
409
420
|
}
|
|
410
|
-
cleanupRun(key) {
|
|
421
|
+
cleanupRun(key, attemptContext) {
|
|
411
422
|
var _a;
|
|
412
|
-
const ctx = this.contexts[key];
|
|
423
|
+
const ctx = attemptContext !== null && attemptContext !== void 0 ? attemptContext : this.contexts[key];
|
|
413
424
|
if (ctx instanceof context_1.DurableContext) {
|
|
414
425
|
this.client.durableListener.cleanupTaskState(ctx.action.taskRunExternalId, ctx.invocationCount);
|
|
415
426
|
}
|
|
427
|
+
// A restored invocation may have re-registered under this key while this
|
|
428
|
+
// attempt was shutting down; the key-owned state now belongs to it.
|
|
429
|
+
if (attemptContext && this.contexts[key] !== attemptContext)
|
|
430
|
+
return;
|
|
416
431
|
(_a = this.evictionManager) === null || _a === void 0 ? void 0 : _a.unregisterRun(key);
|
|
417
432
|
delete this.futures[key];
|
|
418
433
|
delete this.contexts[key];
|
|
@@ -448,7 +463,7 @@ class InternalWorker {
|
|
|
448
463
|
if (!step) {
|
|
449
464
|
this.logger.error(`Registered actions: '${Object.keys(this.action_registry).join(', ')}'`);
|
|
450
465
|
this.logger.error(`Could not find step '${actionId}'`);
|
|
451
|
-
this.cleanupRun(actionKey);
|
|
466
|
+
this.cleanupRun(actionKey, context);
|
|
452
467
|
return;
|
|
453
468
|
}
|
|
454
469
|
const run = () => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -509,7 +524,7 @@ class InternalWorker {
|
|
|
509
524
|
this.logger.error(`Could not send action event: ${actionEventError.message || actionEventError}`);
|
|
510
525
|
}
|
|
511
526
|
finally {
|
|
512
|
-
this.cleanupRun(actionKey);
|
|
527
|
+
this.cleanupRun(actionKey, context);
|
|
513
528
|
}
|
|
514
529
|
});
|
|
515
530
|
const failure = (error) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -518,6 +533,11 @@ class InternalWorker {
|
|
|
518
533
|
if (context.cancelled) {
|
|
519
534
|
return;
|
|
520
535
|
}
|
|
536
|
+
// The run was evicted or cancelled by the worker, not failed by
|
|
537
|
+
// user code; the server already accounts for it.
|
|
538
|
+
if ((0, task_run_terminated_error_1.isTaskRunTerminatedError)(error)) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
521
541
|
this.logger.error((0, logger_1.taskRunLog)(taskName, taskRunExternalId, `failed: ${error.message}`));
|
|
522
542
|
if (error.stack) {
|
|
523
543
|
this.logger.error(error.stack);
|
|
@@ -532,7 +552,7 @@ class InternalWorker {
|
|
|
532
552
|
this.logger.error(`Could not send action event: ${e.message}`);
|
|
533
553
|
}
|
|
534
554
|
finally {
|
|
535
|
-
this.cleanupRun(actionKey);
|
|
555
|
+
this.cleanupRun(actionKey, context);
|
|
536
556
|
}
|
|
537
557
|
});
|
|
538
558
|
const future = new hatchet_promise_1.default((() => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -573,7 +593,7 @@ class InternalWorker {
|
|
|
573
593
|
}
|
|
574
594
|
}
|
|
575
595
|
finally {
|
|
576
|
-
this.cleanupRun(actionKey);
|
|
596
|
+
this.cleanupRun(actionKey, context);
|
|
577
597
|
}
|
|
578
598
|
}
|
|
579
599
|
catch (e) {
|
|
@@ -1096,6 +1116,13 @@ function parseBatchPayload(actionPayload) {
|
|
|
1096
1116
|
function batchOf(task) {
|
|
1097
1117
|
return 'batch' in task ? task.batch : undefined;
|
|
1098
1118
|
}
|
|
1119
|
+
function assertValidConcurrencyArr(concurrency) {
|
|
1120
|
+
concurrency === null || concurrency === void 0 ? void 0 : concurrency.forEach((c) => {
|
|
1121
|
+
if (c.maxRuns !== undefined && (!Number.isInteger(c.maxRuns) || c.maxRuns <= 0)) {
|
|
1122
|
+
throw new Error(`concurrency.maxRuns must be a positive integer when provided, got: ${c.maxRuns}`);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1099
1126
|
function mapBatchConfigPb(batch) {
|
|
1100
1127
|
if (!batch) {
|
|
1101
1128
|
return undefined;
|
package/v1/embedded.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AxiosRequestConfig } from 'axios';
|
|
2
2
|
import type { ClientConfig, HatchetClientOptions } from '../clients/hatchet-client';
|
|
3
3
|
import { HatchetClient } from './client/client';
|
|
4
|
+
/**
|
|
5
|
+
* Options for the embedded engine sidecar. All fields are optional; by default the
|
|
6
|
+
* latest hatchet-embedded release is downloaded and started with a bundled Postgres.
|
|
7
|
+
*/
|
|
4
8
|
export interface EmbeddedOptions {
|
|
5
9
|
/**
|
|
6
10
|
* hatchet-embedded release tag to download (defaults to HATCHET_CLIENT_EMBEDDED_VERSION or
|
|
@@ -20,7 +24,9 @@ export interface EmbeddedOptions {
|
|
|
20
24
|
databaseUrl?: string;
|
|
21
25
|
/** store the bundled Postgres runtime and data under this directory */
|
|
22
26
|
postgresDataDir?: string;
|
|
27
|
+
/** bind the engine's gRPC server to this port */
|
|
23
28
|
grpcPort?: number;
|
|
29
|
+
/** bind the REST API server to this port */
|
|
24
30
|
apiPort?: number;
|
|
25
31
|
/** set to false to start only the engine + gRPC, no REST API */
|
|
26
32
|
startApi?: boolean;
|
|
@@ -28,14 +34,25 @@ export interface EmbeddedOptions {
|
|
|
28
34
|
runMigrations?: boolean;
|
|
29
35
|
/** use RabbitMQ instead of the Postgres message queue */
|
|
30
36
|
rabbitmqUrl?: string;
|
|
37
|
+
/** log level for the engine's output */
|
|
31
38
|
logLevel?: string;
|
|
39
|
+
/** how long to wait for the engine to become ready, in milliseconds (default 300000) */
|
|
32
40
|
readyTimeoutMs?: number;
|
|
33
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* A running embedded engine sidecar and its connection details, as returned by
|
|
44
|
+
* `startEmbeddedSidecar`.
|
|
45
|
+
*/
|
|
34
46
|
export interface EmbeddedSidecar {
|
|
47
|
+
/** API token for the sidecar's default tenant */
|
|
35
48
|
token: string;
|
|
49
|
+
/** ID of the sidecar's default tenant */
|
|
36
50
|
tenantId: string;
|
|
51
|
+
/** host:port of the engine's gRPC server */
|
|
37
52
|
grpcAddress: string;
|
|
53
|
+
/** base URL of the REST API (empty when `startApi` is false) */
|
|
38
54
|
apiUrl: string;
|
|
55
|
+
/** gracefully stops the sidecar and resolves once it has fully exited */
|
|
39
56
|
stop: () => Promise<void>;
|
|
40
57
|
}
|
|
41
58
|
/**
|
|
@@ -46,12 +63,25 @@ export interface EmbeddedSidecar {
|
|
|
46
63
|
* your program has returned.
|
|
47
64
|
*/
|
|
48
65
|
export declare function stopEmbeddedSidecar(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Downloads (and caches) the hatchet-embedded sidecar binary, spawns it, and waits
|
|
68
|
+
* until the embedded engine is ready. The sidecar shuts down when this process exits.
|
|
69
|
+
* Use {@link HatchetEmbeddedClient.init} unless you need the raw connection details.
|
|
70
|
+
*/
|
|
49
71
|
export declare function startEmbeddedSidecar(opts?: EmbeddedOptions): Promise<EmbeddedSidecar>;
|
|
72
|
+
/**
|
|
73
|
+
* Entry point for embedded mode. `init()` starts a full local Hatchet engine and
|
|
74
|
+
* returns a regular Hatchet client connected to it. No API token or Docker is needed.
|
|
75
|
+
* See the [embedded mode guide](https://docs.hatchet.run/v1/embedded).
|
|
76
|
+
*/
|
|
50
77
|
export declare class HatchetEmbeddedClient {
|
|
51
78
|
/**
|
|
52
79
|
* Runs a full Hatchet engine locally via the hatchet-embedded sidecar (downloaded
|
|
53
|
-
* on first use) and returns a client wired to it.
|
|
54
|
-
*
|
|
80
|
+
* on first use) and returns a client wired to it. No API token or Docker is needed,
|
|
81
|
+
* which makes this a good fit for local development and CI. By default the sidecar
|
|
82
|
+
* starts a bundled Postgres; pass `databaseUrl` to point it at your own instead.
|
|
83
|
+
*
|
|
84
|
+
* See the [embedded mode guide](https://docs.hatchet.run/v1/embedded).
|
|
55
85
|
* @param embeddedOpts - Options for the embedded engine (version, ports, database, ...).
|
|
56
86
|
* @param config - Optional configuration overrides for the client.
|
|
57
87
|
* @param options - Optional client options.
|
|
@@ -60,6 +90,10 @@ export declare class HatchetEmbeddedClient {
|
|
|
60
90
|
*/
|
|
61
91
|
static init<T extends Record<string, any> = {}, U extends Record<string, any> = {}>(embeddedOpts?: EmbeddedOptions, config?: Omit<Partial<ClientConfig>, 'middleware'>, options?: HatchetClientOptions, axiosConfig?: AxiosRequestConfig): Promise<EmbeddedClient<T, U>>;
|
|
62
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A `HatchetClient` connected to an embedded engine, extended with `stopEmbedded`.
|
|
95
|
+
* Returned by {@link HatchetEmbeddedClient.init}.
|
|
96
|
+
*/
|
|
63
97
|
export type EmbeddedClient<T extends Record<string, any> = {}, U extends Record<string, any> = {}> = HatchetClient<T, U> & {
|
|
64
98
|
/**
|
|
65
99
|
* Gracefully stops the embedded engine sidecar and resolves once it has
|
package/v1/embedded.js
CHANGED
|
@@ -52,6 +52,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
52
52
|
exports.HatchetEmbeddedClient = void 0;
|
|
53
53
|
exports.stopEmbeddedSidecar = stopEmbeddedSidecar;
|
|
54
54
|
exports.startEmbeddedSidecar = startEmbeddedSidecar;
|
|
55
|
+
/**
|
|
56
|
+
* Embedded mode runs a full local Hatchet engine as a sidecar process managed by your
|
|
57
|
+
* application. It needs no API token, no Docker, and no external services (by default
|
|
58
|
+
* the sidecar starts a bundled Postgres), which makes it a good fit for local
|
|
59
|
+
* development and CI.
|
|
60
|
+
*
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { HatchetEmbeddedClient } from './embedded.js';
|
|
63
|
+
*
|
|
64
|
+
* const hatchet = await HatchetEmbeddedClient.init();
|
|
65
|
+
* // ... register workers and run tasks as usual ...
|
|
66
|
+
* await hatchet.stopEmbedded();
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* See the [embedded mode guide](https://docs.hatchet.run/v1/embedded) for setup and
|
|
70
|
+
* configuration details.
|
|
71
|
+
*
|
|
72
|
+
* @module Embedded
|
|
73
|
+
*/
|
|
55
74
|
const child_process_1 = require("child_process");
|
|
56
75
|
const client_1 = require("./client/client");
|
|
57
76
|
const crypto_1 = require("crypto");
|
|
@@ -211,12 +230,7 @@ function waitForHandshake(child, handshakePath, timeoutMs) {
|
|
|
211
230
|
throw new Error(`hatchet embedded sidecar did not become ready within ${timeoutMs}ms`);
|
|
212
231
|
});
|
|
213
232
|
}
|
|
214
|
-
|
|
215
|
-
* Downloads (and caches) the hatchet-embedded sidecar binary, spawns it, and
|
|
216
|
-
* waits until the embedded engine is ready. The sidecar shuts down when this
|
|
217
|
-
* process exits. Use `HatchetEmbedded()` unless you need the raw
|
|
218
|
-
* connection details.
|
|
219
|
-
*/
|
|
233
|
+
// sidecars started in this process that have not been stopped yet
|
|
220
234
|
const activeSidecars = new Set();
|
|
221
235
|
/**
|
|
222
236
|
* Gracefully stops every sidecar started in this process by
|
|
@@ -232,6 +246,11 @@ function stopEmbeddedSidecar() {
|
|
|
232
246
|
}
|
|
233
247
|
});
|
|
234
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Downloads (and caches) the hatchet-embedded sidecar binary, spawns it, and waits
|
|
251
|
+
* until the embedded engine is ready. The sidecar shuts down when this process exits.
|
|
252
|
+
* Use {@link HatchetEmbeddedClient.init} unless you need the raw connection details.
|
|
253
|
+
*/
|
|
235
254
|
function startEmbeddedSidecar() {
|
|
236
255
|
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
|
237
256
|
var _a, _b;
|
|
@@ -321,11 +340,19 @@ function startEmbeddedSidecar() {
|
|
|
321
340
|
return sidecar;
|
|
322
341
|
});
|
|
323
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Entry point for embedded mode. `init()` starts a full local Hatchet engine and
|
|
345
|
+
* returns a regular Hatchet client connected to it. No API token or Docker is needed.
|
|
346
|
+
* See the [embedded mode guide](https://docs.hatchet.run/v1/embedded).
|
|
347
|
+
*/
|
|
324
348
|
class HatchetEmbeddedClient {
|
|
325
349
|
/**
|
|
326
350
|
* Runs a full Hatchet engine locally via the hatchet-embedded sidecar (downloaded
|
|
327
|
-
* on first use) and returns a client wired to it.
|
|
328
|
-
*
|
|
351
|
+
* on first use) and returns a client wired to it. No API token or Docker is needed,
|
|
352
|
+
* which makes this a good fit for local development and CI. By default the sidecar
|
|
353
|
+
* starts a bundled Postgres; pass `databaseUrl` to point it at your own instead.
|
|
354
|
+
*
|
|
355
|
+
* See the [embedded mode guide](https://docs.hatchet.run/v1/embedded).
|
|
329
356
|
* @param embeddedOpts - Options for the embedded engine (version, ports, database, ...).
|
|
330
357
|
* @param config - Optional configuration overrides for the client.
|
|
331
358
|
* @param options - Optional client options.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmptyTaskOutput } from '../concurrency-types';
|
|
2
|
+
export type WorkflowInput = {
|
|
3
|
+
group: string;
|
|
4
|
+
};
|
|
5
|
+
export type WorkflowOutput = {
|
|
6
|
+
step1: EmptyTaskOutput;
|
|
7
|
+
step2: EmptyTaskOutput;
|
|
8
|
+
};
|
|
9
|
+
export declare const concurrencyCancelQueuedExceptNewestWorkflow: import("../..").WorkflowDeclaration<WorkflowInput, WorkflowOutput, {}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.concurrencyCancelQueuedExceptNewestWorkflow = void 0;
|
|
16
|
+
const sleep_1 = __importDefault(require("../../../util/sleep"));
|
|
17
|
+
const v1_1 = require("../..");
|
|
18
|
+
const hatchet_client_1 = require("../hatchet-client");
|
|
19
|
+
// > Cancel Queued Except Newest
|
|
20
|
+
exports.concurrencyCancelQueuedExceptNewestWorkflow = hatchet_client_1.hatchet.workflow({
|
|
21
|
+
name: 'concurrencycancelqueuedexceptnewest',
|
|
22
|
+
concurrency: {
|
|
23
|
+
expression: 'input.group',
|
|
24
|
+
maxRuns: 1,
|
|
25
|
+
limitStrategy: v1_1.ConcurrencyLimitStrategy.CANCEL_QUEUED_EXCEPT_NEWEST,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
// !!
|
|
29
|
+
const step1 = exports.concurrencyCancelQueuedExceptNewestWorkflow.task({
|
|
30
|
+
name: 'step1',
|
|
31
|
+
fn: (_, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
32
|
+
for (let i = 0; i < 50; i += 1) {
|
|
33
|
+
yield (0, sleep_1.default)(20, ctx.abortController.signal);
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
exports.concurrencyCancelQueuedExceptNewestWorkflow.task({
|
|
39
|
+
name: 'step2',
|
|
40
|
+
parents: [step1],
|
|
41
|
+
fn: (_, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
42
|
+
for (let i = 0; i < 50; i += 1) {
|
|
43
|
+
yield (0, sleep_1.default)(20, ctx.abortController.signal);
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmptyTaskOutput } from '../concurrency-types';
|
|
2
|
+
export type WorkflowInput = {
|
|
3
|
+
group: string;
|
|
4
|
+
};
|
|
5
|
+
export type WorkflowOutput = {
|
|
6
|
+
step1: EmptyTaskOutput;
|
|
7
|
+
step2: EmptyTaskOutput;
|
|
8
|
+
};
|
|
9
|
+
export declare const concurrencyCancelQueuedExceptOldestWorkflow: import("../..").WorkflowDeclaration<WorkflowInput, WorkflowOutput, {}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.concurrencyCancelQueuedExceptOldestWorkflow = void 0;
|
|
16
|
+
const sleep_1 = __importDefault(require("../../../util/sleep"));
|
|
17
|
+
const v1_1 = require("../..");
|
|
18
|
+
const hatchet_client_1 = require("../hatchet-client");
|
|
19
|
+
// > Cancel Queued Except Oldest
|
|
20
|
+
exports.concurrencyCancelQueuedExceptOldestWorkflow = hatchet_client_1.hatchet.workflow({
|
|
21
|
+
name: 'concurrencycancelqueuedexceptoldest',
|
|
22
|
+
concurrency: {
|
|
23
|
+
expression: 'input.group',
|
|
24
|
+
maxRuns: 1,
|
|
25
|
+
limitStrategy: v1_1.ConcurrencyLimitStrategy.CANCEL_QUEUED_EXCEPT_OLDEST,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
// !!
|
|
29
|
+
const step1 = exports.concurrencyCancelQueuedExceptOldestWorkflow.task({
|
|
30
|
+
name: 'step1',
|
|
31
|
+
fn: (_, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
32
|
+
for (let i = 0; i < 50; i += 1) {
|
|
33
|
+
yield (0, sleep_1.default)(20, ctx.abortController.signal);
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
exports.concurrencyCancelQueuedExceptOldestWorkflow.task({
|
|
39
|
+
name: 'step2',
|
|
40
|
+
parents: [step1],
|
|
41
|
+
fn: (_, ctx) => __awaiter(void 0, void 0, void 0, function* () {
|
|
42
|
+
for (let i = 0; i < 50; i += 1) {
|
|
43
|
+
yield (0, sleep_1.default)(20, ctx.abortController.signal);
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type LeafInput = {
|
|
2
|
+
mid: number;
|
|
3
|
+
branch: number;
|
|
4
|
+
delayMs: number;
|
|
5
|
+
generation: number;
|
|
6
|
+
};
|
|
7
|
+
export type LeafOutput = {
|
|
8
|
+
mid: number;
|
|
9
|
+
branch: number;
|
|
10
|
+
generation: number;
|
|
11
|
+
};
|
|
12
|
+
export type MidInput = {
|
|
13
|
+
mid: number;
|
|
14
|
+
branches: number;
|
|
15
|
+
childDelayMs: number;
|
|
16
|
+
delayStepMs: number;
|
|
17
|
+
};
|
|
18
|
+
export type MidOutput = {
|
|
19
|
+
mid: number;
|
|
20
|
+
completedBranches: number[];
|
|
21
|
+
invocationCount: number;
|
|
22
|
+
};
|
|
23
|
+
export type RootInput = {
|
|
24
|
+
durables?: number;
|
|
25
|
+
branches?: number;
|
|
26
|
+
childDelayMs?: number;
|
|
27
|
+
delayStepMs?: number;
|
|
28
|
+
};
|
|
29
|
+
export type RootOutput = {
|
|
30
|
+
rootInvocationCount: number;
|
|
31
|
+
midInvocationCounts: number[];
|
|
32
|
+
completedMids: number[];
|
|
33
|
+
};
|
|
34
|
+
export declare const ROOT_DEFAULTS: {
|
|
35
|
+
durables: number;
|
|
36
|
+
branches: number;
|
|
37
|
+
childDelayMs: number;
|
|
38
|
+
delayStepMs: number;
|
|
39
|
+
};
|
|
40
|
+
export declare const callbackOrderingLeaf: import("../..").TaskWorkflowDeclaration<LeafInput, LeafOutput, {}, {}, {}, {}>;
|
|
41
|
+
export declare const callbackOrderingMid: import("../..").TaskWorkflowDeclaration<MidInput, MidOutput, {}, {}, {}, {}>;
|
|
42
|
+
export declare const callbackOrderingRoot: import("../..").TaskWorkflowDeclaration<RootInput, RootOutput, {}, {}, {}, {}>;
|