@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,22 @@
|
|
|
1
|
+
import type { WorkflowSignal } from '../../signals/workflow.signal.js';
|
|
2
|
+
/**
|
|
3
|
+
* How `@SignalOn()` turns an event into a `WorkflowClient.signal()` call. `payload` may be
|
|
4
|
+
* left out when the event itself has the signal's payload type.
|
|
5
|
+
*/
|
|
6
|
+
export type SignalOnOptions<E, T> = {
|
|
7
|
+
/** The signal the workflow waits for with `ctx.waitForSignal()`. */
|
|
8
|
+
signal: WorkflowSignal<T> | string;
|
|
9
|
+
/** The correlation key, such as the order id. Without one, only waits without a key match. */
|
|
10
|
+
key?: (event: E) => string;
|
|
11
|
+
/**
|
|
12
|
+
* The signal's deduplication id (`WorkflowClient.signal()`'s `id`), such as the id of the
|
|
13
|
+
* delivery the event reports: an event published again sends nothing new. Without one, the
|
|
14
|
+
* signal is sent each time the event is published, except from inside a workflow step,
|
|
15
|
+
* where the id derives from the step's `idempotencyKey`.
|
|
16
|
+
*/
|
|
17
|
+
id?: (event: E) => string;
|
|
18
|
+
} & ([E] extends [T] ? {
|
|
19
|
+
payload?: (event: E) => T;
|
|
20
|
+
} : {
|
|
21
|
+
payload: (event: E) => T;
|
|
22
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** How `@StartOn()` turns an event into a `WorkflowClient.start()` call. */
|
|
2
|
+
export interface StartOnOptions<E, I> {
|
|
3
|
+
/**
|
|
4
|
+
* The instance id, from the event's business key (`order-${event.order.id}`). An event
|
|
5
|
+
* published twice then finds the instance the first one started, instead of starting another.
|
|
6
|
+
*/
|
|
7
|
+
id: (event: E) => string;
|
|
8
|
+
/** The workflow's input. Default: the event itself, as JSON (its own enumerable fields). */
|
|
9
|
+
input?: (event: E) => I;
|
|
10
|
+
/**
|
|
11
|
+
* The instance's priority, as `WorkflowClient.start()`'s: lower is claimed first, an integer from 1 to
|
|
12
|
+
* 2,097,151. A number, or a function of the event that returns one, or `undefined` for none. Default: none.
|
|
13
|
+
*/
|
|
14
|
+
priority?: number | ((event: E) => number | undefined);
|
|
15
|
+
/**
|
|
16
|
+
* The key the workflow's per-key concurrency limit counts the instance under, instead of the one its
|
|
17
|
+
* `concurrency.key` computes from the input, as `start()`'s `concurrencyKey`: a string, or a function of the event
|
|
18
|
+
* that returns one, or `undefined` for the computed one. Needs a concurrency limit with a `key`.
|
|
19
|
+
*/
|
|
20
|
+
concurrencyKey?: string | ((event: E) => string | undefined);
|
|
21
|
+
/** The same for the workflow's per-key rate limit, as `start()`'s `rateLimitKey`. Needs a rate limit with a `key`. */
|
|
22
|
+
rateLimitKey?: string | ((event: E) => string | undefined);
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dispatcher context `@nestjs/workflows/cqrs` reads from `eventBus.publish(event, context)`,
|
|
3
|
+
* `publishAll(events, context)` and, with `@nestjs/cqrs` 12.1 or later, an aggregate's
|
|
4
|
+
* `commit(context)`: `transaction` is your ORM's transaction (Drizzle's `tx`, a TypeORM
|
|
5
|
+
* `EntityManager`, a Prisma transaction client), so the workflows the events start and the
|
|
6
|
+
* signals they send commit with your writes, or not at all.
|
|
7
|
+
*/
|
|
8
|
+
export interface WorkflowDispatcherContext {
|
|
9
|
+
transaction: unknown;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Type } from '@nestjs/common';
|
|
2
|
+
/** What `@StartOn()` and `@SignalOn()` record on a workflow class. */
|
|
3
|
+
export type WorkflowEventRoute = WorkflowStartRoute | WorkflowSignalRoute;
|
|
4
|
+
export interface WorkflowStartRoute {
|
|
5
|
+
kind: 'start';
|
|
6
|
+
event: Type<object>;
|
|
7
|
+
id: (event: object) => string;
|
|
8
|
+
input?: (event: object) => unknown;
|
|
9
|
+
priority?: number | ((event: object) => number | undefined);
|
|
10
|
+
concurrencyKey?: string | ((event: object) => string | undefined);
|
|
11
|
+
rateLimitKey?: string | ((event: object) => string | undefined);
|
|
12
|
+
}
|
|
13
|
+
export interface WorkflowSignalRoute {
|
|
14
|
+
kind: 'signal';
|
|
15
|
+
event: Type<object>;
|
|
16
|
+
signal: string;
|
|
17
|
+
key?: (event: object) => string;
|
|
18
|
+
id?: (event: object) => string;
|
|
19
|
+
payload?: (event: object) => unknown;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Type } from '@nestjs/common';
|
|
2
|
+
import type { WorkflowEventRoute } from '../interfaces/workflow-event-route.interface.js';
|
|
3
|
+
/** The class's own routes, in declaration order (decorators apply bottom-up). */
|
|
4
|
+
export declare function eventRoutesOf(target: Function): WorkflowEventRoute[];
|
|
5
|
+
export declare function addEventRoute(target: Function, route: WorkflowEventRoute): void;
|
|
6
|
+
export declare function assertEventClass(decorator: string, event: unknown): asserts event is Type<object>;
|
|
7
|
+
export declare function assertMapper(decorator: string, event: Type<object>, option: string, value: unknown, required?: boolean): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { WORKFLOW_EVENT_ROUTES_METADATA } from '../../workflows.constants.js';
|
|
2
|
+
/** The class's own routes, in declaration order (decorators apply bottom-up). */
|
|
3
|
+
export function eventRoutesOf(target) {
|
|
4
|
+
return Reflect.getOwnMetadata(WORKFLOW_EVENT_ROUTES_METADATA, target) ?? [];
|
|
5
|
+
}
|
|
6
|
+
export function addEventRoute(target, route) {
|
|
7
|
+
Reflect.defineMetadata(WORKFLOW_EVENT_ROUTES_METADATA, [route, ...eventRoutesOf(target)], target);
|
|
8
|
+
}
|
|
9
|
+
export function assertEventClass(decorator, event) {
|
|
10
|
+
if (typeof event !== 'function') {
|
|
11
|
+
throw new TypeError(`@${decorator}() takes the event class first, got ${event === null ? 'null' : typeof event}.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function assertMapper(decorator, event, option, value, required = false) {
|
|
15
|
+
if (value === undefined && !required) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value !== 'function') {
|
|
19
|
+
throw new TypeError(`@${decorator}(${event.name}): \`${option}\` must be a function of the event, such as (event) => event.orderId` +
|
|
20
|
+
`${required ? '' : ', or left out'}.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap, type OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { EventBus, UnhandledExceptionBus, type AsyncContext, type IEvent, type IEventPublisher } from '@nestjs/cqrs';
|
|
3
|
+
import { WorkflowClient } from '../services/workflow-client.service.js';
|
|
4
|
+
import { WorkflowRegistry } from '../services/workflow-registry.service.js';
|
|
5
|
+
import { WorkflowEventsExplorer } from './workflow-events.explorer.js';
|
|
6
|
+
/**
|
|
7
|
+
* The CQRS `EventBus`'s publisher while `WorkflowsCqrsModule` is imported. It wraps the
|
|
8
|
+
* publisher that was set before it (the in-memory one that feeds `@EventsHandler()`s and
|
|
9
|
+
* sagas, or the one from `CqrsModule.forRoot({ eventPublisher })`): for an event that a
|
|
10
|
+
* workflow maps with `@StartOn()` or `@SignalOn()`, it first starts and signals the workflows,
|
|
11
|
+
* through the transaction in the dispatcher context when there is one, then hands the event to
|
|
12
|
+
* the wrapped publisher. Other events go straight through, synchronously, as before.
|
|
13
|
+
*/
|
|
14
|
+
export declare class WorkflowEventPublisher implements IEventPublisher, OnModuleInit, OnApplicationBootstrap {
|
|
15
|
+
private readonly eventBus;
|
|
16
|
+
private readonly unhandledExceptionBus;
|
|
17
|
+
private readonly workflowClient;
|
|
18
|
+
private readonly explorer;
|
|
19
|
+
private readonly logger;
|
|
20
|
+
private readonly inner;
|
|
21
|
+
private routes?;
|
|
22
|
+
/** Event classes an aggregate's commit() published, warned about once each. */
|
|
23
|
+
private readonly committedByAggregates;
|
|
24
|
+
constructor(eventBus: EventBus, unhandledExceptionBus: UnhandledExceptionBus, workflowClient: WorkflowClient, explorer: WorkflowEventsExplorer, registry: WorkflowRegistry);
|
|
25
|
+
onModuleInit(): void;
|
|
26
|
+
onApplicationBootstrap(): void;
|
|
27
|
+
publish<TEvent extends IEvent>(event: TEvent, dispatcherContext?: unknown, asyncContext?: AsyncContext): unknown;
|
|
28
|
+
publishAll<TEvent extends IEvent>(events: TEvent[], dispatcherContext?: unknown, asyncContext?: AsyncContext): unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Starts and signals the workflows the events map to, then forwards the events. Nothing is
|
|
31
|
+
* forwarded when a write fails, so a retried publish reaches the handlers once.
|
|
32
|
+
*/
|
|
33
|
+
private deliver;
|
|
34
|
+
private write;
|
|
35
|
+
/**
|
|
36
|
+
* Every start and signal, computed before any is written: a mapping function that throws
|
|
37
|
+
* leaves nothing half done. Per event, starts come first, then signals, each signal once.
|
|
38
|
+
*/
|
|
39
|
+
private plan;
|
|
40
|
+
/**
|
|
41
|
+
* The one lossy path this publisher can recognize: an aggregate merged with `EventPublisher`
|
|
42
|
+
* passes itself as the dispatcher context when `commit()` is given none. (`@Publishable()`
|
|
43
|
+
* aggregates pass nothing, like any other untransacted publish, so they can't be told apart.)
|
|
44
|
+
* A warning, not an error: the start or signal still happens, only outside the transaction.
|
|
45
|
+
*/
|
|
46
|
+
private warnAboutCommit;
|
|
47
|
+
private report;
|
|
48
|
+
private isRouted;
|
|
49
|
+
private targetsOf;
|
|
50
|
+
/** Built at `onModuleInit`, or at the first publish if another module's hook publishes earlier. */
|
|
51
|
+
private table;
|
|
52
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
11
|
+
import { EventBus, UnhandledExceptionBus } from '@nestjs/cqrs';
|
|
12
|
+
import { WorkflowClient } from '../services/workflow-client.service.js';
|
|
13
|
+
import { canonical } from '../core/utils/canonical.util.js';
|
|
14
|
+
import { isPriority, MAX_PRIORITY } from '../core/limits/limits.js';
|
|
15
|
+
import { normalize } from '../utils/normalize.util.js';
|
|
16
|
+
import { ROUTE_EVENTS, WorkflowRegistry } from '../services/workflow-registry.service.js';
|
|
17
|
+
import { WorkflowEventsExplorer } from './workflow-events.explorer.js';
|
|
18
|
+
/**
|
|
19
|
+
* The CQRS `EventBus`'s publisher while `WorkflowsCqrsModule` is imported. It wraps the
|
|
20
|
+
* publisher that was set before it (the in-memory one that feeds `@EventsHandler()`s and
|
|
21
|
+
* sagas, or the one from `CqrsModule.forRoot({ eventPublisher })`): for an event that a
|
|
22
|
+
* workflow maps with `@StartOn()` or `@SignalOn()`, it first starts and signals the workflows,
|
|
23
|
+
* through the transaction in the dispatcher context when there is one, then hands the event to
|
|
24
|
+
* the wrapped publisher. Other events go straight through, synchronously, as before.
|
|
25
|
+
*/
|
|
26
|
+
let WorkflowEventPublisher = class WorkflowEventPublisher {
|
|
27
|
+
constructor(eventBus, unhandledExceptionBus, workflowClient, explorer, registry) {
|
|
28
|
+
this.eventBus = eventBus;
|
|
29
|
+
this.unhandledExceptionBus = unhandledExceptionBus;
|
|
30
|
+
this.workflowClient = workflowClient;
|
|
31
|
+
this.explorer = explorer;
|
|
32
|
+
this.logger = new Logger('WorkflowsCqrsModule');
|
|
33
|
+
/** Event classes an aggregate's commit() published, warned about once each. */
|
|
34
|
+
this.committedByAggregates = new Set();
|
|
35
|
+
// In the constructor, so no lifecycle hook can publish an event before it's installed.
|
|
36
|
+
this.inner = eventBus.publisher;
|
|
37
|
+
eventBus.publisher = this;
|
|
38
|
+
registry[ROUTE_EVENTS]();
|
|
39
|
+
}
|
|
40
|
+
onModuleInit() {
|
|
41
|
+
const routes = this.table();
|
|
42
|
+
// A process that publishes mapped events without registering their workflows starts and
|
|
43
|
+
// signals nothing, and would otherwise do so silently.
|
|
44
|
+
if (routes.size === 0) {
|
|
45
|
+
this.logger.warn('WorkflowsCqrsModule is imported, but no registered workflow maps an event with @StartOn() or @SignalOn(), so ' +
|
|
46
|
+
'events start and signal no workflow in this process. Register the workflow classes in every process that ' +
|
|
47
|
+
'publishes their events, with worker: false in WorkflowsModule.forRoot() if the process runs none.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const described = [...routes].map(([event, { starts, signals }]) => {
|
|
51
|
+
const actions = [
|
|
52
|
+
...starts.map(({ name }) => `starts ${name}`),
|
|
53
|
+
...new Set(signals.map(({ route }) => `signals ${route.signal}`)),
|
|
54
|
+
];
|
|
55
|
+
return `${event.name} ${actions.join(', ')}`;
|
|
56
|
+
});
|
|
57
|
+
this.logger.log(`Events routed to workflows: ${described.join('; ')}`);
|
|
58
|
+
}
|
|
59
|
+
onApplicationBootstrap() {
|
|
60
|
+
if (this.eventBus.publisher !== this) {
|
|
61
|
+
throw new Error(`WorkflowsCqrsModule: EventBus.publisher was replaced (by ${this.eventBus.publisher?.constructor?.name ?? 'nothing'}) ` +
|
|
62
|
+
'after WorkflowsCqrsModule installed its publisher, so events would no longer start or signal workflows. ' +
|
|
63
|
+
'Set your publisher with CqrsModule.forRoot({ eventPublisher }) instead: WorkflowsCqrsModule wraps it.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
publish(event, dispatcherContext, asyncContext) {
|
|
67
|
+
if (!this.isRouted(event)) {
|
|
68
|
+
return this.inner.publish(event, dispatcherContext, asyncContext);
|
|
69
|
+
}
|
|
70
|
+
return this.deliver([event], dispatcherContext, () => this.inner.publish(event, dispatcherContext, asyncContext));
|
|
71
|
+
}
|
|
72
|
+
publishAll(events, dispatcherContext, asyncContext) {
|
|
73
|
+
const forward = (all) => this.inner.publishAll
|
|
74
|
+
? this.inner.publishAll(all, dispatcherContext, asyncContext)
|
|
75
|
+
: all.map((event) => this.inner.publish(event, dispatcherContext, asyncContext));
|
|
76
|
+
if (!events?.some((event) => this.isRouted(event))) {
|
|
77
|
+
return forward(events ?? []);
|
|
78
|
+
}
|
|
79
|
+
// Copied before anything is awaited: an aggregate's commit() hands over its own array and
|
|
80
|
+
// empties it as soon as this returns, before the events are forwarded.
|
|
81
|
+
const all = [...events];
|
|
82
|
+
return this.deliver(all, dispatcherContext, async () => {
|
|
83
|
+
const result = forward(all);
|
|
84
|
+
return Array.isArray(result) ? Promise.all(result) : result;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Starts and signals the workflows the events map to, then forwards the events. Nothing is
|
|
89
|
+
* forwarded when a write fails, so a retried publish reaches the handlers once.
|
|
90
|
+
*/
|
|
91
|
+
deliver(events, context, forward) {
|
|
92
|
+
const failure = {};
|
|
93
|
+
const delivered = this.write(events, transactionOf(context), failure).then(forward);
|
|
94
|
+
// An aggregate's commit() without a dispatcher context: before @nestjs/cqrs 12.1 it drops the
|
|
95
|
+
// promise this returns, and from 12.1 on nothing may await it either. Report its failures the
|
|
96
|
+
// way cqrs reports a failing event handler, rather than as an unhandled rejection. (Given a
|
|
97
|
+
// context, commit() passes it instead of the aggregate, and its caller awaits the result.)
|
|
98
|
+
if (isAggregate(context)) {
|
|
99
|
+
this.warnAboutCommit(events);
|
|
100
|
+
delivered.catch((exception) => this.report(failure.event ?? events[0], exception));
|
|
101
|
+
}
|
|
102
|
+
return delivered;
|
|
103
|
+
}
|
|
104
|
+
async write(events, transaction, failure) {
|
|
105
|
+
const writes = this.plan(events);
|
|
106
|
+
if (transaction === undefined) {
|
|
107
|
+
for (const write of writes) {
|
|
108
|
+
failure.event = write.event;
|
|
109
|
+
await write.run(undefined);
|
|
110
|
+
}
|
|
111
|
+
failure.event = undefined;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Issued back to back, not awaited in between: a driver whose transactions are synchronous
|
|
115
|
+
// must see every statement before the application's callback returns, and on the
|
|
116
|
+
// transaction's one connection they still run in this order.
|
|
117
|
+
const settled = await Promise.allSettled(writes.map((write) => write.run(transaction)));
|
|
118
|
+
const index = settled.findIndex((result) => result.status === 'rejected');
|
|
119
|
+
if (index !== -1) {
|
|
120
|
+
failure.event = writes[index].event;
|
|
121
|
+
throw settled[index].reason;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every start and signal, computed before any is written: a mapping function that throws
|
|
126
|
+
* leaves nothing half done. Per event, starts come first, then signals, each signal once.
|
|
127
|
+
*/
|
|
128
|
+
plan(events) {
|
|
129
|
+
const writes = [];
|
|
130
|
+
for (const event of events) {
|
|
131
|
+
const targets = this.targetsOf(event);
|
|
132
|
+
if (!targets) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const { workflow, name, route } of targets.starts) {
|
|
136
|
+
const id = route.id(event);
|
|
137
|
+
const input = route.input ? route.input(event) : event;
|
|
138
|
+
const priority = startOption(event, name, 'priority', route.priority);
|
|
139
|
+
const concurrencyKey = startOption(event, name, 'concurrencyKey', route.concurrencyKey);
|
|
140
|
+
const rateLimitKey = startOption(event, name, 'rateLimitKey', route.rateLimitKey);
|
|
141
|
+
writes.push({
|
|
142
|
+
event,
|
|
143
|
+
run: (transaction) => this.workflowClient.start(workflow, input, { id, transaction, priority, concurrencyKey, rateLimitKey }),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// A signal reaches every instance waiting with its name and key, whatever the workflow:
|
|
147
|
+
// two declarations that agree are one signal, and two that disagree are a mistake.
|
|
148
|
+
const sent = new Map();
|
|
149
|
+
for (const { name, route } of targets.signals) {
|
|
150
|
+
const key = route.key ? route.key(event) : undefined;
|
|
151
|
+
assertString(event, name, 'key', key, 'Waits match keys exactly, so convert it (String(event.orderId)) on both sides.');
|
|
152
|
+
const id = route.id ? route.id(event) : undefined;
|
|
153
|
+
assertString(event, name, 'id', id, 'Derive it from the event, such as event.deliveryId.');
|
|
154
|
+
const payload = route.payload ? route.payload(event) : event;
|
|
155
|
+
const identity = JSON.stringify([route.signal, key ?? null]);
|
|
156
|
+
const json = canonical(normalize(payload) ?? null);
|
|
157
|
+
const earlier = sent.get(identity);
|
|
158
|
+
if (earlier) {
|
|
159
|
+
if (earlier.payload !== json || earlier.id !== id) {
|
|
160
|
+
throw new Error(`${event.constructor.name} maps to two different ${earlier.payload !== json ? 'payloads' : 'ids'} for the signal "${route.signal}"` +
|
|
161
|
+
`${key === undefined ? '' : ` (key "${key}")`}: @SignalOn() on ${earlier.name} and on ${name}. ` +
|
|
162
|
+
'A signal reaches every instance waiting for it: make the declarations agree, or keep one.');
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
sent.set(identity, { payload: json, id, name });
|
|
167
|
+
writes.push({ event, run: (transaction) => this.workflowClient.signal(route.signal, payload, { key, id, transaction }) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return writes;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The one lossy path this publisher can recognize: an aggregate merged with `EventPublisher`
|
|
174
|
+
* passes itself as the dispatcher context when `commit()` is given none. (`@Publishable()`
|
|
175
|
+
* aggregates pass nothing, like any other untransacted publish, so they can't be told apart.)
|
|
176
|
+
* A warning, not an error: the start or signal still happens, only outside the transaction.
|
|
177
|
+
*/
|
|
178
|
+
warnAboutCommit(events) {
|
|
179
|
+
for (const event of events) {
|
|
180
|
+
if (!this.isRouted(event) || this.committedByAggregates.has(event.constructor)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
this.committedByAggregates.add(event.constructor);
|
|
184
|
+
this.logger.warn(`${event.constructor.name} starts or signals workflows, and an aggregate's commit() published it without ` +
|
|
185
|
+
'a transaction: the start or signal is written outside your transaction, so it outlives a rollback of your ' +
|
|
186
|
+
"writes, and a crash before it's written loses it. Pass your transaction and await the result: " +
|
|
187
|
+
'await aggregate.commit({ transaction }) (@nestjs/cqrs 12.1 or later). With older versions, publish the ' +
|
|
188
|
+
"aggregate's events with eventBus.publishAll(aggregate.getUncommittedEvents(), { transaction }), then call " +
|
|
189
|
+
'aggregate.uncommit(). (Logged once per event class.)');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
report(event, exception) {
|
|
193
|
+
this.logger.error(`Publishing ${event.constructor.name} from an aggregate's commit() without a transaction failed. To handle ` +
|
|
194
|
+
'the failure in your command handler, and roll its transaction back, await aggregate.commit({ transaction }) ' +
|
|
195
|
+
"(@nestjs/cqrs 12.1 or later). With older versions, await eventBus.publishAll(aggregate.getUncommittedEvents(), " +
|
|
196
|
+
'{ transaction }), then call aggregate.uncommit().', exception instanceof Error ? exception.stack : String(exception));
|
|
197
|
+
this.unhandledExceptionBus.publish({ cause: event, exception });
|
|
198
|
+
}
|
|
199
|
+
isRouted(event) {
|
|
200
|
+
return this.targetsOf(event) !== undefined;
|
|
201
|
+
}
|
|
202
|
+
targetsOf(event) {
|
|
203
|
+
if (event === null || typeof event !== 'object') {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
return this.table().get(event.constructor);
|
|
207
|
+
}
|
|
208
|
+
/** Built at `onModuleInit`, or at the first publish if another module's hook publishes earlier. */
|
|
209
|
+
table() {
|
|
210
|
+
return (this.routes ??= this.explorer.explore());
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
WorkflowEventPublisher = __decorate([
|
|
214
|
+
Injectable(),
|
|
215
|
+
__metadata("design:paramtypes", [EventBus,
|
|
216
|
+
UnhandledExceptionBus,
|
|
217
|
+
WorkflowClient,
|
|
218
|
+
WorkflowEventsExplorer,
|
|
219
|
+
WorkflowRegistry])
|
|
220
|
+
], WorkflowEventPublisher);
|
|
221
|
+
export { WorkflowEventPublisher };
|
|
222
|
+
/**
|
|
223
|
+
* A `@StartOn()` option for the event: the value it was given, or what its function returns, checked here, before
|
|
224
|
+
* anything is written, rather than by `start()` once earlier writes of the same publish have landed.
|
|
225
|
+
*/
|
|
226
|
+
function startOption(event, workflow, option, given) {
|
|
227
|
+
const value = typeof given === 'function' ? given(event) : given;
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const where = `@StartOn(${event.constructor.name}) on ${workflow}: \`${option}\` returned`;
|
|
232
|
+
if (option === 'priority') {
|
|
233
|
+
if (!isPriority(value)) {
|
|
234
|
+
throw new TypeError(`${where} ${JSON.stringify(value)}. Return an integer from 1 (first) to ${MAX_PRIORITY}, or undefined for none.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (typeof value !== 'string' || value.length === 0) {
|
|
238
|
+
throw new TypeError(`${where} ${value === '' ? 'an empty string' : `${typeof value}, not a string`}. Return a non-empty string, or undefined for the computed one.`);
|
|
239
|
+
}
|
|
240
|
+
return value;
|
|
241
|
+
}
|
|
242
|
+
/** A key may be empty (it matches waits for ''), an id may not (WorkflowClient.signal() refuses it). */
|
|
243
|
+
function assertString(event, workflow, option, value, advice) {
|
|
244
|
+
if (value !== undefined && (typeof value !== 'string' || (option === 'id' && value.length === 0))) {
|
|
245
|
+
throw new TypeError(`@SignalOn(${event.constructor.name}) on ${workflow}: \`${option}\` returned ${value === '' ? 'an empty string' : `${typeof value}, not a string`}. ${advice}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** The dispatcher context's `transaction`. An aggregate (what `mergeObjectContext()` passes) never has one. */
|
|
249
|
+
function transactionOf(context) {
|
|
250
|
+
if (context === null || typeof context !== 'object' || isAggregate(context)) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
return context.transaction;
|
|
254
|
+
}
|
|
255
|
+
function isAggregate(context) {
|
|
256
|
+
return typeof context?.getUncommittedEvents === 'function';
|
|
257
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Type } from '@nestjs/common';
|
|
2
|
+
import { DiscoveryService } from '@nestjs/core';
|
|
3
|
+
import { WorkflowRegistry } from '../services/workflow-registry.service.js';
|
|
4
|
+
import type { WorkflowSignalRoute, WorkflowStartRoute } from './interfaces/workflow-event-route.interface.js';
|
|
5
|
+
/** What publishing one event class does. */
|
|
6
|
+
export interface WorkflowEventTargets {
|
|
7
|
+
/** At most one per workflow name: the highest registered version's `@StartOn()`. */
|
|
8
|
+
starts: Array<{
|
|
9
|
+
workflow: Type<unknown>;
|
|
10
|
+
name: string;
|
|
11
|
+
route: WorkflowStartRoute;
|
|
12
|
+
}>;
|
|
13
|
+
/** From every registered version: an older version's instances still wait for their signals. */
|
|
14
|
+
signals: Array<{
|
|
15
|
+
workflow: Type<unknown>;
|
|
16
|
+
name: string;
|
|
17
|
+
route: WorkflowSignalRoute;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/** Reads `@StartOn()` and `@SignalOn()` from the registered workflows. */
|
|
21
|
+
export declare class WorkflowEventsExplorer {
|
|
22
|
+
private readonly discovery;
|
|
23
|
+
private readonly registry;
|
|
24
|
+
constructor(discovery: DiscoveryService, registry: WorkflowRegistry);
|
|
25
|
+
explore(): Map<Function, WorkflowEventTargets>;
|
|
26
|
+
private rejectRoutesOutsideWorkflows;
|
|
27
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Injectable } from '@nestjs/common';
|
|
11
|
+
import { DiscoveryService } from '@nestjs/core';
|
|
12
|
+
import { WorkflowRegistry } from '../services/workflow-registry.service.js';
|
|
13
|
+
import { WORKFLOW_METADATA } from '../workflows.constants.js';
|
|
14
|
+
import { eventRoutesOf } from './utils/event-routes.util.js';
|
|
15
|
+
/** Reads `@StartOn()` and `@SignalOn()` from the registered workflows. */
|
|
16
|
+
let WorkflowEventsExplorer = class WorkflowEventsExplorer {
|
|
17
|
+
constructor(discovery, registry) {
|
|
18
|
+
this.discovery = discovery;
|
|
19
|
+
this.registry = registry;
|
|
20
|
+
}
|
|
21
|
+
explore() {
|
|
22
|
+
this.rejectRoutesOutsideWorkflows();
|
|
23
|
+
const table = new Map();
|
|
24
|
+
const targetsOf = (event) => {
|
|
25
|
+
let targets = table.get(event);
|
|
26
|
+
if (!targets) {
|
|
27
|
+
table.set(event, (targets = { starts: [], signals: [] }));
|
|
28
|
+
}
|
|
29
|
+
return targets;
|
|
30
|
+
};
|
|
31
|
+
for (const { name, version } of this.registry.versions()) {
|
|
32
|
+
const definition = this.registry.get(name, version);
|
|
33
|
+
const routes = eventRoutesOf(definition.type);
|
|
34
|
+
assertNoDuplicates(definition.type, routes);
|
|
35
|
+
// Like start(Class): an event starts the highest registered version, never an older one
|
|
36
|
+
// that is only kept registered to drain, and never twice.
|
|
37
|
+
const latest = this.registry.latest(name).version === version;
|
|
38
|
+
for (const route of routes) {
|
|
39
|
+
if (route.kind === 'signal') {
|
|
40
|
+
targetsOf(route.event).signals.push({ workflow: definition.type, name, route });
|
|
41
|
+
}
|
|
42
|
+
else if (latest) {
|
|
43
|
+
assertStartKeys(definition, route);
|
|
44
|
+
targetsOf(route.event).starts.push({ workflow: definition.type, name, route });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return table;
|
|
49
|
+
}
|
|
50
|
+
rejectRoutesOutsideWorkflows() {
|
|
51
|
+
for (const wrapper of this.discovery.getProviders()) {
|
|
52
|
+
const type = (!wrapper.metatype || wrapper.inject ? wrapper.instance?.constructor : wrapper.metatype);
|
|
53
|
+
if (type && eventRoutesOf(type).length > 0 && !Reflect.getMetadata(WORKFLOW_METADATA, type)) {
|
|
54
|
+
throw new Error(`${type.name} has @StartOn() or @SignalOn() but no @Workflow(). Events start and signal workflows: ` +
|
|
55
|
+
'put them on the @Workflow() class.');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
WorkflowEventsExplorer = __decorate([
|
|
61
|
+
Injectable(),
|
|
62
|
+
__metadata("design:paramtypes", [DiscoveryService,
|
|
63
|
+
WorkflowRegistry])
|
|
64
|
+
], WorkflowEventsExplorer);
|
|
65
|
+
export { WorkflowEventsExplorer };
|
|
66
|
+
/** A key `@StartOn()` gives an instance would count for nothing, and fail every start, without a limit per key. */
|
|
67
|
+
function assertStartKeys(definition, route) {
|
|
68
|
+
const limits = [
|
|
69
|
+
{ option: 'concurrencyKey', perKey: definition.concurrency?.perKey, what: 'concurrency limit', declare: '{ concurrency: { limit, key } }' },
|
|
70
|
+
{ option: 'rateLimitKey', perKey: definition.rateLimit?.perKey, what: 'rate limit', declare: '{ rateLimit: { max, duration, key } }' },
|
|
71
|
+
];
|
|
72
|
+
for (const { option, perKey, what, declare } of limits) {
|
|
73
|
+
if (route[option] !== undefined && perKey == null) {
|
|
74
|
+
throw new Error(`${definition.type.name} has @StartOn(${route.event.name}) with a ${option}, but workflow "${definition.name}" has no ${what} per ` +
|
|
75
|
+
`key, so the key would count for nothing. Declare one with @Workflow(name, ${declare}).`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function assertNoDuplicates(workflow, routes) {
|
|
80
|
+
// Per event class (not its name): two classes may share a name.
|
|
81
|
+
const seen = new Map();
|
|
82
|
+
for (const route of routes) {
|
|
83
|
+
const kinds = seen.get(route.event) ?? new Set();
|
|
84
|
+
const kind = route.kind === 'start' ? 'start' : `signal:${route.signal}`;
|
|
85
|
+
if (kinds.has(kind)) {
|
|
86
|
+
throw new Error(route.kind === 'start'
|
|
87
|
+
? `${workflow.name} has @StartOn(${route.event.name}) twice. An event starts one instance of a workflow: keep one.`
|
|
88
|
+
: `${workflow.name} has @SignalOn(${route.event.name}) for the signal "${route.signal}" twice: keep one.`);
|
|
89
|
+
}
|
|
90
|
+
seen.set(route.event, kinds.add(kind));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes CQRS events start and signal workflows. Import it next to `CqrsModule.forRoot()` and
|
|
3
|
+
* `WorkflowsModule.forRoot()` (both global): it wraps the `EventBus` publisher, so an event a
|
|
4
|
+
* workflow maps with `@StartOn()` or `@SignalOn()` starts or signals it before `publish()`
|
|
5
|
+
* resolves, in the transaction passed as the dispatcher context
|
|
6
|
+
* (`eventBus.publish(event, { transaction: tx })`), and then reaches the event handlers and
|
|
7
|
+
* sagas as before.
|
|
8
|
+
*/
|
|
9
|
+
export declare class WorkflowsCqrsModule {
|
|
10
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Module } from '@nestjs/common';
|
|
8
|
+
import { DiscoveryModule } from '@nestjs/core';
|
|
9
|
+
import { WorkflowEventPublisher } from './workflow-event.publisher.js';
|
|
10
|
+
import { WorkflowEventsExplorer } from './workflow-events.explorer.js';
|
|
11
|
+
/**
|
|
12
|
+
* Makes CQRS events start and signal workflows. Import it next to `CqrsModule.forRoot()` and
|
|
13
|
+
* `WorkflowsModule.forRoot()` (both global): it wraps the `EventBus` publisher, so an event a
|
|
14
|
+
* workflow maps with `@StartOn()` or `@SignalOn()` starts or signals it before `publish()`
|
|
15
|
+
* resolves, in the transaction passed as the dispatcher context
|
|
16
|
+
* (`eventBus.publish(event, { transaction: tx })`), and then reaches the event handlers and
|
|
17
|
+
* sagas as before.
|
|
18
|
+
*/
|
|
19
|
+
let WorkflowsCqrsModule = class WorkflowsCqrsModule {
|
|
20
|
+
};
|
|
21
|
+
WorkflowsCqrsModule = __decorate([
|
|
22
|
+
Module({
|
|
23
|
+
imports: [DiscoveryModule],
|
|
24
|
+
providers: [WorkflowEventsExplorer, WorkflowEventPublisher],
|
|
25
|
+
})
|
|
26
|
+
], WorkflowsCqrsModule);
|
|
27
|
+
export { WorkflowsCqrsModule };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './workflow.decorator.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './workflow.decorator.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { WorkflowDecoratorOptions } from '../interfaces/workflow-decorator-options.interface.js';
|
|
2
|
+
/**
|
|
3
|
+
* Marks an injectable class with a `run(ctx, input)` method as a durable
|
|
4
|
+
* workflow. Register it as a provider in any module; constructor injection
|
|
5
|
+
* works as usual. Workflows must be singletons (default scope).
|
|
6
|
+
*/
|
|
7
|
+
export declare function Workflow(name: string, options?: WorkflowDecoratorOptions): ClassDecorator;
|