@nestjs/workflows 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +50 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8 -0
- package/dist/core/codecs/aes-gcm-payload.codec.d.ts +42 -0
- package/dist/core/codecs/aes-gcm-payload.codec.js +119 -0
- package/dist/core/codecs/index.d.ts +2 -0
- package/dist/core/codecs/index.js +2 -0
- package/dist/core/codecs/payload-codecs.d.ts +59 -0
- package/dist/core/codecs/payload-codecs.js +130 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +21 -0
- package/dist/core/interfaces/clock.interface.d.ts +18 -0
- package/dist/core/interfaces/clock.interface.js +1 -0
- package/dist/core/interfaces/index.d.ts +9 -0
- package/dist/core/interfaces/index.js +9 -0
- package/dist/core/interfaces/leased-worker-options.interface.d.ts +94 -0
- package/dist/core/interfaces/leased-worker-options.interface.js +1 -0
- package/dist/core/interfaces/limits.interface.d.ts +92 -0
- package/dist/core/interfaces/limits.interface.js +1 -0
- package/dist/core/interfaces/payload-codec.interface.d.ts +64 -0
- package/dist/core/interfaces/payload-codec.interface.js +1 -0
- package/dist/core/interfaces/result-waiter-options.interface.d.ts +52 -0
- package/dist/core/interfaces/result-waiter-options.interface.js +1 -0
- package/dist/core/interfaces/retry-settings.interface.d.ts +71 -0
- package/dist/core/interfaces/retry-settings.interface.js +1 -0
- package/dist/core/interfaces/schedule-store.interface.d.ts +143 -0
- package/dist/core/interfaces/schedule-store.interface.js +1 -0
- package/dist/core/interfaces/schedule.interface.d.ts +106 -0
- package/dist/core/interfaces/schedule.interface.js +1 -0
- package/dist/core/interfaces/scheduler-options.interface.d.ts +152 -0
- package/dist/core/interfaces/scheduler-options.interface.js +1 -0
- package/dist/core/limits/index.d.ts +1 -0
- package/dist/core/limits/index.js +1 -0
- package/dist/core/limits/limits.d.ts +61 -0
- package/dist/core/limits/limits.js +130 -0
- package/dist/core/results/index.d.ts +1 -0
- package/dist/core/results/index.js +1 -0
- package/dist/core/results/result-waiter.d.ts +54 -0
- package/dist/core/results/result-waiter.js +140 -0
- package/dist/core/retries/index.d.ts +1 -0
- package/dist/core/retries/index.js +1 -0
- package/dist/core/retries/retry.d.ts +60 -0
- package/dist/core/retries/retry.js +107 -0
- package/dist/core/scheduling/cron.util.d.ts +52 -0
- package/dist/core/scheduling/cron.util.js +262 -0
- package/dist/core/scheduling/encoded-schedule.store.d.ts +26 -0
- package/dist/core/scheduling/encoded-schedule.store.js +54 -0
- package/dist/core/scheduling/in-memory-schedule.store.d.ts +20 -0
- package/dist/core/scheduling/in-memory-schedule.store.js +90 -0
- package/dist/core/scheduling/index.d.ts +3 -0
- package/dist/core/scheduling/index.js +3 -0
- package/dist/core/scheduling/rrule.util.d.ts +55 -0
- package/dist/core/scheduling/rrule.util.js +371 -0
- package/dist/core/scheduling/schedule-spec.d.ts +57 -0
- package/dist/core/scheduling/schedule-spec.js +233 -0
- package/dist/core/scheduling/scheduler.d.ts +134 -0
- package/dist/core/scheduling/scheduler.js +450 -0
- package/dist/core/scheduling/time-zone.util.d.ts +18 -0
- package/dist/core/scheduling/time-zone.util.js +88 -0
- package/dist/core/time/clock.d.ts +28 -0
- package/dist/core/time/clock.js +35 -0
- package/dist/core/time/duration.d.ts +21 -0
- package/dist/core/time/duration.js +30 -0
- package/dist/core/time/index.d.ts +2 -0
- package/dist/core/time/index.js +2 -0
- package/dist/core/utils/canonical.util.d.ts +2 -0
- package/dist/core/utils/canonical.util.js +4 -0
- package/dist/core/utils/maybe.util.d.ts +8 -0
- package/dist/core/utils/maybe.util.js +14 -0
- package/dist/core/utils/payload-place.util.d.ts +10 -0
- package/dist/core/utils/payload-place.util.js +32 -0
- package/dist/core/workers/index.d.ts +1 -0
- package/dist/core/workers/index.js +1 -0
- package/dist/core/workers/leased-worker.d.ts +86 -0
- package/dist/core/workers/leased-worker.js +280 -0
- package/dist/cqrs/decorators/index.d.ts +2 -0
- package/dist/cqrs/decorators/index.js +2 -0
- package/dist/cqrs/decorators/signal-on.decorator.d.ts +18 -0
- package/dist/cqrs/decorators/signal-on.decorator.js +34 -0
- package/dist/cqrs/decorators/start-on.decorator.d.ts +18 -0
- package/dist/cqrs/decorators/start-on.decorator.js +49 -0
- package/dist/cqrs/index.d.ts +3 -0
- package/dist/cqrs/index.js +5 -0
- package/dist/cqrs/interfaces/index.d.ts +4 -0
- package/dist/cqrs/interfaces/index.js +4 -0
- package/dist/cqrs/interfaces/signal-on-options.interface.d.ts +22 -0
- package/dist/cqrs/interfaces/signal-on-options.interface.js +1 -0
- package/dist/cqrs/interfaces/start-on-options.interface.d.ts +23 -0
- package/dist/cqrs/interfaces/start-on-options.interface.js +1 -0
- package/dist/cqrs/interfaces/workflow-dispatcher-context.interface.d.ts +10 -0
- package/dist/cqrs/interfaces/workflow-dispatcher-context.interface.js +1 -0
- package/dist/cqrs/interfaces/workflow-event-route.interface.d.ts +20 -0
- package/dist/cqrs/interfaces/workflow-event-route.interface.js +1 -0
- package/dist/cqrs/utils/event-routes.util.d.ts +7 -0
- package/dist/cqrs/utils/event-routes.util.js +22 -0
- package/dist/cqrs/workflow-event.publisher.d.ts +52 -0
- package/dist/cqrs/workflow-event.publisher.js +257 -0
- package/dist/cqrs/workflow-events.explorer.d.ts +27 -0
- package/dist/cqrs/workflow-events.explorer.js +92 -0
- package/dist/cqrs/workflows-cqrs.module.d.ts +10 -0
- package/dist/cqrs/workflows-cqrs.module.js +27 -0
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/workflow.decorator.d.ts +7 -0
- package/dist/decorators/workflow.decorator.js +52 -0
- package/dist/errors/child-workflow-failed.error.d.ts +13 -0
- package/dist/errors/child-workflow-failed.error.js +14 -0
- package/dist/errors/index.d.ts +13 -0
- package/dist/errors/index.js +13 -0
- package/dist/errors/non-retryable-step.error.d.ts +4 -0
- package/dist/errors/non-retryable-step.error.js +7 -0
- package/dist/errors/step-failed.error.d.ts +15 -0
- package/dist/errors/step-failed.error.js +16 -0
- package/dist/errors/step-timeout.error.d.ts +5 -0
- package/dist/errors/step-timeout.error.js +8 -0
- package/dist/errors/workflow-definition.error.d.ts +10 -0
- package/dist/errors/workflow-definition.error.js +13 -0
- package/dist/errors/workflow-failed.error.d.ts +20 -0
- package/dist/errors/workflow-failed.error.js +17 -0
- package/dist/errors/workflow-id-conflict.error.d.ts +6 -0
- package/dist/errors/workflow-id-conflict.error.js +9 -0
- package/dist/errors/workflow-interrupt.error.d.ts +32 -0
- package/dist/errors/workflow-interrupt.error.js +19 -0
- package/dist/errors/workflow-non-determinism.error.d.ts +4 -0
- package/dist/errors/workflow-non-determinism.error.js +7 -0
- package/dist/errors/workflow-not-found.error.d.ts +10 -0
- package/dist/errors/workflow-not-found.error.js +13 -0
- package/dist/errors/workflow-result-timeout.error.d.ts +11 -0
- package/dist/errors/workflow-result-timeout.error.js +13 -0
- package/dist/errors/workflow-state.error.d.ts +11 -0
- package/dist/errors/workflow-state.error.js +14 -0
- package/dist/errors/workflow.error.d.ts +7 -0
- package/dist/errors/workflow.error.js +7 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +3 -0
- package/dist/events/workflow-events.interface.d.ts +145 -0
- package/dist/events/workflow-events.interface.js +1 -0
- package/dist/events/workflow-events.service.d.ts +17 -0
- package/dist/events/workflow-events.service.js +38 -0
- package/dist/events/workflows.channels.d.ts +3 -0
- package/dist/events/workflows.channels.js +9 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +23 -0
- package/dist/interfaces/index.d.ts +13 -0
- package/dist/interfaces/index.js +13 -0
- package/dist/interfaces/serialized-workflow-error.interface.d.ts +6 -0
- package/dist/interfaces/serialized-workflow-error.interface.js +1 -0
- package/dist/interfaces/workflow-client.interface.d.ts +156 -0
- package/dist/interfaces/workflow-client.interface.js +1 -0
- package/dist/interfaces/workflow-clock.interface.d.ts +7 -0
- package/dist/interfaces/workflow-clock.interface.js +1 -0
- package/dist/interfaces/workflow-context.interface.d.ts +241 -0
- package/dist/interfaces/workflow-context.interface.js +1 -0
- package/dist/interfaces/workflow-decorator-options.interface.d.ts +101 -0
- package/dist/interfaces/workflow-decorator-options.interface.js +1 -0
- package/dist/interfaces/workflow-instance.interface.d.ts +83 -0
- package/dist/interfaces/workflow-instance.interface.js +1 -0
- package/dist/interfaces/workflow-payload-codec.interface.d.ts +32 -0
- package/dist/interfaces/workflow-payload-codec.interface.js +1 -0
- package/dist/interfaces/workflow-retry-options.interface.d.ts +9 -0
- package/dist/interfaces/workflow-retry-options.interface.js +1 -0
- package/dist/interfaces/workflow-runner.interface.d.ts +11 -0
- package/dist/interfaces/workflow-runner.interface.js +1 -0
- package/dist/interfaces/workflow-schedule.interface.d.ts +104 -0
- package/dist/interfaces/workflow-schedule.interface.js +1 -0
- package/dist/interfaces/workflow-storage.interface.d.ts +4 -0
- package/dist/interfaces/workflow-storage.interface.js +1 -0
- package/dist/interfaces/workflow-store.interface.d.ts +488 -0
- package/dist/interfaces/workflow-store.interface.js +1 -0
- package/dist/interfaces/workflows-module-options.interface.d.ts +80 -0
- package/dist/interfaces/workflows-module-options.interface.js +1 -0
- package/dist/mysql/index.d.ts +5 -0
- package/dist/mysql/index.js +8 -0
- package/dist/mysql/interfaces/index.d.ts +1 -0
- package/dist/mysql/interfaces/index.js +1 -0
- package/dist/mysql/interfaces/mysql-workflow-store-options.interface.d.ts +25 -0
- package/dist/mysql/interfaces/mysql-workflow-store-options.interface.js +1 -0
- package/dist/mysql/key-limits.d.ts +30 -0
- package/dist/mysql/key-limits.js +41 -0
- package/dist/mysql/migrations/index.d.ts +7 -0
- package/dist/mysql/migrations/index.js +16 -0
- package/dist/mysql/migrations/initial.migration.d.ts +9 -0
- package/dist/mysql/migrations/initial.migration.js +104 -0
- package/dist/mysql/mysql-workflow.store.d.ts +197 -0
- package/dist/mysql/mysql-workflow.store.js +828 -0
- package/dist/postgres/errors/index.d.ts +1 -0
- package/dist/postgres/errors/index.js +2 -0
- package/dist/postgres/index.d.ts +5 -0
- package/dist/postgres/index.js +8 -0
- package/dist/postgres/interfaces/index.d.ts +1 -0
- package/dist/postgres/interfaces/index.js +1 -0
- package/dist/postgres/interfaces/postgres-workflow-store-options.interface.d.ts +22 -0
- package/dist/postgres/interfaces/postgres-workflow-store-options.interface.js +1 -0
- package/dist/postgres/migrations/index.d.ts +6 -0
- package/dist/postgres/migrations/index.js +15 -0
- package/dist/postgres/migrations/initial.migration.d.ts +6 -0
- package/dist/postgres/migrations/initial.migration.js +106 -0
- package/dist/postgres/postgres-workflow.store.d.ts +132 -0
- package/dist/postgres/postgres-workflow.store.js +586 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +6 -0
- package/dist/services/workflow-client.service.d.ts +146 -0
- package/dist/services/workflow-client.service.js +436 -0
- package/dist/services/workflow-execution.service.d.ts +267 -0
- package/dist/services/workflow-execution.service.js +1076 -0
- package/dist/services/workflow-registry.service.d.ts +56 -0
- package/dist/services/workflow-registry.service.js +172 -0
- package/dist/services/workflow-scheduler.service.d.ts +54 -0
- package/dist/services/workflow-scheduler.service.js +173 -0
- package/dist/services/workflow-schedules.service.d.ts +60 -0
- package/dist/services/workflow-schedules.service.js +193 -0
- package/dist/services/workflow-worker.service.d.ts +92 -0
- package/dist/services/workflow-worker.service.js +468 -0
- package/dist/signals/index.d.ts +1 -0
- package/dist/signals/index.js +1 -0
- package/dist/signals/workflow.signal.d.ts +17 -0
- package/dist/signals/workflow.signal.js +28 -0
- package/dist/sql/rate-windows.d.ts +41 -0
- package/dist/sql/rate-windows.js +53 -0
- package/dist/sql/workflow-rows.d.ts +16 -0
- package/dist/sql/workflow-rows.js +102 -0
- package/dist/sql/workflow-schema.error.d.ts +21 -0
- package/dist/sql/workflow-schema.error.js +15 -0
- package/dist/storage/encoded-workflow.store.d.ts +71 -0
- package/dist/storage/encoded-workflow.store.js +201 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/workflow.storage.d.ts +55 -0
- package/dist/storage/workflow.storage.js +159 -0
- package/dist/stores/in-memory-workflow.store.d.ts +75 -0
- package/dist/stores/in-memory-workflow.store.js +380 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/testing/assertions.d.ts +10 -0
- package/dist/testing/assertions.js +72 -0
- package/dist/testing/index.d.ts +38 -0
- package/dist/testing/index.js +1089 -0
- package/dist/testing/schedule-store.contract.d.ts +33 -0
- package/dist/testing/schedule-store.contract.js +161 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/journal-limits.util.d.ts +6 -0
- package/dist/utils/journal-limits.util.js +25 -0
- package/dist/utils/new-instance.util.d.ts +26 -0
- package/dist/utils/new-instance.util.js +87 -0
- package/dist/utils/normalize.util.d.ts +5 -0
- package/dist/utils/normalize.util.js +11 -0
- package/dist/utils/run-timeout.util.d.ts +3 -0
- package/dist/utils/run-timeout.util.js +15 -0
- package/dist/utils/schedule-spec.util.d.ts +16 -0
- package/dist/utils/schedule-spec.util.js +9 -0
- package/dist/utils/schedule-store.util.d.ts +15 -0
- package/dist/utils/schedule-store.util.js +44 -0
- package/dist/utils/serialize-error.util.d.ts +7 -0
- package/dist/utils/serialize-error.util.js +22 -0
- package/dist/utils/step-scope.util.d.ts +16 -0
- package/dist/utils/step-scope.util.js +38 -0
- package/dist/workflows.constants.d.ts +10 -0
- package/dist/workflows.constants.js +10 -0
- package/dist/workflows.module-definition.d.ts +4 -0
- package/dist/workflows.module-definition.js +6 -0
- package/dist/workflows.module.d.ts +24 -0
- package/dist/workflows.module.js +68 -0
- package/package.json +123 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const UNITS = {
|
|
2
|
+
ms: 1,
|
|
3
|
+
s: 1_000,
|
|
4
|
+
m: 60_000,
|
|
5
|
+
h: 3_600_000,
|
|
6
|
+
d: 86_400_000,
|
|
7
|
+
w: 604_800_000,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A `Duration` in milliseconds (a string's rounded to a whole one). Throws a `TypeError` for a negative or
|
|
11
|
+
* non-finite number, or a string it can't read.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* parseDuration('15m'); // 900_000
|
|
15
|
+
* parseDuration(250); // 250
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function parseDuration(duration) {
|
|
19
|
+
if (typeof duration === 'number') {
|
|
20
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
21
|
+
throw new TypeError(`Invalid duration ${duration}. Use a non-negative number of milliseconds.`);
|
|
22
|
+
}
|
|
23
|
+
return duration;
|
|
24
|
+
}
|
|
25
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/.exec(duration);
|
|
26
|
+
if (!match) {
|
|
27
|
+
throw new TypeError(`Invalid duration "${duration}". Use milliseconds or a string such as "15m" or "3d".`);
|
|
28
|
+
}
|
|
29
|
+
return Math.round(Number(match[1]) * UNITS[match[2]]);
|
|
30
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** JSON with sorted object keys, to compare inputs and payloads by value. */
|
|
2
|
+
export function canonical(value) {
|
|
3
|
+
return (JSON.stringify(value, (_key, v) => v && typeof v === 'object' && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) : v) ?? 'undefined');
|
|
4
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A value, or a promise of it: what a synchronous codec keeps synchronous. */
|
|
2
|
+
export type Maybe<T> = T | Promise<T>;
|
|
3
|
+
/**
|
|
4
|
+
* `next(value)`, at once when `value` isn't a promise: nothing is awaited before a synchronous codec's result. (Not
|
|
5
|
+
* named `then`: a module that exports a `then` is a thenable, which a dynamic import awaits.)
|
|
6
|
+
*/
|
|
7
|
+
export declare function andThen<T, R>(value: Maybe<T>, next: (value: T) => Maybe<R>): Maybe<R>;
|
|
8
|
+
export declare function toPromise<T>(value: Maybe<T>): Promise<T>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `next(value)`, at once when `value` isn't a promise: nothing is awaited before a synchronous codec's result. (Not
|
|
3
|
+
* named `then`: a module that exports a `then` is a thenable, which a dynamic import awaits.)
|
|
4
|
+
*/
|
|
5
|
+
export function andThen(value, next) {
|
|
6
|
+
return thenable(value) ? Promise.resolve(value).then(next) : next(value);
|
|
7
|
+
}
|
|
8
|
+
export function toPromise(value) {
|
|
9
|
+
return Promise.resolve(value);
|
|
10
|
+
}
|
|
11
|
+
/** A promise, from this realm or not (a codec's library may bring its own). */
|
|
12
|
+
function thenable(value) {
|
|
13
|
+
return typeof value?.then === 'function';
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PayloadContext } from '../interfaces/payload-codec.interface.js';
|
|
2
|
+
/** The keys of workflows' payload contexts, in the order their authenticated data lists them. */
|
|
3
|
+
export declare const WORKFLOW_PLACES: readonly ['instanceId', 'entry', 'signal', 'schedule'];
|
|
4
|
+
/**
|
|
5
|
+
* Where a payload is, for errors: `entry "charge" of instance "order-1" (result)` for workflows' contexts, and the
|
|
6
|
+
* context's keys otherwise (`job "42", queue "emails" (data)`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function describePlace(context: PayloadContext): string;
|
|
9
|
+
/** The context's keys other than `field` and workflows' own, that hold a value, by name. */
|
|
10
|
+
export declare function otherPlaces(context: PayloadContext): string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** The keys of workflows' payload contexts, in the order their authenticated data lists them. */
|
|
2
|
+
export const WORKFLOW_PLACES = ['instanceId', 'entry', 'signal', 'schedule'];
|
|
3
|
+
/**
|
|
4
|
+
* Where a payload is, for errors: `entry "charge" of instance "order-1" (result)` for workflows' contexts, and the
|
|
5
|
+
* context's keys otherwise (`job "42", queue "emails" (data)`).
|
|
6
|
+
*/
|
|
7
|
+
export function describePlace(context) {
|
|
8
|
+
let where;
|
|
9
|
+
if (context.entry !== undefined) {
|
|
10
|
+
where = `entry "${context.entry}" of instance "${context.instanceId}"`;
|
|
11
|
+
}
|
|
12
|
+
else if (context.instanceId !== undefined) {
|
|
13
|
+
where = `instance "${context.instanceId}"`;
|
|
14
|
+
}
|
|
15
|
+
else if (context.signal !== undefined) {
|
|
16
|
+
where = `signal "${context.signal}"`;
|
|
17
|
+
}
|
|
18
|
+
else if (context.schedule !== undefined) {
|
|
19
|
+
where = `schedule "${context.schedule}"`;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const keys = otherPlaces(context);
|
|
23
|
+
where = keys.length > 0 ? keys.map((key) => `${key} "${context[key]}"`).join(', ') : 'an unnamed place';
|
|
24
|
+
}
|
|
25
|
+
return `${where} (${context.field})`;
|
|
26
|
+
}
|
|
27
|
+
/** The context's keys other than `field` and workflows' own, that hold a value, by name. */
|
|
28
|
+
export function otherPlaces(context) {
|
|
29
|
+
return Object.keys(context)
|
|
30
|
+
.filter((key) => key !== 'field' && !WORKFLOW_PLACES.includes(key) && context[key] !== undefined)
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './leased-worker.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './leased-worker.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { LeasedWorkerOptions } from '../interfaces/leased-worker-options.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* A claim-execute loop under leases, any number of processes sharing a store: it claims due items (as many as it has
|
|
4
|
+
* free slots), executes them, renews each one's lease every `heartbeatInterval` (a third of the lease by default), and
|
|
5
|
+
* aborts an item's `signal` when a renewal finds its lease gone. It looks for work every `pollInterval`, and at once
|
|
6
|
+
* after `kick()` (a local write made something due) or when an item ends. `drain()` runs due items on demand, for
|
|
7
|
+
* tests and scripts.
|
|
8
|
+
*
|
|
9
|
+
* Shutdown is graceful: call `shutdown()` from `onModuleDestroy`. It stops claiming, waits for the claims in flight
|
|
10
|
+
* (what they claim starts, told to stop at once), aborts every running item's `signal`, and waits up to
|
|
11
|
+
* `shutdownTimeout` for them; the ones still running are then detached (no more renewals) and keep their lease until
|
|
12
|
+
* it expires, as their work may still be going on.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* @Injectable()
|
|
16
|
+
* export class JobWorker implements OnApplicationBootstrap, OnModuleDestroy {
|
|
17
|
+
* private readonly worker = new LeasedWorker<Job>({
|
|
18
|
+
* claim: (lease, limit) => this.store.claim(lease, limit),
|
|
19
|
+
* renew: (job, until) => this.store.renew(job.id, job.token, until),
|
|
20
|
+
* execute: (job, run) => this.run(job, run),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* constructor(private readonly store: JobStore) {}
|
|
24
|
+
*
|
|
25
|
+
* onApplicationBootstrap() {
|
|
26
|
+
* this.worker.start();
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* onModuleDestroy() {
|
|
30
|
+
* return this.worker.shutdown();
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class LeasedWorker<T> {
|
|
36
|
+
private readonly options;
|
|
37
|
+
/** The worker's id, shown as the lease owner. */
|
|
38
|
+
readonly owner: string;
|
|
39
|
+
/** The lease duration, in milliseconds. */
|
|
40
|
+
readonly leaseMs: number;
|
|
41
|
+
private readonly logger;
|
|
42
|
+
private readonly clock;
|
|
43
|
+
private readonly concurrency;
|
|
44
|
+
private readonly pollMs;
|
|
45
|
+
private readonly heartbeatMs;
|
|
46
|
+
private readonly shutdownMs;
|
|
47
|
+
private readonly produceMs;
|
|
48
|
+
private readonly runs;
|
|
49
|
+
/** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
|
|
50
|
+
private readonly claiming;
|
|
51
|
+
private stopped;
|
|
52
|
+
private stop;
|
|
53
|
+
private readonly stoppedPromise;
|
|
54
|
+
private wake?;
|
|
55
|
+
private kicked;
|
|
56
|
+
private loop?;
|
|
57
|
+
/** When the loop next calls `produce()` (`performance.now()`). */
|
|
58
|
+
private produceAt;
|
|
59
|
+
constructor(options: LeasedWorkerOptions<T>);
|
|
60
|
+
/** The items it executes now. */
|
|
61
|
+
get running(): T[];
|
|
62
|
+
/** Starts the polling loop (once). Without it, the worker only runs what `drain()` asks for. */
|
|
63
|
+
start(): void;
|
|
64
|
+
/** Look for work now instead of at the next poll (after a write in this process made something due). */
|
|
65
|
+
kick(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Executes every due item, round after round (`produce()`, then a claim of `concurrency` items, awaited), until a
|
|
68
|
+
* round finds nothing: for tests (with a `ManualClock`), scripts, and workers a cron drives. Resolves to the
|
|
69
|
+
* number of items it executed. A claim or production that fails rejects it.
|
|
70
|
+
*/
|
|
71
|
+
drain(options?: {
|
|
72
|
+
maxRounds?: number;
|
|
73
|
+
}): Promise<number>;
|
|
74
|
+
/**
|
|
75
|
+
* Stops for good: no more claims, the claims in flight awaited, every running item's `signal` aborted, and at most
|
|
76
|
+
* `shutdownTimeout` spent waiting for them before the rest are detached. Call it from `onModuleDestroy`.
|
|
77
|
+
*/
|
|
78
|
+
shutdown(): Promise<void>;
|
|
79
|
+
private poll;
|
|
80
|
+
private idle;
|
|
81
|
+
/** Claims due items and starts executing them (before any caller sees them). */
|
|
82
|
+
private claim;
|
|
83
|
+
private launch;
|
|
84
|
+
private renew;
|
|
85
|
+
private report;
|
|
86
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { Logger } from '@nestjs/common';
|
|
4
|
+
import { systemClock } from '../time/clock.js';
|
|
5
|
+
import { parseDuration } from '../time/duration.js';
|
|
6
|
+
/**
|
|
7
|
+
* A claim-execute loop under leases, any number of processes sharing a store: it claims due items (as many as it has
|
|
8
|
+
* free slots), executes them, renews each one's lease every `heartbeatInterval` (a third of the lease by default), and
|
|
9
|
+
* aborts an item's `signal` when a renewal finds its lease gone. It looks for work every `pollInterval`, and at once
|
|
10
|
+
* after `kick()` (a local write made something due) or when an item ends. `drain()` runs due items on demand, for
|
|
11
|
+
* tests and scripts.
|
|
12
|
+
*
|
|
13
|
+
* Shutdown is graceful: call `shutdown()` from `onModuleDestroy`. It stops claiming, waits for the claims in flight
|
|
14
|
+
* (what they claim starts, told to stop at once), aborts every running item's `signal`, and waits up to
|
|
15
|
+
* `shutdownTimeout` for them; the ones still running are then detached (no more renewals) and keep their lease until
|
|
16
|
+
* it expires, as their work may still be going on.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* @Injectable()
|
|
20
|
+
* export class JobWorker implements OnApplicationBootstrap, OnModuleDestroy {
|
|
21
|
+
* private readonly worker = new LeasedWorker<Job>({
|
|
22
|
+
* claim: (lease, limit) => this.store.claim(lease, limit),
|
|
23
|
+
* renew: (job, until) => this.store.renew(job.id, job.token, until),
|
|
24
|
+
* execute: (job, run) => this.run(job, run),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* constructor(private readonly store: JobStore) {}
|
|
28
|
+
*
|
|
29
|
+
* onApplicationBootstrap() {
|
|
30
|
+
* this.worker.start();
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* onModuleDestroy() {
|
|
34
|
+
* return this.worker.shutdown();
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class LeasedWorker {
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = options;
|
|
42
|
+
this.runs = new Set();
|
|
43
|
+
/** Claims in flight: shutdown waits for them, so nothing they claim starts unobserved. */
|
|
44
|
+
this.claiming = new Set();
|
|
45
|
+
this.stopped = false;
|
|
46
|
+
this.stoppedPromise = new Promise((resolve) => (this.stop = resolve));
|
|
47
|
+
this.kicked = false;
|
|
48
|
+
/** When the loop next calls `produce()` (`performance.now()`). */
|
|
49
|
+
this.produceAt = 0;
|
|
50
|
+
const name = options.name ?? 'worker';
|
|
51
|
+
this.logger = new Logger(name);
|
|
52
|
+
this.clock = options.clock ?? systemClock;
|
|
53
|
+
this.owner = options.owner ?? `${hostname()}:${process.pid}:${randomUUID().slice(0, 8)}`;
|
|
54
|
+
this.leaseMs = parseDuration(options.leaseDuration ?? '30s');
|
|
55
|
+
this.concurrency = options.concurrency ?? 10;
|
|
56
|
+
this.pollMs = parseDuration(options.pollInterval ?? '1s');
|
|
57
|
+
this.heartbeatMs = parseDuration(options.heartbeatInterval ?? Math.floor(this.leaseMs / 3));
|
|
58
|
+
this.shutdownMs = parseDuration(options.shutdownTimeout ?? '10s');
|
|
59
|
+
this.produceMs = parseDuration(options.produceInterval ?? 0);
|
|
60
|
+
if (!Number.isInteger(this.concurrency) || this.concurrency < 1) {
|
|
61
|
+
throw new TypeError(`${name}.concurrency (${this.concurrency}) must be a positive integer.`);
|
|
62
|
+
}
|
|
63
|
+
if (this.pollMs <= 0 || this.heartbeatMs <= 0) {
|
|
64
|
+
throw new TypeError(`${name}.pollInterval and ${name}.heartbeatInterval must be positive, such as "1s".`);
|
|
65
|
+
}
|
|
66
|
+
if (this.heartbeatMs >= this.leaseMs) {
|
|
67
|
+
throw new TypeError(`${name}.heartbeatInterval (${this.heartbeatMs}ms) must be shorter than ${name}.leaseDuration (${this.leaseMs}ms).`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The items it executes now. */
|
|
71
|
+
get running() {
|
|
72
|
+
return [...this.runs].map((run) => run.item);
|
|
73
|
+
}
|
|
74
|
+
/** Starts the polling loop (once). Without it, the worker only runs what `drain()` asks for. */
|
|
75
|
+
start() {
|
|
76
|
+
if (!this.loop && !this.stopped) {
|
|
77
|
+
this.loop = this.poll();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Look for work now instead of at the next poll (after a write in this process made something due). */
|
|
81
|
+
kick() {
|
|
82
|
+
this.kicked = true;
|
|
83
|
+
this.wake?.();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Executes every due item, round after round (`produce()`, then a claim of `concurrency` items, awaited), until a
|
|
87
|
+
* round finds nothing: for tests (with a `ManualClock`), scripts, and workers a cron drives. Resolves to the
|
|
88
|
+
* number of items it executed. A claim or production that fails rejects it.
|
|
89
|
+
*/
|
|
90
|
+
async drain(options = {}) {
|
|
91
|
+
let total = 0;
|
|
92
|
+
for (let round = 0; round < (options.maxRounds ?? 1_000) && !this.stopped; round++) {
|
|
93
|
+
const produced = this.options.produce ? await this.options.produce() : 0;
|
|
94
|
+
const executions = await this.claim(this.concurrency);
|
|
95
|
+
if (executions.length === 0 && produced === 0) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
total += executions.length;
|
|
99
|
+
await Promise.race([Promise.all(executions), this.stoppedPromise]);
|
|
100
|
+
}
|
|
101
|
+
return total;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Stops for good: no more claims, the claims in flight awaited, every running item's `signal` aborted, and at most
|
|
105
|
+
* `shutdownTimeout` spent waiting for them before the rest are detached. Call it from `onModuleDestroy`.
|
|
106
|
+
*/
|
|
107
|
+
async shutdown() {
|
|
108
|
+
if (this.stopped) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.stopped = true;
|
|
112
|
+
this.stop();
|
|
113
|
+
this.wake?.();
|
|
114
|
+
// What a claim in flight claims starts now, told to stop at once, so it is handed back before the store closes.
|
|
115
|
+
await Promise.allSettled(this.claiming);
|
|
116
|
+
for (const run of this.runs) {
|
|
117
|
+
run.halt();
|
|
118
|
+
}
|
|
119
|
+
let timer;
|
|
120
|
+
await Promise.race([
|
|
121
|
+
Promise.allSettled([...this.runs].map((run) => run.done)),
|
|
122
|
+
new Promise((resolve) => (timer = setTimeout(resolve, this.shutdownMs))),
|
|
123
|
+
]);
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
for (const run of this.runs) {
|
|
126
|
+
run.detach();
|
|
127
|
+
}
|
|
128
|
+
await this.loop;
|
|
129
|
+
}
|
|
130
|
+
async poll() {
|
|
131
|
+
while (!this.stopped) {
|
|
132
|
+
if (this.options.produce && performance.now() >= this.produceAt) {
|
|
133
|
+
this.produceAt = performance.now() + this.produceMs;
|
|
134
|
+
try {
|
|
135
|
+
await this.options.produce();
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.report(error, 'produce');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const free = this.concurrency - this.runs.size;
|
|
142
|
+
if (free > 0) {
|
|
143
|
+
try {
|
|
144
|
+
const executions = await this.claim(free);
|
|
145
|
+
for (const execution of executions) {
|
|
146
|
+
void execution.finally(() => this.kick());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
this.report(error, 'claim');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
await this.idle();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
idle() {
|
|
157
|
+
if (this.kicked || this.stopped) {
|
|
158
|
+
this.kicked = false;
|
|
159
|
+
return Promise.resolve();
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const done = () => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
this.wake = undefined;
|
|
165
|
+
resolve();
|
|
166
|
+
};
|
|
167
|
+
const timer = setTimeout(done, this.pollMs);
|
|
168
|
+
this.wake = () => {
|
|
169
|
+
this.kicked = false;
|
|
170
|
+
done();
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** Claims due items and starts executing them (before any caller sees them). */
|
|
175
|
+
async claim(limit) {
|
|
176
|
+
if (limit <= 0) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
const now = this.clock.now();
|
|
180
|
+
const lease = { owner: this.owner, token: randomUUID(), now, until: now + this.leaseMs };
|
|
181
|
+
const claim = this.options.claim(lease, limit);
|
|
182
|
+
this.claiming.add(claim);
|
|
183
|
+
try {
|
|
184
|
+
const items = await claim;
|
|
185
|
+
return items.map((item) => this.launch(item));
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
this.claiming.delete(claim);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
launch(item) {
|
|
192
|
+
const run = new Run(item, () => this.renew(run));
|
|
193
|
+
this.runs.add(run);
|
|
194
|
+
if (this.stopped) {
|
|
195
|
+
run.halt();
|
|
196
|
+
}
|
|
197
|
+
run.heartbeat = setInterval(() => void run.renew(), this.heartbeatMs);
|
|
198
|
+
run.heartbeat.unref();
|
|
199
|
+
let executing;
|
|
200
|
+
try {
|
|
201
|
+
executing = this.options.execute(item, run);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
executing = Promise.reject(error);
|
|
205
|
+
}
|
|
206
|
+
run.done = executing
|
|
207
|
+
.catch((error) => this.report(error, 'execute'))
|
|
208
|
+
.finally(() => {
|
|
209
|
+
clearInterval(run.heartbeat);
|
|
210
|
+
this.runs.delete(run);
|
|
211
|
+
});
|
|
212
|
+
return run.done;
|
|
213
|
+
}
|
|
214
|
+
renew(run) {
|
|
215
|
+
return this.options.renew(run.item, this.clock.now() + this.leaseMs);
|
|
216
|
+
}
|
|
217
|
+
report(error, stage) {
|
|
218
|
+
if (this.options.onError) {
|
|
219
|
+
this.options.onError(error, stage);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
this.logger.error(`${stage === 'claim' ? 'Claiming' : stage === 'produce' ? 'Producing' : 'Executing'} failed; the worker carries on.`, error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** A claimed item's run: its signal, its heartbeat, and what became of its lease. */
|
|
227
|
+
class Run {
|
|
228
|
+
constructor(item, renewer) {
|
|
229
|
+
this.item = item;
|
|
230
|
+
this.renewer = renewer;
|
|
231
|
+
this.controller = new AbortController();
|
|
232
|
+
this.lost = false;
|
|
233
|
+
this.isDetached = false;
|
|
234
|
+
}
|
|
235
|
+
get signal() {
|
|
236
|
+
return this.controller.signal;
|
|
237
|
+
}
|
|
238
|
+
get leaseLost() {
|
|
239
|
+
return this.lost;
|
|
240
|
+
}
|
|
241
|
+
get detached() {
|
|
242
|
+
return this.isDetached;
|
|
243
|
+
}
|
|
244
|
+
async renew() {
|
|
245
|
+
if (this.lost || this.isDetached) {
|
|
246
|
+
return !this.lost;
|
|
247
|
+
}
|
|
248
|
+
let held;
|
|
249
|
+
try {
|
|
250
|
+
held = await this.renewer();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Transient: the next heartbeat tries again before the lease runs out.
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (!held) {
|
|
257
|
+
this.loseLease();
|
|
258
|
+
}
|
|
259
|
+
return held;
|
|
260
|
+
}
|
|
261
|
+
loseLease() {
|
|
262
|
+
if (this.lost) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
this.lost = true;
|
|
266
|
+
clearInterval(this.heartbeat);
|
|
267
|
+
this.controller.abort(new Error('The lease was lost: another worker may run this item now.'));
|
|
268
|
+
}
|
|
269
|
+
/** The worker shuts down: its work should stop. */
|
|
270
|
+
halt() {
|
|
271
|
+
if (!this.controller.signal.aborted) {
|
|
272
|
+
this.controller.abort(new Error('The worker is shutting down.'));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** The worker stopped waiting for it: no more renewals. */
|
|
276
|
+
detach() {
|
|
277
|
+
this.isDetached = true;
|
|
278
|
+
clearInterval(this.heartbeat);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Type } from '@nestjs/common';
|
|
2
|
+
import type { SignalOnOptions } from '../interfaces/signal-on-options.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sends `signal` whenever the application publishes `event` on the CQRS `EventBus`, through
|
|
5
|
+
* `WorkflowClient.signal()`: in the publisher's transaction when it passes `{ transaction }` as
|
|
6
|
+
* the dispatcher context, and before `publish()` resolves. Declare it on the workflow that
|
|
7
|
+
* waits for the signal. Needs `WorkflowsCqrsModule`.
|
|
8
|
+
*
|
|
9
|
+
* A signal reaches every instance waiting for it with the same key, whatever its workflow, so
|
|
10
|
+
* when several workflows (or versions) map one event to the same signal and key, it is sent once.
|
|
11
|
+
* With `id`, an event published again (a redelivery, a retried handler) stores no second signal.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* @Workflow('order-fulfilment')
|
|
15
|
+
* @SignalOn(OrderDeliveredEvent, { signal: shipmentDelivered, key: (event) => event.orderId, id: (event) => event.deliveryId })
|
|
16
|
+
* export class OrderFulfilmentWorkflow implements WorkflowRunner<Order, FulfilmentResult> {}
|
|
17
|
+
*/
|
|
18
|
+
export declare function SignalOn<E extends object, T>(event: Type<E>, options: SignalOnOptions<E, T>): ClassDecorator;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { signalName } from '../../signals/workflow.signal.js';
|
|
2
|
+
import { addEventRoute, assertEventClass, assertMapper } from '../utils/event-routes.util.js';
|
|
3
|
+
/**
|
|
4
|
+
* Sends `signal` whenever the application publishes `event` on the CQRS `EventBus`, through
|
|
5
|
+
* `WorkflowClient.signal()`: in the publisher's transaction when it passes `{ transaction }` as
|
|
6
|
+
* the dispatcher context, and before `publish()` resolves. Declare it on the workflow that
|
|
7
|
+
* waits for the signal. Needs `WorkflowsCqrsModule`.
|
|
8
|
+
*
|
|
9
|
+
* A signal reaches every instance waiting for it with the same key, whatever its workflow, so
|
|
10
|
+
* when several workflows (or versions) map one event to the same signal and key, it is sent once.
|
|
11
|
+
* With `id`, an event published again (a redelivery, a retried handler) stores no second signal.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* @Workflow('order-fulfilment')
|
|
15
|
+
* @SignalOn(OrderDeliveredEvent, { signal: shipmentDelivered, key: (event) => event.orderId, id: (event) => event.deliveryId })
|
|
16
|
+
* export class OrderFulfilmentWorkflow implements WorkflowRunner<Order, FulfilmentResult> {}
|
|
17
|
+
*/
|
|
18
|
+
export function SignalOn(event, options) {
|
|
19
|
+
assertEventClass('SignalOn', event);
|
|
20
|
+
const signal = signalName(options?.signal);
|
|
21
|
+
assertMapper('SignalOn', event, 'key', options.key);
|
|
22
|
+
assertMapper('SignalOn', event, 'id', options.id);
|
|
23
|
+
assertMapper('SignalOn', event, 'payload', options.payload);
|
|
24
|
+
return (target) => {
|
|
25
|
+
addEventRoute(target, {
|
|
26
|
+
kind: 'signal',
|
|
27
|
+
event,
|
|
28
|
+
signal,
|
|
29
|
+
key: options.key,
|
|
30
|
+
id: options.id,
|
|
31
|
+
payload: options.payload,
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Type } from '@nestjs/common';
|
|
2
|
+
import type { WorkflowRunner } from '../../interfaces/workflow-runner.interface.js';
|
|
3
|
+
import type { StartOnOptions } from '../interfaces/start-on-options.interface.js';
|
|
4
|
+
/**
|
|
5
|
+
* Starts the decorated `@Workflow()` whenever the application publishes `event` on the CQRS
|
|
6
|
+
* `EventBus`, through `WorkflowClient.start()`: in the publisher's transaction when it passes
|
|
7
|
+
* `{ transaction }` as the dispatcher context, and before `publish()` resolves. `id` makes a
|
|
8
|
+
* repeated event find the instance the first one started. Needs `WorkflowsCqrsModule`.
|
|
9
|
+
*
|
|
10
|
+
* When several versions of a workflow are registered, the highest version's `@StartOn()`
|
|
11
|
+
* decorators decide what starts it.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* @Workflow('order-fulfilment')
|
|
15
|
+
* @StartOn(OrderPlacedEvent, { id: (event) => `order-${event.order.id}`, input: (event) => event.order })
|
|
16
|
+
* export class OrderFulfilmentWorkflow implements WorkflowRunner<Order, FulfilmentResult> {}
|
|
17
|
+
*/
|
|
18
|
+
export declare function StartOn<E extends object, I = E>(event: Type<E>, options: StartOnOptions<E, I>): <W extends Type<WorkflowRunner<I, unknown>>>(target: W) => void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isPriority, MAX_PRIORITY } from '../../core/limits/limits.js';
|
|
2
|
+
import { addEventRoute, assertEventClass, assertMapper } from '../utils/event-routes.util.js';
|
|
3
|
+
/**
|
|
4
|
+
* Starts the decorated `@Workflow()` whenever the application publishes `event` on the CQRS
|
|
5
|
+
* `EventBus`, through `WorkflowClient.start()`: in the publisher's transaction when it passes
|
|
6
|
+
* `{ transaction }` as the dispatcher context, and before `publish()` resolves. `id` makes a
|
|
7
|
+
* repeated event find the instance the first one started. Needs `WorkflowsCqrsModule`.
|
|
8
|
+
*
|
|
9
|
+
* When several versions of a workflow are registered, the highest version's `@StartOn()`
|
|
10
|
+
* decorators decide what starts it.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* @Workflow('order-fulfilment')
|
|
14
|
+
* @StartOn(OrderPlacedEvent, { id: (event) => `order-${event.order.id}`, input: (event) => event.order })
|
|
15
|
+
* export class OrderFulfilmentWorkflow implements WorkflowRunner<Order, FulfilmentResult> {}
|
|
16
|
+
*/
|
|
17
|
+
export function StartOn(event, options) {
|
|
18
|
+
assertEventClass('StartOn', event);
|
|
19
|
+
assertMapper('StartOn', event, 'id', options?.id, true);
|
|
20
|
+
assertMapper('StartOn', event, 'input', options.input);
|
|
21
|
+
assertPriority(event, options.priority);
|
|
22
|
+
assertKey(event, 'concurrencyKey', options.concurrencyKey);
|
|
23
|
+
assertKey(event, 'rateLimitKey', options.rateLimitKey);
|
|
24
|
+
return (target) => {
|
|
25
|
+
addEventRoute(target, {
|
|
26
|
+
kind: 'start',
|
|
27
|
+
event,
|
|
28
|
+
id: options.id,
|
|
29
|
+
input: options.input,
|
|
30
|
+
priority: options.priority,
|
|
31
|
+
concurrencyKey: options.concurrencyKey,
|
|
32
|
+
rateLimitKey: options.rateLimitKey,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function assertPriority(event, priority) {
|
|
37
|
+
if (priority === undefined || typeof priority === 'function') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!isPriority(priority)) {
|
|
41
|
+
throw new TypeError(`@StartOn(${event.name}): invalid priority ${JSON.stringify(priority)}. Use an integer from 1 (first) to ${MAX_PRIORITY}, or a function of the event.`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function assertKey(event, option, key) {
|
|
45
|
+
if (key === undefined || typeof key === 'function' || (typeof key === 'string' && key.length > 0)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new TypeError(`@StartOn(${event.name}): \`${option}\` must be a non-empty string, or a function of the event, such as (event) => event.warehouseId.`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The `@nestjs/workflows/cqrs` entry: the only files that import `@nestjs/cqrs`.
|
|
2
|
+
// Module: wraps the EventBus publisher, so mapped events start and signal workflows
|
|
3
|
+
export { WorkflowsCqrsModule } from './workflows-cqrs.module.js';
|
|
4
|
+
// Mapping events to workflows, and the dispatcher context that carries your transaction
|
|
5
|
+
export { SignalOn, StartOn } from './decorators/index.js';
|