@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.
Files changed (32) hide show
  1. package/clients/dispatcher/action-listener.d.ts +1 -1
  2. package/clients/dispatcher/action-listener.js +6 -0
  3. package/clients/listeners/durable-listener/durable-listener-client.d.ts +6 -1
  4. package/clients/listeners/durable-listener/durable-listener-client.js +124 -37
  5. package/clients/rest/generated/Api.d.ts +6 -1
  6. package/clients/rest/generated/Api.js +1 -1
  7. package/clients/rest/generated/data-contracts.d.ts +9 -1
  8. package/clients/rest/generated/data-contracts.js +1 -0
  9. package/legacy/workflow.d.ts +4 -4
  10. package/legacy/workflow.js +1 -1
  11. package/package.json +1 -1
  12. package/protoc/v1/workflows.d.ts +17 -0
  13. package/protoc/v1/workflows.js +74 -2
  14. package/v1/client/worker/context.d.ts +2 -0
  15. package/v1/client/worker/context.js +26 -8
  16. package/v1/client/worker/eviction/eviction-manager.d.ts +1 -1
  17. package/v1/client/worker/eviction/eviction-manager.js +12 -5
  18. package/v1/client/worker/worker-internal.d.ts +2 -1
  19. package/v1/client/worker/worker-internal.js +52 -25
  20. package/v1/embedded.d.ts +36 -2
  21. package/v1/embedded.js +35 -8
  22. package/v1/examples/concurrency_cancel_queued_except_newest/workflow.d.ts +9 -0
  23. package/v1/examples/concurrency_cancel_queued_except_newest/workflow.js +47 -0
  24. package/v1/examples/concurrency_cancel_queued_except_oldest/workflow.d.ts +9 -0
  25. package/v1/examples/concurrency_cancel_queued_except_oldest/workflow.js +47 -0
  26. package/v1/examples/durable_callback_ordering/workflow.d.ts +42 -0
  27. package/v1/examples/durable_callback_ordering/workflow.js +95 -0
  28. package/v1/examples/durable_eviction/workflow.d.ts +5 -0
  29. package/v1/examples/durable_eviction/workflow.js +15 -1
  30. package/v1/examples/e2e-worker.js +99 -90
  31. package/version.d.ts +1 -1
  32. 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
- _evictRun(key) {
57
- this._cancelLocal(key);
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, _m;
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: task.concurrency
333
- ? Array.isArray(task.concurrency)
334
- ? task.concurrency
335
- : [task.concurrency]
336
- : ((_m = workflow.taskDefaults) === null || _m === void 0 ? void 0 : _m.concurrency)
337
- ? Array.isArray(workflow.taskDefaults.concurrency)
338
- ? workflow.taskDefaults.concurrency
339
- : [workflow.taskDefaults.concurrency]
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
- const invocationCount = (_a = ctx.invocationCount) !== null && _a !== void 0 ? _a : 1;
384
- this.client.durableListener.cleanupTaskState(ctx.action.taskRunExternalId, invocationCount);
385
- if (ctx.abortController) {
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
- var _a;
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. By default the sidecar starts a
54
- * bundled Postgres; pass `databaseUrl` to point it at your own instead.
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. By default the sidecar starts a
328
- * bundled Postgres; pass `databaseUrl` to point it at your own instead.
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, {}, {}, {}, {}>;