@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/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 jobStore = createInMemorySchedulerJobStore();
504
- const cursorStore = createInMemoryScheduleCursorStore();
505
- const executionStore = createInMemoryJobExecutionStore();
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
- return jobStore.list().map((job) => {
570
- const cursor = cursorStore.get(job.jobId);
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
- return filterExecutions(jobStore.list().flatMap((job) => executionStore.listByJob(job.jobId)), query);
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 retry = retryFailedExecutionById(jobStore.list().flatMap((job) => executionStore.listByJob(job.jobId)), executionStore, executionId);
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
@@ -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":"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"}
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"}