@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,233 @@
|
|
|
1
|
+
import { parseDuration } from '../time/duration.js';
|
|
2
|
+
import { canonical } from '../utils/canonical.util.js';
|
|
3
|
+
import { cronOccurrences, parseCron } from './cron.util.js';
|
|
4
|
+
import { parseRRule, rruleCounts, rruleOccurrences } from './rrule.util.js';
|
|
5
|
+
import { assertTimeZone } from './time-zone.util.js';
|
|
6
|
+
const MISSED = ['skip', 'once', 'all'];
|
|
7
|
+
const OVERLAP = ['skip', 'allow', 'cancel-previous', 'buffer-one'];
|
|
8
|
+
const MAX_COUNT = 10_000;
|
|
9
|
+
const MAX_PREVIEW = 1_000;
|
|
10
|
+
/** Schedule ids, like workflow names, can't hold the `@` that separates them from the time in an occurrence's run id. */
|
|
11
|
+
const SCHEDULE_ID = /^[\w.:-]+$/;
|
|
12
|
+
/**
|
|
13
|
+
* Checks a schedule's options and returns its spec: validated, times in milliseconds, defaults applied (`tz: 'UTC'`,
|
|
14
|
+
* `missed: 'skip'`, `overlap: 'skip'`). Throws a `TypeError` that starts with `owner` for anything invalid, with the
|
|
15
|
+
* reason (`0 0 30 2 *` never matches: February has no 30th). Fields it doesn't know are left out: a package adds its
|
|
16
|
+
* own to the spec.
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const spec = parseSchedule({ cron: '0 0 8 * * MON', tz: 'Europe/Warsaw', missed: 'once' }, 'Schedule "weekly-digest"');
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function parseSchedule(options, owner = 'Schedule') {
|
|
23
|
+
if (options === null || typeof options !== 'object') {
|
|
24
|
+
throw new TypeError(`${owner}: expected an object with cron, every or rrule, got ${JSON.stringify(options)}.`);
|
|
25
|
+
}
|
|
26
|
+
const kinds = ['cron', 'every', 'rrule'].filter((kind) => options[kind] !== undefined);
|
|
27
|
+
if (kinds.length !== 1) {
|
|
28
|
+
throw new TypeError(`${owner}: give exactly one of cron, every and rrule${kinds.length ? `, not ${kinds.join(' and ')}` : ''}.`);
|
|
29
|
+
}
|
|
30
|
+
const tz = options.tz ?? 'UTC';
|
|
31
|
+
assertTimeZone(tz, owner);
|
|
32
|
+
const startAt = instant(owner, 'startAt', options.startAt);
|
|
33
|
+
const endAt = instant(owner, 'endAt', options.endAt);
|
|
34
|
+
if (startAt !== null && endAt !== null && endAt <= startAt) {
|
|
35
|
+
throw new TypeError(`${owner}: endAt (${new Date(endAt).toISOString()}) must be after startAt (${new Date(startAt).toISOString()}).`);
|
|
36
|
+
}
|
|
37
|
+
const timing = {};
|
|
38
|
+
if (options.cron !== undefined) {
|
|
39
|
+
if (typeof options.cron !== 'string') {
|
|
40
|
+
throw new TypeError(`${owner}: cron must be a string, such as '0 8 * * MON'.`);
|
|
41
|
+
}
|
|
42
|
+
compiledCron(options.cron);
|
|
43
|
+
timing.cron = options.cron;
|
|
44
|
+
}
|
|
45
|
+
else if (options.every !== undefined) {
|
|
46
|
+
let every;
|
|
47
|
+
try {
|
|
48
|
+
every = parseDuration(options.every);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new TypeError(`${owner}: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
if (every < 1_000) {
|
|
54
|
+
throw new TypeError(`${owner}: every (${JSON.stringify(options.every)}) must be at least a second.`);
|
|
55
|
+
}
|
|
56
|
+
if (options.tz !== undefined) {
|
|
57
|
+
throw new TypeError(`${owner}: every repeats a fixed interval from startAt (default: the Unix epoch), whatever the time zone; tz doesn't apply. ` +
|
|
58
|
+
"For a time of day in a time zone, use cron: '0 0 * * *' with tz.");
|
|
59
|
+
}
|
|
60
|
+
timing.every = every;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (typeof options.rrule !== 'string') {
|
|
64
|
+
throw new TypeError(`${owner}: rrule must be a string, such as 'FREQ=WEEKLY;BYDAY=MO;BYHOUR=8'.`);
|
|
65
|
+
}
|
|
66
|
+
const rule = compiledRRule(options.rrule);
|
|
67
|
+
if (rruleCounts(rule) && startAt === null) {
|
|
68
|
+
throw new TypeError(`${owner}: an rrule with COUNT counts from startAt: give one (or use limit).`);
|
|
69
|
+
}
|
|
70
|
+
// COUNT is counted from DTSTART at every computation of the next occurrence: `limit` counts as it goes.
|
|
71
|
+
if ((rule.count ?? 0) > MAX_COUNT) {
|
|
72
|
+
throw new TypeError(`${owner}: an rrule's COUNT (${rule.count}) can be at most ${MAX_COUNT}. For more, use limit.`);
|
|
73
|
+
}
|
|
74
|
+
timing.rrule = options.rrule;
|
|
75
|
+
}
|
|
76
|
+
const limit = options.limit ?? null;
|
|
77
|
+
if (limit !== null && (!Number.isSafeInteger(limit) || limit < 1)) {
|
|
78
|
+
throw new TypeError(`${owner}: invalid limit ${JSON.stringify(limit)}. Use a positive integer.`);
|
|
79
|
+
}
|
|
80
|
+
const missed = options.missed ?? 'skip';
|
|
81
|
+
if (!MISSED.includes(missed)) {
|
|
82
|
+
throw new TypeError(`${owner}: invalid missed ${JSON.stringify(missed)}. Use 'skip', 'once' or 'all'.`);
|
|
83
|
+
}
|
|
84
|
+
const overlap = options.overlap ?? 'skip';
|
|
85
|
+
if (!OVERLAP.includes(overlap)) {
|
|
86
|
+
throw new TypeError(`${owner}: invalid overlap ${JSON.stringify(overlap)}. Use 'skip', 'allow', 'cancel-previous' or 'buffer-one'.`);
|
|
87
|
+
}
|
|
88
|
+
if (missed === 'all' && overlap !== 'allow') {
|
|
89
|
+
throw new TypeError(`${owner}: missed: 'all' starts every missed occurrence at once, so it needs overlap: 'allow'. ` + "For one catch-up run, use missed: 'once'.");
|
|
90
|
+
}
|
|
91
|
+
return { ...timing, tz, startAt, endAt, limit, missed, overlap };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The times (epoch milliseconds, ascending) of a schedule's next occurrences strictly after `from` (default: now),
|
|
95
|
+
* at most `count` (default 10, at most 1,000) and at most what its `limit` leaves after `runs` occurrences: a
|
|
96
|
+
* schedule's preview. No I/O. Throws a `TypeError` that starts with `owner` for an invalid `count` or `from`.
|
|
97
|
+
*
|
|
98
|
+
* ```ts
|
|
99
|
+
* nextOccurrences(parseSchedule({ cron: '0 9 * * MON-FRI', tz: 'America/New_York' }), { from: Date.UTC(2026, 0, 1), count: 2 });
|
|
100
|
+
* // [Date.UTC(2026, 0, 1, 14), Date.UTC(2026, 0, 2, 14)]: 09:00 EST on Thursday and Friday
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function nextOccurrences(spec, options = {}, owner = 'nextOccurrences()') {
|
|
104
|
+
const count = options.count ?? 10;
|
|
105
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > MAX_PREVIEW) {
|
|
106
|
+
throw new TypeError(`${owner}: count (${count}) must be an integer from 1 to ${MAX_PREVIEW}.`);
|
|
107
|
+
}
|
|
108
|
+
const from = options.from === undefined ? Date.now() : options.from instanceof Date ? options.from.getTime() : options.from;
|
|
109
|
+
if (typeof from !== 'number' || !Number.isFinite(from)) {
|
|
110
|
+
throw new TypeError(`${owner}: invalid from ${String(options.from)}. Pass a valid Date or a timestamp in milliseconds.`);
|
|
111
|
+
}
|
|
112
|
+
const left = spec.limit === null ? Infinity : spec.limit - (options.runs ?? 0);
|
|
113
|
+
const times = [];
|
|
114
|
+
for (const at of occurrences(spec, from)) {
|
|
115
|
+
if (times.length >= Math.min(count, left)) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
times.push(at);
|
|
119
|
+
}
|
|
120
|
+
return times;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The id of the run an occurrence starts: `<schedule id>@<ISO time of the occurrence>`, such as
|
|
124
|
+
* `weekly-digest@2026-01-05T07:00:00.000Z`, the same wherever and however often it is started, so a second start of
|
|
125
|
+
* an occurrence (by a worker that took over) finds the first.
|
|
126
|
+
*
|
|
127
|
+
* ```ts
|
|
128
|
+
* occurrenceId('weekly-digest', Date.UTC(2026, 0, 5, 7)); // 'weekly-digest@2026-01-05T07:00:00.000Z'
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function occurrenceId(schedule, at) {
|
|
132
|
+
return `${schedule}@${new Date(at).toISOString()}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Throws a `TypeError` unless `id` can be a schedule's id: letters, digits, `.`, `:`, `_` and `-` (never the `@` of
|
|
136
|
+
* an occurrence's run id). `owner`, when given, is named in the message.
|
|
137
|
+
*
|
|
138
|
+
* ```ts
|
|
139
|
+
* assertScheduleId(id, 'queue "emails"'); // Invalid schedule id "weekly digest" of queue "emails". ...
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export function assertScheduleId(id, owner) {
|
|
143
|
+
if (typeof id !== 'string' || !SCHEDULE_ID.test(id)) {
|
|
144
|
+
throw new TypeError(`Invalid schedule id ${JSON.stringify(id)}${owner === undefined ? '' : ` of ${owner}`}. Use letters, digits, ".", ":", "_" or "-".`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Whether two specs have the same occurrences (their policies aside). */
|
|
148
|
+
export function sameTiming(a, b) {
|
|
149
|
+
const timing = ({ cron, every, rrule, tz, startAt, endAt }) => canonical({ cron, every, rrule, tz, startAt, endAt });
|
|
150
|
+
return timing(a) === timing(b);
|
|
151
|
+
}
|
|
152
|
+
/** The spec's occurrences strictly after `after`, ascending, within `startAt` and `endAt` (not counting `limit`). */
|
|
153
|
+
export function* occurrences(spec, after) {
|
|
154
|
+
const from = spec.startAt === null ? after : Math.max(after, spec.startAt - 1);
|
|
155
|
+
let times;
|
|
156
|
+
if (spec.cron !== undefined) {
|
|
157
|
+
times = cronOccurrences(compiledCron(spec.cron), from, spec.tz);
|
|
158
|
+
}
|
|
159
|
+
else if (spec.rrule !== undefined) {
|
|
160
|
+
times = rruleOccurrences(compiledRRule(spec.rrule), spec.startAt, spec.tz, from);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
times = everyOccurrences(spec.every, spec.startAt ?? 0, from);
|
|
164
|
+
}
|
|
165
|
+
for (const at of times) {
|
|
166
|
+
if (spec.endAt !== null && at > spec.endAt) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
yield at;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** The first occurrence strictly after `after`, or `null`. */
|
|
173
|
+
export function nextOccurrence(spec, after) {
|
|
174
|
+
for (const at of occurrences(spec, after)) {
|
|
175
|
+
return at;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The latest `count` occurrences in `[from, to]`, ascending. Looks back from `to` in growing windows, so a
|
|
181
|
+
* schedule that fell far behind costs as much as the occurrences it returns, not all it missed.
|
|
182
|
+
*/
|
|
183
|
+
export function latestOccurrences(spec, from, to, count) {
|
|
184
|
+
for (let window = 60_000;; window *= 16) {
|
|
185
|
+
const start = Math.max(from, to - window);
|
|
186
|
+
const found = [];
|
|
187
|
+
for (const at of occurrences(spec, start - 1)) {
|
|
188
|
+
if (at > to) {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
found.push(at);
|
|
192
|
+
if (found.length > count) {
|
|
193
|
+
found.shift();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (found.length === count || start === from) {
|
|
197
|
+
return found;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function* everyOccurrences(every, anchor, after) {
|
|
202
|
+
let at = after < anchor ? anchor : anchor + (Math.floor((after - anchor) / every) + 1) * every;
|
|
203
|
+
for (;;) {
|
|
204
|
+
yield at;
|
|
205
|
+
at += every;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function instant(owner, name, value) {
|
|
209
|
+
if (value === undefined) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const ms = value instanceof Date ? value.getTime() : value;
|
|
213
|
+
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
|
|
214
|
+
throw new TypeError(`${owner}: invalid ${name} ${String(value)}. Pass a valid Date or a timestamp in milliseconds.`);
|
|
215
|
+
}
|
|
216
|
+
return ms;
|
|
217
|
+
}
|
|
218
|
+
const crons = new Map();
|
|
219
|
+
const rrules = new Map();
|
|
220
|
+
function compiledCron(expression) {
|
|
221
|
+
let cron = crons.get(expression);
|
|
222
|
+
if (!cron) {
|
|
223
|
+
crons.set(expression, (cron = parseCron(expression)));
|
|
224
|
+
}
|
|
225
|
+
return cron;
|
|
226
|
+
}
|
|
227
|
+
function compiledRRule(rule) {
|
|
228
|
+
let compiled = rrules.get(rule);
|
|
229
|
+
if (!compiled) {
|
|
230
|
+
rrules.set(rule, (compiled = parseRRule(rule)));
|
|
231
|
+
}
|
|
232
|
+
return compiled;
|
|
233
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ScheduleSpec } from '../interfaces/schedule.interface.js';
|
|
2
|
+
import type { ScheduleRecord, ScheduleSave, ScheduleStore } from '../interfaces/schedule-store.interface.js';
|
|
3
|
+
import type { ScheduleProduction, SchedulerOptions } from '../interfaces/scheduler-options.interface.js';
|
|
4
|
+
/**
|
|
5
|
+
* A schedule's fields, as `Scheduler.save()`'s change returns them: what `ScheduleStore.saveSchedule()` takes, without
|
|
6
|
+
* the condition.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* await scheduler.save(id, (current): ScheduleFields | null =>
|
|
10
|
+
* current?.declared ? null : scheduler.changed(current, { target: 'emails', declared: false, spec, payload }),
|
|
11
|
+
* );
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export type ScheduleFields = Omit<ScheduleSave, 'id' | 'expectRevision' | 'now'>;
|
|
15
|
+
/**
|
|
16
|
+
* The engine of schedules, over a `ScheduleStore`: it starts each occurrence's run exactly once however many workers
|
|
17
|
+
* produce them, and keeps the schedules the code declares in step with it, also through rolling deploys. A package
|
|
18
|
+
* plugs in its runs (`fire`, `running`, `cancel`) and calls `produce()` from its worker's loop.
|
|
19
|
+
*
|
|
20
|
+
* - **One run per occurrence.** A production leases the due schedules (`claimSchedules`), records its decisions in
|
|
21
|
+
* each (the starts it will make, the runs to cancel) before making them, then clears them and hands the schedule
|
|
22
|
+
* back. A worker that dies or loses the lease in between leaves them to the next, which makes them again: a run's
|
|
23
|
+
* id, `occurrenceId(schedule, at)`, makes the second start a no-op. `runs`, what `limit` counts, is exact.
|
|
24
|
+
* - **missed** and **overlap** as `ScheduleOptions` say; a start the store refuses is retried, backing off from a
|
|
25
|
+
* second to a minute, ten times in all, while the schedule's other starts go ahead.
|
|
26
|
+
* - **Declared schedules follow the code.** At startup (`synced()`) and every minute (from `produce()`), a process
|
|
27
|
+
* saves the schedules its code declares, confirms them, and takes over one declared otherwise, unless a newer version
|
|
28
|
+
* of the code declared it and a process of that code still confirms it. It deletes a declared schedule its code
|
|
29
|
+
* doesn't declare once no process confirmed it for five minutes: the code that declares it is gone.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const scheduler = new Scheduler({ store, targets, declared, fire, running, cancel, clock });
|
|
33
|
+
* await scheduler.synced(); // at startup
|
|
34
|
+
* const worker = new LeasedWorker({ ..., produce: async () => (await scheduler.produce(owner, leaseMs)).started });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare class Scheduler<S extends ScheduleSpec = ScheduleSpec> {
|
|
38
|
+
private readonly options;
|
|
39
|
+
/** The store, as it holds the schedules: payloads undecoded. */
|
|
40
|
+
readonly store: ScheduleStore;
|
|
41
|
+
private readonly encoded;
|
|
42
|
+
private readonly clock;
|
|
43
|
+
private readonly logger;
|
|
44
|
+
private readonly labels;
|
|
45
|
+
private syncing?;
|
|
46
|
+
/** When `produce()` next reconciles the declared schedules with the code (the clock's time). */
|
|
47
|
+
private reconcileAt;
|
|
48
|
+
private readonly warned;
|
|
49
|
+
constructor(options: SchedulerOptions<S>);
|
|
50
|
+
/** The declared schedules, reconciled with the code once per process (again after a failure). Call it at startup. */
|
|
51
|
+
synced(): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Leases the due schedules of this process's targets and starts their occurrences' runs; every minute it reconciles
|
|
54
|
+
* the declared schedules first. Call it from the worker's loop (`LeasedWorker`'s `produce`), with its owner and
|
|
55
|
+
* lease duration.
|
|
56
|
+
*/
|
|
57
|
+
produce(owner: string, leaseMs: number): Promise<ScheduleProduction>;
|
|
58
|
+
/**
|
|
59
|
+
* Applies `change` to the stored schedule until it lands: a write conditional on the revision it read. `change`
|
|
60
|
+
* returns the new fields, or `null` to leave it. Resolves to the schedule as stored (`null`: none). With `replacing`,
|
|
61
|
+
* a schedule whose payload no codec can read any more is read as it is stored, so the new payload replaces it; with
|
|
62
|
+
* `asStored`, it is read and saved as the store holds it, its payload untouched (a pause needs no codec).
|
|
63
|
+
*/
|
|
64
|
+
save(id: string, change: (current: ScheduleRecord | null) => ScheduleFields | null, options?: {
|
|
65
|
+
replacing?: boolean;
|
|
66
|
+
asStored?: boolean;
|
|
67
|
+
}): Promise<ScheduleRecord | null>;
|
|
68
|
+
/**
|
|
69
|
+
* A schedule's fields after a change of what it is: a new one starts with its first occurrence after now; one whose
|
|
70
|
+
* timing is unchanged keeps its progress; one whose timing changed starts over from now (keeping its runs and the
|
|
71
|
+
* starts in flight, dropping an occurrence buffered with the old timing) and takes the lease from a worker that
|
|
72
|
+
* computed with the old timing. Its pause carries over.
|
|
73
|
+
*/
|
|
74
|
+
changed(current: ScheduleRecord | null, next: {
|
|
75
|
+
target: string;
|
|
76
|
+
declared: boolean;
|
|
77
|
+
spec: S;
|
|
78
|
+
payload: unknown;
|
|
79
|
+
}): ScheduleFields;
|
|
80
|
+
/**
|
|
81
|
+
* Pauses the schedule at once: it takes the lease, so a worker deciding its occurrences at that moment starts only
|
|
82
|
+
* what it had already decided, and nothing else starts until `resume()`. Resolves to it as stored, or `null` for an
|
|
83
|
+
* unknown id.
|
|
84
|
+
*/
|
|
85
|
+
pause(id: string): Promise<ScheduleRecord | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Resumes the schedule from its first occurrence after now: those due while it was paused are passed over, whatever
|
|
88
|
+
* `missed` says. Resolves to it as stored, or `null` for an unknown id.
|
|
89
|
+
*/
|
|
90
|
+
resume(id: string): Promise<ScheduleRecord | null>;
|
|
91
|
+
/** The schedule with its payload decoded, or `null` for an unknown id. Throws when no codec can read its payload. */
|
|
92
|
+
read(id: string): Promise<ScheduleRecord | null>;
|
|
93
|
+
/** A schedule as stored, with its payload decoded; `null` (logged once) when no codec can read it any more. */
|
|
94
|
+
readable(record: ScheduleRecord): Promise<ScheduleRecord>;
|
|
95
|
+
/**
|
|
96
|
+
* Where a stored schedule is: when its next occurrence is due (`null` while paused, or once it ended), how many it
|
|
97
|
+
* started (what `limit` counts), and the occurrence buffered for `overlap: 'buffer-one'`.
|
|
98
|
+
*/
|
|
99
|
+
progress(record: ScheduleRecord): {
|
|
100
|
+
nextAt: number | null;
|
|
101
|
+
runs: number;
|
|
102
|
+
bufferedAt: number | null;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Reconciles the declared schedules with this process's code: at startup, then about once a minute (from the
|
|
106
|
+
* productions). Safe in a rolling deploy, where processes of other code run beside it:
|
|
107
|
+
*
|
|
108
|
+
* - A schedule its code declares is saved when it is missing, and confirmed (`confirmedAt`) when its last
|
|
109
|
+
* confirmation is a minute old. One declared differently is replaced at startup, unless a newer version of the
|
|
110
|
+
* code declared it and a process of that code still confirms it; later, only once no process confirms it (the code
|
|
111
|
+
* that declared it is gone), or when an older version declared it.
|
|
112
|
+
* - A declared schedule its code doesn't declare is left while a process confirms it, since the code of a running
|
|
113
|
+
* process declares it, and deleted once none has for five minutes, whatever its target: otherwise the schedules of
|
|
114
|
+
* a target that no code has any more would stay forever. The processes whose code declares one save it again as
|
|
115
|
+
* they start, so after its processes were gone for longer, it starts over: from its next occurrence, unpaused, its
|
|
116
|
+
* runs counted from zero.
|
|
117
|
+
*/
|
|
118
|
+
private reconcile;
|
|
119
|
+
private declaredChange;
|
|
120
|
+
/**
|
|
121
|
+
* A schedule to save with a new payload (the code's, or an upsert's): one with a payload no codec can read any more
|
|
122
|
+
* (a dropped key) is read as it is stored, its payload marked unreadable, so the new payload can replace it.
|
|
123
|
+
*/
|
|
124
|
+
private readReplacing;
|
|
125
|
+
/**
|
|
126
|
+
* Deletes a declared schedule this process's code doesn't declare, once no process whose code declares it has
|
|
127
|
+
* confirmed it for five minutes; again if it was written meanwhile, unless that write confirmed it.
|
|
128
|
+
*/
|
|
129
|
+
private remove;
|
|
130
|
+
/** Every declared schedule as stored, a page at a time. */
|
|
131
|
+
private allDeclared;
|
|
132
|
+
private declared;
|
|
133
|
+
private produceOne;
|
|
134
|
+
}
|