@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.
- 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/package.json +1 -1
- package/protoc/v1/workflows.d.ts +15 -0
- package/protoc/v1/workflows.js +62 -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 +15 -0
- package/v1/client/worker/worker-internal.js +66 -29
- package/v1/declaration.d.ts +3 -1
- package/v1/embedded.d.ts +36 -2
- package/v1/embedded.js +35 -8
- package/v1/examples/concurrency_dynamic/workflow.d.ts +10 -0
- package/v1/examples/concurrency_dynamic/workflow.js +32 -0
- package/v1/examples/concurrency_shared/workflow.d.ts +17 -0
- package/v1/examples/concurrency_shared/workflow.js +70 -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 +97 -88
- package/v1/task.d.ts +24 -3
- package/version.d.ts +1 -1
- package/version.js +1 -1
|
@@ -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;
|
|
@@ -113,6 +113,21 @@ 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 mapConcurrencyPb(entries: Concurrency[]): {
|
|
117
|
+
expression: string;
|
|
118
|
+
maxRuns: number | undefined;
|
|
119
|
+
limitStrategy: import("../..").ConcurrencyLimitStrategy | undefined;
|
|
120
|
+
name: string | undefined;
|
|
121
|
+
isTenantScoped: boolean | undefined;
|
|
122
|
+
maxRunsExpression: string | undefined;
|
|
123
|
+
}[];
|
|
124
|
+
export declare function taskConcurrencyArr(task: {
|
|
125
|
+
concurrency?: Concurrency | Concurrency[];
|
|
126
|
+
}, workflow: {
|
|
127
|
+
taskDefaults?: {
|
|
128
|
+
concurrency?: Concurrency | Concurrency[];
|
|
129
|
+
};
|
|
130
|
+
}): Concurrency[];
|
|
116
131
|
export declare function assertValidConcurrencyArr(concurrency: Concurrency[] | undefined): void;
|
|
117
132
|
export declare function mapBatchConfigPb(batch: CreateWorkflowTaskOpts<any, any>['batch']): TaskBatchConfig | undefined;
|
|
118
133
|
export declare function resolveExecutionTimeout(task: {
|
|
@@ -55,6 +55,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
exports.InternalWorker = void 0;
|
|
56
56
|
exports.mapSlotRequestsPb = mapSlotRequestsPb;
|
|
57
57
|
exports.mapRateLimitPb = mapRateLimitPb;
|
|
58
|
+
exports.mapConcurrencyPb = mapConcurrencyPb;
|
|
59
|
+
exports.taskConcurrencyArr = taskConcurrencyArr;
|
|
58
60
|
exports.assertValidConcurrencyArr = assertValidConcurrencyArr;
|
|
59
61
|
exports.mapBatchConfigPb = mapBatchConfigPb;
|
|
60
62
|
exports.resolveExecutionTimeout = resolveExecutionTimeout;
|
|
@@ -307,7 +309,7 @@ class InternalWorker {
|
|
|
307
309
|
eventTriggers,
|
|
308
310
|
cronTriggers,
|
|
309
311
|
sticky: stickyStrategy,
|
|
310
|
-
concurrencyArr,
|
|
312
|
+
concurrencyArr: mapConcurrencyPb(concurrencyArr),
|
|
311
313
|
onFailureTask,
|
|
312
314
|
defaultPriority: workflow.defaultPriority,
|
|
313
315
|
inputJsonSchema,
|
|
@@ -333,22 +335,13 @@ class InternalWorker {
|
|
|
333
335
|
slotRequests: mapSlotRequestsPb(task, durableTaskSet.has(task)),
|
|
334
336
|
batch: mapBatchConfigPb(batchOf(task)),
|
|
335
337
|
concurrency: (() => {
|
|
336
|
-
|
|
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
|
-
: [];
|
|
338
|
+
const taskConcurrency = taskConcurrencyArr(task, workflow);
|
|
346
339
|
assertValidConcurrencyArr(taskConcurrency);
|
|
347
|
-
return taskConcurrency;
|
|
340
|
+
return mapConcurrencyPb(taskConcurrency);
|
|
348
341
|
})(),
|
|
349
342
|
});
|
|
350
343
|
}),
|
|
351
|
-
concurrency: concurrencySolo,
|
|
344
|
+
concurrency: concurrencySolo ? mapConcurrencyPb([concurrencySolo])[0] : undefined,
|
|
352
345
|
defaultFilters: (_w = (_v = workflow.defaultFilters) === null || _v === void 0 ? void 0 : _v.map((f) => ({
|
|
353
346
|
scope: f.scope,
|
|
354
347
|
expression: f.expression,
|
|
@@ -383,28 +376,31 @@ class InternalWorker {
|
|
|
383
376
|
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;
|
|
384
377
|
this.evictionManager = new eviction_manager_1.DurableEvictionManager({
|
|
385
378
|
durableSlots: totalDurableSlots,
|
|
386
|
-
cancelLocal: (key) => {
|
|
379
|
+
cancelLocal: (key, invocationCount) => {
|
|
387
380
|
var _a;
|
|
388
381
|
const err = new task_run_terminated_error_1.TaskRunTerminatedError('evicted');
|
|
389
382
|
const ctx = this.contexts[key];
|
|
383
|
+
// A newer invocation may already be registered under this key (the
|
|
384
|
+
// server restored the run before the eviction ack arrived); it must
|
|
385
|
+
// not be aborted for the old invocation's eviction.
|
|
386
|
+
const ctxMatchesEvictedInvocation = ctx && ((_a = ctx.invocationCount) !== null && _a !== void 0 ? _a : 1) === invocationCount;
|
|
390
387
|
if (ctx) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
388
|
+
// Abort before cleanup: waiters must settle as aborted (eviction),
|
|
389
|
+
// not with cleanup's generic rejection, which would surface as a
|
|
390
|
+
// task failure.
|
|
391
|
+
if (ctxMatchesEvictedInvocation && ctx.abortController) {
|
|
394
392
|
ctx.abortController.abort(err);
|
|
395
393
|
}
|
|
394
|
+
this.client.durableListener.cleanupTaskState(ctx.action.taskRunExternalId, invocationCount);
|
|
396
395
|
}
|
|
397
396
|
const future = this.futures[key];
|
|
398
|
-
if (future) {
|
|
397
|
+
if (future && ctxMatchesEvictedInvocation) {
|
|
399
398
|
future.promise.catch(() => undefined);
|
|
400
399
|
future.cancel(hatchet_promise_1.CancellationReason.EVICTED_BY_WORKER);
|
|
401
400
|
}
|
|
402
401
|
},
|
|
403
402
|
requestEvictionWithAck: (key, rec) => __awaiter(this, void 0, void 0, function* () {
|
|
404
|
-
|
|
405
|
-
const ctx = this.contexts[key];
|
|
406
|
-
const invocationCount = (_a = ctx === null || ctx === void 0 ? void 0 : ctx.invocationCount) !== null && _a !== void 0 ? _a : 1;
|
|
407
|
-
yield this.client.durableListener.sendEvictInvocation(rec.taskRunExternalId, invocationCount, rec.evictionReason);
|
|
403
|
+
yield this.client.durableListener.sendEvictInvocation(rec.taskRunExternalId, rec.invocationCount, rec.evictionReason);
|
|
408
404
|
}),
|
|
409
405
|
logger: this.logger,
|
|
410
406
|
});
|
|
@@ -415,12 +411,16 @@ class InternalWorker {
|
|
|
415
411
|
this.evictionManager.start();
|
|
416
412
|
return this.evictionManager;
|
|
417
413
|
}
|
|
418
|
-
cleanupRun(key) {
|
|
414
|
+
cleanupRun(key, attemptContext) {
|
|
419
415
|
var _a;
|
|
420
|
-
const ctx = this.contexts[key];
|
|
416
|
+
const ctx = attemptContext !== null && attemptContext !== void 0 ? attemptContext : this.contexts[key];
|
|
421
417
|
if (ctx instanceof context_1.DurableContext) {
|
|
422
418
|
this.client.durableListener.cleanupTaskState(ctx.action.taskRunExternalId, ctx.invocationCount);
|
|
423
419
|
}
|
|
420
|
+
// A restored invocation may have re-registered under this key while this
|
|
421
|
+
// attempt was shutting down; the key-owned state now belongs to it.
|
|
422
|
+
if (attemptContext && this.contexts[key] !== attemptContext)
|
|
423
|
+
return;
|
|
424
424
|
(_a = this.evictionManager) === null || _a === void 0 ? void 0 : _a.unregisterRun(key);
|
|
425
425
|
delete this.futures[key];
|
|
426
426
|
delete this.contexts[key];
|
|
@@ -456,7 +456,7 @@ class InternalWorker {
|
|
|
456
456
|
if (!step) {
|
|
457
457
|
this.logger.error(`Registered actions: '${Object.keys(this.action_registry).join(', ')}'`);
|
|
458
458
|
this.logger.error(`Could not find step '${actionId}'`);
|
|
459
|
-
this.cleanupRun(actionKey);
|
|
459
|
+
this.cleanupRun(actionKey, context);
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
462
|
const run = () => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -517,7 +517,7 @@ class InternalWorker {
|
|
|
517
517
|
this.logger.error(`Could not send action event: ${actionEventError.message || actionEventError}`);
|
|
518
518
|
}
|
|
519
519
|
finally {
|
|
520
|
-
this.cleanupRun(actionKey);
|
|
520
|
+
this.cleanupRun(actionKey, context);
|
|
521
521
|
}
|
|
522
522
|
});
|
|
523
523
|
const failure = (error) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -526,6 +526,11 @@ class InternalWorker {
|
|
|
526
526
|
if (context.cancelled) {
|
|
527
527
|
return;
|
|
528
528
|
}
|
|
529
|
+
// The run was evicted or cancelled by the worker, not failed by
|
|
530
|
+
// user code; the server already accounts for it.
|
|
531
|
+
if ((0, task_run_terminated_error_1.isTaskRunTerminatedError)(error)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
529
534
|
this.logger.error((0, logger_1.taskRunLog)(taskName, taskRunExternalId, `failed: ${error.message}`));
|
|
530
535
|
if (error.stack) {
|
|
531
536
|
this.logger.error(error.stack);
|
|
@@ -540,7 +545,7 @@ class InternalWorker {
|
|
|
540
545
|
this.logger.error(`Could not send action event: ${e.message}`);
|
|
541
546
|
}
|
|
542
547
|
finally {
|
|
543
|
-
this.cleanupRun(actionKey);
|
|
548
|
+
this.cleanupRun(actionKey, context);
|
|
544
549
|
}
|
|
545
550
|
});
|
|
546
551
|
const future = new hatchet_promise_1.default((() => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -581,7 +586,7 @@ class InternalWorker {
|
|
|
581
586
|
}
|
|
582
587
|
}
|
|
583
588
|
finally {
|
|
584
|
-
this.cleanupRun(actionKey);
|
|
589
|
+
this.cleanupRun(actionKey, context);
|
|
585
590
|
}
|
|
586
591
|
}
|
|
587
592
|
catch (e) {
|
|
@@ -1104,10 +1109,42 @@ function parseBatchPayload(actionPayload) {
|
|
|
1104
1109
|
function batchOf(task) {
|
|
1105
1110
|
return 'batch' in task ? task.batch : undefined;
|
|
1106
1111
|
}
|
|
1112
|
+
// mapConcurrencyPb maps SDK concurrency entries onto the proto shape; entries keep their
|
|
1113
|
+
// declared order, which is the chain order.
|
|
1114
|
+
function mapConcurrencyPb(entries) {
|
|
1115
|
+
return entries.map((c) => ({
|
|
1116
|
+
expression: c.expression,
|
|
1117
|
+
// a string maxRuns is a CEL expression; the static field then carries the default
|
|
1118
|
+
// of 1, which only governs slots created before the expression existed
|
|
1119
|
+
maxRuns: typeof c.maxRuns === 'string' ? 1 : c.maxRuns,
|
|
1120
|
+
limitStrategy: c.limitStrategy,
|
|
1121
|
+
name: c.name,
|
|
1122
|
+
isTenantScoped: c.isTenantScoped,
|
|
1123
|
+
maxRunsExpression: typeof c.maxRuns === 'string' ? c.maxRuns : undefined,
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
function taskConcurrencyArr(task, workflow) {
|
|
1127
|
+
var _a;
|
|
1128
|
+
if (task.concurrency) {
|
|
1129
|
+
return Array.isArray(task.concurrency) ? task.concurrency : [task.concurrency];
|
|
1130
|
+
}
|
|
1131
|
+
if ((_a = workflow.taskDefaults) === null || _a === void 0 ? void 0 : _a.concurrency) {
|
|
1132
|
+
return Array.isArray(workflow.taskDefaults.concurrency)
|
|
1133
|
+
? workflow.taskDefaults.concurrency
|
|
1134
|
+
: [workflow.taskDefaults.concurrency];
|
|
1135
|
+
}
|
|
1136
|
+
return [];
|
|
1137
|
+
}
|
|
1107
1138
|
function assertValidConcurrencyArr(concurrency) {
|
|
1108
1139
|
concurrency === null || concurrency === void 0 ? void 0 : concurrency.forEach((c) => {
|
|
1140
|
+
if (typeof c.maxRuns === 'string') {
|
|
1141
|
+
if (!c.maxRuns.trim()) {
|
|
1142
|
+
throw new Error('concurrency.maxRuns expression must be non-empty');
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1109
1146
|
if (c.maxRuns !== undefined && (!Number.isInteger(c.maxRuns) || c.maxRuns <= 0)) {
|
|
1110
|
-
throw new Error(`concurrency.maxRuns must be a positive integer
|
|
1147
|
+
throw new Error(`concurrency.maxRuns must be a positive integer or a CEL expression, got: ${c.maxRuns}`);
|
|
1111
1148
|
}
|
|
1112
1149
|
});
|
|
1113
1150
|
}
|
package/v1/declaration.d.ts
CHANGED
|
@@ -228,7 +228,9 @@ export type TaskDefaults = {
|
|
|
228
228
|
*/
|
|
229
229
|
workerLabels?: CreateWorkflowTaskOpts<any, any>['desiredWorkerLabels'];
|
|
230
230
|
/**
|
|
231
|
-
* (optional) the concurrency options for the task.
|
|
231
|
+
* (optional) the concurrency options for the task, processed in array order. Entries
|
|
232
|
+
* may be workflow-scoped strategies or tenant-scoped entries (`isTenantScoped`)
|
|
233
|
+
* strategies, whose definitions are upserted as part of workflow registration.
|
|
232
234
|
*/
|
|
233
235
|
concurrency?: Concurrency | Concurrency[];
|
|
234
236
|
};
|
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,10 @@
|
|
|
1
|
+
export type WorkflowInput = {
|
|
2
|
+
account: string;
|
|
3
|
+
tier: string;
|
|
4
|
+
};
|
|
5
|
+
export type WorkflowOutput = {
|
|
6
|
+
'dynamic-task': {
|
|
7
|
+
account: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare const concurrencyDynamicWorkflow: import("../..").WorkflowDeclaration<WorkflowInput, WorkflowOutput, {}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
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.concurrencyDynamicWorkflow = void 0;
|
|
13
|
+
const v1_1 = require("../..");
|
|
14
|
+
const hatchet_client_1 = require("../hatchet-client");
|
|
15
|
+
// > Dynamic Max Runs
|
|
16
|
+
// maxRuns accepts a number or a CEL expression string. With an expression, each
|
|
17
|
+
// concurrency group's limit is computed from the task's input.
|
|
18
|
+
exports.concurrencyDynamicWorkflow = hatchet_client_1.hatchet.workflow({
|
|
19
|
+
name: 'concurrency-dynamic',
|
|
20
|
+
});
|
|
21
|
+
exports.concurrencyDynamicWorkflow.task({
|
|
22
|
+
name: 'dynamic-task',
|
|
23
|
+
concurrency: [
|
|
24
|
+
{
|
|
25
|
+
expression: 'input.account',
|
|
26
|
+
maxRuns: "input.tier == 'premium' ? 10 : 1",
|
|
27
|
+
limitStrategy: v1_1.ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
fn: (input) => __awaiter(void 0, void 0, void 0, function* () { return ({ account: input.account }); }),
|
|
31
|
+
});
|
|
32
|
+
// !!
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Concurrency } from '../..';
|
|
2
|
+
export declare const SLEEP_TIME_MS = 1500;
|
|
3
|
+
export type WorkflowInput = {
|
|
4
|
+
group: string;
|
|
5
|
+
inline?: string;
|
|
6
|
+
};
|
|
7
|
+
export type RunWindow = {
|
|
8
|
+
startMs: number;
|
|
9
|
+
endMs: number;
|
|
10
|
+
};
|
|
11
|
+
export type WorkflowOutput = {
|
|
12
|
+
'shared-task': RunWindow;
|
|
13
|
+
};
|
|
14
|
+
export declare const sharedLimit: Concurrency;
|
|
15
|
+
export declare const concurrencySharedWorkflowA: import("../..").WorkflowDeclaration<WorkflowInput, WorkflowOutput, {}>;
|
|
16
|
+
export declare const concurrencySharedWorkflowB: import("../..").WorkflowDeclaration<WorkflowInput, WorkflowOutput, {}>;
|
|
17
|
+
export declare const concurrencySharedMixedWorkflow: import("../..").WorkflowDeclaration<WorkflowInput, WorkflowOutput, {}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
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.concurrencySharedMixedWorkflow = exports.concurrencySharedWorkflowB = exports.concurrencySharedWorkflowA = exports.sharedLimit = exports.SLEEP_TIME_MS = void 0;
|
|
13
|
+
const v1_1 = require("../..");
|
|
14
|
+
const hatchet_client_1 = require("../hatchet-client");
|
|
15
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
16
|
+
setTimeout(resolve, ms);
|
|
17
|
+
});
|
|
18
|
+
exports.SLEEP_TIME_MS = 1500;
|
|
19
|
+
// > Shared Concurrency Strategy
|
|
20
|
+
// A tenant-scoped strategy is shared across workflows: every task declaring the same name
|
|
21
|
+
// consumes the same concurrency limit. The definition rides on workflow registration and
|
|
22
|
+
// re-registering the name updates it in place.
|
|
23
|
+
exports.sharedLimit = {
|
|
24
|
+
name: 'ts-example-shared-limit',
|
|
25
|
+
isTenantScoped: true,
|
|
26
|
+
expression: 'input.group',
|
|
27
|
+
maxRuns: 1,
|
|
28
|
+
limitStrategy: v1_1.ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
|
|
29
|
+
};
|
|
30
|
+
const runWindowTask = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
+
const startMs = Date.now();
|
|
32
|
+
yield sleep(exports.SLEEP_TIME_MS);
|
|
33
|
+
return { startMs, endMs: Date.now() };
|
|
34
|
+
});
|
|
35
|
+
exports.concurrencySharedWorkflowA = hatchet_client_1.hatchet.workflow({
|
|
36
|
+
name: 'concurrency-shared-a',
|
|
37
|
+
});
|
|
38
|
+
exports.concurrencySharedWorkflowA.task({
|
|
39
|
+
name: 'shared-task',
|
|
40
|
+
concurrency: [exports.sharedLimit],
|
|
41
|
+
fn: runWindowTask,
|
|
42
|
+
});
|
|
43
|
+
exports.concurrencySharedWorkflowB = hatchet_client_1.hatchet.workflow({
|
|
44
|
+
name: 'concurrency-shared-b',
|
|
45
|
+
});
|
|
46
|
+
exports.concurrencySharedWorkflowB.task({
|
|
47
|
+
name: 'shared-task',
|
|
48
|
+
concurrency: [exports.sharedLimit],
|
|
49
|
+
fn: runWindowTask,
|
|
50
|
+
});
|
|
51
|
+
// !!
|
|
52
|
+
// > Mixed Inline And Shared Concurrency
|
|
53
|
+
// A single task can combine a workflow-scoped inline strategy with a shared strategy;
|
|
54
|
+
// both limits apply at once.
|
|
55
|
+
exports.concurrencySharedMixedWorkflow = hatchet_client_1.hatchet.workflow({
|
|
56
|
+
name: 'concurrency-shared-mixed',
|
|
57
|
+
});
|
|
58
|
+
exports.concurrencySharedMixedWorkflow.task({
|
|
59
|
+
name: 'shared-task',
|
|
60
|
+
concurrency: [
|
|
61
|
+
{
|
|
62
|
+
expression: 'input.inline',
|
|
63
|
+
maxRuns: 1,
|
|
64
|
+
limitStrategy: v1_1.ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,
|
|
65
|
+
},
|
|
66
|
+
exports.sharedLimit,
|
|
67
|
+
],
|
|
68
|
+
fn: runWindowTask,
|
|
69
|
+
});
|
|
70
|
+
// !!
|
|
@@ -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, {}, {}, {}, {}>;
|