@objectstack/service-job 4.0.4 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +381 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -34
- package/dist/index.d.ts +97 -34
- package/dist/index.js +380 -28
- package/dist/index.js.map +1 -1
- package/package.json +33 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -177
- package/src/cron-job-adapter.ts +0 -51
- package/src/index.ts +0 -8
- package/src/interval-job-adapter.test.ts +0 -120
- package/src/interval-job-adapter.ts +0 -130
- package/src/job-service-plugin.ts +0 -65
- package/tsconfig.json +0 -10
package/dist/index.cjs
CHANGED
|
@@ -21,11 +21,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CronJobAdapter: () => CronJobAdapter,
|
|
24
|
+
DbJobAdapter: () => DbJobAdapter,
|
|
24
25
|
IntervalJobAdapter: () => IntervalJobAdapter,
|
|
25
26
|
JobServicePlugin: () => JobServicePlugin
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(index_exports);
|
|
28
29
|
|
|
30
|
+
// src/job-service-plugin.ts
|
|
31
|
+
var import_audit = require("@objectstack/platform-objects/audit");
|
|
32
|
+
|
|
29
33
|
// src/interval-job-adapter.ts
|
|
30
34
|
var IntervalJobAdapter = class {
|
|
31
35
|
constructor(options = {}) {
|
|
@@ -109,54 +113,403 @@ var IntervalJobAdapter = class {
|
|
|
109
113
|
}
|
|
110
114
|
};
|
|
111
115
|
|
|
112
|
-
// src/job-
|
|
113
|
-
var
|
|
116
|
+
// src/cron-job-adapter.ts
|
|
117
|
+
var import_croner = require("croner");
|
|
118
|
+
var CronJobAdapter = class {
|
|
114
119
|
constructor(options = {}) {
|
|
115
|
-
this.
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
118
|
-
this.options = { adapter: "interval", ...options };
|
|
120
|
+
this.jobs = /* @__PURE__ */ new Map();
|
|
121
|
+
this.defaultTimezone = options.timezone ?? "UTC";
|
|
122
|
+
this.maxExecutions = options.maxExecutions ?? 100;
|
|
119
123
|
}
|
|
120
|
-
async
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
async schedule(name, schedule, handler) {
|
|
125
|
+
await this.cancel(name);
|
|
126
|
+
const record = { name, schedule, handler, executions: [] };
|
|
127
|
+
if (schedule.type === "cron") {
|
|
128
|
+
if (!schedule.expression) {
|
|
129
|
+
throw new Error(`CronJobAdapter: cron schedule for "${name}" missing expression`);
|
|
130
|
+
}
|
|
131
|
+
const task = new import_croner.Cron(
|
|
132
|
+
schedule.expression,
|
|
133
|
+
{ timezone: schedule.timezone ?? this.defaultTimezone, name },
|
|
134
|
+
async () => {
|
|
135
|
+
await this.execute(record);
|
|
136
|
+
}
|
|
125
137
|
);
|
|
138
|
+
record.task = task;
|
|
139
|
+
} else if (schedule.type === "interval" && schedule.intervalMs) {
|
|
140
|
+
const handle = setInterval(() => {
|
|
141
|
+
void this.execute(record);
|
|
142
|
+
}, schedule.intervalMs);
|
|
143
|
+
handle?.unref?.();
|
|
144
|
+
record.task = { stop: () => clearInterval(handle) };
|
|
145
|
+
} else if (schedule.type === "once" && schedule.at) {
|
|
146
|
+
const delay = new Date(schedule.at).getTime() - Date.now();
|
|
147
|
+
if (delay > 0) {
|
|
148
|
+
const handle = setTimeout(() => {
|
|
149
|
+
void this.execute(record);
|
|
150
|
+
}, delay);
|
|
151
|
+
handle?.unref?.();
|
|
152
|
+
record.task = { stop: () => clearTimeout(handle) };
|
|
153
|
+
}
|
|
126
154
|
}
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
155
|
+
this.jobs.set(name, record);
|
|
156
|
+
}
|
|
157
|
+
async cancel(name) {
|
|
158
|
+
const rec = this.jobs.get(name);
|
|
159
|
+
if (rec?.task) {
|
|
160
|
+
try {
|
|
161
|
+
rec.task.stop();
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.jobs.delete(name);
|
|
166
|
+
}
|
|
167
|
+
async trigger(name, data) {
|
|
168
|
+
const rec = this.jobs.get(name);
|
|
169
|
+
if (!rec) throw new Error(`Job "${name}" not found`);
|
|
170
|
+
await this.execute(rec, data);
|
|
171
|
+
}
|
|
172
|
+
async getExecutions(name, limit) {
|
|
173
|
+
const rec = this.jobs.get(name);
|
|
174
|
+
if (!rec) return [];
|
|
175
|
+
return limit ? rec.executions.slice(-limit) : rec.executions;
|
|
130
176
|
}
|
|
177
|
+
async listJobs() {
|
|
178
|
+
return [...this.jobs.keys()];
|
|
179
|
+
}
|
|
180
|
+
/** Stop all timers — call from plugin destroy. */
|
|
131
181
|
async destroy() {
|
|
132
|
-
|
|
182
|
+
for (const rec of this.jobs.values()) {
|
|
183
|
+
try {
|
|
184
|
+
rec.task?.stop();
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.jobs.clear();
|
|
189
|
+
}
|
|
190
|
+
async execute(record, data) {
|
|
191
|
+
const execution = {
|
|
192
|
+
jobId: record.name,
|
|
193
|
+
status: "running",
|
|
194
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
};
|
|
196
|
+
const startMs = Date.now();
|
|
197
|
+
try {
|
|
198
|
+
await record.handler({ jobId: record.name, data });
|
|
199
|
+
execution.status = "success";
|
|
200
|
+
} catch (err) {
|
|
201
|
+
execution.status = "failed";
|
|
202
|
+
execution.error = err instanceof Error ? err.message : String(err);
|
|
203
|
+
} finally {
|
|
204
|
+
execution.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
205
|
+
execution.durationMs = Date.now() - startMs;
|
|
206
|
+
record.executions.push(execution);
|
|
207
|
+
if (record.executions.length > this.maxExecutions) {
|
|
208
|
+
record.executions.splice(0, record.executions.length - this.maxExecutions);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
133
211
|
}
|
|
134
212
|
};
|
|
135
213
|
|
|
136
|
-
// src/
|
|
137
|
-
var
|
|
138
|
-
|
|
139
|
-
|
|
214
|
+
// src/db-job-adapter.ts
|
|
215
|
+
var JOB_TABLE = "sys_job";
|
|
216
|
+
var RUN_TABLE = "sys_job_run";
|
|
217
|
+
var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
|
|
218
|
+
function uid(prefix) {
|
|
219
|
+
const g = globalThis;
|
|
220
|
+
if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
|
|
221
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
222
|
+
}
|
|
223
|
+
var DbJobAdapter = class {
|
|
224
|
+
constructor(args) {
|
|
225
|
+
this.engine = args.engine;
|
|
226
|
+
this.logger = args.logger;
|
|
227
|
+
this.recordRuns = args.options?.recordRuns ?? true;
|
|
228
|
+
this.inner = new IntervalJobAdapter({ maxExecutions: args.options?.maxExecutions });
|
|
229
|
+
this.cron = args.cron;
|
|
140
230
|
}
|
|
141
|
-
|
|
142
|
-
|
|
231
|
+
// ── IJobService ──────────────────────────────────────────────────
|
|
232
|
+
async schedule(name, schedule, handler) {
|
|
233
|
+
const wrapped = this.wrap(name, handler, "schedule");
|
|
234
|
+
if (schedule.type === "cron") {
|
|
235
|
+
if (this.cron) await this.cron.schedule(name, schedule, wrapped);
|
|
236
|
+
else this.logger?.warn?.(
|
|
237
|
+
`DbJobAdapter: cron schedule registered for "${name}" without CronJobAdapter \u2014 job will only run via manual trigger`
|
|
238
|
+
);
|
|
239
|
+
await this.inner.schedule(name, schedule, wrapped);
|
|
240
|
+
} else {
|
|
241
|
+
await this.inner.schedule(name, schedule, wrapped);
|
|
242
|
+
}
|
|
243
|
+
await this.upsertJobRow(name, schedule, true);
|
|
143
244
|
}
|
|
144
|
-
async cancel(
|
|
145
|
-
|
|
245
|
+
async cancel(name) {
|
|
246
|
+
await this.inner.cancel(name);
|
|
247
|
+
if (this.cron && typeof this.cron.cancel === "function") {
|
|
248
|
+
try {
|
|
249
|
+
await this.cron.cancel(name);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
await this.setActive(name, false);
|
|
146
254
|
}
|
|
147
|
-
async trigger(
|
|
148
|
-
|
|
255
|
+
async trigger(name, data) {
|
|
256
|
+
await this.inner.trigger(name, data);
|
|
149
257
|
}
|
|
150
|
-
async getExecutions(
|
|
151
|
-
|
|
258
|
+
async getExecutions(name, limit) {
|
|
259
|
+
return this.inner.getExecutions(name, limit);
|
|
152
260
|
}
|
|
153
261
|
async listJobs() {
|
|
154
|
-
|
|
262
|
+
return this.inner.listJobs();
|
|
263
|
+
}
|
|
264
|
+
async replay(name, data) {
|
|
265
|
+
const handlers = this.inner.jobs?.get?.(name);
|
|
266
|
+
if (!handlers) throw new Error(`Job "${name}" not found`);
|
|
267
|
+
const runId = await this.startRun(name, "replay");
|
|
268
|
+
try {
|
|
269
|
+
await this.inner.trigger(name, data);
|
|
270
|
+
await this.finishRun(runId, "success");
|
|
271
|
+
} catch (err) {
|
|
272
|
+
await this.finishRun(runId, "failed", err instanceof Error ? err.message : String(err));
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async listExecutionsByStatus(status, limit) {
|
|
277
|
+
const rows = await this.engine.find(RUN_TABLE, {
|
|
278
|
+
where: { status },
|
|
279
|
+
limit: limit ?? 50,
|
|
280
|
+
orderBy: [{ field: "started_at", direction: "desc" }],
|
|
281
|
+
context: SYSTEM_CTX
|
|
282
|
+
});
|
|
283
|
+
return (rows ?? []).map((r) => ({
|
|
284
|
+
jobId: String(r.job_name),
|
|
285
|
+
status: r.status,
|
|
286
|
+
startedAt: r.started_at,
|
|
287
|
+
completedAt: r.completed_at ?? void 0,
|
|
288
|
+
durationMs: r.duration_ms ?? void 0,
|
|
289
|
+
error: r.error ?? void 0
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
async destroy() {
|
|
293
|
+
await this.inner.destroy();
|
|
294
|
+
}
|
|
295
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
296
|
+
wrap(name, handler, defaultTrigger) {
|
|
297
|
+
return async (ctx) => {
|
|
298
|
+
const runId = this.recordRuns ? await this.startRun(name, defaultTrigger) : void 0;
|
|
299
|
+
const startMs = Date.now();
|
|
300
|
+
try {
|
|
301
|
+
await handler(ctx);
|
|
302
|
+
if (runId) await this.finishRun(runId, "success", void 0, Date.now() - startMs);
|
|
303
|
+
await this.bumpJob(name, "success");
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
306
|
+
if (runId) await this.finishRun(runId, "failed", msg, Date.now() - startMs);
|
|
307
|
+
await this.bumpJob(name, "failed", msg);
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async startRun(jobName, trigger) {
|
|
313
|
+
const id = uid("run");
|
|
314
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
315
|
+
try {
|
|
316
|
+
await this.engine.insert(RUN_TABLE, {
|
|
317
|
+
id,
|
|
318
|
+
job_name: jobName,
|
|
319
|
+
status: "running",
|
|
320
|
+
started_at: now,
|
|
321
|
+
trigger,
|
|
322
|
+
attempt: 1,
|
|
323
|
+
created_at: now
|
|
324
|
+
}, { context: SYSTEM_CTX });
|
|
325
|
+
return id;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
this.logger?.warn?.("DbJobAdapter: failed to insert sys_job_run", err);
|
|
328
|
+
return void 0;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async finishRun(id, status, error, durationMs) {
|
|
332
|
+
if (!id) return;
|
|
333
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
334
|
+
try {
|
|
335
|
+
await this.engine.update(RUN_TABLE, {
|
|
336
|
+
id,
|
|
337
|
+
status,
|
|
338
|
+
completed_at: now,
|
|
339
|
+
duration_ms: durationMs,
|
|
340
|
+
error: error ?? null
|
|
341
|
+
}, { context: SYSTEM_CTX });
|
|
342
|
+
} catch (err) {
|
|
343
|
+
this.logger?.warn?.("DbJobAdapter: failed to update sys_job_run", err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async upsertJobRow(name, schedule, active) {
|
|
347
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
348
|
+
const expression = schedule.expression ?? (schedule.intervalMs != null ? String(schedule.intervalMs) : schedule.at);
|
|
349
|
+
try {
|
|
350
|
+
const existing = await this.engine.find(JOB_TABLE, {
|
|
351
|
+
where: { name },
|
|
352
|
+
limit: 1,
|
|
353
|
+
context: SYSTEM_CTX
|
|
354
|
+
});
|
|
355
|
+
const row = existing?.[0];
|
|
356
|
+
if (row) {
|
|
357
|
+
await this.engine.update(JOB_TABLE, {
|
|
358
|
+
id: row.id,
|
|
359
|
+
schedule_type: schedule.type,
|
|
360
|
+
schedule_expression: expression ?? null,
|
|
361
|
+
timezone: schedule.timezone ?? null,
|
|
362
|
+
active,
|
|
363
|
+
updated_at: now
|
|
364
|
+
}, { context: SYSTEM_CTX });
|
|
365
|
+
} else {
|
|
366
|
+
await this.engine.insert(JOB_TABLE, {
|
|
367
|
+
id: uid("job"),
|
|
368
|
+
name,
|
|
369
|
+
schedule_type: schedule.type,
|
|
370
|
+
schedule_expression: expression ?? null,
|
|
371
|
+
timezone: schedule.timezone ?? null,
|
|
372
|
+
active,
|
|
373
|
+
run_count: 0,
|
|
374
|
+
failure_count: 0,
|
|
375
|
+
created_at: now,
|
|
376
|
+
updated_at: now
|
|
377
|
+
}, { context: SYSTEM_CTX });
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
this.logger?.warn?.("DbJobAdapter: failed to upsert sys_job", err);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async setActive(name, active) {
|
|
384
|
+
try {
|
|
385
|
+
const existing = await this.engine.find(JOB_TABLE, {
|
|
386
|
+
where: { name },
|
|
387
|
+
limit: 1,
|
|
388
|
+
context: SYSTEM_CTX
|
|
389
|
+
});
|
|
390
|
+
const row = existing?.[0];
|
|
391
|
+
if (!row) return;
|
|
392
|
+
await this.engine.update(JOB_TABLE, {
|
|
393
|
+
id: row.id,
|
|
394
|
+
active,
|
|
395
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
396
|
+
}, { context: SYSTEM_CTX });
|
|
397
|
+
} catch (err) {
|
|
398
|
+
this.logger?.warn?.("DbJobAdapter: setActive failed", err);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async bumpJob(name, last_status, last_error) {
|
|
402
|
+
try {
|
|
403
|
+
const existing = await this.engine.find(JOB_TABLE, {
|
|
404
|
+
where: { name },
|
|
405
|
+
limit: 1,
|
|
406
|
+
context: SYSTEM_CTX
|
|
407
|
+
});
|
|
408
|
+
const row = existing?.[0];
|
|
409
|
+
if (!row) return;
|
|
410
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
411
|
+
await this.engine.update(JOB_TABLE, {
|
|
412
|
+
id: row.id,
|
|
413
|
+
last_run_at: now,
|
|
414
|
+
last_status,
|
|
415
|
+
last_error: last_status === "failed" ? last_error ?? null : null,
|
|
416
|
+
run_count: (row.run_count ?? 0) + 1,
|
|
417
|
+
failure_count: (row.failure_count ?? 0) + (last_status === "failed" ? 1 : 0),
|
|
418
|
+
updated_at: now
|
|
419
|
+
}, { context: SYSTEM_CTX });
|
|
420
|
+
} catch (err) {
|
|
421
|
+
this.logger?.warn?.("DbJobAdapter: bumpJob failed", err);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/job-service-plugin.ts
|
|
427
|
+
var JobServicePlugin = class {
|
|
428
|
+
constructor(options = {}) {
|
|
429
|
+
this.name = "com.objectstack.service.job";
|
|
430
|
+
this.version = "1.1.0";
|
|
431
|
+
this.type = "standard";
|
|
432
|
+
this.options = { adapter: "auto", enableCron: true, ...options };
|
|
433
|
+
}
|
|
434
|
+
async init(ctx) {
|
|
435
|
+
try {
|
|
436
|
+
ctx.getService("manifest").register({
|
|
437
|
+
id: "com.objectstack.service.job",
|
|
438
|
+
name: "Background Job Service",
|
|
439
|
+
version: "1.1.0",
|
|
440
|
+
type: "plugin",
|
|
441
|
+
scope: "system",
|
|
442
|
+
defaultDatasource: "cloud",
|
|
443
|
+
namespace: "sys",
|
|
444
|
+
objects: [import_audit.SysJob, import_audit.SysJobRun]
|
|
445
|
+
});
|
|
446
|
+
} catch (err) {
|
|
447
|
+
ctx.logger.warn("JobServicePlugin: manifest service unavailable; sys_job/sys_job_run not registered", err);
|
|
448
|
+
}
|
|
449
|
+
const choice = this.options.adapter ?? "auto";
|
|
450
|
+
if (choice === "interval") {
|
|
451
|
+
this.intervalAdapter = new IntervalJobAdapter(this.options.interval);
|
|
452
|
+
ctx.registerService("job", this.intervalAdapter);
|
|
453
|
+
ctx.logger.info("JobServicePlugin: registered IntervalJobAdapter (in-memory)");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (choice === "cron") {
|
|
457
|
+
const cron = new CronJobAdapter({ timezone: "UTC" });
|
|
458
|
+
ctx.registerService("job", cron);
|
|
459
|
+
ctx.logger.info("JobServicePlugin: registered CronJobAdapter");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.intervalAdapter = new IntervalJobAdapter(this.options.interval);
|
|
463
|
+
ctx.registerService("job", this.intervalAdapter);
|
|
464
|
+
ctx.hook("kernel:ready", async () => {
|
|
465
|
+
let engine = null;
|
|
466
|
+
try {
|
|
467
|
+
engine = ctx.getService("objectql");
|
|
468
|
+
} catch {
|
|
469
|
+
try {
|
|
470
|
+
engine = ctx.getService("data");
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!engine) {
|
|
475
|
+
if (choice === "db") {
|
|
476
|
+
ctx.logger.warn("JobServicePlugin: db adapter requested but no ObjectQL engine \u2014 staying on IntervalJobAdapter");
|
|
477
|
+
} else {
|
|
478
|
+
ctx.logger.info("JobServicePlugin: no ObjectQL engine \u2014 staying on IntervalJobAdapter");
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
let cron;
|
|
483
|
+
if (this.options.enableCron !== false) {
|
|
484
|
+
try {
|
|
485
|
+
cron = new CronJobAdapter({ timezone: "UTC" });
|
|
486
|
+
} catch (err) {
|
|
487
|
+
ctx.logger.warn("JobServicePlugin: cron adapter init failed; cron jobs will not auto-run", err);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
this.dbAdapter = new DbJobAdapter({
|
|
491
|
+
engine,
|
|
492
|
+
logger: ctx.logger,
|
|
493
|
+
options: this.options.db,
|
|
494
|
+
cron
|
|
495
|
+
});
|
|
496
|
+
try {
|
|
497
|
+
ctx.replaceService?.("job", this.dbAdapter);
|
|
498
|
+
ctx.logger.info("JobServicePlugin: upgraded to DbJobAdapter (sys_job + sys_job_run persistence)");
|
|
499
|
+
} catch (err) {
|
|
500
|
+
ctx.logger.warn("JobServicePlugin: replaceService failed; staying on IntervalJobAdapter", err);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
async destroy() {
|
|
505
|
+
await this.dbAdapter?.destroy();
|
|
506
|
+
await this.intervalAdapter?.destroy();
|
|
155
507
|
}
|
|
156
508
|
};
|
|
157
509
|
// Annotate the CommonJS export names for ESM import in node:
|
|
158
510
|
0 && (module.exports = {
|
|
159
511
|
CronJobAdapter,
|
|
512
|
+
DbJobAdapter,
|
|
160
513
|
IntervalJobAdapter,
|
|
161
514
|
JobServicePlugin
|
|
162
515
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/interval-job-adapter.ts","../src/job-service-plugin.ts","../src/cron-job-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { JobServicePlugin } from './job-service-plugin.js';\nexport type { JobServicePluginOptions } from './job-service-plugin.js';\nexport { IntervalJobAdapter } from './interval-job-adapter.js';\nexport type { IntervalJobAdapterOptions } from './interval-job-adapter.js';\nexport { CronJobAdapter } from './cron-job-adapter.js';\nexport type { CronJobAdapterOptions } from './cron-job-adapter.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IJobService, JobSchedule, JobHandler, JobExecution } from '@objectstack/spec/contracts';\n\n/**\n * Internal record for a scheduled job.\n */\ninterface JobRecord {\n name: string;\n schedule: JobSchedule;\n handler: JobHandler;\n timerId?: ReturnType<typeof setInterval> | ReturnType<typeof setTimeout>;\n executions: JobExecution[];\n}\n\n/**\n * Configuration options for IntervalJobAdapter.\n */\nexport interface IntervalJobAdapterOptions {\n /** Maximum number of execution records to retain per job (default: 100) */\n maxExecutions?: number;\n}\n\n/**\n * setInterval-based job adapter implementing IJobService.\n *\n * Supports `interval` and `once` schedule types using Node.js timers.\n * `cron` schedules are stored but not actively executed (requires a cron\n * library — see CronJobAdapter skeleton).\n *\n * Suitable for single-process environments, development, and testing.\n */\nexport class IntervalJobAdapter implements IJobService {\n private readonly jobs = new Map<string, JobRecord>();\n private readonly maxExecutions: number;\n\n constructor(options: IntervalJobAdapterOptions = {}) {\n this.maxExecutions = options.maxExecutions ?? 100;\n }\n\n async schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void> {\n // Cancel any existing job with the same name\n await this.cancel(name);\n\n const record: JobRecord = { name, schedule, handler, executions: [] };\n\n if (schedule.type === 'interval' && schedule.intervalMs) {\n record.timerId = setInterval(async () => {\n await this.executeJob(record);\n }, schedule.intervalMs);\n } else if (schedule.type === 'once' && schedule.at) {\n const delay = new Date(schedule.at).getTime() - Date.now();\n if (delay > 0) {\n record.timerId = setTimeout(async () => {\n await this.executeJob(record);\n }, delay);\n }\n }\n // 'cron' type: stored but not actively scheduled (needs cron library)\n\n this.jobs.set(name, record);\n }\n\n async cancel(name: string): Promise<void> {\n const record = this.jobs.get(name);\n if (record?.timerId) {\n clearInterval(record.timerId as ReturnType<typeof setInterval>);\n clearTimeout(record.timerId as ReturnType<typeof setTimeout>);\n }\n this.jobs.delete(name);\n }\n\n async trigger(name: string, data?: unknown): Promise<void> {\n const record = this.jobs.get(name);\n if (!record) {\n throw new Error(`Job \"${name}\" not found`);\n }\n await this.executeJob(record, data);\n }\n\n async getExecutions(name: string, limit?: number): Promise<JobExecution[]> {\n const record = this.jobs.get(name);\n if (!record) return [];\n const execs = record.executions;\n return limit ? execs.slice(-limit) : execs;\n }\n\n async listJobs(): Promise<string[]> {\n return [...this.jobs.keys()];\n }\n\n /**\n * Stop all active timers. Call during plugin destroy phase.\n */\n async destroy(): Promise<void> {\n for (const record of this.jobs.values()) {\n if (record.timerId) {\n clearInterval(record.timerId as ReturnType<typeof setInterval>);\n clearTimeout(record.timerId as ReturnType<typeof setTimeout>);\n }\n }\n this.jobs.clear();\n }\n\n private async executeJob(record: JobRecord, data?: unknown): Promise<void> {\n const execution: JobExecution = {\n jobId: record.name,\n status: 'running',\n startedAt: new Date().toISOString(),\n };\n\n const startMs = Date.now();\n try {\n await record.handler({ jobId: record.name, data });\n execution.status = 'success';\n } catch (err) {\n execution.status = 'failed';\n execution.error = err instanceof Error ? err.message : String(err);\n } finally {\n execution.completedAt = new Date().toISOString();\n execution.durationMs = Date.now() - startMs;\n\n record.executions.push(execution);\n // Trim old executions\n if (record.executions.length > this.maxExecutions) {\n record.executions.splice(0, record.executions.length - this.maxExecutions);\n }\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { IntervalJobAdapter } from './interval-job-adapter.js';\nimport type { IntervalJobAdapterOptions } from './interval-job-adapter.js';\n\n/**\n * Configuration options for the JobServicePlugin.\n */\nexport interface JobServicePluginOptions {\n /** Job adapter type (default: 'interval') */\n adapter?: 'interval' | 'cron';\n /** Options for the interval job adapter */\n interval?: IntervalJobAdapterOptions;\n}\n\n/**\n * JobServicePlugin — Production IJobService implementation.\n *\n * Registers a job scheduler with the kernel during the init phase.\n * Supports setInterval-based and cron-based adapters.\n *\n * @example\n * ```ts\n * import { ObjectKernel } from '@objectstack/core';\n * import { JobServicePlugin } from '@objectstack/service-job';\n *\n * const kernel = new ObjectKernel();\n * kernel.use(new JobServicePlugin({ adapter: 'interval' }));\n * await kernel.bootstrap();\n *\n * const job = kernel.getService('job');\n * await job.schedule('cleanup', { type: 'interval', intervalMs: 60000 }, handler);\n * ```\n */\nexport class JobServicePlugin implements Plugin {\n name = 'com.objectstack.service.job';\n version = '1.0.0';\n type = 'standard';\n\n private readonly options: JobServicePluginOptions;\n private adapter?: IntervalJobAdapter;\n\n constructor(options: JobServicePluginOptions = {}) {\n this.options = { adapter: 'interval', ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n const adapterType = this.options.adapter;\n if (adapterType === 'cron') {\n throw new Error(\n 'Cron job adapter is not yet implemented. ' +\n 'Use adapter: \"interval\" or provide a custom IJobService via ctx.registerService(\"job\", impl).'\n );\n }\n\n this.adapter = new IntervalJobAdapter(this.options.interval);\n ctx.registerService('job', this.adapter);\n ctx.logger.info('JobServicePlugin: registered interval job adapter');\n }\n\n async destroy(): Promise<void> {\n await this.adapter?.destroy();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IJobService, JobSchedule, JobHandler, JobExecution } from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the cron-based job adapter.\n */\nexport interface CronJobAdapterOptions {\n /** Timezone for cron expressions (default: 'UTC') */\n timezone?: string;\n}\n\n/**\n * Cron-based job adapter skeleton implementing IJobService.\n *\n * This is a placeholder for future cron integration (e.g., `node-cron` or `croner`).\n * Concrete implementation will parse cron expressions and schedule jobs accordingly.\n *\n * @example\n * ```ts\n * const scheduler = new CronJobAdapter({ timezone: 'America/New_York' });\n * await scheduler.schedule('nightly-cleanup', { type: 'cron', expression: '0 0 * * *' }, handler);\n * ```\n */\nexport class CronJobAdapter implements IJobService {\n private readonly timezone: string;\n\n constructor(options: CronJobAdapterOptions = {}) {\n this.timezone = options.timezone ?? 'UTC';\n }\n\n async schedule(_name: string, _schedule: JobSchedule, _handler: JobHandler): Promise<void> {\n throw new Error(`CronJobAdapter not yet implemented (timezone: ${this.timezone})`);\n }\n\n async cancel(_name: string): Promise<void> {\n throw new Error('CronJobAdapter not yet implemented');\n }\n\n async trigger(_name: string, _data?: unknown): Promise<void> {\n throw new Error('CronJobAdapter not yet implemented');\n }\n\n async getExecutions(_name: string, _limit?: number): Promise<JobExecution[]> {\n throw new Error('CronJobAdapter not yet implemented');\n }\n\n async listJobs(): Promise<string[]> {\n throw new Error('CronJobAdapter not yet implemented');\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/job-service-plugin.ts","../src/interval-job-adapter.ts","../src/cron-job-adapter.ts","../src/db-job-adapter.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nexport { JobServicePlugin } from './job-service-plugin.js';\nexport type { JobServicePluginOptions } from './job-service-plugin.js';\nexport { IntervalJobAdapter } from './interval-job-adapter.js';\nexport type { IntervalJobAdapterOptions } from './interval-job-adapter.js';\nexport { CronJobAdapter } from './cron-job-adapter.js';\nexport type { CronJobAdapterOptions } from './cron-job-adapter.js';\nexport { DbJobAdapter } from './db-job-adapter.js';\nexport type { DbJobAdapterOptions, JobEngineLike, JobLoggerLike } from './db-job-adapter.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysJob, SysJobRun } from '@objectstack/platform-objects/audit';\nimport { IntervalJobAdapter } from './interval-job-adapter.js';\nimport type { IntervalJobAdapterOptions } from './interval-job-adapter.js';\nimport { CronJobAdapter } from './cron-job-adapter.js';\nimport { DbJobAdapter } from './db-job-adapter.js';\nimport type { DbJobAdapterOptions } from './db-job-adapter.js';\n\n/**\n * Configuration options for the JobServicePlugin.\n */\nexport interface JobServicePluginOptions {\n /**\n * Job adapter type.\n * - 'auto' (default): use DbJobAdapter when objectql engine available, else IntervalJobAdapter\n * - 'db': require objectql; persists schedules and runs to sys_job/sys_job_run\n * - 'interval': in-memory IntervalJobAdapter (legacy, non-durable)\n * - 'cron': in-memory CronJobAdapter using `croner`\n */\n adapter?: 'auto' | 'db' | 'interval' | 'cron';\n /** Options for the interval job adapter */\n interval?: IntervalJobAdapterOptions;\n /** Options for the DB adapter */\n db?: DbJobAdapterOptions;\n /** Whether to also wire CronJobAdapter for cron schedules (default: true when available) */\n enableCron?: boolean;\n}\n\n/**\n * JobServicePlugin — Production IJobService implementation.\n *\n * Default behaviour: registers a `DbJobAdapter` when the ObjectQL engine is\n * available (persisting registry + execution history to `sys_job` and\n * `sys_job_run`), falling back to in-memory `IntervalJobAdapter` otherwise.\n * Cron schedules are routed to `CronJobAdapter` (croner-backed).\n */\nexport class JobServicePlugin implements Plugin {\n name = 'com.objectstack.service.job';\n version = '1.1.0';\n type = 'standard';\n\n private readonly options: JobServicePluginOptions;\n private dbAdapter?: DbJobAdapter;\n private intervalAdapter?: IntervalJobAdapter;\n\n constructor(options: JobServicePluginOptions = {}) {\n this.options = { adapter: 'auto', enableCron: true, ...options };\n }\n\n async init(ctx: PluginContext): Promise<void> {\n // Register platform objects so Studio can see scheduled jobs and runs.\n try {\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.service.job',\n name: 'Background Job Service',\n version: '1.1.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysJob, SysJobRun],\n });\n } catch (err) {\n ctx.logger.warn('JobServicePlugin: manifest service unavailable; sys_job/sys_job_run not registered', err as any);\n }\n\n const choice = this.options.adapter ?? 'auto';\n\n if (choice === 'interval') {\n this.intervalAdapter = new IntervalJobAdapter(this.options.interval);\n ctx.registerService('job', this.intervalAdapter);\n ctx.logger.info('JobServicePlugin: registered IntervalJobAdapter (in-memory)');\n return;\n }\n\n if (choice === 'cron') {\n const cron = new CronJobAdapter({ timezone: 'UTC' });\n ctx.registerService('job', cron);\n ctx.logger.info('JobServicePlugin: registered CronJobAdapter');\n return;\n }\n\n // 'auto' or 'db' — register a placeholder Interval adapter synchronously\n // so callers can `getService('job')` during init, then upgrade in kernel:ready\n // when the objectql engine is wired.\n this.intervalAdapter = new IntervalJobAdapter(this.options.interval);\n ctx.registerService('job', this.intervalAdapter);\n\n ctx.hook('kernel:ready', async () => {\n let engine: any = null;\n try { engine = ctx.getService<any>('objectql'); }\n catch { try { engine = ctx.getService<any>('data'); } catch { /* ignore */ } }\n\n if (!engine) {\n if (choice === 'db') {\n ctx.logger.warn('JobServicePlugin: db adapter requested but no ObjectQL engine — staying on IntervalJobAdapter');\n } else {\n ctx.logger.info('JobServicePlugin: no ObjectQL engine — staying on IntervalJobAdapter');\n }\n return;\n }\n\n // Build cron adapter if enabled\n let cron: CronJobAdapter | undefined;\n if (this.options.enableCron !== false) {\n try {\n cron = new CronJobAdapter({ timezone: 'UTC' });\n } catch (err) {\n ctx.logger.warn('JobServicePlugin: cron adapter init failed; cron jobs will not auto-run', err as any);\n }\n }\n\n this.dbAdapter = new DbJobAdapter({\n engine,\n logger: ctx.logger,\n options: this.options.db,\n cron,\n });\n\n try {\n (ctx as any).replaceService?.('job', this.dbAdapter);\n ctx.logger.info('JobServicePlugin: upgraded to DbJobAdapter (sys_job + sys_job_run persistence)');\n } catch (err) {\n ctx.logger.warn('JobServicePlugin: replaceService failed; staying on IntervalJobAdapter', err as any);\n }\n });\n }\n\n async destroy(): Promise<void> {\n await this.dbAdapter?.destroy();\n await this.intervalAdapter?.destroy();\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IJobService, JobSchedule, JobHandler, JobExecution } from '@objectstack/spec/contracts';\n\n/**\n * Internal record for a scheduled job.\n */\ninterface JobRecord {\n name: string;\n schedule: JobSchedule;\n handler: JobHandler;\n timerId?: ReturnType<typeof setInterval> | ReturnType<typeof setTimeout>;\n executions: JobExecution[];\n}\n\n/**\n * Configuration options for IntervalJobAdapter.\n */\nexport interface IntervalJobAdapterOptions {\n /** Maximum number of execution records to retain per job (default: 100) */\n maxExecutions?: number;\n}\n\n/**\n * setInterval-based job adapter implementing IJobService.\n *\n * Supports `interval` and `once` schedule types using Node.js timers.\n * `cron` schedules are stored but not actively executed (requires a cron\n * library — see CronJobAdapter skeleton).\n *\n * Suitable for single-process environments, development, and testing.\n */\nexport class IntervalJobAdapter implements IJobService {\n private readonly jobs = new Map<string, JobRecord>();\n private readonly maxExecutions: number;\n\n constructor(options: IntervalJobAdapterOptions = {}) {\n this.maxExecutions = options.maxExecutions ?? 100;\n }\n\n async schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void> {\n // Cancel any existing job with the same name\n await this.cancel(name);\n\n const record: JobRecord = { name, schedule, handler, executions: [] };\n\n if (schedule.type === 'interval' && schedule.intervalMs) {\n record.timerId = setInterval(async () => {\n await this.executeJob(record);\n }, schedule.intervalMs);\n } else if (schedule.type === 'once' && schedule.at) {\n const delay = new Date(schedule.at).getTime() - Date.now();\n if (delay > 0) {\n record.timerId = setTimeout(async () => {\n await this.executeJob(record);\n }, delay);\n }\n }\n // 'cron' type: stored but not actively scheduled (needs cron library)\n\n this.jobs.set(name, record);\n }\n\n async cancel(name: string): Promise<void> {\n const record = this.jobs.get(name);\n if (record?.timerId) {\n clearInterval(record.timerId as ReturnType<typeof setInterval>);\n clearTimeout(record.timerId as ReturnType<typeof setTimeout>);\n }\n this.jobs.delete(name);\n }\n\n async trigger(name: string, data?: unknown): Promise<void> {\n const record = this.jobs.get(name);\n if (!record) {\n throw new Error(`Job \"${name}\" not found`);\n }\n await this.executeJob(record, data);\n }\n\n async getExecutions(name: string, limit?: number): Promise<JobExecution[]> {\n const record = this.jobs.get(name);\n if (!record) return [];\n const execs = record.executions;\n return limit ? execs.slice(-limit) : execs;\n }\n\n async listJobs(): Promise<string[]> {\n return [...this.jobs.keys()];\n }\n\n /**\n * Stop all active timers. Call during plugin destroy phase.\n */\n async destroy(): Promise<void> {\n for (const record of this.jobs.values()) {\n if (record.timerId) {\n clearInterval(record.timerId as ReturnType<typeof setInterval>);\n clearTimeout(record.timerId as ReturnType<typeof setTimeout>);\n }\n }\n this.jobs.clear();\n }\n\n private async executeJob(record: JobRecord, data?: unknown): Promise<void> {\n const execution: JobExecution = {\n jobId: record.name,\n status: 'running',\n startedAt: new Date().toISOString(),\n };\n\n const startMs = Date.now();\n try {\n await record.handler({ jobId: record.name, data });\n execution.status = 'success';\n } catch (err) {\n execution.status = 'failed';\n execution.error = err instanceof Error ? err.message : String(err);\n } finally {\n execution.completedAt = new Date().toISOString();\n execution.durationMs = Date.now() - startMs;\n\n record.executions.push(execution);\n // Trim old executions\n if (record.executions.length > this.maxExecutions) {\n record.executions.splice(0, record.executions.length - this.maxExecutions);\n }\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { Cron } from 'croner';\nimport type {\n IJobService,\n JobSchedule,\n JobHandler,\n JobExecution,\n} from '@objectstack/spec/contracts';\n\n/**\n * Configuration for the cron-based job adapter.\n */\nexport interface CronJobAdapterOptions {\n /** Timezone for cron expressions (default: 'UTC') */\n timezone?: string;\n /** Maximum execution history per job (default: 100) */\n maxExecutions?: number;\n}\n\ninterface CronJobRecord {\n name: string;\n schedule: JobSchedule;\n handler: JobHandler;\n task?: Cron;\n executions: JobExecution[];\n}\n\n/**\n * Cron-based job adapter implementing IJobService using the `croner`\n * library. Honours per-job timezones, supports the standard 5-field cron\n * syntax, and falls back to setInterval / setTimeout for `interval` and\n * `once` schedule types (so a single CronJobAdapter can serve as the\n * \"real\" production job runner).\n */\nexport class CronJobAdapter implements IJobService {\n private readonly defaultTimezone: string;\n private readonly maxExecutions: number;\n private readonly jobs = new Map<string, CronJobRecord>();\n\n constructor(options: CronJobAdapterOptions = {}) {\n this.defaultTimezone = options.timezone ?? 'UTC';\n this.maxExecutions = options.maxExecutions ?? 100;\n }\n\n async schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void> {\n await this.cancel(name);\n\n const record: CronJobRecord = { name, schedule, handler, executions: [] };\n\n if (schedule.type === 'cron') {\n if (!schedule.expression) {\n throw new Error(`CronJobAdapter: cron schedule for \"${name}\" missing expression`);\n }\n const task = new Cron(\n schedule.expression,\n { timezone: schedule.timezone ?? this.defaultTimezone, name },\n async () => { await this.execute(record); },\n );\n record.task = task;\n } else if (schedule.type === 'interval' && schedule.intervalMs) {\n const handle = setInterval(() => { void this.execute(record); }, schedule.intervalMs);\n (handle as any)?.unref?.();\n // Use a sentinel Cron-like shape with stop() for cancel()\n record.task = { stop: () => clearInterval(handle) } as unknown as Cron;\n } else if (schedule.type === 'once' && schedule.at) {\n const delay = new Date(schedule.at).getTime() - Date.now();\n if (delay > 0) {\n const handle = setTimeout(() => { void this.execute(record); }, delay);\n (handle as any)?.unref?.();\n record.task = { stop: () => clearTimeout(handle) } as unknown as Cron;\n }\n }\n\n this.jobs.set(name, record);\n }\n\n async cancel(name: string): Promise<void> {\n const rec = this.jobs.get(name);\n if (rec?.task) {\n try { rec.task.stop(); } catch { /* ignore */ }\n }\n this.jobs.delete(name);\n }\n\n async trigger(name: string, data?: unknown): Promise<void> {\n const rec = this.jobs.get(name);\n if (!rec) throw new Error(`Job \"${name}\" not found`);\n await this.execute(rec, data);\n }\n\n async getExecutions(name: string, limit?: number): Promise<JobExecution[]> {\n const rec = this.jobs.get(name);\n if (!rec) return [];\n return limit ? rec.executions.slice(-limit) : rec.executions;\n }\n\n async listJobs(): Promise<string[]> {\n return [...this.jobs.keys()];\n }\n\n /** Stop all timers — call from plugin destroy. */\n async destroy(): Promise<void> {\n for (const rec of this.jobs.values()) {\n try { rec.task?.stop(); } catch { /* ignore */ }\n }\n this.jobs.clear();\n }\n\n private async execute(record: CronJobRecord, data?: unknown): Promise<void> {\n const execution: JobExecution = {\n jobId: record.name,\n status: 'running',\n startedAt: new Date().toISOString(),\n };\n const startMs = Date.now();\n try {\n await record.handler({ jobId: record.name, data });\n execution.status = 'success';\n } catch (err) {\n execution.status = 'failed';\n execution.error = err instanceof Error ? err.message : String(err);\n } finally {\n execution.completedAt = new Date().toISOString();\n execution.durationMs = Date.now() - startMs;\n record.executions.push(execution);\n if (record.executions.length > this.maxExecutions) {\n record.executions.splice(0, record.executions.length - this.maxExecutions);\n }\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type {\n IJobService,\n JobSchedule,\n JobHandler,\n JobExecution,\n} from '@objectstack/spec/contracts';\nimport { IntervalJobAdapter } from './interval-job-adapter.js';\n\nconst JOB_TABLE = 'sys_job';\nconst RUN_TABLE = 'sys_job_run';\nconst SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] } as const;\n\nexport interface JobEngineLike {\n find(object: string, options?: any): Promise<any[]>;\n insert(object: string, data: any, options?: any): Promise<any>;\n update(object: string, idOrData: any, dataOrOptions?: any, options?: any): Promise<any>;\n delete?(object: string, options?: any): Promise<any>;\n}\n\nexport interface JobLoggerLike {\n info(msg: string, meta?: unknown): void;\n warn(msg: string, meta?: unknown): void;\n error?(msg: string, meta?: unknown): void;\n}\n\nexport interface DbJobAdapterOptions {\n /** Maximum executions kept in memory per job (default 100) */\n maxExecutions?: number;\n /** Soft cap on sys_job_run rows recorded per job (defaults to none — handled by retention jobs) */\n recordRuns?: boolean;\n}\n\nfunction uid(prefix: string): string {\n const g: any = globalThis as any;\n if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;\n return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * DbJobAdapter — IJobService that persists job registry and execution\n * history to ObjectQL while delegating timer mechanics to\n * `IntervalJobAdapter`. Cron is delegated to `CronJobAdapter` callers\n * supplied via {@link withCron}.\n *\n * Persisted side effects:\n * - `schedule(name, …)` upserts a `sys_job` row (active=true)\n * - `cancel(name)` marks the row inactive\n * - every execution writes a `sys_job_run` row\n * - every execution updates `sys_job.last_run_at / last_status / run_count / failure_count`\n *\n * The persistence is best-effort: a DB failure is logged but does not\n * break job execution. This keeps a healthy job system resilient to\n * transient storage hiccups.\n */\nexport class DbJobAdapter implements IJobService {\n private readonly inner: IntervalJobAdapter;\n private readonly cron?: IJobService;\n private readonly engine: JobEngineLike;\n private readonly logger?: JobLoggerLike;\n private readonly recordRuns: boolean;\n\n constructor(args: {\n engine: JobEngineLike;\n logger?: JobLoggerLike;\n options?: DbJobAdapterOptions;\n cron?: IJobService;\n }) {\n this.engine = args.engine;\n this.logger = args.logger;\n this.recordRuns = args.options?.recordRuns ?? true;\n this.inner = new IntervalJobAdapter({ maxExecutions: args.options?.maxExecutions });\n this.cron = args.cron;\n }\n\n // ── IJobService ──────────────────────────────────────────────────\n\n async schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void> {\n const wrapped = this.wrap(name, handler, 'schedule');\n\n if (schedule.type === 'cron') {\n if (this.cron) await this.cron.schedule(name, schedule, wrapped);\n else this.logger?.warn?.(\n `DbJobAdapter: cron schedule registered for \"${name}\" without CronJobAdapter — job will only run via manual trigger`,\n );\n // Still record in inner so trigger() works\n await this.inner.schedule(name, schedule, wrapped);\n } else {\n await this.inner.schedule(name, schedule, wrapped);\n }\n\n await this.upsertJobRow(name, schedule, true);\n }\n\n async cancel(name: string): Promise<void> {\n await this.inner.cancel(name);\n if (this.cron && typeof this.cron.cancel === 'function') {\n try { await this.cron.cancel(name); } catch { /* ignore */ }\n }\n await this.setActive(name, false);\n }\n\n async trigger(name: string, data?: unknown): Promise<void> {\n await this.inner.trigger(name, data);\n }\n\n async getExecutions(name: string, limit?: number): Promise<JobExecution[]> {\n return this.inner.getExecutions(name, limit);\n }\n\n async listJobs(): Promise<string[]> {\n return this.inner.listJobs();\n }\n\n async replay(name: string, data?: unknown): Promise<void> {\n // Same execution path as trigger but tag the run as 'replay'.\n const handlers = (this.inner as any).jobs?.get?.(name);\n if (!handlers) throw new Error(`Job \"${name}\" not found`);\n // Reuse trigger; the wrap function uses a closure flag — simpler:\n // expose by calling inner.trigger with a marker via data is intrusive,\n // so we record a synthetic run row before/after to ensure 'replay' tag.\n const runId = await this.startRun(name, 'replay');\n try {\n await this.inner.trigger(name, data);\n // The wrap already recorded a run; mark our synthetic run as success.\n await this.finishRun(runId, 'success');\n } catch (err) {\n await this.finishRun(runId, 'failed', err instanceof Error ? err.message : String(err));\n throw err;\n }\n }\n\n async listExecutionsByStatus(\n status: JobExecution['status'],\n limit?: number,\n ): Promise<JobExecution[]> {\n const rows = await this.engine.find(RUN_TABLE, {\n where: { status },\n limit: limit ?? 50,\n orderBy: [{ field: 'started_at', direction: 'desc' }],\n context: SYSTEM_CTX,\n });\n return (rows ?? []).map((r: any) => ({\n jobId: String(r.job_name),\n status: r.status,\n startedAt: r.started_at,\n completedAt: r.completed_at ?? undefined,\n durationMs: r.duration_ms ?? undefined,\n error: r.error ?? undefined,\n }));\n }\n\n async destroy(): Promise<void> {\n await this.inner.destroy();\n }\n\n // ── Internals ────────────────────────────────────────────────────\n\n private wrap(name: string, handler: JobHandler, defaultTrigger: 'schedule' | 'manual' | 'replay'): JobHandler {\n return async (ctx) => {\n const runId = this.recordRuns ? await this.startRun(name, defaultTrigger) : undefined;\n const startMs = Date.now();\n try {\n await handler(ctx);\n if (runId) await this.finishRun(runId, 'success', undefined, Date.now() - startMs);\n await this.bumpJob(name, 'success');\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (runId) await this.finishRun(runId, 'failed', msg, Date.now() - startMs);\n await this.bumpJob(name, 'failed', msg);\n throw err;\n }\n };\n }\n\n private async startRun(jobName: string, trigger: 'schedule' | 'manual' | 'replay'): Promise<string | undefined> {\n const id = uid('run');\n const now = new Date().toISOString();\n try {\n await this.engine.insert(RUN_TABLE, {\n id,\n job_name: jobName,\n status: 'running',\n started_at: now,\n trigger,\n attempt: 1,\n created_at: now,\n }, { context: SYSTEM_CTX });\n return id;\n } catch (err) {\n this.logger?.warn?.('DbJobAdapter: failed to insert sys_job_run', err as any);\n return undefined;\n }\n }\n\n private async finishRun(\n id: string | undefined,\n status: JobExecution['status'],\n error?: string,\n durationMs?: number,\n ): Promise<void> {\n if (!id) return;\n const now = new Date().toISOString();\n try {\n await this.engine.update(RUN_TABLE, {\n id,\n status,\n completed_at: now,\n duration_ms: durationMs,\n error: error ?? null,\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbJobAdapter: failed to update sys_job_run', err as any);\n }\n }\n\n private async upsertJobRow(name: string, schedule: JobSchedule, active: boolean): Promise<void> {\n const now = new Date().toISOString();\n const expression =\n schedule.expression ?? (schedule.intervalMs != null ? String(schedule.intervalMs) : schedule.at);\n try {\n const existing = await this.engine.find(JOB_TABLE, {\n where: { name },\n limit: 1,\n context: SYSTEM_CTX,\n });\n const row = existing?.[0];\n if (row) {\n await this.engine.update(JOB_TABLE, {\n id: row.id,\n schedule_type: schedule.type,\n schedule_expression: expression ?? null,\n timezone: schedule.timezone ?? null,\n active,\n updated_at: now,\n }, { context: SYSTEM_CTX });\n } else {\n await this.engine.insert(JOB_TABLE, {\n id: uid('job'),\n name,\n schedule_type: schedule.type,\n schedule_expression: expression ?? null,\n timezone: schedule.timezone ?? null,\n active,\n run_count: 0,\n failure_count: 0,\n created_at: now,\n updated_at: now,\n }, { context: SYSTEM_CTX });\n }\n } catch (err) {\n this.logger?.warn?.('DbJobAdapter: failed to upsert sys_job', err as any);\n }\n }\n\n private async setActive(name: string, active: boolean): Promise<void> {\n try {\n const existing = await this.engine.find(JOB_TABLE, {\n where: { name },\n limit: 1,\n context: SYSTEM_CTX,\n });\n const row = existing?.[0];\n if (!row) return;\n await this.engine.update(JOB_TABLE, {\n id: row.id,\n active,\n updated_at: new Date().toISOString(),\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbJobAdapter: setActive failed', err as any);\n }\n }\n\n private async bumpJob(name: string, last_status: 'success' | 'failed', last_error?: string): Promise<void> {\n try {\n const existing = await this.engine.find(JOB_TABLE, {\n where: { name },\n limit: 1,\n context: SYSTEM_CTX,\n });\n const row = existing?.[0];\n if (!row) return;\n const now = new Date().toISOString();\n await this.engine.update(JOB_TABLE, {\n id: row.id,\n last_run_at: now,\n last_status,\n last_error: last_status === 'failed' ? (last_error ?? null) : null,\n run_count: (row.run_count ?? 0) + 1,\n failure_count: (row.failure_count ?? 0) + (last_status === 'failed' ? 1 : 0),\n updated_at: now,\n }, { context: SYSTEM_CTX });\n } catch (err) {\n this.logger?.warn?.('DbJobAdapter: bumpJob failed', err as any);\n }\n }\n}\n"],"mappings":"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","names":[]}
|