@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,146 @@
|
|
|
1
|
+
import { type BeforeApplicationShutdown, type Type } from '@nestjs/common';
|
|
2
|
+
import type { WorkflowInstance, WorkflowJournalEntry } from '../interfaces/workflow-instance.interface.js';
|
|
3
|
+
import type { WorkflowInput, WorkflowOutput } from '../interfaces/workflow-runner.interface.js';
|
|
4
|
+
import type { Journaled } from '../interfaces/workflow-context.interface.js';
|
|
5
|
+
import { type WorkflowSignal } from '../signals/workflow.signal.js';
|
|
6
|
+
import { WorkflowStorage } from '../storage/workflow.storage.js';
|
|
7
|
+
import type { WorkflowInstanceDetails, WorkflowPurgeResult } from '../interfaces/workflow-store.interface.js';
|
|
8
|
+
import { WorkflowRegistry } from './workflow-registry.service.js';
|
|
9
|
+
import { WorkflowSchedules } from './workflow-schedules.service.js';
|
|
10
|
+
import { WorkflowWorker } from './workflow-worker.service.js';
|
|
11
|
+
import type { WorkflowsModuleOptions } from '../interfaces/workflows-module-options.interface.js';
|
|
12
|
+
import type { StartWorkflowOptions, SignalWorkflowOptions, WorkflowStartResult, WorkflowCancelResult, WorkflowListFilter, WorkflowDeleteOptions, WorkflowPurgeOptions, WorkflowResultOptions, WorkflowRetryInstanceOptions, WorkflowSignalSendResult } from '../interfaces/workflow-client.interface.js';
|
|
13
|
+
import { WorkflowEvents } from '../events/workflow-events.service.js';
|
|
14
|
+
/** Starts, signals, inspects and cancels workflow instances. Works with or without a local worker. */
|
|
15
|
+
export declare class WorkflowClient implements BeforeApplicationShutdown {
|
|
16
|
+
private readonly storage;
|
|
17
|
+
private readonly registry;
|
|
18
|
+
private readonly worker;
|
|
19
|
+
private readonly events;
|
|
20
|
+
/** The schedules that start instances: `upsert()`, `get()`, `list()`, `pause()`, `resume()`, `trigger()`... */
|
|
21
|
+
readonly schedules: WorkflowSchedules;
|
|
22
|
+
private readonly clock;
|
|
23
|
+
/** `result()`'s waits: settled by this process's end events, or by reading the store. */
|
|
24
|
+
private readonly results;
|
|
25
|
+
constructor(storage: WorkflowStorage, options: WorkflowsModuleOptions, registry: WorkflowRegistry, worker: WorkflowWorker, events: WorkflowEvents,
|
|
26
|
+
/** The schedules that start instances: `upsert()`, `get()`, `list()`, `pause()`, `resume()`, `trigger()`... */
|
|
27
|
+
schedules: WorkflowSchedules);
|
|
28
|
+
/** Read at each call, never in the constructor: sources register while providers are created. */
|
|
29
|
+
private get store();
|
|
30
|
+
/**
|
|
31
|
+
* Ends the waits of `result()` and `startAndWait()`. Nest closes the HTTP server after this hook, and the server
|
|
32
|
+
* waits for the requests in flight: a route still waiting would hold the shutdown up to its wait's timeout. It runs
|
|
33
|
+
* after `onModuleDestroy`, where the worker drains, so an instance that ends meanwhile answers with its result.
|
|
34
|
+
*/
|
|
35
|
+
beforeApplicationShutdown(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Creates an instance, or returns the existing one with the same id and
|
|
38
|
+
* input (`created: false`). Throws `WorkflowIdConflictError` for the same id
|
|
39
|
+
* with a different workflow or input. With `transaction`, the instance is
|
|
40
|
+
* created in your transaction and commits or rolls back with it. Inside a
|
|
41
|
+
* workflow step, a start without an `id` gets one derived from the step, so a
|
|
42
|
+
* retried step gets its instance back instead of starting another.
|
|
43
|
+
*/
|
|
44
|
+
start<W>(workflow: Type<W> | string, input: WorkflowInput<W>, options?: StartWorkflowOptions): Promise<WorkflowStartResult>;
|
|
45
|
+
/**
|
|
46
|
+
* The instance, with the signals it waits for and, on request, its journal and its children
|
|
47
|
+
* (every instance it started with `ctx.startChild()`, oldest first; `list({ parentId })` pages
|
|
48
|
+
* through them). Its own parent is `parentId`. `null` for an unknown id.
|
|
49
|
+
*/
|
|
50
|
+
getStatus(id: string, options: {
|
|
51
|
+
journal: true;
|
|
52
|
+
children?: boolean;
|
|
53
|
+
}): Promise<(WorkflowInstanceDetails & {
|
|
54
|
+
journal: WorkflowJournalEntry[];
|
|
55
|
+
}) | null>;
|
|
56
|
+
getStatus(id: string, options?: {
|
|
57
|
+
journal?: boolean;
|
|
58
|
+
children?: boolean;
|
|
59
|
+
}): Promise<WorkflowInstanceDetails | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Waits for the instance to end and resolves with its output, or rejects with a
|
|
62
|
+
* `WorkflowFailedError` (its `status` is `failed`, `cancelled` or `compensation_failed`, its
|
|
63
|
+
* `cause` the instance's error). Rejects with `WorkflowResultTimeoutError` past `timeout` (the
|
|
64
|
+
* instance keeps running), `WorkflowNotFoundError` for an unknown id or one deleted while
|
|
65
|
+
* waiting, and an `Error` as soon as the application starts shutting down. An instance run by
|
|
66
|
+
* this process's worker is seen the moment it ends, also while the worker drains at shutdown;
|
|
67
|
+
* one run elsewhere, at the next read of the store (every 25ms at first, backing off to every
|
|
68
|
+
* second).
|
|
69
|
+
*/
|
|
70
|
+
result<O = unknown>(id: string, options?: WorkflowResultOptions): Promise<Journaled<O>>;
|
|
71
|
+
/** How the stored instance ended, or `null` while it hasn't. */
|
|
72
|
+
private outcomeOf;
|
|
73
|
+
/**
|
|
74
|
+
* `start()`, then `result()`: resolves with the output of the instance (the existing one, for
|
|
75
|
+
* an id that already has one), or rejects as `result()` does. `wait` is `result()`'s options;
|
|
76
|
+
* `options.timeout` stays the instance's run timeout. Not with `{ transaction }`: the instance
|
|
77
|
+
* wouldn't exist before your transaction commits.
|
|
78
|
+
*/
|
|
79
|
+
startAndWait<W>(workflow: Type<W> | string, input: WorkflowInput<W>, options?: StartWorkflowOptions, wait?: WorkflowResultOptions): Promise<Journaled<WorkflowOutput<W>>>;
|
|
80
|
+
/** Instances by status, workflow name, version, parent and schedule, oldest first. At most `limit` (default 100). */
|
|
81
|
+
list(filter?: WorkflowListFilter): Promise<WorkflowInstance[]>;
|
|
82
|
+
/**
|
|
83
|
+
* Requests cancellation. A parked instance wakes at once. A running one lets
|
|
84
|
+
* its current step finish and stops at its next `ctx` call: at once when it
|
|
85
|
+
* runs in this process, otherwise once its worker's heartbeat reads the flag
|
|
86
|
+
* (within a third of `leaseDuration`). Then it runs its compensations and
|
|
87
|
+
* ends as `cancelled`. Compensations of steps before a `ctx.commit()` never
|
|
88
|
+
* run. An instance that completes first stays completed, and one that is
|
|
89
|
+
* already compensating is not accepted. Throws `WorkflowNotFoundError` for an
|
|
90
|
+
* unknown id.
|
|
91
|
+
*/
|
|
92
|
+
cancel(id: string, reason?: string): Promise<WorkflowCancelResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Stops an instance without running its compensations, for one that can't or mustn't finish
|
|
95
|
+
* on its own: a parked one wakes at once, a running one stops at its next `ctx` call (its
|
|
96
|
+
* current step finishes first), and a compensating one after its current compensation. It
|
|
97
|
+
* ends as `cancelled` with a `WorkflowTerminatedError` whose message is `reason`. Accepted
|
|
98
|
+
* after a `cancel()` too. Throws `WorkflowNotFoundError` for an unknown id.
|
|
99
|
+
*/
|
|
100
|
+
terminate(id: string, reason?: string): Promise<WorkflowCancelResult>;
|
|
101
|
+
private stop;
|
|
102
|
+
/**
|
|
103
|
+
* Sends a signal to the instances waiting for it with the same `key`, and
|
|
104
|
+
* wakes the suspended ones. Signals are durable: an instance that reaches
|
|
105
|
+
* `ctx.waitForSignal()` later still gets a signal sent after it started, and
|
|
106
|
+
* each signal is consumed at most once per instance. `woken` counts only the
|
|
107
|
+
* instances parked on a matching wait right now. With `transaction`, the
|
|
108
|
+
* signal and the wake-ups commit or roll back with your transaction. With
|
|
109
|
+
* `id`, or from inside a workflow step, a repeated signal is stored once.
|
|
110
|
+
*/
|
|
111
|
+
signal<T>(signal: WorkflowSignal<T> | string, payload: NoInfer<T>, options?: SignalWorkflowOptions): Promise<WorkflowSignalSendResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Retries an instance that needs a person, once its cause is fixed:
|
|
114
|
+
*
|
|
115
|
+
* - `failed`: runs again from its journal. Completed steps return their results, and each
|
|
116
|
+
* step that gave up gets its attempts back. Refused when its compensations ran: the
|
|
117
|
+
* completed steps were undone, and resuming would build on undone work.
|
|
118
|
+
* - `compensation_failed`: runs the compensations that didn't complete again, each with its
|
|
119
|
+
* attempts back, and ends as `failed` or `cancelled` as it would have.
|
|
120
|
+
*
|
|
121
|
+
* Journaled under `$retry:<n>`, and emitted as `workflow-retried`. Throws
|
|
122
|
+
* `WorkflowNotFoundError` for an unknown id, and `WorkflowStateError` for any other status or
|
|
123
|
+
* when another change to the instance races it.
|
|
124
|
+
*/
|
|
125
|
+
retry(id: string, options?: WorkflowRetryInstanceOptions): Promise<WorkflowInstance>;
|
|
126
|
+
/**
|
|
127
|
+
* Deletes an instance with its journal, and emits `workflow-deleted`. Only a finished one,
|
|
128
|
+
* unless `force`: then an unfinished one too, without compensating (to remove an instance no
|
|
129
|
+
* worker can run any more, such as one of a version you no longer deploy). A parent waiting
|
|
130
|
+
* for a child deleted that way gets a `ChildWorkflowFailedError` (`cancelled`); a deleted
|
|
131
|
+
* parent's children keep running. Throws
|
|
132
|
+
* `WorkflowNotFoundError` for an unknown id, and `WorkflowStateError` for an unfinished one
|
|
133
|
+
* without `force`.
|
|
134
|
+
*/
|
|
135
|
+
delete(id: string, options?: WorkflowDeleteOptions): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* Deletes finished instances older than `olderThan`, with their journals, the signals no
|
|
138
|
+
* unfinished instance can take any more, and the rate-limit windows that ended before it, in
|
|
139
|
+
* batches until none is left. Returns how many of each it deleted. Run it from a scheduled job;
|
|
140
|
+
* concurrent runs are safe, only wasteful.
|
|
141
|
+
*/
|
|
142
|
+
purge(options: WorkflowPurgeOptions): Promise<WorkflowPurgeResult>;
|
|
143
|
+
private emit;
|
|
144
|
+
/** The store's method for joining the application's transaction, bound; throws if it has none. */
|
|
145
|
+
private storeMethod;
|
|
146
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
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
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
15
|
+
import { ResultWaiter } from '../core/results/result-waiter.js';
|
|
16
|
+
import { systemClock } from '../core/time/clock.js';
|
|
17
|
+
import { parseDuration } from '../core/time/duration.js';
|
|
18
|
+
import { runTimeoutMs } from '../utils/run-timeout.util.js';
|
|
19
|
+
import { WorkflowIdConflictError } from '../errors/workflow-id-conflict.error.js';
|
|
20
|
+
import { WorkflowNotFoundError } from '../errors/workflow-not-found.error.js';
|
|
21
|
+
import { WorkflowFailedError } from '../errors/workflow-failed.error.js';
|
|
22
|
+
import { WorkflowResultTimeoutError } from '../errors/workflow-result-timeout.error.js';
|
|
23
|
+
import { normalize } from '../utils/normalize.util.js';
|
|
24
|
+
import { assertSameInstance, newInstance } from '../utils/new-instance.util.js';
|
|
25
|
+
import { signalName } from '../signals/workflow.signal.js';
|
|
26
|
+
import { ENGINE_STORE, WorkflowStorage } from '../storage/workflow.storage.js';
|
|
27
|
+
import { stepSignalId, stepStartId } from '../utils/step-scope.util.js';
|
|
28
|
+
import { WorkflowRegistry } from './workflow-registry.service.js';
|
|
29
|
+
import { WorkflowSchedules } from './workflow-schedules.service.js';
|
|
30
|
+
import { WorkflowWorker } from './workflow-worker.service.js';
|
|
31
|
+
import { WORKFLOWS_MODULE_OPTIONS } from '../workflows.module-definition.js';
|
|
32
|
+
import { CHILD_ENDED_SIGNAL } from '../workflows.constants.js';
|
|
33
|
+
import { WorkflowStateError } from '../errors/workflow-state.error.js';
|
|
34
|
+
import { WorkflowEvents } from '../events/workflow-events.service.js';
|
|
35
|
+
/** Starts, signals, inspects and cancels workflow instances. Works with or without a local worker. */
|
|
36
|
+
let WorkflowClient = class WorkflowClient {
|
|
37
|
+
constructor(storage, options, registry, worker, events,
|
|
38
|
+
/** The schedules that start instances: `upsert()`, `get()`, `list()`, `pause()`, `resume()`, `trigger()`... */
|
|
39
|
+
schedules) {
|
|
40
|
+
this.storage = storage;
|
|
41
|
+
this.registry = registry;
|
|
42
|
+
this.worker = worker;
|
|
43
|
+
this.events = events;
|
|
44
|
+
this.schedules = schedules;
|
|
45
|
+
this.clock = options.clock ?? systemClock;
|
|
46
|
+
this.results = new ResultWaiter({
|
|
47
|
+
read: (id) => this.outcomeOf(id),
|
|
48
|
+
timeoutError: (id, timeoutMs) => new WorkflowResultTimeoutError(id, timeoutMs),
|
|
49
|
+
closedError: (id) => new Error(`The application shut down while waiting for the result of instance "${id}".`),
|
|
50
|
+
});
|
|
51
|
+
// An end event of this process's worker carries what the worker stored: the waits need no read.
|
|
52
|
+
this.events.events$.subscribe((event) => {
|
|
53
|
+
if (isEnded(event)) {
|
|
54
|
+
this.results.settle(event.id, () => outcome(event));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Read at each call, never in the constructor: sources register while providers are created. */
|
|
59
|
+
get store() {
|
|
60
|
+
return this.storage[ENGINE_STORE];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Ends the waits of `result()` and `startAndWait()`. Nest closes the HTTP server after this hook, and the server
|
|
64
|
+
* waits for the requests in flight: a route still waiting would hold the shutdown up to its wait's timeout. It runs
|
|
65
|
+
* after `onModuleDestroy`, where the worker drains, so an instance that ends meanwhile answers with its result.
|
|
66
|
+
*/
|
|
67
|
+
async beforeApplicationShutdown() {
|
|
68
|
+
// The routes that stopped waiting answer before it resolves: the server closes only the connections idle by then,
|
|
69
|
+
// and one that answers later stays open until its keep-alive timeout (72 seconds on Fastify), holding the shutdown.
|
|
70
|
+
await this.results.close();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Creates an instance, or returns the existing one with the same id and
|
|
74
|
+
* input (`created: false`). Throws `WorkflowIdConflictError` for the same id
|
|
75
|
+
* with a different workflow or input. With `transaction`, the instance is
|
|
76
|
+
* created in your transaction and commits or rolls back with it. Inside a
|
|
77
|
+
* workflow step, a start without an `id` gets one derived from the step, so a
|
|
78
|
+
* retried step gets its instance back instead of starting another.
|
|
79
|
+
*/
|
|
80
|
+
async start(workflow, input, options = {}) {
|
|
81
|
+
const resolved = this.registry.resolve(workflow, options.version);
|
|
82
|
+
const derived = options.id === undefined ? stepStartId(resolved.name) : undefined;
|
|
83
|
+
const id = options.id ?? derived ?? randomUUID();
|
|
84
|
+
const data = newInstance(resolved, id, input, {
|
|
85
|
+
caller: 'start()',
|
|
86
|
+
now: this.clock.now(),
|
|
87
|
+
timeout: options.timeout,
|
|
88
|
+
concurrencyKey: options.concurrencyKey,
|
|
89
|
+
rateLimitKey: options.rateLimitKey,
|
|
90
|
+
priority: options.priority,
|
|
91
|
+
});
|
|
92
|
+
// Nothing is awaited before the store's call: on a driver whose transactions are
|
|
93
|
+
// synchronous, its statements must run before the application's transaction callback returns.
|
|
94
|
+
const { instance, created } = await (options.transaction === undefined
|
|
95
|
+
? this.store.create(data)
|
|
96
|
+
: this.storeMethod('createInTransaction', 'start')(options.transaction, data));
|
|
97
|
+
if (!created) {
|
|
98
|
+
// A retried step may build its input anew (a timestamp in it, say): with an id derived
|
|
99
|
+
// from the step, the first input wins.
|
|
100
|
+
assertSameInstance(instance, data, { input: derived === undefined, parent: false });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.worker.kick();
|
|
104
|
+
}
|
|
105
|
+
return { id: data.id, workflow: data.workflow, version: instance.version, created, status: instance.status };
|
|
106
|
+
}
|
|
107
|
+
async getStatus(id, options = {}) {
|
|
108
|
+
const details = await this.store.get(id, { journal: options.journal === true });
|
|
109
|
+
if (!details) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (!options.journal) {
|
|
113
|
+
delete details.journal;
|
|
114
|
+
}
|
|
115
|
+
if (options.children) {
|
|
116
|
+
details.children = [];
|
|
117
|
+
for (let offset = 0;; offset += 500) {
|
|
118
|
+
const page = await this.store.inner.list({ parentId: id, limit: 500, offset });
|
|
119
|
+
details.children.push(...(await Promise.all(page.map((child) => this.store.readableInstance(child)))));
|
|
120
|
+
if (page.length < 500) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return details;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Waits for the instance to end and resolves with its output, or rejects with a
|
|
129
|
+
* `WorkflowFailedError` (its `status` is `failed`, `cancelled` or `compensation_failed`, its
|
|
130
|
+
* `cause` the instance's error). Rejects with `WorkflowResultTimeoutError` past `timeout` (the
|
|
131
|
+
* instance keeps running), `WorkflowNotFoundError` for an unknown id or one deleted while
|
|
132
|
+
* waiting, and an `Error` as soon as the application starts shutting down. An instance run by
|
|
133
|
+
* this process's worker is seen the moment it ends, also while the worker drains at shutdown;
|
|
134
|
+
* one run elsewhere, at the next read of the store (every 25ms at first, backing off to every
|
|
135
|
+
* second).
|
|
136
|
+
*/
|
|
137
|
+
async result(id, options = {}) {
|
|
138
|
+
return (await this.results.wait(id, options));
|
|
139
|
+
}
|
|
140
|
+
/** How the stored instance ended, or `null` while it hasn't. */
|
|
141
|
+
async outcomeOf(id) {
|
|
142
|
+
const instance = await this.store.get(id);
|
|
143
|
+
if (!instance) {
|
|
144
|
+
return { error: new WorkflowNotFoundError(`No workflow instance with id "${id}".`) };
|
|
145
|
+
}
|
|
146
|
+
if (instance.status === 'completed') {
|
|
147
|
+
return { value: instance.output };
|
|
148
|
+
}
|
|
149
|
+
return FINISHED.includes(instance.status) ? { error: failure(instance) } : null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* `start()`, then `result()`: resolves with the output of the instance (the existing one, for
|
|
153
|
+
* an id that already has one), or rejects as `result()` does. `wait` is `result()`'s options;
|
|
154
|
+
* `options.timeout` stays the instance's run timeout. Not with `{ transaction }`: the instance
|
|
155
|
+
* wouldn't exist before your transaction commits.
|
|
156
|
+
*/
|
|
157
|
+
async startAndWait(workflow, input, options = {}, wait = {}) {
|
|
158
|
+
if (options.transaction !== undefined) {
|
|
159
|
+
throw new TypeError("startAndWait() can't take { transaction }: the instance only exists once your transaction commits. Call start() in it, and result() after the commit.");
|
|
160
|
+
}
|
|
161
|
+
const { id } = await this.start(workflow, input, options);
|
|
162
|
+
return this.result(id, wait);
|
|
163
|
+
}
|
|
164
|
+
/** Instances by status, workflow name, version, parent and schedule, oldest first. At most `limit` (default 100). */
|
|
165
|
+
async list(filter = {}) {
|
|
166
|
+
const limit = filter.limit ?? 100;
|
|
167
|
+
const offset = filter.offset ?? 0;
|
|
168
|
+
if (!Number.isSafeInteger(limit) || limit < 0 || !Number.isSafeInteger(offset) || offset < 0) {
|
|
169
|
+
throw new TypeError(`list(): limit (${limit}) and offset (${offset}) must be non-negative integers.`);
|
|
170
|
+
}
|
|
171
|
+
const status = filter.status === undefined ? undefined : Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
172
|
+
if (status?.length === 0 || limit === 0) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
// Read as stored, then decoded one by one: one no codec can read any more is listed without its payloads.
|
|
176
|
+
const instances = await this.store.inner.list({
|
|
177
|
+
limit,
|
|
178
|
+
offset,
|
|
179
|
+
...(status ? { status } : {}),
|
|
180
|
+
...(filter.workflow !== undefined ? { workflow: filter.workflow } : {}),
|
|
181
|
+
...(filter.version !== undefined ? { version: filter.version } : {}),
|
|
182
|
+
...(filter.parentId !== undefined ? { parentId: filter.parentId } : {}),
|
|
183
|
+
...(filter.scheduleId !== undefined ? { scheduleId: filter.scheduleId } : {}),
|
|
184
|
+
});
|
|
185
|
+
return Promise.all(instances.map((instance) => this.store.readableInstance(instance)));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Requests cancellation. A parked instance wakes at once. A running one lets
|
|
189
|
+
* its current step finish and stops at its next `ctx` call: at once when it
|
|
190
|
+
* runs in this process, otherwise once its worker's heartbeat reads the flag
|
|
191
|
+
* (within a third of `leaseDuration`). Then it runs its compensations and
|
|
192
|
+
* ends as `cancelled`. Compensations of steps before a `ctx.commit()` never
|
|
193
|
+
* run. An instance that completes first stays completed, and one that is
|
|
194
|
+
* already compensating is not accepted. Throws `WorkflowNotFoundError` for an
|
|
195
|
+
* unknown id.
|
|
196
|
+
*/
|
|
197
|
+
cancel(id, reason) {
|
|
198
|
+
return this.stop(id, reason, false);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Stops an instance without running its compensations, for one that can't or mustn't finish
|
|
202
|
+
* on its own: a parked one wakes at once, a running one stops at its next `ctx` call (its
|
|
203
|
+
* current step finishes first), and a compensating one after its current compensation. It
|
|
204
|
+
* ends as `cancelled` with a `WorkflowTerminatedError` whose message is `reason`. Accepted
|
|
205
|
+
* after a `cancel()` too. Throws `WorkflowNotFoundError` for an unknown id.
|
|
206
|
+
*/
|
|
207
|
+
terminate(id, reason) {
|
|
208
|
+
return this.stop(id, reason, true);
|
|
209
|
+
}
|
|
210
|
+
async stop(id, reason, terminate) {
|
|
211
|
+
const accepted = await this.store.requestCancel(id, { reason: reason ?? null, now: this.clock.now(), terminate });
|
|
212
|
+
// Accepted is accepted: an instance whose payloads no codec can read any more is returned as stored.
|
|
213
|
+
const details = await this.store.get(id).catch(() => this.store.inner.get(id));
|
|
214
|
+
if (!details) {
|
|
215
|
+
throw new WorkflowNotFoundError(`No workflow instance with id "${id}".`);
|
|
216
|
+
}
|
|
217
|
+
if (accepted) {
|
|
218
|
+
this.worker.noticeCancel(id, terminate);
|
|
219
|
+
}
|
|
220
|
+
const { waits: _waits, journal: _journal, ...instance } = details;
|
|
221
|
+
return { ...instance, accepted };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Sends a signal to the instances waiting for it with the same `key`, and
|
|
225
|
+
* wakes the suspended ones. Signals are durable: an instance that reaches
|
|
226
|
+
* `ctx.waitForSignal()` later still gets a signal sent after it started, and
|
|
227
|
+
* each signal is consumed at most once per instance. `woken` counts only the
|
|
228
|
+
* instances parked on a matching wait right now. With `transaction`, the
|
|
229
|
+
* signal and the wake-ups commit or roll back with your transaction. With
|
|
230
|
+
* `id`, or from inside a workflow step, a repeated signal is stored once.
|
|
231
|
+
*/
|
|
232
|
+
async signal(signal, payload, options = {}) {
|
|
233
|
+
const name = signalName(signal);
|
|
234
|
+
const key = options.key ?? null;
|
|
235
|
+
if (options.id !== undefined && (typeof options.id !== 'string' || options.id.length === 0)) {
|
|
236
|
+
throw new TypeError(`Invalid signal id ${JSON.stringify(options.id)}. Use a non-empty string, such as the id of the event that causes it.`);
|
|
237
|
+
}
|
|
238
|
+
const dedupeId = options.id ?? stepSignalId(name, key) ?? null;
|
|
239
|
+
const data = { name, key, dedupeId, payload: normalize(payload), now: this.clock.now() };
|
|
240
|
+
// Nothing is awaited before the store's call, as in start().
|
|
241
|
+
const result = await (options.transaction === undefined
|
|
242
|
+
? this.store.signal(data)
|
|
243
|
+
: this.storeMethod('signalInTransaction', 'signal')(options.transaction, data));
|
|
244
|
+
if (!result.created && result.key !== key) {
|
|
245
|
+
throw new WorkflowIdConflictError(`Signal id "${dedupeId}" of "${name}" was already used with ${describeKey(result.key)}, not ${describeKey(key)}.`);
|
|
246
|
+
}
|
|
247
|
+
if (result.woken > 0) {
|
|
248
|
+
this.worker.kick();
|
|
249
|
+
}
|
|
250
|
+
return { signalId: result.id, woken: result.woken, created: result.created };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Retries an instance that needs a person, once its cause is fixed:
|
|
254
|
+
*
|
|
255
|
+
* - `failed`: runs again from its journal. Completed steps return their results, and each
|
|
256
|
+
* step that gave up gets its attempts back. Refused when its compensations ran: the
|
|
257
|
+
* completed steps were undone, and resuming would build on undone work.
|
|
258
|
+
* - `compensation_failed`: runs the compensations that didn't complete again, each with its
|
|
259
|
+
* attempts back, and ends as `failed` or `cancelled` as it would have.
|
|
260
|
+
*
|
|
261
|
+
* Journaled under `$retry:<n>`, and emitted as `workflow-retried`. Throws
|
|
262
|
+
* `WorkflowNotFoundError` for an unknown id, and `WorkflowStateError` for any other status or
|
|
263
|
+
* when another change to the instance races it.
|
|
264
|
+
*/
|
|
265
|
+
async retry(id, options = {}) {
|
|
266
|
+
const details = await this.store.get(id, { journal: true });
|
|
267
|
+
if (!details) {
|
|
268
|
+
throw new WorkflowNotFoundError(`No workflow instance with id "${id}".`);
|
|
269
|
+
}
|
|
270
|
+
const { status, journal = [] } = details;
|
|
271
|
+
const now = this.clock.now();
|
|
272
|
+
const reset = (entry) => ({ ...entry, status: 'pending', attempts: 0, wakeAt: null, updatedAt: now });
|
|
273
|
+
let entries;
|
|
274
|
+
let error;
|
|
275
|
+
if (status === 'failed') {
|
|
276
|
+
const undone = journal.filter((entry) => entry.kind === 'compensation' && entry.status === 'completed').map((entry) => `"${entry.name}"`);
|
|
277
|
+
if (undone.length > 0) {
|
|
278
|
+
throw new WorkflowStateError(`Instance "${id}" failed and its compensations ran (${undone.join(', ')}): its completed steps were undone, so ` +
|
|
279
|
+
'resuming it would build on undone work. Start a new instance instead.');
|
|
280
|
+
}
|
|
281
|
+
entries = journal.filter((entry) => entry.kind === 'step' && entry.status === 'failed').map(reset);
|
|
282
|
+
error = null;
|
|
283
|
+
}
|
|
284
|
+
else if (status === 'compensation_failed') {
|
|
285
|
+
entries = journal.filter((entry) => entry.kind === 'compensation' && entry.status !== 'completed').map(reset);
|
|
286
|
+
const { compensation: _compensation, ...reason } = details.error ?? { name: 'Error', message: 'Unknown failure.' };
|
|
287
|
+
error = reason;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
throw new WorkflowStateError(`Instance "${id}" is ${status}: only failed and compensation_failed instances can be retried.` +
|
|
291
|
+
(FINISHED.includes(status) ? '' : ' cancel() stops one that is still running.'));
|
|
292
|
+
}
|
|
293
|
+
let deadline;
|
|
294
|
+
if (options.timeout !== undefined) {
|
|
295
|
+
deadline = options.timeout === false ? null : now + runTimeoutMs(options.timeout, 'retry()');
|
|
296
|
+
}
|
|
297
|
+
else if (status === 'failed' && details.deadline !== null && details.deadline <= now) {
|
|
298
|
+
throw new WorkflowStateError(`Instance "${id}" is past its run timeout, so it would time out again at once. Pass { timeout } with a new one, or false for none.`);
|
|
299
|
+
}
|
|
300
|
+
const retries = journal.filter((entry) => entry.kind === 'retry').length;
|
|
301
|
+
entries.push({ name: `$retry:${retries + 1}`, kind: 'retry', status: 'completed', attempts: 0, data: { from: status, error: details.error ?? null }, updatedAt: now });
|
|
302
|
+
const accepted = await this.store.reopen(id, {
|
|
303
|
+
expect: { status, runs: details.runs },
|
|
304
|
+
status: status === 'failed' ? 'pending' : 'compensating',
|
|
305
|
+
error,
|
|
306
|
+
...(deadline !== undefined ? { deadline } : {}),
|
|
307
|
+
entries,
|
|
308
|
+
now,
|
|
309
|
+
});
|
|
310
|
+
if (!accepted) {
|
|
311
|
+
throw new WorkflowStateError(`Instance "${id}" changed while it was being retried. Read it again, and retry if it still needs it.`);
|
|
312
|
+
}
|
|
313
|
+
this.emit(details, { type: 'workflow-retried', from: status, error: details.error ?? null });
|
|
314
|
+
this.worker.kick();
|
|
315
|
+
const { waits: _waits, journal: _journal, ...instance } = (await this.store.get(id));
|
|
316
|
+
return instance;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Deletes an instance with its journal, and emits `workflow-deleted`. Only a finished one,
|
|
320
|
+
* unless `force`: then an unfinished one too, without compensating (to remove an instance no
|
|
321
|
+
* worker can run any more, such as one of a version you no longer deploy). A parent waiting
|
|
322
|
+
* for a child deleted that way gets a `ChildWorkflowFailedError` (`cancelled`); a deleted
|
|
323
|
+
* parent's children keep running. Throws
|
|
324
|
+
* `WorkflowNotFoundError` for an unknown id, and `WorkflowStateError` for an unfinished one
|
|
325
|
+
* without `force`.
|
|
326
|
+
*/
|
|
327
|
+
async delete(id, options = {}) {
|
|
328
|
+
// Read as stored: deleting needs no payload, so an instance no codec can read any more can go too.
|
|
329
|
+
const details = await this.store.inner.get(id);
|
|
330
|
+
if (!details) {
|
|
331
|
+
throw new WorkflowNotFoundError(`No workflow instance with id "${id}".`);
|
|
332
|
+
}
|
|
333
|
+
const deleted = await this.store.delete(id, options.force ? ALL : FINISHED);
|
|
334
|
+
if (!deleted) {
|
|
335
|
+
const current = await this.store.inner.get(id);
|
|
336
|
+
if (!current) {
|
|
337
|
+
throw new WorkflowNotFoundError(`No workflow instance with id "${id}".`);
|
|
338
|
+
}
|
|
339
|
+
throw new WorkflowStateError(`Instance "${id}" is ${current.status}: delete() removes finished instances. cancel() it first, or pass ` +
|
|
340
|
+
'{ force: true } to delete it without running its compensations.');
|
|
341
|
+
}
|
|
342
|
+
this.emit(details, { type: 'workflow-deleted', status: details.status });
|
|
343
|
+
// A parent waiting for this child would wait forever: it ends for the parent as cancelled.
|
|
344
|
+
if (details.parentId !== null && !FINISHED.includes(details.status)) {
|
|
345
|
+
const ended = { status: 'cancelled', error: { name: 'WorkflowDeletedError', message: `Child instance "${id}" was deleted before it ended.` } };
|
|
346
|
+
await this.store.signal({ name: CHILD_ENDED_SIGNAL, key: id, dedupeId: id, payload: ended, now: this.clock.now() });
|
|
347
|
+
this.worker.kick();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Deletes finished instances older than `olderThan`, with their journals, the signals no
|
|
352
|
+
* unfinished instance can take any more, and the rate-limit windows that ended before it, in
|
|
353
|
+
* batches until none is left. Returns how many of each it deleted. Run it from a scheduled job;
|
|
354
|
+
* concurrent runs are safe, only wasteful.
|
|
355
|
+
*/
|
|
356
|
+
async purge(options) {
|
|
357
|
+
const olderThan = parseDuration(options.olderThan);
|
|
358
|
+
const statuses = options.status === undefined ? DEFAULT_PURGE : Array.isArray(options.status) ? options.status : [options.status];
|
|
359
|
+
const unfinished = statuses.filter((status) => !FINISHED.includes(status));
|
|
360
|
+
if (statuses.length === 0 || unfinished.length > 0) {
|
|
361
|
+
throw new TypeError(`purge(): status must list finished statuses (${FINISHED.join(', ')}), not ${unfinished.length ? unfinished.join(', ') : 'none'}. ` +
|
|
362
|
+
'Cancel an unfinished instance first.');
|
|
363
|
+
}
|
|
364
|
+
const limit = options.batchSize ?? 500;
|
|
365
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
366
|
+
throw new TypeError(`purge(): batchSize (${limit}) must be a positive integer.`);
|
|
367
|
+
}
|
|
368
|
+
const before = this.clock.now() - olderThan;
|
|
369
|
+
const total = { instances: 0, signals: 0, rateLimits: 0 };
|
|
370
|
+
for (;;) {
|
|
371
|
+
const batch = await this.store.purge({ statuses, before, limit });
|
|
372
|
+
total.instances += batch.instances;
|
|
373
|
+
total.signals += batch.signals;
|
|
374
|
+
total.rateLimits += batch.rateLimits;
|
|
375
|
+
if (batch.instances < limit && batch.signals < limit && batch.rateLimits < limit) {
|
|
376
|
+
return total;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
emit(instance, body) {
|
|
381
|
+
this.events.emit({ id: instance.id, workflow: instance.workflow, version: instance.version, at: this.clock.now(), ...body });
|
|
382
|
+
}
|
|
383
|
+
/** The store's method for joining the application's transaction, bound; throws if it has none. */
|
|
384
|
+
storeMethod(method, caller) {
|
|
385
|
+
const store = this.store;
|
|
386
|
+
const fn = store[method];
|
|
387
|
+
if (typeof fn !== 'function') {
|
|
388
|
+
throw new TypeError(`${caller}() with { transaction } needs a WorkflowStore on your database that implements ${method}(); ` +
|
|
389
|
+
`${this.storage.source.constructor.name} has none. See https://docs.nestjs.com/reliability/workflows#the-store-contract.`);
|
|
390
|
+
}
|
|
391
|
+
return fn.bind(store);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
WorkflowClient = __decorate([
|
|
395
|
+
Injectable(),
|
|
396
|
+
__param(1, Inject(WORKFLOWS_MODULE_OPTIONS)),
|
|
397
|
+
__metadata("design:paramtypes", [WorkflowStorage, Object, WorkflowRegistry,
|
|
398
|
+
WorkflowWorker,
|
|
399
|
+
WorkflowEvents,
|
|
400
|
+
WorkflowSchedules])
|
|
401
|
+
], WorkflowClient);
|
|
402
|
+
export { WorkflowClient };
|
|
403
|
+
const FINISHED = ['completed', 'failed', 'cancelled', 'compensation_failed'];
|
|
404
|
+
const FAILURES = {
|
|
405
|
+
'workflow-failed': 'failed',
|
|
406
|
+
'workflow-cancelled': 'cancelled',
|
|
407
|
+
'workflow-compensation-failed': 'compensation_failed',
|
|
408
|
+
};
|
|
409
|
+
function isEnded(event) {
|
|
410
|
+
return event.type === 'workflow-completed' || event.type === 'workflow-deleted' || FAILURES[event.type] !== undefined;
|
|
411
|
+
}
|
|
412
|
+
/** What `result()` settles with on an end event, which carries what the worker stored: no read needed. */
|
|
413
|
+
function outcome(event) {
|
|
414
|
+
if (event.type === 'workflow-completed') {
|
|
415
|
+
return { value: event.output };
|
|
416
|
+
}
|
|
417
|
+
if (event.type === 'workflow-deleted') {
|
|
418
|
+
return { error: new WorkflowNotFoundError(`No workflow instance with id "${event.id}".`) };
|
|
419
|
+
}
|
|
420
|
+
return { error: failure({ id: event.id, workflow: event.workflow, status: FAILURES[event.type], error: event.error }) };
|
|
421
|
+
}
|
|
422
|
+
/** What `result()` rejects with for an instance that ended without completing. */
|
|
423
|
+
function failure(instance) {
|
|
424
|
+
const status = instance.status;
|
|
425
|
+
const cause = instance.error ?? { name: 'Error', message: 'Unknown failure.' };
|
|
426
|
+
return new WorkflowFailedError(`Instance "${instance.id}" of workflow "${instance.workflow}" ${status.replace('_', ' ')}: ${cause.name}: ${cause.message}`, {
|
|
427
|
+
instanceId: instance.id,
|
|
428
|
+
status,
|
|
429
|
+
cause,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const DEFAULT_PURGE = ['completed', 'failed', 'cancelled'];
|
|
433
|
+
const ALL = ['pending', 'running', 'suspended', 'compensating', ...FINISHED];
|
|
434
|
+
function describeKey(key) {
|
|
435
|
+
return key === null ? 'no key' : `key "${key}"`;
|
|
436
|
+
}
|