@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.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
@@ -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":";KAAY,aAAA;AAAA,KAEA,YAAA;AAAA,KAEA,sBAAA;AAAA,KAEA,wBAAA;AAAA,UAEK,uBAAA;EACf,YAAA,EAAc,YAAA;EACd,UAAA;EACA,QAAA;EACA,aAAA,GAAgB,sBAAA;EAChB,eAAA,GAAkB,wBAAA;AAAA;AAAA,UAGH,sBAAA;EACf,IAAA;EACA,UAAA;EACA,QAAA,EAAU,uBAAA;EACV,MAAA,EAAQ,OAAO;EACf,OAAA;AAAA;AAAA,UAGe,kBAAA,4BAA8C,sBAAsB,CAAC,OAAA;EACpF,KAAA;EACA,OAAA;EACA,SAAA;EACA,SAAA;AAAA;AAAA,UAGe,wBAAA;EACf,WAAA;EACA,KAAA;EACA,YAAA;EACA,WAAA;EACA,MAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA;AAAA;AAAA,UAGe,oBAAA;EACf,KAAA;EACA,IAAA;EACA,OAAA;EACA,SAAA;EACA,QAAA;AAAA;AAAA,UAGe,uBAAA;EACf,KAAA;EACA,MAAA,GAAS,wBAAwB;EACjC,IAAA;EACA,EAAA;EACA,KAAA;AAAA;AAAA,UAGe,gBAAA;EACf,WAAA,UAAqB,GAAA,EAAK,sBAAA,CAAuB,OAAA,IAAW,OAAA;IAAU,KAAA;EAAA;EACtE,SAAA,UAAmB,KAAA,UAAe,KAAA,EAAO,OAAA,CAAQ,sBAAA,CAAuB,OAAA,KAAY,OAAA;EACpF,QAAA,CAAS,KAAA,WAAgB,OAAA;EACzB,SAAA,CAAU,KAAA,WAAgB,OAAA;EAC1B,SAAA,CAAU,KAAA,WAAgB,OAAA;EAC1B,QAAA,IAAY,OAAA,CAAQ,oBAAA;EACpB,cAAA,CAAe,KAAA,GAAQ,uBAAA,GAA0B,OAAA,CAAQ,wBAAA;EACzD,oBAAA,CAAqB,WAAA,WAAsB,OAAA;EAC3C,KAAA,IAAS,OAAA;EACT,IAAA,IAAQ,OAAA;AAAA;AAAA,UAGO,uBAAA;EACf,IAAA,EAAM,aAAA;EACN,SAAA;EACA,YAAA,0BAAsC,YAAA;EACtC,KAAA,0BAA+B,KAAA;EAC/B,MAAA;IACE,KAAA,CAAM,OAAA,UAAiB,IAAA,GAAO,MAAA;IAC9B,IAAA,CAAK,OAAA,UAAiB,IAAA,GAAO,MAAA;IAC7B,IAAA,CAAK,OAAA,UAAiB,IAAA,GAAO,MAAA;IAC7B,KAAA,CAAM,OAAA,UAAiB,IAAA,GAAO,MAAA;EAAA;EAEhC,cAAA;EACA,iBAAA;AAAA;AAAA,UAGe,wBAAA;EACf,QAAA,UAAkB,UAAA,UAAoB,OAAA,GAAU,KAAA,EAAO,OAAA,KAAY,OAAA;EACnE,OAAA,CAAQ,UAAA,aAAuB,KAAA,cAAmB,OAAA;EAClD,GAAA,CAAI,UAAA;AAAA;;;UCzFW,oBAAA;EACf,OAAA,CAAQ,QAAA,QAAgB,OAAA;EACxB,MAAA,CAAO,QAAA,QAAgB,OAAO;AAAA;;;iBCChB,mBAAA,CACd,SAAA,EAAW,gBAAA,EACX,IAAA,EAAM,oBAAoB;;;cCLf,WAAA;EAAA;;;;;;;;KAUD,gBAAA,WAA2B,WAAA,eAA0B,WAAW;AAAA,cAyB/D,qBAAA;EAAA,QACH,MAAA;EAAA,QACA,IAAA;EAAA,QACA,UAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAA;EAKA,aAAA,CAAc,QAAA;EAMd,QAAA,CAAS,MAAA;EAMT,SAAA,CAAU,OAAA;EASV,SAAA,CAAA;EAKA,WAAA,CAAY,QAAA;EAMZ,MAAA,CAAO,IAAA;EAMP,OAAA,CAAQ,KAAA;EASR,YAAA,CAAa,GAAA;EAMb,aAAA,CAAc,IAAA;EASd,iBAAA,CAAkB,QAAA;EAMlB,UAAA,CAAA;EAKA,OAAA,CAAQ,KAAA;EAMR,QAAA,CAAS,MAAA;EAST,YAAA,CAAa,QAAA;EAMb,SAAA,CAAU,OAAA,EAAS,gBAAA;EAMnB,UAAA,CAAW,QAAA,EAAU,gBAAA;EASrB,cAAA,CAAe,QAAA;EAMf,QAAA,CAAA;EAKA,QAAA,CAAA;EAKA,OAAA,CAAQ,IAAA,UAAc,MAAA;EAStB,QAAA,CAAS,OAAA,EAAS,gBAAA,EAAkB,IAAA,WAAU,MAAA;EAS9C,SAAA,CAAU,UAAA,UAAoB,IAAA,WAAU,MAAA;EASxC,KAAA,CAAA;EAIA,QAAA,CAAA;AAAA;AAAA,iBAKc,oBAAA,CAAA,GAAwB,qBAAqB;;;UC9M5C,wBAAA;EACf,SAAA;EACA,WAAA;EACA,UAAA;EACA,gBAAA;AAAA;AAAA,iBAGoB,oBAAA,CACpB,SAAA,EAAW,gBAAA,GACV,OAAA,CAAQ,wBAAA;AAAA,iBAYW,mBAAA,CAAoB,SAAA,EAAW,gBAAA,GAAgB,OAAA;cAAA,wBAAA;;;;;;;;;UCpBpD,yBAAA;EACf,SAAA,IAAa,UAAA,QAAkB,oBAAA;EAC/B,SAAA,IAAa,UAAA,QAAkB,mBAAA;EAC/B,QAAA,CAAS,KAAA,WAAgB,OAAA;EACzB,SAAA,CAAU,KAAA,WAAgB,OAAA;EAC1B,cAAA,CAAe,WAAA,WAAsB,OAAA;AAAA;AAAA,iBAGvB,+BAAA,CACd,SAAA,EAAW,gBAAA,GACV,yBAAyB;;;iBCgBZ,sBAAA,CAAuB,OAAA,EAAS,uBAAA;EAC9C,OAAA,EAAS,gBAAA;EACT,QAAA,EAAU,wBAAA;AAAA"}
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 jobStore = createInMemorySchedulerJobStore();
503
- const cursorStore = createInMemoryScheduleCursorStore();
504
- const executionStore = createInMemoryJobExecutionStore();
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
- return jobStore.list().map((job) => {
569
- const cursor = cursorStore.get(job.jobId);
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
- return filterExecutions(jobStore.list().flatMap((job) => executionStore.listByJob(job.jobId)), query);
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 retry = retryFailedExecutionById(jobStore.list().flatMap((job) => executionStore.listByJob(job.jobId)), executionStore, executionId);
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