@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,468 @@
|
|
|
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 { Inject, Injectable, Logger, } from '@nestjs/common';
|
|
14
|
+
import { systemClock } from '../core/time/clock.js';
|
|
15
|
+
import { LeasedWorker } from '../core/workers/leased-worker.js';
|
|
16
|
+
import { WorkflowNonDeterminismError } from '../errors/workflow-non-determinism.error.js';
|
|
17
|
+
import { isWorkflowInterrupt } from '../errors/workflow-interrupt.error.js';
|
|
18
|
+
import { serializeError } from '../utils/serialize-error.util.js';
|
|
19
|
+
import { resolveRetry } from '../core/retries/retry.js';
|
|
20
|
+
import { resolveJournalLimits } from '../utils/journal-limits.util.js';
|
|
21
|
+
import { normalize } from '../utils/normalize.util.js';
|
|
22
|
+
import { assertSameInstance } from '../utils/new-instance.util.js';
|
|
23
|
+
import { CHILD_ENDED_SIGNAL } from '../workflows.constants.js';
|
|
24
|
+
import { uniqueEntries, WorkflowExecution, } from './workflow-execution.service.js';
|
|
25
|
+
import { WorkflowEvents } from '../events/workflow-events.service.js';
|
|
26
|
+
import { ENGINE_STORE, WorkflowStorage } from '../storage/workflow.storage.js';
|
|
27
|
+
import { WorkflowRegistry } from './workflow-registry.service.js';
|
|
28
|
+
import { WorkflowScheduler } from './workflow-scheduler.service.js';
|
|
29
|
+
import { WORKFLOWS_MODULE_OPTIONS } from '../workflows.module-definition.js';
|
|
30
|
+
/**
|
|
31
|
+
* Claims due instances under a lease, executes them and records the outcome.
|
|
32
|
+
* Any number of processes can run a worker against the same store. Inject it
|
|
33
|
+
* for `drain()`, which runs due instances on demand (tests, scripts).
|
|
34
|
+
*/
|
|
35
|
+
let WorkflowWorker = class WorkflowWorker {
|
|
36
|
+
constructor(storage, options, registry, events, scheduler) {
|
|
37
|
+
this.storage = storage;
|
|
38
|
+
this.registry = registry;
|
|
39
|
+
this.events = events;
|
|
40
|
+
this.scheduler = scheduler;
|
|
41
|
+
this.logger = new Logger('Workflows');
|
|
42
|
+
const worker = options.worker === false ? { enabled: false } : (options.worker ?? {});
|
|
43
|
+
this.clock = options.clock ?? systemClock;
|
|
44
|
+
this.enabled = worker.enabled ?? true;
|
|
45
|
+
this.leased = new LeasedWorker({
|
|
46
|
+
name: 'worker',
|
|
47
|
+
owner: worker.id,
|
|
48
|
+
concurrency: worker.concurrency,
|
|
49
|
+
pollInterval: worker.pollInterval,
|
|
50
|
+
leaseDuration: worker.leaseDuration,
|
|
51
|
+
heartbeatInterval: worker.heartbeatInterval,
|
|
52
|
+
shutdownTimeout: worker.shutdownTimeout,
|
|
53
|
+
clock: this.clock,
|
|
54
|
+
// Schedules have second precision: their occurrences are started at most once a second, however often it polls.
|
|
55
|
+
produceInterval: '1s',
|
|
56
|
+
produce: async () => this.produced(await this.scheduler.produce(this.id, this.leased.leaseMs)),
|
|
57
|
+
claim: (lease, limit) => this.claim(lease, limit),
|
|
58
|
+
renew: (claimed, until) => this.renew(claimed, until),
|
|
59
|
+
execute: (claimed, run) => this.execute(claimed, run),
|
|
60
|
+
onError: (error, stage) => this.logger.error(FAILED[stage], error),
|
|
61
|
+
});
|
|
62
|
+
this.id = this.leased.owner;
|
|
63
|
+
this.deps = {
|
|
64
|
+
get store() {
|
|
65
|
+
return storage[ENGINE_STORE];
|
|
66
|
+
},
|
|
67
|
+
resolve: (workflow, version) => registry.resolve(workflow, version),
|
|
68
|
+
createChild: async (child) => {
|
|
69
|
+
const { instance, created } = await storage[ENGINE_STORE].create(child);
|
|
70
|
+
if (created) {
|
|
71
|
+
this.kick();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
assertSameInstance(instance, child, { input: true, parent: true });
|
|
75
|
+
}
|
|
76
|
+
return instance;
|
|
77
|
+
},
|
|
78
|
+
clock: this.clock,
|
|
79
|
+
events,
|
|
80
|
+
defaultRetry: resolveRetry(options.retry),
|
|
81
|
+
journalLimits: resolveJournalLimits(options.journal),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
onApplicationBootstrap() {
|
|
85
|
+
this.registry.keys(); // validate definitions at startup
|
|
86
|
+
if (this.enabled) {
|
|
87
|
+
this.leased.start();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Read at each use, never in the constructor: sources register while providers are created. */
|
|
91
|
+
get store() {
|
|
92
|
+
return this.storage[ENGINE_STORE];
|
|
93
|
+
}
|
|
94
|
+
async onModuleDestroy() {
|
|
95
|
+
await this.shutdown();
|
|
96
|
+
}
|
|
97
|
+
/** Stops the worker if `onModuleDestroy` didn't run (an application closed before it initialized). */
|
|
98
|
+
async onApplicationShutdown() {
|
|
99
|
+
await this.shutdown();
|
|
100
|
+
}
|
|
101
|
+
/** @internal Look for work now instead of at the next poll (after a local start or signal). */
|
|
102
|
+
kick() {
|
|
103
|
+
this.leased.kick();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* @internal After an accepted `WorkflowClient.cancel()` or `terminate()` in this process: an
|
|
107
|
+
* execution of the instance running here stops at its next `ctx` call now,
|
|
108
|
+
* instead of when its next heartbeat reads the flag.
|
|
109
|
+
*/
|
|
110
|
+
noticeCancel(id, terminate = false) {
|
|
111
|
+
for (const { exec } of this.leased.running) {
|
|
112
|
+
if (exec?.instance.id === id) {
|
|
113
|
+
exec.cancelRequested = true;
|
|
114
|
+
exec.terminateRequested ||= terminate;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.kick();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Executes every due instance, repeatedly, until none is left that its workflow's concurrency and rate
|
|
121
|
+
* limits let start. Returns the number of executions. For tests (with a `ManualWorkflowClock` and
|
|
122
|
+
* `worker: false`), scripts and cron-driven workers.
|
|
123
|
+
*/
|
|
124
|
+
drain(options = {}) {
|
|
125
|
+
return this.leased.drain(options);
|
|
126
|
+
}
|
|
127
|
+
/** After a production of the schedules' occurrences: cancels its executions of the instances it cancelled. */
|
|
128
|
+
produced(production) {
|
|
129
|
+
for (const id of production.cancelled) {
|
|
130
|
+
this.noticeCancel(id);
|
|
131
|
+
}
|
|
132
|
+
return production.started;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* @internal Called on application shutdown. Stops claiming, asks running
|
|
136
|
+
* executions to stop (their steps see `signal.aborted`), and waits up to
|
|
137
|
+
* `shutdownTimeout`. Executions that stopped are handed back at once; the
|
|
138
|
+
* rest keep their lease until it expires, because their step may still be running.
|
|
139
|
+
*/
|
|
140
|
+
shutdown() {
|
|
141
|
+
return this.leased.shutdown();
|
|
142
|
+
}
|
|
143
|
+
/** Leases due instances of the workflow versions this process runs, under their limits. */
|
|
144
|
+
async claim(lease, limit) {
|
|
145
|
+
const workflows = this.registry.versions();
|
|
146
|
+
if (workflows.length === 0) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const { instances, lastSignalId } = await this.store.claim({
|
|
150
|
+
owner: lease.owner,
|
|
151
|
+
token: lease.token,
|
|
152
|
+
now: lease.now,
|
|
153
|
+
leaseUntil: lease.until,
|
|
154
|
+
limit,
|
|
155
|
+
workflows,
|
|
156
|
+
limits: this.registry.limits(),
|
|
157
|
+
rateLimits: this.registry.rateLimits(),
|
|
158
|
+
});
|
|
159
|
+
return instances.map((instance) => ({ instance: { ...instance, leaseToken: lease.token }, signalCursor: lastSignalId }));
|
|
160
|
+
}
|
|
161
|
+
/** Extends an instance's lease, and hands its execution the cancel and terminate requests the renewal read. */
|
|
162
|
+
async renew(claimed, until) {
|
|
163
|
+
const { instance, exec } = claimed;
|
|
164
|
+
const flags = await this.store.renew(instance.id, instance.leaseToken, until);
|
|
165
|
+
if (flags && exec) {
|
|
166
|
+
exec.cancelRequested ||= flags.cancelRequested;
|
|
167
|
+
exec.terminateRequested ||= flags.terminateRequested;
|
|
168
|
+
}
|
|
169
|
+
return flags !== null;
|
|
170
|
+
}
|
|
171
|
+
async execute(claimed, run) {
|
|
172
|
+
const { instance, signalCursor } = claimed;
|
|
173
|
+
const definition = this.registry.get(instance.workflow, instance.version);
|
|
174
|
+
try {
|
|
175
|
+
const details = await this.claimed(instance, signalCursor);
|
|
176
|
+
if (!details) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const replayOnly = instance.status === 'compensating' || instance.cancelRequested;
|
|
180
|
+
const exec = new WorkflowExecution(instance, details.journal ?? [], signalCursor, this.deps, run, replayOnly);
|
|
181
|
+
claimed.exec = exec;
|
|
182
|
+
// Nothing of it runs any more, not even its compensations.
|
|
183
|
+
if (instance.terminateRequested) {
|
|
184
|
+
return await this.terminate(exec, instance);
|
|
185
|
+
}
|
|
186
|
+
this.emit(instance, instance.runs === 1 ? { type: 'workflow-started' } : { type: 'workflow-resumed', run: instance.runs });
|
|
187
|
+
const outcome = await exec.run(async () => definition.instance.run(exec.context, instance.input));
|
|
188
|
+
await this.conclude(exec, instance, outcome);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.logger.error(`Executing "${instance.id}" (${instance.workflow}@${instance.version}) failed; it is retried when its lease expires.`, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The claimed instance with its journal, or `null` when that can't be read (a codec whose key is gone wrote part of
|
|
196
|
+
* the journal, as after a key rotation that was rolled back): then it is handed back, as a claim hands back an
|
|
197
|
+
* instance whose own payloads can't be read, instead of holding its lease and its concurrency slot until the lease
|
|
198
|
+
* expires, and going first at every claim after that.
|
|
199
|
+
*/
|
|
200
|
+
async claimed(instance, signalCursor) {
|
|
201
|
+
let details;
|
|
202
|
+
try {
|
|
203
|
+
details = await this.store.get(instance.id, { journal: true });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
this.logger.error(`Instance "${instance.id}" can't be read, so it isn't run; it is claimed again later.`, error);
|
|
207
|
+
const lease = { now: this.clock.now(), leaseUntil: instance.leaseUntil, signalCursor };
|
|
208
|
+
// At worst its lease expires, as if its worker had died.
|
|
209
|
+
await this.storage[ENGINE_STORE].handBack(instance.id, instance.leaseToken, lease).catch(() => undefined);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
if (!details) {
|
|
213
|
+
throw new Error(`The store has no instance "${instance.id}", which it just leased.`);
|
|
214
|
+
}
|
|
215
|
+
return details;
|
|
216
|
+
}
|
|
217
|
+
async conclude(exec, instance, outcome) {
|
|
218
|
+
if (await this.interrupted(exec, instance, outcome.ok)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (exec.fatal) {
|
|
222
|
+
return this.finish(exec, instance, 'failed', { error: serializeError(exec.fatal) });
|
|
223
|
+
}
|
|
224
|
+
if (instance.status === 'compensating') {
|
|
225
|
+
return this.compensate(exec, instance, instance.error ?? { name: 'Error', message: 'Unknown failure.' }, true);
|
|
226
|
+
}
|
|
227
|
+
if (instance.cancelRequested) {
|
|
228
|
+
return this.compensate(exec, instance, cancelled(instance.cancelReason), false);
|
|
229
|
+
}
|
|
230
|
+
// A cancel noticed during the run (a heartbeat, or cancel() in this process) stopped it.
|
|
231
|
+
// Checked before a suspension: a parked sleep, wait or retry backoff left next to it
|
|
232
|
+
// will never resume, so the instance compensates now instead of parking first and being
|
|
233
|
+
// woken again for that. The same goes for a run timeout.
|
|
234
|
+
if (!outcome.ok && isWorkflowInterrupt(outcome.error) && outcome.error.reason === 'cancel') {
|
|
235
|
+
const current = await this.store.get(instance.id);
|
|
236
|
+
if (current?.terminateRequested) {
|
|
237
|
+
return this.finish(exec, instance, 'cancelled', { error: terminated(current.cancelReason) });
|
|
238
|
+
}
|
|
239
|
+
return this.compensate(exec, instance, cancelled(current?.cancelReason ?? null), false);
|
|
240
|
+
}
|
|
241
|
+
if (!outcome.ok && isWorkflowInterrupt(outcome.error) && outcome.error.reason === 'timeout') {
|
|
242
|
+
return this.compensate(exec, instance, timedOut(instance), false);
|
|
243
|
+
}
|
|
244
|
+
if (exec.journalLimitError) {
|
|
245
|
+
return this.compensate(exec, instance, exec.journalLimitError, false);
|
|
246
|
+
}
|
|
247
|
+
if (exec.suspension) {
|
|
248
|
+
const { waits } = exec.suspension;
|
|
249
|
+
// Parked no later than the run timeout, so a sleep or wait past it can't outlive it.
|
|
250
|
+
const wakeAt = instance.deadline === null ? exec.suspension.wakeAt : Math.min(exec.suspension.wakeAt ?? instance.deadline, instance.deadline);
|
|
251
|
+
const ok = await this.write(exec, instance, {
|
|
252
|
+
entries: exec.drainBuffer(),
|
|
253
|
+
status: 'suspended',
|
|
254
|
+
release: this.release(exec, wakeAt, waits),
|
|
255
|
+
});
|
|
256
|
+
if (ok) {
|
|
257
|
+
this.emit(instance, { type: 'workflow-suspended', wakeAt, waits });
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.leaseLostWarning(instance);
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (outcome.ok) {
|
|
265
|
+
const missing = exec.unvisited();
|
|
266
|
+
if (missing.length) {
|
|
267
|
+
const error = new WorkflowNonDeterminismError(`Instance "${instance.id}" of workflow "${instance.workflow}@${instance.version}" does not match its journal: ` +
|
|
268
|
+
`${missing.map((n) => `"${n}"`).join(', ')} ${missing.length === 1 ? 'was' : 'were'} recorded by an earlier run but never reached. ` +
|
|
269
|
+
'Ship step removals as a new workflow version.');
|
|
270
|
+
return this.finish(exec, instance, 'failed', { error: serializeError(error) });
|
|
271
|
+
}
|
|
272
|
+
let output;
|
|
273
|
+
try {
|
|
274
|
+
output = normalize(outcome.value);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
return this.compensate(exec, instance, serializeError(error), false);
|
|
278
|
+
}
|
|
279
|
+
return this.finish(exec, instance, 'completed', { output });
|
|
280
|
+
}
|
|
281
|
+
return this.compensate(exec, instance, serializeError(outcome.error), false);
|
|
282
|
+
}
|
|
283
|
+
async compensate(exec, instance, reason, alreadyCompensating) {
|
|
284
|
+
if (!alreadyCompensating) {
|
|
285
|
+
const ok = await this.write(exec, instance, { entries: exec.drainBuffer(), status: 'compensating', error: reason });
|
|
286
|
+
if (!ok) {
|
|
287
|
+
return this.leaseLostWarning(instance);
|
|
288
|
+
}
|
|
289
|
+
this.emit(instance, { type: 'workflow-compensating', error: reason });
|
|
290
|
+
}
|
|
291
|
+
// Every execution that compensates closes them again: a crash can't leave one running.
|
|
292
|
+
await this.closeChildren(exec, instance, `its parent "${instance.id}" is compensating (${reason.name}: ${reason.message})`);
|
|
293
|
+
const result = await exec.compensate(reason);
|
|
294
|
+
switch (result.state) {
|
|
295
|
+
case 'done':
|
|
296
|
+
return this.finish(exec, instance, reason.name === 'WorkflowCancelledError' ? 'cancelled' : 'failed', {
|
|
297
|
+
error: reason,
|
|
298
|
+
});
|
|
299
|
+
case 'failed':
|
|
300
|
+
return this.finish(exec, instance, 'compensation_failed', { error: { ...reason, compensation: result.error } });
|
|
301
|
+
case 'suspended': {
|
|
302
|
+
const ok = await this.write(exec, instance, {
|
|
303
|
+
entries: exec.drainBuffer(),
|
|
304
|
+
status: 'compensating',
|
|
305
|
+
release: this.release(exec, result.wakeAt, []),
|
|
306
|
+
});
|
|
307
|
+
if (ok) {
|
|
308
|
+
this.emit(instance, { type: 'workflow-suspended', wakeAt: result.wakeAt, waits: [] });
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
case 'interrupted':
|
|
313
|
+
await this.interrupted(exec, instance, false);
|
|
314
|
+
return;
|
|
315
|
+
case 'terminated':
|
|
316
|
+
return this.terminate(exec, instance);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Applies each child's `parentClose` to the children of this instance that are still running:
|
|
321
|
+
* cancels or terminates them. At least once: it runs again after a crash, and a request that
|
|
322
|
+
* was already accepted is refused, changing nothing.
|
|
323
|
+
*/
|
|
324
|
+
async closeChildren(exec, instance, reason) {
|
|
325
|
+
if (!exec.hasChildren()) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
// Every child, oldest first: the list's order doesn't change while the statuses do.
|
|
329
|
+
for (let offset = 0;; offset += CHILDREN_PAGE) {
|
|
330
|
+
// Read as stored: closing a child needs no payload, and one no codec can read any more is closed too.
|
|
331
|
+
const children = await this.storage[ENGINE_STORE].inner.list({ parentId: instance.id, limit: CHILDREN_PAGE, offset });
|
|
332
|
+
for (const child of children) {
|
|
333
|
+
if (!RUNNABLE.includes(child.status) || child.parentClose === 'abandon' || child.parentClose === null) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const terminate = child.parentClose === 'terminate';
|
|
337
|
+
const reasonText = `${terminate ? 'Terminated' : 'Cancelled'}: ${reason}.`;
|
|
338
|
+
if (await this.store.requestCancel(child.id, { reason: reasonText, now: this.clock.now(), terminate })) {
|
|
339
|
+
this.noticeCancel(child.id, terminate);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (children.length < CHILDREN_PAGE) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/** Ends the instance as `cancelled` without running (more of) its compensations. */
|
|
348
|
+
async terminate(exec, instance) {
|
|
349
|
+
const current = await this.store.get(instance.id);
|
|
350
|
+
return this.finish(exec, instance, 'cancelled', { error: terminated(current?.cancelReason ?? instance.cancelReason) });
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Handles executions that must not record an outcome. Returns true if
|
|
354
|
+
* handled. A run that finished cleanly during shutdown is still recorded;
|
|
355
|
+
* one that was cut short is handed back, and whatever it was doing is
|
|
356
|
+
* re-derived by the next execution.
|
|
357
|
+
*/
|
|
358
|
+
async interrupted(exec, instance, finished) {
|
|
359
|
+
if (exec.leaseLost) {
|
|
360
|
+
this.leaseLostWarning(instance);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (exec.storeError) {
|
|
364
|
+
this.logger.error(`Store failed while executing "${instance.id}"; it is retried when its lease expires.`, exec.storeError);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (exec.detached) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
if (exec.shuttingDown && !finished) {
|
|
371
|
+
// Handed back: due at once, for another worker (or this one, after a restart).
|
|
372
|
+
await this.write(exec, instance, {
|
|
373
|
+
entries: [...exec.drainBuffer(), ...exec.rollbacks],
|
|
374
|
+
release: this.release(exec, this.clock.now(), []),
|
|
375
|
+
});
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
async finish(exec, instance, status, result) {
|
|
381
|
+
// Before the instance ends, so a crash in between closes them again on the next execution.
|
|
382
|
+
await this.closeChildren(exec, instance, `its parent "${instance.id}" ended as ${status}`);
|
|
383
|
+
const entries = exec.drainBuffer();
|
|
384
|
+
if (status !== 'completed') {
|
|
385
|
+
entries.push(...exec.abandoned().map((entry) => ({ ...entry, updatedAt: this.clock.now() })));
|
|
386
|
+
}
|
|
387
|
+
// A child tells its parent, in the same transaction: a parent never misses its child's end.
|
|
388
|
+
const ended = { status, output: result.output, error: result.error ?? null };
|
|
389
|
+
const ok = await this.write(exec, instance, {
|
|
390
|
+
entries,
|
|
391
|
+
status,
|
|
392
|
+
output: result.output,
|
|
393
|
+
error: result.error ?? null,
|
|
394
|
+
release: this.release(exec, null, []),
|
|
395
|
+
...(instance.parentId === null
|
|
396
|
+
? {}
|
|
397
|
+
: { signal: { name: CHILD_ENDED_SIGNAL, key: instance.id, dedupeId: instance.id, payload: ended, now: this.clock.now() } }),
|
|
398
|
+
});
|
|
399
|
+
if (!ok) {
|
|
400
|
+
return this.leaseLostWarning(instance);
|
|
401
|
+
}
|
|
402
|
+
if (status === 'completed') {
|
|
403
|
+
this.emit(instance, { type: 'workflow-completed', output: result.output });
|
|
404
|
+
}
|
|
405
|
+
else if (status === 'cancelled') {
|
|
406
|
+
this.emit(instance, { type: 'workflow-cancelled', error: result.error });
|
|
407
|
+
}
|
|
408
|
+
else if (status === 'failed') {
|
|
409
|
+
this.emit(instance, { type: 'workflow-failed', error: result.error });
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this.emit(instance, { type: 'workflow-compensation-failed', error: result.error });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/** A fenced write under the execution's lease, stamped with the clock, with the custom status if it changed. */
|
|
416
|
+
async write(exec, instance, write) {
|
|
417
|
+
const change = exec.statusChange();
|
|
418
|
+
const ok = await this.store.write(instance.id, instance.leaseToken, { ...write, ...change, entries: uniqueEntries(write.entries), now: this.clock.now() });
|
|
419
|
+
if (ok) {
|
|
420
|
+
exec.statusWritten(change);
|
|
421
|
+
}
|
|
422
|
+
return ok;
|
|
423
|
+
}
|
|
424
|
+
release(exec, wakeAt, waits) {
|
|
425
|
+
return { wakeAt, waits, signalCursor: exec.signalCursor };
|
|
426
|
+
}
|
|
427
|
+
leaseLostWarning(instance) {
|
|
428
|
+
this.logger.warn(`Lost the lease on "${instance.id}" (${instance.workflow}); another worker took over. Nothing from this execution after that point was recorded.`);
|
|
429
|
+
}
|
|
430
|
+
emit(instance, body) {
|
|
431
|
+
this.events.emit({
|
|
432
|
+
id: instance.id,
|
|
433
|
+
workflow: instance.workflow,
|
|
434
|
+
version: instance.version,
|
|
435
|
+
at: this.clock.now(),
|
|
436
|
+
...body,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
WorkflowWorker = __decorate([
|
|
441
|
+
Injectable(),
|
|
442
|
+
__param(1, Inject(WORKFLOWS_MODULE_OPTIONS)),
|
|
443
|
+
__metadata("design:paramtypes", [WorkflowStorage, Object, WorkflowRegistry,
|
|
444
|
+
WorkflowEvents,
|
|
445
|
+
WorkflowScheduler])
|
|
446
|
+
], WorkflowWorker);
|
|
447
|
+
export { WorkflowWorker };
|
|
448
|
+
/** What the loop logs when a stage fails; it carries on. */
|
|
449
|
+
const FAILED = {
|
|
450
|
+
claim: 'Claiming workflow instances failed.',
|
|
451
|
+
produce: "Starting the schedules' occurrences failed.",
|
|
452
|
+
execute: 'Executing a workflow instance failed.',
|
|
453
|
+
};
|
|
454
|
+
function cancelled(reason) {
|
|
455
|
+
return { name: 'WorkflowCancelledError', message: reason ?? 'Cancelled.' };
|
|
456
|
+
}
|
|
457
|
+
const RUNNABLE = ['pending', 'running', 'suspended', 'compensating'];
|
|
458
|
+
const CHILDREN_PAGE = 500;
|
|
459
|
+
function terminated(reason) {
|
|
460
|
+
return { name: 'WorkflowTerminatedError', message: reason ?? 'Terminated.' };
|
|
461
|
+
}
|
|
462
|
+
function timedOut(instance) {
|
|
463
|
+
return {
|
|
464
|
+
name: 'WorkflowTimeoutError',
|
|
465
|
+
message: `Instance "${instance.id}" of workflow "${instance.workflow}@${instance.version}" did not finish within its timeout ` +
|
|
466
|
+
`(its deadline was ${new Date(instance.deadline).toISOString()}).`,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './workflow.signal.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './workflow.signal.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named signal with a payload type, shared by the code that sends it
|
|
3
|
+
* (`WorkflowClient.signal()`) and the workflows that wait for it
|
|
4
|
+
* (`ctx.waitForSignal()`). One constant for both ends means the name can't be
|
|
5
|
+
* misspelled on one side, and the payload is type-checked on both.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* export const shipmentDelivered = new WorkflowSignal<CarrierEvent>('shipment.delivered');
|
|
9
|
+
*/
|
|
10
|
+
export declare class WorkflowSignal<T = unknown> {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** Type-only: the payload type. Never set at runtime. */
|
|
13
|
+
readonly payload?: T;
|
|
14
|
+
constructor(name: string);
|
|
15
|
+
}
|
|
16
|
+
export declare function assertSignalName(name: unknown): asserts name is string;
|
|
17
|
+
export declare function signalName(signal: WorkflowSignal<unknown> | string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named signal with a payload type, shared by the code that sends it
|
|
3
|
+
* (`WorkflowClient.signal()`) and the workflows that wait for it
|
|
4
|
+
* (`ctx.waitForSignal()`). One constant for both ends means the name can't be
|
|
5
|
+
* misspelled on one side, and the payload is type-checked on both.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* export const shipmentDelivered = new WorkflowSignal<CarrierEvent>('shipment.delivered');
|
|
9
|
+
*/
|
|
10
|
+
export class WorkflowSignal {
|
|
11
|
+
constructor(name) {
|
|
12
|
+
this.name = name;
|
|
13
|
+
assertSignalName(name);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function assertSignalName(name) {
|
|
17
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
18
|
+
throw new TypeError(`Invalid signal name ${JSON.stringify(name)}. Use a non-empty string such as "shipment.delivered".`);
|
|
19
|
+
}
|
|
20
|
+
if (name.startsWith('$')) {
|
|
21
|
+
throw new TypeError(`Invalid signal name "${name}": names starting with "$" are reserved for the engine's own signals.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function signalName(signal) {
|
|
25
|
+
const name = typeof signal === 'string' ? signal : signal?.name;
|
|
26
|
+
assertSignalName(name);
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WorkflowClaimRequest } from '../interfaces/workflow-store.interface.js';
|
|
2
|
+
/** An instance a claim under rate limits picked and locked, in claim order. */
|
|
3
|
+
export interface PickedInstance {
|
|
4
|
+
id: string;
|
|
5
|
+
workflow: string;
|
|
6
|
+
rateLimitKey: string | null;
|
|
7
|
+
}
|
|
8
|
+
/** A rate-limit window's row: its workflow's own (`key` ''), or a rate-limit key's. */
|
|
9
|
+
export interface RateWindowRow {
|
|
10
|
+
workflow: string;
|
|
11
|
+
key: string;
|
|
12
|
+
windowEnd: number;
|
|
13
|
+
count: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The rate-limit windows of one claim (`WorkflowStore.claim()` under `rateLimits`), as the SQL stores take room in
|
|
17
|
+
* them: lock the rows of `windows` (inserted where missing, so there is always a row to lock), in that order, which is
|
|
18
|
+
* every claim's order; read them; then write back what `grant()` changed. The counts read under the locks are exact:
|
|
19
|
+
* a concurrent claim of the same window waits for this one.
|
|
20
|
+
*/
|
|
21
|
+
export declare class RateWindowClaim {
|
|
22
|
+
private readonly request;
|
|
23
|
+
private readonly picked;
|
|
24
|
+
/** The windows the picked instances take room in, each once, by workflow then key. */
|
|
25
|
+
readonly windows: ReadonlyArray<{
|
|
26
|
+
workflow: string;
|
|
27
|
+
key: string;
|
|
28
|
+
}>;
|
|
29
|
+
private readonly rules;
|
|
30
|
+
constructor(request: WorkflowClaimRequest, picked: readonly PickedInstance[]);
|
|
31
|
+
/**
|
|
32
|
+
* Of the picked instances (in claim order), those their windows have room for, and the windows whose rows change:
|
|
33
|
+
* a window that ended counts as none, and the first claim after it opens the next one, `duration` long. `locked`:
|
|
34
|
+
* the windows' rows as read under their locks.
|
|
35
|
+
*/
|
|
36
|
+
grant(locked: readonly RateWindowRow[]): {
|
|
37
|
+
granted: string[];
|
|
38
|
+
changed: RateWindowRow[];
|
|
39
|
+
};
|
|
40
|
+
private windowsOf;
|
|
41
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rate-limit windows of one claim (`WorkflowStore.claim()` under `rateLimits`), as the SQL stores take room in
|
|
3
|
+
* them: lock the rows of `windows` (inserted where missing, so there is always a row to lock), in that order, which is
|
|
4
|
+
* every claim's order; read them; then write back what `grant()` changed. The counts read under the locks are exact:
|
|
5
|
+
* a concurrent claim of the same window waits for this one.
|
|
6
|
+
*/
|
|
7
|
+
export class RateWindowClaim {
|
|
8
|
+
constructor(request, picked) {
|
|
9
|
+
this.request = request;
|
|
10
|
+
this.picked = picked;
|
|
11
|
+
this.rules = new Map((request.rateLimits ?? []).map((rule) => [rule.workflow, rule]));
|
|
12
|
+
const named = new Map(picked.flatMap((instance) => this.windowsOf(instance)).map((window) => [nameOf(window), window]));
|
|
13
|
+
this.windows = [...named.values()]
|
|
14
|
+
.map(({ workflow, key }) => ({ workflow, key }))
|
|
15
|
+
.sort((a, b) => (a.workflow < b.workflow ? -1 : a.workflow > b.workflow ? 1 : a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Of the picked instances (in claim order), those their windows have room for, and the windows whose rows change:
|
|
19
|
+
* a window that ended counts as none, and the first claim after it opens the next one, `duration` long. `locked`:
|
|
20
|
+
* the windows' rows as read under their locks.
|
|
21
|
+
*/
|
|
22
|
+
grant(locked) {
|
|
23
|
+
const { now } = this.request;
|
|
24
|
+
const open = new Map(locked.map((row) => [nameOf(row), row.windowEnd > now ? { windowEnd: row.windowEnd, count: row.count } : null]));
|
|
25
|
+
const granted = [];
|
|
26
|
+
const changed = new Map();
|
|
27
|
+
for (const instance of this.picked) {
|
|
28
|
+
const windows = this.windowsOf(instance);
|
|
29
|
+
if (!windows.every((window) => (open.get(nameOf(window))?.count ?? 0) < window.max)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
for (const window of windows) {
|
|
33
|
+
const name = nameOf(window);
|
|
34
|
+
const current = open.get(name) ?? { windowEnd: now + window.duration, count: 0 };
|
|
35
|
+
current.count++;
|
|
36
|
+
open.set(name, current);
|
|
37
|
+
changed.set(name, { workflow: window.workflow, key: window.key, ...current });
|
|
38
|
+
}
|
|
39
|
+
granted.push(instance.id);
|
|
40
|
+
}
|
|
41
|
+
return { granted, changed: [...changed.values()] };
|
|
42
|
+
}
|
|
43
|
+
windowsOf({ workflow, rateLimitKey }) {
|
|
44
|
+
const rule = this.rules.get(workflow);
|
|
45
|
+
return [
|
|
46
|
+
...(rule?.limit ? [{ workflow, key: '', ...rule.limit }] : []),
|
|
47
|
+
...(rule?.perKey && rateLimitKey !== null ? [{ workflow, key: rateLimitKey, ...rule.perKey }] : []),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function nameOf(window) {
|
|
52
|
+
return JSON.stringify([window.workflow, window.key]);
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowInstance, WorkflowStatus } from '../interfaces/workflow-instance.interface.js';
|
|
2
|
+
import type { WorkflowScheduleRecord } from '../interfaces/workflow-store.interface.js';
|
|
3
|
+
/** A row as the SQL stores read it: every column cast to text (see the kit's `columns()`). */
|
|
4
|
+
export type Row = Record<string, string | null>;
|
|
5
|
+
/** The statuses a claim takes: an instance that hasn't finished. */
|
|
6
|
+
export declare const RUNNABLE_STATUSES: readonly WorkflowStatus[];
|
|
7
|
+
/** The statuses a cancel applies to (a terminate: `RUNNABLE_STATUSES`). */
|
|
8
|
+
export declare const CANCELLABLE_STATUSES: readonly WorkflowStatus[];
|
|
9
|
+
/** An instance's columns, as `toInstance()` reads them. */
|
|
10
|
+
export declare const INSTANCE_COLUMNS: readonly string[];
|
|
11
|
+
/** A schedule's columns, as `toSchedule()` reads them. */
|
|
12
|
+
export declare const SCHEDULE_COLUMNS: readonly string[];
|
|
13
|
+
/** The columns of a signal as `WorkflowStore.signals()` returns it. */
|
|
14
|
+
export declare const SIGNAL_COLUMNS: readonly string[];
|
|
15
|
+
export declare function toInstance(row: Row): WorkflowInstance;
|
|
16
|
+
export declare function toSchedule(row: Row): WorkflowScheduleRecord;
|