@azlib/scheduler 0.2.0 → 1.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/README.md +72 -91
- package/dist/index.cjs +375 -35
- package/dist/index.d.cts +48 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +48 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +375 -36
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -7
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SqlClientAdapter, SqlDialectAdapter } from "@azlib/persistence";
|
|
1
2
|
//#region src/contracts/scheduler-types.d.ts
|
|
2
3
|
type SchedulerMode = "standalone" | "embedded";
|
|
3
4
|
type ScheduleType = "once" | "cron";
|
|
@@ -74,6 +75,12 @@ interface SchedulerServiceOptions {
|
|
|
74
75
|
};
|
|
75
76
|
tickIntervalMs?: number;
|
|
76
77
|
maxDueJobsPerTick?: number;
|
|
78
|
+
persistence?: SchedulerPersistenceConfig;
|
|
79
|
+
}
|
|
80
|
+
interface SchedulerPersistenceConfig {
|
|
81
|
+
client: SqlClientAdapter;
|
|
82
|
+
dialect: SqlDialectAdapter;
|
|
83
|
+
namespace?: string;
|
|
77
84
|
}
|
|
78
85
|
interface SchedulerHandlerRegistry {
|
|
79
86
|
register<TConfig>(handlerKey: string, handler: (input: TConfig) => Promise<void>): void;
|
|
@@ -162,11 +169,51 @@ interface SchedulerDashboardService {
|
|
|
162
169
|
}
|
|
163
170
|
declare function createSchedulerDashboardService(scheduler: SchedulerService): SchedulerDashboardService;
|
|
164
171
|
//#endregion
|
|
172
|
+
//#region src/core/job-execution-store.d.ts
|
|
173
|
+
interface JobExecutionStore {
|
|
174
|
+
create(record: SchedulerExecutionRecord): Promise<SchedulerExecutionRecord>;
|
|
175
|
+
update(executionId: string, patch: Partial<Omit<SchedulerExecutionRecord, "executionId">>): Promise<SchedulerExecutionRecord | null>;
|
|
176
|
+
listByJob(jobId: string): Promise<SchedulerExecutionRecord[]>;
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/core/schedule-cursor-store.d.ts
|
|
180
|
+
interface ScheduleCursorRecord {
|
|
181
|
+
jobId: string;
|
|
182
|
+
lastEvaluatedAt: string;
|
|
183
|
+
lastTriggeredAt?: string;
|
|
184
|
+
nextRunAt: string;
|
|
185
|
+
version: number;
|
|
186
|
+
}
|
|
187
|
+
interface ScheduleCursorStore {
|
|
188
|
+
set(jobId: string, nextRunAt: string, now: string): Promise<ScheduleCursorRecord>;
|
|
189
|
+
markTriggered(jobId: string, triggeredAt: string): Promise<ScheduleCursorRecord | null>;
|
|
190
|
+
get(jobId: string): Promise<ScheduleCursorRecord | null>;
|
|
191
|
+
remove(jobId: string): Promise<boolean>;
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/core/scheduler-job-store.d.ts
|
|
195
|
+
interface SchedulerJobStore {
|
|
196
|
+
create(jobId: string, definition: SchedulerJobDefinition): Promise<SchedulerJobRecord>;
|
|
197
|
+
update(jobId: string, patch: Partial<SchedulerJobDefinition>): Promise<SchedulerJobRecord | null>;
|
|
198
|
+
setEnabled(jobId: string, enabled: boolean): Promise<SchedulerJobRecord | null>;
|
|
199
|
+
get(jobId: string): Promise<SchedulerJobRecord | null>;
|
|
200
|
+
list(): Promise<SchedulerJobRecord[]>;
|
|
201
|
+
remove(jobId: string): Promise<boolean>;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/core/persistence.d.ts
|
|
205
|
+
interface SchedulerPersistence {
|
|
206
|
+
jobStore: SchedulerJobStore;
|
|
207
|
+
cursorStore: ScheduleCursorStore;
|
|
208
|
+
executionStore: JobExecutionStore;
|
|
209
|
+
}
|
|
210
|
+
declare function createSchedulerPersistence(config: SchedulerPersistenceConfig): SchedulerPersistence;
|
|
211
|
+
//#endregion
|
|
165
212
|
//#region index.d.ts
|
|
166
213
|
declare function createSchedulerService(options: SchedulerServiceOptions): {
|
|
167
214
|
service: SchedulerService;
|
|
168
215
|
handlers: SchedulerHandlerRegistry;
|
|
169
216
|
};
|
|
170
217
|
//#endregion
|
|
171
|
-
export { CronWeekday, type CronWeekdayValue, type SchedulerHandlerRegistry, type SchedulerHostAdapter, type SchedulerJobDefinition, type SchedulerJobRecord, type SchedulerMissedRunPolicy, type SchedulerMode, type SchedulerOverlapPolicy, type SchedulerScheduleConfig, type SchedulerService, type SchedulerServiceOptions, bindSchedulerToHost, createCronExpression, createSchedulerDashboardService, createSchedulerService };
|
|
218
|
+
export { CronWeekday, type CronWeekdayValue, type SchedulerHandlerRegistry, type SchedulerHostAdapter, type SchedulerJobDefinition, type SchedulerJobRecord, type SchedulerMissedRunPolicy, type SchedulerMode, type SchedulerOverlapPolicy, type SchedulerPersistenceConfig, type SchedulerScheduleConfig, type SchedulerService, type SchedulerServiceOptions, bindSchedulerToHost, createCronExpression, createSchedulerDashboardService, createSchedulerPersistence, createSchedulerService };
|
|
172
219
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/contracts/scheduler-types.ts","../src/contracts/scheduler-host.ts","../src/core/scheduler-host-binding.ts","../src/core/cron-expression-builder.ts","../src/dashboard/queue-queries.ts","../src/dashboard/queue-dashboard-service.ts","../index.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/contracts/scheduler-types.ts","../src/contracts/scheduler-host.ts","../src/core/scheduler-host-binding.ts","../src/core/cron-expression-builder.ts","../src/dashboard/queue-queries.ts","../src/dashboard/queue-dashboard-service.ts","../src/core/job-execution-store.ts","../src/core/schedule-cursor-store.ts","../src/core/scheduler-job-store.ts","../src/core/persistence.ts","../index.ts"],"mappings":";;KAEY;KAEA;KAEA;KAEA;UAEK;EACf,cAAc;EACd;EACA;EACA,gBAAgB;EAChB,kBAAkB;;UAGH,uBAAuB;EACtC;EACA;EACA,UAAU;EACV,QAAQ;EACR;;UAGe,mBAAmB,2BAA2B,uBAAuB;EACpF;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA,SAAS;EACT;EACA;EACA;;UAGe;EACf,YAAY,SAAS,KAAK,uBAAuB,WAAW;IAAU;;EACtE,UAAU,SAAS,eAAe,OAAO,QAAQ,uBAAuB,YAAY;EACpF,SAAS,gBAAgB;EACzB,UAAU,gBAAgB;EAC1B,UAAU,gBAAgB;EAC1B,YAAY,QAAQ;EACpB,eAAe,QAAQ,0BAA0B,QAAQ;EACzD,qBAAqB,sBAAsB;EAC3C,SAAS;EACT,QAAQ;;UAGO;EACf,MAAM;EACN;EACA,sCAAsC;EACtC,+BAA+B;EAC/B;IACE,MAAM,iBAAiB,OAAO;IAC9B,KAAK,iBAAiB,OAAO;IAC7B,KAAK,iBAAiB,OAAO;IAC7B,MAAM,iBAAiB,OAAO;;EAEhC;EACA;EACA,cAAc;;UAGC;EACf,QAAQ;EACR,SAAS;EACT;;UAGe;EACf,SAAS,SAAS,oBAAoB,UAAU,OAAO,YAAY;EACnE,QAAQ,uBAAuB,mBAAmB;EAClD,IAAI;;;;UClGW;EACf,QAAQ,gBAAgB;EACxB,OAAO,gBAAgB;;;;iBCCT,oBACd,WAAW,kBACX,MAAM;;;cCLK;;;;;;;;;KAUD,2BAA2B,0BAA0B;cAyBpD;UACH;UACA;UACA;UACA;UACA;EAER;EAKA,cAAc;EAMd,SAAS;EAMT,UAAU;EASV;EAKA,YAAY;EAMZ,OAAO;EAMP,QAAQ;EASR,aAAa;EAMb,cAAc;EASd,kBAAkB;EAMlB;EAKA,QAAQ;EAMR,SAAS;EAST,aAAa;EAMb,UAAU,SAAS;EAMnB,WAAW,UAAU;EASrB,eAAe;EAMf;EAKA;EAKA,QAAQ,cAAc;EAStB,SAAS,SAAS,kBAAkB,eAAU;EAS9C,UAAU,oBAAoB,eAAU;EASxC;EAIA;;iBAKc,wBAAwB;;;UC9MvB;EACf;EACA;EACA;EACA;;iBAGoB,qBACpB,WAAW,mBACV,QAAQ;iBAYW,oBAAoB,WAAW,mBAAgB;cAAhB;;;;;;;;;UCpBpC;EACf,aAAa,kBAAkB;EAC/B,aAAa,kBAAkB;EAC/B,SAAS,gBAAgB;EACzB,UAAU,gBAAgB;EAC1B,eAAe,sBAAsB;;iBAGvB,gCACd,WAAW,mBACV;;;UCXc;EACf,OAAO,QAAQ,2BAA2B,QAAQ;EAClD,OACE,qBACA,OAAO,QAAQ,KAAK,4CACnB,QAAQ;EACX,UAAU,gBAAgB,QAAQ;;;;UCRnB;EACf;EACA;EACA;EACA;EACA;;UAGe;EACf,IAAI,eAAe,mBAAmB,cAAc,QAAQ;EAC5D,cAAc,eAAe,sBAAsB,QAAQ;EAC3D,IAAI,gBAAgB,QAAQ;EAC5B,OAAO,gBAAgB;;;;UCVR;EACf,OAAO,eAAe,YAAY,yBAAyB,QAAQ;EACnE,OAAO,eAAe,OAAO,QAAQ,0BAA0B,QAAQ;EACvE,WAAW,eAAe,mBAAmB,QAAQ;EACrD,IAAI,gBAAgB,QAAQ;EAC5B,QAAQ,QAAQ;EAChB,OAAO,gBAAgB;;;;UCgLR;EACf,UAAU;EACV,aAAa;EACb,gBAAgB;;iBAGF,2BAA2B,QAAQ,6BAA6B;;;iBC/JhE,uBAAuB,SAAS;EAC9C,SAAS;EACT,UAAU"}
|
package/dist/index.mjs
CHANGED
|
@@ -175,11 +175,11 @@ function createCronExpression() {
|
|
|
175
175
|
function createInMemoryJobExecutionStore() {
|
|
176
176
|
const byExecution = /* @__PURE__ */ new Map();
|
|
177
177
|
return {
|
|
178
|
-
create(record) {
|
|
178
|
+
async create(record) {
|
|
179
179
|
byExecution.set(record.executionId, record);
|
|
180
180
|
return record;
|
|
181
181
|
},
|
|
182
|
-
update(executionId, patch) {
|
|
182
|
+
async update(executionId, patch) {
|
|
183
183
|
const current = byExecution.get(executionId);
|
|
184
184
|
if (!current) return null;
|
|
185
185
|
const next = {
|
|
@@ -189,7 +189,7 @@ function createInMemoryJobExecutionStore() {
|
|
|
189
189
|
byExecution.set(executionId, next);
|
|
190
190
|
return next;
|
|
191
191
|
},
|
|
192
|
-
listByJob(jobId) {
|
|
192
|
+
async listByJob(jobId) {
|
|
193
193
|
return Array.from(byExecution.values()).filter((item) => item.jobId === jobId);
|
|
194
194
|
}
|
|
195
195
|
};
|
|
@@ -199,7 +199,7 @@ function createInMemoryJobExecutionStore() {
|
|
|
199
199
|
function createInMemoryScheduleCursorStore() {
|
|
200
200
|
const cursors = /* @__PURE__ */ new Map();
|
|
201
201
|
return {
|
|
202
|
-
set(jobId, nextRunAt, now) {
|
|
202
|
+
async set(jobId, nextRunAt, now) {
|
|
203
203
|
const previous = cursors.get(jobId);
|
|
204
204
|
const next = {
|
|
205
205
|
jobId,
|
|
@@ -211,7 +211,7 @@ function createInMemoryScheduleCursorStore() {
|
|
|
211
211
|
cursors.set(jobId, next);
|
|
212
212
|
return next;
|
|
213
213
|
},
|
|
214
|
-
markTriggered(jobId, triggeredAt) {
|
|
214
|
+
async markTriggered(jobId, triggeredAt) {
|
|
215
215
|
const current = cursors.get(jobId);
|
|
216
216
|
if (!current) return null;
|
|
217
217
|
const next = {
|
|
@@ -222,10 +222,10 @@ function createInMemoryScheduleCursorStore() {
|
|
|
222
222
|
cursors.set(jobId, next);
|
|
223
223
|
return next;
|
|
224
224
|
},
|
|
225
|
-
get(jobId) {
|
|
225
|
+
async get(jobId) {
|
|
226
226
|
return cursors.get(jobId) ?? null;
|
|
227
227
|
},
|
|
228
|
-
remove(jobId) {
|
|
228
|
+
async remove(jobId) {
|
|
229
229
|
return cursors.delete(jobId);
|
|
230
230
|
}
|
|
231
231
|
};
|
|
@@ -284,17 +284,17 @@ function createSchedulerEngine(dependencies) {
|
|
|
284
284
|
if (cached?.status === "hit" && cached.value) return cached.value;
|
|
285
285
|
const nextRunAt = computeNextRunAt(parseSchedule(job.schedule.scheduleType, job.schedule.expression, job.schedule.timezone), now);
|
|
286
286
|
await dependencies.cache?.set(cacheKey, nextRunAt);
|
|
287
|
-
dependencies.cursorStore.set(job.jobId, nextRunAt, now.toISOString());
|
|
287
|
+
await dependencies.cursorStore.set(job.jobId, nextRunAt, now.toISOString());
|
|
288
288
|
return nextRunAt;
|
|
289
289
|
}
|
|
290
290
|
async function evaluateTick() {
|
|
291
291
|
const now = /* @__PURE__ */ new Date();
|
|
292
|
-
const candidates = dependencies.jobStore.list().filter((job) => job.enabled).slice(0, dependencies.maxDueJobsPerTick);
|
|
292
|
+
const candidates = (await dependencies.jobStore.list()).filter((job) => job.enabled).slice(0, dependencies.maxDueJobsPerTick);
|
|
293
293
|
for (const job of candidates) {
|
|
294
294
|
const nextRunAt = await computeNextRun(job, now);
|
|
295
295
|
if (new Date(nextRunAt).getTime() > now.getTime()) continue;
|
|
296
296
|
const executionId = `exec_${randomUUID()}`;
|
|
297
|
-
dependencies.executionStore.create({
|
|
297
|
+
await dependencies.executionStore.create({
|
|
298
298
|
executionId,
|
|
299
299
|
jobId: job.jobId,
|
|
300
300
|
scheduledFor: nextRunAt,
|
|
@@ -302,7 +302,7 @@ function createSchedulerEngine(dependencies) {
|
|
|
302
302
|
status: "queued",
|
|
303
303
|
attemptCount: 0
|
|
304
304
|
});
|
|
305
|
-
dependencies.cursorStore.markTriggered(job.jobId, now.toISOString());
|
|
305
|
+
await dependencies.cursorStore.markTriggered(job.jobId, now.toISOString());
|
|
306
306
|
if (dependencies.queueService) await dependencies.queueService.enqueue({
|
|
307
307
|
idempotencyKey: `scheduler:${job.jobId}:${nextRunAt}`,
|
|
308
308
|
payloadRef: {
|
|
@@ -377,7 +377,7 @@ function createSchedulerHandlerRegistry() {
|
|
|
377
377
|
function createInMemorySchedulerJobStore() {
|
|
378
378
|
const jobs = /* @__PURE__ */ new Map();
|
|
379
379
|
return {
|
|
380
|
-
create(jobId, definition) {
|
|
380
|
+
async create(jobId, definition) {
|
|
381
381
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
382
382
|
const created = {
|
|
383
383
|
...definition,
|
|
@@ -389,7 +389,7 @@ function createInMemorySchedulerJobStore() {
|
|
|
389
389
|
jobs.set(jobId, created);
|
|
390
390
|
return created;
|
|
391
391
|
},
|
|
392
|
-
update(jobId, patch) {
|
|
392
|
+
async update(jobId, patch) {
|
|
393
393
|
const current = jobs.get(jobId);
|
|
394
394
|
if (!current) return null;
|
|
395
395
|
const updated = {
|
|
@@ -400,7 +400,7 @@ function createInMemorySchedulerJobStore() {
|
|
|
400
400
|
jobs.set(jobId, updated);
|
|
401
401
|
return updated;
|
|
402
402
|
},
|
|
403
|
-
setEnabled(jobId, enabled) {
|
|
403
|
+
async setEnabled(jobId, enabled) {
|
|
404
404
|
const current = jobs.get(jobId);
|
|
405
405
|
if (!current) return null;
|
|
406
406
|
const updated = {
|
|
@@ -411,13 +411,13 @@ function createInMemorySchedulerJobStore() {
|
|
|
411
411
|
jobs.set(jobId, updated);
|
|
412
412
|
return updated;
|
|
413
413
|
},
|
|
414
|
-
get(jobId) {
|
|
414
|
+
async get(jobId) {
|
|
415
415
|
return jobs.get(jobId) ?? null;
|
|
416
416
|
},
|
|
417
|
-
list() {
|
|
417
|
+
async list() {
|
|
418
418
|
return Array.from(jobs.values());
|
|
419
419
|
},
|
|
420
|
-
remove(jobId) {
|
|
420
|
+
async remove(jobId) {
|
|
421
421
|
return jobs.delete(jobId);
|
|
422
422
|
}
|
|
423
423
|
};
|
|
@@ -478,7 +478,7 @@ function filterExecutions(items, query) {
|
|
|
478
478
|
}
|
|
479
479
|
//#endregion
|
|
480
480
|
//#region src/core/scheduler-service-retry.ts
|
|
481
|
-
function retryFailedExecutionById(allExecutions, executionStore, executionId) {
|
|
481
|
+
async function retryFailedExecutionById(allExecutions, executionStore, executionId) {
|
|
482
482
|
const target = allExecutions.find((item) => item.executionId === executionId);
|
|
483
483
|
if (!target) throw new Error(`Execution not found: ${executionId}`);
|
|
484
484
|
if (target.status !== "failed" && target.status !== "dead-letter") throw new Error("Only failed or dead-letter executions can be retried");
|
|
@@ -490,18 +490,354 @@ function retryFailedExecutionById(allExecutions, executionStore, executionId) {
|
|
|
490
490
|
attemptCount: target.attemptCount + 1,
|
|
491
491
|
triggeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
492
492
|
};
|
|
493
|
-
executionStore.create(retry);
|
|
493
|
+
await executionStore.create(retry);
|
|
494
494
|
return retry;
|
|
495
495
|
}
|
|
496
496
|
//#endregion
|
|
497
|
+
//#region src/core/persistence.ts
|
|
498
|
+
function namespaceFor(config) {
|
|
499
|
+
return config.namespace?.trim() || "azlib";
|
|
500
|
+
}
|
|
501
|
+
function tableNames(namespace) {
|
|
502
|
+
return {
|
|
503
|
+
jobs: `${namespace}__scheduler_jobs`,
|
|
504
|
+
cursors: `${namespace}__scheduler_cursors`,
|
|
505
|
+
executions: `${namespace}__scheduler_executions`
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function quoted(dialect, tableName) {
|
|
509
|
+
return dialect.quoteIdentifier(tableName);
|
|
510
|
+
}
|
|
511
|
+
async function bootstrapSchedulerPersistence(client, dialect, names) {
|
|
512
|
+
await client.transaction(async (tx) => {
|
|
513
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS ${quoted(dialect, names.jobs)} (
|
|
514
|
+
job_id TEXT PRIMARY KEY,
|
|
515
|
+
name TEXT NOT NULL,
|
|
516
|
+
handler_key TEXT NOT NULL,
|
|
517
|
+
schedule_type TEXT NOT NULL,
|
|
518
|
+
expression TEXT NOT NULL,
|
|
519
|
+
timezone TEXT NOT NULL,
|
|
520
|
+
overlap_policy TEXT,
|
|
521
|
+
missed_run_policy TEXT,
|
|
522
|
+
config_json TEXT NOT NULL,
|
|
523
|
+
enabled INTEGER NOT NULL,
|
|
524
|
+
created_at TEXT NOT NULL,
|
|
525
|
+
updated_at TEXT NOT NULL
|
|
526
|
+
)`);
|
|
527
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS ${quoted(dialect, names.cursors)} (
|
|
528
|
+
job_id TEXT PRIMARY KEY,
|
|
529
|
+
last_evaluated_at TEXT NOT NULL,
|
|
530
|
+
last_triggered_at TEXT,
|
|
531
|
+
next_run_at TEXT NOT NULL,
|
|
532
|
+
version INTEGER NOT NULL
|
|
533
|
+
)`);
|
|
534
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS ${quoted(dialect, names.executions)} (
|
|
535
|
+
execution_id TEXT PRIMARY KEY,
|
|
536
|
+
job_id TEXT NOT NULL,
|
|
537
|
+
scheduled_for TEXT NOT NULL,
|
|
538
|
+
triggered_at TEXT NOT NULL,
|
|
539
|
+
status TEXT NOT NULL,
|
|
540
|
+
queue_task_id TEXT,
|
|
541
|
+
attempt_count INTEGER NOT NULL,
|
|
542
|
+
last_error TEXT
|
|
543
|
+
)`);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function toJobRow(jobId, definition) {
|
|
547
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
548
|
+
return {
|
|
549
|
+
jobId,
|
|
550
|
+
name: definition.name,
|
|
551
|
+
handlerKey: definition.handlerKey,
|
|
552
|
+
scheduleType: definition.schedule.scheduleType,
|
|
553
|
+
expression: definition.schedule.expression,
|
|
554
|
+
timezone: definition.schedule.timezone,
|
|
555
|
+
overlapPolicy: definition.schedule.overlapPolicy ?? null,
|
|
556
|
+
missedRunPolicy: definition.schedule.missedRunPolicy ?? null,
|
|
557
|
+
configJson: JSON.stringify(definition.config),
|
|
558
|
+
enabled: definition.enabled ?? true ? 1 : 0,
|
|
559
|
+
createdAt: now,
|
|
560
|
+
updatedAt: now
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function rowToJobRecord(row) {
|
|
564
|
+
return {
|
|
565
|
+
jobId: row.jobId,
|
|
566
|
+
name: row.name,
|
|
567
|
+
handlerKey: row.handlerKey,
|
|
568
|
+
schedule: {
|
|
569
|
+
scheduleType: row.scheduleType,
|
|
570
|
+
expression: row.expression,
|
|
571
|
+
timezone: row.timezone,
|
|
572
|
+
overlapPolicy: row.overlapPolicy === null ? void 0 : row.overlapPolicy,
|
|
573
|
+
missedRunPolicy: row.missedRunPolicy === null ? void 0 : row.missedRunPolicy
|
|
574
|
+
},
|
|
575
|
+
config: JSON.parse(row.configJson),
|
|
576
|
+
enabled: Boolean(row.enabled),
|
|
577
|
+
createdAt: row.createdAt,
|
|
578
|
+
updatedAt: row.updatedAt
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function readJobRow(client, dialect, names, jobId) {
|
|
582
|
+
const rows = await client.query(`SELECT job_id AS jobId, name, handler_key AS handlerKey, schedule_type AS scheduleType, expression, timezone, overlap_policy AS overlapPolicy, missed_run_policy AS missedRunPolicy, config_json AS configJson, enabled, created_at AS createdAt, updated_at AS updatedAt
|
|
583
|
+
FROM ${quoted(dialect, names.jobs)} WHERE job_id = ? LIMIT 1`, [jobId]);
|
|
584
|
+
return rows[0] ? rowToJobRecord(rows[0]) : null;
|
|
585
|
+
}
|
|
586
|
+
async function readCursorRow(client, dialect, names, jobId) {
|
|
587
|
+
const row = (await client.query(`SELECT job_id AS jobId, last_evaluated_at AS lastEvaluatedAt, last_triggered_at AS lastTriggeredAt, next_run_at AS nextRunAt, version
|
|
588
|
+
FROM ${quoted(dialect, names.cursors)} WHERE job_id = ? LIMIT 1`, [jobId]))[0];
|
|
589
|
+
return row ? {
|
|
590
|
+
jobId: row.jobId,
|
|
591
|
+
lastEvaluatedAt: row.lastEvaluatedAt,
|
|
592
|
+
lastTriggeredAt: row.lastTriggeredAt ?? void 0,
|
|
593
|
+
nextRunAt: row.nextRunAt,
|
|
594
|
+
version: row.version
|
|
595
|
+
} : null;
|
|
596
|
+
}
|
|
597
|
+
async function readExecutionRow(client, dialect, names, executionId) {
|
|
598
|
+
const row = (await client.query(`SELECT execution_id AS executionId, job_id AS jobId, scheduled_for AS scheduledFor, triggered_at AS triggeredAt, status, queue_task_id AS queueTaskId, attempt_count AS attemptCount, last_error AS lastError
|
|
599
|
+
FROM ${quoted(dialect, names.executions)} WHERE execution_id = ? LIMIT 1`, [executionId]))[0];
|
|
600
|
+
return row ? {
|
|
601
|
+
executionId: row.executionId,
|
|
602
|
+
jobId: row.jobId,
|
|
603
|
+
scheduledFor: row.scheduledFor,
|
|
604
|
+
triggeredAt: row.triggeredAt,
|
|
605
|
+
status: row.status,
|
|
606
|
+
queueTaskId: row.queueTaskId ?? void 0,
|
|
607
|
+
attemptCount: row.attemptCount,
|
|
608
|
+
lastError: row.lastError ?? void 0
|
|
609
|
+
} : null;
|
|
610
|
+
}
|
|
611
|
+
function createSchedulerPersistence(config) {
|
|
612
|
+
const names = tableNames(namespaceFor(config));
|
|
613
|
+
let bootstrapPromise = null;
|
|
614
|
+
async function ensureBootstrapped() {
|
|
615
|
+
if (!bootstrapPromise) bootstrapPromise = bootstrapSchedulerPersistence(config.client, config.dialect, names);
|
|
616
|
+
await bootstrapPromise;
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
jobStore: {
|
|
620
|
+
async create(jobId, definition) {
|
|
621
|
+
await ensureBootstrapped();
|
|
622
|
+
const row = toJobRow(jobId, definition);
|
|
623
|
+
await config.client.transaction(async (tx) => {
|
|
624
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
625
|
+
await tx.execute(`INSERT INTO ${quoted(config.dialect, names.jobs)} (job_id, name, handler_key, schedule_type, expression, timezone, overlap_policy, missed_run_policy, config_json, enabled, created_at, updated_at)
|
|
626
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
627
|
+
row.jobId,
|
|
628
|
+
row.name,
|
|
629
|
+
row.handlerKey,
|
|
630
|
+
row.scheduleType,
|
|
631
|
+
row.expression,
|
|
632
|
+
row.timezone,
|
|
633
|
+
row.overlapPolicy,
|
|
634
|
+
row.missedRunPolicy,
|
|
635
|
+
row.configJson,
|
|
636
|
+
row.enabled,
|
|
637
|
+
row.createdAt,
|
|
638
|
+
row.updatedAt
|
|
639
|
+
]);
|
|
640
|
+
});
|
|
641
|
+
return rowToJobRecord(row);
|
|
642
|
+
},
|
|
643
|
+
async update(jobId, patch) {
|
|
644
|
+
const current = await readJobRow(config.client, config.dialect, names, jobId);
|
|
645
|
+
if (!current) return null;
|
|
646
|
+
const next = {
|
|
647
|
+
...current,
|
|
648
|
+
...patch,
|
|
649
|
+
schedule: {
|
|
650
|
+
...current.schedule,
|
|
651
|
+
...patch.schedule
|
|
652
|
+
},
|
|
653
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
654
|
+
};
|
|
655
|
+
await config.client.transaction(async (tx) => {
|
|
656
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
657
|
+
await tx.execute(`INSERT INTO ${quoted(config.dialect, names.jobs)} (job_id, name, handler_key, schedule_type, expression, timezone, overlap_policy, missed_run_policy, config_json, enabled, created_at, updated_at)
|
|
658
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
659
|
+
next.jobId,
|
|
660
|
+
next.name,
|
|
661
|
+
next.handlerKey,
|
|
662
|
+
next.schedule.scheduleType,
|
|
663
|
+
next.schedule.expression,
|
|
664
|
+
next.schedule.timezone,
|
|
665
|
+
next.schedule.overlapPolicy ?? null,
|
|
666
|
+
next.schedule.missedRunPolicy ?? null,
|
|
667
|
+
JSON.stringify(next.config),
|
|
668
|
+
next.enabled ? 1 : 0,
|
|
669
|
+
next.createdAt,
|
|
670
|
+
next.updatedAt
|
|
671
|
+
]);
|
|
672
|
+
});
|
|
673
|
+
return next;
|
|
674
|
+
},
|
|
675
|
+
async setEnabled(jobId, enabled) {
|
|
676
|
+
const current = await readJobRow(config.client, config.dialect, names, jobId);
|
|
677
|
+
if (!current) return null;
|
|
678
|
+
const next = {
|
|
679
|
+
...current,
|
|
680
|
+
enabled,
|
|
681
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
682
|
+
};
|
|
683
|
+
await config.client.transaction(async (tx) => {
|
|
684
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
685
|
+
await tx.execute(`INSERT INTO ${quoted(config.dialect, names.jobs)} (job_id, name, handler_key, schedule_type, expression, timezone, overlap_policy, missed_run_policy, config_json, enabled, created_at, updated_at)
|
|
686
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
687
|
+
next.jobId,
|
|
688
|
+
next.name,
|
|
689
|
+
next.handlerKey,
|
|
690
|
+
next.schedule.scheduleType,
|
|
691
|
+
next.schedule.expression,
|
|
692
|
+
next.schedule.timezone,
|
|
693
|
+
next.schedule.overlapPolicy ?? null,
|
|
694
|
+
next.schedule.missedRunPolicy ?? null,
|
|
695
|
+
JSON.stringify(next.config),
|
|
696
|
+
next.enabled ? 1 : 0,
|
|
697
|
+
next.createdAt,
|
|
698
|
+
next.updatedAt
|
|
699
|
+
]);
|
|
700
|
+
});
|
|
701
|
+
return next;
|
|
702
|
+
},
|
|
703
|
+
async get(jobId) {
|
|
704
|
+
await ensureBootstrapped();
|
|
705
|
+
return readJobRow(config.client, config.dialect, names, jobId);
|
|
706
|
+
},
|
|
707
|
+
async list() {
|
|
708
|
+
await ensureBootstrapped();
|
|
709
|
+
return (await config.client.query(`SELECT job_id AS jobId, name, handler_key AS handlerKey, schedule_type AS scheduleType, expression, timezone, overlap_policy AS overlapPolicy, missed_run_policy AS missedRunPolicy, config_json AS configJson, enabled, created_at AS createdAt, updated_at AS updatedAt
|
|
710
|
+
FROM ${quoted(config.dialect, names.jobs)} ORDER BY created_at ASC`, [])).map(rowToJobRecord);
|
|
711
|
+
},
|
|
712
|
+
async remove(jobId) {
|
|
713
|
+
await ensureBootstrapped();
|
|
714
|
+
await config.client.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
cursorStore: {
|
|
719
|
+
async set(jobId, nextRunAt, now) {
|
|
720
|
+
await ensureBootstrapped();
|
|
721
|
+
const current = await readCursorRow(config.client, config.dialect, names, jobId);
|
|
722
|
+
const next = {
|
|
723
|
+
jobId,
|
|
724
|
+
lastEvaluatedAt: now,
|
|
725
|
+
lastTriggeredAt: current?.lastTriggeredAt ?? null,
|
|
726
|
+
nextRunAt,
|
|
727
|
+
version: (current?.version ?? 0) + 1
|
|
728
|
+
};
|
|
729
|
+
await config.client.transaction(async (tx) => {
|
|
730
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.cursors)} WHERE job_id = ?`, [jobId]);
|
|
731
|
+
await tx.execute(`INSERT INTO ${quoted(config.dialect, names.cursors)} (job_id, last_evaluated_at, last_triggered_at, next_run_at, version) VALUES (?, ?, ?, ?, ?)`, [
|
|
732
|
+
next.jobId,
|
|
733
|
+
next.lastEvaluatedAt,
|
|
734
|
+
next.lastTriggeredAt,
|
|
735
|
+
next.nextRunAt,
|
|
736
|
+
next.version
|
|
737
|
+
]);
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
jobId: next.jobId,
|
|
741
|
+
lastEvaluatedAt: next.lastEvaluatedAt,
|
|
742
|
+
lastTriggeredAt: next.lastTriggeredAt ?? void 0,
|
|
743
|
+
nextRunAt: next.nextRunAt,
|
|
744
|
+
version: next.version
|
|
745
|
+
};
|
|
746
|
+
},
|
|
747
|
+
async markTriggered(jobId, triggeredAt) {
|
|
748
|
+
await ensureBootstrapped();
|
|
749
|
+
const current = await readCursorRow(config.client, config.dialect, names, jobId);
|
|
750
|
+
if (!current) return null;
|
|
751
|
+
const next = {
|
|
752
|
+
...current,
|
|
753
|
+
lastTriggeredAt: triggeredAt,
|
|
754
|
+
version: current.version + 1
|
|
755
|
+
};
|
|
756
|
+
await config.client.execute(`UPDATE ${quoted(config.dialect, names.cursors)} SET last_triggered_at = ?, version = ? WHERE job_id = ?`, [
|
|
757
|
+
next.lastTriggeredAt,
|
|
758
|
+
next.version,
|
|
759
|
+
jobId
|
|
760
|
+
]);
|
|
761
|
+
return {
|
|
762
|
+
jobId: next.jobId,
|
|
763
|
+
lastEvaluatedAt: next.lastEvaluatedAt,
|
|
764
|
+
lastTriggeredAt: next.lastTriggeredAt ?? void 0,
|
|
765
|
+
nextRunAt: next.nextRunAt,
|
|
766
|
+
version: next.version
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
async get(jobId) {
|
|
770
|
+
await ensureBootstrapped();
|
|
771
|
+
return readCursorRow(config.client, config.dialect, names, jobId);
|
|
772
|
+
},
|
|
773
|
+
async remove(jobId) {
|
|
774
|
+
await ensureBootstrapped();
|
|
775
|
+
await config.client.execute(`DELETE FROM ${quoted(config.dialect, names.cursors)} WHERE job_id = ?`, [jobId]);
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
executionStore: {
|
|
780
|
+
async create(record) {
|
|
781
|
+
await ensureBootstrapped();
|
|
782
|
+
await config.client.execute(`INSERT INTO ${quoted(config.dialect, names.executions)} (execution_id, job_id, scheduled_for, triggered_at, status, queue_task_id, attempt_count, last_error)
|
|
783
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
784
|
+
record.executionId,
|
|
785
|
+
record.jobId,
|
|
786
|
+
record.scheduledFor,
|
|
787
|
+
record.triggeredAt,
|
|
788
|
+
record.status,
|
|
789
|
+
record.queueTaskId ?? null,
|
|
790
|
+
record.attemptCount,
|
|
791
|
+
record.lastError ?? null
|
|
792
|
+
]);
|
|
793
|
+
return record;
|
|
794
|
+
},
|
|
795
|
+
async update(executionId, patch) {
|
|
796
|
+
const current = await readExecutionRow(config.client, config.dialect, names, executionId);
|
|
797
|
+
if (!current) return null;
|
|
798
|
+
const next = {
|
|
799
|
+
...current,
|
|
800
|
+
...patch
|
|
801
|
+
};
|
|
802
|
+
await config.client.execute(`UPDATE ${quoted(config.dialect, names.executions)} SET job_id = ?, scheduled_for = ?, triggered_at = ?, status = ?, queue_task_id = ?, attempt_count = ?, last_error = ? WHERE execution_id = ?`, [
|
|
803
|
+
next.jobId,
|
|
804
|
+
next.scheduledFor,
|
|
805
|
+
next.triggeredAt,
|
|
806
|
+
next.status,
|
|
807
|
+
next.queueTaskId ?? null,
|
|
808
|
+
next.attemptCount,
|
|
809
|
+
next.lastError ?? null,
|
|
810
|
+
executionId
|
|
811
|
+
]);
|
|
812
|
+
return next;
|
|
813
|
+
},
|
|
814
|
+
async listByJob(jobId) {
|
|
815
|
+
await ensureBootstrapped();
|
|
816
|
+
return (await config.client.query(`SELECT execution_id AS executionId, job_id AS jobId, scheduled_for AS scheduledFor, triggered_at AS triggeredAt, status, queue_task_id AS queueTaskId, attempt_count AS attemptCount, last_error AS lastError
|
|
817
|
+
FROM ${quoted(config.dialect, names.executions)} WHERE job_id = ? ORDER BY triggered_at ASC`, [jobId])).map((row) => ({
|
|
818
|
+
executionId: row.executionId,
|
|
819
|
+
jobId: row.jobId,
|
|
820
|
+
scheduledFor: row.scheduledFor,
|
|
821
|
+
triggeredAt: row.triggeredAt,
|
|
822
|
+
status: row.status,
|
|
823
|
+
queueTaskId: row.queueTaskId ?? void 0,
|
|
824
|
+
attemptCount: row.attemptCount,
|
|
825
|
+
lastError: row.lastError ?? void 0
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
//#endregion
|
|
497
832
|
//#region src/core/scheduler-service.ts
|
|
498
833
|
function createSchedulerService$1(options) {
|
|
499
834
|
assertRuntimeConfig(options);
|
|
500
835
|
const logger = options.logger ?? createSchedulerLogger();
|
|
501
836
|
const cache = options.cache ?? createSchedulerCache();
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
const
|
|
837
|
+
const persistence = options.persistence ? createSchedulerPersistence(options.persistence) : void 0;
|
|
838
|
+
const jobStore = persistence?.jobStore ?? createInMemorySchedulerJobStore();
|
|
839
|
+
const cursorStore = persistence?.cursorStore ?? createInMemoryScheduleCursorStore();
|
|
840
|
+
const executionStore = persistence?.executionStore ?? createInMemoryJobExecutionStore();
|
|
505
841
|
const events = createInMemorySchedulerEventBus();
|
|
506
842
|
const handlers = createSchedulerHandlerRegistry();
|
|
507
843
|
const queueService = options.queueService;
|
|
@@ -521,9 +857,9 @@ function createSchedulerService$1(options) {
|
|
|
521
857
|
async registerJob(job) {
|
|
522
858
|
if (!handlers.has(job.handlerKey)) throw new Error(`Unknown handlerKey: ${job.handlerKey}`);
|
|
523
859
|
const jobId = `job_${randomUUID()}`;
|
|
524
|
-
const created = jobStore.create(jobId, job);
|
|
860
|
+
const created = await jobStore.create(jobId, job);
|
|
525
861
|
const nextRunAt = await engine.computeNextRun(created);
|
|
526
|
-
cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
862
|
+
await cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
527
863
|
events.emit({
|
|
528
864
|
eventType: "job-created",
|
|
529
865
|
jobId,
|
|
@@ -532,32 +868,32 @@ function createSchedulerService$1(options) {
|
|
|
532
868
|
return { jobId };
|
|
533
869
|
},
|
|
534
870
|
async updateJob(jobId, patch) {
|
|
535
|
-
const updated = jobStore.update(jobId, patch);
|
|
871
|
+
const updated = await jobStore.update(jobId, patch);
|
|
536
872
|
if (!updated) throw new Error(`Job not found: ${jobId}`);
|
|
537
873
|
const nextRunAt = await engine.computeNextRun(updated);
|
|
538
|
-
cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
874
|
+
await cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
539
875
|
events.emit({
|
|
540
876
|
eventType: "job-updated",
|
|
541
877
|
jobId
|
|
542
878
|
});
|
|
543
879
|
},
|
|
544
880
|
async pauseJob(jobId) {
|
|
545
|
-
if (!jobStore.setEnabled(jobId, false)) throw new Error(`Job not found: ${jobId}`);
|
|
881
|
+
if (!await jobStore.setEnabled(jobId, false)) throw new Error(`Job not found: ${jobId}`);
|
|
546
882
|
events.emit({
|
|
547
883
|
eventType: "job-paused",
|
|
548
884
|
jobId
|
|
549
885
|
});
|
|
550
886
|
},
|
|
551
887
|
async resumeJob(jobId) {
|
|
552
|
-
if (!jobStore.setEnabled(jobId, true)) throw new Error(`Job not found: ${jobId}`);
|
|
888
|
+
if (!await jobStore.setEnabled(jobId, true)) throw new Error(`Job not found: ${jobId}`);
|
|
553
889
|
events.emit({
|
|
554
890
|
eventType: "job-resumed",
|
|
555
891
|
jobId
|
|
556
892
|
});
|
|
557
893
|
},
|
|
558
894
|
async deleteJob(jobId) {
|
|
559
|
-
const removed = jobStore.remove(jobId);
|
|
560
|
-
cursorStore.remove(jobId);
|
|
895
|
+
const removed = await jobStore.remove(jobId);
|
|
896
|
+
await cursorStore.remove(jobId);
|
|
561
897
|
if (!removed) throw new Error(`Job not found: ${jobId}`);
|
|
562
898
|
events.emit({
|
|
563
899
|
eventType: "job-deleted",
|
|
@@ -565,8 +901,9 @@ function createSchedulerService$1(options) {
|
|
|
565
901
|
});
|
|
566
902
|
},
|
|
567
903
|
async listJobs() {
|
|
568
|
-
|
|
569
|
-
|
|
904
|
+
const jobs = await jobStore.list();
|
|
905
|
+
return Promise.all(jobs.map(async (job) => {
|
|
906
|
+
const cursor = await cursorStore.get(job.jobId);
|
|
570
907
|
return toJobListItem({
|
|
571
908
|
jobId: job.jobId,
|
|
572
909
|
name: job.name,
|
|
@@ -574,13 +911,15 @@ function createSchedulerService$1(options) {
|
|
|
574
911
|
timezone: job.schedule.timezone,
|
|
575
912
|
nextRunAt: cursor?.nextRunAt
|
|
576
913
|
});
|
|
577
|
-
});
|
|
914
|
+
}));
|
|
578
915
|
},
|
|
579
916
|
async listExecutions(query) {
|
|
580
|
-
|
|
917
|
+
const jobs = await jobStore.list();
|
|
918
|
+
return filterExecutions((await Promise.all(jobs.map((job) => executionStore.listByJob(job.jobId)))).flat(), query);
|
|
581
919
|
},
|
|
582
920
|
async retryFailedExecution(executionId) {
|
|
583
|
-
const
|
|
921
|
+
const jobs = await jobStore.list();
|
|
922
|
+
const retry = await retryFailedExecutionById((await Promise.all(jobs.map((job) => executionStore.listByJob(job.jobId)))).flat(), executionStore, executionId);
|
|
584
923
|
events.emit({
|
|
585
924
|
eventType: "retry",
|
|
586
925
|
jobId: retry.jobId,
|
|
@@ -637,6 +976,6 @@ function createSchedulerService(options) {
|
|
|
637
976
|
return createSchedulerService$1(options);
|
|
638
977
|
}
|
|
639
978
|
//#endregion
|
|
640
|
-
export { CronWeekday, bindSchedulerToHost, createCronExpression, createSchedulerDashboardService, createSchedulerService };
|
|
979
|
+
export { CronWeekday, bindSchedulerToHost, createCronExpression, createSchedulerDashboardService, createSchedulerPersistence, createSchedulerService };
|
|
641
980
|
|
|
642
981
|
//# sourceMappingURL=index.mjs.map
|