@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.
Files changed (53) hide show
  1. package/README.md +33 -5
  2. package/dist/dialects/mysql/index.d.ts +1 -0
  3. package/dist/dialects/mysql/index.js +1 -0
  4. package/dist/dialects/mysql/index.js.map +1 -1
  5. package/dist/dialects/mysql/schedule-store.d.ts +36 -0
  6. package/dist/dialects/mysql/schedule-store.js +185 -0
  7. package/dist/dialects/mysql/schedule-store.js.map +1 -0
  8. package/dist/dialects/mysql/schema.d.ts +274 -0
  9. package/dist/dialects/mysql/schema.js +31 -1
  10. package/dist/dialects/mysql/schema.js.map +1 -1
  11. package/dist/dialects/postgres/index.d.ts +1 -0
  12. package/dist/dialects/postgres/index.js +1 -0
  13. package/dist/dialects/postgres/index.js.map +1 -1
  14. package/dist/dialects/postgres/schedule-store.d.ts +22 -0
  15. package/dist/dialects/postgres/schedule-store.js +143 -0
  16. package/dist/dialects/postgres/schedule-store.js.map +1 -0
  17. package/dist/dialects/postgres/schema.d.ts +273 -0
  18. package/dist/dialects/postgres/schema.js +30 -1
  19. package/dist/dialects/postgres/schema.js.map +1 -1
  20. package/dist/dialects/sqlite/index.d.ts +1 -0
  21. package/dist/dialects/sqlite/index.js +2 -1
  22. package/dist/dialects/sqlite/index.js.map +1 -1
  23. package/dist/dialects/sqlite/schedule-store.d.ts +23 -0
  24. package/dist/dialects/sqlite/schedule-store.js +153 -0
  25. package/dist/dialects/sqlite/schedule-store.js.map +1 -0
  26. package/dist/dialects/sqlite/schema.d.ts +295 -0
  27. package/dist/dialects/sqlite/schema.js +32 -1
  28. package/dist/dialects/sqlite/schema.js.map +1 -1
  29. package/dist/errors.d.ts +10 -0
  30. package/dist/errors.js +15 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/interfaces.d.ts +137 -0
  36. package/dist/job-schedules.service.d.ts +44 -0
  37. package/dist/job-schedules.service.js +125 -0
  38. package/dist/job-schedules.service.js.map +1 -0
  39. package/dist/jobs-claimer.service.d.ts +16 -2
  40. package/dist/jobs-claimer.service.js +71 -2
  41. package/dist/jobs-claimer.service.js.map +1 -1
  42. package/dist/jobs.module.d.ts +3 -1
  43. package/dist/jobs.module.js +17 -2
  44. package/dist/jobs.module.js.map +1 -1
  45. package/dist/schedule-planner.d.ts +30 -0
  46. package/dist/schedule-planner.js +58 -0
  47. package/dist/schedule-planner.js.map +1 -0
  48. package/dist/testing/harness.js +2 -1
  49. package/dist/testing/harness.js.map +1 -1
  50. package/dist/tokens.d.ts +5 -0
  51. package/dist/tokens.js +6 -1
  52. package/dist/tokens.js.map +1 -1
  53. package/package.json +6 -4
@@ -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
- __metadata("design:paramtypes", [Object, Object, jobs_handler_explorer_1.JobsHandlerExplorer])
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":"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"}
1
+ {"version":3,"file":"jobs-claimer.service.js","sourceRoot":"","sources":["../jobs-claimer.service.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -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: [...base, jobs_service_1.JobsService, jobs_claimer_service_1.JobsClaimer, jobs_handler_explorer_1.JobsHandlerExplorer],
46
- exports: [jobs_service_1.JobsService, jobs_claimer_service_1.JobsClaimer, jobs_handler_explorer_1.JobsHandlerExplorer],
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
@@ -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,qCAAkE;AAoB3D,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,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,EAAE,OAAO,EAAE,qBAAY,EAAE,WAAW,EAAE,OAAO,CAAC,oBAAoB,EAAE;SACrE,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAnBY,gCAAU;qBAAV,UAAU;IADtB,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,UAAU,CAmBtB;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,CAAC,GAAG,IAAI,EAAE,0BAAW,EAAE,kCAAW,EAAE,2CAAmB,CAAC;QACnE,OAAO,EAAE,CAAC,0BAAW,EAAE,kCAAW,EAAE,2CAAmB,CAAC;KACzD,CAAC;AACJ,CAAC"}
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