@tinyrack/tinyauth-server 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/entities/background-job.entity.d.ts +111 -0
  2. package/dist/entities/background-job.entity.d.ts.map +1 -0
  3. package/dist/entities/background-job.entity.js +41 -0
  4. package/dist/entities/background-job.entity.js.map +1 -0
  5. package/dist/entities/scheduler-job.entity.d.ts +134 -0
  6. package/dist/entities/scheduler-job.entity.d.ts.map +1 -0
  7. package/dist/entities/scheduler-job.entity.js +48 -0
  8. package/dist/entities/scheduler-job.entity.js.map +1 -0
  9. package/dist/entrypoints/app.d.ts +1 -1
  10. package/dist/entrypoints/database/postgres/compiled-functions.d.ts +280 -170
  11. package/dist/entrypoints/database/postgres/compiled-functions.d.ts.map +1 -1
  12. package/dist/entrypoints/database/postgres/compiled-functions.js +3442 -2124
  13. package/dist/entrypoints/database/postgres/compiled-functions.js.map +1 -1
  14. package/dist/entrypoints/database/postgres/postgres.d.ts +7 -2
  15. package/dist/entrypoints/database/postgres/postgres.d.ts.map +1 -1
  16. package/dist/entrypoints/database/postgres/postgres.js +2 -2
  17. package/dist/entrypoints/database/postgres/postgres.js.map +1 -1
  18. package/dist/entrypoints/database/sqlite/compiled-functions.d.ts +280 -170
  19. package/dist/entrypoints/database/sqlite/compiled-functions.d.ts.map +1 -1
  20. package/dist/entrypoints/database/sqlite/compiled-functions.js +3442 -2124
  21. package/dist/entrypoints/database/sqlite/compiled-functions.js.map +1 -1
  22. package/dist/entrypoints/database/sqlite/sqlite.d.ts +6 -2
  23. package/dist/entrypoints/database/sqlite/sqlite.d.ts.map +1 -1
  24. package/dist/entrypoints/database/sqlite/sqlite.js +1 -1
  25. package/dist/entrypoints/database/sqlite/sqlite.js.map +1 -1
  26. package/dist/entrypoints/scheduler/cron.d.ts +3 -0
  27. package/dist/entrypoints/scheduler/cron.d.ts.map +1 -0
  28. package/dist/entrypoints/scheduler/cron.js +34 -0
  29. package/dist/entrypoints/scheduler/cron.js.map +1 -0
  30. package/dist/entrypoints/scheduler/croner.d.ts +1 -1
  31. package/dist/entrypoints/scheduler/croner.d.ts.map +1 -1
  32. package/dist/entrypoints/scheduler/croner.js +27 -7
  33. package/dist/entrypoints/scheduler/croner.js.map +1 -1
  34. package/dist/entrypoints/scheduler/database.d.ts +38 -0
  35. package/dist/entrypoints/scheduler/database.d.ts.map +1 -0
  36. package/dist/entrypoints/scheduler/database.js +444 -0
  37. package/dist/entrypoints/scheduler/database.js.map +1 -0
  38. package/dist/entrypoints/scheduler/distributed-runner.d.ts +134 -0
  39. package/dist/entrypoints/scheduler/distributed-runner.d.ts.map +1 -0
  40. package/dist/entrypoints/scheduler/distributed-runner.js +327 -0
  41. package/dist/entrypoints/scheduler/distributed-runner.js.map +1 -0
  42. package/dist/lib/config/index.d.ts +2 -2
  43. package/dist/lib/config/index.d.ts.map +1 -1
  44. package/dist/lib/config/index.js +1 -1
  45. package/dist/lib/config/index.js.map +1 -1
  46. package/dist/lib/config/resolved.d.ts +1 -1
  47. package/dist/lib/config/resolved.d.ts.map +1 -1
  48. package/dist/lib/config/resolved.js +1 -1
  49. package/dist/lib/config/resolved.js.map +1 -1
  50. package/dist/lib/config/scheduler.d.ts +41 -2
  51. package/dist/lib/config/scheduler.d.ts.map +1 -1
  52. package/dist/lib/config/scheduler.js +9 -3
  53. package/dist/lib/config/scheduler.js.map +1 -1
  54. package/dist/lib/database/entities.d.ts.map +1 -1
  55. package/dist/lib/database/entities.js +4 -0
  56. package/dist/lib/database/entities.js.map +1 -1
  57. package/dist/migrations/postgres/Migration20260512120000_add_scheduler_jobs.d.ts +5 -0
  58. package/dist/migrations/postgres/Migration20260512120000_add_scheduler_jobs.d.ts.map +1 -0
  59. package/dist/migrations/postgres/Migration20260512120000_add_scheduler_jobs.js +15 -0
  60. package/dist/migrations/postgres/Migration20260512120000_add_scheduler_jobs.js.map +1 -0
  61. package/dist/migrations/postgres/index.d.ts.map +1 -1
  62. package/dist/migrations/postgres/index.js +5 -1
  63. package/dist/migrations/postgres/index.js.map +1 -1
  64. package/dist/migrations/sqlite/Migration20260512120000_add_scheduler_jobs.d.ts +5 -0
  65. package/dist/migrations/sqlite/Migration20260512120000_add_scheduler_jobs.d.ts.map +1 -0
  66. package/dist/migrations/sqlite/Migration20260512120000_add_scheduler_jobs.js +13 -0
  67. package/dist/migrations/sqlite/Migration20260512120000_add_scheduler_jobs.js.map +1 -0
  68. package/dist/migrations/sqlite/index.d.ts.map +1 -1
  69. package/dist/migrations/sqlite/index.js +5 -1
  70. package/dist/migrations/sqlite/index.js.map +1 -1
  71. package/dist/repositories/background-job.repository.d.ts +5 -0
  72. package/dist/repositories/background-job.repository.d.ts.map +1 -0
  73. package/dist/repositories/background-job.repository.js +4 -0
  74. package/dist/repositories/background-job.repository.js.map +1 -0
  75. package/dist/repositories/scheduler-job.repository.d.ts +5 -0
  76. package/dist/repositories/scheduler-job.repository.d.ts.map +1 -0
  77. package/dist/repositories/scheduler-job.repository.js +4 -0
  78. package/dist/repositories/scheduler-job.repository.js.map +1 -0
  79. package/dist/services/container.d.ts +2 -2
  80. package/dist/services/container.d.ts.map +1 -1
  81. package/dist/services/container.js +7 -1
  82. package/dist/services/container.js.map +1 -1
  83. package/dist/services/mikro.service.d.ts +4 -0
  84. package/dist/services/mikro.service.d.ts.map +1 -1
  85. package/dist/services/mikro.service.js +6 -0
  86. package/dist/services/mikro.service.js.map +1 -1
  87. package/dist/services/scheduler.service.d.ts +6 -1
  88. package/dist/services/scheduler.service.d.ts.map +1 -1
  89. package/dist/services/scheduler.service.js +44 -11
  90. package/dist/services/scheduler.service.js.map +1 -1
  91. package/package.json +5 -1
@@ -1,6 +1,10 @@
1
+ import { type Options } from '@mikro-orm/core';
1
2
  import type { DatabaseConfig } from '../../../lib/config/index.ts';
2
- export declare function sqlite(database: {
3
+ type SqliteDatabaseConfig = {
3
4
  path: string;
4
5
  test: boolean;
5
- }): DatabaseConfig;
6
+ debug?: Options['debug'];
7
+ };
8
+ export declare function sqlite(database: SqliteDatabaseConfig): DatabaseConfig;
9
+ export {};
6
10
  //# sourceMappingURL=sqlite.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../../../src/entrypoints/database/sqlite/sqlite.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAKnE,wBAAgB,MAAM,CAAC,QAAQ,EAAE;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,cAAc,CA0BjB"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../../../src/entrypoints/database/sqlite/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAI5E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAKnE,KAAK,oBAAoB,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B,CAAC;AAEF,wBAAgB,MAAM,CAAC,QAAQ,EAAE,oBAAoB,GAAG,cAAc,CA0BrE"}
@@ -19,7 +19,7 @@ export function sqlite(database) {
19
19
  migrations: {
20
20
  migrationsList: SQLITE_MIGRATIONS,
21
21
  },
22
- debug: false,
22
+ debug: database.debug ?? false,
23
23
  });
24
24
  },
25
25
  initialize: async (orm) => {
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../../../src/entrypoints/database/sqlite/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAiB,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AAExD,MAAM,UAAU,MAAM,CAAC,QAGtB;IACC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;IAE1D,OAAO;QACL,kBAAkB,EAAE,KAAK,IAAI,EAAE;YAC7B,OAAO,YAAY,CAAC;gBAClB,MAAM,EAAE,YAAY;gBACpB,MAAM,EAAE,MAAM;gBACd,aAAa,EAAE,IAAI,iBAAiB,CAAC,MAAM,CAAC;gBAC5C,iBAAiB,EAAE,iBAAiB;gBACpC,QAAQ,EAAE,CAAC,GAAG,mBAAmB,EAAE,CAAC;gBACpC,UAAU,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC;gBACnC,UAAU,EAAE;oBACV,cAAc,EAAE,iBAAiB;iBAClC;gBACD,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;QACD,UAAU,EAAE,KAAK,EAAE,GAAa,EAAE,EAAE;YAClC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAClB,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC7B,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAC1B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../../../src/entrypoints/database/sqlite/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA+B,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,iBAAiB,MAAM,yBAAyB,CAAC;AAQxD,MAAM,UAAU,MAAM,CAAC,QAA8B;IACnD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;IAE1D,OAAO;QACL,kBAAkB,EAAE,KAAK,IAAI,EAAE;YAC7B,OAAO,YAAY,CAAC;gBAClB,MAAM,EAAE,YAAY;gBACpB,MAAM,EAAE,MAAM;gBACd,aAAa,EAAE,IAAI,iBAAiB,CAAC,MAAM,CAAC;gBAC5C,iBAAiB,EAAE,iBAAiB;gBACpC,QAAQ,EAAE,CAAC,GAAG,mBAAmB,EAAE,CAAC;gBACpC,UAAU,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC;gBACnC,UAAU,EAAE;oBACV,cAAc,EAAE,iBAAiB;iBAClC;gBACD,KAAK,EAAE,QAAQ,CAAC,KAAK,IAAI,KAAK;aAC/B,CAAC,CAAC;QACL,CAAC;QACD,UAAU,EAAE,KAAK,EAAE,GAAa,EAAE,EAAE;YAClC,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAClB,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAC7B,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YAC1B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function validateCronExpression(expression: string): string;
2
+ export declare function getNextCronRunAt(expression: string, from: Date): Date;
3
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/cron.ts"],"names":[],"mappings":"AAEA,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAgBjE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAcrE"}
@@ -0,0 +1,34 @@
1
+ import { Cron } from 'croner';
2
+ export function validateCronExpression(expression) {
3
+ let job;
4
+ try {
5
+ job = new Cron(expression, { paused: true });
6
+ const nextRunAt = job.nextRun(new Date()) ?? null;
7
+ if (!nextRunAt) {
8
+ throw new Error('Cron expression has no future run');
9
+ }
10
+ }
11
+ catch (err) {
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ throw new Error(`Invalid cron expression "${expression}": ${message}`);
14
+ }
15
+ finally {
16
+ job?.stop();
17
+ }
18
+ return expression;
19
+ }
20
+ export function getNextCronRunAt(expression, from) {
21
+ let job;
22
+ try {
23
+ job = new Cron(expression, { paused: true });
24
+ const nextRunAt = job.nextRun(from) ?? null;
25
+ if (!nextRunAt) {
26
+ throw new Error(`Cron expression has no future run: ${expression}`);
27
+ }
28
+ return nextRunAt;
29
+ }
30
+ finally {
31
+ job?.stop();
32
+ }
33
+ }
34
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.js","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/cron.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,IAAI,GAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,IAAI,CAAC;QAClD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,4BAA4B,UAAU,MAAM,OAAO,EAAE,CAAC,CAAC;IACzE,CAAC;YAAS,CAAC;QACT,GAAG,EAAE,IAAI,EAAE,CAAC;IACd,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,UAAkB,EAAE,IAAU;IAC7D,IAAI,GAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,IAAI,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;QAE5C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,sCAAsC,UAAU,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,GAAG,EAAE,IAAI,EAAE,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import type { SchedulerConfig } from '../../lib/config/index.ts';
2
2
  export interface CronerSchedulerOptions {
3
- cron?: string | undefined;
3
+ cleanupCron?: string | undefined;
4
4
  }
5
5
  export declare function croner(options?: CronerSchedulerOptions): SchedulerConfig;
6
6
  //# sourceMappingURL=croner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"croner.d.ts","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/croner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,2BAA2B,CAAC;AAInC,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,wBAAgB,MAAM,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAqB5E"}
1
+ {"version":3,"file":"croner.d.ts","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/croner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,wBAAgB,MAAM,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAgD5E"}
@@ -1,18 +1,38 @@
1
1
  import { Cron } from 'croner';
2
+ import { validateCronExpression } from "./cron.js";
2
3
  const DEFAULT_CRON = '0 2 * * *';
3
4
  export function croner(options = {}) {
4
- const cron = options.cron ?? DEFAULT_CRON;
5
+ const cleanupCron = validateCronExpression(options.cleanupCron ?? DEFAULT_CRON);
5
6
  return {
6
- start({ runCleanup }) {
7
- const job = new Cron(cron, async () => {
8
- await runCleanup();
9
- });
7
+ cleanupCron,
8
+ start({ scheduledJobs, logger }) {
9
+ const cronJobs = scheduledJobs.map((job) => new Cron(job.schedule.expression, async () => {
10
+ try {
11
+ await job.handler({ logger });
12
+ }
13
+ catch (err) {
14
+ logger?.error({ err, jobId: job.id }, 'Scheduled job failed');
15
+ }
16
+ }));
10
17
  const handle = {
11
18
  stop() {
12
- job.stop();
19
+ for (const cronJob of cronJobs) {
20
+ cronJob.stop();
21
+ }
13
22
  },
14
23
  getNextRunAt() {
15
- return job.nextRun() ?? null;
24
+ const nextRuns = cronJobs
25
+ .map((cronJob) => cronJob.nextRun() ?? null)
26
+ .filter((nextRun) => nextRun !== null);
27
+ return nextRuns.reduce((earliest, nextRun) => {
28
+ if (!earliest || nextRun.getTime() < earliest.getTime()) {
29
+ return nextRun;
30
+ }
31
+ return earliest;
32
+ }, null);
33
+ },
34
+ async enqueue() {
35
+ throw new Error('Background jobs require a durable scheduler backend');
16
36
  },
17
37
  };
18
38
  return handle;
@@ -1 +1 @@
1
- {"version":3,"file":"croner.js","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/croner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAM9B,MAAM,YAAY,GAAG,WAAW,CAAC;AAMjC,MAAM,UAAU,MAAM,CAAC,UAAkC,EAAE;IACzD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC;IAE1C,OAAO;QACL,KAAK,CAAC,EAAE,UAAU,EAAE;YAClB,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;gBACpC,MAAM,UAAU,EAAE,CAAC;YACrB,CAAC,CAAC,CAAC;YAEH,MAAM,MAAM,GAAoB;gBAC9B,IAAI;oBACF,GAAG,CAAC,IAAI,EAAE,CAAC;gBACb,CAAC;gBACD,YAAY;oBACV,OAAO,GAAG,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC;gBAC/B,CAAC;aACF,CAAC;YAEF,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"croner.js","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/croner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAEnD,MAAM,YAAY,GAAG,WAAW,CAAC;AAMjC,MAAM,UAAU,MAAM,CAAC,UAAkC,EAAE;IACzD,MAAM,WAAW,GAAG,sBAAsB,CACxC,OAAO,CAAC,WAAW,IAAI,YAAY,CACpC,CAAC;IAEF,OAAO;QACL,WAAW;QACX,KAAK,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE;YAC7B,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAChC,CAAC,GAAG,EAAE,EAAE,CACN,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;gBAC3C,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;gBAChC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,sBAAsB,CAAC,CAAC;gBAChE,CAAC;YACH,CAAC,CAAC,CACL,CAAC;YAEF,MAAM,MAAM,GAAoB;gBAC9B,IAAI;oBACF,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;wBAC/B,OAAO,CAAC,IAAI,EAAE,CAAC;oBACjB,CAAC;gBACH,CAAC;gBACD,YAAY;oBACV,MAAM,QAAQ,GAAG,QAAQ;yBACtB,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC;yBAC3C,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC;oBAEzC,OAAO,QAAQ,CAAC,MAAM,CAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE;wBACxD,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;4BACxD,OAAO,OAAO,CAAC;wBACjB,CAAC;wBAED,OAAO,QAAQ,CAAC;oBAClB,CAAC,EAAE,IAAI,CAAC,CAAC;gBACX,CAAC;gBACD,KAAK,CAAC,OAAO;oBACX,MAAM,IAAI,KAAK,CACb,qDAAqD,CACtD,CAAC;gBACJ,CAAC;aACF,CAAC;YAEF,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { SchedulerConfig, SchedulerConfigResolver } from '../../lib/config/index.ts';
2
+ import type { MikroService } from '../../services/mikro.service.ts';
3
+ import { type AcquiredBackgroundJob, type AcquiredSchedulerJob, type BackgroundJobCompletionInput, type BackgroundJobEnqueueInput, type BackgroundJobFailureCompletionInput, type DistributedBackgroundJobStore, type DistributedSchedulerStore, type PersistedSchedulerJobDefinition, type SchedulerCompletionInput, type SchedulerFailureCompletionInput } from './distributed-runner.ts';
4
+ export interface DatabaseSchedulerOptions {
5
+ cleanupCron?: string | undefined;
6
+ pollIntervalMs?: number | undefined;
7
+ lockTtlMs?: number | undefined;
8
+ backgroundRetryDelayMs?: number | undefined;
9
+ backgroundMaxAttempts?: number | undefined;
10
+ backgroundRetentionMs?: number | undefined;
11
+ instanceId?: string | undefined;
12
+ }
13
+ export interface BoundDatabaseSchedulerOptions extends DatabaseSchedulerOptions {
14
+ mikro: MikroService;
15
+ }
16
+ export declare class DatabaseSchedulerStore implements DistributedSchedulerStore {
17
+ private readonly mikro;
18
+ constructor(mikro: MikroService);
19
+ reconcileJobs(jobs: readonly PersistedSchedulerJobDefinition[], now: Date): Promise<void>;
20
+ acquireDueJob(now: Date, lockedUntil: Date, instanceId: string): Promise<AcquiredSchedulerJob | null>;
21
+ renewLease(jobId: string, instanceId: string, lockedUntil: Date, now: Date): Promise<boolean>;
22
+ completeJobSuccess(input: SchedulerCompletionInput): Promise<boolean>;
23
+ completeJobFailure(input: SchedulerFailureCompletionInput): Promise<boolean>;
24
+ findNextRunAt(): Promise<Date | null>;
25
+ }
26
+ export declare class DatabaseBackgroundJobStore implements DistributedBackgroundJobStore {
27
+ private readonly mikro;
28
+ constructor(mikro: MikroService);
29
+ enqueue(input: BackgroundJobEnqueueInput): Promise<string>;
30
+ acquireDueJob(now: Date, lockedUntil: Date, instanceId: string): Promise<AcquiredBackgroundJob | null>;
31
+ renewLease(id: string, instanceId: string, lockedUntil: Date, now: Date): Promise<boolean>;
32
+ completeJobSuccess(input: BackgroundJobCompletionInput): Promise<boolean>;
33
+ completeJobFailure(input: BackgroundJobFailureCompletionInput): Promise<boolean>;
34
+ cleanupCompletedJobs(before: Date): Promise<number>;
35
+ }
36
+ export declare function database(options: BoundDatabaseSchedulerOptions): SchedulerConfig;
37
+ export declare function database(options?: DatabaseSchedulerOptions): SchedulerConfigResolver;
38
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/entrypoints/scheduler/database.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAEV,eAAe,EACf,uBAAuB,EAExB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,EACjC,KAAK,yBAAyB,EAC9B,KAAK,mCAAmC,EAExC,KAAK,6BAA6B,EAElC,KAAK,yBAAyB,EAE9B,KAAK,+BAA+B,EACpC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACrC,MAAM,yBAAyB,CAAC;AAWjC,MAAM,WAAW,wBAAwB;IACvC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,qBAAqB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC;AAED,MAAM,WAAW,6BACf,SAAQ,wBAAwB;IAChC,KAAK,EAAE,YAAY,CAAC;CACrB;AAkKD,qBAAa,sBAAuB,YAAW,yBAAyB;IACtE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;gBAEzB,KAAK,EAAE,YAAY;IAIzB,aAAa,CACjB,IAAI,EAAE,SAAS,+BAA+B,EAAE,EAChD,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,IAAI,CAAC;IAkCV,aAAa,CACjB,GAAG,EAAE,IAAI,EACT,WAAW,EAAE,IAAI,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAmDjC,UAAU,CACd,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,EACjB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC;IAeb,kBAAkB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,OAAO,CAAC;IAsBrE,kBAAkB,CACtB,KAAK,EAAE,+BAA+B,GACrC,OAAO,CAAC,OAAO,CAAC;IAwBb,aAAa,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;CAU5C;AAED,qBAAa,0BACX,YAAW,6BAA6B;IAExC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;gBAEzB,KAAK,EAAE,YAAY;IAIzB,OAAO,CAAC,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAyB1D,aAAa,CACjB,GAAG,EAAE,IAAI,EACT,WAAW,EAAE,IAAI,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAqGlC,UAAU,CACd,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,IAAI,EACjB,GAAG,EAAE,IAAI,GACR,OAAO,CAAC,OAAO,CAAC;IAgBb,kBAAkB,CACtB,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,OAAO,CAAC;IAqBb,kBAAkB,CACtB,KAAK,EAAE,mCAAmC,GACzC,OAAO,CAAC,OAAO,CAAC;IAwBb,oBAAoB,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;CAS1D;AA2DD,wBAAgB,QAAQ,CACtB,OAAO,EAAE,6BAA6B,GACrC,eAAe,CAAC;AACnB,wBAAgB,QAAQ,CACtB,OAAO,CAAC,EAAE,wBAAwB,GACjC,uBAAuB,CAAC"}
@@ -0,0 +1,444 @@
1
+ import os from 'node:os';
2
+ import { BackgroundJobEntitySchema } from "../../entities/background-job.entity.js";
3
+ import { SchedulerJobEntitySchema } from "../../entities/scheduler-job.entity.js";
4
+ import { validateCronExpression } from "./cron.js";
5
+ import { DistributedBackgroundJobRunner, DistributedSchedulerRunner, getDistributedSchedulerNextRunAt, } from "./distributed-runner.js";
6
+ const DEFAULT_CLEANUP_CRON = '0 2 * * *';
7
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
8
+ const DEFAULT_LOCK_TTL_MS = 60000;
9
+ const DEFAULT_BACKGROUND_RETRY_DELAY_MS = 1000;
10
+ const DEFAULT_BACKGROUND_MAX_ATTEMPTS = 3;
11
+ const DEFAULT_BACKGROUND_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
12
+ const MAX_ERROR_LENGTH = 2000;
13
+ const JSON_SAFE_PAYLOAD_ERROR = 'Background job payload must be JSON-safe';
14
+ function createInstanceId(instanceId) {
15
+ return instanceId ?? `${os.hostname()}:${process.pid}:${crypto.randomUUID()}`;
16
+ }
17
+ function resolvePositiveNumber(name, value, defaultValue) {
18
+ const resolved = value ?? defaultValue;
19
+ if (!Number.isFinite(resolved) || resolved <= 0) {
20
+ throw new Error(`${name} must be a positive number`);
21
+ }
22
+ return resolved;
23
+ }
24
+ function resolvePositiveInteger(name, value, defaultValue) {
25
+ const resolved = resolvePositiveNumber(name, value, defaultValue);
26
+ if (!Number.isInteger(resolved)) {
27
+ throw new Error(`${name} must be a positive integer`);
28
+ }
29
+ return resolved;
30
+ }
31
+ function errorToMessage(err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ return message.slice(0, MAX_ERROR_LENGTH);
34
+ }
35
+ function validateJsonSafePayload(value, path, seen) {
36
+ if (value === null) {
37
+ return;
38
+ }
39
+ switch (typeof value) {
40
+ case 'boolean':
41
+ case 'string':
42
+ return;
43
+ case 'number':
44
+ if (!Number.isFinite(value)) {
45
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path} must be finite`);
46
+ }
47
+ return;
48
+ case 'undefined':
49
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path} is undefined`);
50
+ case 'bigint':
51
+ case 'function':
52
+ case 'symbol':
53
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path} has unsupported type ${typeof value}`);
54
+ }
55
+ if (seen.has(value)) {
56
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path} contains a cycle`);
57
+ }
58
+ if (Object.hasOwn(value, 'toJSON')) {
59
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path} must not define toJSON`);
60
+ }
61
+ const prototype = Object.getPrototypeOf(value);
62
+ if (!Array.isArray(value) &&
63
+ prototype !== Object.prototype &&
64
+ prototype !== null) {
65
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path} must be a plain object`);
66
+ }
67
+ seen.add(value);
68
+ if (Array.isArray(value)) {
69
+ for (let index = 0; index < value.length; index += 1) {
70
+ if (!(index in value)) {
71
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: ${path}[${index}] is undefined`);
72
+ }
73
+ validateJsonSafePayload(value[index], `${path}[${index}]`, seen);
74
+ }
75
+ }
76
+ else {
77
+ for (const [key, child] of Object.entries(value)) {
78
+ validateJsonSafePayload(child, `${path}.${key}`, seen);
79
+ }
80
+ }
81
+ seen.delete(value);
82
+ }
83
+ function stringifyJsonSafePayload(payload) {
84
+ validateJsonSafePayload(payload, 'payload', new WeakSet());
85
+ const serialized = JSON.stringify(payload);
86
+ if (serialized === undefined) {
87
+ throw new Error(`${JSON_SAFE_PAYLOAD_ERROR}: payload is undefined`);
88
+ }
89
+ return serialized;
90
+ }
91
+ function resolveOptions(options) {
92
+ return {
93
+ cleanupCron: validateCronExpression(options.cleanupCron ?? DEFAULT_CLEANUP_CRON),
94
+ pollIntervalMs: resolvePositiveNumber('pollIntervalMs', options.pollIntervalMs, DEFAULT_POLL_INTERVAL_MS),
95
+ lockTtlMs: resolvePositiveNumber('lockTtlMs', options.lockTtlMs, DEFAULT_LOCK_TTL_MS),
96
+ backgroundRetryDelayMs: resolvePositiveNumber('backgroundRetryDelayMs', options.backgroundRetryDelayMs, DEFAULT_BACKGROUND_RETRY_DELAY_MS),
97
+ backgroundMaxAttempts: resolvePositiveInteger('backgroundMaxAttempts', options.backgroundMaxAttempts, DEFAULT_BACKGROUND_MAX_ATTEMPTS),
98
+ backgroundRetentionMs: resolvePositiveNumber('backgroundRetentionMs', options.backgroundRetentionMs, DEFAULT_BACKGROUND_RETENTION_MS),
99
+ instanceId: createInstanceId(options.instanceId),
100
+ };
101
+ }
102
+ export class DatabaseSchedulerStore {
103
+ mikro;
104
+ constructor(mikro) {
105
+ this.mikro = mikro;
106
+ }
107
+ async reconcileJobs(jobs, now) {
108
+ const em = this.mikro.em.fork();
109
+ const repo = em.getRepository(SchedulerJobEntitySchema);
110
+ const jobIds = jobs.map((job) => job.id);
111
+ for (const job of jobs) {
112
+ const existing = await repo.findOne({ id: job.id });
113
+ const nextRunAt = getDistributedSchedulerNextRunAt(job.cron, now);
114
+ const shouldResetNextRunAt = !existing?.nextRunAt || existing.cron !== job.cron;
115
+ await em.upsert(SchedulerJobEntitySchema, {
116
+ id: job.id,
117
+ name: job.name,
118
+ enabled: true,
119
+ cron: job.cron,
120
+ nextRunAt: shouldResetNextRunAt ? nextRunAt : existing.nextRunAt,
121
+ created_at: now,
122
+ updated_at: now,
123
+ }, {
124
+ onConflictFields: ['id'],
125
+ onConflictAction: 'merge',
126
+ onConflictExcludeFields: ['id', 'created_at'],
127
+ });
128
+ }
129
+ const staleJobFilter = jobIds.length > 0 ? { id: { $nin: jobIds } } : {};
130
+ await repo.nativeUpdate(staleJobFilter, { enabled: false });
131
+ }
132
+ async acquireDueJob(now, lockedUntil, instanceId) {
133
+ const em = this.mikro.em.fork();
134
+ const repo = em.getRepository(SchedulerJobEntitySchema);
135
+ const seenCandidateIds = [];
136
+ const eligibleFilter = {
137
+ enabled: true,
138
+ nextRunAt: { $lte: now },
139
+ $or: [{ lockedUntil: null }, { lockedUntil: { $lte: now } }],
140
+ };
141
+ while (true) {
142
+ const candidateFilter = {
143
+ ...eligibleFilter,
144
+ ...(seenCandidateIds.length > 0
145
+ ? { id: { $nin: seenCandidateIds } }
146
+ : {}),
147
+ };
148
+ const candidate = await repo.findOne(candidateFilter, {
149
+ orderBy: { nextRunAt: 'ASC' },
150
+ });
151
+ if (!candidate) {
152
+ return null;
153
+ }
154
+ const updated = await repo.nativeUpdate({
155
+ id: candidate.id,
156
+ ...eligibleFilter,
157
+ }, {
158
+ lockedBy: instanceId,
159
+ lockedUntil,
160
+ lastRunAt: now,
161
+ });
162
+ if (updated !== 1) {
163
+ seenCandidateIds.push(candidate.id);
164
+ continue;
165
+ }
166
+ return {
167
+ id: candidate.id,
168
+ cron: candidate.cron,
169
+ runCount: candidate.runCount,
170
+ failureCount: candidate.failureCount,
171
+ };
172
+ }
173
+ }
174
+ async renewLease(jobId, instanceId, lockedUntil, now) {
175
+ const em = this.mikro.em.fork();
176
+ const repo = em.getRepository(SchedulerJobEntitySchema);
177
+ const updated = await repo.nativeUpdate({
178
+ id: jobId,
179
+ lockedBy: instanceId,
180
+ lockedUntil: { $gt: now, $lt: lockedUntil },
181
+ }, { lockedUntil });
182
+ return updated === 1;
183
+ }
184
+ async completeJobSuccess(input) {
185
+ const em = this.mikro.em.fork();
186
+ const repo = em.getRepository(SchedulerJobEntitySchema);
187
+ const updated = await repo.nativeUpdate({
188
+ id: input.jobId,
189
+ cron: input.cron,
190
+ lockedBy: input.instanceId,
191
+ lockedUntil: { $gt: input.now },
192
+ }, {
193
+ lockedBy: null,
194
+ lockedUntil: null,
195
+ nextRunAt: input.nextRunAt,
196
+ runCount: input.runCount,
197
+ lastSuccessAt: input.now,
198
+ });
199
+ return updated === 1;
200
+ }
201
+ async completeJobFailure(input) {
202
+ const em = this.mikro.em.fork();
203
+ const repo = em.getRepository(SchedulerJobEntitySchema);
204
+ const updated = await repo.nativeUpdate({
205
+ id: input.jobId,
206
+ cron: input.cron,
207
+ lockedBy: input.instanceId,
208
+ lockedUntil: { $gt: input.now },
209
+ }, {
210
+ lockedBy: null,
211
+ lockedUntil: null,
212
+ nextRunAt: input.nextRunAt,
213
+ runCount: input.runCount,
214
+ lastErrorAt: input.now,
215
+ lastError: input.error,
216
+ failureCount: input.failureCount,
217
+ });
218
+ return updated === 1;
219
+ }
220
+ async findNextRunAt() {
221
+ const em = this.mikro.em.fork();
222
+ const repo = em.getRepository(SchedulerJobEntitySchema);
223
+ const next = await repo.findOne({ enabled: true, nextRunAt: { $ne: null } }, { orderBy: { nextRunAt: 'ASC' } });
224
+ return next?.nextRunAt ?? null;
225
+ }
226
+ }
227
+ export class DatabaseBackgroundJobStore {
228
+ mikro;
229
+ constructor(mikro) {
230
+ this.mikro = mikro;
231
+ }
232
+ async enqueue(input) {
233
+ const em = this.mikro.em.fork();
234
+ const id = crypto.randomUUID();
235
+ const job = em.create(BackgroundJobEntitySchema, {
236
+ id,
237
+ jobId: input.jobId,
238
+ payload: stringifyJsonSafePayload(input.payload),
239
+ status: 'pending',
240
+ availableAt: input.availableAt,
241
+ lockedBy: null,
242
+ lockedUntil: null,
243
+ attemptCount: 0,
244
+ maxAttempts: input.maxAttempts,
245
+ lastError: null,
246
+ completedAt: null,
247
+ created_at: input.now,
248
+ updated_at: input.now,
249
+ });
250
+ em.persist(job);
251
+ await em.flush();
252
+ return id;
253
+ }
254
+ async acquireDueJob(now, lockedUntil, instanceId) {
255
+ const em = this.mikro.em.fork();
256
+ const repo = em.getRepository(BackgroundJobEntitySchema);
257
+ const seenCandidateIds = [];
258
+ const eligibleFilter = {
259
+ $or: [
260
+ {
261
+ status: 'pending',
262
+ availableAt: { $lte: now },
263
+ },
264
+ {
265
+ status: 'running',
266
+ lockedUntil: { $lte: now },
267
+ },
268
+ ],
269
+ };
270
+ while (true) {
271
+ const candidateFilter = {
272
+ ...eligibleFilter,
273
+ ...(seenCandidateIds.length > 0
274
+ ? { id: { $nin: seenCandidateIds } }
275
+ : {}),
276
+ };
277
+ const candidate = await repo.findOne(candidateFilter, {
278
+ orderBy: { availableAt: 'ASC' },
279
+ });
280
+ if (!candidate) {
281
+ return null;
282
+ }
283
+ if (candidate.status === 'running' &&
284
+ candidate.attemptCount >= candidate.maxAttempts) {
285
+ await repo.nativeUpdate({ id: candidate.id, ...eligibleFilter }, {
286
+ status: 'failed',
287
+ availableAt: now,
288
+ lockedBy: null,
289
+ lockedUntil: null,
290
+ lastError: 'Background job exceeded maximum attempts',
291
+ completedAt: now,
292
+ });
293
+ continue;
294
+ }
295
+ const attemptCount = candidate.attemptCount + 1;
296
+ const updated = await repo.nativeUpdate({ id: candidate.id, ...eligibleFilter }, {
297
+ status: 'running',
298
+ lockedBy: instanceId,
299
+ lockedUntil,
300
+ attemptCount,
301
+ });
302
+ if (updated !== 1) {
303
+ seenCandidateIds.push(candidate.id);
304
+ continue;
305
+ }
306
+ let payload;
307
+ try {
308
+ payload = JSON.parse(candidate.payload);
309
+ }
310
+ catch (err) {
311
+ await repo.nativeUpdate({
312
+ id: candidate.id,
313
+ lockedBy: instanceId,
314
+ status: 'running',
315
+ lockedUntil: { $gt: now },
316
+ }, {
317
+ status: 'failed',
318
+ availableAt: now,
319
+ lockedBy: null,
320
+ lockedUntil: null,
321
+ attemptCount,
322
+ lastError: errorToMessage(err),
323
+ completedAt: now,
324
+ });
325
+ continue;
326
+ }
327
+ return {
328
+ id: candidate.id,
329
+ jobId: candidate.jobId,
330
+ payload,
331
+ attemptCount,
332
+ maxAttempts: candidate.maxAttempts,
333
+ };
334
+ }
335
+ }
336
+ async renewLease(id, instanceId, lockedUntil, now) {
337
+ const em = this.mikro.em.fork();
338
+ const repo = em.getRepository(BackgroundJobEntitySchema);
339
+ const updated = await repo.nativeUpdate({
340
+ id,
341
+ lockedBy: instanceId,
342
+ status: 'running',
343
+ lockedUntil: { $gt: now, $lt: lockedUntil },
344
+ }, { lockedUntil });
345
+ return updated === 1;
346
+ }
347
+ async completeJobSuccess(input) {
348
+ const em = this.mikro.em.fork();
349
+ const repo = em.getRepository(BackgroundJobEntitySchema);
350
+ const updated = await repo.nativeUpdate({
351
+ id: input.id,
352
+ lockedBy: input.instanceId,
353
+ status: 'running',
354
+ lockedUntil: { $gt: input.now },
355
+ }, {
356
+ status: 'succeeded',
357
+ lockedBy: null,
358
+ lockedUntil: null,
359
+ completedAt: input.now,
360
+ });
361
+ return updated === 1;
362
+ }
363
+ async completeJobFailure(input) {
364
+ const em = this.mikro.em.fork();
365
+ const repo = em.getRepository(BackgroundJobEntitySchema);
366
+ const updated = await repo.nativeUpdate({
367
+ id: input.id,
368
+ lockedBy: input.instanceId,
369
+ status: 'running',
370
+ lockedUntil: { $gt: input.now },
371
+ }, {
372
+ status: input.retryAt ? 'pending' : 'failed',
373
+ availableAt: input.retryAt ?? input.now,
374
+ lockedBy: null,
375
+ lockedUntil: null,
376
+ attemptCount: input.attemptCount,
377
+ lastError: input.error,
378
+ completedAt: input.retryAt ? null : input.now,
379
+ });
380
+ return updated === 1;
381
+ }
382
+ async cleanupCompletedJobs(before) {
383
+ const em = this.mikro.em.fork();
384
+ const repo = em.getRepository(BackgroundJobEntitySchema);
385
+ return repo.nativeDelete({
386
+ status: { $in: ['succeeded', 'failed'] },
387
+ completedAt: { $lte: before },
388
+ });
389
+ }
390
+ }
391
+ function createDatabaseSchedulerConfig(options, mikro) {
392
+ return {
393
+ cleanupCron: options.cleanupCron,
394
+ async start({ scheduledJobs, backgroundJobs, logger }) {
395
+ const scheduledRunner = new DistributedSchedulerRunner({
396
+ name: 'Database',
397
+ pollIntervalMs: options.pollIntervalMs,
398
+ lockTtlMs: options.lockTtlMs,
399
+ instanceId: options.instanceId,
400
+ jobs: scheduledJobs,
401
+ logger,
402
+ store: new DatabaseSchedulerStore(mikro),
403
+ });
404
+ const backgroundRunner = new DistributedBackgroundJobRunner({
405
+ name: 'Database',
406
+ pollIntervalMs: options.pollIntervalMs,
407
+ lockTtlMs: options.lockTtlMs,
408
+ retryDelayMs: options.backgroundRetryDelayMs,
409
+ maxAttempts: options.backgroundMaxAttempts,
410
+ retentionMs: options.backgroundRetentionMs,
411
+ instanceId: options.instanceId,
412
+ jobs: backgroundJobs,
413
+ logger,
414
+ store: new DatabaseBackgroundJobStore(mikro),
415
+ });
416
+ const scheduledHandle = await scheduledRunner.start();
417
+ const backgroundHandle = backgroundRunner.start();
418
+ return {
419
+ stop: async () => {
420
+ await backgroundHandle.stop();
421
+ await scheduledHandle.stop();
422
+ },
423
+ getNextRunAt: () => scheduledHandle.getNextRunAt?.() ?? null,
424
+ enqueue: async (enqueueOptions) => {
425
+ if (!backgroundHandle.enqueue) {
426
+ throw new Error('Background jobs require a durable scheduler backend');
427
+ }
428
+ return backgroundHandle.enqueue(enqueueOptions);
429
+ },
430
+ };
431
+ },
432
+ };
433
+ }
434
+ function hasBoundMikro(options) {
435
+ return 'mikro' in options;
436
+ }
437
+ export function database(options = {}) {
438
+ const resolved = resolveOptions(options);
439
+ if (hasBoundMikro(options)) {
440
+ return createDatabaseSchedulerConfig(resolved, options.mikro);
441
+ }
442
+ return ({ mikro }) => createDatabaseSchedulerConfig(resolved, mikro);
443
+ }
444
+ //# sourceMappingURL=database.js.map