@nest-native/jobs 0.1.0 → 0.2.0
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/README.md +33 -5
- package/dist/dialects/mysql/index.d.ts +1 -0
- package/dist/dialects/mysql/index.js +1 -0
- package/dist/dialects/mysql/index.js.map +1 -1
- package/dist/dialects/mysql/schedule-store.d.ts +36 -0
- package/dist/dialects/mysql/schedule-store.js +185 -0
- package/dist/dialects/mysql/schedule-store.js.map +1 -0
- package/dist/dialects/mysql/schema.d.ts +274 -0
- package/dist/dialects/mysql/schema.js +31 -1
- package/dist/dialects/mysql/schema.js.map +1 -1
- package/dist/dialects/postgres/index.d.ts +1 -0
- package/dist/dialects/postgres/index.js +1 -0
- package/dist/dialects/postgres/index.js.map +1 -1
- package/dist/dialects/postgres/schedule-store.d.ts +22 -0
- package/dist/dialects/postgres/schedule-store.js +143 -0
- package/dist/dialects/postgres/schedule-store.js.map +1 -0
- package/dist/dialects/postgres/schema.d.ts +273 -0
- package/dist/dialects/postgres/schema.js +30 -1
- package/dist/dialects/postgres/schema.js.map +1 -1
- package/dist/dialects/sqlite/index.d.ts +1 -0
- package/dist/dialects/sqlite/index.js +2 -1
- package/dist/dialects/sqlite/index.js.map +1 -1
- package/dist/dialects/sqlite/schedule-store.d.ts +23 -0
- package/dist/dialects/sqlite/schedule-store.js +153 -0
- package/dist/dialects/sqlite/schedule-store.js.map +1 -0
- package/dist/dialects/sqlite/schema.d.ts +295 -0
- package/dist/dialects/sqlite/schema.js +32 -1
- package/dist/dialects/sqlite/schema.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.js +15 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +137 -0
- package/dist/job-schedules.service.d.ts +44 -0
- package/dist/job-schedules.service.js +125 -0
- package/dist/job-schedules.service.js.map +1 -0
- package/dist/jobs-claimer.service.d.ts +16 -2
- package/dist/jobs-claimer.service.js +71 -2
- package/dist/jobs-claimer.service.js.map +1 -1
- package/dist/jobs.module.d.ts +3 -1
- package/dist/jobs.module.js +17 -2
- package/dist/jobs.module.js.map +1 -1
- package/dist/schedule-planner.d.ts +30 -0
- package/dist/schedule-planner.js +58 -0
- package/dist/schedule-planner.js.map +1 -0
- package/dist/testing/harness.js +2 -1
- package/dist/testing/harness.js.map +1 -1
- package/dist/tokens.d.ts +5 -0
- package/dist/tokens.js +6 -1
- package/dist/tokens.js.map +1 -1
- package/package.json +6 -4
package/dist/interfaces.d.ts
CHANGED
|
@@ -104,6 +104,137 @@ export interface JobStore {
|
|
|
104
104
|
/** Terminal: sets `failed`, attempts+1, and clears `uniqueKey`. */
|
|
105
105
|
markFailed(db: unknown, id: string, reason: string): Promise<void>;
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The dialect-agnostic shape of a `job_schedules` row. Like {@link JobRow},
|
|
109
|
+
* timestamps are ISO-8601 strings on every dialect (lexicographic comparison
|
|
110
|
+
* is what `listDue` relies on). `nextRunAt` is `null` only when the schedule
|
|
111
|
+
* has no future occurrence (the claimer also disables it then).
|
|
112
|
+
*/
|
|
113
|
+
export interface ScheduleRow {
|
|
114
|
+
id: string;
|
|
115
|
+
/** Unique schedule identity — upserts key on it. */
|
|
116
|
+
name: string;
|
|
117
|
+
/** The `@JobHandler` name each occurrence enqueues. */
|
|
118
|
+
jobName: string;
|
|
119
|
+
payload: Record<string, unknown>;
|
|
120
|
+
/** Cron expression (croner syntax). */
|
|
121
|
+
cron: string;
|
|
122
|
+
/** IANA timezone; `null` means UTC. */
|
|
123
|
+
timezone: string | null;
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
nextRunAt: string | null;
|
|
126
|
+
/** Occurrence enqueue overrides; `null` falls back to the store default. */
|
|
127
|
+
maxAttempts: number | null;
|
|
128
|
+
priority: number | null;
|
|
129
|
+
/**
|
|
130
|
+
* When set, every occurrence enqueues with this `uniqueKey` — so while one
|
|
131
|
+
* occurrence is still ACTIVE the next is a dedup no-op (the overlap guard),
|
|
132
|
+
* reusing the jobs uniqueKey contract verbatim.
|
|
133
|
+
*/
|
|
134
|
+
uniqueKey: string | null;
|
|
135
|
+
lastEnqueuedAt: string | null;
|
|
136
|
+
lastError: string | null;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
updatedAt: string;
|
|
139
|
+
}
|
|
140
|
+
/** What a caller supplies to {@link JobSchedulesService.upsert}. */
|
|
141
|
+
export interface UpsertScheduleInput<TPayload extends object = Record<string, unknown>> {
|
|
142
|
+
/** Unique schedule identity — upserting an existing name updates it. */
|
|
143
|
+
name: string;
|
|
144
|
+
/** The `@JobHandler` name each occurrence enqueues. */
|
|
145
|
+
jobName: string;
|
|
146
|
+
payload?: TPayload;
|
|
147
|
+
/** Cron expression (croner syntax); validated at call time — expressions with no future occurrence are rejected. */
|
|
148
|
+
cron: string;
|
|
149
|
+
/** IANA timezone (default UTC). */
|
|
150
|
+
timezone?: string;
|
|
151
|
+
/**
|
|
152
|
+
* OMITTING this is not the same as `true`: an omitted `enabled` defaults to
|
|
153
|
+
* true on INSERT but PRESERVES the stored value on update — so a boot-time
|
|
154
|
+
* upsert never resurrects a schedule that ops disabled at runtime with
|
|
155
|
+
* `setEnabled(name, false)`.
|
|
156
|
+
*/
|
|
157
|
+
enabled?: boolean;
|
|
158
|
+
maxAttempts?: number;
|
|
159
|
+
priority?: number;
|
|
160
|
+
/** Occurrence dedup key — the overlap guard (see {@link ScheduleRow.uniqueKey}). */
|
|
161
|
+
uniqueKey?: string;
|
|
162
|
+
}
|
|
163
|
+
/** The fully-resolved row content the service hands `ScheduleStore.upsert`. */
|
|
164
|
+
export interface ResolvedScheduleUpsert {
|
|
165
|
+
name: string;
|
|
166
|
+
jobName: string;
|
|
167
|
+
payload: Record<string, unknown>;
|
|
168
|
+
cron: string;
|
|
169
|
+
timezone: string | null;
|
|
170
|
+
/** `null` = the caller omitted it: default true on insert, PRESERVE the stored value on update. */
|
|
171
|
+
enabled: boolean | null;
|
|
172
|
+
/**
|
|
173
|
+
* The freshly-armed due time. On a conflict-update the store only applies
|
|
174
|
+
* it when `cron`/`timezone` changed or the stored `nextRunAt` is null —
|
|
175
|
+
* otherwise the stored due time is preserved, so a redeploy's boot upsert
|
|
176
|
+
* neither skips a pending catch-up nor perturbs the rhythm.
|
|
177
|
+
*/
|
|
178
|
+
nextRunAt: string;
|
|
179
|
+
maxAttempts: number | null;
|
|
180
|
+
priority: number | null;
|
|
181
|
+
uniqueKey: string | null;
|
|
182
|
+
}
|
|
183
|
+
/** One due-schedule claim attempt (see {@link ScheduleStore.claimAndEnqueue}). */
|
|
184
|
+
export interface ScheduleClaim {
|
|
185
|
+
id: string;
|
|
186
|
+
/** CAS guard: the `nextRunAt` this claimer read — the claim wins only if it is unchanged. */
|
|
187
|
+
expectedNextRunAt: string;
|
|
188
|
+
/** The new `nextRunAt`; `null` disables the schedule (no future occurrence). */
|
|
189
|
+
nextRunAt: string | null;
|
|
190
|
+
nowIso: string;
|
|
191
|
+
/** The occurrence to enqueue when the claim wins. */
|
|
192
|
+
input: EnqueueJobInput<object>;
|
|
193
|
+
}
|
|
194
|
+
export interface ScheduleClaimResult {
|
|
195
|
+
/** True when THIS claimer won the compare-and-swap (the row advanced). */
|
|
196
|
+
claimed: boolean;
|
|
197
|
+
/**
|
|
198
|
+
* The occurrence job inserted by THIS claim. `null` when the claim lost,
|
|
199
|
+
* or when the overlap guard suppressed the insert (an occurrence with the
|
|
200
|
+
* schedule's `(name, uniqueKey)` is still active) — in the latter case the
|
|
201
|
+
* schedule still advanced.
|
|
202
|
+
*/
|
|
203
|
+
job: JobRow | null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The transactional persistence seam for schedules — same design as
|
|
207
|
+
* {@link JobStore}: dialect-specific, owns its Drizzle table and transactions,
|
|
208
|
+
* `db` is opaque to the engine. `upsert` returns the store's native shape
|
|
209
|
+
* (synchronous on sqlite, a `Promise` on pg/mysql) so it composes inside the
|
|
210
|
+
* caller's `@Transactional` body.
|
|
211
|
+
*
|
|
212
|
+
* `claimAndEnqueue` is the exactly-once occurrence handoff: in ONE store
|
|
213
|
+
* transaction it (1) compare-and-swaps the row's `nextRunAt` from
|
|
214
|
+
* `expectedNextRunAt` to the new value (losing the race returns
|
|
215
|
+
* `{ claimed: false }` with nothing written) and (2) inserts the occurrence
|
|
216
|
+
* job conflict-tolerantly (a `(name, unique_key)` duplicate is the overlap
|
|
217
|
+
* guard, not an error — the schedule still advances).
|
|
218
|
+
*/
|
|
219
|
+
export interface ScheduleStore {
|
|
220
|
+
upsert(db: unknown, input: ResolvedScheduleUpsert): ScheduleRow | Promise<ScheduleRow>;
|
|
221
|
+
get(db: unknown, name: string): Promise<ScheduleRow | undefined>;
|
|
222
|
+
list(db: unknown): Promise<ScheduleRow[]>;
|
|
223
|
+
/** Resolves true when a row was deleted. */
|
|
224
|
+
remove(db: unknown, name: string): Promise<boolean>;
|
|
225
|
+
/**
|
|
226
|
+
* Flips `enabled` and overwrites `nextRunAt`; resolves the updated row, or
|
|
227
|
+
* undefined when the name is unknown OR `expectedUpdatedAt` was supplied
|
|
228
|
+
* and no longer matches (optimistic-concurrency guard — the caller re-reads
|
|
229
|
+
* and retries).
|
|
230
|
+
*/
|
|
231
|
+
setEnabled(db: unknown, name: string, enabled: boolean, nextRunAt: string | null, expectedUpdatedAt?: string): Promise<ScheduleRow | undefined>;
|
|
232
|
+
/** Enabled rows with a non-null `nextRunAt <= nowIso`, oldest due first. */
|
|
233
|
+
listDue(db: unknown, nowIso: string, limit: number): Promise<ScheduleRow[]>;
|
|
234
|
+
claimAndEnqueue(db: unknown, claim: ScheduleClaim): Promise<ScheduleClaimResult>;
|
|
235
|
+
/** Disables a corrupted schedule (invalid cron found at claim time) recording `lastError`. */
|
|
236
|
+
disable(db: unknown, id: string, lastError: string): Promise<void>;
|
|
237
|
+
}
|
|
107
238
|
/** Options for {@link JobsModule.forRoot}. */
|
|
108
239
|
export interface JobsModuleOptions {
|
|
109
240
|
/**
|
|
@@ -114,6 +245,12 @@ export interface JobsModuleOptions {
|
|
|
114
245
|
drizzleInstanceToken: symbol | string;
|
|
115
246
|
/** The dialect-specific job store. */
|
|
116
247
|
store: JobStore;
|
|
248
|
+
/**
|
|
249
|
+
* The dialect-specific schedule store. OPT-IN: without it the claimer never
|
|
250
|
+
* touches schedules and `JobSchedulesService` throws on use — 0.1 behavior
|
|
251
|
+
* is unchanged.
|
|
252
|
+
*/
|
|
253
|
+
scheduleStore?: ScheduleStore;
|
|
117
254
|
/**
|
|
118
255
|
* Modules that provide (and export) the `drizzleInstanceToken`. Required when
|
|
119
256
|
* that token is not registered by a global module — `JobsModule` imports
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ScheduleRow, ScheduleStore, UpsertScheduleInput } from './interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Injectable CRUD for DB-stored cron schedules — deliberately **no REST
|
|
4
|
+
* controller and no admin UI**: expose it from your own endpoints if you want
|
|
5
|
+
* runtime editing over HTTP.
|
|
6
|
+
*
|
|
7
|
+
* `upsert` returns the store's native shape (synchronous `ScheduleRow` on
|
|
8
|
+
* sqlite, a `Promise` on pg/mysql) and — like `JobsService.enqueue` — runs on
|
|
9
|
+
* the transaction-scoped Drizzle instance, so creating a schedule can ride the
|
|
10
|
+
* caller's business transaction.
|
|
11
|
+
*
|
|
12
|
+
* Upserts are safe to run on every boot (the documented pattern): on an
|
|
13
|
+
* existing schedule, an OMITTED `enabled` preserves the stored value (an ops
|
|
14
|
+
* `setEnabled(name, false)` survives redeploys), and the stored `nextRunAt`
|
|
15
|
+
* is preserved while `cron`/`timezone` are unchanged (a pending catch-up
|
|
16
|
+
* survives restarts).
|
|
17
|
+
*
|
|
18
|
+
* Cron expressions are validated (croner) at call time; invalid ones — and
|
|
19
|
+
* expressions with no future occurrence at all — throw `InvalidScheduleError`
|
|
20
|
+
* and never reach the table. The misfire policy is fixed: arming always
|
|
21
|
+
* computes from *now*, so `setEnabled(name, true)` on a long-disabled
|
|
22
|
+
* schedule resumes at the next FUTURE occurrence instead of bursting.
|
|
23
|
+
*/
|
|
24
|
+
export declare class JobSchedulesService<TStore extends ScheduleStore = ScheduleStore> {
|
|
25
|
+
private readonly db;
|
|
26
|
+
private readonly store;
|
|
27
|
+
constructor(db: unknown, store?: TStore | null);
|
|
28
|
+
upsert<TPayload extends object>(input: UpsertScheduleInput<TPayload>): ReturnType<TStore['upsert']>;
|
|
29
|
+
get(name: string): Promise<ScheduleRow | undefined>;
|
|
30
|
+
list(): Promise<ScheduleRow[]>;
|
|
31
|
+
/** Resolves true when the schedule existed and was deleted. */
|
|
32
|
+
remove(name: string): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Enables or disables a schedule. Enabling re-arms `nextRunAt` from now
|
|
35
|
+
* (skip-missed policy — no catch-up burst); disabling clears it. Resolves
|
|
36
|
+
* the updated row, or undefined when the name is unknown.
|
|
37
|
+
*
|
|
38
|
+
* The enable path is a read-compute-write guarded by the row's `updatedAt`
|
|
39
|
+
* (so a concurrent upsert changing the cron cannot be overwritten with a
|
|
40
|
+
* stale computation) and retried a few times; persistent contention throws.
|
|
41
|
+
*/
|
|
42
|
+
setEnabled(name: string, enabled: boolean): Promise<ScheduleRow | undefined>;
|
|
43
|
+
private requireStore;
|
|
44
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.JobSchedulesService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const transactional_1 = require("@nestjs-cls/transactional");
|
|
18
|
+
const schedule_planner_1 = require("./schedule-planner");
|
|
19
|
+
const tokens_1 = require("./tokens");
|
|
20
|
+
/**
|
|
21
|
+
* Injectable CRUD for DB-stored cron schedules — deliberately **no REST
|
|
22
|
+
* controller and no admin UI**: expose it from your own endpoints if you want
|
|
23
|
+
* runtime editing over HTTP.
|
|
24
|
+
*
|
|
25
|
+
* `upsert` returns the store's native shape (synchronous `ScheduleRow` on
|
|
26
|
+
* sqlite, a `Promise` on pg/mysql) and — like `JobsService.enqueue` — runs on
|
|
27
|
+
* the transaction-scoped Drizzle instance, so creating a schedule can ride the
|
|
28
|
+
* caller's business transaction.
|
|
29
|
+
*
|
|
30
|
+
* Upserts are safe to run on every boot (the documented pattern): on an
|
|
31
|
+
* existing schedule, an OMITTED `enabled` preserves the stored value (an ops
|
|
32
|
+
* `setEnabled(name, false)` survives redeploys), and the stored `nextRunAt`
|
|
33
|
+
* is preserved while `cron`/`timezone` are unchanged (a pending catch-up
|
|
34
|
+
* survives restarts).
|
|
35
|
+
*
|
|
36
|
+
* Cron expressions are validated (croner) at call time; invalid ones — and
|
|
37
|
+
* expressions with no future occurrence at all — throw `InvalidScheduleError`
|
|
38
|
+
* and never reach the table. The misfire policy is fixed: arming always
|
|
39
|
+
* computes from *now*, so `setEnabled(name, true)` on a long-disabled
|
|
40
|
+
* schedule resumes at the next FUTURE occurrence instead of bursting.
|
|
41
|
+
*/
|
|
42
|
+
let JobSchedulesService = class JobSchedulesService {
|
|
43
|
+
constructor(db, store = null) {
|
|
44
|
+
this.db = db;
|
|
45
|
+
this.store = store;
|
|
46
|
+
}
|
|
47
|
+
upsert(input) {
|
|
48
|
+
const store = this.requireStore();
|
|
49
|
+
const timezone = input.timezone ?? null;
|
|
50
|
+
const resolved = {
|
|
51
|
+
name: input.name,
|
|
52
|
+
jobName: input.jobName,
|
|
53
|
+
// The one place the structural payload widens to the stored shape.
|
|
54
|
+
payload: (input.payload ?? {}),
|
|
55
|
+
cron: input.cron,
|
|
56
|
+
timezone,
|
|
57
|
+
// null = omitted: the store defaults it to true on insert and
|
|
58
|
+
// preserves the stored value on update.
|
|
59
|
+
enabled: input.enabled ?? null,
|
|
60
|
+
// Validates the expression AND rejects never-firing ones.
|
|
61
|
+
nextRunAt: (0, schedule_planner_1.armSchedule)(input.cron, timezone, new Date()),
|
|
62
|
+
maxAttempts: input.maxAttempts ?? null,
|
|
63
|
+
priority: input.priority ?? null,
|
|
64
|
+
uniqueKey: input.uniqueKey ?? null,
|
|
65
|
+
};
|
|
66
|
+
return store.upsert(this.db, resolved);
|
|
67
|
+
}
|
|
68
|
+
async get(name) {
|
|
69
|
+
return this.requireStore().get(this.db, name);
|
|
70
|
+
}
|
|
71
|
+
async list() {
|
|
72
|
+
return this.requireStore().list(this.db);
|
|
73
|
+
}
|
|
74
|
+
/** Resolves true when the schedule existed and was deleted. */
|
|
75
|
+
async remove(name) {
|
|
76
|
+
return this.requireStore().remove(this.db, name);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Enables or disables a schedule. Enabling re-arms `nextRunAt` from now
|
|
80
|
+
* (skip-missed policy — no catch-up burst); disabling clears it. Resolves
|
|
81
|
+
* the updated row, or undefined when the name is unknown.
|
|
82
|
+
*
|
|
83
|
+
* The enable path is a read-compute-write guarded by the row's `updatedAt`
|
|
84
|
+
* (so a concurrent upsert changing the cron cannot be overwritten with a
|
|
85
|
+
* stale computation) and retried a few times; persistent contention throws.
|
|
86
|
+
*/
|
|
87
|
+
async setEnabled(name, enabled) {
|
|
88
|
+
const store = this.requireStore();
|
|
89
|
+
if (!enabled) {
|
|
90
|
+
return store.setEnabled(this.db, name, false, null);
|
|
91
|
+
}
|
|
92
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
93
|
+
const row = await store.get(this.db, name);
|
|
94
|
+
if (!row) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
// The row may have been hand-edited behind the service's back — arming
|
|
98
|
+
// validates (and rejects never-firing expressions) before enabling.
|
|
99
|
+
const next = (0, schedule_planner_1.armSchedule)(row.cron, row.timezone, new Date());
|
|
100
|
+
const updated = await store.setEnabled(this.db, name, true, next, row.updatedAt);
|
|
101
|
+
if (updated) {
|
|
102
|
+
return updated;
|
|
103
|
+
}
|
|
104
|
+
// Guard miss: someone modified the row between our read and write —
|
|
105
|
+
// re-read and recompute from the fresh cron/timezone.
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Schedule "${name}" is being modified concurrently — retry setEnabled.`);
|
|
108
|
+
}
|
|
109
|
+
requireStore() {
|
|
110
|
+
if (!this.store) {
|
|
111
|
+
throw new Error('JobsModule was configured without a scheduleStore — pass one ' +
|
|
112
|
+
'(e.g. new SqliteScheduleStore()) to JobsModule.forRoot to use schedules.');
|
|
113
|
+
}
|
|
114
|
+
return this.store;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
exports.JobSchedulesService = JobSchedulesService;
|
|
118
|
+
exports.JobSchedulesService = JobSchedulesService = __decorate([
|
|
119
|
+
(0, common_1.Injectable)(),
|
|
120
|
+
__param(0, (0, transactional_1.InjectTransaction)()),
|
|
121
|
+
__param(1, (0, common_1.Optional)()),
|
|
122
|
+
__param(1, (0, common_1.Inject)(tokens_1.JOBS_SCHEDULE_STORE)),
|
|
123
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
124
|
+
], JobSchedulesService);
|
|
125
|
+
//# sourceMappingURL=job-schedules.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-schedules.service.js","sourceRoot":"","sources":["../job-schedules.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import type { JobStore, ResolvedRunnerConfig, RunnerConfig } from './interfaces';
|
|
1
|
+
import type { JobStore, ResolvedRunnerConfig, RunnerConfig, ScheduleStore } from './interfaces';
|
|
2
2
|
import { JobsHandlerExplorer } from './jobs-handler.explorer';
|
|
3
3
|
export declare const DEFAULT_RUNNER_CONFIG: ResolvedRunnerConfig;
|
|
4
4
|
export interface TickReport {
|
|
5
|
+
/** Schedule occurrences THIS tick enqueued (won claims; dedup-suppressed occurrences still count as claimed). */
|
|
6
|
+
scheduled: number;
|
|
5
7
|
claimed: number;
|
|
6
8
|
completed: number;
|
|
7
9
|
retried: number;
|
|
@@ -27,9 +29,21 @@ export declare class JobsClaimer {
|
|
|
27
29
|
private readonly db;
|
|
28
30
|
private readonly store;
|
|
29
31
|
private readonly explorer;
|
|
32
|
+
private readonly scheduleStore;
|
|
30
33
|
private readonly logger;
|
|
31
|
-
constructor(db: unknown, store: JobStore, explorer: JobsHandlerExplorer);
|
|
34
|
+
constructor(db: unknown, store: JobStore, explorer: JobsHandlerExplorer, scheduleStore?: ScheduleStore | null);
|
|
32
35
|
tick(overrides?: RunnerConfig): Promise<TickReport>;
|
|
36
|
+
/**
|
|
37
|
+
* Fires every due schedule at most once. Per schedule: compute the next
|
|
38
|
+
* occurrence strictly after *now* (skip-missed policy — a schedule that was
|
|
39
|
+
* down for a week gets at most this one catch-up), then let the store
|
|
40
|
+
* compare-and-swap `nextRunAt` and insert the occurrence in ONE transaction.
|
|
41
|
+
* A lost CAS means another instance fired it — not an error, not counted.
|
|
42
|
+
* A schedule whose cron cannot be evaluated (hand-corrupted row) is disabled
|
|
43
|
+
* with the error recorded, and the loop continues.
|
|
44
|
+
*/
|
|
45
|
+
private drainSchedules;
|
|
46
|
+
private fireSchedule;
|
|
33
47
|
private processOne;
|
|
34
48
|
private onHandlerError;
|
|
35
49
|
private fail;
|
|
@@ -18,6 +18,7 @@ const node_os_1 = require("node:os");
|
|
|
18
18
|
const common_1 = require("@nestjs/common");
|
|
19
19
|
const errors_1 = require("./errors");
|
|
20
20
|
const jobs_handler_explorer_1 = require("./jobs-handler.explorer");
|
|
21
|
+
const schedule_planner_1 = require("./schedule-planner");
|
|
21
22
|
const tokens_1 = require("./tokens");
|
|
22
23
|
exports.DEFAULT_RUNNER_CONFIG = {
|
|
23
24
|
workerInstanceId: `${(0, node_os_1.hostname)()}-${process.pid}`,
|
|
@@ -43,16 +44,21 @@ exports.DEFAULT_RUNNER_CONFIG = {
|
|
|
43
44
|
* transaction — so it freely awaits the store and the handlers.
|
|
44
45
|
*/
|
|
45
46
|
let JobsClaimer = JobsClaimer_1 = class JobsClaimer {
|
|
46
|
-
constructor(db, store, explorer) {
|
|
47
|
+
constructor(db, store, explorer, scheduleStore = null) {
|
|
47
48
|
this.db = db;
|
|
48
49
|
this.store = store;
|
|
49
50
|
this.explorer = explorer;
|
|
51
|
+
this.scheduleStore = scheduleStore;
|
|
50
52
|
this.logger = new common_1.Logger(JobsClaimer_1.name);
|
|
51
53
|
}
|
|
52
54
|
async tick(overrides = {}) {
|
|
53
55
|
const cfg = { ...exports.DEFAULT_RUNNER_CONFIG, ...overrides };
|
|
56
|
+
// Due schedules fire first, so their occurrences (due immediately) are
|
|
57
|
+
// claimable by this very tick's batch claim.
|
|
58
|
+
const scheduled = this.scheduleStore ? await this.drainSchedules(cfg) : 0;
|
|
54
59
|
const claimed = await this.store.claimBatch(this.db, cfg);
|
|
55
60
|
const report = {
|
|
61
|
+
scheduled,
|
|
56
62
|
claimed: claimed.length,
|
|
57
63
|
completed: 0,
|
|
58
64
|
retried: 0,
|
|
@@ -64,6 +70,57 @@ let JobsClaimer = JobsClaimer_1 = class JobsClaimer {
|
|
|
64
70
|
}
|
|
65
71
|
return report;
|
|
66
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Fires every due schedule at most once. Per schedule: compute the next
|
|
75
|
+
* occurrence strictly after *now* (skip-missed policy — a schedule that was
|
|
76
|
+
* down for a week gets at most this one catch-up), then let the store
|
|
77
|
+
* compare-and-swap `nextRunAt` and insert the occurrence in ONE transaction.
|
|
78
|
+
* A lost CAS means another instance fired it — not an error, not counted.
|
|
79
|
+
* A schedule whose cron cannot be evaluated (hand-corrupted row) is disabled
|
|
80
|
+
* with the error recorded, and the loop continues.
|
|
81
|
+
*/
|
|
82
|
+
async drainSchedules(cfg) {
|
|
83
|
+
const now = new Date();
|
|
84
|
+
const due = await this.scheduleStore.listDue(this.db, now.toISOString(), cfg.batchSize);
|
|
85
|
+
let scheduled = 0;
|
|
86
|
+
for (const schedule of due) {
|
|
87
|
+
scheduled += await this.fireSchedule(schedule, now);
|
|
88
|
+
}
|
|
89
|
+
return scheduled;
|
|
90
|
+
}
|
|
91
|
+
async fireSchedule(schedule, now) {
|
|
92
|
+
let nextRunAt;
|
|
93
|
+
try {
|
|
94
|
+
nextRunAt = (0, schedule_planner_1.nextOccurrence)(schedule.cron, schedule.timezone, now);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// Only InvalidScheduleError (an Error subclass) escapes nextOccurrence —
|
|
98
|
+
// the hand-corrupted row case. THAT alone disables a schedule.
|
|
99
|
+
const message = error.message;
|
|
100
|
+
this.logger.warn(`schedule ${schedule.id} ("${schedule.name}") disabled: ${message}`);
|
|
101
|
+
await this.scheduleStore.disable(this.db, schedule.id, message);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const result = await this.scheduleStore.claimAndEnqueue(this.db, {
|
|
106
|
+
id: schedule.id,
|
|
107
|
+
// listDue only returns rows with a non-null nextRunAt.
|
|
108
|
+
expectedNextRunAt: schedule.nextRunAt,
|
|
109
|
+
nextRunAt,
|
|
110
|
+
nowIso: now.toISOString(),
|
|
111
|
+
input: occurrenceInput(schedule),
|
|
112
|
+
});
|
|
113
|
+
return result.claimed ? 1 : 0;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// A transient store error (connection drop, lock timeout, serialization
|
|
117
|
+
// failure) must NOT kill the schedule: nothing was written, the row is
|
|
118
|
+
// still due, and the next tick retries it naturally.
|
|
119
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
120
|
+
this.logger.warn(`schedule ${schedule.id} ("${schedule.name}") claim failed, will retry next tick: ${message}`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
67
124
|
async processOne(job, cfg) {
|
|
68
125
|
try {
|
|
69
126
|
const handler = this.explorer.get(job.name);
|
|
@@ -116,6 +173,18 @@ exports.JobsClaimer = JobsClaimer = JobsClaimer_1 = __decorate([
|
|
|
116
173
|
(0, common_1.Injectable)(),
|
|
117
174
|
__param(0, (0, common_1.Inject)(tokens_1.JOBS_DRIZZLE)),
|
|
118
175
|
__param(1, (0, common_1.Inject)(tokens_1.JOBS_STORE)),
|
|
119
|
-
|
|
176
|
+
__param(3, (0, common_1.Optional)()),
|
|
177
|
+
__param(3, (0, common_1.Inject)(tokens_1.JOBS_SCHEDULE_STORE)),
|
|
178
|
+
__metadata("design:paramtypes", [Object, Object, jobs_handler_explorer_1.JobsHandlerExplorer, Object])
|
|
120
179
|
], JobsClaimer);
|
|
180
|
+
/** The occurrence a schedule enqueues: due immediately, overrides only when set. */
|
|
181
|
+
function occurrenceInput(schedule) {
|
|
182
|
+
return {
|
|
183
|
+
name: schedule.jobName,
|
|
184
|
+
payload: schedule.payload,
|
|
185
|
+
...(schedule.maxAttempts !== null && { maxAttempts: schedule.maxAttempts }),
|
|
186
|
+
...(schedule.priority !== null && { priority: schedule.priority }),
|
|
187
|
+
...(schedule.uniqueKey !== null && { uniqueKey: schedule.uniqueKey }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
121
190
|
//# sourceMappingURL=jobs-claimer.service.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs-claimer.service.js","sourceRoot":"","sources":["../jobs-claimer.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qCAAmC;AACnC,
|
|
1
|
+
{"version":3,"file":"jobs-claimer.service.js","sourceRoot":"","sources":["../jobs-claimer.service.ts"],"names":[],"mappings":"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"}
|
package/dist/jobs.module.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DynamicModule, type InjectionToken, type ModuleMetadata, type OptionalFactoryDependency } from '@nestjs/common';
|
|
2
|
-
import type { JobsModuleOptions, JobStore } from './interfaces';
|
|
2
|
+
import type { JobsModuleOptions, JobStore, ScheduleStore } from './interfaces';
|
|
3
3
|
/**
|
|
4
4
|
* Async configuration. The Drizzle token is static (a DI token is known at
|
|
5
5
|
* module-definition time); the store is built by a factory so it can inject
|
|
@@ -12,6 +12,8 @@ export interface JobsModuleAsyncOptions {
|
|
|
12
12
|
imports?: ModuleMetadata['imports'];
|
|
13
13
|
inject?: (InjectionToken | OptionalFactoryDependency)[];
|
|
14
14
|
useStore: (...args: any[]) => JobStore | Promise<JobStore>;
|
|
15
|
+
/** Optional schedules opt-in; shares `inject` with `useStore`. */
|
|
16
|
+
useScheduleStore?: (...args: any[]) => ScheduleStore | Promise<ScheduleStore>;
|
|
15
17
|
}
|
|
16
18
|
export declare class JobsModule {
|
|
17
19
|
static forRoot(options: JobsModuleOptions): DynamicModule;
|
package/dist/jobs.module.js
CHANGED
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.JobsModule = void 0;
|
|
10
10
|
const common_1 = require("@nestjs/common");
|
|
11
11
|
const core_1 = require("@nestjs/core");
|
|
12
|
+
const job_schedules_service_1 = require("./job-schedules.service");
|
|
12
13
|
const jobs_claimer_service_1 = require("./jobs-claimer.service");
|
|
13
14
|
const jobs_handler_explorer_1 = require("./jobs-handler.explorer");
|
|
14
15
|
const jobs_service_1 = require("./jobs.service");
|
|
@@ -18,6 +19,7 @@ let JobsModule = class JobsModule {
|
|
|
18
19
|
return assemble(options.isGlobal ?? true, options.imports ?? [], [
|
|
19
20
|
{ provide: tokens_1.JOBS_OPTIONS, useValue: options },
|
|
20
21
|
{ provide: tokens_1.JOBS_STORE, useValue: options.store },
|
|
22
|
+
{ provide: tokens_1.JOBS_SCHEDULE_STORE, useValue: options.scheduleStore ?? null },
|
|
21
23
|
{ provide: tokens_1.JOBS_DRIZZLE, useExisting: options.drizzleInstanceToken },
|
|
22
24
|
]);
|
|
23
25
|
}
|
|
@@ -28,6 +30,13 @@ let JobsModule = class JobsModule {
|
|
|
28
30
|
useFactory: options.useStore,
|
|
29
31
|
inject: options.inject ?? [],
|
|
30
32
|
},
|
|
33
|
+
options.useScheduleStore
|
|
34
|
+
? {
|
|
35
|
+
provide: tokens_1.JOBS_SCHEDULE_STORE,
|
|
36
|
+
useFactory: options.useScheduleStore,
|
|
37
|
+
inject: options.inject ?? [],
|
|
38
|
+
}
|
|
39
|
+
: { provide: tokens_1.JOBS_SCHEDULE_STORE, useValue: null },
|
|
31
40
|
{ provide: tokens_1.JOBS_DRIZZLE, useExisting: options.drizzleInstanceToken },
|
|
32
41
|
]);
|
|
33
42
|
}
|
|
@@ -42,8 +51,14 @@ function assemble(global, imports, base) {
|
|
|
42
51
|
global,
|
|
43
52
|
// DiscoveryModule powers the @JobHandler scan at bootstrap.
|
|
44
53
|
imports: [core_1.DiscoveryModule, ...imports],
|
|
45
|
-
providers: [
|
|
46
|
-
|
|
54
|
+
providers: [
|
|
55
|
+
...base,
|
|
56
|
+
jobs_service_1.JobsService,
|
|
57
|
+
job_schedules_service_1.JobSchedulesService,
|
|
58
|
+
jobs_claimer_service_1.JobsClaimer,
|
|
59
|
+
jobs_handler_explorer_1.JobsHandlerExplorer,
|
|
60
|
+
],
|
|
61
|
+
exports: [jobs_service_1.JobsService, job_schedules_service_1.JobSchedulesService, jobs_claimer_service_1.JobsClaimer, jobs_handler_explorer_1.JobsHandlerExplorer],
|
|
47
62
|
};
|
|
48
63
|
}
|
|
49
64
|
//# sourceMappingURL=jobs.module.js.map
|
package/dist/jobs.module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jobs.module.js","sourceRoot":"","sources":["../jobs.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAOwB;AACxB,uCAA+C;AAE/C,iEAAqD;AACrD,mEAA8D;AAC9D,iDAA6C;AAC7C,
|
|
1
|
+
{"version":3,"file":"jobs.module.js","sourceRoot":"","sources":["../jobs.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAOwB;AACxB,uCAA+C;AAE/C,mEAA8D;AAC9D,iEAAqD;AACrD,mEAA8D;AAC9D,iDAA6C;AAC7C,qCAKkB;AAsBX,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,MAAM,CAAC,OAAO,CAAC,OAA0B;QACvC,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;YAC/D,EAAE,OAAO,EAAE,qBAAY,EAAE,QAAQ,EAAE,OAAO,EAAE;YAC5C,EAAE,OAAO,EAAE,mBAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE;YAChD,EAAE,OAAO,EAAE,4BAAmB,EAAE,QAAQ,EAAE,OAAO,CAAC,aAAa,IAAI,IAAI,EAAE;YACzE,EAAE,OAAO,EAAE,qBAAY,EAAE,WAAW,EAAE,OAAO,CAAC,oBAAoB,EAAE;SACrE,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,OAA+B;QACjD,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;YAC/D;gBACE,OAAO,EAAE,mBAAU;gBACnB,UAAU,EAAE,OAAO,CAAC,QAAQ;gBAC5B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;aAC7B;YACD,OAAO,CAAC,gBAAgB;gBACtB,CAAC,CAAC;oBACE,OAAO,EAAE,4BAAmB;oBAC5B,UAAU,EAAE,OAAO,CAAC,gBAAgB;oBACpC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;iBAC7B;gBACH,CAAC,CAAC,EAAE,OAAO,EAAE,4BAAmB,EAAE,QAAQ,EAAE,IAAI,EAAE;YACpD,EAAE,OAAO,EAAE,qBAAY,EAAE,WAAW,EAAE,OAAO,CAAC,oBAAoB,EAAE;SACrE,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AA3BY,gCAAU;qBAAV,UAAU;IADtB,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,UAAU,CA2BtB;AAED,SAAS,QAAQ,CACf,MAAe,EACf,OAA+C,EAC/C,IAAgB;IAEhB,OAAO;QACL,MAAM,EAAE,UAAU;QAClB,MAAM;QACN,4DAA4D;QAC5D,OAAO,EAAE,CAAC,sBAAe,EAAE,GAAG,OAAO,CAAC;QACtC,SAAS,EAAE;YACT,GAAG,IAAI;YACP,0BAAW;YACX,2CAAmB;YACnB,kCAAW;YACX,2CAAmB;SACpB;QACD,OAAO,EAAE,CAAC,0BAAW,EAAE,2CAAmB,EAAE,kCAAW,EAAE,2CAAmB,CAAC;KAC9E,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place cron math happens. Parsing, next-occurrence computation,
|
|
3
|
+
* timezones, and DST semantics are all delegated to `croner` (the package's
|
|
4
|
+
* single runtime dependency) — hand-rolling cron is forbidden by the
|
|
5
|
+
* guidelines. The default timezone is **UTC**, not server-local, so a
|
|
6
|
+
* schedule means the same thing on every instance.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The next occurrence STRICTLY AFTER `after`, as an ISO-8601 string, or
|
|
10
|
+
* `null` when the expression has no future occurrence. Used by the claimer,
|
|
11
|
+
* where `null` means the schedule just fired for the last time and disables
|
|
12
|
+
* itself.
|
|
13
|
+
*
|
|
14
|
+
* Always advancing from *now* — never from the previously stored due time —
|
|
15
|
+
* is the misfire policy: missed occurrences are skipped, and a schedule that
|
|
16
|
+
* was down for a week fires at most one catch-up.
|
|
17
|
+
*
|
|
18
|
+
* Throws {@link InvalidScheduleError} when the expression (or timezone)
|
|
19
|
+
* cannot be evaluated. Croner reports a bad pattern at construction but a bad
|
|
20
|
+
* timezone only when computing a run, so both surface here.
|
|
21
|
+
*/
|
|
22
|
+
export declare function nextOccurrence(cron: string, timezone: string | null, after: Date): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Like {@link nextOccurrence}, but for ARMING a schedule (upsert /
|
|
25
|
+
* `setEnabled(true)`): an expression with no future occurrence at all (e.g.
|
|
26
|
+
* `0 0 30 2 *` — February 30th never comes) is rejected with
|
|
27
|
+
* {@link InvalidScheduleError} instead of creating a schedule that can never
|
|
28
|
+
* fire.
|
|
29
|
+
*/
|
|
30
|
+
export declare function armSchedule(cron: string, timezone: string | null, after: Date): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nextOccurrence = nextOccurrence;
|
|
4
|
+
exports.armSchedule = armSchedule;
|
|
5
|
+
const croner_1 = require("croner");
|
|
6
|
+
const errors_1 = require("./errors");
|
|
7
|
+
/**
|
|
8
|
+
* The one place cron math happens. Parsing, next-occurrence computation,
|
|
9
|
+
* timezones, and DST semantics are all delegated to `croner` (the package's
|
|
10
|
+
* single runtime dependency) — hand-rolling cron is forbidden by the
|
|
11
|
+
* guidelines. The default timezone is **UTC**, not server-local, so a
|
|
12
|
+
* schedule means the same thing on every instance.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The next occurrence STRICTLY AFTER `after`, as an ISO-8601 string, or
|
|
16
|
+
* `null` when the expression has no future occurrence. Used by the claimer,
|
|
17
|
+
* where `null` means the schedule just fired for the last time and disables
|
|
18
|
+
* itself.
|
|
19
|
+
*
|
|
20
|
+
* Always advancing from *now* — never from the previously stored due time —
|
|
21
|
+
* is the misfire policy: missed occurrences are skipped, and a schedule that
|
|
22
|
+
* was down for a week fires at most one catch-up.
|
|
23
|
+
*
|
|
24
|
+
* Throws {@link InvalidScheduleError} when the expression (or timezone)
|
|
25
|
+
* cannot be evaluated. Croner reports a bad pattern at construction but a bad
|
|
26
|
+
* timezone only when computing a run, so both surface here.
|
|
27
|
+
*/
|
|
28
|
+
function nextOccurrence(cron, timezone, after) {
|
|
29
|
+
const parsed = wrap(cron, timezone, () => new croner_1.Cron(cron, { timezone: timezone ?? 'UTC' }));
|
|
30
|
+
return wrap(cron, timezone, () => parsed.nextRun(after)?.toISOString() ?? null);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Like {@link nextOccurrence}, but for ARMING a schedule (upsert /
|
|
34
|
+
* `setEnabled(true)`): an expression with no future occurrence at all (e.g.
|
|
35
|
+
* `0 0 30 2 *` — February 30th never comes) is rejected with
|
|
36
|
+
* {@link InvalidScheduleError} instead of creating a schedule that can never
|
|
37
|
+
* fire.
|
|
38
|
+
*/
|
|
39
|
+
function armSchedule(cron, timezone, after) {
|
|
40
|
+
const next = nextOccurrence(cron, timezone, after);
|
|
41
|
+
if (next === null) {
|
|
42
|
+
throw new errors_1.InvalidScheduleError(`Invalid cron schedule "${cron}"${tzSuffix(timezone)}: it has no future occurrence`);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
function wrap(cron, timezone, run) {
|
|
47
|
+
try {
|
|
48
|
+
return run();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Croner throws Error subclasses (TypeError for bad patterns/timezones).
|
|
52
|
+
throw new errors_1.InvalidScheduleError(`Invalid cron schedule "${cron}"${tzSuffix(timezone)}: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function tzSuffix(timezone) {
|
|
56
|
+
return timezone ? ` (timezone "${timezone}")` : '';
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=schedule-planner.js.map
|