@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 +1 -1
- package/dist/interfaces/workflow-store.interface.d.ts +11 -0
- package/dist/mysql/mysql-workflow.store.d.ts +6 -2
- package/dist/mysql/mysql-workflow.store.js +20 -4
- package/dist/services/workflow-execution.service.d.ts +8 -0
- package/dist/services/workflow-execution.service.js +31 -0
- package/dist/storage/encoded-workflow.store.d.ts +1 -0
- package/dist/storage/encoded-workflow.store.js +4 -0
- package/package.json +8 -8
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(
|
|
608
|
-
checkKey(
|
|
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(
|
|
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.
|
|
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.
|
|
76
|
+
"@nestjs/locks": "0.0.2",
|
|
77
77
|
"@nestjs/mail": "0.0.1",
|
|
78
|
-
"@nestjs/outbox": "0.1.
|
|
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.
|
|
85
|
+
"@types/node": "24.19.2",
|
|
86
86
|
"@types/pg": "8.23.1",
|
|
87
|
-
"drizzle-orm": "0.45.
|
|
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.
|
|
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.
|
|
99
|
+
"typeorm": "1.1.2",
|
|
100
100
|
"typescript": "7.0.2",
|
|
101
|
-
"vite": "8.3.
|
|
101
|
+
"vite": "8.3.4",
|
|
102
102
|
"vitest": "5.0.3"
|
|
103
103
|
},
|
|
104
104
|
"peerDependencies": {
|