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.
- package/README.md +127 -0
- package/dist/chunk-I6QFT3MR.mjs +1732 -0
- package/dist/chunk-PLN5A6LU.mjs +1447 -0
- package/dist/core/config/config-loader.d.ts +3 -0
- package/dist/core/config/config-loader.d.ts.map +1 -0
- package/dist/core/config/default-config.d.ts +4 -0
- package/dist/core/config/default-config.d.ts.map +1 -0
- package/dist/core/config/define-config.d.ts +3 -0
- package/dist/core/config/define-config.d.ts.map +1 -0
- package/dist/core/config/find-up.d.ts +2 -0
- package/dist/core/config/find-up.d.ts.map +1 -0
- package/dist/core/config/schema.d.ts +306 -0
- package/dist/core/config/schema.d.ts.map +1 -0
- package/dist/core/context/cron-context.d.ts +14 -0
- package/dist/core/context/cron-context.d.ts.map +1 -0
- package/dist/core/context/cron-context.interface.d.ts +14 -0
- package/dist/core/context/cron-context.interface.d.ts.map +1 -0
- package/dist/core/context/job-context.d.ts +22 -0
- package/dist/core/context/job-context.d.ts.map +1 -0
- package/dist/core/context/schedule-context.d.ts +31 -0
- package/dist/core/context/schedule-context.d.ts.map +1 -0
- package/dist/core/errors/base.error.d.ts +13 -0
- package/dist/core/errors/base.error.d.ts.map +1 -0
- package/dist/core/events/event-bus.d.ts +16 -0
- package/dist/core/events/event-bus.d.ts.map +1 -0
- package/dist/core/index.d.ts +24 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/logger/index.d.ts +30 -0
- package/dist/core/logger/index.d.ts.map +1 -0
- package/dist/core/registry.d.ts +13 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/types/config.types.d.ts +119 -0
- package/dist/core/types/config.types.d.ts.map +1 -0
- package/dist/core/types/cron.types.d.ts +53 -0
- package/dist/core/types/cron.types.d.ts.map +1 -0
- package/dist/core/types/db.types.d.ts +32 -0
- package/dist/core/types/db.types.d.ts.map +1 -0
- package/dist/core/types/index.d.ts +7 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/lock.types.d.ts +7 -0
- package/dist/core/types/lock.types.d.ts.map +1 -0
- package/dist/core/types/module.types.d.ts +8 -0
- package/dist/core/types/module.types.d.ts.map +1 -0
- package/dist/core/types/scheduler.types.d.ts +62 -0
- package/dist/core/types/scheduler.types.d.ts.map +1 -0
- package/dist/cron-V0k1GcxJ.d.mts +102 -0
- package/dist/cron-V0k1GcxJ.d.ts +102 -0
- package/dist/cron.d.mts +2 -0
- package/dist/cron.d.ts +2 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +215 -0
- package/dist/cron.mjs +1 -0
- package/dist/db/adapters/memory.adapter.d.ts +25 -0
- package/dist/db/adapters/memory.adapter.d.ts.map +1 -0
- package/dist/db/adapters/namespaced.adapter.d.ts +26 -0
- package/dist/db/adapters/namespaced.adapter.d.ts.map +1 -0
- package/dist/db/adapters/sqlite.adapter.d.ts +30 -0
- package/dist/db/adapters/sqlite.adapter.d.ts.map +1 -0
- package/dist/db/index.d.ts +4 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/index.d.mts +797 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2738 -0
- package/dist/index.mjs +747 -0
- package/dist/lock/adapters/db-lock.adapter.d.ts +17 -0
- package/dist/lock/adapters/db-lock.adapter.d.ts.map +1 -0
- package/dist/lock/adapters/memory-lock.adapter.d.ts +13 -0
- package/dist/lock/adapters/memory-lock.adapter.d.ts.map +1 -0
- package/dist/lock/adapters/namespaced-lock.adapter.d.ts +12 -0
- package/dist/lock/adapters/namespaced-lock.adapter.d.ts.map +1 -0
- package/dist/lock/heartbeat-worker.d.ts +16 -0
- package/dist/lock/heartbeat-worker.d.ts.map +1 -0
- package/dist/lock/index.d.ts +7 -0
- package/dist/lock/index.d.ts.map +1 -0
- package/dist/lock/leader-election.d.ts +16 -0
- package/dist/lock/leader-election.d.ts.map +1 -0
- package/dist/lock/stall-detector.d.ts +23 -0
- package/dist/lock/stall-detector.d.ts.map +1 -0
- package/dist/scheduler/cron-engine.d.ts +42 -0
- package/dist/scheduler/cron-engine.d.ts.map +1 -0
- package/dist/scheduler/define-cron.d.ts +36 -0
- package/dist/scheduler/define-cron.d.ts.map +1 -0
- package/dist/scheduler/define-schedule.d.ts +52 -0
- package/dist/scheduler/define-schedule.d.ts.map +1 -0
- package/dist/scheduler/expression-parser.d.ts +2 -0
- package/dist/scheduler/expression-parser.d.ts.map +1 -0
- package/dist/scheduler/index.d.ts +10 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/missed-fire.handler.d.ts +8 -0
- package/dist/scheduler/missed-fire.handler.d.ts.map +1 -0
- package/dist/scheduler/registry-schedule.d.ts +6 -0
- package/dist/scheduler/registry-schedule.d.ts.map +1 -0
- package/dist/scheduler/registry.d.ts +6 -0
- package/dist/scheduler/registry.d.ts.map +1 -0
- package/dist/scheduler/schedule-engine.d.ts +36 -0
- package/dist/scheduler/schedule-engine.d.ts.map +1 -0
- package/dist/scheduler-HRR3UXGE.mjs +1 -0
- package/dist/scheduler.d.mts +248 -0
- package/dist/scheduler.d.ts +248 -0
- package/dist/scheduler.js +1461 -0
- package/dist/scheduler.mjs +1 -0
- package/dist/server/express.d.ts +9 -0
- package/dist/server/express.d.ts.map +1 -0
- package/dist/server/fastify.d.ts +9 -0
- package/dist/server/fastify.d.ts.map +1 -0
- package/dist/server/handlers.d.ts +15 -0
- package/dist/server/handlers.d.ts.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2738 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var zod = require('zod');
|
|
6
|
+
var url = require('url');
|
|
7
|
+
var findUp = require('find-up');
|
|
8
|
+
var eventemitter3 = require('eventemitter3');
|
|
9
|
+
var voltlogIo = require('voltlog-io');
|
|
10
|
+
var Database2 = require('better-sqlite3');
|
|
11
|
+
var _cronParser = require('cron-parser');
|
|
12
|
+
var crypto = require('crypto');
|
|
13
|
+
var rrule = require('rrule');
|
|
14
|
+
var fs = require('fs/promises');
|
|
15
|
+
var path = require('path');
|
|
16
|
+
|
|
17
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
|
+
|
|
19
|
+
var Database2__default = /*#__PURE__*/_interopDefault(Database2);
|
|
20
|
+
var _cronParser__default = /*#__PURE__*/_interopDefault(_cronParser);
|
|
21
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
22
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
23
|
+
|
|
24
|
+
var __defProp = Object.defineProperty;
|
|
25
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
26
|
+
var __esm = (fn, res) => function __init() {
|
|
27
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
28
|
+
};
|
|
29
|
+
var __export = (target, all) => {
|
|
30
|
+
for (var name in all)
|
|
31
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ../../node_modules/.bun/tsup@8.5.1+b62311710f622f18/node_modules/tsup/assets/cjs_shims.js
|
|
35
|
+
var init_cjs_shims = __esm({
|
|
36
|
+
"../../node_modules/.bun/tsup@8.5.1+b62311710f622f18/node_modules/tsup/assets/cjs_shims.js"() {
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// src/core/config/default-config.ts
|
|
41
|
+
function reconfigureConfig(config) {
|
|
42
|
+
const isIOqronAdapter2 = (val) => val && typeof val === "object" && typeof val.upsertSchedule === "function";
|
|
43
|
+
const isILockAdapter2 = (val) => val && typeof val === "object" && typeof val.acquire === "function";
|
|
44
|
+
return {
|
|
45
|
+
project: config.project ?? defaultConfig.project,
|
|
46
|
+
environment: config.environment ?? defaultConfig.environment,
|
|
47
|
+
db: config.db ? isIOqronAdapter2(config.db) ? config.db : { ...defaultConfig.db, ...config.db } : defaultConfig.db,
|
|
48
|
+
lock: config.lock ? isILockAdapter2(config.lock) ? config.lock : { ...defaultConfig.lock, ...config.lock } : defaultConfig.lock,
|
|
49
|
+
broker: config.broker,
|
|
50
|
+
modules: config.modules ?? defaultConfig.modules,
|
|
51
|
+
cron: {
|
|
52
|
+
...defaultConfig.cron,
|
|
53
|
+
...config.cron
|
|
54
|
+
},
|
|
55
|
+
scheduler: {
|
|
56
|
+
...defaultConfig.scheduler,
|
|
57
|
+
...config.scheduler
|
|
58
|
+
},
|
|
59
|
+
jobsDir: config.jobsDir ?? defaultConfig.jobsDir,
|
|
60
|
+
tags: config.tags ?? defaultConfig.tags,
|
|
61
|
+
worker: {
|
|
62
|
+
...defaultConfig.worker,
|
|
63
|
+
...config.worker
|
|
64
|
+
},
|
|
65
|
+
logger: config.logger === false ? false : {
|
|
66
|
+
...defaultConfig.logger,
|
|
67
|
+
...config.logger
|
|
68
|
+
},
|
|
69
|
+
telemetry: {
|
|
70
|
+
prometheus: {
|
|
71
|
+
...defaultConfig.telemetry.prometheus,
|
|
72
|
+
...config.telemetry?.prometheus
|
|
73
|
+
},
|
|
74
|
+
opentelemetry: {
|
|
75
|
+
...defaultConfig.telemetry.opentelemetry,
|
|
76
|
+
...config.telemetry?.opentelemetry
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
shutdown: {
|
|
80
|
+
...defaultConfig.shutdown,
|
|
81
|
+
...config.shutdown
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var defaultConfig;
|
|
86
|
+
var init_default_config = __esm({
|
|
87
|
+
"src/core/config/default-config.ts"() {
|
|
88
|
+
init_cjs_shims();
|
|
89
|
+
defaultConfig = {
|
|
90
|
+
project: "oqronkit",
|
|
91
|
+
environment: "development",
|
|
92
|
+
db: {
|
|
93
|
+
adapter: "memory",
|
|
94
|
+
poolMin: 2,
|
|
95
|
+
poolMax: 10,
|
|
96
|
+
tablePrefix: "chrono_",
|
|
97
|
+
migrations: "auto",
|
|
98
|
+
ssl: false
|
|
99
|
+
},
|
|
100
|
+
lock: {
|
|
101
|
+
adapter: "memory",
|
|
102
|
+
ttl: 3e4,
|
|
103
|
+
retryDelay: 200,
|
|
104
|
+
retryCount: 5
|
|
105
|
+
},
|
|
106
|
+
modules: [],
|
|
107
|
+
cron: {
|
|
108
|
+
enable: true,
|
|
109
|
+
timezone: "UTC",
|
|
110
|
+
tickInterval: 1e3,
|
|
111
|
+
missedFirePolicy: "run-once",
|
|
112
|
+
maxConcurrentJobs: 5,
|
|
113
|
+
leaderElection: true,
|
|
114
|
+
keepJobHistory: true,
|
|
115
|
+
keepFailedJobHistory: true
|
|
116
|
+
},
|
|
117
|
+
scheduler: {
|
|
118
|
+
enable: true,
|
|
119
|
+
tickInterval: 1e3,
|
|
120
|
+
keepJobHistory: true,
|
|
121
|
+
keepFailedJobHistory: true
|
|
122
|
+
},
|
|
123
|
+
jobsDir: "./src/jobs",
|
|
124
|
+
tags: [],
|
|
125
|
+
worker: {
|
|
126
|
+
concurrency: 50,
|
|
127
|
+
gracefulShutdownMs: 3e4
|
|
128
|
+
},
|
|
129
|
+
logger: {
|
|
130
|
+
enabled: true,
|
|
131
|
+
level: "info",
|
|
132
|
+
prettify: false,
|
|
133
|
+
showMetadata: true,
|
|
134
|
+
redact: []
|
|
135
|
+
},
|
|
136
|
+
telemetry: {
|
|
137
|
+
prometheus: {
|
|
138
|
+
enabled: false,
|
|
139
|
+
path: "/metrics"
|
|
140
|
+
},
|
|
141
|
+
opentelemetry: {
|
|
142
|
+
enabled: false
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
shutdown: {
|
|
146
|
+
enabled: true,
|
|
147
|
+
timeout: 3e4,
|
|
148
|
+
signals: ["SIGINT", "SIGTERM"]
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
var isIOqronAdapter, isILockAdapter, OqronConfigSchema;
|
|
154
|
+
var init_schema = __esm({
|
|
155
|
+
"src/core/config/schema.ts"() {
|
|
156
|
+
init_cjs_shims();
|
|
157
|
+
isIOqronAdapter = (val) => {
|
|
158
|
+
if (!val || typeof val !== "object") return false;
|
|
159
|
+
return typeof val.upsertSchedule === "function" && typeof val.getDueSchedules === "function";
|
|
160
|
+
};
|
|
161
|
+
isILockAdapter = (val) => {
|
|
162
|
+
if (!val || typeof val !== "object") return false;
|
|
163
|
+
return typeof val.acquire === "function" && typeof val.renew === "function";
|
|
164
|
+
};
|
|
165
|
+
OqronConfigSchema = zod.z.object({
|
|
166
|
+
project: zod.z.string().optional(),
|
|
167
|
+
environment: zod.z.string().default("development"),
|
|
168
|
+
// Infrastructure — Union of explicit DI or declarative config
|
|
169
|
+
db: zod.z.union([
|
|
170
|
+
zod.z.custom(
|
|
171
|
+
isIOqronAdapter,
|
|
172
|
+
"db must be an instance of IOqronAdapter"
|
|
173
|
+
),
|
|
174
|
+
zod.z.object({
|
|
175
|
+
adapter: zod.z.enum([
|
|
176
|
+
"sqlite",
|
|
177
|
+
"memory",
|
|
178
|
+
"postgres",
|
|
179
|
+
"mysql",
|
|
180
|
+
"mongodb",
|
|
181
|
+
"redis"
|
|
182
|
+
]),
|
|
183
|
+
url: zod.z.string().optional(),
|
|
184
|
+
poolMin: zod.z.number().default(2),
|
|
185
|
+
poolMax: zod.z.number().default(10),
|
|
186
|
+
tablePrefix: zod.z.string().default("chrono_"),
|
|
187
|
+
migrations: zod.z.union([zod.z.enum(["auto", "manual"]), zod.z.literal(false)]).default("auto"),
|
|
188
|
+
ssl: zod.z.boolean().default(false)
|
|
189
|
+
})
|
|
190
|
+
]).optional(),
|
|
191
|
+
lock: zod.z.union([
|
|
192
|
+
zod.z.custom(
|
|
193
|
+
isILockAdapter,
|
|
194
|
+
"lock must be an instance of ILockAdapter"
|
|
195
|
+
),
|
|
196
|
+
zod.z.object({
|
|
197
|
+
adapter: zod.z.enum(["db", "memory", "redis"]),
|
|
198
|
+
url: zod.z.string().optional(),
|
|
199
|
+
ttl: zod.z.number().default(3e4),
|
|
200
|
+
retryDelay: zod.z.number().default(200),
|
|
201
|
+
retryCount: zod.z.number().default(5)
|
|
202
|
+
})
|
|
203
|
+
]).optional(),
|
|
204
|
+
broker: zod.z.any().optional(),
|
|
205
|
+
// Modules
|
|
206
|
+
modules: zod.z.array(
|
|
207
|
+
zod.z.enum([
|
|
208
|
+
"cron",
|
|
209
|
+
"scheduler",
|
|
210
|
+
"queue",
|
|
211
|
+
"workflow",
|
|
212
|
+
"batch",
|
|
213
|
+
"webhook",
|
|
214
|
+
"pipeline"
|
|
215
|
+
])
|
|
216
|
+
).default([]),
|
|
217
|
+
// Module-specific configs
|
|
218
|
+
cron: zod.z.object({
|
|
219
|
+
enable: zod.z.boolean().default(true),
|
|
220
|
+
timezone: zod.z.string().optional(),
|
|
221
|
+
tickInterval: zod.z.number().default(1e3),
|
|
222
|
+
missedFirePolicy: zod.z.enum(["skip", "run-once", "run-all"]).default("run-once"),
|
|
223
|
+
maxConcurrentJobs: zod.z.number().default(5),
|
|
224
|
+
leaderElection: zod.z.boolean().default(true),
|
|
225
|
+
keepJobHistory: zod.z.union([zod.z.boolean(), zod.z.number()]).default(true),
|
|
226
|
+
keepFailedJobHistory: zod.z.union([zod.z.boolean(), zod.z.number()]).default(true)
|
|
227
|
+
}).default({}),
|
|
228
|
+
scheduler: zod.z.object({
|
|
229
|
+
enable: zod.z.boolean().default(true),
|
|
230
|
+
tickInterval: zod.z.number().default(1e3),
|
|
231
|
+
keepJobHistory: zod.z.union([zod.z.boolean(), zod.z.number()]).default(true),
|
|
232
|
+
keepFailedJobHistory: zod.z.union([zod.z.boolean(), zod.z.number()]).default(true)
|
|
233
|
+
}).default({}),
|
|
234
|
+
// Auto-discovery directory
|
|
235
|
+
jobsDir: zod.z.string().default("./src/jobs"),
|
|
236
|
+
// Global tags
|
|
237
|
+
tags: zod.z.array(zod.z.string()).default([]),
|
|
238
|
+
// Worker
|
|
239
|
+
worker: zod.z.object({
|
|
240
|
+
concurrency: zod.z.number().default(50),
|
|
241
|
+
gracefulShutdownMs: zod.z.number().default(3e4)
|
|
242
|
+
}).default({ concurrency: 50, gracefulShutdownMs: 3e4 }),
|
|
243
|
+
// Logger (voltlog-io config or false to disable)
|
|
244
|
+
logger: zod.z.union([
|
|
245
|
+
zod.z.literal(false),
|
|
246
|
+
zod.z.object({
|
|
247
|
+
enabled: zod.z.boolean().default(true),
|
|
248
|
+
level: zod.z.string().default("info"),
|
|
249
|
+
prettify: zod.z.boolean().default(false),
|
|
250
|
+
showMetadata: zod.z.boolean().default(true),
|
|
251
|
+
redact: zod.z.array(zod.z.string()).default([])
|
|
252
|
+
})
|
|
253
|
+
]).default({
|
|
254
|
+
enabled: true,
|
|
255
|
+
level: "info",
|
|
256
|
+
prettify: false,
|
|
257
|
+
showMetadata: true,
|
|
258
|
+
redact: []
|
|
259
|
+
}),
|
|
260
|
+
// Telemetry
|
|
261
|
+
telemetry: zod.z.object({
|
|
262
|
+
prometheus: zod.z.object({
|
|
263
|
+
enabled: zod.z.boolean().default(false),
|
|
264
|
+
path: zod.z.string().default("/metrics")
|
|
265
|
+
}).default({}),
|
|
266
|
+
opentelemetry: zod.z.object({
|
|
267
|
+
enabled: zod.z.boolean().default(false)
|
|
268
|
+
}).default({})
|
|
269
|
+
}).default({}),
|
|
270
|
+
// Shutdown
|
|
271
|
+
shutdown: zod.z.object({
|
|
272
|
+
enabled: zod.z.boolean().default(true),
|
|
273
|
+
timeout: zod.z.number().default(3e4),
|
|
274
|
+
signals: zod.z.array(zod.z.string()).default(["SIGINT", "SIGTERM"])
|
|
275
|
+
}).default({})
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
280
|
+
const configPath = await findUp.findUp("oqron.config.ts", { cwd }) || await findUp.findUp("oqron.config.js", { cwd });
|
|
281
|
+
if (!configPath) {
|
|
282
|
+
return OqronConfigSchema.parse({});
|
|
283
|
+
}
|
|
284
|
+
let rawConfig;
|
|
285
|
+
try {
|
|
286
|
+
const mod = await import(
|
|
287
|
+
/* @vite-ignore */
|
|
288
|
+
url.pathToFileURL(configPath).href
|
|
289
|
+
);
|
|
290
|
+
rawConfig = mod;
|
|
291
|
+
while (rawConfig && typeof rawConfig === "object" && "default" in rawConfig) {
|
|
292
|
+
rawConfig = rawConfig.default;
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`[OqronKit] Failed to load config from: ${configPath}
|
|
297
|
+
${err}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const parseResult = OqronConfigSchema.safeParse(rawConfig);
|
|
301
|
+
if (!parseResult.success) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`[OqronKit] Invalid oqron.config.ts:
|
|
304
|
+
${parseResult.error.message}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return reconfigureConfig(parseResult.data);
|
|
308
|
+
}
|
|
309
|
+
var init_config_loader = __esm({
|
|
310
|
+
"src/core/config/config-loader.ts"() {
|
|
311
|
+
init_cjs_shims();
|
|
312
|
+
init_default_config();
|
|
313
|
+
init_schema();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// src/core/config/define-config.ts
|
|
318
|
+
function defineConfig(config) {
|
|
319
|
+
return config;
|
|
320
|
+
}
|
|
321
|
+
var init_define_config = __esm({
|
|
322
|
+
"src/core/config/define-config.ts"() {
|
|
323
|
+
init_cjs_shims();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// src/core/context/job-context.ts
|
|
328
|
+
var JobContext;
|
|
329
|
+
var init_job_context = __esm({
|
|
330
|
+
"src/core/context/job-context.ts"() {
|
|
331
|
+
init_cjs_shims();
|
|
332
|
+
JobContext = class {
|
|
333
|
+
id;
|
|
334
|
+
log;
|
|
335
|
+
signal;
|
|
336
|
+
environment;
|
|
337
|
+
project;
|
|
338
|
+
_progress = 0;
|
|
339
|
+
_onProgress;
|
|
340
|
+
constructor(opts) {
|
|
341
|
+
this.id = opts.id;
|
|
342
|
+
this.log = opts.logger;
|
|
343
|
+
this.signal = opts.signal;
|
|
344
|
+
this.environment = opts.environment;
|
|
345
|
+
this.project = opts.project;
|
|
346
|
+
this._onProgress = opts.onProgress;
|
|
347
|
+
this.progress = this.progress.bind(this);
|
|
348
|
+
}
|
|
349
|
+
progress(value, label) {
|
|
350
|
+
this._progress = Math.max(0, Math.min(100, value));
|
|
351
|
+
if (this._onProgress) {
|
|
352
|
+
this._onProgress(this._progress, label);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
getProgress() {
|
|
356
|
+
return this._progress;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// src/core/context/cron-context.ts
|
|
363
|
+
var CronContext;
|
|
364
|
+
var init_cron_context = __esm({
|
|
365
|
+
"src/core/context/cron-context.ts"() {
|
|
366
|
+
init_cjs_shims();
|
|
367
|
+
init_job_context();
|
|
368
|
+
CronContext = class extends JobContext {
|
|
369
|
+
firedAt;
|
|
370
|
+
scheduleName;
|
|
371
|
+
startedLocalAt;
|
|
372
|
+
constructor(opts) {
|
|
373
|
+
super(opts);
|
|
374
|
+
this.firedAt = opts.firedAt;
|
|
375
|
+
this.scheduleName = opts.scheduleName;
|
|
376
|
+
this.startedLocalAt = Date.now();
|
|
377
|
+
}
|
|
378
|
+
get duration() {
|
|
379
|
+
return Date.now() - this.startedLocalAt;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// src/core/context/schedule-context.ts
|
|
386
|
+
var ScheduleContext;
|
|
387
|
+
var init_schedule_context = __esm({
|
|
388
|
+
"src/core/context/schedule-context.ts"() {
|
|
389
|
+
init_cjs_shims();
|
|
390
|
+
ScheduleContext = class {
|
|
391
|
+
id;
|
|
392
|
+
name;
|
|
393
|
+
firedAt;
|
|
394
|
+
payload;
|
|
395
|
+
environment;
|
|
396
|
+
project;
|
|
397
|
+
logger;
|
|
398
|
+
signal;
|
|
399
|
+
startedLocalAt;
|
|
400
|
+
_onProgress;
|
|
401
|
+
constructor(opts) {
|
|
402
|
+
this.id = opts.id;
|
|
403
|
+
this.name = opts.scheduleName;
|
|
404
|
+
this.firedAt = opts.firedAt;
|
|
405
|
+
this.payload = opts.payload;
|
|
406
|
+
this.logger = opts.logger;
|
|
407
|
+
this.signal = opts.signal;
|
|
408
|
+
this.environment = opts.environment;
|
|
409
|
+
this.project = opts.project;
|
|
410
|
+
this.startedLocalAt = Date.now();
|
|
411
|
+
this._onProgress = opts.onProgress;
|
|
412
|
+
this.log = this.log.bind(this);
|
|
413
|
+
this.progress = this.progress.bind(this);
|
|
414
|
+
}
|
|
415
|
+
get aborted() {
|
|
416
|
+
return this.signal.aborted;
|
|
417
|
+
}
|
|
418
|
+
get duration() {
|
|
419
|
+
return Date.now() - this.startedLocalAt;
|
|
420
|
+
}
|
|
421
|
+
log(level, message, meta) {
|
|
422
|
+
if (this.logger && typeof this.logger[level] === "function") {
|
|
423
|
+
this.logger[level](message, meta);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
progress(percent, label) {
|
|
427
|
+
if (this._onProgress) {
|
|
428
|
+
this._onProgress(percent, label);
|
|
429
|
+
} else {
|
|
430
|
+
this.logger.debug("Progress updated", { percent, label, runId: this.id });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// src/core/errors/base.error.ts
|
|
438
|
+
var init_base_error = __esm({
|
|
439
|
+
"src/core/errors/base.error.ts"() {
|
|
440
|
+
init_cjs_shims();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
var OqronEventBusClass; exports.OqronEventBus = void 0;
|
|
444
|
+
var init_event_bus = __esm({
|
|
445
|
+
"src/core/events/event-bus.ts"() {
|
|
446
|
+
init_cjs_shims();
|
|
447
|
+
OqronEventBusClass = class _OqronEventBusClass extends eventemitter3.EventEmitter {
|
|
448
|
+
static _instance;
|
|
449
|
+
static getInstance() {
|
|
450
|
+
if (!_OqronEventBusClass._instance) {
|
|
451
|
+
_OqronEventBusClass._instance = new _OqronEventBusClass();
|
|
452
|
+
}
|
|
453
|
+
return _OqronEventBusClass._instance;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
exports.OqronEventBus = OqronEventBusClass.getInstance();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
function createLogger(config, context) {
|
|
460
|
+
if (typeof config?.enabled !== "undefined" && config?.enabled === false)
|
|
461
|
+
return NOOP_LOGGER;
|
|
462
|
+
if (config?.logger) return config.logger;
|
|
463
|
+
const level = (config?.level ?? "INFO").toUpperCase();
|
|
464
|
+
const opts = {
|
|
465
|
+
level,
|
|
466
|
+
transports: [],
|
|
467
|
+
context
|
|
468
|
+
};
|
|
469
|
+
if (config?.redact?.length) {
|
|
470
|
+
const keys = config.redact;
|
|
471
|
+
opts.middleware = [
|
|
472
|
+
voltlogIo.redactionMiddleware({
|
|
473
|
+
paths: keys,
|
|
474
|
+
deep: true
|
|
475
|
+
})
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
if (config?.prettify) {
|
|
479
|
+
opts.transports.push(voltlogIo.prettyTransport());
|
|
480
|
+
} else {
|
|
481
|
+
opts.transports.push(voltlogIo.consoleTransport());
|
|
482
|
+
}
|
|
483
|
+
return voltlogIo.createLogger(opts);
|
|
484
|
+
}
|
|
485
|
+
var NOOP_LOGGER;
|
|
486
|
+
var init_logger = __esm({
|
|
487
|
+
"src/core/logger/index.ts"() {
|
|
488
|
+
init_cjs_shims();
|
|
489
|
+
NOOP_LOGGER = {
|
|
490
|
+
trace() {
|
|
491
|
+
},
|
|
492
|
+
debug() {
|
|
493
|
+
},
|
|
494
|
+
info() {
|
|
495
|
+
},
|
|
496
|
+
warn() {
|
|
497
|
+
},
|
|
498
|
+
error() {
|
|
499
|
+
},
|
|
500
|
+
fatal() {
|
|
501
|
+
},
|
|
502
|
+
child() {
|
|
503
|
+
return NOOP_LOGGER;
|
|
504
|
+
},
|
|
505
|
+
addTransport() {
|
|
506
|
+
},
|
|
507
|
+
removeTransport() {
|
|
508
|
+
},
|
|
509
|
+
addMiddleware() {
|
|
510
|
+
},
|
|
511
|
+
removeMiddleware() {
|
|
512
|
+
},
|
|
513
|
+
setLevel() {
|
|
514
|
+
},
|
|
515
|
+
getLevel() {
|
|
516
|
+
return "SILENT";
|
|
517
|
+
},
|
|
518
|
+
isLevelEnabled() {
|
|
519
|
+
return false;
|
|
520
|
+
},
|
|
521
|
+
startTimer() {
|
|
522
|
+
return { done() {
|
|
523
|
+
}, elapsed: () => 0 };
|
|
524
|
+
},
|
|
525
|
+
async flush() {
|
|
526
|
+
},
|
|
527
|
+
async close() {
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// src/core/registry.ts
|
|
534
|
+
var OqronRegistry;
|
|
535
|
+
var init_registry = __esm({
|
|
536
|
+
"src/core/registry.ts"() {
|
|
537
|
+
init_cjs_shims();
|
|
538
|
+
OqronRegistry = class _OqronRegistry {
|
|
539
|
+
static _instance;
|
|
540
|
+
_modules = /* @__PURE__ */ new Map();
|
|
541
|
+
constructor() {
|
|
542
|
+
}
|
|
543
|
+
static getInstance() {
|
|
544
|
+
if (!_OqronRegistry._instance) {
|
|
545
|
+
_OqronRegistry._instance = new _OqronRegistry();
|
|
546
|
+
}
|
|
547
|
+
return _OqronRegistry._instance;
|
|
548
|
+
}
|
|
549
|
+
register(mod) {
|
|
550
|
+
if (this._modules.has(mod.name)) {
|
|
551
|
+
throw new Error(`[OqronKit] Module "${mod.name}" is already registered.`);
|
|
552
|
+
}
|
|
553
|
+
this._modules.set(mod.name, mod);
|
|
554
|
+
}
|
|
555
|
+
get(name) {
|
|
556
|
+
return this._modules.get(name);
|
|
557
|
+
}
|
|
558
|
+
getAll() {
|
|
559
|
+
return [...this._modules.values()];
|
|
560
|
+
}
|
|
561
|
+
/** Reset registry — useful for testing */
|
|
562
|
+
_reset() {
|
|
563
|
+
this._modules.clear();
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// src/core/index.ts
|
|
570
|
+
var init_core = __esm({
|
|
571
|
+
"src/core/index.ts"() {
|
|
572
|
+
init_cjs_shims();
|
|
573
|
+
init_config_loader();
|
|
574
|
+
init_default_config();
|
|
575
|
+
init_define_config();
|
|
576
|
+
init_schema();
|
|
577
|
+
init_cron_context();
|
|
578
|
+
init_job_context();
|
|
579
|
+
init_schedule_context();
|
|
580
|
+
init_base_error();
|
|
581
|
+
init_event_bus();
|
|
582
|
+
init_logger();
|
|
583
|
+
init_registry();
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
exports.DbLockAdapter = void 0;
|
|
587
|
+
var init_db_lock_adapter = __esm({
|
|
588
|
+
"src/lock/adapters/db-lock.adapter.ts"() {
|
|
589
|
+
init_cjs_shims();
|
|
590
|
+
exports.DbLockAdapter = class {
|
|
591
|
+
db;
|
|
592
|
+
defaultTtl;
|
|
593
|
+
/**
|
|
594
|
+
* @param dbOrPath - Either a `better-sqlite3` Database instance or a file path.
|
|
595
|
+
* When sharing the same SQLite file as SqliteAdapter, pass the same path.
|
|
596
|
+
* @param defaultTtlMs - Default TTL for locks if not provided in acquire (optional)
|
|
597
|
+
*/
|
|
598
|
+
constructor(dbOrPath = "oqron.sqlite", defaultTtlMs = 3e4) {
|
|
599
|
+
this.defaultTtl = defaultTtlMs;
|
|
600
|
+
if (typeof dbOrPath === "string") {
|
|
601
|
+
this.db = new Database2__default.default(dbOrPath);
|
|
602
|
+
} else {
|
|
603
|
+
this.db = dbOrPath;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async acquire(key, ownerId, ttlMs) {
|
|
607
|
+
const finalTtl = ttlMs ?? this.defaultTtl;
|
|
608
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
609
|
+
const expiresAt = new Date(Date.now() + finalTtl).toISOString();
|
|
610
|
+
this.db.prepare(
|
|
611
|
+
`
|
|
612
|
+
INSERT INTO chrono_locks (resourceKey, ownerId, expiresAt)
|
|
613
|
+
VALUES (?, ?, ?)
|
|
614
|
+
ON CONFLICT(resourceKey) DO UPDATE SET
|
|
615
|
+
ownerId = CASE WHEN expiresAt < ? THEN excluded.ownerId ELSE ownerId END,
|
|
616
|
+
expiresAt = CASE WHEN expiresAt < ? THEN excluded.expiresAt ELSE expiresAt END
|
|
617
|
+
`
|
|
618
|
+
).run(key, ownerId, expiresAt, now, now);
|
|
619
|
+
const row = this.db.prepare(`SELECT ownerId FROM chrono_locks WHERE resourceKey = ?`).get(key);
|
|
620
|
+
return row?.ownerId === ownerId;
|
|
621
|
+
}
|
|
622
|
+
async renew(key, ownerId, ttlMs) {
|
|
623
|
+
const finalTtl = ttlMs ?? this.defaultTtl;
|
|
624
|
+
const expiresAt = new Date(Date.now() + finalTtl).toISOString();
|
|
625
|
+
const result = this.db.prepare(
|
|
626
|
+
`
|
|
627
|
+
UPDATE chrono_locks SET expiresAt = ?
|
|
628
|
+
WHERE resourceKey = ? AND ownerId = ?
|
|
629
|
+
`
|
|
630
|
+
).run(expiresAt, key, ownerId);
|
|
631
|
+
return result.changes > 0;
|
|
632
|
+
}
|
|
633
|
+
async release(key, ownerId) {
|
|
634
|
+
this.db.prepare(`DELETE FROM chrono_locks WHERE resourceKey = ? AND ownerId = ?`).run(key, ownerId);
|
|
635
|
+
}
|
|
636
|
+
async isOwner(key, ownerId) {
|
|
637
|
+
const row = this.db.prepare(
|
|
638
|
+
`SELECT ownerId, expiresAt FROM chrono_locks WHERE resourceKey = ?`
|
|
639
|
+
).get(key);
|
|
640
|
+
if (!row) return false;
|
|
641
|
+
const expired = new Date(row.expiresAt) < /* @__PURE__ */ new Date();
|
|
642
|
+
return !expired && row.ownerId === ownerId;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// src/lock/adapters/memory-lock.adapter.ts
|
|
649
|
+
exports.MemoryLockAdapter = void 0;
|
|
650
|
+
var init_memory_lock_adapter = __esm({
|
|
651
|
+
"src/lock/adapters/memory-lock.adapter.ts"() {
|
|
652
|
+
init_cjs_shims();
|
|
653
|
+
exports.MemoryLockAdapter = class {
|
|
654
|
+
locks = /* @__PURE__ */ new Map();
|
|
655
|
+
async acquire(key, ownerId, ttlMs) {
|
|
656
|
+
const existing = this.locks.get(key);
|
|
657
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
658
|
+
return existing.ownerId === ownerId;
|
|
659
|
+
}
|
|
660
|
+
this.locks.set(key, { ownerId, expiresAt: Date.now() + ttlMs });
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
async renew(key, ownerId, ttlMs) {
|
|
664
|
+
const existing = this.locks.get(key);
|
|
665
|
+
if (!existing || existing.ownerId !== ownerId) return false;
|
|
666
|
+
existing.expiresAt = Date.now() + ttlMs;
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
async release(key, ownerId) {
|
|
670
|
+
const existing = this.locks.get(key);
|
|
671
|
+
if (existing?.ownerId === ownerId) this.locks.delete(key);
|
|
672
|
+
}
|
|
673
|
+
async isOwner(key, ownerId) {
|
|
674
|
+
const existing = this.locks.get(key);
|
|
675
|
+
return !!existing && existing.ownerId === ownerId && existing.expiresAt > Date.now();
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// src/lock/adapters/namespaced-lock.adapter.ts
|
|
682
|
+
var NamespacedLockAdapter;
|
|
683
|
+
var init_namespaced_lock_adapter = __esm({
|
|
684
|
+
"src/lock/adapters/namespaced-lock.adapter.ts"() {
|
|
685
|
+
init_cjs_shims();
|
|
686
|
+
NamespacedLockAdapter = class {
|
|
687
|
+
constructor(base, project = "default", environment = "development") {
|
|
688
|
+
this.base = base;
|
|
689
|
+
this.prefix = `${project}:${environment}:`;
|
|
690
|
+
}
|
|
691
|
+
prefix;
|
|
692
|
+
ns(key) {
|
|
693
|
+
return `${this.prefix}${key}`;
|
|
694
|
+
}
|
|
695
|
+
async acquire(key, ownerId, ttlMs) {
|
|
696
|
+
return this.base.acquire(this.ns(key), ownerId, ttlMs);
|
|
697
|
+
}
|
|
698
|
+
async renew(key, ownerId, ttlMs) {
|
|
699
|
+
return this.base.renew(this.ns(key), ownerId, ttlMs);
|
|
700
|
+
}
|
|
701
|
+
async release(key, ownerId) {
|
|
702
|
+
return this.base.release(this.ns(key), ownerId);
|
|
703
|
+
}
|
|
704
|
+
async isOwner(key, ownerId) {
|
|
705
|
+
return this.base.isOwner(this.ns(key), ownerId);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Note: Some lock adapters might implement advanced prefixing natively,
|
|
709
|
+
* but we proxy all core lock operations through the namespace.
|
|
710
|
+
*/
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// src/lock/heartbeat-worker.ts
|
|
716
|
+
var HeartbeatWorker;
|
|
717
|
+
var init_heartbeat_worker = __esm({
|
|
718
|
+
"src/lock/heartbeat-worker.ts"() {
|
|
719
|
+
init_cjs_shims();
|
|
720
|
+
HeartbeatWorker = class {
|
|
721
|
+
constructor(lock, logger, key, ownerId, ttlMs = 3e4, heartbeatMs) {
|
|
722
|
+
this.lock = lock;
|
|
723
|
+
this.logger = logger;
|
|
724
|
+
this.key = key;
|
|
725
|
+
this.ownerId = ownerId;
|
|
726
|
+
this.ttlMs = ttlMs;
|
|
727
|
+
this.heartbeatMs = heartbeatMs;
|
|
728
|
+
}
|
|
729
|
+
heartbeatTimer;
|
|
730
|
+
_active = false;
|
|
731
|
+
async start() {
|
|
732
|
+
const acquired = await this.lock.acquire(
|
|
733
|
+
this.key,
|
|
734
|
+
this.ownerId,
|
|
735
|
+
this.ttlMs
|
|
736
|
+
);
|
|
737
|
+
if (!acquired) return false;
|
|
738
|
+
this._active = true;
|
|
739
|
+
const pingInterval = this.heartbeatMs ?? Math.floor(this.ttlMs / 3);
|
|
740
|
+
this.heartbeatTimer = setInterval(async () => {
|
|
741
|
+
if (!this._active) return;
|
|
742
|
+
try {
|
|
743
|
+
const renewed = await this.lock.renew(
|
|
744
|
+
this.key,
|
|
745
|
+
this.ownerId,
|
|
746
|
+
this.ttlMs
|
|
747
|
+
);
|
|
748
|
+
if (!renewed) {
|
|
749
|
+
this.logger.warn("Heartbeat renewal failed \u2014 lock lost", {
|
|
750
|
+
key: this.key,
|
|
751
|
+
ownerId: this.ownerId
|
|
752
|
+
});
|
|
753
|
+
this._active = false;
|
|
754
|
+
clearInterval(this.heartbeatTimer);
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
this.logger.error("Heartbeat renewal threw", {
|
|
758
|
+
key: this.key,
|
|
759
|
+
err: String(err)
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}, pingInterval);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
async stop() {
|
|
766
|
+
this._active = false;
|
|
767
|
+
if (this.heartbeatTimer) {
|
|
768
|
+
clearInterval(this.heartbeatTimer);
|
|
769
|
+
this.heartbeatTimer = void 0;
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
await this.lock.release(this.key, this.ownerId);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
this.logger.error("Failed to release lock cleanly", {
|
|
775
|
+
key: this.key,
|
|
776
|
+
err: String(err)
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
get isActive() {
|
|
781
|
+
return this._active;
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// src/lock/leader-election.ts
|
|
788
|
+
var LeaderElection;
|
|
789
|
+
var init_leader_election = __esm({
|
|
790
|
+
"src/lock/leader-election.ts"() {
|
|
791
|
+
init_cjs_shims();
|
|
792
|
+
LeaderElection = class {
|
|
793
|
+
constructor(lock, logger, leaderKey, nodeId, ttlMs = 3e4) {
|
|
794
|
+
this.lock = lock;
|
|
795
|
+
this.logger = logger;
|
|
796
|
+
this.leaderKey = leaderKey;
|
|
797
|
+
this.nodeId = nodeId;
|
|
798
|
+
this.ttlMs = ttlMs;
|
|
799
|
+
}
|
|
800
|
+
electionTimer;
|
|
801
|
+
_isLeader = false;
|
|
802
|
+
async start() {
|
|
803
|
+
await this.campaign();
|
|
804
|
+
const interval = Math.floor(this.ttlMs / 3);
|
|
805
|
+
this.electionTimer = setInterval(() => void this.campaign(), interval);
|
|
806
|
+
}
|
|
807
|
+
async campaign() {
|
|
808
|
+
try {
|
|
809
|
+
if (this._isLeader) {
|
|
810
|
+
const ok = await this.lock.renew(
|
|
811
|
+
this.leaderKey,
|
|
812
|
+
this.nodeId,
|
|
813
|
+
this.ttlMs
|
|
814
|
+
);
|
|
815
|
+
if (!ok) {
|
|
816
|
+
this._isLeader = false;
|
|
817
|
+
this.logger.warn("Lost leadership", {
|
|
818
|
+
leaderKey: this.leaderKey,
|
|
819
|
+
nodeId: this.nodeId
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
const ok = await this.lock.acquire(
|
|
824
|
+
this.leaderKey,
|
|
825
|
+
this.nodeId,
|
|
826
|
+
this.ttlMs
|
|
827
|
+
);
|
|
828
|
+
if (ok) {
|
|
829
|
+
this._isLeader = true;
|
|
830
|
+
this.logger.info("Became leader", {
|
|
831
|
+
leaderKey: this.leaderKey,
|
|
832
|
+
nodeId: this.nodeId
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch (err) {
|
|
837
|
+
this.logger.error("Leader election error", {
|
|
838
|
+
leaderKey: this.leaderKey,
|
|
839
|
+
err: String(err)
|
|
840
|
+
});
|
|
841
|
+
this._isLeader = false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
get isLeader() {
|
|
845
|
+
return this._isLeader;
|
|
846
|
+
}
|
|
847
|
+
async stop() {
|
|
848
|
+
if (this.electionTimer) {
|
|
849
|
+
clearInterval(this.electionTimer);
|
|
850
|
+
this.electionTimer = void 0;
|
|
851
|
+
}
|
|
852
|
+
if (this._isLeader) {
|
|
853
|
+
try {
|
|
854
|
+
await this.lock.release(this.leaderKey, this.nodeId);
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
this._isLeader = false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// src/lock/stall-detector.ts
|
|
865
|
+
var StallDetector;
|
|
866
|
+
var init_stall_detector = __esm({
|
|
867
|
+
"src/lock/stall-detector.ts"() {
|
|
868
|
+
init_cjs_shims();
|
|
869
|
+
StallDetector = class {
|
|
870
|
+
constructor(lock, logger, checkIntervalMs = 15e3) {
|
|
871
|
+
this.lock = lock;
|
|
872
|
+
this.logger = logger;
|
|
873
|
+
this.checkIntervalMs = checkIntervalMs;
|
|
874
|
+
}
|
|
875
|
+
timer;
|
|
876
|
+
/**
|
|
877
|
+
* Start the stall detection loop.
|
|
878
|
+
* @param getActiveJobs - Returns a list of { key, ownerId } for all currently tracked jobs.
|
|
879
|
+
* @param onStalled - Called when a job is detected as stalled (lock lost).
|
|
880
|
+
*/
|
|
881
|
+
start(getActiveJobs, onStalled) {
|
|
882
|
+
this.timer = setInterval(async () => {
|
|
883
|
+
try {
|
|
884
|
+
const jobs = getActiveJobs();
|
|
885
|
+
for (const job of jobs) {
|
|
886
|
+
const owned = await this.lock.isOwner(job.key, job.ownerId);
|
|
887
|
+
if (!owned) {
|
|
888
|
+
this.logger.warn("Stalled job detected \u2014 lock lost", {
|
|
889
|
+
key: job.key,
|
|
890
|
+
ownerId: job.ownerId
|
|
891
|
+
});
|
|
892
|
+
onStalled(job.key);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} catch (err) {
|
|
896
|
+
this.logger.error("StallDetector tick error", { err: String(err) });
|
|
897
|
+
}
|
|
898
|
+
}, this.checkIntervalMs);
|
|
899
|
+
}
|
|
900
|
+
stop() {
|
|
901
|
+
if (this.timer) {
|
|
902
|
+
clearInterval(this.timer);
|
|
903
|
+
this.timer = void 0;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/lock/index.ts
|
|
911
|
+
var init_lock = __esm({
|
|
912
|
+
"src/lock/index.ts"() {
|
|
913
|
+
init_cjs_shims();
|
|
914
|
+
init_db_lock_adapter();
|
|
915
|
+
init_memory_lock_adapter();
|
|
916
|
+
init_namespaced_lock_adapter();
|
|
917
|
+
init_heartbeat_worker();
|
|
918
|
+
init_leader_election();
|
|
919
|
+
init_stall_detector();
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
function getNextRunDate(expression, timezone, from = /* @__PURE__ */ new Date()) {
|
|
923
|
+
const opts = { currentDate: from };
|
|
924
|
+
if (timezone) opts.tz = timezone;
|
|
925
|
+
return cronParser.parseExpression(expression, opts).next().toDate();
|
|
926
|
+
}
|
|
927
|
+
var cronParser;
|
|
928
|
+
var init_expression_parser = __esm({
|
|
929
|
+
"src/scheduler/expression-parser.ts"() {
|
|
930
|
+
init_cjs_shims();
|
|
931
|
+
cronParser = _cronParser__default.default.default ?? _cronParser__default.default;
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
var cronParser2, MissedFireHandler;
|
|
935
|
+
var init_missed_fire_handler = __esm({
|
|
936
|
+
"src/scheduler/missed-fire.handler.ts"() {
|
|
937
|
+
init_cjs_shims();
|
|
938
|
+
cronParser2 = _cronParser__default.default.default ?? _cronParser__default.default;
|
|
939
|
+
MissedFireHandler = class {
|
|
940
|
+
constructor(logger, _db2) {
|
|
941
|
+
this.logger = logger;
|
|
942
|
+
this._db = _db2;
|
|
943
|
+
}
|
|
944
|
+
async checkMissed(def, lastRunAt, now) {
|
|
945
|
+
if (!lastRunAt) return false;
|
|
946
|
+
try {
|
|
947
|
+
let missed = false;
|
|
948
|
+
if (def.expression) {
|
|
949
|
+
const opts = { currentDate: now, tz: def.timezone };
|
|
950
|
+
const prevRun = cronParser2.parseExpression(def.expression, opts).prev().toDate();
|
|
951
|
+
missed = prevRun > lastRunAt;
|
|
952
|
+
} else if (def.intervalMs) {
|
|
953
|
+
const elapsed = now.getTime() - lastRunAt.getTime();
|
|
954
|
+
missed = elapsed > def.intervalMs;
|
|
955
|
+
}
|
|
956
|
+
if (missed) {
|
|
957
|
+
this.logger.warn("Missed execution detected", {
|
|
958
|
+
name: def.name,
|
|
959
|
+
policy: def.missedFire
|
|
960
|
+
});
|
|
961
|
+
if (def.hooks?.onMissedFire) {
|
|
962
|
+
try {
|
|
963
|
+
const ctx = {
|
|
964
|
+
id: crypto.randomUUID(),
|
|
965
|
+
log: this.logger.child({
|
|
966
|
+
schedule: def.name,
|
|
967
|
+
scope: "missed-fire"
|
|
968
|
+
}),
|
|
969
|
+
logger: this.logger.child({
|
|
970
|
+
schedule: def.name,
|
|
971
|
+
scope: "missed-fire"
|
|
972
|
+
}),
|
|
973
|
+
signal: new AbortController().signal,
|
|
974
|
+
firedAt: now,
|
|
975
|
+
scheduleName: def.name,
|
|
976
|
+
progress: () => {
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
await def.hooks.onMissedFire(ctx, lastRunAt);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
this.logger.error("onMissedFire hook threw", {
|
|
982
|
+
name: def.name,
|
|
983
|
+
err: String(err)
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (def.missedFire === "run-once" || def.missedFire === "run-all") {
|
|
988
|
+
return true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
var SchedulerModule;
|
|
999
|
+
var init_cron_engine = __esm({
|
|
1000
|
+
"src/scheduler/cron-engine.ts"() {
|
|
1001
|
+
init_cjs_shims();
|
|
1002
|
+
init_core();
|
|
1003
|
+
init_lock();
|
|
1004
|
+
init_expression_parser();
|
|
1005
|
+
init_missed_fire_handler();
|
|
1006
|
+
SchedulerModule = class {
|
|
1007
|
+
constructor(schedules, db, lock, logger, environment, project, config) {
|
|
1008
|
+
this.schedules = schedules;
|
|
1009
|
+
this.db = db;
|
|
1010
|
+
this.lock = lock;
|
|
1011
|
+
this.environment = environment;
|
|
1012
|
+
this.project = project;
|
|
1013
|
+
this.config = config;
|
|
1014
|
+
this.nodeId = crypto.randomUUID();
|
|
1015
|
+
this.logger = logger ?? createLogger({ level: "info" }, { module: "scheduler" });
|
|
1016
|
+
this.stallDetector = new StallDetector(this.lock, this.logger, 15e3);
|
|
1017
|
+
this.missedFireHandler = new MissedFireHandler(this.logger, this.db);
|
|
1018
|
+
}
|
|
1019
|
+
name = "cron";
|
|
1020
|
+
enabled = true;
|
|
1021
|
+
nodeId;
|
|
1022
|
+
logger;
|
|
1023
|
+
leader;
|
|
1024
|
+
stallDetector;
|
|
1025
|
+
missedFireHandler;
|
|
1026
|
+
tickTimer;
|
|
1027
|
+
activeJobs = /* @__PURE__ */ new Map();
|
|
1028
|
+
_hasRunLeaderInit = false;
|
|
1029
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
1030
|
+
async init() {
|
|
1031
|
+
this.logger.info("Initializing scheduler", {
|
|
1032
|
+
nodeId: this.nodeId,
|
|
1033
|
+
count: this.schedules.length
|
|
1034
|
+
});
|
|
1035
|
+
for (const def of this.schedules) {
|
|
1036
|
+
await this.db.upsertSchedule(def);
|
|
1037
|
+
this.logger.debug("Registered schedule", {
|
|
1038
|
+
name: def.name,
|
|
1039
|
+
expression: def.expression,
|
|
1040
|
+
intervalMs: def.intervalMs
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
const existing = await this.db.getSchedules();
|
|
1044
|
+
const now = /* @__PURE__ */ new Date();
|
|
1045
|
+
for (const record of existing) {
|
|
1046
|
+
if (record.nextRunAt !== null) continue;
|
|
1047
|
+
const def = this.schedules.find((s) => s.name === record.name);
|
|
1048
|
+
if (!def) continue;
|
|
1049
|
+
const nextRun = this.computeNextRun(def, now);
|
|
1050
|
+
if (nextRun) {
|
|
1051
|
+
await this.db.updateNextRun(def.name, nextRun);
|
|
1052
|
+
this.logger.debug("Seeded nextRunAt", {
|
|
1053
|
+
name: def.name,
|
|
1054
|
+
nextRunAt: nextRun.toISOString()
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async start() {
|
|
1060
|
+
if (this.config?.leaderElection !== false) {
|
|
1061
|
+
this.leader = new LeaderElection(
|
|
1062
|
+
this.lock,
|
|
1063
|
+
this.logger,
|
|
1064
|
+
"oqron:scheduler:leader",
|
|
1065
|
+
this.nodeId,
|
|
1066
|
+
3e4
|
|
1067
|
+
);
|
|
1068
|
+
await this.leader.start();
|
|
1069
|
+
}
|
|
1070
|
+
const interval = this.config?.tickInterval ?? 1e3;
|
|
1071
|
+
this.tickTimer = setInterval(() => {
|
|
1072
|
+
void this.tick();
|
|
1073
|
+
}, interval);
|
|
1074
|
+
this.logger.info("Scheduler started", { nodeId: this.nodeId, interval });
|
|
1075
|
+
}
|
|
1076
|
+
async stop() {
|
|
1077
|
+
if (this.tickTimer) clearInterval(this.tickTimer);
|
|
1078
|
+
if (this.leader) await this.leader.stop();
|
|
1079
|
+
this.stallDetector.stop();
|
|
1080
|
+
for (const job of this.activeJobs.values()) {
|
|
1081
|
+
if (job.abort) job.abort.abort();
|
|
1082
|
+
if (job.worker) {
|
|
1083
|
+
await job.worker.stop();
|
|
1084
|
+
} else {
|
|
1085
|
+
await this.lock.release(job.lockKey, this.nodeId).catch(() => {
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
this.activeJobs.clear();
|
|
1090
|
+
this.logger.info("Scheduler stopped");
|
|
1091
|
+
}
|
|
1092
|
+
// ── Core scheduling helpers ─────────────────────────────────────────────────
|
|
1093
|
+
/**
|
|
1094
|
+
* Compute the next fire time for a definition.
|
|
1095
|
+
* Returns null if computation fails (invalid expression, etc).
|
|
1096
|
+
*/
|
|
1097
|
+
computeNextRun(def, from) {
|
|
1098
|
+
if (def.expression) {
|
|
1099
|
+
try {
|
|
1100
|
+
const timezone = def.timezone ?? this.config?.timezone;
|
|
1101
|
+
return getNextRunDate(def.expression, timezone, from);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
this.logger.error("Failed to compute next run from expression", {
|
|
1104
|
+
name: def.name,
|
|
1105
|
+
expression: def.expression,
|
|
1106
|
+
err: String(err)
|
|
1107
|
+
});
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (def.intervalMs) {
|
|
1112
|
+
return new Date(from.getTime() + def.intervalMs);
|
|
1113
|
+
}
|
|
1114
|
+
this.logger.error("Schedule has neither expression nor intervalMs", {
|
|
1115
|
+
name: def.name
|
|
1116
|
+
});
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
// ── Leader init (missed-fire recovery + stall detector) ─────────────────────
|
|
1120
|
+
async handleLeaderInit() {
|
|
1121
|
+
this.logger.info("Performing leader initialization...");
|
|
1122
|
+
const knownSchedules = await this.db.getSchedules();
|
|
1123
|
+
const now = /* @__PURE__ */ new Date();
|
|
1124
|
+
for (const record of knownSchedules) {
|
|
1125
|
+
const def = this.schedules.find((s) => s.name === record.name);
|
|
1126
|
+
if (!def) continue;
|
|
1127
|
+
const missed = await this.missedFireHandler.checkMissed(
|
|
1128
|
+
def,
|
|
1129
|
+
record.lastRunAt,
|
|
1130
|
+
now
|
|
1131
|
+
);
|
|
1132
|
+
if (missed) {
|
|
1133
|
+
this.logger.info("Triggering recovery run for missed schedule", {
|
|
1134
|
+
name: def.name
|
|
1135
|
+
});
|
|
1136
|
+
void this.fire(def);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
this.stallDetector.start(
|
|
1140
|
+
() => {
|
|
1141
|
+
return Array.from(this.activeJobs.values()).filter((job) => job.worker !== void 0).map((job) => ({
|
|
1142
|
+
key: job.lockKey,
|
|
1143
|
+
ownerId: this.nodeId
|
|
1144
|
+
}));
|
|
1145
|
+
},
|
|
1146
|
+
(key) => {
|
|
1147
|
+
this.logger.warn("Local stall detected", { key });
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
async detectClusterStalls() {
|
|
1152
|
+
try {
|
|
1153
|
+
const activeDbJobs = await this.db.getActiveJobs();
|
|
1154
|
+
for (const job of activeDbJobs) {
|
|
1155
|
+
if (!job.scheduleId) continue;
|
|
1156
|
+
const def = this.schedules.find((s) => s.name === job.scheduleId);
|
|
1157
|
+
if (!def?.guaranteedWorker) continue;
|
|
1158
|
+
const ageMs = Date.now() - job.startedAt.getTime();
|
|
1159
|
+
const ttl = def.lockTtlMs ?? 5e4;
|
|
1160
|
+
if (ageMs > ttl + 1e4) {
|
|
1161
|
+
this.logger.warn("Cluster stall detected", { runId: job.id });
|
|
1162
|
+
await this.db.recordExecution({
|
|
1163
|
+
id: job.id,
|
|
1164
|
+
scheduleId: job.scheduleId,
|
|
1165
|
+
status: "failed",
|
|
1166
|
+
error: "Stall detected (lock assumed expired)",
|
|
1167
|
+
startedAt: job.startedAt,
|
|
1168
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
this.logger.error("Failed to detect cluster stalls", {
|
|
1174
|
+
err: String(err)
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// ── Tick loop ───────────────────────────────────────────────────────────────
|
|
1179
|
+
async tick() {
|
|
1180
|
+
if (this.leader && !this.leader.isLeader) return;
|
|
1181
|
+
if (!this._hasRunLeaderInit) {
|
|
1182
|
+
this._hasRunLeaderInit = true;
|
|
1183
|
+
await this.handleLeaderInit();
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
if (Math.random() < 0.1) {
|
|
1187
|
+
void this.detectClusterStalls();
|
|
1188
|
+
}
|
|
1189
|
+
const now = /* @__PURE__ */ new Date();
|
|
1190
|
+
const due = await this.db.getDueSchedules(now, 50);
|
|
1191
|
+
for (const { name } of due) {
|
|
1192
|
+
const def = this.schedules.find((s) => s.name === name);
|
|
1193
|
+
if (!def) continue;
|
|
1194
|
+
const nextRun = this.computeNextRun(def, now);
|
|
1195
|
+
if (!nextRun) {
|
|
1196
|
+
this.logger.error(
|
|
1197
|
+
"Cannot compute next run \u2014 suspending cron to prevent runaway loop",
|
|
1198
|
+
{ name: def.name }
|
|
1199
|
+
);
|
|
1200
|
+
await this.db.updateNextRun(def.name, null).catch(() => {
|
|
1201
|
+
});
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
await this.db.updateNextRun(def.name, nextRun);
|
|
1205
|
+
void this.fire(def);
|
|
1206
|
+
}
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
this.logger.error("Tick error", { err: String(err) });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// ── Fire handler ────────────────────────────────────────────────────────────
|
|
1212
|
+
async fire(def) {
|
|
1213
|
+
const isOverlapSkip = def.overlap === "skip" || def.overlap === false;
|
|
1214
|
+
if (isOverlapSkip) {
|
|
1215
|
+
for (const job of this.activeJobs.values()) {
|
|
1216
|
+
if (job.lockKey === `oqron:run:${def.name}`) {
|
|
1217
|
+
this.logger.debug("Skipping overlapping run", { name: def.name });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const runId = crypto.randomUUID();
|
|
1223
|
+
const lockKey = isOverlapSkip ? `oqron:run:${def.name}` : `oqron:run:${def.name}:${runId}`;
|
|
1224
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
1225
|
+
let worker;
|
|
1226
|
+
let acquired = false;
|
|
1227
|
+
if (def.guaranteedWorker) {
|
|
1228
|
+
worker = new HeartbeatWorker(
|
|
1229
|
+
this.lock,
|
|
1230
|
+
this.logger,
|
|
1231
|
+
lockKey,
|
|
1232
|
+
this.nodeId,
|
|
1233
|
+
def.lockTtlMs ?? 3e4,
|
|
1234
|
+
def.heartbeatMs ?? 1e4
|
|
1235
|
+
);
|
|
1236
|
+
acquired = await worker.start();
|
|
1237
|
+
} else {
|
|
1238
|
+
acquired = await this.lock.acquire(
|
|
1239
|
+
lockKey,
|
|
1240
|
+
this.nodeId,
|
|
1241
|
+
def.lockTtlMs ?? 3e4
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (!acquired) return;
|
|
1245
|
+
const abort = new AbortController();
|
|
1246
|
+
this.activeJobs.set(runId, { runId, lockKey, worker, abort });
|
|
1247
|
+
await this.db.recordExecution({
|
|
1248
|
+
id: runId,
|
|
1249
|
+
scheduleId: def.name,
|
|
1250
|
+
status: "running",
|
|
1251
|
+
startedAt
|
|
1252
|
+
});
|
|
1253
|
+
void Promise.resolve().then(async () => {
|
|
1254
|
+
const ctx = new CronContext({
|
|
1255
|
+
id: runId,
|
|
1256
|
+
logger: this.logger.child({ schedule: def.name }),
|
|
1257
|
+
signal: abort.signal,
|
|
1258
|
+
firedAt: startedAt,
|
|
1259
|
+
scheduleName: def.name,
|
|
1260
|
+
environment: this.environment,
|
|
1261
|
+
project: this.project,
|
|
1262
|
+
onProgress: (percent, label) => {
|
|
1263
|
+
this.db.updateJobProgress(runId, percent, label).catch(
|
|
1264
|
+
(err) => this.logger.error("Failed to update progress", { runId, err })
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
let status = "completed";
|
|
1269
|
+
let error;
|
|
1270
|
+
let timeoutHandle;
|
|
1271
|
+
let finalResult;
|
|
1272
|
+
let attempts = 1;
|
|
1273
|
+
const maxAttempts = (def.retries?.max ?? 0) + 1;
|
|
1274
|
+
while (attempts <= maxAttempts) {
|
|
1275
|
+
try {
|
|
1276
|
+
if (def.hooks?.beforeRun) await def.hooks.beforeRun(ctx);
|
|
1277
|
+
if (def.timeout) {
|
|
1278
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1279
|
+
timeoutHandle = setTimeout(() => {
|
|
1280
|
+
abort.abort();
|
|
1281
|
+
reject(new Error(`Handler timed out after ${def.timeout}ms`));
|
|
1282
|
+
}, def.timeout);
|
|
1283
|
+
});
|
|
1284
|
+
finalResult = await Promise.race([
|
|
1285
|
+
def.handler(ctx),
|
|
1286
|
+
timeoutPromise
|
|
1287
|
+
]);
|
|
1288
|
+
} else {
|
|
1289
|
+
finalResult = await def.handler(ctx);
|
|
1290
|
+
}
|
|
1291
|
+
if (def.hooks?.afterRun) {
|
|
1292
|
+
await def.hooks.afterRun(ctx, finalResult);
|
|
1293
|
+
}
|
|
1294
|
+
status = "completed";
|
|
1295
|
+
error = void 0;
|
|
1296
|
+
break;
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
error = err instanceof Error ? err.message : String(err);
|
|
1299
|
+
status = "failed";
|
|
1300
|
+
if (def.hooks?.onError && err instanceof Error) {
|
|
1301
|
+
try {
|
|
1302
|
+
await Promise.resolve(def.hooks.onError(ctx, err));
|
|
1303
|
+
} catch (e) {
|
|
1304
|
+
this.logger.error("onError hook threw", { err: String(e) });
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
if (attempts < maxAttempts) {
|
|
1308
|
+
this.logger.warn("Job handler threw, retrying...", {
|
|
1309
|
+
name: def.name,
|
|
1310
|
+
runId,
|
|
1311
|
+
attempt: attempts,
|
|
1312
|
+
error
|
|
1313
|
+
});
|
|
1314
|
+
const baseDelay = def.retries?.baseDelay ?? 2e3;
|
|
1315
|
+
const delay = def.retries?.strategy === "exponential" ? baseDelay * 2 ** (attempts - 1) : baseDelay;
|
|
1316
|
+
attempts++;
|
|
1317
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1318
|
+
} else {
|
|
1319
|
+
this.logger.error("Job handler failed completely", {
|
|
1320
|
+
name: def.name,
|
|
1321
|
+
runId,
|
|
1322
|
+
attempts,
|
|
1323
|
+
error
|
|
1324
|
+
});
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
} finally {
|
|
1328
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const completedAt = /* @__PURE__ */ new Date();
|
|
1332
|
+
await this.db.recordExecution({
|
|
1333
|
+
id: runId,
|
|
1334
|
+
scheduleId: def.name,
|
|
1335
|
+
status,
|
|
1336
|
+
startedAt,
|
|
1337
|
+
completedAt,
|
|
1338
|
+
error,
|
|
1339
|
+
result: finalResult !== void 0 ? JSON.stringify(finalResult) : void 0,
|
|
1340
|
+
attempts,
|
|
1341
|
+
durationMs: completedAt.getTime() - startedAt.getTime(),
|
|
1342
|
+
...status === "completed" ? { progressPercent: 100, progressLabel: "Completed" } : {}
|
|
1343
|
+
});
|
|
1344
|
+
if (worker) {
|
|
1345
|
+
await worker.stop();
|
|
1346
|
+
} else {
|
|
1347
|
+
await this.lock.release(lockKey, this.nodeId).catch(() => {
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
this.activeJobs.delete(runId);
|
|
1351
|
+
if (def.intervalMs) {
|
|
1352
|
+
const nextRun = this.computeNextRun(def, /* @__PURE__ */ new Date());
|
|
1353
|
+
if (nextRun) {
|
|
1354
|
+
await this.db.updateNextRun(def.name, nextRun).catch(() => {
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const keepJobHistory = def.keepHistory ?? this.config?.keepJobHistory ?? true;
|
|
1359
|
+
const keepFailedHistory = def.keepFailedHistory ?? this.config?.keepFailedJobHistory ?? true;
|
|
1360
|
+
if (keepJobHistory !== true || keepFailedHistory !== true) {
|
|
1361
|
+
this.db.pruneHistoryForSchedule(def.name, keepJobHistory, keepFailedHistory).catch(
|
|
1362
|
+
(err) => this.logger.debug("Failed to prune history", {
|
|
1363
|
+
err,
|
|
1364
|
+
name: def.name
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
this.logger.info("Job finished", {
|
|
1369
|
+
name: def.name,
|
|
1370
|
+
runId,
|
|
1371
|
+
status,
|
|
1372
|
+
attempts
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
// src/scheduler/registry.ts
|
|
1381
|
+
function _getPending() {
|
|
1382
|
+
if (!globalThis[GLOBAL_KEY]) {
|
|
1383
|
+
globalThis[GLOBAL_KEY] = [];
|
|
1384
|
+
}
|
|
1385
|
+
return globalThis[GLOBAL_KEY];
|
|
1386
|
+
}
|
|
1387
|
+
function _registerCron(def) {
|
|
1388
|
+
_getPending().push(def);
|
|
1389
|
+
}
|
|
1390
|
+
function _drainPending() {
|
|
1391
|
+
const pending = _getPending();
|
|
1392
|
+
return pending.splice(0);
|
|
1393
|
+
}
|
|
1394
|
+
var GLOBAL_KEY;
|
|
1395
|
+
var init_registry2 = __esm({
|
|
1396
|
+
"src/scheduler/registry.ts"() {
|
|
1397
|
+
init_cjs_shims();
|
|
1398
|
+
GLOBAL_KEY = "__oqronkit_pending_crons__";
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
function everyToIntervalMs(every) {
|
|
1402
|
+
let ms = 0;
|
|
1403
|
+
if (every.seconds) ms += every.seconds * 1e3;
|
|
1404
|
+
if (every.minutes) ms += every.minutes * 6e4;
|
|
1405
|
+
if (every.hours) ms += every.hours * 36e5;
|
|
1406
|
+
if (ms <= 0)
|
|
1407
|
+
throw new Error(
|
|
1408
|
+
"[OqronKit] `every` config must resolve to a positive interval"
|
|
1409
|
+
);
|
|
1410
|
+
return ms;
|
|
1411
|
+
}
|
|
1412
|
+
var cronParser3; exports.cron = void 0;
|
|
1413
|
+
var init_define_cron = __esm({
|
|
1414
|
+
"src/scheduler/define-cron.ts"() {
|
|
1415
|
+
init_cjs_shims();
|
|
1416
|
+
init_registry2();
|
|
1417
|
+
cronParser3 = _cronParser__default.default.default ?? _cronParser__default.default;
|
|
1418
|
+
exports.cron = (options) => {
|
|
1419
|
+
let expression;
|
|
1420
|
+
let intervalMs;
|
|
1421
|
+
if ("expression" in options && options.expression) {
|
|
1422
|
+
try {
|
|
1423
|
+
cronParser3.parseExpression(options.expression, { tz: options.timezone });
|
|
1424
|
+
} catch {
|
|
1425
|
+
throw new Error(
|
|
1426
|
+
`[OqronKit] Invalid cron expression for "${options.name}": "${options.expression}"`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
expression = options.expression;
|
|
1430
|
+
} else if ("every" in options && options.every) {
|
|
1431
|
+
intervalMs = everyToIntervalMs(options.every);
|
|
1432
|
+
} else {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
`[OqronKit] Cron "${options.name}" must specify either "expression" or "every"`
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
const def = {
|
|
1438
|
+
name: options.name,
|
|
1439
|
+
expression,
|
|
1440
|
+
intervalMs,
|
|
1441
|
+
timezone: options.timezone,
|
|
1442
|
+
missedFire: options.missedFire ?? "skip",
|
|
1443
|
+
overlap: options.overlap ?? "skip",
|
|
1444
|
+
guaranteedWorker: options.guaranteedWorker ?? false,
|
|
1445
|
+
heartbeatMs: options.heartbeatMs,
|
|
1446
|
+
lockTtlMs: options.lockTtlMs,
|
|
1447
|
+
timeout: options.timeout,
|
|
1448
|
+
tags: options.tags ?? [],
|
|
1449
|
+
handler: options.handler,
|
|
1450
|
+
hooks: options.hooks,
|
|
1451
|
+
retries: options.retries
|
|
1452
|
+
};
|
|
1453
|
+
_registerCron(def);
|
|
1454
|
+
return def;
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
// src/scheduler/registry-schedule.ts
|
|
1460
|
+
function _getPending2() {
|
|
1461
|
+
if (!globalThis[GLOBAL_KEY2]) {
|
|
1462
|
+
globalThis[GLOBAL_KEY2] = [];
|
|
1463
|
+
}
|
|
1464
|
+
return globalThis[GLOBAL_KEY2];
|
|
1465
|
+
}
|
|
1466
|
+
function _registerSchedule(def) {
|
|
1467
|
+
_getPending2().push(def);
|
|
1468
|
+
}
|
|
1469
|
+
function _drainPendingSchedules() {
|
|
1470
|
+
const pending = _getPending2();
|
|
1471
|
+
return pending.splice(0);
|
|
1472
|
+
}
|
|
1473
|
+
var GLOBAL_KEY2;
|
|
1474
|
+
var init_registry_schedule = __esm({
|
|
1475
|
+
"src/scheduler/registry-schedule.ts"() {
|
|
1476
|
+
init_cjs_shims();
|
|
1477
|
+
GLOBAL_KEY2 = "__oqronkit_pending_schedules__";
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// src/scheduler/define-schedule.ts
|
|
1482
|
+
function _attachScheduleEngine(engine) {
|
|
1483
|
+
engineRef.current = engine;
|
|
1484
|
+
}
|
|
1485
|
+
var engineRef; exports.schedule = void 0;
|
|
1486
|
+
var init_define_schedule = __esm({
|
|
1487
|
+
"src/scheduler/define-schedule.ts"() {
|
|
1488
|
+
init_cjs_shims();
|
|
1489
|
+
init_registry_schedule();
|
|
1490
|
+
engineRef = { current: null };
|
|
1491
|
+
exports.schedule = (options) => {
|
|
1492
|
+
const def = {
|
|
1493
|
+
name: options.name,
|
|
1494
|
+
runAt: options.runAt,
|
|
1495
|
+
runAfter: options.runAfter,
|
|
1496
|
+
recurring: options.recurring,
|
|
1497
|
+
rrule: options.rrule,
|
|
1498
|
+
every: options.every,
|
|
1499
|
+
timezone: options.timezone,
|
|
1500
|
+
missedFire: options.missedFire ?? "skip",
|
|
1501
|
+
overlap: options.overlap ?? "skip",
|
|
1502
|
+
guaranteedWorker: options.guaranteedWorker ?? false,
|
|
1503
|
+
heartbeatMs: options.heartbeatMs,
|
|
1504
|
+
lockTtlMs: options.lockTtlMs,
|
|
1505
|
+
timeout: options.timeout,
|
|
1506
|
+
tags: options.tags ?? [],
|
|
1507
|
+
condition: options.condition,
|
|
1508
|
+
handler: options.handler,
|
|
1509
|
+
hooks: options.hooks,
|
|
1510
|
+
payload: options.payload,
|
|
1511
|
+
retries: options.retries
|
|
1512
|
+
};
|
|
1513
|
+
_registerSchedule(def);
|
|
1514
|
+
return {
|
|
1515
|
+
...def,
|
|
1516
|
+
trigger: async (opts) => {
|
|
1517
|
+
if (!engineRef.current) {
|
|
1518
|
+
throw new Error(
|
|
1519
|
+
`[OqronKit] Cannot trigger "${options.name}" \u2014 ScheduleEngine is not running.`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const dynamicDef = { ...def };
|
|
1523
|
+
if (opts) {
|
|
1524
|
+
if (opts.nameSuffix) dynamicDef.name = `${def.name}:${opts.nameSuffix}`;
|
|
1525
|
+
if (opts.runAt) dynamicDef.runAt = opts.runAt;
|
|
1526
|
+
if (opts.runAfter) dynamicDef.runAfter = opts.runAfter;
|
|
1527
|
+
if (opts.recurring) dynamicDef.recurring = opts.recurring;
|
|
1528
|
+
if (opts.rrule) dynamicDef.rrule = opts.rrule;
|
|
1529
|
+
if (opts.every) dynamicDef.every = opts.every;
|
|
1530
|
+
if (opts.payload) dynamicDef.payload = opts.payload;
|
|
1531
|
+
}
|
|
1532
|
+
if (!opts?.runAt && !opts?.runAfter && !opts?.recurring && !opts?.rrule && !opts?.every) {
|
|
1533
|
+
dynamicDef.runAt = /* @__PURE__ */ new Date();
|
|
1534
|
+
}
|
|
1535
|
+
await engineRef.current.registerDynamic(dynamicDef);
|
|
1536
|
+
},
|
|
1537
|
+
schedule: async (opts) => {
|
|
1538
|
+
if (!engineRef.current) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
`[OqronKit] Cannot schedule "${options.name}" \u2014 ScheduleEngine is not running.`
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const dynamicDef = { ...def };
|
|
1544
|
+
if (opts) {
|
|
1545
|
+
if (opts.nameSuffix) dynamicDef.name = `${def.name}:${opts.nameSuffix}`;
|
|
1546
|
+
if (opts.runAt) dynamicDef.runAt = opts.runAt;
|
|
1547
|
+
if (opts.runAfter) dynamicDef.runAfter = opts.runAfter;
|
|
1548
|
+
if (opts.recurring) dynamicDef.recurring = opts.recurring;
|
|
1549
|
+
if (opts.rrule) dynamicDef.rrule = opts.rrule;
|
|
1550
|
+
if (opts.every) dynamicDef.every = opts.every;
|
|
1551
|
+
if (opts.payload) dynamicDef.payload = opts.payload;
|
|
1552
|
+
}
|
|
1553
|
+
await engineRef.current.registerDynamic(dynamicDef);
|
|
1554
|
+
},
|
|
1555
|
+
cancel: async () => {
|
|
1556
|
+
if (!engineRef.current) return;
|
|
1557
|
+
await engineRef.current.cancel(def.name);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
var ScheduleEngine;
|
|
1564
|
+
var init_schedule_engine = __esm({
|
|
1565
|
+
"src/scheduler/schedule-engine.ts"() {
|
|
1566
|
+
init_cjs_shims();
|
|
1567
|
+
init_core();
|
|
1568
|
+
init_lock();
|
|
1569
|
+
init_define_schedule();
|
|
1570
|
+
ScheduleEngine = class {
|
|
1571
|
+
constructor(staticSchedules, db, lock, logger, environment, project, config) {
|
|
1572
|
+
this.db = db;
|
|
1573
|
+
this.lock = lock;
|
|
1574
|
+
this.environment = environment;
|
|
1575
|
+
this.project = project;
|
|
1576
|
+
this.config = config;
|
|
1577
|
+
this.nodeId = crypto.randomUUID();
|
|
1578
|
+
this.logger = logger ?? createLogger({ level: "info" }, { module: "scheduler" });
|
|
1579
|
+
this.stallDetector = new StallDetector(this.lock, this.logger, 15e3);
|
|
1580
|
+
for (const def of staticSchedules) {
|
|
1581
|
+
this.schedules.set(def.name, def);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
name = "scheduler";
|
|
1585
|
+
enabled = true;
|
|
1586
|
+
nodeId;
|
|
1587
|
+
logger;
|
|
1588
|
+
leader;
|
|
1589
|
+
stallDetector;
|
|
1590
|
+
tickTimer;
|
|
1591
|
+
activeJobs = /* @__PURE__ */ new Map();
|
|
1592
|
+
// Includes static instances and dynamically triggered instances
|
|
1593
|
+
schedules = /* @__PURE__ */ new Map();
|
|
1594
|
+
_hasRunLeaderInit = false;
|
|
1595
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
1596
|
+
async init() {
|
|
1597
|
+
this.logger.info("Initializing schedule engine", {
|
|
1598
|
+
nodeId: this.nodeId,
|
|
1599
|
+
staticCount: this.schedules.size
|
|
1600
|
+
});
|
|
1601
|
+
_attachScheduleEngine(this);
|
|
1602
|
+
const now = /* @__PURE__ */ new Date();
|
|
1603
|
+
for (const def of this.schedules.values()) {
|
|
1604
|
+
await this.upsertAndSeed(def, now);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
async upsertAndSeed(def, now) {
|
|
1608
|
+
await this.db.upsertSchedule(def);
|
|
1609
|
+
const nextRun = this.computeNextRun(def, now);
|
|
1610
|
+
if (nextRun) {
|
|
1611
|
+
await this.db.updateNextRun(def.name, nextRun);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
/** Dynamically register and schedule a definition from trigger/schedule call */
|
|
1615
|
+
async registerDynamic(def) {
|
|
1616
|
+
this.schedules.set(def.name, def);
|
|
1617
|
+
await this.upsertAndSeed(def, /* @__PURE__ */ new Date());
|
|
1618
|
+
this.logger.debug("Registered dynamic schedule", { name: def.name });
|
|
1619
|
+
}
|
|
1620
|
+
async cancel(name) {
|
|
1621
|
+
this.schedules.delete(name);
|
|
1622
|
+
}
|
|
1623
|
+
async start() {
|
|
1624
|
+
this.leader = new LeaderElection(
|
|
1625
|
+
this.lock,
|
|
1626
|
+
this.logger,
|
|
1627
|
+
"oqron:scheduleengine:leader",
|
|
1628
|
+
this.nodeId,
|
|
1629
|
+
3e4
|
|
1630
|
+
);
|
|
1631
|
+
await this.leader.start();
|
|
1632
|
+
const interval = this.config?.tickInterval ?? 1e3;
|
|
1633
|
+
this.tickTimer = setInterval(() => {
|
|
1634
|
+
void this.tick();
|
|
1635
|
+
}, interval);
|
|
1636
|
+
this.logger.info("Schedule engine started", {
|
|
1637
|
+
nodeId: this.nodeId,
|
|
1638
|
+
interval
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
async stop() {
|
|
1642
|
+
if (this.tickTimer) clearInterval(this.tickTimer);
|
|
1643
|
+
if (this.leader) await this.leader.stop();
|
|
1644
|
+
this.stallDetector.stop();
|
|
1645
|
+
for (const job of this.activeJobs.values()) {
|
|
1646
|
+
if (job.abort) job.abort.abort();
|
|
1647
|
+
if (job.worker) {
|
|
1648
|
+
await job.worker.stop();
|
|
1649
|
+
} else {
|
|
1650
|
+
await this.lock.release(job.lockKey, this.nodeId).catch(() => {
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
this.activeJobs.clear();
|
|
1655
|
+
this.logger.info("Schedule engine stopped");
|
|
1656
|
+
}
|
|
1657
|
+
// ── Core scheduling helpers ─────────────────────────────────────────────────
|
|
1658
|
+
computeNextRun(def, from) {
|
|
1659
|
+
try {
|
|
1660
|
+
if (def.runAt) {
|
|
1661
|
+
return new Date(def.runAt);
|
|
1662
|
+
}
|
|
1663
|
+
if (def.runAfter) {
|
|
1664
|
+
const add = (def.runAfter.days ?? 0) * 864e5 + (def.runAfter.hours ?? 0) * 36e5 + (def.runAfter.minutes ?? 0) * 6e4 + (def.runAfter.seconds ?? 0) * 1e3;
|
|
1665
|
+
return new Date(from.getTime() + add);
|
|
1666
|
+
}
|
|
1667
|
+
if (def.rrule) {
|
|
1668
|
+
const rule = rrule.rrulestr(def.rrule);
|
|
1669
|
+
return rule.after(from);
|
|
1670
|
+
}
|
|
1671
|
+
if (def.recurring) {
|
|
1672
|
+
const freqMapHash = {
|
|
1673
|
+
daily: rrule.RRule.DAILY,
|
|
1674
|
+
weekly: rrule.RRule.WEEKLY,
|
|
1675
|
+
monthly: rrule.RRule.MONTHLY,
|
|
1676
|
+
yearly: rrule.RRule.YEARLY
|
|
1677
|
+
};
|
|
1678
|
+
const freq = freqMapHash[def.recurring.frequency] ?? rrule.RRule.DAILY;
|
|
1679
|
+
const options = { freq };
|
|
1680
|
+
if (def.recurring.months?.length)
|
|
1681
|
+
options.bymonth = def.recurring.months;
|
|
1682
|
+
if (def.recurring.dayOfMonth)
|
|
1683
|
+
options.bymonthday = [def.recurring.dayOfMonth];
|
|
1684
|
+
if (def.recurring.at) {
|
|
1685
|
+
options.byhour = [def.recurring.at.hour];
|
|
1686
|
+
options.byminute = [def.recurring.at.minute];
|
|
1687
|
+
options.bysecond = [0];
|
|
1688
|
+
}
|
|
1689
|
+
const rule = new rrule.RRule(options);
|
|
1690
|
+
return rule.after(from);
|
|
1691
|
+
}
|
|
1692
|
+
if (def.every) {
|
|
1693
|
+
const add = (def.every.hours ?? 0) * 36e5 + (def.every.minutes ?? 0) * 6e4 + (def.every.seconds ?? 0) * 1e3;
|
|
1694
|
+
return new Date(from.getTime() + add);
|
|
1695
|
+
}
|
|
1696
|
+
} catch (err) {
|
|
1697
|
+
this.logger.error("Failed to compute next run", {
|
|
1698
|
+
name: def.name,
|
|
1699
|
+
err: String(err)
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
// ── Leader init (missed-fire recovery + stall detector) ─────────────────────
|
|
1705
|
+
async handleLeaderInit() {
|
|
1706
|
+
this.logger.info("Schedule Engine: Performing leader initialization...");
|
|
1707
|
+
this.stallDetector.start(
|
|
1708
|
+
() => {
|
|
1709
|
+
return Array.from(this.activeJobs.values()).filter((job) => job.worker !== void 0).map((job) => ({
|
|
1710
|
+
key: job.lockKey,
|
|
1711
|
+
ownerId: this.nodeId
|
|
1712
|
+
}));
|
|
1713
|
+
},
|
|
1714
|
+
(key) => {
|
|
1715
|
+
this.logger.warn("Local stall detected", { key });
|
|
1716
|
+
}
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
// ── Tick loop ───────────────────────────────────────────────────────────────
|
|
1720
|
+
async tick() {
|
|
1721
|
+
if (!this.leader?.isLeader) return;
|
|
1722
|
+
if (!this._hasRunLeaderInit) {
|
|
1723
|
+
this._hasRunLeaderInit = true;
|
|
1724
|
+
await this.handleLeaderInit();
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
const now = /* @__PURE__ */ new Date();
|
|
1728
|
+
const due = await this.db.getDueSchedules(now, 50);
|
|
1729
|
+
for (const { name } of due) {
|
|
1730
|
+
const def = this.schedules.get(name);
|
|
1731
|
+
if (!def) continue;
|
|
1732
|
+
if (def.condition) {
|
|
1733
|
+
try {
|
|
1734
|
+
const conditionVal = await def.condition(
|
|
1735
|
+
new ScheduleContext({
|
|
1736
|
+
id: "eval",
|
|
1737
|
+
scheduleName: def.name,
|
|
1738
|
+
firedAt: now,
|
|
1739
|
+
logger: this.logger,
|
|
1740
|
+
signal: new AbortController().signal,
|
|
1741
|
+
payload: def.payload,
|
|
1742
|
+
environment: this.environment,
|
|
1743
|
+
project: this.project
|
|
1744
|
+
})
|
|
1745
|
+
);
|
|
1746
|
+
if (!conditionVal) {
|
|
1747
|
+
const nextRun2 = this.computeNextRun(def, now);
|
|
1748
|
+
if (nextRun2) await this.db.updateNextRun(def.name, nextRun2);
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
this.logger.error("Condition crashed", {
|
|
1753
|
+
name: def.name,
|
|
1754
|
+
error: String(e)
|
|
1755
|
+
});
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
let nextRun = null;
|
|
1760
|
+
if (!def.runAt && !def.runAfter) {
|
|
1761
|
+
nextRun = this.computeNextRun(def, now);
|
|
1762
|
+
if (!nextRun) {
|
|
1763
|
+
this.logger.error(
|
|
1764
|
+
"Cannot compute next run \u2014 suspending schedule to prevent runaway loop",
|
|
1765
|
+
{ name: def.name }
|
|
1766
|
+
);
|
|
1767
|
+
await this.db.updateNextRun(def.name, null).catch(() => {
|
|
1768
|
+
});
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
await this.db.updateNextRun(def.name, nextRun);
|
|
1773
|
+
void this.fire(def);
|
|
1774
|
+
}
|
|
1775
|
+
} catch (err) {
|
|
1776
|
+
this.logger.error("Tick error", { err: String(err) });
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
// ── Fire handler ────────────────────────────────────────────────────────────
|
|
1780
|
+
async fire(def) {
|
|
1781
|
+
const isOverlapSkip = def.overlap === "skip" || def.overlap === false;
|
|
1782
|
+
if (isOverlapSkip) {
|
|
1783
|
+
for (const job of this.activeJobs.values()) {
|
|
1784
|
+
if (job.lockKey === `oqron:schedule:run:${def.name}`) {
|
|
1785
|
+
this.logger.debug("Skipping overlapping run", { name: def.name });
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const runId = crypto.randomUUID();
|
|
1791
|
+
const lockKey = isOverlapSkip ? `oqron:schedule:run:${def.name}` : `oqron:schedule:run:${def.name}:${runId}`;
|
|
1792
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
1793
|
+
let worker;
|
|
1794
|
+
let acquired = false;
|
|
1795
|
+
if (def.guaranteedWorker) {
|
|
1796
|
+
worker = new HeartbeatWorker(
|
|
1797
|
+
this.lock,
|
|
1798
|
+
this.logger,
|
|
1799
|
+
lockKey,
|
|
1800
|
+
this.nodeId,
|
|
1801
|
+
def.lockTtlMs ?? 3e4,
|
|
1802
|
+
def.heartbeatMs ?? 1e4
|
|
1803
|
+
);
|
|
1804
|
+
acquired = await worker.start();
|
|
1805
|
+
} else {
|
|
1806
|
+
acquired = await this.lock.acquire(
|
|
1807
|
+
lockKey,
|
|
1808
|
+
this.nodeId,
|
|
1809
|
+
def.lockTtlMs ?? 3e4
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (!acquired) return;
|
|
1813
|
+
const abort = new AbortController();
|
|
1814
|
+
this.activeJobs.set(runId, { runId, lockKey, worker, abort });
|
|
1815
|
+
await this.db.recordExecution({
|
|
1816
|
+
id: runId,
|
|
1817
|
+
scheduleId: def.name,
|
|
1818
|
+
status: "running",
|
|
1819
|
+
startedAt
|
|
1820
|
+
});
|
|
1821
|
+
void Promise.resolve().then(async () => {
|
|
1822
|
+
const ctx = new ScheduleContext({
|
|
1823
|
+
id: runId,
|
|
1824
|
+
logger: this.logger.child({ schedule: def.name }),
|
|
1825
|
+
signal: abort.signal,
|
|
1826
|
+
firedAt: startedAt,
|
|
1827
|
+
scheduleName: def.name,
|
|
1828
|
+
payload: def.payload,
|
|
1829
|
+
environment: this.environment,
|
|
1830
|
+
project: this.project,
|
|
1831
|
+
onProgress: (percent, label) => {
|
|
1832
|
+
this.db.updateJobProgress(runId, percent, label).catch(
|
|
1833
|
+
(err) => this.logger.error("Failed to update schedule progress", {
|
|
1834
|
+
runId,
|
|
1835
|
+
err
|
|
1836
|
+
})
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1840
|
+
let status = "completed";
|
|
1841
|
+
let error;
|
|
1842
|
+
let timeoutHandle;
|
|
1843
|
+
let finalResult;
|
|
1844
|
+
let attempts = 1;
|
|
1845
|
+
const maxAttempts = (def.retries?.max ?? 0) + 1;
|
|
1846
|
+
while (attempts <= maxAttempts) {
|
|
1847
|
+
try {
|
|
1848
|
+
if (def.hooks?.beforeRun) await def.hooks.beforeRun(ctx);
|
|
1849
|
+
if (def.timeout) {
|
|
1850
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1851
|
+
timeoutHandle = setTimeout(() => {
|
|
1852
|
+
abort.abort();
|
|
1853
|
+
reject(new Error(`Handler timed out after ${def.timeout}ms`));
|
|
1854
|
+
}, def.timeout);
|
|
1855
|
+
});
|
|
1856
|
+
finalResult = await Promise.race([
|
|
1857
|
+
def.handler(ctx),
|
|
1858
|
+
timeoutPromise
|
|
1859
|
+
]);
|
|
1860
|
+
} else {
|
|
1861
|
+
finalResult = await def.handler(ctx);
|
|
1862
|
+
}
|
|
1863
|
+
if (def.hooks?.afterRun) {
|
|
1864
|
+
await def.hooks.afterRun(ctx, finalResult);
|
|
1865
|
+
}
|
|
1866
|
+
status = "completed";
|
|
1867
|
+
error = void 0;
|
|
1868
|
+
break;
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
error = err instanceof Error ? err.message : String(err);
|
|
1871
|
+
status = "failed";
|
|
1872
|
+
if (def.hooks?.onError && err instanceof Error) {
|
|
1873
|
+
try {
|
|
1874
|
+
await Promise.resolve(def.hooks.onError(ctx, err));
|
|
1875
|
+
} catch (e) {
|
|
1876
|
+
this.logger.error("onError hook threw", { err: String(e) });
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (attempts < maxAttempts) {
|
|
1880
|
+
this.logger.warn("Schedule handler threw, retrying...", {
|
|
1881
|
+
name: def.name,
|
|
1882
|
+
runId,
|
|
1883
|
+
attempt: attempts,
|
|
1884
|
+
error
|
|
1885
|
+
});
|
|
1886
|
+
const baseDelay = def.retries?.baseDelay ?? 2e3;
|
|
1887
|
+
const delay = def.retries?.strategy === "exponential" ? baseDelay * 2 ** (attempts - 1) : baseDelay;
|
|
1888
|
+
attempts++;
|
|
1889
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1890
|
+
} else {
|
|
1891
|
+
this.logger.error("Schedule handler failed completely", {
|
|
1892
|
+
name: def.name,
|
|
1893
|
+
runId,
|
|
1894
|
+
attempts,
|
|
1895
|
+
error
|
|
1896
|
+
});
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
} finally {
|
|
1900
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const completedAt = /* @__PURE__ */ new Date();
|
|
1904
|
+
await this.db.recordExecution({
|
|
1905
|
+
id: runId,
|
|
1906
|
+
scheduleId: def.name,
|
|
1907
|
+
status,
|
|
1908
|
+
startedAt,
|
|
1909
|
+
completedAt,
|
|
1910
|
+
error,
|
|
1911
|
+
result: finalResult !== void 0 ? JSON.stringify(finalResult) : void 0,
|
|
1912
|
+
attempts,
|
|
1913
|
+
durationMs: completedAt.getTime() - startedAt.getTime(),
|
|
1914
|
+
...status === "completed" ? { progressPercent: 100, progressLabel: "Completed" } : {}
|
|
1915
|
+
});
|
|
1916
|
+
if (worker) {
|
|
1917
|
+
await worker.stop();
|
|
1918
|
+
} else {
|
|
1919
|
+
await this.lock.release(lockKey, this.nodeId).catch(() => {
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
this.activeJobs.delete(runId);
|
|
1923
|
+
const keepJobHistory = def.keepHistory ?? this.config?.keepJobHistory ?? true;
|
|
1924
|
+
const keepFailedHistory = def.keepFailedHistory ?? this.config?.keepFailedJobHistory ?? true;
|
|
1925
|
+
if (keepJobHistory !== true || keepFailedHistory !== true) {
|
|
1926
|
+
this.db.pruneHistoryForSchedule(def.name, keepJobHistory, keepFailedHistory).catch(
|
|
1927
|
+
(err) => this.logger.debug("Failed to prune history", {
|
|
1928
|
+
err,
|
|
1929
|
+
name: def.name
|
|
1930
|
+
})
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
this.logger.info("Schedule finished", {
|
|
1934
|
+
name: def.name,
|
|
1935
|
+
runId,
|
|
1936
|
+
status,
|
|
1937
|
+
attempts
|
|
1938
|
+
});
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
// src/scheduler/index.ts
|
|
1946
|
+
var scheduler_exports = {};
|
|
1947
|
+
__export(scheduler_exports, {
|
|
1948
|
+
ScheduleEngine: () => ScheduleEngine,
|
|
1949
|
+
SchedulerModule: () => SchedulerModule,
|
|
1950
|
+
_drainPending: () => _drainPending,
|
|
1951
|
+
_drainPendingSchedules: () => _drainPendingSchedules,
|
|
1952
|
+
_registerCron: () => _registerCron,
|
|
1953
|
+
_registerSchedule: () => _registerSchedule,
|
|
1954
|
+
cron: () => exports.cron,
|
|
1955
|
+
getNextRunDate: () => getNextRunDate,
|
|
1956
|
+
schedule: () => exports.schedule
|
|
1957
|
+
});
|
|
1958
|
+
var init_scheduler = __esm({
|
|
1959
|
+
"src/scheduler/index.ts"() {
|
|
1960
|
+
init_cjs_shims();
|
|
1961
|
+
init_cron_engine();
|
|
1962
|
+
init_define_cron();
|
|
1963
|
+
init_define_schedule();
|
|
1964
|
+
init_expression_parser();
|
|
1965
|
+
init_registry2();
|
|
1966
|
+
init_registry_schedule();
|
|
1967
|
+
init_schedule_engine();
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// src/index.ts
|
|
1972
|
+
init_cjs_shims();
|
|
1973
|
+
init_core();
|
|
1974
|
+
|
|
1975
|
+
// src/db/index.ts
|
|
1976
|
+
init_cjs_shims();
|
|
1977
|
+
|
|
1978
|
+
// src/db/adapters/memory.adapter.ts
|
|
1979
|
+
init_cjs_shims();
|
|
1980
|
+
var MemoryOqronAdapter = class {
|
|
1981
|
+
schedules = /* @__PURE__ */ new Map();
|
|
1982
|
+
jobs = /* @__PURE__ */ new Map();
|
|
1983
|
+
async upsertSchedule(def) {
|
|
1984
|
+
if (!this.schedules.has(def.name)) {
|
|
1985
|
+
this.schedules.set(def.name, {
|
|
1986
|
+
name: def.name,
|
|
1987
|
+
nextRunAt: null,
|
|
1988
|
+
lastRunAt: null
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
async getDueSchedules(now, limit, prefix) {
|
|
1993
|
+
const due = [];
|
|
1994
|
+
for (const s of this.schedules.values()) {
|
|
1995
|
+
if (prefix && !s.name.startsWith(prefix)) continue;
|
|
1996
|
+
if (s.nextRunAt !== null && s.nextRunAt <= now) {
|
|
1997
|
+
due.push({ name: s.name });
|
|
1998
|
+
}
|
|
1999
|
+
if (due.length >= limit) break;
|
|
2000
|
+
}
|
|
2001
|
+
return due;
|
|
2002
|
+
}
|
|
2003
|
+
async getSchedules(prefix) {
|
|
2004
|
+
const filtered = prefix ? Array.from(this.schedules.values()).filter(
|
|
2005
|
+
(s) => s.name.startsWith(prefix)
|
|
2006
|
+
) : Array.from(this.schedules.values());
|
|
2007
|
+
return filtered.map((s) => ({
|
|
2008
|
+
name: s.name,
|
|
2009
|
+
lastRunAt: s.lastRunAt,
|
|
2010
|
+
nextRunAt: s.nextRunAt
|
|
2011
|
+
}));
|
|
2012
|
+
}
|
|
2013
|
+
async updateNextRun(scheduleId, nextRunAt) {
|
|
2014
|
+
const s = this.schedules.get(scheduleId);
|
|
2015
|
+
if (s) {
|
|
2016
|
+
s.nextRunAt = nextRunAt;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
async recordExecution(job) {
|
|
2020
|
+
const existing = this.jobs.get(job.id);
|
|
2021
|
+
if (existing) {
|
|
2022
|
+
this.jobs.set(job.id, {
|
|
2023
|
+
...existing,
|
|
2024
|
+
...job,
|
|
2025
|
+
progressPercent: job.progressPercent ?? existing.progressPercent,
|
|
2026
|
+
progressLabel: job.progressLabel ?? existing.progressLabel
|
|
2027
|
+
});
|
|
2028
|
+
} else {
|
|
2029
|
+
this.jobs.set(job.id, { ...job });
|
|
2030
|
+
}
|
|
2031
|
+
if (job.scheduleId && (job.status === "completed" || job.status === "failed")) {
|
|
2032
|
+
const s = this.schedules.get(job.scheduleId);
|
|
2033
|
+
if (s) {
|
|
2034
|
+
s.lastRunAt = job.completedAt ?? /* @__PURE__ */ new Date();
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async updateJobProgress(id, progressPercent, progressLabel) {
|
|
2039
|
+
const job = this.jobs.get(id);
|
|
2040
|
+
if (job) {
|
|
2041
|
+
job.progressPercent = progressPercent;
|
|
2042
|
+
job.progressLabel = progressLabel ?? job.progressLabel;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
async getExecutions(scheduleId, opts) {
|
|
2046
|
+
const records = Array.from(this.jobs.values()).filter((j) => j.scheduleId === scheduleId).sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
2047
|
+
return records.slice(opts.offset, opts.offset + opts.limit);
|
|
2048
|
+
}
|
|
2049
|
+
async getActiveJobs() {
|
|
2050
|
+
return Array.from(this.jobs.values()).filter((j) => j.status === "running");
|
|
2051
|
+
}
|
|
2052
|
+
async cleanOldExecutions(before) {
|
|
2053
|
+
let removed = 0;
|
|
2054
|
+
for (const [id, job] of this.jobs.entries()) {
|
|
2055
|
+
if (job.startedAt < before) {
|
|
2056
|
+
this.jobs.delete(id);
|
|
2057
|
+
removed++;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return removed;
|
|
2061
|
+
}
|
|
2062
|
+
async pruneHistoryForSchedule(scheduleId, keepJobHistory, keepFailedJobHistory) {
|
|
2063
|
+
if (keepJobHistory === false && keepFailedJobHistory === false) return;
|
|
2064
|
+
const all = Array.from(this.jobs.values()).filter(
|
|
2065
|
+
(j) => j.scheduleId === scheduleId && ["completed", "failed", "dead"].includes(j.status)
|
|
2066
|
+
);
|
|
2067
|
+
all.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
2068
|
+
const successes = all.filter((j) => j.status === "completed");
|
|
2069
|
+
const failures = all.filter((j) => ["failed", "dead"].includes(j.status));
|
|
2070
|
+
if (typeof keepJobHistory === "number" && successes.length > keepJobHistory) {
|
|
2071
|
+
const toRemove = successes.slice(keepJobHistory);
|
|
2072
|
+
for (const j of toRemove) this.jobs.delete(j.id);
|
|
2073
|
+
}
|
|
2074
|
+
if (typeof keepFailedJobHistory === "number" && failures.length > keepFailedJobHistory) {
|
|
2075
|
+
const toRemove = failures.slice(keepFailedJobHistory);
|
|
2076
|
+
for (const j of toRemove) this.jobs.delete(j.id);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
|
|
2081
|
+
// src/db/adapters/namespaced.adapter.ts
|
|
2082
|
+
init_cjs_shims();
|
|
2083
|
+
var NamespacedOqronAdapter = class {
|
|
2084
|
+
constructor(base, project = "default", environment = "development") {
|
|
2085
|
+
this.base = base;
|
|
2086
|
+
this.prefix = `${project}:${environment}:`;
|
|
2087
|
+
}
|
|
2088
|
+
prefix;
|
|
2089
|
+
ns(id) {
|
|
2090
|
+
return `${this.prefix}${id}`;
|
|
2091
|
+
}
|
|
2092
|
+
un(id) {
|
|
2093
|
+
return id.startsWith(this.prefix) ? id.slice(this.prefix.length) : id;
|
|
2094
|
+
}
|
|
2095
|
+
async upsertSchedule(def) {
|
|
2096
|
+
return this.base.upsertSchedule({ ...def, name: this.ns(def.name) });
|
|
2097
|
+
}
|
|
2098
|
+
async getDueSchedules(now, limit, prefix) {
|
|
2099
|
+
const combinedPrefix = prefix ? `${this.prefix}${prefix}` : this.prefix;
|
|
2100
|
+
const records = await this.base.getDueSchedules(now, limit, combinedPrefix);
|
|
2101
|
+
return records.filter((r) => r.name.startsWith(this.prefix)).map((r) => ({ name: this.un(r.name) }));
|
|
2102
|
+
}
|
|
2103
|
+
async getSchedules(prefix) {
|
|
2104
|
+
const combinedPrefix = prefix ? `${this.prefix}${prefix}` : this.prefix;
|
|
2105
|
+
const records = await this.base.getSchedules(combinedPrefix);
|
|
2106
|
+
return records.filter((r) => r.name.startsWith(this.prefix)).map((r) => ({
|
|
2107
|
+
...r,
|
|
2108
|
+
name: this.un(r.name)
|
|
2109
|
+
}));
|
|
2110
|
+
}
|
|
2111
|
+
async updateNextRun(scheduleId, nextRunAt) {
|
|
2112
|
+
return this.base.updateNextRun(this.ns(scheduleId), nextRunAt);
|
|
2113
|
+
}
|
|
2114
|
+
async recordExecution(job) {
|
|
2115
|
+
return this.base.recordExecution({
|
|
2116
|
+
...job,
|
|
2117
|
+
scheduleId: job.scheduleId ? this.ns(job.scheduleId) : void 0
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
async updateJobProgress(id, progressPercent, progressLabel) {
|
|
2121
|
+
return this.base.updateJobProgress(id, progressPercent, progressLabel);
|
|
2122
|
+
}
|
|
2123
|
+
async getExecutions(scheduleId, opts) {
|
|
2124
|
+
const records = await this.base.getExecutions(this.ns(scheduleId), opts);
|
|
2125
|
+
return records.map((r) => ({
|
|
2126
|
+
...r,
|
|
2127
|
+
scheduleId: r.scheduleId ? this.un(r.scheduleId) : void 0
|
|
2128
|
+
}));
|
|
2129
|
+
}
|
|
2130
|
+
async getActiveJobs() {
|
|
2131
|
+
const records = await this.base.getActiveJobs();
|
|
2132
|
+
return records.filter((r) => r.scheduleId?.startsWith(this.prefix)).map((r) => ({
|
|
2133
|
+
...r,
|
|
2134
|
+
scheduleId: r.scheduleId ? this.un(r.scheduleId) : void 0
|
|
2135
|
+
}));
|
|
2136
|
+
}
|
|
2137
|
+
async cleanOldExecutions(before) {
|
|
2138
|
+
return this.base.cleanOldExecutions(before);
|
|
2139
|
+
}
|
|
2140
|
+
async pruneHistoryForSchedule(scheduleId, keepJobHistory, keepFailedJobHistory) {
|
|
2141
|
+
return this.base.pruneHistoryForSchedule(
|
|
2142
|
+
this.ns(scheduleId),
|
|
2143
|
+
keepJobHistory,
|
|
2144
|
+
keepFailedJobHistory
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
|
|
2149
|
+
// src/db/adapters/sqlite.adapter.ts
|
|
2150
|
+
init_cjs_shims();
|
|
2151
|
+
var SqliteAdapter = class {
|
|
2152
|
+
db;
|
|
2153
|
+
/**
|
|
2154
|
+
* @param dbOrPath - Either a `better-sqlite3` Database instance (for testing with `:memory:`)
|
|
2155
|
+
* or a file path string. Defaults to `"oqron.sqlite"`.
|
|
2156
|
+
*/
|
|
2157
|
+
constructor(dbOrPath = "oqron.sqlite") {
|
|
2158
|
+
if (typeof dbOrPath === "string") {
|
|
2159
|
+
this.db = new Database2__default.default(dbOrPath);
|
|
2160
|
+
} else {
|
|
2161
|
+
this.db = dbOrPath;
|
|
2162
|
+
}
|
|
2163
|
+
this.db.pragma("journal_mode = WAL");
|
|
2164
|
+
this.db.pragma("synchronous = NORMAL");
|
|
2165
|
+
this.db.pragma("foreign_keys = ON");
|
|
2166
|
+
this.migrate();
|
|
2167
|
+
}
|
|
2168
|
+
/** Create tables if they don't exist */
|
|
2169
|
+
migrate() {
|
|
2170
|
+
this.db.exec(`
|
|
2171
|
+
CREATE TABLE IF NOT EXISTS oqron_schedules (
|
|
2172
|
+
id TEXT PRIMARY KEY,
|
|
2173
|
+
expression TEXT, -- Can be null now since rrule/runAt are supported
|
|
2174
|
+
timezone TEXT,
|
|
2175
|
+
missedFirePolicy TEXT NOT NULL DEFAULT 'skip',
|
|
2176
|
+
overlap INTEGER NOT NULL DEFAULT 1,
|
|
2177
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
2178
|
+
|
|
2179
|
+
-- Advanced Scheduling Columns
|
|
2180
|
+
runAt TEXT,
|
|
2181
|
+
runAfterOpts TEXT,
|
|
2182
|
+
rrule TEXT,
|
|
2183
|
+
recurring TEXT,
|
|
2184
|
+
|
|
2185
|
+
lastRunAt TEXT,
|
|
2186
|
+
nextRunAt TEXT
|
|
2187
|
+
);
|
|
2188
|
+
|
|
2189
|
+
CREATE TABLE IF NOT EXISTS oqron_jobs (
|
|
2190
|
+
id TEXT PRIMARY KEY,
|
|
2191
|
+
scheduleId TEXT,
|
|
2192
|
+
status TEXT NOT NULL,
|
|
2193
|
+
startedAt TEXT NOT NULL,
|
|
2194
|
+
completedAt TEXT,
|
|
2195
|
+
error TEXT,
|
|
2196
|
+
result TEXT,
|
|
2197
|
+
progressPercent INTEGER,
|
|
2198
|
+
progressLabel TEXT,
|
|
2199
|
+
attempts INTEGER DEFAULT 1,
|
|
2200
|
+
FOREIGN KEY(scheduleId) REFERENCES oqron_schedules(id) ON DELETE CASCADE
|
|
2201
|
+
);
|
|
2202
|
+
|
|
2203
|
+
CREATE TABLE IF NOT EXISTS chrono_locks (
|
|
2204
|
+
resourceKey TEXT PRIMARY KEY,
|
|
2205
|
+
ownerId TEXT NOT NULL,
|
|
2206
|
+
expiresAt TEXT NOT NULL
|
|
2207
|
+
);
|
|
2208
|
+
`);
|
|
2209
|
+
const alters = [
|
|
2210
|
+
"ALTER TABLE oqron_schedules ADD COLUMN runAt TEXT",
|
|
2211
|
+
"ALTER TABLE oqron_schedules ADD COLUMN runAfterOpts TEXT",
|
|
2212
|
+
"ALTER TABLE oqron_schedules ADD COLUMN rrule TEXT",
|
|
2213
|
+
"ALTER TABLE oqron_schedules ADD COLUMN recurring TEXT",
|
|
2214
|
+
"ALTER TABLE oqron_jobs ADD COLUMN result TEXT",
|
|
2215
|
+
"ALTER TABLE oqron_jobs ADD COLUMN progressPercent INTEGER",
|
|
2216
|
+
"ALTER TABLE oqron_jobs ADD COLUMN progressLabel TEXT",
|
|
2217
|
+
"ALTER TABLE oqron_jobs ADD COLUMN attempts INTEGER",
|
|
2218
|
+
"ALTER TABLE oqron_jobs ADD COLUMN durationMs INTEGER"
|
|
2219
|
+
];
|
|
2220
|
+
for (const sql of alters) {
|
|
2221
|
+
try {
|
|
2222
|
+
this.db.exec(sql);
|
|
2223
|
+
} catch (_e) {
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async upsertSchedule(def) {
|
|
2228
|
+
const isSchedule = "runAt" in def || "rrule" in def || "recurring" in def || "runAfter" in def;
|
|
2229
|
+
this.db.prepare(
|
|
2230
|
+
`INSERT INTO oqron_schedules (
|
|
2231
|
+
id, expression, timezone, missedFirePolicy, overlap, tags,
|
|
2232
|
+
runAt, runAfterOpts, rrule, recurring
|
|
2233
|
+
)
|
|
2234
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2235
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2236
|
+
expression = excluded.expression,
|
|
2237
|
+
timezone = excluded.timezone,
|
|
2238
|
+
missedFirePolicy = excluded.missedFirePolicy,
|
|
2239
|
+
overlap = excluded.overlap,
|
|
2240
|
+
tags = excluded.tags,
|
|
2241
|
+
runAt = excluded.runAt,
|
|
2242
|
+
runAfterOpts = excluded.runAfterOpts,
|
|
2243
|
+
rrule = excluded.rrule,
|
|
2244
|
+
recurring = excluded.recurring`
|
|
2245
|
+
).run(
|
|
2246
|
+
def.name,
|
|
2247
|
+
"expression" in def ? def.expression ?? (def.intervalMs ? `every:${def.intervalMs}ms` : null) : "every" in def && def.every ? JSON.stringify(def.every) : null,
|
|
2248
|
+
def.timezone ?? null,
|
|
2249
|
+
def.missedFire,
|
|
2250
|
+
def.overlap !== "skip" && def.overlap !== false ? 1 : 0,
|
|
2251
|
+
JSON.stringify(def.tags),
|
|
2252
|
+
isSchedule && def.runAt ? def.runAt.toISOString() : null,
|
|
2253
|
+
isSchedule && def.runAfter ? JSON.stringify(def.runAfter) : null,
|
|
2254
|
+
isSchedule && def.rrule ? def.rrule : null,
|
|
2255
|
+
isSchedule && def.recurring ? JSON.stringify(def.recurring) : null
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
async getDueSchedules(now, limit, prefix) {
|
|
2259
|
+
let sql = `SELECT id as name FROM oqron_schedules WHERE nextRunAt <= ?`;
|
|
2260
|
+
const params = [now.toISOString()];
|
|
2261
|
+
if (prefix) {
|
|
2262
|
+
sql += ` AND id LIKE ?`;
|
|
2263
|
+
params.push(`${prefix}%`);
|
|
2264
|
+
}
|
|
2265
|
+
sql += ` LIMIT ?`;
|
|
2266
|
+
params.push(limit);
|
|
2267
|
+
return this.db.prepare(sql).all(...params);
|
|
2268
|
+
}
|
|
2269
|
+
async getSchedules(prefix) {
|
|
2270
|
+
let sql = `SELECT id, lastRunAt, nextRunAt FROM oqron_schedules`;
|
|
2271
|
+
const params = [];
|
|
2272
|
+
if (prefix) {
|
|
2273
|
+
sql += ` WHERE id LIKE ?`;
|
|
2274
|
+
params.push(`${prefix}%`);
|
|
2275
|
+
}
|
|
2276
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
2277
|
+
return rows.map((r) => ({
|
|
2278
|
+
name: r.id,
|
|
2279
|
+
lastRunAt: r.lastRunAt ? new Date(r.lastRunAt) : null,
|
|
2280
|
+
nextRunAt: r.nextRunAt ? new Date(r.nextRunAt) : null
|
|
2281
|
+
}));
|
|
2282
|
+
}
|
|
2283
|
+
async updateNextRun(scheduleId, nextRunAt) {
|
|
2284
|
+
this.db.prepare(`UPDATE oqron_schedules SET nextRunAt = ? WHERE id = ?`).run(nextRunAt ? nextRunAt.toISOString() : null, scheduleId);
|
|
2285
|
+
}
|
|
2286
|
+
async updateJobProgress(id, progressPercent, progressLabel) {
|
|
2287
|
+
this.db.prepare(
|
|
2288
|
+
`UPDATE oqron_jobs SET progressPercent = ?, progressLabel = ? WHERE id = ?`
|
|
2289
|
+
).run(progressPercent, progressLabel ?? null, id);
|
|
2290
|
+
}
|
|
2291
|
+
async recordExecution(job) {
|
|
2292
|
+
this.db.prepare(
|
|
2293
|
+
`INSERT INTO oqron_jobs (id, scheduleId, status, startedAt, completedAt, error, result, attempts, progressPercent, progressLabel, durationMs)
|
|
2294
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2295
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
2296
|
+
status = excluded.status,
|
|
2297
|
+
completedAt = excluded.completedAt,
|
|
2298
|
+
error = excluded.error,
|
|
2299
|
+
result = excluded.result,
|
|
2300
|
+
attempts = excluded.attempts,
|
|
2301
|
+
progressPercent = coalesce(excluded.progressPercent, progressPercent),
|
|
2302
|
+
progressLabel = coalesce(excluded.progressLabel, progressLabel),
|
|
2303
|
+
durationMs = excluded.durationMs`
|
|
2304
|
+
).run(
|
|
2305
|
+
job.id,
|
|
2306
|
+
job.scheduleId ?? null,
|
|
2307
|
+
job.status,
|
|
2308
|
+
job.startedAt.toISOString(),
|
|
2309
|
+
job.completedAt?.toISOString() ?? null,
|
|
2310
|
+
job.error ?? null,
|
|
2311
|
+
job.result ?? null,
|
|
2312
|
+
job.attempts ?? 1,
|
|
2313
|
+
job.progressPercent ?? null,
|
|
2314
|
+
job.progressLabel ?? null,
|
|
2315
|
+
job.durationMs ?? null
|
|
2316
|
+
);
|
|
2317
|
+
if (job.scheduleId && (job.status === "completed" || job.status === "failed")) {
|
|
2318
|
+
this.db.prepare(`UPDATE oqron_schedules SET lastRunAt = ? WHERE id = ?`).run((job.completedAt ?? /* @__PURE__ */ new Date()).toISOString(), job.scheduleId);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
async getExecutions(scheduleId, opts) {
|
|
2322
|
+
const rows = this.db.prepare(
|
|
2323
|
+
`SELECT id, scheduleId, status, startedAt, completedAt, error, result, progressPercent, progressLabel, attempts, durationMs
|
|
2324
|
+
FROM oqron_jobs WHERE scheduleId = ?
|
|
2325
|
+
ORDER BY startedAt DESC
|
|
2326
|
+
LIMIT ? OFFSET ?`
|
|
2327
|
+
).all(scheduleId, opts.limit, opts.offset);
|
|
2328
|
+
return rows.map((r) => ({
|
|
2329
|
+
id: r.id,
|
|
2330
|
+
scheduleId: r.scheduleId ?? void 0,
|
|
2331
|
+
status: r.status,
|
|
2332
|
+
startedAt: new Date(r.startedAt),
|
|
2333
|
+
completedAt: r.completedAt ? new Date(r.completedAt) : void 0,
|
|
2334
|
+
error: r.error ?? void 0,
|
|
2335
|
+
result: r.result ?? void 0,
|
|
2336
|
+
progressPercent: r.progressPercent ?? void 0,
|
|
2337
|
+
progressLabel: r.progressLabel ?? void 0,
|
|
2338
|
+
attempts: r.attempts ?? void 0,
|
|
2339
|
+
durationMs: r.durationMs ?? void 0
|
|
2340
|
+
}));
|
|
2341
|
+
}
|
|
2342
|
+
async getActiveJobs() {
|
|
2343
|
+
const rows = this.db.prepare(
|
|
2344
|
+
`SELECT id, scheduleId, status, startedAt, completedAt, error, result, progressPercent, progressLabel, attempts, durationMs
|
|
2345
|
+
FROM oqron_jobs WHERE status = 'running'`
|
|
2346
|
+
).all();
|
|
2347
|
+
return rows.map((r) => ({
|
|
2348
|
+
id: r.id,
|
|
2349
|
+
scheduleId: r.scheduleId ?? void 0,
|
|
2350
|
+
status: r.status,
|
|
2351
|
+
startedAt: new Date(r.startedAt),
|
|
2352
|
+
completedAt: r.completedAt ? new Date(r.completedAt) : void 0,
|
|
2353
|
+
error: r.error ?? void 0,
|
|
2354
|
+
result: r.result ?? void 0,
|
|
2355
|
+
progressPercent: r.progressPercent ?? void 0,
|
|
2356
|
+
progressLabel: r.progressLabel ?? void 0,
|
|
2357
|
+
attempts: r.attempts ?? void 0,
|
|
2358
|
+
durationMs: r.durationMs ?? void 0
|
|
2359
|
+
}));
|
|
2360
|
+
}
|
|
2361
|
+
async cleanOldExecutions(before) {
|
|
2362
|
+
const result = this.db.prepare(`DELETE FROM oqron_jobs WHERE startedAt < ?`).run(before.toISOString());
|
|
2363
|
+
return result.changes;
|
|
2364
|
+
}
|
|
2365
|
+
async pruneHistoryForSchedule(scheduleId, keepJobHistory, keepFailedJobHistory) {
|
|
2366
|
+
if (keepJobHistory === false && keepFailedJobHistory === false) return;
|
|
2367
|
+
if (typeof keepJobHistory === "number") {
|
|
2368
|
+
this.db.prepare(
|
|
2369
|
+
`
|
|
2370
|
+
DELETE FROM oqron_jobs
|
|
2371
|
+
WHERE scheduleId = ? AND status = 'completed'
|
|
2372
|
+
AND id NOT IN (
|
|
2373
|
+
SELECT id FROM oqron_jobs
|
|
2374
|
+
WHERE scheduleId = ? AND status = 'completed'
|
|
2375
|
+
ORDER BY startedAt DESC LIMIT ?
|
|
2376
|
+
)
|
|
2377
|
+
`
|
|
2378
|
+
).run(scheduleId, scheduleId, keepJobHistory);
|
|
2379
|
+
}
|
|
2380
|
+
if (typeof keepFailedJobHistory === "number") {
|
|
2381
|
+
this.db.prepare(
|
|
2382
|
+
`
|
|
2383
|
+
DELETE FROM oqron_jobs
|
|
2384
|
+
WHERE scheduleId = ? AND status IN ('failed', 'dead')
|
|
2385
|
+
AND id NOT IN (
|
|
2386
|
+
SELECT id FROM oqron_jobs
|
|
2387
|
+
WHERE scheduleId = ? AND status IN ('failed', 'dead')
|
|
2388
|
+
ORDER BY startedAt DESC LIMIT ?
|
|
2389
|
+
)
|
|
2390
|
+
`
|
|
2391
|
+
).run(scheduleId, scheduleId, keepFailedJobHistory);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
// src/index.ts
|
|
2397
|
+
init_lock();
|
|
2398
|
+
|
|
2399
|
+
// src/server/express.ts
|
|
2400
|
+
init_cjs_shims();
|
|
2401
|
+
|
|
2402
|
+
// src/server/handlers.ts
|
|
2403
|
+
init_cjs_shims();
|
|
2404
|
+
init_core();
|
|
2405
|
+
var recentEvents = [];
|
|
2406
|
+
function appendEvent(event, data) {
|
|
2407
|
+
recentEvents.unshift({ ts: (/* @__PURE__ */ new Date()).toISOString(), event, data });
|
|
2408
|
+
if (recentEvents.length > 500) recentEvents.pop();
|
|
2409
|
+
}
|
|
2410
|
+
exports.OqronEventBus.on(
|
|
2411
|
+
"job:start",
|
|
2412
|
+
(jobId, mod) => appendEvent("job:start", { jobId, mod })
|
|
2413
|
+
);
|
|
2414
|
+
exports.OqronEventBus.on(
|
|
2415
|
+
"job:success",
|
|
2416
|
+
(jobId) => appendEvent("job:success", { jobId })
|
|
2417
|
+
);
|
|
2418
|
+
exports.OqronEventBus.on(
|
|
2419
|
+
"job:fail",
|
|
2420
|
+
(jobId, err) => appendEvent("job:fail", { jobId, error: err.message })
|
|
2421
|
+
);
|
|
2422
|
+
exports.OqronEventBus.on("system:ready", () => appendEvent("system:ready", {}));
|
|
2423
|
+
exports.OqronEventBus.on("system:stop", () => appendEvent("system:stop", {}));
|
|
2424
|
+
async function handleHealth(_req) {
|
|
2425
|
+
return {
|
|
2426
|
+
status: 200,
|
|
2427
|
+
body: {
|
|
2428
|
+
ok: true,
|
|
2429
|
+
status: "running",
|
|
2430
|
+
uptime: process.uptime(),
|
|
2431
|
+
env: process.env.CHRONO_ENV ?? "development",
|
|
2432
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
async function handleEvents(req) {
|
|
2437
|
+
const limit = Math.min(Number(req.query.limit ?? 50), 200);
|
|
2438
|
+
return {
|
|
2439
|
+
status: 200,
|
|
2440
|
+
body: { ok: true, events: recentEvents.slice(0, limit) }
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
async function handleTrigger(req) {
|
|
2444
|
+
const id = req.params.id;
|
|
2445
|
+
if (!id)
|
|
2446
|
+
return { status: 400, body: { ok: false, error: "Missing :id param" } };
|
|
2447
|
+
appendEvent("manual:trigger", { id });
|
|
2448
|
+
return {
|
|
2449
|
+
status: 200,
|
|
2450
|
+
body: { ok: true, message: `Trigger queued for "${id}"` }
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async function dispatch(req) {
|
|
2454
|
+
const { method, path: path2 } = req;
|
|
2455
|
+
if (method === "GET" && path2 === "/health") return handleHealth();
|
|
2456
|
+
if (method === "GET" && path2 === "/events") return handleEvents(req);
|
|
2457
|
+
if (method === "POST" && path2.startsWith("/jobs/")) return handleTrigger(req);
|
|
2458
|
+
return { status: 404, body: { ok: false, error: "Not found" } };
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// src/server/express.ts
|
|
2462
|
+
function expressRouter() {
|
|
2463
|
+
return async function chronoMiddleware(req, res, next) {
|
|
2464
|
+
const matchPath = req.path;
|
|
2465
|
+
try {
|
|
2466
|
+
const result = await dispatch({
|
|
2467
|
+
method: req.method,
|
|
2468
|
+
path: matchPath,
|
|
2469
|
+
query: req.query || {},
|
|
2470
|
+
params: req.params || {}
|
|
2471
|
+
});
|
|
2472
|
+
if (result.status === 404 && result.body && result.body.error === "Not found") {
|
|
2473
|
+
return next();
|
|
2474
|
+
}
|
|
2475
|
+
res.status(result.status).json(result.body);
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
next(err);
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/server/fastify.ts
|
|
2483
|
+
init_cjs_shims();
|
|
2484
|
+
function fastifyPlugin(fastify, _opts, done) {
|
|
2485
|
+
fastify.all("*", async (req, reply) => {
|
|
2486
|
+
const pathParams = req.params["*"];
|
|
2487
|
+
const path2 = pathParams ? `/${pathParams}` : req.url;
|
|
2488
|
+
const result = await dispatch({
|
|
2489
|
+
method: req.method,
|
|
2490
|
+
path: path2,
|
|
2491
|
+
query: req.query,
|
|
2492
|
+
params: req.params
|
|
2493
|
+
});
|
|
2494
|
+
return reply.status(result.status).send(result.body);
|
|
2495
|
+
});
|
|
2496
|
+
done();
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
// src/index.ts
|
|
2500
|
+
init_core();
|
|
2501
|
+
init_lock();
|
|
2502
|
+
init_scheduler();
|
|
2503
|
+
var _config = null;
|
|
2504
|
+
var _db = null;
|
|
2505
|
+
var _lock = null;
|
|
2506
|
+
var _logger = null;
|
|
2507
|
+
var OqronKit = {
|
|
2508
|
+
/**
|
|
2509
|
+
* Initialize OqronKit: loads config, auto-discovers jobs, and boots modules.
|
|
2510
|
+
*
|
|
2511
|
+
* @param opts.cwd - Working directory for config file lookup and jobsDir resolution
|
|
2512
|
+
* @param opts.config - Explicit config object (skips loadConfig)
|
|
2513
|
+
*/
|
|
2514
|
+
async init(opts) {
|
|
2515
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
2516
|
+
_config = reconfigureConfig(opts?.config ?? await loadConfig(cwd));
|
|
2517
|
+
const loggerConfig = _config.logger === false ? { enabled: true } : _config.logger;
|
|
2518
|
+
_logger = createLogger(loggerConfig, { module: "oqronkit" });
|
|
2519
|
+
_logger.info(`Starting OqronKit in "${_config.environment}" environment`);
|
|
2520
|
+
if (!_config.db) {
|
|
2521
|
+
const msg = "No 'db' adapter configured. Falling back to ephemeral 'MemoryOqronAdapter'. [WARNING: Data will not persist across restarts]";
|
|
2522
|
+
if (_config.environment === "production") {
|
|
2523
|
+
_logger.fatal(`STERN WARNING: ${msg}`);
|
|
2524
|
+
} else {
|
|
2525
|
+
_logger.warn(msg);
|
|
2526
|
+
}
|
|
2527
|
+
_db = new MemoryOqronAdapter();
|
|
2528
|
+
} else if ("adapter" in _config.db) {
|
|
2529
|
+
const { adapter, url } = _config.db;
|
|
2530
|
+
if (adapter === "sqlite") {
|
|
2531
|
+
_db = new SqliteAdapter(url ?? "oqron.sqlite");
|
|
2532
|
+
} else if (adapter === "memory") {
|
|
2533
|
+
const msg = "Using ephemeral 'memory' database adapter. [WARNING: Data will not persist across restarts]";
|
|
2534
|
+
if (_config.environment === "production") {
|
|
2535
|
+
_logger.fatal(`STERN WARNING: ${msg}`);
|
|
2536
|
+
} else {
|
|
2537
|
+
_logger.warn(msg);
|
|
2538
|
+
}
|
|
2539
|
+
_db = new MemoryOqronAdapter();
|
|
2540
|
+
} else {
|
|
2541
|
+
throw new Error(
|
|
2542
|
+
`[OqronKit] Database adapter '${adapter}' not yet bundled. Please pass a custom IOqronAdapter instance.`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
} else {
|
|
2546
|
+
_db = _config.db;
|
|
2547
|
+
}
|
|
2548
|
+
if (!_config.lock) {
|
|
2549
|
+
_logger.warn(
|
|
2550
|
+
"No 'lock' adapter configured. Falling back to ephemeral 'MemoryLockAdapter'."
|
|
2551
|
+
);
|
|
2552
|
+
_lock = new exports.MemoryLockAdapter();
|
|
2553
|
+
} else if ("adapter" in _config.lock) {
|
|
2554
|
+
const { adapter, url, ttl } = _config.lock;
|
|
2555
|
+
if (adapter === "db") {
|
|
2556
|
+
if (_db instanceof SqliteAdapter) {
|
|
2557
|
+
_lock = new exports.DbLockAdapter(_db.db, ttl);
|
|
2558
|
+
} else {
|
|
2559
|
+
_lock = new exports.DbLockAdapter(url ?? "oqron.sqlite", ttl);
|
|
2560
|
+
}
|
|
2561
|
+
} else if (adapter === "memory") {
|
|
2562
|
+
_lock = new exports.MemoryLockAdapter();
|
|
2563
|
+
} else {
|
|
2564
|
+
throw new Error(
|
|
2565
|
+
`[OqronKit] Lock adapter '${adapter}' not yet bundled. Please pass a custom ILockAdapter instance.`
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
} else {
|
|
2569
|
+
_lock = _config.lock;
|
|
2570
|
+
}
|
|
2571
|
+
if (_config.shutdown.enabled) {
|
|
2572
|
+
for (const signal of _config.shutdown.signals) {
|
|
2573
|
+
process.on(signal, () => {
|
|
2574
|
+
_logger?.info(`${signal} received \u2014 initiating graceful shutdown\u2026`);
|
|
2575
|
+
void this.stop().then(() => process.exit(0));
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
if (_config.modules.includes("cron")) {
|
|
2580
|
+
const { SchedulerModule: SchedulerModule2, _drainPending: _drainPending2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
|
|
2581
|
+
if (_config.jobsDir) {
|
|
2582
|
+
const jobsPath = path__default.default.resolve(cwd, _config.jobsDir);
|
|
2583
|
+
_logger.debug(`Scanning jobsDir: ${jobsPath}`);
|
|
2584
|
+
try {
|
|
2585
|
+
async function scan(dir) {
|
|
2586
|
+
try {
|
|
2587
|
+
const entries = await fs__default.default.readdir(dir, { withFileTypes: true });
|
|
2588
|
+
for (const entry of entries) {
|
|
2589
|
+
const fullPath = path__default.default.join(dir, entry.name);
|
|
2590
|
+
if (entry.isDirectory()) {
|
|
2591
|
+
await scan(fullPath);
|
|
2592
|
+
} else if (entry.isFile() && /\.(js|ts|mjs|cjs)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
2593
|
+
_logger?.debug(`Auto-importing job file: ${entry.name}`);
|
|
2594
|
+
await import(url.pathToFileURL(fullPath).toString());
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
} catch (err) {
|
|
2598
|
+
if (err.code !== "ENOENT") throw err;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
await scan(jobsPath);
|
|
2602
|
+
} catch (err) {
|
|
2603
|
+
_logger.warn(`Failed to scan jobsDir: ${_config.jobsDir}`, {
|
|
2604
|
+
error: String(err)
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
const schedules = _drainPending2();
|
|
2609
|
+
for (const s of schedules) {
|
|
2610
|
+
s.tags = [.../* @__PURE__ */ new Set([...s.tags ?? [], ..._config.tags])];
|
|
2611
|
+
}
|
|
2612
|
+
const nsDb = new NamespacedOqronAdapter(
|
|
2613
|
+
_db,
|
|
2614
|
+
_config.project,
|
|
2615
|
+
_config.environment
|
|
2616
|
+
);
|
|
2617
|
+
const nsLock = new NamespacedLockAdapter(
|
|
2618
|
+
_lock,
|
|
2619
|
+
_config.project,
|
|
2620
|
+
_config.environment
|
|
2621
|
+
);
|
|
2622
|
+
const scheduler = new SchedulerModule2(
|
|
2623
|
+
schedules,
|
|
2624
|
+
nsDb,
|
|
2625
|
+
nsLock,
|
|
2626
|
+
_logger,
|
|
2627
|
+
_config.environment,
|
|
2628
|
+
_config.project,
|
|
2629
|
+
_config.cron
|
|
2630
|
+
);
|
|
2631
|
+
OqronRegistry.getInstance().register(scheduler);
|
|
2632
|
+
}
|
|
2633
|
+
if (_config.modules.includes("scheduler")) {
|
|
2634
|
+
const { ScheduleEngine: ScheduleEngine2, _drainPendingSchedules: _drainPendingSchedules2 } = await Promise.resolve().then(() => (init_scheduler(), scheduler_exports));
|
|
2635
|
+
if (!_config.modules.includes("cron") && _config.jobsDir) {
|
|
2636
|
+
_logger.warn(
|
|
2637
|
+
"jobsDir scanning currently bound to cron module block. Consider enabling 'cron' module or creating global scanner."
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const schedules = _drainPendingSchedules2();
|
|
2641
|
+
for (const s of schedules) {
|
|
2642
|
+
s.tags = [.../* @__PURE__ */ new Set([...s.tags ?? [], ..._config.tags])];
|
|
2643
|
+
}
|
|
2644
|
+
const nsDb = new NamespacedOqronAdapter(
|
|
2645
|
+
_db,
|
|
2646
|
+
_config.project,
|
|
2647
|
+
_config.environment
|
|
2648
|
+
);
|
|
2649
|
+
const nsLock = new NamespacedLockAdapter(
|
|
2650
|
+
_lock,
|
|
2651
|
+
_config.project,
|
|
2652
|
+
_config.environment
|
|
2653
|
+
);
|
|
2654
|
+
const engine = new ScheduleEngine2(
|
|
2655
|
+
schedules,
|
|
2656
|
+
nsDb,
|
|
2657
|
+
nsLock,
|
|
2658
|
+
_logger,
|
|
2659
|
+
_config.environment,
|
|
2660
|
+
_config.project,
|
|
2661
|
+
_config.scheduler
|
|
2662
|
+
);
|
|
2663
|
+
OqronRegistry.getInstance().register(engine);
|
|
2664
|
+
}
|
|
2665
|
+
const registry = OqronRegistry.getInstance();
|
|
2666
|
+
const modules = registry.getAll();
|
|
2667
|
+
for (const mod of modules) {
|
|
2668
|
+
if (mod.enabled) {
|
|
2669
|
+
_logger.debug(`init() \u2192 ${mod.name}`);
|
|
2670
|
+
await mod.init();
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
for (const mod of modules) {
|
|
2674
|
+
if (mod.enabled) {
|
|
2675
|
+
_logger.info(`start() \u2192 ${mod.name}`);
|
|
2676
|
+
await mod.start();
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
_logger.info("OqronKit ready \u2713");
|
|
2680
|
+
},
|
|
2681
|
+
/** Gracefully stop all modules */
|
|
2682
|
+
async stop() {
|
|
2683
|
+
const log = _logger ?? createLogger({ enabled: true, level: "info" }, { module: "oqronkit" });
|
|
2684
|
+
log.info("Stopping OqronKit\u2026");
|
|
2685
|
+
const timeoutMs = _config?.shutdown.timeout ?? 3e4;
|
|
2686
|
+
const registry = OqronRegistry.getInstance();
|
|
2687
|
+
const stopPromise = Promise.all(
|
|
2688
|
+
registry.getAll().filter((m) => m.enabled).map((m) => m.stop())
|
|
2689
|
+
);
|
|
2690
|
+
const timeoutPromise = new Promise(
|
|
2691
|
+
(_, reject) => setTimeout(
|
|
2692
|
+
() => reject(new Error(`Graceful shutdown timed out after ${timeoutMs}ms`)),
|
|
2693
|
+
timeoutMs
|
|
2694
|
+
)
|
|
2695
|
+
);
|
|
2696
|
+
try {
|
|
2697
|
+
await Promise.race([stopPromise, timeoutPromise]);
|
|
2698
|
+
log.info("OqronKit stopped.");
|
|
2699
|
+
} catch (err) {
|
|
2700
|
+
log.error("Error during stop or shutdown timeout", {
|
|
2701
|
+
error: String(err)
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
},
|
|
2705
|
+
/** Get the current validated config */
|
|
2706
|
+
getConfig() {
|
|
2707
|
+
if (!_config)
|
|
2708
|
+
throw new Error(
|
|
2709
|
+
"[OqronKit] Not initialized yet. Call OqronKit.init() first."
|
|
2710
|
+
);
|
|
2711
|
+
return _config;
|
|
2712
|
+
},
|
|
2713
|
+
/** Get the database adapter (available after init()) */
|
|
2714
|
+
getDb() {
|
|
2715
|
+
if (!_db) throw new Error("[OqronKit] Not initialized yet.");
|
|
2716
|
+
return _db;
|
|
2717
|
+
},
|
|
2718
|
+
/** Get the lock adapter (available after init()) */
|
|
2719
|
+
getLock() {
|
|
2720
|
+
if (!_lock) throw new Error("[OqronKit] Not initialized yet.");
|
|
2721
|
+
return _lock;
|
|
2722
|
+
},
|
|
2723
|
+
/** Get the Express router for monitoring */
|
|
2724
|
+
expressRouter() {
|
|
2725
|
+
return expressRouter();
|
|
2726
|
+
},
|
|
2727
|
+
/** Get the Fastify plugin for monitoring */
|
|
2728
|
+
fastifyPlugin(fastify, opts, done) {
|
|
2729
|
+
return fastifyPlugin(fastify, opts, done);
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
var index_default = OqronKit;
|
|
2733
|
+
|
|
2734
|
+
exports.OqronKit = OqronKit;
|
|
2735
|
+
exports.SqliteAdapter = SqliteAdapter;
|
|
2736
|
+
exports.createLogger = createLogger;
|
|
2737
|
+
exports.default = index_default;
|
|
2738
|
+
exports.defineConfig = defineConfig;
|