@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.cjs
CHANGED
|
@@ -176,11 +176,11 @@ function createCronExpression() {
|
|
|
176
176
|
function createInMemoryJobExecutionStore() {
|
|
177
177
|
const byExecution = /* @__PURE__ */ new Map();
|
|
178
178
|
return {
|
|
179
|
-
create(record) {
|
|
179
|
+
async create(record) {
|
|
180
180
|
byExecution.set(record.executionId, record);
|
|
181
181
|
return record;
|
|
182
182
|
},
|
|
183
|
-
update(executionId, patch) {
|
|
183
|
+
async update(executionId, patch) {
|
|
184
184
|
const current = byExecution.get(executionId);
|
|
185
185
|
if (!current) return null;
|
|
186
186
|
const next = {
|
|
@@ -190,7 +190,7 @@ function createInMemoryJobExecutionStore() {
|
|
|
190
190
|
byExecution.set(executionId, next);
|
|
191
191
|
return next;
|
|
192
192
|
},
|
|
193
|
-
listByJob(jobId) {
|
|
193
|
+
async listByJob(jobId) {
|
|
194
194
|
return Array.from(byExecution.values()).filter((item) => item.jobId === jobId);
|
|
195
195
|
}
|
|
196
196
|
};
|
|
@@ -200,7 +200,7 @@ function createInMemoryJobExecutionStore() {
|
|
|
200
200
|
function createInMemoryScheduleCursorStore() {
|
|
201
201
|
const cursors = /* @__PURE__ */ new Map();
|
|
202
202
|
return {
|
|
203
|
-
set(jobId, nextRunAt, now) {
|
|
203
|
+
async set(jobId, nextRunAt, now) {
|
|
204
204
|
const previous = cursors.get(jobId);
|
|
205
205
|
const next = {
|
|
206
206
|
jobId,
|
|
@@ -212,7 +212,7 @@ function createInMemoryScheduleCursorStore() {
|
|
|
212
212
|
cursors.set(jobId, next);
|
|
213
213
|
return next;
|
|
214
214
|
},
|
|
215
|
-
markTriggered(jobId, triggeredAt) {
|
|
215
|
+
async markTriggered(jobId, triggeredAt) {
|
|
216
216
|
const current = cursors.get(jobId);
|
|
217
217
|
if (!current) return null;
|
|
218
218
|
const next = {
|
|
@@ -223,10 +223,10 @@ function createInMemoryScheduleCursorStore() {
|
|
|
223
223
|
cursors.set(jobId, next);
|
|
224
224
|
return next;
|
|
225
225
|
},
|
|
226
|
-
get(jobId) {
|
|
226
|
+
async get(jobId) {
|
|
227
227
|
return cursors.get(jobId) ?? null;
|
|
228
228
|
},
|
|
229
|
-
remove(jobId) {
|
|
229
|
+
async remove(jobId) {
|
|
230
230
|
return cursors.delete(jobId);
|
|
231
231
|
}
|
|
232
232
|
};
|
|
@@ -285,17 +285,17 @@ function createSchedulerEngine(dependencies) {
|
|
|
285
285
|
if (cached?.status === "hit" && cached.value) return cached.value;
|
|
286
286
|
const nextRunAt = computeNextRunAt(parseSchedule(job.schedule.scheduleType, job.schedule.expression, job.schedule.timezone), now);
|
|
287
287
|
await dependencies.cache?.set(cacheKey, nextRunAt);
|
|
288
|
-
dependencies.cursorStore.set(job.jobId, nextRunAt, now.toISOString());
|
|
288
|
+
await dependencies.cursorStore.set(job.jobId, nextRunAt, now.toISOString());
|
|
289
289
|
return nextRunAt;
|
|
290
290
|
}
|
|
291
291
|
async function evaluateTick() {
|
|
292
292
|
const now = /* @__PURE__ */ new Date();
|
|
293
|
-
const candidates = dependencies.jobStore.list().filter((job) => job.enabled).slice(0, dependencies.maxDueJobsPerTick);
|
|
293
|
+
const candidates = (await dependencies.jobStore.list()).filter((job) => job.enabled).slice(0, dependencies.maxDueJobsPerTick);
|
|
294
294
|
for (const job of candidates) {
|
|
295
295
|
const nextRunAt = await computeNextRun(job, now);
|
|
296
296
|
if (new Date(nextRunAt).getTime() > now.getTime()) continue;
|
|
297
297
|
const executionId = `exec_${(0, node_crypto.randomUUID)()}`;
|
|
298
|
-
dependencies.executionStore.create({
|
|
298
|
+
await dependencies.executionStore.create({
|
|
299
299
|
executionId,
|
|
300
300
|
jobId: job.jobId,
|
|
301
301
|
scheduledFor: nextRunAt,
|
|
@@ -303,7 +303,7 @@ function createSchedulerEngine(dependencies) {
|
|
|
303
303
|
status: "queued",
|
|
304
304
|
attemptCount: 0
|
|
305
305
|
});
|
|
306
|
-
dependencies.cursorStore.markTriggered(job.jobId, now.toISOString());
|
|
306
|
+
await dependencies.cursorStore.markTriggered(job.jobId, now.toISOString());
|
|
307
307
|
if (dependencies.queueService) await dependencies.queueService.enqueue({
|
|
308
308
|
idempotencyKey: `scheduler:${job.jobId}:${nextRunAt}`,
|
|
309
309
|
payloadRef: {
|
|
@@ -378,7 +378,7 @@ function createSchedulerHandlerRegistry() {
|
|
|
378
378
|
function createInMemorySchedulerJobStore() {
|
|
379
379
|
const jobs = /* @__PURE__ */ new Map();
|
|
380
380
|
return {
|
|
381
|
-
create(jobId, definition) {
|
|
381
|
+
async create(jobId, definition) {
|
|
382
382
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
383
383
|
const created = {
|
|
384
384
|
...definition,
|
|
@@ -390,7 +390,7 @@ function createInMemorySchedulerJobStore() {
|
|
|
390
390
|
jobs.set(jobId, created);
|
|
391
391
|
return created;
|
|
392
392
|
},
|
|
393
|
-
update(jobId, patch) {
|
|
393
|
+
async update(jobId, patch) {
|
|
394
394
|
const current = jobs.get(jobId);
|
|
395
395
|
if (!current) return null;
|
|
396
396
|
const updated = {
|
|
@@ -401,7 +401,7 @@ function createInMemorySchedulerJobStore() {
|
|
|
401
401
|
jobs.set(jobId, updated);
|
|
402
402
|
return updated;
|
|
403
403
|
},
|
|
404
|
-
setEnabled(jobId, enabled) {
|
|
404
|
+
async setEnabled(jobId, enabled) {
|
|
405
405
|
const current = jobs.get(jobId);
|
|
406
406
|
if (!current) return null;
|
|
407
407
|
const updated = {
|
|
@@ -412,13 +412,13 @@ function createInMemorySchedulerJobStore() {
|
|
|
412
412
|
jobs.set(jobId, updated);
|
|
413
413
|
return updated;
|
|
414
414
|
},
|
|
415
|
-
get(jobId) {
|
|
415
|
+
async get(jobId) {
|
|
416
416
|
return jobs.get(jobId) ?? null;
|
|
417
417
|
},
|
|
418
|
-
list() {
|
|
418
|
+
async list() {
|
|
419
419
|
return Array.from(jobs.values());
|
|
420
420
|
},
|
|
421
|
-
remove(jobId) {
|
|
421
|
+
async remove(jobId) {
|
|
422
422
|
return jobs.delete(jobId);
|
|
423
423
|
}
|
|
424
424
|
};
|
|
@@ -479,7 +479,7 @@ function filterExecutions(items, query) {
|
|
|
479
479
|
}
|
|
480
480
|
//#endregion
|
|
481
481
|
//#region src/core/scheduler-service-retry.ts
|
|
482
|
-
function retryFailedExecutionById(allExecutions, executionStore, executionId) {
|
|
482
|
+
async function retryFailedExecutionById(allExecutions, executionStore, executionId) {
|
|
483
483
|
const target = allExecutions.find((item) => item.executionId === executionId);
|
|
484
484
|
if (!target) throw new Error(`Execution not found: ${executionId}`);
|
|
485
485
|
if (target.status !== "failed" && target.status !== "dead-letter") throw new Error("Only failed or dead-letter executions can be retried");
|
|
@@ -491,18 +491,354 @@ function retryFailedExecutionById(allExecutions, executionStore, executionId) {
|
|
|
491
491
|
attemptCount: target.attemptCount + 1,
|
|
492
492
|
triggeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
493
493
|
};
|
|
494
|
-
executionStore.create(retry);
|
|
494
|
+
await executionStore.create(retry);
|
|
495
495
|
return retry;
|
|
496
496
|
}
|
|
497
497
|
//#endregion
|
|
498
|
+
//#region src/core/persistence.ts
|
|
499
|
+
function namespaceFor(config) {
|
|
500
|
+
return config.namespace?.trim() || "azlib";
|
|
501
|
+
}
|
|
502
|
+
function tableNames(namespace) {
|
|
503
|
+
return {
|
|
504
|
+
jobs: `${namespace}__scheduler_jobs`,
|
|
505
|
+
cursors: `${namespace}__scheduler_cursors`,
|
|
506
|
+
executions: `${namespace}__scheduler_executions`
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function quoted(dialect, tableName) {
|
|
510
|
+
return dialect.quoteIdentifier(tableName);
|
|
511
|
+
}
|
|
512
|
+
async function bootstrapSchedulerPersistence(client, dialect, names) {
|
|
513
|
+
await client.transaction(async (tx) => {
|
|
514
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS ${quoted(dialect, names.jobs)} (
|
|
515
|
+
job_id TEXT PRIMARY KEY,
|
|
516
|
+
name TEXT NOT NULL,
|
|
517
|
+
handler_key TEXT NOT NULL,
|
|
518
|
+
schedule_type TEXT NOT NULL,
|
|
519
|
+
expression TEXT NOT NULL,
|
|
520
|
+
timezone TEXT NOT NULL,
|
|
521
|
+
overlap_policy TEXT,
|
|
522
|
+
missed_run_policy TEXT,
|
|
523
|
+
config_json TEXT NOT NULL,
|
|
524
|
+
enabled INTEGER NOT NULL,
|
|
525
|
+
created_at TEXT NOT NULL,
|
|
526
|
+
updated_at TEXT NOT NULL
|
|
527
|
+
)`);
|
|
528
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS ${quoted(dialect, names.cursors)} (
|
|
529
|
+
job_id TEXT PRIMARY KEY,
|
|
530
|
+
last_evaluated_at TEXT NOT NULL,
|
|
531
|
+
last_triggered_at TEXT,
|
|
532
|
+
next_run_at TEXT NOT NULL,
|
|
533
|
+
version INTEGER NOT NULL
|
|
534
|
+
)`);
|
|
535
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS ${quoted(dialect, names.executions)} (
|
|
536
|
+
execution_id TEXT PRIMARY KEY,
|
|
537
|
+
job_id TEXT NOT NULL,
|
|
538
|
+
scheduled_for TEXT NOT NULL,
|
|
539
|
+
triggered_at TEXT NOT NULL,
|
|
540
|
+
status TEXT NOT NULL,
|
|
541
|
+
queue_task_id TEXT,
|
|
542
|
+
attempt_count INTEGER NOT NULL,
|
|
543
|
+
last_error TEXT
|
|
544
|
+
)`);
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
function toJobRow(jobId, definition) {
|
|
548
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
549
|
+
return {
|
|
550
|
+
jobId,
|
|
551
|
+
name: definition.name,
|
|
552
|
+
handlerKey: definition.handlerKey,
|
|
553
|
+
scheduleType: definition.schedule.scheduleType,
|
|
554
|
+
expression: definition.schedule.expression,
|
|
555
|
+
timezone: definition.schedule.timezone,
|
|
556
|
+
overlapPolicy: definition.schedule.overlapPolicy ?? null,
|
|
557
|
+
missedRunPolicy: definition.schedule.missedRunPolicy ?? null,
|
|
558
|
+
configJson: JSON.stringify(definition.config),
|
|
559
|
+
enabled: definition.enabled ?? true ? 1 : 0,
|
|
560
|
+
createdAt: now,
|
|
561
|
+
updatedAt: now
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function rowToJobRecord(row) {
|
|
565
|
+
return {
|
|
566
|
+
jobId: row.jobId,
|
|
567
|
+
name: row.name,
|
|
568
|
+
handlerKey: row.handlerKey,
|
|
569
|
+
schedule: {
|
|
570
|
+
scheduleType: row.scheduleType,
|
|
571
|
+
expression: row.expression,
|
|
572
|
+
timezone: row.timezone,
|
|
573
|
+
overlapPolicy: row.overlapPolicy === null ? void 0 : row.overlapPolicy,
|
|
574
|
+
missedRunPolicy: row.missedRunPolicy === null ? void 0 : row.missedRunPolicy
|
|
575
|
+
},
|
|
576
|
+
config: JSON.parse(row.configJson),
|
|
577
|
+
enabled: Boolean(row.enabled),
|
|
578
|
+
createdAt: row.createdAt,
|
|
579
|
+
updatedAt: row.updatedAt
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
async function readJobRow(client, dialect, names, jobId) {
|
|
583
|
+
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
|
|
584
|
+
FROM ${quoted(dialect, names.jobs)} WHERE job_id = ? LIMIT 1`, [jobId]);
|
|
585
|
+
return rows[0] ? rowToJobRecord(rows[0]) : null;
|
|
586
|
+
}
|
|
587
|
+
async function readCursorRow(client, dialect, names, jobId) {
|
|
588
|
+
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
|
|
589
|
+
FROM ${quoted(dialect, names.cursors)} WHERE job_id = ? LIMIT 1`, [jobId]))[0];
|
|
590
|
+
return row ? {
|
|
591
|
+
jobId: row.jobId,
|
|
592
|
+
lastEvaluatedAt: row.lastEvaluatedAt,
|
|
593
|
+
lastTriggeredAt: row.lastTriggeredAt ?? void 0,
|
|
594
|
+
nextRunAt: row.nextRunAt,
|
|
595
|
+
version: row.version
|
|
596
|
+
} : null;
|
|
597
|
+
}
|
|
598
|
+
async function readExecutionRow(client, dialect, names, executionId) {
|
|
599
|
+
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
|
|
600
|
+
FROM ${quoted(dialect, names.executions)} WHERE execution_id = ? LIMIT 1`, [executionId]))[0];
|
|
601
|
+
return row ? {
|
|
602
|
+
executionId: row.executionId,
|
|
603
|
+
jobId: row.jobId,
|
|
604
|
+
scheduledFor: row.scheduledFor,
|
|
605
|
+
triggeredAt: row.triggeredAt,
|
|
606
|
+
status: row.status,
|
|
607
|
+
queueTaskId: row.queueTaskId ?? void 0,
|
|
608
|
+
attemptCount: row.attemptCount,
|
|
609
|
+
lastError: row.lastError ?? void 0
|
|
610
|
+
} : null;
|
|
611
|
+
}
|
|
612
|
+
function createSchedulerPersistence(config) {
|
|
613
|
+
const names = tableNames(namespaceFor(config));
|
|
614
|
+
let bootstrapPromise = null;
|
|
615
|
+
async function ensureBootstrapped() {
|
|
616
|
+
if (!bootstrapPromise) bootstrapPromise = bootstrapSchedulerPersistence(config.client, config.dialect, names);
|
|
617
|
+
await bootstrapPromise;
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
jobStore: {
|
|
621
|
+
async create(jobId, definition) {
|
|
622
|
+
await ensureBootstrapped();
|
|
623
|
+
const row = toJobRow(jobId, definition);
|
|
624
|
+
await config.client.transaction(async (tx) => {
|
|
625
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
626
|
+
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)
|
|
627
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
628
|
+
row.jobId,
|
|
629
|
+
row.name,
|
|
630
|
+
row.handlerKey,
|
|
631
|
+
row.scheduleType,
|
|
632
|
+
row.expression,
|
|
633
|
+
row.timezone,
|
|
634
|
+
row.overlapPolicy,
|
|
635
|
+
row.missedRunPolicy,
|
|
636
|
+
row.configJson,
|
|
637
|
+
row.enabled,
|
|
638
|
+
row.createdAt,
|
|
639
|
+
row.updatedAt
|
|
640
|
+
]);
|
|
641
|
+
});
|
|
642
|
+
return rowToJobRecord(row);
|
|
643
|
+
},
|
|
644
|
+
async update(jobId, patch) {
|
|
645
|
+
const current = await readJobRow(config.client, config.dialect, names, jobId);
|
|
646
|
+
if (!current) return null;
|
|
647
|
+
const next = {
|
|
648
|
+
...current,
|
|
649
|
+
...patch,
|
|
650
|
+
schedule: {
|
|
651
|
+
...current.schedule,
|
|
652
|
+
...patch.schedule
|
|
653
|
+
},
|
|
654
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
655
|
+
};
|
|
656
|
+
await config.client.transaction(async (tx) => {
|
|
657
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
658
|
+
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)
|
|
659
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
660
|
+
next.jobId,
|
|
661
|
+
next.name,
|
|
662
|
+
next.handlerKey,
|
|
663
|
+
next.schedule.scheduleType,
|
|
664
|
+
next.schedule.expression,
|
|
665
|
+
next.schedule.timezone,
|
|
666
|
+
next.schedule.overlapPolicy ?? null,
|
|
667
|
+
next.schedule.missedRunPolicy ?? null,
|
|
668
|
+
JSON.stringify(next.config),
|
|
669
|
+
next.enabled ? 1 : 0,
|
|
670
|
+
next.createdAt,
|
|
671
|
+
next.updatedAt
|
|
672
|
+
]);
|
|
673
|
+
});
|
|
674
|
+
return next;
|
|
675
|
+
},
|
|
676
|
+
async setEnabled(jobId, enabled) {
|
|
677
|
+
const current = await readJobRow(config.client, config.dialect, names, jobId);
|
|
678
|
+
if (!current) return null;
|
|
679
|
+
const next = {
|
|
680
|
+
...current,
|
|
681
|
+
enabled,
|
|
682
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
683
|
+
};
|
|
684
|
+
await config.client.transaction(async (tx) => {
|
|
685
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
686
|
+
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)
|
|
687
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
688
|
+
next.jobId,
|
|
689
|
+
next.name,
|
|
690
|
+
next.handlerKey,
|
|
691
|
+
next.schedule.scheduleType,
|
|
692
|
+
next.schedule.expression,
|
|
693
|
+
next.schedule.timezone,
|
|
694
|
+
next.schedule.overlapPolicy ?? null,
|
|
695
|
+
next.schedule.missedRunPolicy ?? null,
|
|
696
|
+
JSON.stringify(next.config),
|
|
697
|
+
next.enabled ? 1 : 0,
|
|
698
|
+
next.createdAt,
|
|
699
|
+
next.updatedAt
|
|
700
|
+
]);
|
|
701
|
+
});
|
|
702
|
+
return next;
|
|
703
|
+
},
|
|
704
|
+
async get(jobId) {
|
|
705
|
+
await ensureBootstrapped();
|
|
706
|
+
return readJobRow(config.client, config.dialect, names, jobId);
|
|
707
|
+
},
|
|
708
|
+
async list() {
|
|
709
|
+
await ensureBootstrapped();
|
|
710
|
+
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
|
|
711
|
+
FROM ${quoted(config.dialect, names.jobs)} ORDER BY created_at ASC`, [])).map(rowToJobRecord);
|
|
712
|
+
},
|
|
713
|
+
async remove(jobId) {
|
|
714
|
+
await ensureBootstrapped();
|
|
715
|
+
await config.client.execute(`DELETE FROM ${quoted(config.dialect, names.jobs)} WHERE job_id = ?`, [jobId]);
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
cursorStore: {
|
|
720
|
+
async set(jobId, nextRunAt, now) {
|
|
721
|
+
await ensureBootstrapped();
|
|
722
|
+
const current = await readCursorRow(config.client, config.dialect, names, jobId);
|
|
723
|
+
const next = {
|
|
724
|
+
jobId,
|
|
725
|
+
lastEvaluatedAt: now,
|
|
726
|
+
lastTriggeredAt: current?.lastTriggeredAt ?? null,
|
|
727
|
+
nextRunAt,
|
|
728
|
+
version: (current?.version ?? 0) + 1
|
|
729
|
+
};
|
|
730
|
+
await config.client.transaction(async (tx) => {
|
|
731
|
+
await tx.execute(`DELETE FROM ${quoted(config.dialect, names.cursors)} WHERE job_id = ?`, [jobId]);
|
|
732
|
+
await tx.execute(`INSERT INTO ${quoted(config.dialect, names.cursors)} (job_id, last_evaluated_at, last_triggered_at, next_run_at, version) VALUES (?, ?, ?, ?, ?)`, [
|
|
733
|
+
next.jobId,
|
|
734
|
+
next.lastEvaluatedAt,
|
|
735
|
+
next.lastTriggeredAt,
|
|
736
|
+
next.nextRunAt,
|
|
737
|
+
next.version
|
|
738
|
+
]);
|
|
739
|
+
});
|
|
740
|
+
return {
|
|
741
|
+
jobId: next.jobId,
|
|
742
|
+
lastEvaluatedAt: next.lastEvaluatedAt,
|
|
743
|
+
lastTriggeredAt: next.lastTriggeredAt ?? void 0,
|
|
744
|
+
nextRunAt: next.nextRunAt,
|
|
745
|
+
version: next.version
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
async markTriggered(jobId, triggeredAt) {
|
|
749
|
+
await ensureBootstrapped();
|
|
750
|
+
const current = await readCursorRow(config.client, config.dialect, names, jobId);
|
|
751
|
+
if (!current) return null;
|
|
752
|
+
const next = {
|
|
753
|
+
...current,
|
|
754
|
+
lastTriggeredAt: triggeredAt,
|
|
755
|
+
version: current.version + 1
|
|
756
|
+
};
|
|
757
|
+
await config.client.execute(`UPDATE ${quoted(config.dialect, names.cursors)} SET last_triggered_at = ?, version = ? WHERE job_id = ?`, [
|
|
758
|
+
next.lastTriggeredAt,
|
|
759
|
+
next.version,
|
|
760
|
+
jobId
|
|
761
|
+
]);
|
|
762
|
+
return {
|
|
763
|
+
jobId: next.jobId,
|
|
764
|
+
lastEvaluatedAt: next.lastEvaluatedAt,
|
|
765
|
+
lastTriggeredAt: next.lastTriggeredAt ?? void 0,
|
|
766
|
+
nextRunAt: next.nextRunAt,
|
|
767
|
+
version: next.version
|
|
768
|
+
};
|
|
769
|
+
},
|
|
770
|
+
async get(jobId) {
|
|
771
|
+
await ensureBootstrapped();
|
|
772
|
+
return readCursorRow(config.client, config.dialect, names, jobId);
|
|
773
|
+
},
|
|
774
|
+
async remove(jobId) {
|
|
775
|
+
await ensureBootstrapped();
|
|
776
|
+
await config.client.execute(`DELETE FROM ${quoted(config.dialect, names.cursors)} WHERE job_id = ?`, [jobId]);
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
executionStore: {
|
|
781
|
+
async create(record) {
|
|
782
|
+
await ensureBootstrapped();
|
|
783
|
+
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)
|
|
784
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
785
|
+
record.executionId,
|
|
786
|
+
record.jobId,
|
|
787
|
+
record.scheduledFor,
|
|
788
|
+
record.triggeredAt,
|
|
789
|
+
record.status,
|
|
790
|
+
record.queueTaskId ?? null,
|
|
791
|
+
record.attemptCount,
|
|
792
|
+
record.lastError ?? null
|
|
793
|
+
]);
|
|
794
|
+
return record;
|
|
795
|
+
},
|
|
796
|
+
async update(executionId, patch) {
|
|
797
|
+
const current = await readExecutionRow(config.client, config.dialect, names, executionId);
|
|
798
|
+
if (!current) return null;
|
|
799
|
+
const next = {
|
|
800
|
+
...current,
|
|
801
|
+
...patch
|
|
802
|
+
};
|
|
803
|
+
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 = ?`, [
|
|
804
|
+
next.jobId,
|
|
805
|
+
next.scheduledFor,
|
|
806
|
+
next.triggeredAt,
|
|
807
|
+
next.status,
|
|
808
|
+
next.queueTaskId ?? null,
|
|
809
|
+
next.attemptCount,
|
|
810
|
+
next.lastError ?? null,
|
|
811
|
+
executionId
|
|
812
|
+
]);
|
|
813
|
+
return next;
|
|
814
|
+
},
|
|
815
|
+
async listByJob(jobId) {
|
|
816
|
+
await ensureBootstrapped();
|
|
817
|
+
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
|
|
818
|
+
FROM ${quoted(config.dialect, names.executions)} WHERE job_id = ? ORDER BY triggered_at ASC`, [jobId])).map((row) => ({
|
|
819
|
+
executionId: row.executionId,
|
|
820
|
+
jobId: row.jobId,
|
|
821
|
+
scheduledFor: row.scheduledFor,
|
|
822
|
+
triggeredAt: row.triggeredAt,
|
|
823
|
+
status: row.status,
|
|
824
|
+
queueTaskId: row.queueTaskId ?? void 0,
|
|
825
|
+
attemptCount: row.attemptCount,
|
|
826
|
+
lastError: row.lastError ?? void 0
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
//#endregion
|
|
498
833
|
//#region src/core/scheduler-service.ts
|
|
499
834
|
function createSchedulerService$1(options) {
|
|
500
835
|
assertRuntimeConfig(options);
|
|
501
836
|
const logger = options.logger ?? createSchedulerLogger();
|
|
502
837
|
const cache = options.cache ?? createSchedulerCache();
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
const
|
|
838
|
+
const persistence = options.persistence ? createSchedulerPersistence(options.persistence) : void 0;
|
|
839
|
+
const jobStore = persistence?.jobStore ?? createInMemorySchedulerJobStore();
|
|
840
|
+
const cursorStore = persistence?.cursorStore ?? createInMemoryScheduleCursorStore();
|
|
841
|
+
const executionStore = persistence?.executionStore ?? createInMemoryJobExecutionStore();
|
|
506
842
|
const events = createInMemorySchedulerEventBus();
|
|
507
843
|
const handlers = createSchedulerHandlerRegistry();
|
|
508
844
|
const queueService = options.queueService;
|
|
@@ -522,9 +858,9 @@ function createSchedulerService$1(options) {
|
|
|
522
858
|
async registerJob(job) {
|
|
523
859
|
if (!handlers.has(job.handlerKey)) throw new Error(`Unknown handlerKey: ${job.handlerKey}`);
|
|
524
860
|
const jobId = `job_${(0, node_crypto.randomUUID)()}`;
|
|
525
|
-
const created = jobStore.create(jobId, job);
|
|
861
|
+
const created = await jobStore.create(jobId, job);
|
|
526
862
|
const nextRunAt = await engine.computeNextRun(created);
|
|
527
|
-
cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
863
|
+
await cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
528
864
|
events.emit({
|
|
529
865
|
eventType: "job-created",
|
|
530
866
|
jobId,
|
|
@@ -533,32 +869,32 @@ function createSchedulerService$1(options) {
|
|
|
533
869
|
return { jobId };
|
|
534
870
|
},
|
|
535
871
|
async updateJob(jobId, patch) {
|
|
536
|
-
const updated = jobStore.update(jobId, patch);
|
|
872
|
+
const updated = await jobStore.update(jobId, patch);
|
|
537
873
|
if (!updated) throw new Error(`Job not found: ${jobId}`);
|
|
538
874
|
const nextRunAt = await engine.computeNextRun(updated);
|
|
539
|
-
cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
875
|
+
await cursorStore.set(jobId, nextRunAt, (/* @__PURE__ */ new Date()).toISOString());
|
|
540
876
|
events.emit({
|
|
541
877
|
eventType: "job-updated",
|
|
542
878
|
jobId
|
|
543
879
|
});
|
|
544
880
|
},
|
|
545
881
|
async pauseJob(jobId) {
|
|
546
|
-
if (!jobStore.setEnabled(jobId, false)) throw new Error(`Job not found: ${jobId}`);
|
|
882
|
+
if (!await jobStore.setEnabled(jobId, false)) throw new Error(`Job not found: ${jobId}`);
|
|
547
883
|
events.emit({
|
|
548
884
|
eventType: "job-paused",
|
|
549
885
|
jobId
|
|
550
886
|
});
|
|
551
887
|
},
|
|
552
888
|
async resumeJob(jobId) {
|
|
553
|
-
if (!jobStore.setEnabled(jobId, true)) throw new Error(`Job not found: ${jobId}`);
|
|
889
|
+
if (!await jobStore.setEnabled(jobId, true)) throw new Error(`Job not found: ${jobId}`);
|
|
554
890
|
events.emit({
|
|
555
891
|
eventType: "job-resumed",
|
|
556
892
|
jobId
|
|
557
893
|
});
|
|
558
894
|
},
|
|
559
895
|
async deleteJob(jobId) {
|
|
560
|
-
const removed = jobStore.remove(jobId);
|
|
561
|
-
cursorStore.remove(jobId);
|
|
896
|
+
const removed = await jobStore.remove(jobId);
|
|
897
|
+
await cursorStore.remove(jobId);
|
|
562
898
|
if (!removed) throw new Error(`Job not found: ${jobId}`);
|
|
563
899
|
events.emit({
|
|
564
900
|
eventType: "job-deleted",
|
|
@@ -566,8 +902,9 @@ function createSchedulerService$1(options) {
|
|
|
566
902
|
});
|
|
567
903
|
},
|
|
568
904
|
async listJobs() {
|
|
569
|
-
|
|
570
|
-
|
|
905
|
+
const jobs = await jobStore.list();
|
|
906
|
+
return Promise.all(jobs.map(async (job) => {
|
|
907
|
+
const cursor = await cursorStore.get(job.jobId);
|
|
571
908
|
return toJobListItem({
|
|
572
909
|
jobId: job.jobId,
|
|
573
910
|
name: job.name,
|
|
@@ -575,13 +912,15 @@ function createSchedulerService$1(options) {
|
|
|
575
912
|
timezone: job.schedule.timezone,
|
|
576
913
|
nextRunAt: cursor?.nextRunAt
|
|
577
914
|
});
|
|
578
|
-
});
|
|
915
|
+
}));
|
|
579
916
|
},
|
|
580
917
|
async listExecutions(query) {
|
|
581
|
-
|
|
918
|
+
const jobs = await jobStore.list();
|
|
919
|
+
return filterExecutions((await Promise.all(jobs.map((job) => executionStore.listByJob(job.jobId)))).flat(), query);
|
|
582
920
|
},
|
|
583
921
|
async retryFailedExecution(executionId) {
|
|
584
|
-
const
|
|
922
|
+
const jobs = await jobStore.list();
|
|
923
|
+
const retry = await retryFailedExecutionById((await Promise.all(jobs.map((job) => executionStore.listByJob(job.jobId)))).flat(), executionStore, executionId);
|
|
585
924
|
events.emit({
|
|
586
925
|
eventType: "retry",
|
|
587
926
|
jobId: retry.jobId,
|
|
@@ -642,4 +981,5 @@ exports.CronWeekday = CronWeekday;
|
|
|
642
981
|
exports.bindSchedulerToHost = bindSchedulerToHost;
|
|
643
982
|
exports.createCronExpression = createCronExpression;
|
|
644
983
|
exports.createSchedulerDashboardService = createSchedulerDashboardService;
|
|
984
|
+
exports.createSchedulerPersistence = createSchedulerPersistence;
|
|
645
985
|
exports.createSchedulerService = createSchedulerService;
|
package/dist/index.d.cts
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.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","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.cts","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"}
|