oqronkit 0.0.1-alpha.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.

Potentially problematic release.


This version of oqronkit might be problematic. Click here for more details.

Files changed (109) hide show
  1. package/README.md +127 -0
  2. package/dist/chunk-I6QFT3MR.mjs +1732 -0
  3. package/dist/chunk-PLN5A6LU.mjs +1447 -0
  4. package/dist/core/config/config-loader.d.ts +3 -0
  5. package/dist/core/config/config-loader.d.ts.map +1 -0
  6. package/dist/core/config/default-config.d.ts +4 -0
  7. package/dist/core/config/default-config.d.ts.map +1 -0
  8. package/dist/core/config/define-config.d.ts +3 -0
  9. package/dist/core/config/define-config.d.ts.map +1 -0
  10. package/dist/core/config/find-up.d.ts +2 -0
  11. package/dist/core/config/find-up.d.ts.map +1 -0
  12. package/dist/core/config/schema.d.ts +306 -0
  13. package/dist/core/config/schema.d.ts.map +1 -0
  14. package/dist/core/context/cron-context.d.ts +14 -0
  15. package/dist/core/context/cron-context.d.ts.map +1 -0
  16. package/dist/core/context/cron-context.interface.d.ts +14 -0
  17. package/dist/core/context/cron-context.interface.d.ts.map +1 -0
  18. package/dist/core/context/job-context.d.ts +22 -0
  19. package/dist/core/context/job-context.d.ts.map +1 -0
  20. package/dist/core/context/schedule-context.d.ts +31 -0
  21. package/dist/core/context/schedule-context.d.ts.map +1 -0
  22. package/dist/core/errors/base.error.d.ts +13 -0
  23. package/dist/core/errors/base.error.d.ts.map +1 -0
  24. package/dist/core/events/event-bus.d.ts +16 -0
  25. package/dist/core/events/event-bus.d.ts.map +1 -0
  26. package/dist/core/index.d.ts +24 -0
  27. package/dist/core/index.d.ts.map +1 -0
  28. package/dist/core/logger/index.d.ts +30 -0
  29. package/dist/core/logger/index.d.ts.map +1 -0
  30. package/dist/core/registry.d.ts +13 -0
  31. package/dist/core/registry.d.ts.map +1 -0
  32. package/dist/core/types/config.types.d.ts +119 -0
  33. package/dist/core/types/config.types.d.ts.map +1 -0
  34. package/dist/core/types/cron.types.d.ts +53 -0
  35. package/dist/core/types/cron.types.d.ts.map +1 -0
  36. package/dist/core/types/db.types.d.ts +32 -0
  37. package/dist/core/types/db.types.d.ts.map +1 -0
  38. package/dist/core/types/index.d.ts +7 -0
  39. package/dist/core/types/index.d.ts.map +1 -0
  40. package/dist/core/types/lock.types.d.ts +7 -0
  41. package/dist/core/types/lock.types.d.ts.map +1 -0
  42. package/dist/core/types/module.types.d.ts +8 -0
  43. package/dist/core/types/module.types.d.ts.map +1 -0
  44. package/dist/core/types/scheduler.types.d.ts +62 -0
  45. package/dist/core/types/scheduler.types.d.ts.map +1 -0
  46. package/dist/cron-V0k1GcxJ.d.mts +102 -0
  47. package/dist/cron-V0k1GcxJ.d.ts +102 -0
  48. package/dist/cron.d.mts +2 -0
  49. package/dist/cron.d.ts +2 -0
  50. package/dist/cron.d.ts.map +1 -0
  51. package/dist/cron.js +215 -0
  52. package/dist/cron.mjs +1 -0
  53. package/dist/db/adapters/memory.adapter.d.ts +25 -0
  54. package/dist/db/adapters/memory.adapter.d.ts.map +1 -0
  55. package/dist/db/adapters/namespaced.adapter.d.ts +26 -0
  56. package/dist/db/adapters/namespaced.adapter.d.ts.map +1 -0
  57. package/dist/db/adapters/sqlite.adapter.d.ts +30 -0
  58. package/dist/db/adapters/sqlite.adapter.d.ts.map +1 -0
  59. package/dist/db/index.d.ts +4 -0
  60. package/dist/db/index.d.ts.map +1 -0
  61. package/dist/index.d.mts +797 -0
  62. package/dist/index.d.ts +32 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +2738 -0
  65. package/dist/index.mjs +747 -0
  66. package/dist/lock/adapters/db-lock.adapter.d.ts +17 -0
  67. package/dist/lock/adapters/db-lock.adapter.d.ts.map +1 -0
  68. package/dist/lock/adapters/memory-lock.adapter.d.ts +13 -0
  69. package/dist/lock/adapters/memory-lock.adapter.d.ts.map +1 -0
  70. package/dist/lock/adapters/namespaced-lock.adapter.d.ts +12 -0
  71. package/dist/lock/adapters/namespaced-lock.adapter.d.ts.map +1 -0
  72. package/dist/lock/heartbeat-worker.d.ts +16 -0
  73. package/dist/lock/heartbeat-worker.d.ts.map +1 -0
  74. package/dist/lock/index.d.ts +7 -0
  75. package/dist/lock/index.d.ts.map +1 -0
  76. package/dist/lock/leader-election.d.ts +16 -0
  77. package/dist/lock/leader-election.d.ts.map +1 -0
  78. package/dist/lock/stall-detector.d.ts +23 -0
  79. package/dist/lock/stall-detector.d.ts.map +1 -0
  80. package/dist/scheduler/cron-engine.d.ts +42 -0
  81. package/dist/scheduler/cron-engine.d.ts.map +1 -0
  82. package/dist/scheduler/define-cron.d.ts +36 -0
  83. package/dist/scheduler/define-cron.d.ts.map +1 -0
  84. package/dist/scheduler/define-schedule.d.ts +52 -0
  85. package/dist/scheduler/define-schedule.d.ts.map +1 -0
  86. package/dist/scheduler/expression-parser.d.ts +2 -0
  87. package/dist/scheduler/expression-parser.d.ts.map +1 -0
  88. package/dist/scheduler/index.d.ts +10 -0
  89. package/dist/scheduler/index.d.ts.map +1 -0
  90. package/dist/scheduler/missed-fire.handler.d.ts +8 -0
  91. package/dist/scheduler/missed-fire.handler.d.ts.map +1 -0
  92. package/dist/scheduler/registry-schedule.d.ts +6 -0
  93. package/dist/scheduler/registry-schedule.d.ts.map +1 -0
  94. package/dist/scheduler/registry.d.ts +6 -0
  95. package/dist/scheduler/registry.d.ts.map +1 -0
  96. package/dist/scheduler/schedule-engine.d.ts +36 -0
  97. package/dist/scheduler/schedule-engine.d.ts.map +1 -0
  98. package/dist/scheduler-HRR3UXGE.mjs +1 -0
  99. package/dist/scheduler.d.mts +248 -0
  100. package/dist/scheduler.d.ts +248 -0
  101. package/dist/scheduler.js +1461 -0
  102. package/dist/scheduler.mjs +1 -0
  103. package/dist/server/express.d.ts +9 -0
  104. package/dist/server/express.d.ts.map +1 -0
  105. package/dist/server/fastify.d.ts +9 -0
  106. package/dist/server/fastify.d.ts.map +1 -0
  107. package/dist/server/handlers.d.ts +15 -0
  108. package/dist/server/handlers.d.ts.map +1 -0
  109. package/package.json +59 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,747 @@
1
+ import { OqronEventBus, createLogger, OqronRegistry, reconfigureConfig, loadConfig, MemoryLockAdapter, DbLockAdapter, NamespacedLockAdapter } from './chunk-I6QFT3MR.mjs';
2
+ export { DbLockAdapter, MemoryLockAdapter, OqronEventBus, createLogger, cron, defineConfig, schedule } from './chunk-I6QFT3MR.mjs';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { pathToFileURL } from 'url';
6
+ import Database from 'better-sqlite3';
7
+
8
+ // src/db/adapters/memory.adapter.ts
9
+ var MemoryOqronAdapter = class {
10
+ schedules = /* @__PURE__ */ new Map();
11
+ jobs = /* @__PURE__ */ new Map();
12
+ async upsertSchedule(def) {
13
+ if (!this.schedules.has(def.name)) {
14
+ this.schedules.set(def.name, {
15
+ name: def.name,
16
+ nextRunAt: null,
17
+ lastRunAt: null
18
+ });
19
+ }
20
+ }
21
+ async getDueSchedules(now, limit, prefix) {
22
+ const due = [];
23
+ for (const s of this.schedules.values()) {
24
+ if (prefix && !s.name.startsWith(prefix)) continue;
25
+ if (s.nextRunAt !== null && s.nextRunAt <= now) {
26
+ due.push({ name: s.name });
27
+ }
28
+ if (due.length >= limit) break;
29
+ }
30
+ return due;
31
+ }
32
+ async getSchedules(prefix) {
33
+ const filtered = prefix ? Array.from(this.schedules.values()).filter(
34
+ (s) => s.name.startsWith(prefix)
35
+ ) : Array.from(this.schedules.values());
36
+ return filtered.map((s) => ({
37
+ name: s.name,
38
+ lastRunAt: s.lastRunAt,
39
+ nextRunAt: s.nextRunAt
40
+ }));
41
+ }
42
+ async updateNextRun(scheduleId, nextRunAt) {
43
+ const s = this.schedules.get(scheduleId);
44
+ if (s) {
45
+ s.nextRunAt = nextRunAt;
46
+ }
47
+ }
48
+ async recordExecution(job) {
49
+ const existing = this.jobs.get(job.id);
50
+ if (existing) {
51
+ this.jobs.set(job.id, {
52
+ ...existing,
53
+ ...job,
54
+ progressPercent: job.progressPercent ?? existing.progressPercent,
55
+ progressLabel: job.progressLabel ?? existing.progressLabel
56
+ });
57
+ } else {
58
+ this.jobs.set(job.id, { ...job });
59
+ }
60
+ if (job.scheduleId && (job.status === "completed" || job.status === "failed")) {
61
+ const s = this.schedules.get(job.scheduleId);
62
+ if (s) {
63
+ s.lastRunAt = job.completedAt ?? /* @__PURE__ */ new Date();
64
+ }
65
+ }
66
+ }
67
+ async updateJobProgress(id, progressPercent, progressLabel) {
68
+ const job = this.jobs.get(id);
69
+ if (job) {
70
+ job.progressPercent = progressPercent;
71
+ job.progressLabel = progressLabel ?? job.progressLabel;
72
+ }
73
+ }
74
+ async getExecutions(scheduleId, opts) {
75
+ const records = Array.from(this.jobs.values()).filter((j) => j.scheduleId === scheduleId).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
76
+ return records.slice(opts.offset, opts.offset + opts.limit);
77
+ }
78
+ async getActiveJobs() {
79
+ return Array.from(this.jobs.values()).filter((j) => j.status === "running");
80
+ }
81
+ async cleanOldExecutions(before) {
82
+ let removed = 0;
83
+ for (const [id, job] of this.jobs.entries()) {
84
+ if (job.startedAt < before) {
85
+ this.jobs.delete(id);
86
+ removed++;
87
+ }
88
+ }
89
+ return removed;
90
+ }
91
+ async pruneHistoryForSchedule(scheduleId, keepJobHistory, keepFailedJobHistory) {
92
+ if (keepJobHistory === false && keepFailedJobHistory === false) return;
93
+ const all = Array.from(this.jobs.values()).filter(
94
+ (j) => j.scheduleId === scheduleId && ["completed", "failed", "dead"].includes(j.status)
95
+ );
96
+ all.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
97
+ const successes = all.filter((j) => j.status === "completed");
98
+ const failures = all.filter((j) => ["failed", "dead"].includes(j.status));
99
+ if (typeof keepJobHistory === "number" && successes.length > keepJobHistory) {
100
+ const toRemove = successes.slice(keepJobHistory);
101
+ for (const j of toRemove) this.jobs.delete(j.id);
102
+ }
103
+ if (typeof keepFailedJobHistory === "number" && failures.length > keepFailedJobHistory) {
104
+ const toRemove = failures.slice(keepFailedJobHistory);
105
+ for (const j of toRemove) this.jobs.delete(j.id);
106
+ }
107
+ }
108
+ };
109
+
110
+ // src/db/adapters/namespaced.adapter.ts
111
+ var NamespacedOqronAdapter = class {
112
+ constructor(base, project = "default", environment = "development") {
113
+ this.base = base;
114
+ this.prefix = `${project}:${environment}:`;
115
+ }
116
+ prefix;
117
+ ns(id) {
118
+ return `${this.prefix}${id}`;
119
+ }
120
+ un(id) {
121
+ return id.startsWith(this.prefix) ? id.slice(this.prefix.length) : id;
122
+ }
123
+ async upsertSchedule(def) {
124
+ return this.base.upsertSchedule({ ...def, name: this.ns(def.name) });
125
+ }
126
+ async getDueSchedules(now, limit, prefix) {
127
+ const combinedPrefix = prefix ? `${this.prefix}${prefix}` : this.prefix;
128
+ const records = await this.base.getDueSchedules(now, limit, combinedPrefix);
129
+ return records.filter((r) => r.name.startsWith(this.prefix)).map((r) => ({ name: this.un(r.name) }));
130
+ }
131
+ async getSchedules(prefix) {
132
+ const combinedPrefix = prefix ? `${this.prefix}${prefix}` : this.prefix;
133
+ const records = await this.base.getSchedules(combinedPrefix);
134
+ return records.filter((r) => r.name.startsWith(this.prefix)).map((r) => ({
135
+ ...r,
136
+ name: this.un(r.name)
137
+ }));
138
+ }
139
+ async updateNextRun(scheduleId, nextRunAt) {
140
+ return this.base.updateNextRun(this.ns(scheduleId), nextRunAt);
141
+ }
142
+ async recordExecution(job) {
143
+ return this.base.recordExecution({
144
+ ...job,
145
+ scheduleId: job.scheduleId ? this.ns(job.scheduleId) : void 0
146
+ });
147
+ }
148
+ async updateJobProgress(id, progressPercent, progressLabel) {
149
+ return this.base.updateJobProgress(id, progressPercent, progressLabel);
150
+ }
151
+ async getExecutions(scheduleId, opts) {
152
+ const records = await this.base.getExecutions(this.ns(scheduleId), opts);
153
+ return records.map((r) => ({
154
+ ...r,
155
+ scheduleId: r.scheduleId ? this.un(r.scheduleId) : void 0
156
+ }));
157
+ }
158
+ async getActiveJobs() {
159
+ const records = await this.base.getActiveJobs();
160
+ return records.filter((r) => r.scheduleId?.startsWith(this.prefix)).map((r) => ({
161
+ ...r,
162
+ scheduleId: r.scheduleId ? this.un(r.scheduleId) : void 0
163
+ }));
164
+ }
165
+ async cleanOldExecutions(before) {
166
+ return this.base.cleanOldExecutions(before);
167
+ }
168
+ async pruneHistoryForSchedule(scheduleId, keepJobHistory, keepFailedJobHistory) {
169
+ return this.base.pruneHistoryForSchedule(
170
+ this.ns(scheduleId),
171
+ keepJobHistory,
172
+ keepFailedJobHistory
173
+ );
174
+ }
175
+ };
176
+ var SqliteAdapter = class {
177
+ db;
178
+ /**
179
+ * @param dbOrPath - Either a `better-sqlite3` Database instance (for testing with `:memory:`)
180
+ * or a file path string. Defaults to `"oqron.sqlite"`.
181
+ */
182
+ constructor(dbOrPath = "oqron.sqlite") {
183
+ if (typeof dbOrPath === "string") {
184
+ this.db = new Database(dbOrPath);
185
+ } else {
186
+ this.db = dbOrPath;
187
+ }
188
+ this.db.pragma("journal_mode = WAL");
189
+ this.db.pragma("synchronous = NORMAL");
190
+ this.db.pragma("foreign_keys = ON");
191
+ this.migrate();
192
+ }
193
+ /** Create tables if they don't exist */
194
+ migrate() {
195
+ this.db.exec(`
196
+ CREATE TABLE IF NOT EXISTS oqron_schedules (
197
+ id TEXT PRIMARY KEY,
198
+ expression TEXT, -- Can be null now since rrule/runAt are supported
199
+ timezone TEXT,
200
+ missedFirePolicy TEXT NOT NULL DEFAULT 'skip',
201
+ overlap INTEGER NOT NULL DEFAULT 1,
202
+ tags TEXT NOT NULL DEFAULT '[]',
203
+
204
+ -- Advanced Scheduling Columns
205
+ runAt TEXT,
206
+ runAfterOpts TEXT,
207
+ rrule TEXT,
208
+ recurring TEXT,
209
+
210
+ lastRunAt TEXT,
211
+ nextRunAt TEXT
212
+ );
213
+
214
+ CREATE TABLE IF NOT EXISTS oqron_jobs (
215
+ id TEXT PRIMARY KEY,
216
+ scheduleId TEXT,
217
+ status TEXT NOT NULL,
218
+ startedAt TEXT NOT NULL,
219
+ completedAt TEXT,
220
+ error TEXT,
221
+ result TEXT,
222
+ progressPercent INTEGER,
223
+ progressLabel TEXT,
224
+ attempts INTEGER DEFAULT 1,
225
+ FOREIGN KEY(scheduleId) REFERENCES oqron_schedules(id) ON DELETE CASCADE
226
+ );
227
+
228
+ CREATE TABLE IF NOT EXISTS chrono_locks (
229
+ resourceKey TEXT PRIMARY KEY,
230
+ ownerId TEXT NOT NULL,
231
+ expiresAt TEXT NOT NULL
232
+ );
233
+ `);
234
+ const alters = [
235
+ "ALTER TABLE oqron_schedules ADD COLUMN runAt TEXT",
236
+ "ALTER TABLE oqron_schedules ADD COLUMN runAfterOpts TEXT",
237
+ "ALTER TABLE oqron_schedules ADD COLUMN rrule TEXT",
238
+ "ALTER TABLE oqron_schedules ADD COLUMN recurring TEXT",
239
+ "ALTER TABLE oqron_jobs ADD COLUMN result TEXT",
240
+ "ALTER TABLE oqron_jobs ADD COLUMN progressPercent INTEGER",
241
+ "ALTER TABLE oqron_jobs ADD COLUMN progressLabel TEXT",
242
+ "ALTER TABLE oqron_jobs ADD COLUMN attempts INTEGER",
243
+ "ALTER TABLE oqron_jobs ADD COLUMN durationMs INTEGER"
244
+ ];
245
+ for (const sql of alters) {
246
+ try {
247
+ this.db.exec(sql);
248
+ } catch (_e) {
249
+ }
250
+ }
251
+ }
252
+ async upsertSchedule(def) {
253
+ const isSchedule = "runAt" in def || "rrule" in def || "recurring" in def || "runAfter" in def;
254
+ this.db.prepare(
255
+ `INSERT INTO oqron_schedules (
256
+ id, expression, timezone, missedFirePolicy, overlap, tags,
257
+ runAt, runAfterOpts, rrule, recurring
258
+ )
259
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
260
+ ON CONFLICT(id) DO UPDATE SET
261
+ expression = excluded.expression,
262
+ timezone = excluded.timezone,
263
+ missedFirePolicy = excluded.missedFirePolicy,
264
+ overlap = excluded.overlap,
265
+ tags = excluded.tags,
266
+ runAt = excluded.runAt,
267
+ runAfterOpts = excluded.runAfterOpts,
268
+ rrule = excluded.rrule,
269
+ recurring = excluded.recurring`
270
+ ).run(
271
+ def.name,
272
+ "expression" in def ? def.expression ?? (def.intervalMs ? `every:${def.intervalMs}ms` : null) : "every" in def && def.every ? JSON.stringify(def.every) : null,
273
+ def.timezone ?? null,
274
+ def.missedFire,
275
+ def.overlap !== "skip" && def.overlap !== false ? 1 : 0,
276
+ JSON.stringify(def.tags),
277
+ isSchedule && def.runAt ? def.runAt.toISOString() : null,
278
+ isSchedule && def.runAfter ? JSON.stringify(def.runAfter) : null,
279
+ isSchedule && def.rrule ? def.rrule : null,
280
+ isSchedule && def.recurring ? JSON.stringify(def.recurring) : null
281
+ );
282
+ }
283
+ async getDueSchedules(now, limit, prefix) {
284
+ let sql = `SELECT id as name FROM oqron_schedules WHERE nextRunAt <= ?`;
285
+ const params = [now.toISOString()];
286
+ if (prefix) {
287
+ sql += ` AND id LIKE ?`;
288
+ params.push(`${prefix}%`);
289
+ }
290
+ sql += ` LIMIT ?`;
291
+ params.push(limit);
292
+ return this.db.prepare(sql).all(...params);
293
+ }
294
+ async getSchedules(prefix) {
295
+ let sql = `SELECT id, lastRunAt, nextRunAt FROM oqron_schedules`;
296
+ const params = [];
297
+ if (prefix) {
298
+ sql += ` WHERE id LIKE ?`;
299
+ params.push(`${prefix}%`);
300
+ }
301
+ const rows = this.db.prepare(sql).all(...params);
302
+ return rows.map((r) => ({
303
+ name: r.id,
304
+ lastRunAt: r.lastRunAt ? new Date(r.lastRunAt) : null,
305
+ nextRunAt: r.nextRunAt ? new Date(r.nextRunAt) : null
306
+ }));
307
+ }
308
+ async updateNextRun(scheduleId, nextRunAt) {
309
+ this.db.prepare(`UPDATE oqron_schedules SET nextRunAt = ? WHERE id = ?`).run(nextRunAt ? nextRunAt.toISOString() : null, scheduleId);
310
+ }
311
+ async updateJobProgress(id, progressPercent, progressLabel) {
312
+ this.db.prepare(
313
+ `UPDATE oqron_jobs SET progressPercent = ?, progressLabel = ? WHERE id = ?`
314
+ ).run(progressPercent, progressLabel ?? null, id);
315
+ }
316
+ async recordExecution(job) {
317
+ this.db.prepare(
318
+ `INSERT INTO oqron_jobs (id, scheduleId, status, startedAt, completedAt, error, result, attempts, progressPercent, progressLabel, durationMs)
319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
320
+ ON CONFLICT(id) DO UPDATE SET
321
+ status = excluded.status,
322
+ completedAt = excluded.completedAt,
323
+ error = excluded.error,
324
+ result = excluded.result,
325
+ attempts = excluded.attempts,
326
+ progressPercent = coalesce(excluded.progressPercent, progressPercent),
327
+ progressLabel = coalesce(excluded.progressLabel, progressLabel),
328
+ durationMs = excluded.durationMs`
329
+ ).run(
330
+ job.id,
331
+ job.scheduleId ?? null,
332
+ job.status,
333
+ job.startedAt.toISOString(),
334
+ job.completedAt?.toISOString() ?? null,
335
+ job.error ?? null,
336
+ job.result ?? null,
337
+ job.attempts ?? 1,
338
+ job.progressPercent ?? null,
339
+ job.progressLabel ?? null,
340
+ job.durationMs ?? null
341
+ );
342
+ if (job.scheduleId && (job.status === "completed" || job.status === "failed")) {
343
+ this.db.prepare(`UPDATE oqron_schedules SET lastRunAt = ? WHERE id = ?`).run((job.completedAt ?? /* @__PURE__ */ new Date()).toISOString(), job.scheduleId);
344
+ }
345
+ }
346
+ async getExecutions(scheduleId, opts) {
347
+ const rows = this.db.prepare(
348
+ `SELECT id, scheduleId, status, startedAt, completedAt, error, result, progressPercent, progressLabel, attempts, durationMs
349
+ FROM oqron_jobs WHERE scheduleId = ?
350
+ ORDER BY startedAt DESC
351
+ LIMIT ? OFFSET ?`
352
+ ).all(scheduleId, opts.limit, opts.offset);
353
+ return rows.map((r) => ({
354
+ id: r.id,
355
+ scheduleId: r.scheduleId ?? void 0,
356
+ status: r.status,
357
+ startedAt: new Date(r.startedAt),
358
+ completedAt: r.completedAt ? new Date(r.completedAt) : void 0,
359
+ error: r.error ?? void 0,
360
+ result: r.result ?? void 0,
361
+ progressPercent: r.progressPercent ?? void 0,
362
+ progressLabel: r.progressLabel ?? void 0,
363
+ attempts: r.attempts ?? void 0,
364
+ durationMs: r.durationMs ?? void 0
365
+ }));
366
+ }
367
+ async getActiveJobs() {
368
+ const rows = this.db.prepare(
369
+ `SELECT id, scheduleId, status, startedAt, completedAt, error, result, progressPercent, progressLabel, attempts, durationMs
370
+ FROM oqron_jobs WHERE status = 'running'`
371
+ ).all();
372
+ return rows.map((r) => ({
373
+ id: r.id,
374
+ scheduleId: r.scheduleId ?? void 0,
375
+ status: r.status,
376
+ startedAt: new Date(r.startedAt),
377
+ completedAt: r.completedAt ? new Date(r.completedAt) : void 0,
378
+ error: r.error ?? void 0,
379
+ result: r.result ?? void 0,
380
+ progressPercent: r.progressPercent ?? void 0,
381
+ progressLabel: r.progressLabel ?? void 0,
382
+ attempts: r.attempts ?? void 0,
383
+ durationMs: r.durationMs ?? void 0
384
+ }));
385
+ }
386
+ async cleanOldExecutions(before) {
387
+ const result = this.db.prepare(`DELETE FROM oqron_jobs WHERE startedAt < ?`).run(before.toISOString());
388
+ return result.changes;
389
+ }
390
+ async pruneHistoryForSchedule(scheduleId, keepJobHistory, keepFailedJobHistory) {
391
+ if (keepJobHistory === false && keepFailedJobHistory === false) return;
392
+ if (typeof keepJobHistory === "number") {
393
+ this.db.prepare(
394
+ `
395
+ DELETE FROM oqron_jobs
396
+ WHERE scheduleId = ? AND status = 'completed'
397
+ AND id NOT IN (
398
+ SELECT id FROM oqron_jobs
399
+ WHERE scheduleId = ? AND status = 'completed'
400
+ ORDER BY startedAt DESC LIMIT ?
401
+ )
402
+ `
403
+ ).run(scheduleId, scheduleId, keepJobHistory);
404
+ }
405
+ if (typeof keepFailedJobHistory === "number") {
406
+ this.db.prepare(
407
+ `
408
+ DELETE FROM oqron_jobs
409
+ WHERE scheduleId = ? AND status IN ('failed', 'dead')
410
+ AND id NOT IN (
411
+ SELECT id FROM oqron_jobs
412
+ WHERE scheduleId = ? AND status IN ('failed', 'dead')
413
+ ORDER BY startedAt DESC LIMIT ?
414
+ )
415
+ `
416
+ ).run(scheduleId, scheduleId, keepFailedJobHistory);
417
+ }
418
+ }
419
+ };
420
+
421
+ // src/server/handlers.ts
422
+ var recentEvents = [];
423
+ function appendEvent(event, data) {
424
+ recentEvents.unshift({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, data });
425
+ if (recentEvents.length > 500) recentEvents.pop();
426
+ }
427
+ OqronEventBus.on(
428
+ "job:start",
429
+ (jobId, mod) => appendEvent("job:start", { jobId, mod })
430
+ );
431
+ OqronEventBus.on(
432
+ "job:success",
433
+ (jobId) => appendEvent("job:success", { jobId })
434
+ );
435
+ OqronEventBus.on(
436
+ "job:fail",
437
+ (jobId, err) => appendEvent("job:fail", { jobId, error: err.message })
438
+ );
439
+ OqronEventBus.on("system:ready", () => appendEvent("system:ready", {}));
440
+ OqronEventBus.on("system:stop", () => appendEvent("system:stop", {}));
441
+ async function handleHealth(_req) {
442
+ return {
443
+ status: 200,
444
+ body: {
445
+ ok: true,
446
+ status: "running",
447
+ uptime: process.uptime(),
448
+ env: process.env.CHRONO_ENV ?? "development",
449
+ ts: (/* @__PURE__ */ new Date()).toISOString()
450
+ }
451
+ };
452
+ }
453
+ async function handleEvents(req) {
454
+ const limit = Math.min(Number(req.query.limit ?? 50), 200);
455
+ return {
456
+ status: 200,
457
+ body: { ok: true, events: recentEvents.slice(0, limit) }
458
+ };
459
+ }
460
+ async function handleTrigger(req) {
461
+ const id = req.params.id;
462
+ if (!id)
463
+ return { status: 400, body: { ok: false, error: "Missing :id param" } };
464
+ appendEvent("manual:trigger", { id });
465
+ return {
466
+ status: 200,
467
+ body: { ok: true, message: `Trigger queued for "${id}"` }
468
+ };
469
+ }
470
+ async function dispatch(req) {
471
+ const { method, path: path2 } = req;
472
+ if (method === "GET" && path2 === "/health") return handleHealth();
473
+ if (method === "GET" && path2 === "/events") return handleEvents(req);
474
+ if (method === "POST" && path2.startsWith("/jobs/")) return handleTrigger(req);
475
+ return { status: 404, body: { ok: false, error: "Not found" } };
476
+ }
477
+
478
+ // src/server/express.ts
479
+ function expressRouter() {
480
+ return async function chronoMiddleware(req, res, next) {
481
+ const matchPath = req.path;
482
+ try {
483
+ const result = await dispatch({
484
+ method: req.method,
485
+ path: matchPath,
486
+ query: req.query || {},
487
+ params: req.params || {}
488
+ });
489
+ if (result.status === 404 && result.body && result.body.error === "Not found") {
490
+ return next();
491
+ }
492
+ res.status(result.status).json(result.body);
493
+ } catch (err) {
494
+ next(err);
495
+ }
496
+ };
497
+ }
498
+
499
+ // src/server/fastify.ts
500
+ function fastifyPlugin(fastify, _opts, done) {
501
+ fastify.all("*", async (req, reply) => {
502
+ const pathParams = req.params["*"];
503
+ const path2 = pathParams ? `/${pathParams}` : req.url;
504
+ const result = await dispatch({
505
+ method: req.method,
506
+ path: path2,
507
+ query: req.query,
508
+ params: req.params
509
+ });
510
+ return reply.status(result.status).send(result.body);
511
+ });
512
+ done();
513
+ }
514
+
515
+ // src/index.ts
516
+ var _config = null;
517
+ var _db = null;
518
+ var _lock = null;
519
+ var _logger = null;
520
+ var OqronKit = {
521
+ /**
522
+ * Initialize OqronKit: loads config, auto-discovers jobs, and boots modules.
523
+ *
524
+ * @param opts.cwd - Working directory for config file lookup and jobsDir resolution
525
+ * @param opts.config - Explicit config object (skips loadConfig)
526
+ */
527
+ async init(opts) {
528
+ const cwd = opts?.cwd ?? process.cwd();
529
+ _config = reconfigureConfig(opts?.config ?? await loadConfig(cwd));
530
+ const loggerConfig = _config.logger === false ? { enabled: true } : _config.logger;
531
+ _logger = createLogger(loggerConfig, { module: "oqronkit" });
532
+ _logger.info(`Starting OqronKit in "${_config.environment}" environment`);
533
+ if (!_config.db) {
534
+ const msg = "No 'db' adapter configured. Falling back to ephemeral 'MemoryOqronAdapter'. [WARNING: Data will not persist across restarts]";
535
+ if (_config.environment === "production") {
536
+ _logger.fatal(`STERN WARNING: ${msg}`);
537
+ } else {
538
+ _logger.warn(msg);
539
+ }
540
+ _db = new MemoryOqronAdapter();
541
+ } else if ("adapter" in _config.db) {
542
+ const { adapter, url } = _config.db;
543
+ if (adapter === "sqlite") {
544
+ _db = new SqliteAdapter(url ?? "oqron.sqlite");
545
+ } else if (adapter === "memory") {
546
+ const msg = "Using ephemeral 'memory' database adapter. [WARNING: Data will not persist across restarts]";
547
+ if (_config.environment === "production") {
548
+ _logger.fatal(`STERN WARNING: ${msg}`);
549
+ } else {
550
+ _logger.warn(msg);
551
+ }
552
+ _db = new MemoryOqronAdapter();
553
+ } else {
554
+ throw new Error(
555
+ `[OqronKit] Database adapter '${adapter}' not yet bundled. Please pass a custom IOqronAdapter instance.`
556
+ );
557
+ }
558
+ } else {
559
+ _db = _config.db;
560
+ }
561
+ if (!_config.lock) {
562
+ _logger.warn(
563
+ "No 'lock' adapter configured. Falling back to ephemeral 'MemoryLockAdapter'."
564
+ );
565
+ _lock = new MemoryLockAdapter();
566
+ } else if ("adapter" in _config.lock) {
567
+ const { adapter, url, ttl } = _config.lock;
568
+ if (adapter === "db") {
569
+ if (_db instanceof SqliteAdapter) {
570
+ _lock = new DbLockAdapter(_db.db, ttl);
571
+ } else {
572
+ _lock = new DbLockAdapter(url ?? "oqron.sqlite", ttl);
573
+ }
574
+ } else if (adapter === "memory") {
575
+ _lock = new MemoryLockAdapter();
576
+ } else {
577
+ throw new Error(
578
+ `[OqronKit] Lock adapter '${adapter}' not yet bundled. Please pass a custom ILockAdapter instance.`
579
+ );
580
+ }
581
+ } else {
582
+ _lock = _config.lock;
583
+ }
584
+ if (_config.shutdown.enabled) {
585
+ for (const signal of _config.shutdown.signals) {
586
+ process.on(signal, () => {
587
+ _logger?.info(`${signal} received \u2014 initiating graceful shutdown\u2026`);
588
+ void this.stop().then(() => process.exit(0));
589
+ });
590
+ }
591
+ }
592
+ if (_config.modules.includes("cron")) {
593
+ const { SchedulerModule, _drainPending } = await import('./scheduler-HRR3UXGE.mjs');
594
+ if (_config.jobsDir) {
595
+ const jobsPath = path.resolve(cwd, _config.jobsDir);
596
+ _logger.debug(`Scanning jobsDir: ${jobsPath}`);
597
+ try {
598
+ async function scan(dir) {
599
+ try {
600
+ const entries = await fs.readdir(dir, { withFileTypes: true });
601
+ for (const entry of entries) {
602
+ const fullPath = path.join(dir, entry.name);
603
+ if (entry.isDirectory()) {
604
+ await scan(fullPath);
605
+ } else if (entry.isFile() && /\.(js|ts|mjs|cjs)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
606
+ _logger?.debug(`Auto-importing job file: ${entry.name}`);
607
+ await import(pathToFileURL(fullPath).toString());
608
+ }
609
+ }
610
+ } catch (err) {
611
+ if (err.code !== "ENOENT") throw err;
612
+ }
613
+ }
614
+ await scan(jobsPath);
615
+ } catch (err) {
616
+ _logger.warn(`Failed to scan jobsDir: ${_config.jobsDir}`, {
617
+ error: String(err)
618
+ });
619
+ }
620
+ }
621
+ const schedules = _drainPending();
622
+ for (const s of schedules) {
623
+ s.tags = [.../* @__PURE__ */ new Set([...s.tags ?? [], ..._config.tags])];
624
+ }
625
+ const nsDb = new NamespacedOqronAdapter(
626
+ _db,
627
+ _config.project,
628
+ _config.environment
629
+ );
630
+ const nsLock = new NamespacedLockAdapter(
631
+ _lock,
632
+ _config.project,
633
+ _config.environment
634
+ );
635
+ const scheduler = new SchedulerModule(
636
+ schedules,
637
+ nsDb,
638
+ nsLock,
639
+ _logger,
640
+ _config.environment,
641
+ _config.project,
642
+ _config.cron
643
+ );
644
+ OqronRegistry.getInstance().register(scheduler);
645
+ }
646
+ if (_config.modules.includes("scheduler")) {
647
+ const { ScheduleEngine, _drainPendingSchedules } = await import('./scheduler-HRR3UXGE.mjs');
648
+ if (!_config.modules.includes("cron") && _config.jobsDir) {
649
+ _logger.warn(
650
+ "jobsDir scanning currently bound to cron module block. Consider enabling 'cron' module or creating global scanner."
651
+ );
652
+ }
653
+ const schedules = _drainPendingSchedules();
654
+ for (const s of schedules) {
655
+ s.tags = [.../* @__PURE__ */ new Set([...s.tags ?? [], ..._config.tags])];
656
+ }
657
+ const nsDb = new NamespacedOqronAdapter(
658
+ _db,
659
+ _config.project,
660
+ _config.environment
661
+ );
662
+ const nsLock = new NamespacedLockAdapter(
663
+ _lock,
664
+ _config.project,
665
+ _config.environment
666
+ );
667
+ const engine = new ScheduleEngine(
668
+ schedules,
669
+ nsDb,
670
+ nsLock,
671
+ _logger,
672
+ _config.environment,
673
+ _config.project,
674
+ _config.scheduler
675
+ );
676
+ OqronRegistry.getInstance().register(engine);
677
+ }
678
+ const registry = OqronRegistry.getInstance();
679
+ const modules = registry.getAll();
680
+ for (const mod of modules) {
681
+ if (mod.enabled) {
682
+ _logger.debug(`init() \u2192 ${mod.name}`);
683
+ await mod.init();
684
+ }
685
+ }
686
+ for (const mod of modules) {
687
+ if (mod.enabled) {
688
+ _logger.info(`start() \u2192 ${mod.name}`);
689
+ await mod.start();
690
+ }
691
+ }
692
+ _logger.info("OqronKit ready \u2713");
693
+ },
694
+ /** Gracefully stop all modules */
695
+ async stop() {
696
+ const log = _logger ?? createLogger({ enabled: true, level: "info" }, { module: "oqronkit" });
697
+ log.info("Stopping OqronKit\u2026");
698
+ const timeoutMs = _config?.shutdown.timeout ?? 3e4;
699
+ const registry = OqronRegistry.getInstance();
700
+ const stopPromise = Promise.all(
701
+ registry.getAll().filter((m) => m.enabled).map((m) => m.stop())
702
+ );
703
+ const timeoutPromise = new Promise(
704
+ (_, reject) => setTimeout(
705
+ () => reject(new Error(`Graceful shutdown timed out after ${timeoutMs}ms`)),
706
+ timeoutMs
707
+ )
708
+ );
709
+ try {
710
+ await Promise.race([stopPromise, timeoutPromise]);
711
+ log.info("OqronKit stopped.");
712
+ } catch (err) {
713
+ log.error("Error during stop or shutdown timeout", {
714
+ error: String(err)
715
+ });
716
+ }
717
+ },
718
+ /** Get the current validated config */
719
+ getConfig() {
720
+ if (!_config)
721
+ throw new Error(
722
+ "[OqronKit] Not initialized yet. Call OqronKit.init() first."
723
+ );
724
+ return _config;
725
+ },
726
+ /** Get the database adapter (available after init()) */
727
+ getDb() {
728
+ if (!_db) throw new Error("[OqronKit] Not initialized yet.");
729
+ return _db;
730
+ },
731
+ /** Get the lock adapter (available after init()) */
732
+ getLock() {
733
+ if (!_lock) throw new Error("[OqronKit] Not initialized yet.");
734
+ return _lock;
735
+ },
736
+ /** Get the Express router for monitoring */
737
+ expressRouter() {
738
+ return expressRouter();
739
+ },
740
+ /** Get the Fastify plugin for monitoring */
741
+ fastifyPlugin(fastify, opts, done) {
742
+ return fastifyPlugin(fastify, opts, done);
743
+ }
744
+ };
745
+ var index_default = OqronKit;
746
+
747
+ export { OqronKit, SqliteAdapter, index_default as default };