@nestjs/workflows 0.0.2 → 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.
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
+ };
@@ -253,6 +253,14 @@ export declare class WorkflowExecution {
253
253
  * compensates (compensations may still record their entries) and fails.
254
254
  */
255
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;
256
264
  /** Fenced write of staged entries plus `entries`. */
257
265
  private write;
258
266
  private loseLease;
@@ -254,6 +254,7 @@ export class WorkflowExecution {
254
254
  return { state: 'failed', error: entry.error };
255
255
  }
256
256
  try {
257
+ this.checkKey('journalName', name);
257
258
  const attempt = await this.attempt(name, 'compensation',
258
259
  // Whatever the undo returns (a provider's response, say) is discarded: nothing
259
260
  // reads it, so it can't fail the undo by being unserializable.
@@ -297,6 +298,10 @@ export class WorkflowExecution {
297
298
  throw this.interrupt('halt');
298
299
  }
299
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
+ }
300
305
  const plan = this.plan(name, options);
301
306
  // Reserved in call order, so parallel steps compensate in the same order
302
307
  // on the first run and on a replay, whichever finished first.
@@ -341,6 +346,7 @@ export class WorkflowExecution {
341
346
  throw this.interrupt('halt');
342
347
  }
343
348
  this.assertAlive();
349
+ this.checkWait(wait);
344
350
  const deadline = entry
345
351
  ? (entry.wakeAt ?? null)
346
352
  : options.timeout !== undefined
@@ -424,6 +430,10 @@ export class WorkflowExecution {
424
430
  parentId: this.instance.id,
425
431
  parentClose,
426
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);
427
437
  await this.reachFrontier(name);
428
438
  this.assertCanStart();
429
439
  // Recorded before the child exists, so a parent that crashes right after creating it still
@@ -475,6 +485,11 @@ export class WorkflowExecution {
475
485
  throw this.interrupt('halt');
476
486
  }
477
487
  this.assertAlive();
488
+ for (const branch of branches.values()) {
489
+ if (branch.wait) {
490
+ this.checkWait(branch.wait);
491
+ }
492
+ }
478
493
  // Timers count from when the wait was first reached: a replay keeps their deadlines.
479
494
  const now = this.deps.clock.now();
480
495
  const reached = entry?.data;
@@ -831,6 +846,7 @@ export class WorkflowExecution {
831
846
  const entry = this.journal.get(name);
832
847
  if (!entry && this.mode === 'run') {
833
848
  this.assertJournalRoom(name);
849
+ this.checkKey('journalName', name);
834
850
  }
835
851
  if (entry && entry.kind !== kind) {
836
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}`));
@@ -958,6 +974,21 @@ export class WorkflowExecution {
958
974
  };
959
975
  throw this.interrupt('journal-limit');
960
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
+ }
961
992
  /** Fenced write of staged entries plus `entries`. */
962
993
  async write(entries) {
963
994
  if (this.detached) {
@@ -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.2",
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",
@@ -73,32 +73,32 @@
73
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.1.0",
78
+ "@nestjs/outbox": "0.1.1",
79
79
  "@nestjs/platform-express": "12.1.2",
80
80
  "@nestjs/platform-fastify": "12.1.2",
81
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.19.1",
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
90
  "lint-staged": "17.6.0",
91
91
  "mysql2": "3.24.5",
92
92
  "oxlint": "1.87.0",
93
93
  "pg": "8.23.1",
94
- "prettier": "3.9.9",
94
+ "prettier": "3.9.10",
95
95
  "prisma": "7.10.0",
96
96
  "reflect-metadata": "0.2.2",
97
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.3",
101
+ "vite": "8.3.4",
102
102
  "vitest": "5.0.3"
103
103
  },
104
104
  "peerDependencies": {