@nestjs/workflows 0.0.1 → 0.0.3

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.
@@ -49,6 +49,11 @@ export declare class ResultWaiter<T = unknown> {
49
49
  * connections idle at that moment, and one that answers later stays open until its keep-alive timeout.
50
50
  */
51
51
  close(): Promise<void>;
52
+ /**
53
+ * Reads `id`, unless the wait settles first. It races a promise of its own, not one the whole wait shares: each
54
+ * race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
55
+ */
56
+ private read;
52
57
  private add;
53
58
  private remove;
54
59
  }
@@ -47,13 +47,11 @@ export class ResultWaiter {
47
47
  const timeoutMs = options.timeout === undefined ? Infinity : parseDuration(options.timeout);
48
48
  const deadline = performance.now() + timeoutMs;
49
49
  options.signal?.throwIfAborted();
50
- let settle;
51
- const settled = new Promise((resolve) => (settle = () => resolve(SETTLED)));
52
- const wait = { closed: this.closed, settle };
50
+ const wait = { closed: this.closed };
53
51
  const nudge = () => wait.wake?.();
54
52
  const close = () => {
55
53
  wait.closed = true;
56
- settle();
54
+ wait.settle?.();
57
55
  nudge();
58
56
  };
59
57
  this.add(id, wait);
@@ -61,7 +59,7 @@ export class ResultWaiter {
61
59
  options.signal?.addEventListener('abort', nudge);
62
60
  try {
63
61
  for (let delay = 25;; delay = Math.min(delay * 2, 1_000)) {
64
- const read = wait.outcome || wait.closed ? SETTLED : await Promise.race([this.options.read(id), settled]);
62
+ const read = wait.outcome || wait.closed ? SETTLED : await this.read(id, wait);
65
63
  if (wait.outcome) {
66
64
  return unwrap(wait.outcome);
67
65
  }
@@ -104,7 +102,7 @@ export class ResultWaiter {
104
102
  const settled = typeof outcome === 'function' ? outcome() : outcome;
105
103
  for (const wait of waits) {
106
104
  wait.outcome ??= settled;
107
- wait.settle();
105
+ wait.settle?.();
108
106
  wait.wake?.();
109
107
  }
110
108
  }
@@ -117,6 +115,19 @@ export class ResultWaiter {
117
115
  this.closing.abort();
118
116
  await nextMacrotask();
119
117
  }
118
+ /**
119
+ * Reads `id`, unless the wait settles first. It races a promise of its own, not one the whole wait shares: each
120
+ * race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
121
+ */
122
+ async read(id, wait) {
123
+ const settled = new Promise((resolve) => (wait.settle = () => resolve(SETTLED)));
124
+ try {
125
+ return await Promise.race([this.options.read(id), settled]);
126
+ }
127
+ finally {
128
+ wait.settle = undefined;
129
+ }
130
+ }
120
131
  add(id, wait) {
121
132
  let waits = this.waits.get(id);
122
133
  if (!waits) {
@@ -49,8 +49,8 @@ export declare class LeasedWorker<T> {
49
49
  /** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
50
50
  private readonly claiming;
51
51
  private stopped;
52
- private stop;
53
- private readonly stoppedPromise;
52
+ /** Ends the `drain()` rounds waiting now, at shutdown. */
53
+ private readonly stopping;
54
54
  private wake?;
55
55
  private kicked;
56
56
  private loop?;
@@ -76,6 +76,11 @@ export declare class LeasedWorker<T> {
76
76
  * `shutdownTimeout` spent waiting for them before the rest are detached. Call it from `onModuleDestroy`.
77
77
  */
78
78
  shutdown(): Promise<void>;
79
+ /**
80
+ * Waits for `work`, or until shutdown. It races a promise of its own, not one the worker keeps: each race leaves a
81
+ * reaction on its inputs until they settle, so a long-lived one would keep one per round.
82
+ */
83
+ private untilStopped;
79
84
  private poll;
80
85
  private idle;
81
86
  /** Claims due items and starts executing them (before any caller sees them). */
@@ -43,7 +43,8 @@ export class LeasedWorker {
43
43
  /** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
44
44
  this.claiming = new Set();
45
45
  this.stopped = false;
46
- this.stoppedPromise = new Promise((resolve) => (this.stop = resolve));
46
+ /** Ends the `drain()` rounds waiting now, at shutdown. */
47
+ this.stopping = new Set();
47
48
  this.kicked = false;
48
49
  /** When the loop next calls `produce()` (`performance.now()`). */
49
50
  this.produceAt = 0;
@@ -96,7 +97,7 @@ export class LeasedWorker {
96
97
  break;
97
98
  }
98
99
  total += executions.length;
99
- await Promise.race([Promise.all(executions), this.stoppedPromise]);
100
+ await this.untilStopped(Promise.all(executions));
100
101
  }
101
102
  return total;
102
103
  }
@@ -109,7 +110,9 @@ export class LeasedWorker {
109
110
  return;
110
111
  }
111
112
  this.stopped = true;
112
- this.stop();
113
+ for (const stop of this.stopping) {
114
+ stop();
115
+ }
113
116
  this.wake?.();
114
117
  // What a claim in flight claims starts now, told to stop at once, so it is handed back before the store closes.
115
118
  await Promise.allSettled(this.claiming);
@@ -127,6 +130,24 @@ export class LeasedWorker {
127
130
  }
128
131
  await this.loop;
129
132
  }
133
+ /**
134
+ * Waits for `work`, or until shutdown. It races a promise of its own, not one the worker keeps: each race leaves a
135
+ * reaction on its inputs until they settle, so a long-lived one would keep one per round.
136
+ */
137
+ async untilStopped(work) {
138
+ let stop;
139
+ const stopped = new Promise((resolve) => (stop = resolve));
140
+ if (this.stopped) {
141
+ stop();
142
+ }
143
+ this.stopping.add(stop);
144
+ try {
145
+ await Promise.race([work, stopped]);
146
+ }
147
+ finally {
148
+ this.stopping.delete(stop);
149
+ }
150
+ }
130
151
  async poll() {
131
152
  while (!this.stopped) {
132
153
  if (this.options.produce && performance.now() >= this.produceAt) {
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { ChildWorkflowFailedError, isWorkflowInterrupt, NonRetryableStepError, S
11
11
  export type { SerializedWorkflowError } from './interfaces/index.js';
12
12
  export { WorkflowEvents, type WorkflowCancelledEvent, type WorkflowChildStartedEvent, type WorkflowCompensatingEvent, type WorkflowCompensationFailedEvent, type WorkflowCompletedEvent, type WorkflowCustomStatusEvent, type WorkflowDeletedEvent, type WorkflowEvent, type WorkflowFailedEvent, type WorkflowJournalLargeEvent, type WorkflowResumedEvent, type WorkflowRetriedEvent, type WorkflowScheduleSkippedEvent, type WorkflowSignalReceivedEvent, type WorkflowSignalTimedOutEvent, type WorkflowStartedEvent, type WorkflowStepCompensatedEvent, type WorkflowStepCompletedEvent, type WorkflowStepFailedEvent, type WorkflowSuspendedEvent, } from './events/index.js';
13
13
  export { WorkflowStorage } from './storage/index.js';
14
- export type { NewWorkflowInstance, NewWorkflowSignal, WorkflowCancelRequest, WorkflowClaim, WorkflowClaimRequest, WorkflowConcurrencyLimit, WorkflowInstanceDetails, WorkflowListQuery, WorkflowPurgeQuery, WorkflowPurgeResult, WorkflowRateLimitRule, WorkflowRateWindow, WorkflowRelease, WorkflowReopen, WorkflowScheduleClaimRequest, WorkflowScheduleQuery, WorkflowScheduleRecord, WorkflowScheduleSave, WorkflowScheduleWrite, WorkflowSignalQuery, WorkflowSignalRecord, WorkflowSignalResult, WorkflowStorageRegisterOptions, WorkflowStore, WorkflowWrite, } from './interfaces/index.js';
14
+ export type { NewWorkflowInstance, NewWorkflowSignal, WorkflowCancelRequest, WorkflowClaim, WorkflowClaimRequest, WorkflowConcurrencyLimit, WorkflowInstanceDetails, WorkflowKeyKind, WorkflowListQuery, WorkflowPurgeQuery, WorkflowPurgeResult, WorkflowRateLimitRule, WorkflowRateWindow, WorkflowRelease, WorkflowReopen, WorkflowScheduleClaimRequest, WorkflowScheduleQuery, WorkflowScheduleRecord, WorkflowScheduleSave, WorkflowScheduleWrite, WorkflowSignalQuery, WorkflowSignalRecord, WorkflowSignalResult, WorkflowStorageRegisterOptions, WorkflowStore, WorkflowWrite, } from './interfaces/index.js';
15
15
  export * from './stores/index.js';
16
16
  export { AesGcmPayloadCodec, type AesGcmPayloadCodecOptions } from './core/codecs/index.js';
17
17
  export type { WorkflowPayloadCodec, WorkflowPayloadContext } from './interfaces/index.js';
@@ -204,7 +204,18 @@ export interface WorkflowStore {
204
204
  * See `WorkflowWrite` for what each field does.
205
205
  */
206
206
  write(id: string, token: string, write: WorkflowWrite): Promise<boolean>;
207
+ /**
208
+ * Optional, for a store whose ids, names or keys are bounded (columns of a fixed length): throws, synchronously, an
209
+ * error that names the key (a `RangeError`) when the store can't hold `value` as a `kind`. The engine calls it for
210
+ * what a run makes up as it goes (a step's or a wait's name, `$compensate:<step>`, a child's id and keys, a wait's
211
+ * signal and key) before it journals or starts anything with it, so the error fails the run at the call that made
212
+ * the key, and the instance compensates, instead of failing every write that carries it. Without this method, the
213
+ * store's own check in `write()` or `create()` is all there is, and the instance is retried as after any store error.
214
+ */
215
+ checkKey?(kind: WorkflowKeyKind, value: string): void;
207
216
  }
217
+ /** The ids, names and keys `WorkflowStore.checkKey()` checks. */
218
+ export type WorkflowKeyKind = 'instanceId' | 'workflow' | 'concurrencyKey' | 'rateLimitKey' | 'journalName' | 'signal' | 'signalKey';
208
219
  /** `WorkflowStore.get()`'s result, and `WorkflowClient.getStatus()`'s. */
209
220
  export interface WorkflowInstanceDetails extends WorkflowInstance {
210
221
  /** Signals the instance waits for, if suspended in `waitForSignal()`. */
@@ -1,7 +1,7 @@
1
1
  import { type OnModuleInit } from '@nestjs/common';
2
2
  import { type MigrationSqlOptions, type MigrationStatementsOptions } from '@nestjs/store-kit/mysql';
3
3
  import type { WorkflowInstance, WorkflowStatus } from '../interfaces/workflow-instance.interface.js';
4
- import type { NewWorkflowInstance, NewWorkflowSignal, WorkflowCancelRequest, WorkflowClaim, WorkflowClaimRequest, WorkflowInstanceDetails, WorkflowListQuery, WorkflowPurgeQuery, WorkflowPurgeResult, WorkflowReopen, WorkflowScheduleClaimRequest, WorkflowScheduleQuery, WorkflowScheduleRecord, WorkflowScheduleSave, WorkflowScheduleWrite, WorkflowSignalQuery, WorkflowSignalRecord, WorkflowSignalResult, WorkflowStore, WorkflowWrite } from '../interfaces/workflow-store.interface.js';
4
+ import type { NewWorkflowInstance, NewWorkflowSignal, WorkflowCancelRequest, WorkflowClaim, WorkflowClaimRequest, WorkflowInstanceDetails, WorkflowKeyKind, WorkflowListQuery, WorkflowPurgeQuery, WorkflowPurgeResult, WorkflowReopen, WorkflowScheduleClaimRequest, WorkflowScheduleQuery, WorkflowScheduleRecord, WorkflowScheduleSave, WorkflowScheduleWrite, WorkflowSignalQuery, WorkflowSignalRecord, WorkflowSignalResult, WorkflowStore, WorkflowWrite } from '../interfaces/workflow-store.interface.js';
5
5
  import type { WorkflowStorage } from '../storage/workflow.storage.js';
6
6
  import type { MySqlWorkflowStoreOptions } from './interfaces/mysql-workflow-store-options.interface.js';
7
7
  /**
@@ -31,7 +31,9 @@ import type { MySqlWorkflowStoreOptions } from './interfaces/mysql-workflow-stor
31
31
  * On MySQL:
32
32
  * - Ids, names and keys are case- and accent-sensitive, and bounded: an instance id, a workflow's or a signal's name,
33
33
  * a signal's key or id, a concurrency or rate-limit key hold at most 255 characters, a schedule's id 230, a
34
- * journal entry's name 512. A longer one fails with a `RangeError` that names it, before any SQL.
34
+ * journal entry's name 512. A longer one fails with a `RangeError` that names it, before any SQL. One a run makes up
35
+ * (a step's or a wait's name, `$compensate:<step>`, a child's id or keys) is thrown where the run makes it
36
+ * (`checkKey()`), so the run fails and compensates, as any error it throws.
35
37
  * - The application's transactions may be REPEATABLE READ, MySQL's default. `signal(..., { transaction })` finds the
36
38
  * instances to wake with a locking read of their waits, which sees the latest committed ones whatever the level; the
37
39
  * signal lock is held until the application's transaction ends, as on PostgreSQL. `start(..., { transaction })`
@@ -164,6 +166,8 @@ export declare class MySqlWorkflowStore implements WorkflowStore, OnModuleInit {
164
166
  terminateRequested: boolean;
165
167
  } | null>;
166
168
  write(id: string, token: string, write: WorkflowWrite): Promise<boolean>;
169
+ /** What a run makes up (a step's name, a child's id...), checked by the engine before it writes or starts anything with it. */
170
+ checkKey(kind: WorkflowKeyKind, value: string): void;
167
171
  /** The store's readiness (the server and the schema), then the signal lock's row: before its own first statement. */
168
172
  private ready;
169
173
  /**
@@ -47,7 +47,9 @@ const ROWS_PER_INSERT = 1_000;
47
47
  * On MySQL:
48
48
  * - Ids, names and keys are case- and accent-sensitive, and bounded: an instance id, a workflow's or a signal's name,
49
49
  * a signal's key or id, a concurrency or rate-limit key hold at most 255 characters, a schedule's id 230, a
50
- * journal entry's name 512. A longer one fails with a `RangeError` that names it, before any SQL.
50
+ * journal entry's name 512. A longer one fails with a `RangeError` that names it, before any SQL. One a run makes up
51
+ * (a step's or a wait's name, `$compensate:<step>`, a child's id or keys) is thrown where the run makes it
52
+ * (`checkKey()`), so the run fails and compensates, as any error it throws.
51
53
  * - The application's transactions may be REPEATABLE READ, MySQL's default. `signal(..., { transaction })` finds the
52
54
  * instances to wake with a locking read of their waits, which sees the latest committed ones whatever the level; the
53
55
  * signal lock is held until the application's transaction ends, as on PostgreSQL. `start(..., { transaction })`
@@ -604,8 +606,8 @@ WHERE ${p.in('id', ids)}`, p.values);
604
606
  checkSignal(write.signal);
605
607
  }
606
608
  for (const wait of write.release?.waits ?? []) {
607
- checkKey("a wait's signal name", wait.signal, L.signal);
608
- checkKey("a wait's signal key", wait.key, L.signalKey);
609
+ checkKey(KEYS.signal, wait.signal, L.signal);
610
+ checkKey(KEYS.signalKey, wait.key, L.signalKey);
609
611
  }
610
612
  return retryOnDeadlock(this.executor, async (tx) => {
611
613
  const release = write.release;
@@ -650,6 +652,10 @@ WHERE ${p.in('id', ids)}`, p.values);
650
652
  return true;
651
653
  });
652
654
  }
655
+ /** What a run makes up (a step's name, a child's id...), checked by the engine before it writes or starts anything with it. */
656
+ checkKey(kind, value) {
657
+ checkKey(KEYS[kind], value, L[kind]);
658
+ }
653
659
  // ---------------------------------------------------------------- internals
654
660
  /** The store's readiness (the server and the schema), then the signal lock's row: before its own first statement. */
655
661
  async ready() {
@@ -823,6 +829,16 @@ function checkSignal(s) {
823
829
  }
824
830
  function checkEntries(entries) {
825
831
  for (const entry of entries) {
826
- checkKey("a journal entry's name (a step's, a wait's, or the engine's, such as $child: and a child's id)", entry.name, L.journalName);
832
+ checkKey(KEYS.journalName, entry.name, L.journalName);
827
833
  }
828
834
  }
835
+ /** How the errors of `checkKey()`, and of `write()`'s own checks, name each key. */
836
+ const KEYS = {
837
+ instanceId: 'an instance id',
838
+ workflow: "a workflow's name",
839
+ concurrencyKey: 'a concurrency key',
840
+ rateLimitKey: 'a rate-limit key',
841
+ journalName: "a journal entry's name (a step's, a wait's, or the engine's, such as $child: and a child's id)",
842
+ signal: "a wait's signal name",
843
+ signalKey: "a wait's signal key",
844
+ };
@@ -151,8 +151,6 @@ export declare class WorkflowExecution {
151
151
  private readonly childCounters;
152
152
  /** The first interrupt that stops this execution (not a suspension). */
153
153
  private stoppedBy;
154
- /** The step whose function is running, in that function's async context. */
155
- private readonly insideStep;
156
154
  /** Each journal entry's size in bytes, by name, and their sum. */
157
155
  private readonly entrySizes;
158
156
  private journalBytes;
@@ -255,6 +253,14 @@ export declare class WorkflowExecution {
255
253
  * compensates (compensations may still record their entries) and fails.
256
254
  */
257
255
  private assertJournalRoom;
256
+ /**
257
+ * Throws the store's error for a key it can't hold (`WorkflowStore.checkKey()`), where the run makes the key: the
258
+ * run fails with it and compensates. Reaching the store, it would fail every write that carries it, and the instance
259
+ * would be retried at every lease, forever.
260
+ */
261
+ private checkKey;
262
+ /** A wait's signal and key, registered when the instance suspends. */
263
+ private checkWait;
258
264
  /** Fenced write of staged entries plus `entries`. */
259
265
  private write;
260
266
  private loseLease;
@@ -132,8 +132,6 @@ export class WorkflowExecution {
132
132
  this.childCounters = new Map();
133
133
  /** The first interrupt that stops this execution (not a suspension). */
134
134
  this.stoppedBy = null;
135
- /** The step whose function is running, in that function's async context. */
136
- this.insideStep = new AsyncLocalStorage();
137
135
  /** Each journal entry's size in bytes, by name, and their sum. */
138
136
  this.entrySizes = new Map();
139
137
  this.journalBytes = 0;
@@ -256,6 +254,7 @@ export class WorkflowExecution {
256
254
  return { state: 'failed', error: entry.error };
257
255
  }
258
256
  try {
257
+ this.checkKey('journalName', name);
259
258
  const attempt = await this.attempt(name, 'compensation',
260
259
  // Whatever the undo returns (a provider's response, say) is discarded: nothing
261
260
  // reads it, so it can't fail the undo by being unserializable.
@@ -299,6 +298,10 @@ export class WorkflowExecution {
299
298
  throw this.interrupt('halt');
300
299
  }
301
300
  this.assertCanStart();
301
+ if (options.compensate) {
302
+ // Checked before the step runs: a side effect whose undo the store couldn't journal never happens.
303
+ this.checkKey('journalName', `${COMPENSATE}${name}`);
304
+ }
302
305
  const plan = this.plan(name, options);
303
306
  // Reserved in call order, so parallel steps compensate in the same order
304
307
  // on the first run and on a replay, whichever finished first.
@@ -343,6 +346,7 @@ export class WorkflowExecution {
343
346
  throw this.interrupt('halt');
344
347
  }
345
348
  this.assertAlive();
349
+ this.checkWait(wait);
346
350
  const deadline = entry
347
351
  ? (entry.wakeAt ?? null)
348
352
  : options.timeout !== undefined
@@ -426,6 +430,10 @@ export class WorkflowExecution {
426
430
  parentId: this.instance.id,
427
431
  parentClose,
428
432
  });
433
+ this.checkKey('instanceId', child.id);
434
+ this.checkKey('workflow', child.workflow);
435
+ this.checkKey('concurrencyKey', child.concurrencyKey);
436
+ this.checkKey('rateLimitKey', child.rateLimitKey);
429
437
  await this.reachFrontier(name);
430
438
  this.assertCanStart();
431
439
  // Recorded before the child exists, so a parent that crashes right after creating it still
@@ -477,6 +485,11 @@ export class WorkflowExecution {
477
485
  throw this.interrupt('halt');
478
486
  }
479
487
  this.assertAlive();
488
+ for (const branch of branches.values()) {
489
+ if (branch.wait) {
490
+ this.checkWait(branch.wait);
491
+ }
492
+ }
480
493
  // Timers count from when the wait was first reached: a replay keeps their deadlines.
481
494
  const now = this.deps.clock.now();
482
495
  const reached = entry?.data;
@@ -776,7 +789,7 @@ export class WorkflowExecution {
776
789
  await this.write([current]);
777
790
  },
778
791
  };
779
- const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () => this.insideStep.run(name, () => fn(ctx))));
792
+ const run = Promise.resolve().then(() => runInStepScope(ctx.idempotencyKey, () => insideStep.run({ execution: this, name, outer: insideStep.getStore() }, () => fn(ctx))));
780
793
  return Promise.race([run, watchdog]).finally(() => {
781
794
  settled = true;
782
795
  clearTimeout(overall);
@@ -805,10 +818,14 @@ export class WorkflowExecution {
805
818
  * replay, so it is a definition error, reported where it happens.
806
819
  */
807
820
  assertNotInStep(call, advice = 'Call ctx methods from run() and pass the values the step needs into it.') {
808
- const step = this.insideStep.getStore();
809
- if (step === undefined) {
821
+ let frame = insideStep.getStore();
822
+ while (frame && frame.execution !== this) {
823
+ frame = frame.outer;
824
+ }
825
+ if (!frame) {
810
826
  return;
811
827
  }
828
+ const step = frame.name;
812
829
  const where = step.startsWith(COMPENSATE) ? `the compensation of step "${step.slice(COMPENSATE.length)}"` : `step "${step}"`;
813
830
  throw this.setFatal(new WorkflowDefinitionError(`${this.describe()}: ${where} called ctx.${call}. A step's function runs once and replays return its ` +
814
831
  `result, so it can't use ctx. ${advice}`));
@@ -829,6 +846,7 @@ export class WorkflowExecution {
829
846
  const entry = this.journal.get(name);
830
847
  if (!entry && this.mode === 'run') {
831
848
  this.assertJournalRoom(name);
849
+ this.checkKey('journalName', name);
832
850
  }
833
851
  if (entry && entry.kind !== kind) {
834
852
  throw this.setFatal(new WorkflowNonDeterminismError(`${this.describe()} does not match its journal: "${name}" was recorded as a ${entry.kind}, but the code now calls it as a ${kind}. ${ADVICE}`));
@@ -956,6 +974,21 @@ export class WorkflowExecution {
956
974
  };
957
975
  throw this.interrupt('journal-limit');
958
976
  }
977
+ /**
978
+ * Throws the store's error for a key it can't hold (`WorkflowStore.checkKey()`), where the run makes the key: the
979
+ * run fails with it and compensates. Reaching the store, it would fail every write that carries it, and the instance
980
+ * would be retried at every lease, forever.
981
+ */
982
+ checkKey(kind, value) {
983
+ if (typeof value === 'string') {
984
+ this.deps.store.checkKey?.(kind, value);
985
+ }
986
+ }
987
+ /** A wait's signal and key, registered when the instance suspends. */
988
+ checkWait(wait) {
989
+ this.checkKey('signal', wait.signal);
990
+ this.checkKey('signalKey', wait.key);
991
+ }
959
992
  /** Fenced write of staged entries plus `entries`. */
960
993
  async write(entries) {
961
994
  if (this.detached) {
@@ -1069,6 +1102,11 @@ function conditionsOf(name, conditions) {
1069
1102
  return branches;
1070
1103
  }
1071
1104
  const COMPENSATE = '$compensate:';
1105
+ /**
1106
+ * The steps whose functions are running, in their async context. One store for every execution, not one each: on
1107
+ * Node 20 and 22, a store that ran stays in a process-wide list every async resource walks, until `disable()`.
1108
+ */
1109
+ const insideStep = new AsyncLocalStorage();
1072
1110
  const PARENT_CLOSE = ['cancel', 'terminate', 'abandon'];
1073
1111
  /** The most a custom status (`ctx.setStatus()`) may take as JSON. */
1074
1112
  const MAX_CUSTOM_STATUS_BYTES = 16_384;
@@ -18,6 +18,7 @@ export declare class EncodedWorkflowStore implements WorkflowInstanceStore {
18
18
  private readonly unreadable;
19
19
  readonly createInTransaction?: WorkflowStore['createInTransaction'];
20
20
  readonly signalInTransaction?: WorkflowStore['signalInTransaction'];
21
+ readonly checkKey?: WorkflowStore['checkKey'];
21
22
  constructor(
22
23
  /** The store itself: what it holds, undecoded. */
23
24
  inner: WorkflowStore, codecs: PayloadCodecs<WorkflowPayloadContext>);
@@ -21,6 +21,10 @@ export class EncodedWorkflowStore {
21
21
  if (typeof inner.signalInTransaction === 'function') {
22
22
  this.signalInTransaction = (transaction, signal) => toPromise(andThen(this.encodeSignal(signal), (encoded) => inner.signalInTransaction(transaction, encoded)));
23
23
  }
24
+ // Keys aren't encoded: the store checks them as they are.
25
+ if (typeof inner.checkKey === 'function') {
26
+ this.checkKey = (kind, value) => inner.checkKey(kind, value);
27
+ }
24
28
  }
25
29
  /** Whether a codec encodes what is written. */
26
30
  get encodes() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs/workflows",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Nest - modern, fast, powerful node.js web framework (@workflows)",
5
5
  "author": "Kamil Mysliwiec",
6
6
  "license": "MIT",
@@ -69,37 +69,37 @@
69
69
  "@commitlint/cli": "21.2.3",
70
70
  "@commitlint/config-angular": "21.2.3",
71
71
  "@electric-sql/pglite": "0.5.8",
72
- "@nestjs/common": "12.1.0",
73
- "@nestjs/core": "12.1.0",
72
+ "@nestjs/common": "12.1.2",
73
+ "@nestjs/core": "12.1.2",
74
74
  "@nestjs/cqrs": "12.1.0",
75
75
  "@nestjs/drizzle": "0.0.1",
76
- "@nestjs/locks": "0.0.1",
76
+ "@nestjs/locks": "0.0.2",
77
77
  "@nestjs/mail": "0.0.1",
78
- "@nestjs/outbox": "0.0.1",
79
- "@nestjs/platform-express": "12.1.0",
80
- "@nestjs/platform-fastify": "12.1.0",
81
- "@nestjs/testing": "12.1.0",
78
+ "@nestjs/outbox": "0.1.1",
79
+ "@nestjs/platform-express": "12.1.2",
80
+ "@nestjs/platform-fastify": "12.1.2",
81
+ "@nestjs/testing": "12.1.2",
82
82
  "@prisma/adapter-mariadb": "7.10.0",
83
83
  "@prisma/adapter-pg": "7.10.0",
84
84
  "@prisma/client": "7.10.0",
85
- "@types/node": "24.13.6",
85
+ "@types/node": "24.19.2",
86
86
  "@types/pg": "8.23.1",
87
- "drizzle-orm": "0.45.3",
87
+ "drizzle-orm": "0.45.4",
88
88
  "husky": "9.1.7",
89
89
  "kysely": "0.29.6",
90
- "lint-staged": "17.5.1",
90
+ "lint-staged": "17.6.0",
91
91
  "mysql2": "3.24.5",
92
- "oxlint": "1.85.0",
93
- "pg": "8.23.0",
94
- "prettier": "3.9.8",
92
+ "oxlint": "1.87.0",
93
+ "pg": "8.23.1",
94
+ "prettier": "3.9.10",
95
95
  "prisma": "7.10.0",
96
96
  "reflect-metadata": "0.2.2",
97
- "release-it": "21.1.0",
97
+ "release-it": "21.1.1",
98
98
  "rxjs": "7.8.2",
99
- "typeorm": "1.1.1",
99
+ "typeorm": "1.1.2",
100
100
  "typescript": "7.0.2",
101
- "vite": "8.3.0",
102
- "vitest": "5.0.1"
101
+ "vite": "8.3.4",
102
+ "vitest": "5.0.3"
103
103
  },
104
104
  "peerDependencies": {
105
105
  "@nestjs/common": "^11.0.0 || ^12.0.0",