@nestjs/workflows 0.0.2 → 0.0.4
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/core/interfaces/result-waiter-options.interface.d.ts +6 -2
- package/dist/core/results/result-waiter.d.ts +5 -3
- package/dist/core/results/result-waiter.js +34 -10
- package/dist/core/utils/timer.util.d.ts +12 -0
- package/dist/core/utils/timer.util.js +16 -0
- package/dist/core/workers/leased-worker.js +4 -3
- package/dist/cqrs/workflow-event.publisher.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/interfaces/workflow-client.interface.d.ts +4 -1
- package/dist/interfaces/workflow-context.interface.d.ts +2 -2
- package/dist/interfaces/workflow-store.interface.d.ts +29 -1
- package/dist/mysql/interfaces/mysql-workflow-store-options.interface.d.ts +1 -1
- package/dist/mysql/mysql-workflow.store.d.ts +10 -2
- package/dist/mysql/mysql-workflow.store.js +27 -4
- package/dist/postgres/interfaces/postgres-workflow-store-options.interface.d.ts +1 -1
- package/dist/postgres/postgres-workflow.store.d.ts +6 -1
- package/dist/postgres/postgres-workflow.store.js +8 -0
- package/dist/services/workflow-client.service.d.ts +11 -2
- package/dist/services/workflow-client.service.js +23 -5
- package/dist/services/workflow-execution.service.d.ts +11 -0
- package/dist/services/workflow-execution.service.js +61 -4
- package/dist/services/workflow-scheduler.service.d.ts +8 -0
- package/dist/services/workflow-scheduler.service.js +20 -1
- package/dist/services/workflow-schedules.service.js +2 -0
- package/dist/services/workflow-worker.service.js +3 -0
- package/dist/storage/encoded-workflow.store.d.ts +12 -0
- package/dist/storage/encoded-workflow.store.js +42 -3
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/new-instance.util.d.ts +3 -0
- package/dist/utils/new-instance.util.js +9 -2
- package/dist/utils/serialize-error.util.js +25 -1
- package/dist/utils/storable-text.util.d.ts +36 -0
- package/dist/utils/storable-text.util.js +96 -0
- package/package.json +27 -26
|
@@ -29,7 +29,8 @@ export type ResultOutcome<T> = {
|
|
|
29
29
|
export interface ResultWaiterOptions<T> {
|
|
30
30
|
/**
|
|
31
31
|
* Reads the outcome from the store: `null` while it hasn't ended. Called when a wait starts, then after a backoff
|
|
32
|
-
* (25 ms at first, doubling up to a second), until the wait ends; a rejection rejects the wait.
|
|
32
|
+
* (25 ms at first, doubling up to a second), until the wait ends; a rejection rejects the wait. A wait that times
|
|
33
|
+
* out, is aborted or closes doesn't wait for a read in flight: its answer or rejection is then ignored.
|
|
33
34
|
*/
|
|
34
35
|
read(id: string): Promise<ResultOutcome<T> | null>;
|
|
35
36
|
/** The error a wait rejects with past its `timeout`. Default: an `Error` that names `id`. */
|
|
@@ -45,7 +46,10 @@ export interface ResultWaiterOptions<T> {
|
|
|
45
46
|
* ```
|
|
46
47
|
*/
|
|
47
48
|
export interface ResultWaitOptions {
|
|
48
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Stop waiting after this long, in wall-clock time (not a `Clock`'s). Default: no limit. A hard deadline: a read
|
|
51
|
+
* still in flight when it passes isn't awaited, so a timeout shorter than one read (0 included) rejects.
|
|
52
|
+
*/
|
|
49
53
|
timeout?: Duration;
|
|
50
54
|
/** Stop waiting when it aborts: the wait rejects with its reason. */
|
|
51
55
|
signal?: AbortSignal;
|
|
@@ -3,7 +3,8 @@ import type { ResultOutcome, ResultWaiterOptions, ResultWaitOptions } from '../i
|
|
|
3
3
|
* Waits for the outcome of something another part of the application runs (a workflow instance, a job): at once
|
|
4
4
|
* when this process ends it and says so (`settle()`), else by reading the store with a backoff (every 25 ms at
|
|
5
5
|
* first, doubling up to every second), which sees what other processes ended. A wait rejects past its `timeout`
|
|
6
|
-
* (the work goes on), when its `signal` aborts, and as soon as the waiter closes, without reading the store again
|
|
6
|
+
* (the work goes on), when its `signal` aborts, and as soon as the waiter closes, without reading the store again
|
|
7
|
+
* nor waiting for a read in flight (it ends in the background, and its answer or failure is ignored):
|
|
7
8
|
* close it in `beforeApplicationShutdown`, which Nest runs after `onModuleDestroy` (where workers drain, so work that
|
|
8
9
|
* ends during the drain still answers) and before the HTTP server closes.
|
|
9
10
|
*
|
|
@@ -50,8 +51,9 @@ export declare class ResultWaiter<T = unknown> {
|
|
|
50
51
|
*/
|
|
51
52
|
close(): Promise<void>;
|
|
52
53
|
/**
|
|
53
|
-
* Reads `id`, unless the wait settles
|
|
54
|
-
*
|
|
54
|
+
* Reads `id`, unless the wait settles, times out or is aborted first: the read is then given up, and left to end in
|
|
55
|
+
* the background (`Promise.race` handles its failure). It races a promise of its own, not one the whole wait
|
|
56
|
+
* shares: each race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
|
|
55
57
|
*/
|
|
56
58
|
private read;
|
|
57
59
|
private add;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { setImmediate as nextMacrotask } from 'node:timers/promises';
|
|
2
2
|
import { parseDuration } from '../time/duration.js';
|
|
3
|
+
import { MAX_TIMER_DELAY, setTimer } from '../utils/timer.util.js';
|
|
3
4
|
/** What a read in flight gives way to: an outcome settled locally, or the waiter closing. */
|
|
4
5
|
const SETTLED = Symbol('settled');
|
|
6
|
+
/** What a read in flight gives way to when the wait's timeout passes. */
|
|
7
|
+
const EXPIRED = Symbol('expired');
|
|
5
8
|
/**
|
|
6
9
|
* Waits for the outcome of something another part of the application runs (a workflow instance, a job): at once
|
|
7
10
|
* when this process ends it and says so (`settle()`), else by reading the store with a backoff (every 25 ms at
|
|
8
11
|
* first, doubling up to every second), which sees what other processes ended. A wait rejects past its `timeout`
|
|
9
|
-
* (the work goes on), when its `signal` aborts, and as soon as the waiter closes, without reading the store again
|
|
12
|
+
* (the work goes on), when its `signal` aborts, and as soon as the waiter closes, without reading the store again
|
|
13
|
+
* nor waiting for a read in flight (it ends in the background, and its answer or failure is ignored):
|
|
10
14
|
* close it in `beforeApplicationShutdown`, which Nest runs after `onModuleDestroy` (where workers drain, so work that
|
|
11
15
|
* ends during the drain still answers) and before the HTTP server closes.
|
|
12
16
|
*
|
|
@@ -48,7 +52,10 @@ export class ResultWaiter {
|
|
|
48
52
|
const deadline = performance.now() + timeoutMs;
|
|
49
53
|
options.signal?.throwIfAborted();
|
|
50
54
|
const wait = { closed: this.closed };
|
|
51
|
-
const nudge = () =>
|
|
55
|
+
const nudge = () => {
|
|
56
|
+
wait.giveUp?.();
|
|
57
|
+
wait.wake?.();
|
|
58
|
+
};
|
|
52
59
|
const close = () => {
|
|
53
60
|
wait.closed = true;
|
|
54
61
|
wait.settle?.();
|
|
@@ -59,19 +66,19 @@ export class ResultWaiter {
|
|
|
59
66
|
options.signal?.addEventListener('abort', nudge);
|
|
60
67
|
try {
|
|
61
68
|
for (let delay = 25;; delay = Math.min(delay * 2, 1_000)) {
|
|
62
|
-
const read = wait.outcome || wait.closed ? SETTLED : await this.read(id, wait);
|
|
69
|
+
const read = wait.outcome || wait.closed ? SETTLED : await this.read(id, wait, deadline, options.signal);
|
|
63
70
|
if (wait.outcome) {
|
|
64
71
|
return unwrap(wait.outcome);
|
|
65
72
|
}
|
|
66
73
|
if (read === SETTLED) {
|
|
67
74
|
throw this.options.closedError?.(id) ?? new Error(`The waiter closed while waiting for the result of "${id}".`);
|
|
68
75
|
}
|
|
69
|
-
if (read) {
|
|
76
|
+
if (read && read !== EXPIRED) {
|
|
70
77
|
return unwrap(read);
|
|
71
78
|
}
|
|
72
79
|
options.signal?.throwIfAborted();
|
|
73
80
|
const remaining = deadline - performance.now();
|
|
74
|
-
if (remaining <= 0) {
|
|
81
|
+
if (read === EXPIRED || remaining <= 0) {
|
|
75
82
|
throw this.options.timeoutError?.(id, timeoutMs) ?? new Error(`"${id}" didn't end within ${timeoutMs}ms.`);
|
|
76
83
|
}
|
|
77
84
|
await new Promise((resolve) => {
|
|
@@ -116,16 +123,33 @@ export class ResultWaiter {
|
|
|
116
123
|
await nextMacrotask();
|
|
117
124
|
}
|
|
118
125
|
/**
|
|
119
|
-
* Reads `id`, unless the wait settles
|
|
120
|
-
*
|
|
126
|
+
* Reads `id`, unless the wait settles, times out or is aborted first: the read is then given up, and left to end in
|
|
127
|
+
* the background (`Promise.race` handles its failure). It races a promise of its own, not one the whole wait
|
|
128
|
+
* shares: each race leaves a reaction on its inputs until they settle, so a shared one would keep one per poll.
|
|
121
129
|
*/
|
|
122
|
-
async read(id, wait) {
|
|
123
|
-
|
|
130
|
+
async read(id, wait, deadline, signal) {
|
|
131
|
+
// Aborted during the backoff's sleep: no read for a caller that went away.
|
|
132
|
+
if (signal?.aborted) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
let timer;
|
|
136
|
+
const givenWay = new Promise((resolve) => {
|
|
137
|
+
wait.settle = () => resolve(SETTLED);
|
|
138
|
+
wait.giveUp = () => resolve(null);
|
|
139
|
+
const remaining = deadline - performance.now();
|
|
140
|
+
if (remaining !== Infinity) {
|
|
141
|
+
// A deadline beyond what a timer supports: the read is given up at the longest delay, and the next one waits on.
|
|
142
|
+
const expired = remaining <= MAX_TIMER_DELAY ? EXPIRED : null;
|
|
143
|
+
timer = setTimer(() => resolve(expired), Math.max(remaining, 0));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
124
146
|
try {
|
|
125
|
-
return await Promise.race([this.options.read(id),
|
|
147
|
+
return await Promise.race([this.options.read(id), givenWay]);
|
|
126
148
|
}
|
|
127
149
|
finally {
|
|
128
150
|
wait.settle = undefined;
|
|
151
|
+
wait.giveUp = undefined;
|
|
152
|
+
clearTimeout(timer);
|
|
129
153
|
}
|
|
130
154
|
}
|
|
131
155
|
add(id, wait) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The longest delay `setTimeout` and `setInterval` support: 2^31 - 1 ms, about 24.8 days. Node runs a longer one
|
|
3
|
+
* after 1 ms instead (with a `TimeoutOverflowWarning`), so a step `timeout: '30d'` would time out every attempt at
|
|
4
|
+
* once, and a `heartbeatInterval` of '30d' would renew the lease every millisecond.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare const MAX_TIMER_DELAY: number;
|
|
9
|
+
/** @internal `setTimeout`, with delays beyond what timers support treated as the longest one. */
|
|
10
|
+
export declare function setTimer(callback: () => void, ms: number): NodeJS.Timeout;
|
|
11
|
+
/** @internal `setInterval`, capped like `setTimer()`. */
|
|
12
|
+
export declare function setRepeatingTimer(callback: () => void, ms: number): NodeJS.Timeout;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The longest delay `setTimeout` and `setInterval` support: 2^31 - 1 ms, about 24.8 days. Node runs a longer one
|
|
3
|
+
* after 1 ms instead (with a `TimeoutOverflowWarning`), so a step `timeout: '30d'` would time out every attempt at
|
|
4
|
+
* once, and a `heartbeatInterval` of '30d' would renew the lease every millisecond.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_TIMER_DELAY = 2 ** 31 - 1;
|
|
9
|
+
/** @internal `setTimeout`, with delays beyond what timers support treated as the longest one. */
|
|
10
|
+
export function setTimer(callback, ms) {
|
|
11
|
+
return setTimeout(callback, Math.min(ms, MAX_TIMER_DELAY));
|
|
12
|
+
}
|
|
13
|
+
/** @internal `setInterval`, capped like `setTimer()`. */
|
|
14
|
+
export function setRepeatingTimer(callback, ms) {
|
|
15
|
+
return setInterval(callback, Math.min(ms, MAX_TIMER_DELAY));
|
|
16
|
+
}
|
|
@@ -3,6 +3,7 @@ import { hostname } from 'node:os';
|
|
|
3
3
|
import { Logger } from '@nestjs/common';
|
|
4
4
|
import { systemClock } from '../time/clock.js';
|
|
5
5
|
import { parseDuration } from '../time/duration.js';
|
|
6
|
+
import { setRepeatingTimer, setTimer } from '../utils/timer.util.js';
|
|
6
7
|
/**
|
|
7
8
|
* A claim-execute loop under leases, any number of processes sharing a store: it claims due items (as many as it has
|
|
8
9
|
* free slots), executes them, renews each one's lease every `heartbeatInterval` (a third of the lease by default), and
|
|
@@ -122,7 +123,7 @@ export class LeasedWorker {
|
|
|
122
123
|
let timer;
|
|
123
124
|
await Promise.race([
|
|
124
125
|
Promise.allSettled([...this.runs].map((run) => run.done)),
|
|
125
|
-
new Promise((resolve) => (timer =
|
|
126
|
+
new Promise((resolve) => (timer = setTimer(resolve, this.shutdownMs))),
|
|
126
127
|
]);
|
|
127
128
|
clearTimeout(timer);
|
|
128
129
|
for (const run of this.runs) {
|
|
@@ -185,7 +186,7 @@ export class LeasedWorker {
|
|
|
185
186
|
this.wake = undefined;
|
|
186
187
|
resolve();
|
|
187
188
|
};
|
|
188
|
-
const timer =
|
|
189
|
+
const timer = setTimer(done, this.pollMs);
|
|
189
190
|
this.wake = () => {
|
|
190
191
|
this.kicked = false;
|
|
191
192
|
done();
|
|
@@ -215,7 +216,7 @@ export class LeasedWorker {
|
|
|
215
216
|
if (this.stopped) {
|
|
216
217
|
run.halt();
|
|
217
218
|
}
|
|
218
|
-
run.heartbeat =
|
|
219
|
+
run.heartbeat = setRepeatingTimer(() => void run.renew(), this.heartbeatMs);
|
|
219
220
|
run.heartbeat.unref();
|
|
220
221
|
let executing;
|
|
221
222
|
try {
|
|
@@ -111,9 +111,9 @@ let WorkflowEventPublisher = class WorkflowEventPublisher {
|
|
|
111
111
|
failure.event = undefined;
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
|
-
// Issued back to back, not awaited in between:
|
|
115
|
-
//
|
|
116
|
-
//
|
|
114
|
+
// Issued back to back, not awaited in between: on the transaction's one connection they still
|
|
115
|
+
// run in this order. The application awaits publish() in its transaction callback, so its
|
|
116
|
+
// driver must accept an async one (not better-sqlite3's), as start() and signal() require.
|
|
117
117
|
const settled = await Promise.allSettled(writes.map((write) => write.run(transaction)));
|
|
118
118
|
const index = settled.findIndex((result) => result.status === 'rejected');
|
|
119
119
|
if (index !== -1) {
|
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, WorkflowTextColumn, 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';
|
|
@@ -72,7 +72,10 @@ export interface SignalWorkflowOptions {
|
|
|
72
72
|
export interface WorkflowResultOptions {
|
|
73
73
|
/**
|
|
74
74
|
* How long to wait. Past it, the call rejects with `WorkflowResultTimeoutError`, and the
|
|
75
|
-
* instance keeps running. Wall-clock time, not the module's clock. Default: no limit.
|
|
75
|
+
* instance keeps running. Wall-clock time, not the module's clock. Default: no limit. A hard
|
|
76
|
+
* deadline: a store read still in flight when it passes isn't awaited, so a timeout shorter than
|
|
77
|
+
* one round trip to the store (0 included) rejects, also for an instance that has ended. To
|
|
78
|
+
* check an instance without waiting, use `getStatus()`.
|
|
76
79
|
*/
|
|
77
80
|
timeout?: Duration;
|
|
78
81
|
/** Stops waiting once aborted (a client that went away), rejecting with the signal's reason. */
|
|
@@ -53,9 +53,9 @@ export interface WorkflowCompensationContext extends WorkflowStepContext {
|
|
|
53
53
|
export interface WorkflowStepOptions<T> {
|
|
54
54
|
/** Default: the module's `retry` (3 attempts, 1s doubling up to 5m). */
|
|
55
55
|
retry?: number | false | WorkflowRetryOptions;
|
|
56
|
-
/** Fail the attempt (retryably) after this long. */
|
|
56
|
+
/** Fail the attempt (retryably) after this long. Capped at about 24.8 days, the longest timer Node supports. */
|
|
57
57
|
timeout?: Duration;
|
|
58
|
-
/** Fail the attempt (retryably) if `heartbeat()` is not called this often. */
|
|
58
|
+
/** Fail the attempt (retryably) if `heartbeat()` is not called this often. Capped like `timeout`. */
|
|
59
59
|
heartbeatTimeout?: Duration;
|
|
60
60
|
/**
|
|
61
61
|
* Undo this step, once it completed, if the workflow later fails or is
|
|
@@ -35,7 +35,10 @@ export interface WorkflowStore {
|
|
|
35
35
|
* hands its transaction callback (a Drizzle `tx`, a TypeORM `EntityManager`...). Write only
|
|
36
36
|
* through it, so the instance commits or rolls back with the application's rows, and never
|
|
37
37
|
* catch a database error inside it (on PostgreSQL that aborts the transaction: use
|
|
38
|
-
* insert-or-ignore). Without this method, `start()` with a transaction throws.
|
|
38
|
+
* insert-or-ignore). Without this method, `start()` with a transaction throws. The
|
|
39
|
+
* application awaits `start()` in its transaction callback, so its driver must accept an
|
|
40
|
+
* async one: one whose transactions are synchronous (better-sqlite3) commits before a
|
|
41
|
+
* failure is reported.
|
|
39
42
|
*/
|
|
40
43
|
createInTransaction?(transaction: unknown, instance: NewWorkflowInstance): Promise<{
|
|
41
44
|
instance: WorkflowInstance;
|
|
@@ -204,7 +207,32 @@ export interface WorkflowStore {
|
|
|
204
207
|
* See `WorkflowWrite` for what each field does.
|
|
205
208
|
*/
|
|
206
209
|
write(id: string, token: string, write: WorkflowWrite): Promise<boolean>;
|
|
210
|
+
/**
|
|
211
|
+
* Optional, for a store whose ids, names or keys are bounded (columns of a fixed length): throws, synchronously, an
|
|
212
|
+
* error that names the key (a `RangeError`) when the store can't hold `value` as a `kind`. The engine calls it for
|
|
213
|
+
* 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
|
|
214
|
+
* signal and key) before it journals or starts anything with it, so the error fails the run at the call that made
|
|
215
|
+
* the key, and the instance compensates, instead of failing every write that carries it. Without this method, the
|
|
216
|
+
* store's own check in `write()` or `create()` is all there is, and the instance is retried as after any store error.
|
|
217
|
+
*/
|
|
218
|
+
checkKey?(kind: WorkflowKeyKind, value: string): void;
|
|
219
|
+
/**
|
|
220
|
+
* Optional, for a store that can't keep some text (PostgreSQL can't keep U+0000, and neither PostgreSQL nor MySQL a
|
|
221
|
+
* lone UTF-16 surrogate in JSON): what `text` holds that the store can't keep in a `column`, such as `'U+0000'`, or
|
|
222
|
+
* `undefined`. `'json'` is text inside a JSON value (an input, a result, a payload, a journal entry, which holds its
|
|
223
|
+
* name); `'text'` is an id, a name, a key or a cancel reason in a column of its own. The engine asks before it stores
|
|
224
|
+
* what the application gives or a run makes, and refuses such text with a `TypeError` that names where it sits, so
|
|
225
|
+
* the call fails (a run fails and compensates) instead of every write that carries it. It doesn't ask about what a
|
|
226
|
+
* codec encodes, which the store sees only encoded. In an error's text, which can't be refused, U+0000 and lone
|
|
227
|
+
* surrogates are replaced with U+FFFD where the store refuses it. Without this method, nothing is refused or
|
|
228
|
+
* replaced, and the store's own failure in `write()` or `create()` is all there is.
|
|
229
|
+
*/
|
|
230
|
+
unstorableText?(text: string, column: WorkflowTextColumn): string | undefined;
|
|
207
231
|
}
|
|
232
|
+
/** The ids, names and keys `WorkflowStore.checkKey()` checks. */
|
|
233
|
+
export type WorkflowKeyKind = 'instanceId' | 'workflow' | 'concurrencyKey' | 'rateLimitKey' | 'journalName' | 'signal' | 'signalKey';
|
|
234
|
+
/** Where `WorkflowStore.unstorableText()`'s text is stored: inside a JSON value, or in a text column of its own. */
|
|
235
|
+
export type WorkflowTextColumn = 'json' | 'text';
|
|
208
236
|
/** `WorkflowStore.get()`'s result, and `WorkflowClient.getStatus()`'s. */
|
|
209
237
|
export interface WorkflowInstanceDetails extends WorkflowInstance {
|
|
210
238
|
/** Signals the instance waits for, if suspended in `waitForSignal()`. */
|
|
@@ -18,7 +18,7 @@ export interface MySqlWorkflowStoreOptions {
|
|
|
18
18
|
* Apply the store's pending migrations at startup, one statement at a time under a lock (`GET_LOCK()`), so processes
|
|
19
19
|
* that start together migrate once, and a run that stopped halfway resumes where it stopped. With `false`, startup
|
|
20
20
|
* fails with a `WorkflowSchemaError` while the schema is behind this version of the package: apply them with
|
|
21
|
-
* `npx
|
|
21
|
+
* `npx @nestjs/workflows migrate`, or with your own migration tool (`MySqlWorkflowStore.migrationSql()` or
|
|
22
22
|
* `migrationStatements()`). Default: `true`, except when `NODE_ENV` is `production`.
|
|
23
23
|
*/
|
|
24
24
|
migrate?: boolean;
|
|
@@ -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, WorkflowTextColumn, 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,11 @@ 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.
|
|
37
|
+
* - JSON can't hold a lone UTF-16 surrogate (`unstorableText()`): the engine refuses one in an input, a payload, a
|
|
38
|
+
* result or a step's name with a `TypeError` where the application or the run gives it, unless a codec encodes it.
|
|
35
39
|
* - The application's transactions may be REPEATABLE READ, MySQL's default. `signal(..., { transaction })` finds the
|
|
36
40
|
* instances to wake with a locking read of their waits, which sees the latest committed ones whatever the level; the
|
|
37
41
|
* signal lock is held until the application's transaction ends, as on PostgreSQL. `start(..., { transaction })`
|
|
@@ -164,6 +168,10 @@ export declare class MySqlWorkflowStore implements WorkflowStore, OnModuleInit {
|
|
|
164
168
|
terminateRequested: boolean;
|
|
165
169
|
} | null>;
|
|
166
170
|
write(id: string, token: string, write: WorkflowWrite): Promise<boolean>;
|
|
171
|
+
/** 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. */
|
|
172
|
+
checkKey(kind: WorkflowKeyKind, value: string): void;
|
|
173
|
+
/** A lone surrogate in JSON: what the engine refuses before it writes (see the class). */
|
|
174
|
+
unstorableText(text: string, column: WorkflowTextColumn): string | undefined;
|
|
167
175
|
/** The store's readiness (the server and the schema), then the signal lock's row: before its own first statement. */
|
|
168
176
|
private ready;
|
|
169
177
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
2
|
import { columns, ensureLockRows, keyColumn, lockKeys, mysqlErrorCode, quoteTable, retryOnDeadlock, SqlParams, toBool, toInt, toJson, toText, } from '@nestjs/store-kit/mysql';
|
|
3
|
+
import { loneSurrogateIn } from '../utils/storable-text.util.js';
|
|
3
4
|
import { RateWindowClaim } from '../sql/rate-windows.js';
|
|
4
5
|
import { CANCELLABLE_STATUSES, INSTANCE_COLUMNS, RUNNABLE_STATUSES, SCHEDULE_COLUMNS, SIGNAL_COLUMNS, toInstance, toSchedule } from '../sql/workflow-rows.js';
|
|
5
6
|
import { checkKey, MYSQL_KEY_LIMITS as L } from './key-limits.js';
|
|
@@ -47,7 +48,11 @@ const ROWS_PER_INSERT = 1_000;
|
|
|
47
48
|
* On MySQL:
|
|
48
49
|
* - Ids, names and keys are case- and accent-sensitive, and bounded: an instance id, a workflow's or a signal's name,
|
|
49
50
|
* 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.
|
|
51
|
+
* journal entry's name 512. A longer one fails with a `RangeError` that names it, before any SQL. One a run makes up
|
|
52
|
+
* (a step's or a wait's name, `$compensate:<step>`, a child's id or keys) is thrown where the run makes it
|
|
53
|
+
* (`checkKey()`), so the run fails and compensates, as any error it throws.
|
|
54
|
+
* - JSON can't hold a lone UTF-16 surrogate (`unstorableText()`): the engine refuses one in an input, a payload, a
|
|
55
|
+
* result or a step's name with a `TypeError` where the application or the run gives it, unless a codec encodes it.
|
|
51
56
|
* - The application's transactions may be REPEATABLE READ, MySQL's default. `signal(..., { transaction })` finds the
|
|
52
57
|
* instances to wake with a locking read of their waits, which sees the latest committed ones whatever the level; the
|
|
53
58
|
* signal lock is held until the application's transaction ends, as on PostgreSQL. `start(..., { transaction })`
|
|
@@ -604,8 +609,8 @@ WHERE ${p.in('id', ids)}`, p.values);
|
|
|
604
609
|
checkSignal(write.signal);
|
|
605
610
|
}
|
|
606
611
|
for (const wait of write.release?.waits ?? []) {
|
|
607
|
-
checkKey(
|
|
608
|
-
checkKey(
|
|
612
|
+
checkKey(KEYS.signal, wait.signal, L.signal);
|
|
613
|
+
checkKey(KEYS.signalKey, wait.key, L.signalKey);
|
|
609
614
|
}
|
|
610
615
|
return retryOnDeadlock(this.executor, async (tx) => {
|
|
611
616
|
const release = write.release;
|
|
@@ -650,6 +655,14 @@ WHERE ${p.in('id', ids)}`, p.values);
|
|
|
650
655
|
return true;
|
|
651
656
|
});
|
|
652
657
|
}
|
|
658
|
+
/** 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. */
|
|
659
|
+
checkKey(kind, value) {
|
|
660
|
+
checkKey(KEYS[kind], value, L[kind]);
|
|
661
|
+
}
|
|
662
|
+
/** A lone surrogate in JSON: what the engine refuses before it writes (see the class). */
|
|
663
|
+
unstorableText(text, column) {
|
|
664
|
+
return column === 'json' ? loneSurrogateIn(text) : undefined;
|
|
665
|
+
}
|
|
653
666
|
// ---------------------------------------------------------------- internals
|
|
654
667
|
/** The store's readiness (the server and the schema), then the signal lock's row: before its own first statement. */
|
|
655
668
|
async ready() {
|
|
@@ -823,6 +836,16 @@ function checkSignal(s) {
|
|
|
823
836
|
}
|
|
824
837
|
function checkEntries(entries) {
|
|
825
838
|
for (const entry of entries) {
|
|
826
|
-
checkKey(
|
|
839
|
+
checkKey(KEYS.journalName, entry.name, L.journalName);
|
|
827
840
|
}
|
|
828
841
|
}
|
|
842
|
+
/** How the errors of `checkKey()`, and of `write()`'s own checks, name each key. */
|
|
843
|
+
const KEYS = {
|
|
844
|
+
instanceId: 'an instance id',
|
|
845
|
+
workflow: "a workflow's name",
|
|
846
|
+
concurrencyKey: 'a concurrency key',
|
|
847
|
+
rateLimitKey: 'a rate-limit key',
|
|
848
|
+
journalName: "a journal entry's name (a step's, a wait's, or the engine's, such as $child: and a child's id)",
|
|
849
|
+
signal: "a wait's signal name",
|
|
850
|
+
signalKey: "a wait's signal key",
|
|
851
|
+
};
|
|
@@ -15,7 +15,7 @@ export interface PostgresWorkflowStoreOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Apply the store's pending migrations at startup, in one transaction under an advisory lock, so processes that
|
|
17
17
|
* start together migrate once. With `false`, startup fails with a `WorkflowSchemaError` while the schema is behind
|
|
18
|
-
* this version of the package: apply them with `npx
|
|
18
|
+
* this version of the package: apply them with `npx @nestjs/workflows migrate`, or with your own migration tool
|
|
19
19
|
* (`PostgresWorkflowStore.migrationSql()`). Default: `true`, except when `NODE_ENV` is `production`.
|
|
20
20
|
*/
|
|
21
21
|
migrate?: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type OnModuleInit } from '@nestjs/common';
|
|
2
2
|
import { type MigrationSqlOptions } from '@nestjs/store-kit/postgres';
|
|
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, WorkflowListQuery, WorkflowPurgeQuery, WorkflowPurgeResult, WorkflowReopen, WorkflowScheduleClaimRequest, WorkflowScheduleQuery, WorkflowScheduleRecord, WorkflowScheduleSave, WorkflowScheduleWrite, WorkflowSignalQuery, WorkflowSignalRecord, WorkflowSignalResult, WorkflowStore, WorkflowTextColumn, WorkflowWrite } from '../interfaces/workflow-store.interface.js';
|
|
5
5
|
import type { WorkflowStorage } from '../storage/workflow.storage.js';
|
|
6
6
|
import type { PostgresWorkflowStoreOptions } from './interfaces/postgres-workflow-store-options.interface.js';
|
|
7
7
|
/**
|
|
@@ -26,6 +26,9 @@ import type { PostgresWorkflowStoreOptions } from './interfaces/postgres-workflo
|
|
|
26
26
|
*
|
|
27
27
|
* Given `storage`, it registers itself (`storage.registerSource(this)`), as every store does. It checks its schema,
|
|
28
28
|
* or migrates it (`migrate`), in `onModuleInit` (so before the worker starts), or at its first call outside Nest.
|
|
29
|
+
*
|
|
30
|
+
* PostgreSQL keeps U+0000 in neither `text` nor `jsonb`, nor a lone UTF-16 surrogate in `jsonb` (`unstorableText()`):
|
|
31
|
+
* the engine refuses them with a `TypeError` where the application or a run gives them, unless a codec encodes them.
|
|
29
32
|
*/
|
|
30
33
|
export declare class PostgresWorkflowStore implements WorkflowStore, OnModuleInit {
|
|
31
34
|
/**
|
|
@@ -117,6 +120,8 @@ export declare class PostgresWorkflowStore implements WorkflowStore, OnModuleIni
|
|
|
117
120
|
terminateRequested: boolean;
|
|
118
121
|
} | null>;
|
|
119
122
|
write(id: string, token: string, write: WorkflowWrite): Promise<boolean>;
|
|
123
|
+
/** U+0000 anywhere, and a lone surrogate in JSON: what the engine refuses before it writes (see the class). */
|
|
124
|
+
unstorableText(text: string, column: WorkflowTextColumn): string | undefined;
|
|
120
125
|
private insertInstance;
|
|
121
126
|
private insertSignal;
|
|
122
127
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Logger } from '@nestjs/common';
|
|
2
2
|
import { advisoryLock, assertReadCommittedTransaction, columns, quoteSchema, SqlParams, toBool, toInt, toJson, toText, } from '@nestjs/store-kit/postgres';
|
|
3
|
+
import { loneSurrogateIn, nulIn } from '../utils/storable-text.util.js';
|
|
3
4
|
import { RateWindowClaim } from '../sql/rate-windows.js';
|
|
4
5
|
import { CANCELLABLE_STATUSES, INSTANCE_COLUMNS, RUNNABLE_STATUSES, SCHEDULE_COLUMNS, toInstance, toSchedule } from '../sql/workflow-rows.js';
|
|
5
6
|
import { workflowStoreSchema } from './migrations/index.js';
|
|
@@ -29,6 +30,9 @@ const CANCELLABLE = `(${CANCELLABLE_STATUSES.map((status) => `'${status}'`).join
|
|
|
29
30
|
*
|
|
30
31
|
* Given `storage`, it registers itself (`storage.registerSource(this)`), as every store does. It checks its schema,
|
|
31
32
|
* or migrates it (`migrate`), in `onModuleInit` (so before the worker starts), or at its first call outside Nest.
|
|
33
|
+
*
|
|
34
|
+
* PostgreSQL keeps U+0000 in neither `text` nor `jsonb`, nor a lone UTF-16 surrogate in `jsonb` (`unstorableText()`):
|
|
35
|
+
* the engine refuses them with a `TypeError` where the application or a run gives them, unless a codec encodes them.
|
|
32
36
|
*/
|
|
33
37
|
export class PostgresWorkflowStore {
|
|
34
38
|
/**
|
|
@@ -491,6 +495,10 @@ RETURNING cancel_requested::text AS cancel_requested, terminate_requested::text
|
|
|
491
495
|
return true;
|
|
492
496
|
}, READ_COMMITTED);
|
|
493
497
|
}
|
|
498
|
+
/** U+0000 anywhere, and a lone surrogate in JSON: what the engine refuses before it writes (see the class). */
|
|
499
|
+
unstorableText(text, column) {
|
|
500
|
+
return nulIn(text) ?? (column === 'json' ? loneSurrogateIn(text) : undefined);
|
|
501
|
+
}
|
|
494
502
|
// ---------------------------------------------------------------- internals
|
|
495
503
|
async insertInstance(db, i) {
|
|
496
504
|
// An instance deleted between the insert that met it and the read of it is created again.
|
|
@@ -39,7 +39,12 @@ export declare class WorkflowClient implements BeforeApplicationShutdown {
|
|
|
39
39
|
* with a different workflow or input. With `transaction`, the instance is
|
|
40
40
|
* created in your transaction and commits or rolls back with it. Inside a
|
|
41
41
|
* workflow step, a start without an `id` gets one derived from the step, so a
|
|
42
|
-
* retried step gets its instance back instead of starting another.
|
|
42
|
+
* retried step gets its instance back instead of starting another. An id, a
|
|
43
|
+
* key or an input string the store can't keep (`WorkflowStore.unstorableText()`:
|
|
44
|
+
* U+0000 on PostgreSQL, a lone UTF-16 surrogate in JSON on PostgreSQL and MySQL)
|
|
45
|
+
* is refused with a `TypeError`. Await it in your transaction's callback: a
|
|
46
|
+
* driver whose callback can't be async (better-sqlite3) commits before a failure
|
|
47
|
+
* is reported.
|
|
43
48
|
*/
|
|
44
49
|
start<W>(workflow: Type<W> | string, input: WorkflowInput<W>, options?: StartWorkflowOptions): Promise<WorkflowStartResult>;
|
|
45
50
|
/**
|
|
@@ -72,7 +77,8 @@ export declare class WorkflowClient implements BeforeApplicationShutdown {
|
|
|
72
77
|
private outcomeOf;
|
|
73
78
|
/**
|
|
74
79
|
* `start()`, then `result()`: resolves with the output of the instance (the existing one, for
|
|
75
|
-
* an id that already has one), or rejects as `result()` does. `wait` is `result()`'s options
|
|
80
|
+
* an id that already has one), or rejects as `result()` does. `wait` is `result()`'s options,
|
|
81
|
+
* counted once the instance is stored (`start()` isn't bounded by them);
|
|
76
82
|
* `options.timeout` stays the instance's run timeout. Not with `{ transaction }`: the instance
|
|
77
83
|
* wouldn't exist before your transaction commits.
|
|
78
84
|
*/
|
|
@@ -107,6 +113,9 @@ export declare class WorkflowClient implements BeforeApplicationShutdown {
|
|
|
107
113
|
* instances parked on a matching wait right now. With `transaction`, the
|
|
108
114
|
* signal and the wake-ups commit or roll back with your transaction. With
|
|
109
115
|
* `id`, or from inside a workflow step, a repeated signal is stored once.
|
|
116
|
+
* A name, key, id or payload string the store can't keep is refused with a
|
|
117
|
+
* `TypeError`, as in `start()`. Await it in your transaction's callback, as
|
|
118
|
+
* `start()`.
|
|
110
119
|
*/
|
|
111
120
|
signal<T>(signal: WorkflowSignal<T> | string, payload: NoInfer<T>, options?: SignalWorkflowOptions): Promise<WorkflowSignalSendResult>;
|
|
112
121
|
/**
|
|
@@ -25,6 +25,7 @@ import { assertSameInstance, newInstance } from '../utils/new-instance.util.js';
|
|
|
25
25
|
import { signalName } from '../signals/workflow.signal.js';
|
|
26
26
|
import { ENGINE_STORE, WorkflowStorage } from '../storage/workflow.storage.js';
|
|
27
27
|
import { stepSignalId, stepStartId } from '../utils/step-scope.util.js';
|
|
28
|
+
import { assertStorableJson, assertStorableText } from '../utils/storable-text.util.js';
|
|
28
29
|
import { WorkflowRegistry } from './workflow-registry.service.js';
|
|
29
30
|
import { WorkflowSchedules } from './workflow-schedules.service.js';
|
|
30
31
|
import { WorkflowWorker } from './workflow-worker.service.js';
|
|
@@ -75,7 +76,12 @@ let WorkflowClient = class WorkflowClient {
|
|
|
75
76
|
* with a different workflow or input. With `transaction`, the instance is
|
|
76
77
|
* created in your transaction and commits or rolls back with it. Inside a
|
|
77
78
|
* workflow step, a start without an `id` gets one derived from the step, so a
|
|
78
|
-
* retried step gets its instance back instead of starting another.
|
|
79
|
+
* retried step gets its instance back instead of starting another. An id, a
|
|
80
|
+
* key or an input string the store can't keep (`WorkflowStore.unstorableText()`:
|
|
81
|
+
* U+0000 on PostgreSQL, a lone UTF-16 surrogate in JSON on PostgreSQL and MySQL)
|
|
82
|
+
* is refused with a `TypeError`. Await it in your transaction's callback: a
|
|
83
|
+
* driver whose callback can't be async (better-sqlite3) commits before a failure
|
|
84
|
+
* is reported.
|
|
79
85
|
*/
|
|
80
86
|
async start(workflow, input, options = {}) {
|
|
81
87
|
const resolved = this.registry.resolve(workflow, options.version);
|
|
@@ -84,13 +90,14 @@ let WorkflowClient = class WorkflowClient {
|
|
|
84
90
|
const data = newInstance(resolved, id, input, {
|
|
85
91
|
caller: 'start()',
|
|
86
92
|
now: this.clock.now(),
|
|
93
|
+
text: this.store.textCheck,
|
|
87
94
|
timeout: options.timeout,
|
|
88
95
|
concurrencyKey: options.concurrencyKey,
|
|
89
96
|
rateLimitKey: options.rateLimitKey,
|
|
90
97
|
priority: options.priority,
|
|
91
98
|
});
|
|
92
|
-
// Nothing is awaited before the store's call:
|
|
93
|
-
//
|
|
99
|
+
// Nothing is awaited before the store's call: calls issued back to back (the CQRS publisher's)
|
|
100
|
+
// reach the transaction's connection in the order they were made.
|
|
94
101
|
const { instance, created } = await (options.transaction === undefined
|
|
95
102
|
? this.store.create(data)
|
|
96
103
|
: this.storeMethod('createInTransaction', 'start')(options.transaction, data));
|
|
@@ -150,7 +157,8 @@ let WorkflowClient = class WorkflowClient {
|
|
|
150
157
|
}
|
|
151
158
|
/**
|
|
152
159
|
* `start()`, then `result()`: resolves with the output of the instance (the existing one, for
|
|
153
|
-
* an id that already has one), or rejects as `result()` does. `wait` is `result()`'s options
|
|
160
|
+
* an id that already has one), or rejects as `result()` does. `wait` is `result()`'s options,
|
|
161
|
+
* counted once the instance is stored (`start()` isn't bounded by them);
|
|
154
162
|
* `options.timeout` stays the instance's run timeout. Not with `{ transaction }`: the instance
|
|
155
163
|
* wouldn't exist before your transaction commits.
|
|
156
164
|
*/
|
|
@@ -208,6 +216,7 @@ let WorkflowClient = class WorkflowClient {
|
|
|
208
216
|
return this.stop(id, reason, true);
|
|
209
217
|
}
|
|
210
218
|
async stop(id, reason, terminate) {
|
|
219
|
+
assertStorableText('Reason', reason, this.store.textCheck('cancelReason'));
|
|
211
220
|
const accepted = await this.store.requestCancel(id, { reason: reason ?? null, now: this.clock.now(), terminate });
|
|
212
221
|
// Accepted is accepted: an instance whose payloads no codec can read any more is returned as stored.
|
|
213
222
|
const details = await this.store.get(id).catch(() => this.store.inner.get(id));
|
|
@@ -228,6 +237,9 @@ let WorkflowClient = class WorkflowClient {
|
|
|
228
237
|
* instances parked on a matching wait right now. With `transaction`, the
|
|
229
238
|
* signal and the wake-ups commit or roll back with your transaction. With
|
|
230
239
|
* `id`, or from inside a workflow step, a repeated signal is stored once.
|
|
240
|
+
* A name, key, id or payload string the store can't keep is refused with a
|
|
241
|
+
* `TypeError`, as in `start()`. Await it in your transaction's callback, as
|
|
242
|
+
* `start()`.
|
|
231
243
|
*/
|
|
232
244
|
async signal(signal, payload, options = {}) {
|
|
233
245
|
const name = signalName(signal);
|
|
@@ -236,7 +248,13 @@ let WorkflowClient = class WorkflowClient {
|
|
|
236
248
|
throw new TypeError(`Invalid signal id ${JSON.stringify(options.id)}. Use a non-empty string, such as the id of the event that causes it.`);
|
|
237
249
|
}
|
|
238
250
|
const dedupeId = options.id ?? stepSignalId(name, key) ?? null;
|
|
239
|
-
const
|
|
251
|
+
const keyCheck = this.store.textCheck('key');
|
|
252
|
+
assertStorableText('A signal name', name, keyCheck);
|
|
253
|
+
assertStorableText('A signal key', key, keyCheck);
|
|
254
|
+
assertStorableText('A signal id', dedupeId, keyCheck);
|
|
255
|
+
const json = normalize(payload);
|
|
256
|
+
assertStorableJson('Signal payload', json, this.store.textCheck('payload'));
|
|
257
|
+
const data = { name, key, dedupeId, payload: json, now: this.clock.now() };
|
|
240
258
|
// Nothing is awaited before the store's call, as in start().
|
|
241
259
|
const result = await (options.transaction === undefined
|
|
242
260
|
? this.store.signal(data)
|