@objectstack/service-job 9.4.0 → 9.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +96 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -1
- package/dist/index.d.ts +70 -1
- package/dist/index.js +93 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -21,8 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
CronJobAdapter: () => CronJobAdapter,
|
|
24
|
+
DEFAULT_JOB_RUN_RETENTION_DAYS: () => DEFAULT_JOB_RUN_RETENTION_DAYS,
|
|
25
|
+
DEFAULT_JOB_RUN_SWEEP_MS: () => DEFAULT_JOB_RUN_SWEEP_MS,
|
|
24
26
|
DbJobAdapter: () => DbJobAdapter,
|
|
25
27
|
IntervalJobAdapter: () => IntervalJobAdapter,
|
|
28
|
+
JobRunRetention: () => JobRunRetention,
|
|
26
29
|
JobServicePlugin: () => JobServicePlugin
|
|
27
30
|
});
|
|
28
31
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -423,13 +426,79 @@ var DbJobAdapter = class {
|
|
|
423
426
|
}
|
|
424
427
|
};
|
|
425
428
|
|
|
429
|
+
// src/job-run-retention.ts
|
|
430
|
+
var RUN_TABLE2 = "sys_job_run";
|
|
431
|
+
var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
|
|
432
|
+
var DEFAULT_JOB_RUN_RETENTION_DAYS = 30;
|
|
433
|
+
var DEFAULT_JOB_RUN_SWEEP_MS = 6 * 36e5;
|
|
434
|
+
var JobRunRetention = class {
|
|
435
|
+
constructor(opts) {
|
|
436
|
+
this.opts = opts;
|
|
437
|
+
this.now = opts.now ?? (() => Date.now());
|
|
438
|
+
this.object = opts.object ?? RUN_TABLE2;
|
|
439
|
+
this.tsField = opts.tsField ?? "created_at";
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Delete `sys_job_run` rows older than `retentionDays`. No-op when no data
|
|
443
|
+
* engine is available, the engine can't delete, or `retentionDays` is not a
|
|
444
|
+
* positive number.
|
|
445
|
+
*/
|
|
446
|
+
async prune(retentionDays) {
|
|
447
|
+
const engine = this.opts.getEngine();
|
|
448
|
+
if (!engine || typeof engine.delete !== "function") {
|
|
449
|
+
this.opts.logger.warn("[job] retention: no deletable data engine; prune skipped");
|
|
450
|
+
return { object: this.object, deleted: 0 };
|
|
451
|
+
}
|
|
452
|
+
if (!(retentionDays > 0)) {
|
|
453
|
+
this.opts.logger.warn(`[job] retention: invalid retentionDays=${retentionDays}; prune skipped`);
|
|
454
|
+
return { object: this.object, deleted: 0 };
|
|
455
|
+
}
|
|
456
|
+
const cutoffIso = new Date(this.now() - retentionDays * 864e5).toISOString();
|
|
457
|
+
try {
|
|
458
|
+
const res = await engine.delete(this.object, {
|
|
459
|
+
where: { [this.tsField]: { $lt: cutoffIso } },
|
|
460
|
+
multi: true,
|
|
461
|
+
context: SYSTEM_CTX2
|
|
462
|
+
});
|
|
463
|
+
const deleted = countDeleted(res);
|
|
464
|
+
if (deleted === void 0 || deleted > 0) {
|
|
465
|
+
this.opts.logger.info(
|
|
466
|
+
`[job] retention: pruned ${deleted ?? "?"} ${this.object} rows older than ${cutoffIso}`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return { object: this.object, deleted };
|
|
470
|
+
} catch (err) {
|
|
471
|
+
const msg = err?.message ?? String(err);
|
|
472
|
+
this.opts.logger.warn(`[job] retention: prune of ${this.object} failed (${msg})`);
|
|
473
|
+
return { object: this.object, error: msg };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
function countDeleted(res) {
|
|
478
|
+
if (typeof res === "number") return res;
|
|
479
|
+
if (Array.isArray(res)) return res.length;
|
|
480
|
+
if (res && typeof res === "object") {
|
|
481
|
+
const r = res;
|
|
482
|
+
for (const k of ["deletedCount", "deleted", "count", "affected", "affectedRows"]) {
|
|
483
|
+
if (typeof r[k] === "number") return r[k];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return void 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
426
489
|
// src/job-service-plugin.ts
|
|
427
490
|
var JobServicePlugin = class {
|
|
428
491
|
constructor(options = {}) {
|
|
429
492
|
this.name = "com.objectstack.service.job";
|
|
430
493
|
this.version = "1.1.0";
|
|
431
494
|
this.type = "standard";
|
|
432
|
-
this.options = {
|
|
495
|
+
this.options = {
|
|
496
|
+
adapter: "auto",
|
|
497
|
+
enableCron: true,
|
|
498
|
+
retentionDays: DEFAULT_JOB_RUN_RETENTION_DAYS,
|
|
499
|
+
retentionSweepMs: DEFAULT_JOB_RUN_SWEEP_MS,
|
|
500
|
+
...options
|
|
501
|
+
};
|
|
433
502
|
}
|
|
434
503
|
async init(ctx) {
|
|
435
504
|
try {
|
|
@@ -499,9 +568,32 @@ var JobServicePlugin = class {
|
|
|
499
568
|
} catch (err) {
|
|
500
569
|
ctx.logger.warn("JobServicePlugin: replaceService failed; staying on IntervalJobAdapter", err);
|
|
501
570
|
}
|
|
571
|
+
const retentionDays = this.options.retentionDays ?? DEFAULT_JOB_RUN_RETENTION_DAYS;
|
|
572
|
+
if (retentionDays > 0) {
|
|
573
|
+
const retention = new JobRunRetention({
|
|
574
|
+
getEngine: () => engine,
|
|
575
|
+
logger: ctx.logger
|
|
576
|
+
});
|
|
577
|
+
const sweepMs = this.options.retentionSweepMs ?? DEFAULT_JOB_RUN_SWEEP_MS;
|
|
578
|
+
const sweep = () => {
|
|
579
|
+
void retention.prune(retentionDays).catch(
|
|
580
|
+
(err) => ctx.logger.warn(`JobServicePlugin: retention sweep failed: ${err?.message ?? err}`)
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
sweep();
|
|
584
|
+
this.retentionTimer = setInterval(sweep, sweepMs);
|
|
585
|
+
this.retentionTimer.unref?.();
|
|
586
|
+
ctx.logger.info(
|
|
587
|
+
`JobServicePlugin: sys_job_run retention on (prune > ${retentionDays}d every ${Math.round(sweepMs / 1e3)}s)`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
502
590
|
});
|
|
503
591
|
}
|
|
504
592
|
async destroy() {
|
|
593
|
+
if (this.retentionTimer) {
|
|
594
|
+
clearInterval(this.retentionTimer);
|
|
595
|
+
this.retentionTimer = void 0;
|
|
596
|
+
}
|
|
505
597
|
await this.dbAdapter?.destroy();
|
|
506
598
|
await this.intervalAdapter?.destroy();
|
|
507
599
|
}
|
|
@@ -509,8 +601,11 @@ var JobServicePlugin = class {
|
|
|
509
601
|
// Annotate the CommonJS export names for ESM import in node:
|
|
510
602
|
0 && (module.exports = {
|
|
511
603
|
CronJobAdapter,
|
|
604
|
+
DEFAULT_JOB_RUN_RETENTION_DAYS,
|
|
605
|
+
DEFAULT_JOB_RUN_SWEEP_MS,
|
|
512
606
|
DbJobAdapter,
|
|
513
607
|
IntervalJobAdapter,
|
|
608
|
+
JobRunRetention,
|
|
514
609
|
JobServicePlugin
|
|
515
610
|
});
|
|
516
611
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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', order: '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":[]}
|
|
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","../src/job-run-retention.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';\nexport {\n JobRunRetention,\n DEFAULT_JOB_RUN_RETENTION_DAYS,\n DEFAULT_JOB_RUN_SWEEP_MS,\n} from './job-run-retention.js';\nexport type {\n JobRunRetentionOptions,\n JobRunPruneOutcome,\n} from './job-run-retention.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, JobEngineLike } from './db-job-adapter.js';\nimport {\n JobRunRetention,\n DEFAULT_JOB_RUN_RETENTION_DAYS,\n DEFAULT_JOB_RUN_SWEEP_MS,\n} from './job-run-retention.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 * Retention window in days for `sys_job_run` execution-history rows\n * (launch-readiness.md P1-2). Every run appends a row, so without pruning the\n * table grows unbounded. **Default-on** at {@link DEFAULT_JOB_RUN_RETENTION_DAYS}\n * — a periodic sweep deletes rows older than this. Set to `0` to disable\n * retention (rows kept forever; operator owns cleanup). Only applies on the\n * DB-backed adapter (no `sys_job_run` table exists for interval/cron).\n */\n retentionDays?: number;\n /** Retention sweep interval in ms (default {@link DEFAULT_JOB_RUN_SWEEP_MS}). Only used when `retentionDays > 0`. */\n retentionSweepMs?: number;\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 private retentionTimer?: ReturnType<typeof setInterval>;\n\n constructor(options: JobServicePluginOptions = {}) {\n this.options = {\n adapter: 'auto',\n enableCron: true,\n retentionDays: DEFAULT_JOB_RUN_RETENTION_DAYS,\n retentionSweepMs: DEFAULT_JOB_RUN_SWEEP_MS,\n ...options,\n };\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 // Retention sweep (launch-readiness.md P1-2): bound the append-only\n // sys_job_run log. Default-on — an unbounded run history is a guaranteed\n // slow leak. Runs once now then on a low-frequency interval; the timer is\n // unref'd so it never keeps the process alive. Only wired on the DB path\n // (the table exists only there).\n const retentionDays = this.options.retentionDays ?? DEFAULT_JOB_RUN_RETENTION_DAYS;\n if (retentionDays > 0) {\n const retention = new JobRunRetention({\n getEngine: () => engine as JobEngineLike,\n logger: ctx.logger,\n });\n const sweepMs = this.options.retentionSweepMs ?? DEFAULT_JOB_RUN_SWEEP_MS;\n const sweep = () => {\n void retention.prune(retentionDays).catch((err) =>\n ctx.logger.warn(`JobServicePlugin: retention sweep failed: ${(err as Error)?.message ?? err}`),\n );\n };\n sweep();\n this.retentionTimer = setInterval(sweep, sweepMs);\n this.retentionTimer.unref?.();\n ctx.logger.info(\n `JobServicePlugin: sys_job_run retention on (prune > ${retentionDays}d every ${Math.round(sweepMs / 1000)}s)`,\n );\n }\n });\n }\n\n async destroy(): Promise<void> {\n if (this.retentionTimer) {\n clearInterval(this.retentionTimer);\n this.retentionTimer = undefined;\n }\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', order: '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","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { JobEngineLike, JobLoggerLike } from './db-job-adapter.js';\n\nconst RUN_TABLE = 'sys_job_run';\nconst SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] } as const;\n\n/**\n * Default retention window for `sys_job_run` rows, in days. Every job execution\n * appends a run row (see {@link DbJobAdapter}); without pruning the table grows\n * unbounded on a long-running deployment (launch-readiness.md P1-2). 30 days\n * keeps recent history for operational triage while bounding growth. Operators\n * raise/lower it via `JobServicePlugin` options; `0` disables retention.\n */\nexport const DEFAULT_JOB_RUN_RETENTION_DAYS = 30;\n\n/**\n * Default interval between retention sweeps. Job-run volume is far lower than the\n * notification pipeline's, so a 6-hour cadence is ample — the sweep is a single\n * bulk `delete … where created_at < cutoff`.\n */\nexport const DEFAULT_JOB_RUN_SWEEP_MS = 6 * 3_600_000;\n\nexport interface JobRunRetentionOptions {\n /** Resolve the data engine; `undefined` ⇒ prune is a no-op. */\n getEngine(): JobEngineLike | undefined;\n logger: JobLoggerLike;\n /** Override the swept object (tests). Defaults to `sys_job_run`. */\n object?: string;\n /** Timestamp field used for the cutoff (ISO-8601). Defaults to `created_at`. */\n tsField?: string;\n /** Clock injection for deterministic tests. Defaults to `Date.now()`. */\n now?(): number;\n}\n\nexport interface JobRunPruneOutcome {\n object: string;\n /** `undefined` when the driver doesn't report a count. */\n deleted?: number;\n error?: string;\n}\n\n/**\n * Retention sweeper for `sys_job_run` (launch-readiness.md P1-2).\n *\n * Mirrors the proven `NotificationRetention` shape in `service-messaging`:\n * a single bulk delete of rows older than a cutoff, under a system context\n * (retention is a cross-tenant operator policy). Isolated from job execution —\n * a sweep failure is logged and never throws into the scheduler.\n *\n * Unlike the messaging sweeper, this one is **default-on** in the plugin: an\n * append-only run log with no ceiling is a guaranteed slow leak, so GA ships\n * with a sensible window rather than requiring opt-in.\n */\nexport class JobRunRetention {\n private readonly now: () => number;\n private readonly object: string;\n private readonly tsField: string;\n\n constructor(private readonly opts: JobRunRetentionOptions) {\n this.now = opts.now ?? (() => Date.now());\n this.object = opts.object ?? RUN_TABLE;\n this.tsField = opts.tsField ?? 'created_at';\n }\n\n /**\n * Delete `sys_job_run` rows older than `retentionDays`. No-op when no data\n * engine is available, the engine can't delete, or `retentionDays` is not a\n * positive number.\n */\n async prune(retentionDays: number): Promise<JobRunPruneOutcome> {\n const engine = this.opts.getEngine();\n if (!engine || typeof engine.delete !== 'function') {\n this.opts.logger.warn('[job] retention: no deletable data engine; prune skipped');\n return { object: this.object, deleted: 0 };\n }\n if (!(retentionDays > 0)) {\n this.opts.logger.warn(`[job] retention: invalid retentionDays=${retentionDays}; prune skipped`);\n return { object: this.object, deleted: 0 };\n }\n\n const cutoffIso = new Date(this.now() - retentionDays * 86_400_000).toISOString();\n try {\n const res = await engine.delete(this.object, {\n where: { [this.tsField]: { $lt: cutoffIso } },\n multi: true,\n context: SYSTEM_CTX,\n });\n const deleted = countDeleted(res);\n if (deleted === undefined || deleted > 0) {\n this.opts.logger.info(\n `[job] retention: pruned ${deleted ?? '?'} ${this.object} rows older than ${cutoffIso}`,\n );\n }\n return { object: this.object, deleted };\n } catch (err) {\n const msg = (err as Error)?.message ?? String(err);\n this.opts.logger.warn(`[job] retention: prune of ${this.object} failed (${msg})`);\n return { object: this.object, error: msg };\n }\n }\n}\n\n/** Best-effort row-count extraction from a driver's delete result. */\nfunction countDeleted(res: unknown): number | undefined {\n if (typeof res === 'number') return res;\n if (Array.isArray(res)) return res.length;\n if (res && typeof res === 'object') {\n const r = res as Record<string, unknown>;\n for (const k of ['deletedCount', 'deleted', 'count', 'affected', 'affectedRows']) {\n if (typeof r[k] === 'number') return r[k] as number;\n }\n }\n return undefined;\n}\n"],"mappings":"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","names":["RUN_TABLE","SYSTEM_CTX"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -112,6 +112,17 @@ interface JobServicePluginOptions {
|
|
|
112
112
|
db?: DbJobAdapterOptions;
|
|
113
113
|
/** Whether to also wire CronJobAdapter for cron schedules (default: true when available) */
|
|
114
114
|
enableCron?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Retention window in days for `sys_job_run` execution-history rows
|
|
117
|
+
* (launch-readiness.md P1-2). Every run appends a row, so without pruning the
|
|
118
|
+
* table grows unbounded. **Default-on** at {@link DEFAULT_JOB_RUN_RETENTION_DAYS}
|
|
119
|
+
* — a periodic sweep deletes rows older than this. Set to `0` to disable
|
|
120
|
+
* retention (rows kept forever; operator owns cleanup). Only applies on the
|
|
121
|
+
* DB-backed adapter (no `sys_job_run` table exists for interval/cron).
|
|
122
|
+
*/
|
|
123
|
+
retentionDays?: number;
|
|
124
|
+
/** Retention sweep interval in ms (default {@link DEFAULT_JOB_RUN_SWEEP_MS}). Only used when `retentionDays > 0`. */
|
|
125
|
+
retentionSweepMs?: number;
|
|
115
126
|
}
|
|
116
127
|
/**
|
|
117
128
|
* JobServicePlugin — Production IJobService implementation.
|
|
@@ -128,6 +139,7 @@ declare class JobServicePlugin implements Plugin {
|
|
|
128
139
|
private readonly options;
|
|
129
140
|
private dbAdapter?;
|
|
130
141
|
private intervalAdapter?;
|
|
142
|
+
private retentionTimer?;
|
|
131
143
|
constructor(options?: JobServicePluginOptions);
|
|
132
144
|
init(ctx: PluginContext): Promise<void>;
|
|
133
145
|
destroy(): Promise<void>;
|
|
@@ -164,4 +176,61 @@ declare class CronJobAdapter implements IJobService {
|
|
|
164
176
|
private execute;
|
|
165
177
|
}
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Default retention window for `sys_job_run` rows, in days. Every job execution
|
|
181
|
+
* appends a run row (see {@link DbJobAdapter}); without pruning the table grows
|
|
182
|
+
* unbounded on a long-running deployment (launch-readiness.md P1-2). 30 days
|
|
183
|
+
* keeps recent history for operational triage while bounding growth. Operators
|
|
184
|
+
* raise/lower it via `JobServicePlugin` options; `0` disables retention.
|
|
185
|
+
*/
|
|
186
|
+
declare const DEFAULT_JOB_RUN_RETENTION_DAYS = 30;
|
|
187
|
+
/**
|
|
188
|
+
* Default interval between retention sweeps. Job-run volume is far lower than the
|
|
189
|
+
* notification pipeline's, so a 6-hour cadence is ample — the sweep is a single
|
|
190
|
+
* bulk `delete … where created_at < cutoff`.
|
|
191
|
+
*/
|
|
192
|
+
declare const DEFAULT_JOB_RUN_SWEEP_MS: number;
|
|
193
|
+
interface JobRunRetentionOptions {
|
|
194
|
+
/** Resolve the data engine; `undefined` ⇒ prune is a no-op. */
|
|
195
|
+
getEngine(): JobEngineLike | undefined;
|
|
196
|
+
logger: JobLoggerLike;
|
|
197
|
+
/** Override the swept object (tests). Defaults to `sys_job_run`. */
|
|
198
|
+
object?: string;
|
|
199
|
+
/** Timestamp field used for the cutoff (ISO-8601). Defaults to `created_at`. */
|
|
200
|
+
tsField?: string;
|
|
201
|
+
/** Clock injection for deterministic tests. Defaults to `Date.now()`. */
|
|
202
|
+
now?(): number;
|
|
203
|
+
}
|
|
204
|
+
interface JobRunPruneOutcome {
|
|
205
|
+
object: string;
|
|
206
|
+
/** `undefined` when the driver doesn't report a count. */
|
|
207
|
+
deleted?: number;
|
|
208
|
+
error?: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Retention sweeper for `sys_job_run` (launch-readiness.md P1-2).
|
|
212
|
+
*
|
|
213
|
+
* Mirrors the proven `NotificationRetention` shape in `service-messaging`:
|
|
214
|
+
* a single bulk delete of rows older than a cutoff, under a system context
|
|
215
|
+
* (retention is a cross-tenant operator policy). Isolated from job execution —
|
|
216
|
+
* a sweep failure is logged and never throws into the scheduler.
|
|
217
|
+
*
|
|
218
|
+
* Unlike the messaging sweeper, this one is **default-on** in the plugin: an
|
|
219
|
+
* append-only run log with no ceiling is a guaranteed slow leak, so GA ships
|
|
220
|
+
* with a sensible window rather than requiring opt-in.
|
|
221
|
+
*/
|
|
222
|
+
declare class JobRunRetention {
|
|
223
|
+
private readonly opts;
|
|
224
|
+
private readonly now;
|
|
225
|
+
private readonly object;
|
|
226
|
+
private readonly tsField;
|
|
227
|
+
constructor(opts: JobRunRetentionOptions);
|
|
228
|
+
/**
|
|
229
|
+
* Delete `sys_job_run` rows older than `retentionDays`. No-op when no data
|
|
230
|
+
* engine is available, the engine can't delete, or `retentionDays` is not a
|
|
231
|
+
* positive number.
|
|
232
|
+
*/
|
|
233
|
+
prune(retentionDays: number): Promise<JobRunPruneOutcome>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { CronJobAdapter, type CronJobAdapterOptions, DEFAULT_JOB_RUN_RETENTION_DAYS, DEFAULT_JOB_RUN_SWEEP_MS, DbJobAdapter, type DbJobAdapterOptions, IntervalJobAdapter, type IntervalJobAdapterOptions, type JobEngineLike, type JobLoggerLike, type JobRunPruneOutcome, JobRunRetention, type JobRunRetentionOptions, JobServicePlugin, type JobServicePluginOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -112,6 +112,17 @@ interface JobServicePluginOptions {
|
|
|
112
112
|
db?: DbJobAdapterOptions;
|
|
113
113
|
/** Whether to also wire CronJobAdapter for cron schedules (default: true when available) */
|
|
114
114
|
enableCron?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Retention window in days for `sys_job_run` execution-history rows
|
|
117
|
+
* (launch-readiness.md P1-2). Every run appends a row, so without pruning the
|
|
118
|
+
* table grows unbounded. **Default-on** at {@link DEFAULT_JOB_RUN_RETENTION_DAYS}
|
|
119
|
+
* — a periodic sweep deletes rows older than this. Set to `0` to disable
|
|
120
|
+
* retention (rows kept forever; operator owns cleanup). Only applies on the
|
|
121
|
+
* DB-backed adapter (no `sys_job_run` table exists for interval/cron).
|
|
122
|
+
*/
|
|
123
|
+
retentionDays?: number;
|
|
124
|
+
/** Retention sweep interval in ms (default {@link DEFAULT_JOB_RUN_SWEEP_MS}). Only used when `retentionDays > 0`. */
|
|
125
|
+
retentionSweepMs?: number;
|
|
115
126
|
}
|
|
116
127
|
/**
|
|
117
128
|
* JobServicePlugin — Production IJobService implementation.
|
|
@@ -128,6 +139,7 @@ declare class JobServicePlugin implements Plugin {
|
|
|
128
139
|
private readonly options;
|
|
129
140
|
private dbAdapter?;
|
|
130
141
|
private intervalAdapter?;
|
|
142
|
+
private retentionTimer?;
|
|
131
143
|
constructor(options?: JobServicePluginOptions);
|
|
132
144
|
init(ctx: PluginContext): Promise<void>;
|
|
133
145
|
destroy(): Promise<void>;
|
|
@@ -164,4 +176,61 @@ declare class CronJobAdapter implements IJobService {
|
|
|
164
176
|
private execute;
|
|
165
177
|
}
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Default retention window for `sys_job_run` rows, in days. Every job execution
|
|
181
|
+
* appends a run row (see {@link DbJobAdapter}); without pruning the table grows
|
|
182
|
+
* unbounded on a long-running deployment (launch-readiness.md P1-2). 30 days
|
|
183
|
+
* keeps recent history for operational triage while bounding growth. Operators
|
|
184
|
+
* raise/lower it via `JobServicePlugin` options; `0` disables retention.
|
|
185
|
+
*/
|
|
186
|
+
declare const DEFAULT_JOB_RUN_RETENTION_DAYS = 30;
|
|
187
|
+
/**
|
|
188
|
+
* Default interval between retention sweeps. Job-run volume is far lower than the
|
|
189
|
+
* notification pipeline's, so a 6-hour cadence is ample — the sweep is a single
|
|
190
|
+
* bulk `delete … where created_at < cutoff`.
|
|
191
|
+
*/
|
|
192
|
+
declare const DEFAULT_JOB_RUN_SWEEP_MS: number;
|
|
193
|
+
interface JobRunRetentionOptions {
|
|
194
|
+
/** Resolve the data engine; `undefined` ⇒ prune is a no-op. */
|
|
195
|
+
getEngine(): JobEngineLike | undefined;
|
|
196
|
+
logger: JobLoggerLike;
|
|
197
|
+
/** Override the swept object (tests). Defaults to `sys_job_run`. */
|
|
198
|
+
object?: string;
|
|
199
|
+
/** Timestamp field used for the cutoff (ISO-8601). Defaults to `created_at`. */
|
|
200
|
+
tsField?: string;
|
|
201
|
+
/** Clock injection for deterministic tests. Defaults to `Date.now()`. */
|
|
202
|
+
now?(): number;
|
|
203
|
+
}
|
|
204
|
+
interface JobRunPruneOutcome {
|
|
205
|
+
object: string;
|
|
206
|
+
/** `undefined` when the driver doesn't report a count. */
|
|
207
|
+
deleted?: number;
|
|
208
|
+
error?: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Retention sweeper for `sys_job_run` (launch-readiness.md P1-2).
|
|
212
|
+
*
|
|
213
|
+
* Mirrors the proven `NotificationRetention` shape in `service-messaging`:
|
|
214
|
+
* a single bulk delete of rows older than a cutoff, under a system context
|
|
215
|
+
* (retention is a cross-tenant operator policy). Isolated from job execution —
|
|
216
|
+
* a sweep failure is logged and never throws into the scheduler.
|
|
217
|
+
*
|
|
218
|
+
* Unlike the messaging sweeper, this one is **default-on** in the plugin: an
|
|
219
|
+
* append-only run log with no ceiling is a guaranteed slow leak, so GA ships
|
|
220
|
+
* with a sensible window rather than requiring opt-in.
|
|
221
|
+
*/
|
|
222
|
+
declare class JobRunRetention {
|
|
223
|
+
private readonly opts;
|
|
224
|
+
private readonly now;
|
|
225
|
+
private readonly object;
|
|
226
|
+
private readonly tsField;
|
|
227
|
+
constructor(opts: JobRunRetentionOptions);
|
|
228
|
+
/**
|
|
229
|
+
* Delete `sys_job_run` rows older than `retentionDays`. No-op when no data
|
|
230
|
+
* engine is available, the engine can't delete, or `retentionDays` is not a
|
|
231
|
+
* positive number.
|
|
232
|
+
*/
|
|
233
|
+
prune(retentionDays: number): Promise<JobRunPruneOutcome>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { CronJobAdapter, type CronJobAdapterOptions, DEFAULT_JOB_RUN_RETENTION_DAYS, DEFAULT_JOB_RUN_SWEEP_MS, DbJobAdapter, type DbJobAdapterOptions, IntervalJobAdapter, type IntervalJobAdapterOptions, type JobEngineLike, type JobLoggerLike, type JobRunPruneOutcome, JobRunRetention, type JobRunRetentionOptions, JobServicePlugin, type JobServicePluginOptions };
|
package/dist/index.js
CHANGED
|
@@ -394,13 +394,79 @@ var DbJobAdapter = class {
|
|
|
394
394
|
}
|
|
395
395
|
};
|
|
396
396
|
|
|
397
|
+
// src/job-run-retention.ts
|
|
398
|
+
var RUN_TABLE2 = "sys_job_run";
|
|
399
|
+
var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
|
|
400
|
+
var DEFAULT_JOB_RUN_RETENTION_DAYS = 30;
|
|
401
|
+
var DEFAULT_JOB_RUN_SWEEP_MS = 6 * 36e5;
|
|
402
|
+
var JobRunRetention = class {
|
|
403
|
+
constructor(opts) {
|
|
404
|
+
this.opts = opts;
|
|
405
|
+
this.now = opts.now ?? (() => Date.now());
|
|
406
|
+
this.object = opts.object ?? RUN_TABLE2;
|
|
407
|
+
this.tsField = opts.tsField ?? "created_at";
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Delete `sys_job_run` rows older than `retentionDays`. No-op when no data
|
|
411
|
+
* engine is available, the engine can't delete, or `retentionDays` is not a
|
|
412
|
+
* positive number.
|
|
413
|
+
*/
|
|
414
|
+
async prune(retentionDays) {
|
|
415
|
+
const engine = this.opts.getEngine();
|
|
416
|
+
if (!engine || typeof engine.delete !== "function") {
|
|
417
|
+
this.opts.logger.warn("[job] retention: no deletable data engine; prune skipped");
|
|
418
|
+
return { object: this.object, deleted: 0 };
|
|
419
|
+
}
|
|
420
|
+
if (!(retentionDays > 0)) {
|
|
421
|
+
this.opts.logger.warn(`[job] retention: invalid retentionDays=${retentionDays}; prune skipped`);
|
|
422
|
+
return { object: this.object, deleted: 0 };
|
|
423
|
+
}
|
|
424
|
+
const cutoffIso = new Date(this.now() - retentionDays * 864e5).toISOString();
|
|
425
|
+
try {
|
|
426
|
+
const res = await engine.delete(this.object, {
|
|
427
|
+
where: { [this.tsField]: { $lt: cutoffIso } },
|
|
428
|
+
multi: true,
|
|
429
|
+
context: SYSTEM_CTX2
|
|
430
|
+
});
|
|
431
|
+
const deleted = countDeleted(res);
|
|
432
|
+
if (deleted === void 0 || deleted > 0) {
|
|
433
|
+
this.opts.logger.info(
|
|
434
|
+
`[job] retention: pruned ${deleted ?? "?"} ${this.object} rows older than ${cutoffIso}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return { object: this.object, deleted };
|
|
438
|
+
} catch (err) {
|
|
439
|
+
const msg = err?.message ?? String(err);
|
|
440
|
+
this.opts.logger.warn(`[job] retention: prune of ${this.object} failed (${msg})`);
|
|
441
|
+
return { object: this.object, error: msg };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
function countDeleted(res) {
|
|
446
|
+
if (typeof res === "number") return res;
|
|
447
|
+
if (Array.isArray(res)) return res.length;
|
|
448
|
+
if (res && typeof res === "object") {
|
|
449
|
+
const r = res;
|
|
450
|
+
for (const k of ["deletedCount", "deleted", "count", "affected", "affectedRows"]) {
|
|
451
|
+
if (typeof r[k] === "number") return r[k];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
397
457
|
// src/job-service-plugin.ts
|
|
398
458
|
var JobServicePlugin = class {
|
|
399
459
|
constructor(options = {}) {
|
|
400
460
|
this.name = "com.objectstack.service.job";
|
|
401
461
|
this.version = "1.1.0";
|
|
402
462
|
this.type = "standard";
|
|
403
|
-
this.options = {
|
|
463
|
+
this.options = {
|
|
464
|
+
adapter: "auto",
|
|
465
|
+
enableCron: true,
|
|
466
|
+
retentionDays: DEFAULT_JOB_RUN_RETENTION_DAYS,
|
|
467
|
+
retentionSweepMs: DEFAULT_JOB_RUN_SWEEP_MS,
|
|
468
|
+
...options
|
|
469
|
+
};
|
|
404
470
|
}
|
|
405
471
|
async init(ctx) {
|
|
406
472
|
try {
|
|
@@ -470,17 +536,43 @@ var JobServicePlugin = class {
|
|
|
470
536
|
} catch (err) {
|
|
471
537
|
ctx.logger.warn("JobServicePlugin: replaceService failed; staying on IntervalJobAdapter", err);
|
|
472
538
|
}
|
|
539
|
+
const retentionDays = this.options.retentionDays ?? DEFAULT_JOB_RUN_RETENTION_DAYS;
|
|
540
|
+
if (retentionDays > 0) {
|
|
541
|
+
const retention = new JobRunRetention({
|
|
542
|
+
getEngine: () => engine,
|
|
543
|
+
logger: ctx.logger
|
|
544
|
+
});
|
|
545
|
+
const sweepMs = this.options.retentionSweepMs ?? DEFAULT_JOB_RUN_SWEEP_MS;
|
|
546
|
+
const sweep = () => {
|
|
547
|
+
void retention.prune(retentionDays).catch(
|
|
548
|
+
(err) => ctx.logger.warn(`JobServicePlugin: retention sweep failed: ${err?.message ?? err}`)
|
|
549
|
+
);
|
|
550
|
+
};
|
|
551
|
+
sweep();
|
|
552
|
+
this.retentionTimer = setInterval(sweep, sweepMs);
|
|
553
|
+
this.retentionTimer.unref?.();
|
|
554
|
+
ctx.logger.info(
|
|
555
|
+
`JobServicePlugin: sys_job_run retention on (prune > ${retentionDays}d every ${Math.round(sweepMs / 1e3)}s)`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
473
558
|
});
|
|
474
559
|
}
|
|
475
560
|
async destroy() {
|
|
561
|
+
if (this.retentionTimer) {
|
|
562
|
+
clearInterval(this.retentionTimer);
|
|
563
|
+
this.retentionTimer = void 0;
|
|
564
|
+
}
|
|
476
565
|
await this.dbAdapter?.destroy();
|
|
477
566
|
await this.intervalAdapter?.destroy();
|
|
478
567
|
}
|
|
479
568
|
};
|
|
480
569
|
export {
|
|
481
570
|
CronJobAdapter,
|
|
571
|
+
DEFAULT_JOB_RUN_RETENTION_DAYS,
|
|
572
|
+
DEFAULT_JOB_RUN_SWEEP_MS,
|
|
482
573
|
DbJobAdapter,
|
|
483
574
|
IntervalJobAdapter,
|
|
575
|
+
JobRunRetention,
|
|
484
576
|
JobServicePlugin
|
|
485
577
|
};
|
|
486
578
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../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\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', order: '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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/job-service-plugin.ts","../src/interval-job-adapter.ts","../src/cron-job-adapter.ts","../src/db-job-adapter.ts","../src/job-run-retention.ts"],"sourcesContent":["// 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, JobEngineLike } from './db-job-adapter.js';\nimport {\n JobRunRetention,\n DEFAULT_JOB_RUN_RETENTION_DAYS,\n DEFAULT_JOB_RUN_SWEEP_MS,\n} from './job-run-retention.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 * Retention window in days for `sys_job_run` execution-history rows\n * (launch-readiness.md P1-2). Every run appends a row, so without pruning the\n * table grows unbounded. **Default-on** at {@link DEFAULT_JOB_RUN_RETENTION_DAYS}\n * — a periodic sweep deletes rows older than this. Set to `0` to disable\n * retention (rows kept forever; operator owns cleanup). Only applies on the\n * DB-backed adapter (no `sys_job_run` table exists for interval/cron).\n */\n retentionDays?: number;\n /** Retention sweep interval in ms (default {@link DEFAULT_JOB_RUN_SWEEP_MS}). Only used when `retentionDays > 0`. */\n retentionSweepMs?: number;\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 private retentionTimer?: ReturnType<typeof setInterval>;\n\n constructor(options: JobServicePluginOptions = {}) {\n this.options = {\n adapter: 'auto',\n enableCron: true,\n retentionDays: DEFAULT_JOB_RUN_RETENTION_DAYS,\n retentionSweepMs: DEFAULT_JOB_RUN_SWEEP_MS,\n ...options,\n };\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 // Retention sweep (launch-readiness.md P1-2): bound the append-only\n // sys_job_run log. Default-on — an unbounded run history is a guaranteed\n // slow leak. Runs once now then on a low-frequency interval; the timer is\n // unref'd so it never keeps the process alive. Only wired on the DB path\n // (the table exists only there).\n const retentionDays = this.options.retentionDays ?? DEFAULT_JOB_RUN_RETENTION_DAYS;\n if (retentionDays > 0) {\n const retention = new JobRunRetention({\n getEngine: () => engine as JobEngineLike,\n logger: ctx.logger,\n });\n const sweepMs = this.options.retentionSweepMs ?? DEFAULT_JOB_RUN_SWEEP_MS;\n const sweep = () => {\n void retention.prune(retentionDays).catch((err) =>\n ctx.logger.warn(`JobServicePlugin: retention sweep failed: ${(err as Error)?.message ?? err}`),\n );\n };\n sweep();\n this.retentionTimer = setInterval(sweep, sweepMs);\n this.retentionTimer.unref?.();\n ctx.logger.info(\n `JobServicePlugin: sys_job_run retention on (prune > ${retentionDays}d every ${Math.round(sweepMs / 1000)}s)`,\n );\n }\n });\n }\n\n async destroy(): Promise<void> {\n if (this.retentionTimer) {\n clearInterval(this.retentionTimer);\n this.retentionTimer = undefined;\n }\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', order: '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","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { JobEngineLike, JobLoggerLike } from './db-job-adapter.js';\n\nconst RUN_TABLE = 'sys_job_run';\nconst SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] } as const;\n\n/**\n * Default retention window for `sys_job_run` rows, in days. Every job execution\n * appends a run row (see {@link DbJobAdapter}); without pruning the table grows\n * unbounded on a long-running deployment (launch-readiness.md P1-2). 30 days\n * keeps recent history for operational triage while bounding growth. Operators\n * raise/lower it via `JobServicePlugin` options; `0` disables retention.\n */\nexport const DEFAULT_JOB_RUN_RETENTION_DAYS = 30;\n\n/**\n * Default interval between retention sweeps. Job-run volume is far lower than the\n * notification pipeline's, so a 6-hour cadence is ample — the sweep is a single\n * bulk `delete … where created_at < cutoff`.\n */\nexport const DEFAULT_JOB_RUN_SWEEP_MS = 6 * 3_600_000;\n\nexport interface JobRunRetentionOptions {\n /** Resolve the data engine; `undefined` ⇒ prune is a no-op. */\n getEngine(): JobEngineLike | undefined;\n logger: JobLoggerLike;\n /** Override the swept object (tests). Defaults to `sys_job_run`. */\n object?: string;\n /** Timestamp field used for the cutoff (ISO-8601). Defaults to `created_at`. */\n tsField?: string;\n /** Clock injection for deterministic tests. Defaults to `Date.now()`. */\n now?(): number;\n}\n\nexport interface JobRunPruneOutcome {\n object: string;\n /** `undefined` when the driver doesn't report a count. */\n deleted?: number;\n error?: string;\n}\n\n/**\n * Retention sweeper for `sys_job_run` (launch-readiness.md P1-2).\n *\n * Mirrors the proven `NotificationRetention` shape in `service-messaging`:\n * a single bulk delete of rows older than a cutoff, under a system context\n * (retention is a cross-tenant operator policy). Isolated from job execution —\n * a sweep failure is logged and never throws into the scheduler.\n *\n * Unlike the messaging sweeper, this one is **default-on** in the plugin: an\n * append-only run log with no ceiling is a guaranteed slow leak, so GA ships\n * with a sensible window rather than requiring opt-in.\n */\nexport class JobRunRetention {\n private readonly now: () => number;\n private readonly object: string;\n private readonly tsField: string;\n\n constructor(private readonly opts: JobRunRetentionOptions) {\n this.now = opts.now ?? (() => Date.now());\n this.object = opts.object ?? RUN_TABLE;\n this.tsField = opts.tsField ?? 'created_at';\n }\n\n /**\n * Delete `sys_job_run` rows older than `retentionDays`. No-op when no data\n * engine is available, the engine can't delete, or `retentionDays` is not a\n * positive number.\n */\n async prune(retentionDays: number): Promise<JobRunPruneOutcome> {\n const engine = this.opts.getEngine();\n if (!engine || typeof engine.delete !== 'function') {\n this.opts.logger.warn('[job] retention: no deletable data engine; prune skipped');\n return { object: this.object, deleted: 0 };\n }\n if (!(retentionDays > 0)) {\n this.opts.logger.warn(`[job] retention: invalid retentionDays=${retentionDays}; prune skipped`);\n return { object: this.object, deleted: 0 };\n }\n\n const cutoffIso = new Date(this.now() - retentionDays * 86_400_000).toISOString();\n try {\n const res = await engine.delete(this.object, {\n where: { [this.tsField]: { $lt: cutoffIso } },\n multi: true,\n context: SYSTEM_CTX,\n });\n const deleted = countDeleted(res);\n if (deleted === undefined || deleted > 0) {\n this.opts.logger.info(\n `[job] retention: pruned ${deleted ?? '?'} ${this.object} rows older than ${cutoffIso}`,\n );\n }\n return { object: this.object, deleted };\n } catch (err) {\n const msg = (err as Error)?.message ?? String(err);\n this.opts.logger.warn(`[job] retention: prune of ${this.object} failed (${msg})`);\n return { object: this.object, error: msg };\n }\n }\n}\n\n/** Best-effort row-count extraction from a driver's delete result. */\nfunction countDeleted(res: unknown): number | undefined {\n if (typeof res === 'number') return res;\n if (Array.isArray(res)) return res.length;\n if (res && typeof res === 'object') {\n const r = res as Record<string, unknown>;\n for (const k of ['deletedCount', 'deleted', 'count', 'affected', 'affectedRows']) {\n if (typeof r[k] === 'number') return r[k] as number;\n }\n }\n return undefined;\n}\n"],"mappings":";AAGA,SAAS,QAAQ,iBAAiB;;;AC6B3B,IAAM,qBAAN,MAAgD;AAAA,EAIrD,YAAY,UAAqC,CAAC,GAAG;AAHrD,SAAiB,OAAO,oBAAI,IAAuB;AAIjD,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,MAAc,UAAuB,SAAoC;AAEtF,UAAM,KAAK,OAAO,IAAI;AAEtB,UAAM,SAAoB,EAAE,MAAM,UAAU,SAAS,YAAY,CAAC,EAAE;AAEpE,QAAI,SAAS,SAAS,cAAc,SAAS,YAAY;AACvD,aAAO,UAAU,YAAY,YAAY;AACvC,cAAM,KAAK,WAAW,MAAM;AAAA,MAC9B,GAAG,SAAS,UAAU;AAAA,IACxB,WAAW,SAAS,SAAS,UAAU,SAAS,IAAI;AAClD,YAAM,QAAQ,IAAI,KAAK,SAAS,EAAE,EAAE,QAAQ,IAAI,KAAK,IAAI;AACzD,UAAI,QAAQ,GAAG;AACb,eAAO,UAAU,WAAW,YAAY;AACtC,gBAAM,KAAK,WAAW,MAAM;AAAA,QAC9B,GAAG,KAAK;AAAA,MACV;AAAA,IACF;AAGA,SAAK,KAAK,IAAI,MAAM,MAAM;AAAA,EAC5B;AAAA,EAEA,MAAM,OAAO,MAA6B;AACxC,UAAM,SAAS,KAAK,KAAK,IAAI,IAAI;AACjC,QAAI,QAAQ,SAAS;AACnB,oBAAc,OAAO,OAAyC;AAC9D,mBAAa,OAAO,OAAwC;AAAA,IAC9D;AACA,SAAK,KAAK,OAAO,IAAI;AAAA,EACvB;AAAA,EAEA,MAAM,QAAQ,MAAc,MAA+B;AACzD,UAAM,SAAS,KAAK,KAAK,IAAI,IAAI;AACjC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,QAAQ,IAAI,aAAa;AAAA,IAC3C;AACA,UAAM,KAAK,WAAW,QAAQ,IAAI;AAAA,EACpC;AAAA,EAEA,MAAM,cAAc,MAAc,OAAyC;AACzE,UAAM,SAAS,KAAK,KAAK,IAAI,IAAI;AACjC,QAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,UAAM,QAAQ,OAAO;AACrB,WAAO,QAAQ,MAAM,MAAM,CAAC,KAAK,IAAI;AAAA,EACvC;AAAA,EAEA,MAAM,WAA8B;AAClC,WAAO,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAyB;AAC7B,eAAW,UAAU,KAAK,KAAK,OAAO,GAAG;AACvC,UAAI,OAAO,SAAS;AAClB,sBAAc,OAAO,OAAyC;AAC9D,qBAAa,OAAO,OAAwC;AAAA,MAC9D;AAAA,IACF;AACA,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEA,MAAc,WAAW,QAAmB,MAA+B;AACzE,UAAM,YAA0B;AAAA,MAC9B,OAAO,OAAO;AAAA,MACd,QAAQ;AAAA,MACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AAEA,UAAM,UAAU,KAAK,IAAI;AACzB,QAAI;AACF,YAAM,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,KAAK,CAAC;AACjD,gBAAU,SAAS;AAAA,IACrB,SAAS,KAAK;AACZ,gBAAU,SAAS;AACnB,gBAAU,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACnE,UAAE;AACA,gBAAU,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC/C,gBAAU,aAAa,KAAK,IAAI,IAAI;AAEpC,aAAO,WAAW,KAAK,SAAS;AAEhC,UAAI,OAAO,WAAW,SAAS,KAAK,eAAe;AACjD,eAAO,WAAW,OAAO,GAAG,OAAO,WAAW,SAAS,KAAK,aAAa;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACF;;;AC/HA,SAAS,YAAY;AAiCd,IAAM,iBAAN,MAA4C;AAAA,EAKjD,YAAY,UAAiC,CAAC,GAAG;AAFjD,SAAiB,OAAO,oBAAI,IAA2B;AAGrD,SAAK,kBAAkB,QAAQ,YAAY;AAC3C,SAAK,gBAAgB,QAAQ,iBAAiB;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,MAAc,UAAuB,SAAoC;AACtF,UAAM,KAAK,OAAO,IAAI;AAEtB,UAAM,SAAwB,EAAE,MAAM,UAAU,SAAS,YAAY,CAAC,EAAE;AAExE,QAAI,SAAS,SAAS,QAAQ;AAC5B,UAAI,CAAC,SAAS,YAAY;AACxB,cAAM,IAAI,MAAM,sCAAsC,IAAI,sBAAsB;AAAA,MAClF;AACA,YAAM,OAAO,IAAI;AAAA,QACf,SAAS;AAAA,QACT,EAAE,UAAU,SAAS,YAAY,KAAK,iBAAiB,KAAK;AAAA,QAC5D,YAAY;AAAE,gBAAM,KAAK,QAAQ,MAAM;AAAA,QAAG;AAAA,MAC5C;AACA,aAAO,OAAO;AAAA,IAChB,WAAW,SAAS,SAAS,cAAc,SAAS,YAAY;AAC9D,YAAM,SAAS,YAAY,MAAM;AAAE,aAAK,KAAK,QAAQ,MAAM;AAAA,MAAG,GAAG,SAAS,UAAU;AACpF,MAAC,QAAgB,QAAQ;AAEzB,aAAO,OAAO,EAAE,MAAM,MAAM,cAAc,MAAM,EAAE;AAAA,IACpD,WAAW,SAAS,SAAS,UAAU,SAAS,IAAI;AAClD,YAAM,QAAQ,IAAI,KAAK,SAAS,EAAE,EAAE,QAAQ,IAAI,KAAK,IAAI;AACzD,UAAI,QAAQ,GAAG;AACb,cAAM,SAAS,WAAW,MAAM;AAAE,eAAK,KAAK,QAAQ,MAAM;AAAA,QAAG,GAAG,KAAK;AACrE,QAAC,QAAgB,QAAQ;AACzB,eAAO,OAAO,EAAE,MAAM,MAAM,aAAa,MAAM,EAAE;AAAA,MACnD;AAAA,IACF;AAEA,SAAK,KAAK,IAAI,MAAM,MAAM;AAAA,EAC5B;AAAA,EAEA,MAAM,OAAO,MAA6B;AACxC,UAAM,MAAM,KAAK,KAAK,IAAI,IAAI;AAC9B,QAAI,KAAK,MAAM;AACb,UAAI;AAAE,YAAI,KAAK,KAAK;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IAChD;AACA,SAAK,KAAK,OAAO,IAAI;AAAA,EACvB;AAAA,EAEA,MAAM,QAAQ,MAAc,MAA+B;AACzD,UAAM,MAAM,KAAK,KAAK,IAAI,IAAI;AAC9B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,QAAQ,IAAI,aAAa;AACnD,UAAM,KAAK,QAAQ,KAAK,IAAI;AAAA,EAC9B;AAAA,EAEA,MAAM,cAAc,MAAc,OAAyC;AACzE,UAAM,MAAM,KAAK,KAAK,IAAI,IAAI;AAC9B,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,WAAO,QAAQ,IAAI,WAAW,MAAM,CAAC,KAAK,IAAI,IAAI;AAAA,EACpD;AAAA,EAEA,MAAM,WAA8B;AAClC,WAAO,CAAC,GAAG,KAAK,KAAK,KAAK,CAAC;AAAA,EAC7B;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,eAAW,OAAO,KAAK,KAAK,OAAO,GAAG;AACpC,UAAI;AAAE,YAAI,MAAM,KAAK;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IACjD;AACA,SAAK,KAAK,MAAM;AAAA,EAClB;AAAA,EAEA,MAAc,QAAQ,QAAuB,MAA+B;AAC1E,UAAM,YAA0B;AAAA,MAC9B,OAAO,OAAO;AAAA,MACd,QAAQ;AAAA,MACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AACA,UAAM,UAAU,KAAK,IAAI;AACzB,QAAI;AACF,YAAM,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,KAAK,CAAC;AACjD,gBAAU,SAAS;AAAA,IACrB,SAAS,KAAK;AACZ,gBAAU,SAAS;AACnB,gBAAU,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACnE,UAAE;AACA,gBAAU,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC/C,gBAAU,aAAa,KAAK,IAAI,IAAI;AACpC,aAAO,WAAW,KAAK,SAAS;AAChC,UAAI,OAAO,WAAW,SAAS,KAAK,eAAe;AACjD,eAAO,WAAW,OAAO,GAAG,OAAO,WAAW,SAAS,KAAK,aAAa;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACF;;;ACzHA,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,aAAa,EAAE,UAAU,MAAM,OAAO,CAAC,GAAG,aAAa,CAAC,EAAE;AAsBhE,SAAS,IAAI,QAAwB;AACnC,QAAM,IAAS;AACf,MAAI,EAAE,QAAQ,WAAY,QAAO,GAAG,MAAM,IAAI,EAAE,OAAO,WAAW,CAAC;AACnE,SAAO,GAAG,MAAM,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AACxF;AAkBO,IAAM,eAAN,MAA0C;AAAA,EAO/C,YAAY,MAKT;AACD,SAAK,SAAS,KAAK;AACnB,SAAK,SAAS,KAAK;AACnB,SAAK,aAAa,KAAK,SAAS,cAAc;AAC9C,SAAK,QAAQ,IAAI,mBAAmB,EAAE,eAAe,KAAK,SAAS,cAAc,CAAC;AAClF,SAAK,OAAO,KAAK;AAAA,EACnB;AAAA;AAAA,EAIA,MAAM,SAAS,MAAc,UAAuB,SAAoC;AACtF,UAAM,UAAU,KAAK,KAAK,MAAM,SAAS,UAAU;AAEnD,QAAI,SAAS,SAAS,QAAQ;AAC5B,UAAI,KAAK,KAAM,OAAM,KAAK,KAAK,SAAS,MAAM,UAAU,OAAO;AAAA,UAC1D,MAAK,QAAQ;AAAA,QAChB,+CAA+C,IAAI;AAAA,MACrD;AAEA,YAAM,KAAK,MAAM,SAAS,MAAM,UAAU,OAAO;AAAA,IACnD,OAAO;AACL,YAAM,KAAK,MAAM,SAAS,MAAM,UAAU,OAAO;AAAA,IACnD;AAEA,UAAM,KAAK,aAAa,MAAM,UAAU,IAAI;AAAA,EAC9C;AAAA,EAEA,MAAM,OAAO,MAA6B;AACxC,UAAM,KAAK,MAAM,OAAO,IAAI;AAC5B,QAAI,KAAK,QAAQ,OAAO,KAAK,KAAK,WAAW,YAAY;AACvD,UAAI;AAAE,cAAM,KAAK,KAAK,OAAO,IAAI;AAAA,MAAG,QAAQ;AAAA,MAAe;AAAA,IAC7D;AACA,UAAM,KAAK,UAAU,MAAM,KAAK;AAAA,EAClC;AAAA,EAEA,MAAM,QAAQ,MAAc,MAA+B;AACzD,UAAM,KAAK,MAAM,QAAQ,MAAM,IAAI;AAAA,EACrC;AAAA,EAEA,MAAM,cAAc,MAAc,OAAyC;AACzE,WAAO,KAAK,MAAM,cAAc,MAAM,KAAK;AAAA,EAC7C;AAAA,EAEA,MAAM,WAA8B;AAClC,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B;AAAA,EAEA,MAAM,OAAO,MAAc,MAA+B;AAExD,UAAM,WAAY,KAAK,MAAc,MAAM,MAAM,IAAI;AACrD,QAAI,CAAC,SAAU,OAAM,IAAI,MAAM,QAAQ,IAAI,aAAa;AAIxD,UAAM,QAAQ,MAAM,KAAK,SAAS,MAAM,QAAQ;AAChD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,MAAM,IAAI;AAEnC,YAAM,KAAK,UAAU,OAAO,SAAS;AAAA,IACvC,SAAS,KAAK;AACZ,YAAM,KAAK,UAAU,OAAO,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AACtF,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,uBACJ,QACA,OACyB;AACzB,UAAM,OAAO,MAAM,KAAK,OAAO,KAAK,WAAW;AAAA,MAC7C,OAAO,EAAE,OAAO;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB,SAAS,CAAC,EAAE,OAAO,cAAc,OAAO,OAAO,CAAC;AAAA,MAChD,SAAS;AAAA,IACX,CAAC;AACD,YAAQ,QAAQ,CAAC,GAAG,IAAI,CAAC,OAAY;AAAA,MACnC,OAAO,OAAO,EAAE,QAAQ;AAAA,MACxB,QAAQ,EAAE;AAAA,MACV,WAAW,EAAE;AAAA,MACb,aAAa,EAAE,gBAAgB;AAAA,MAC/B,YAAY,EAAE,eAAe;AAAA,MAC7B,OAAO,EAAE,SAAS;AAAA,IACpB,EAAE;AAAA,EACJ;AAAA,EAEA,MAAM,UAAyB;AAC7B,UAAM,KAAK,MAAM,QAAQ;AAAA,EAC3B;AAAA;AAAA,EAIQ,KAAK,MAAc,SAAqB,gBAA8D;AAC5G,WAAO,OAAO,QAAQ;AACpB,YAAM,QAAQ,KAAK,aAAa,MAAM,KAAK,SAAS,MAAM,cAAc,IAAI;AAC5E,YAAM,UAAU,KAAK,IAAI;AACzB,UAAI;AACF,cAAM,QAAQ,GAAG;AACjB,YAAI,MAAO,OAAM,KAAK,UAAU,OAAO,WAAW,QAAW,KAAK,IAAI,IAAI,OAAO;AACjF,cAAM,KAAK,QAAQ,MAAM,SAAS;AAAA,MACpC,SAAS,KAAK;AACZ,cAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,YAAI,MAAO,OAAM,KAAK,UAAU,OAAO,UAAU,KAAK,KAAK,IAAI,IAAI,OAAO;AAC1E,cAAM,KAAK,QAAQ,MAAM,UAAU,GAAG;AACtC,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,SAAS,SAAiB,SAAwE;AAC9G,UAAM,KAAK,IAAI,KAAK;AACpB,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,QAAI;AACF,YAAM,KAAK,OAAO,OAAO,WAAW;AAAA,QAClC;AAAA,QACA,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,YAAY;AAAA,QACZ;AAAA,QACA,SAAS;AAAA,QACT,YAAY;AAAA,MACd,GAAG,EAAE,SAAS,WAAW,CAAC;AAC1B,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,WAAK,QAAQ,OAAO,8CAA8C,GAAU;AAC5E,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAc,UACZ,IACA,QACA,OACA,YACe;AACf,QAAI,CAAC,GAAI;AACT,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,QAAI;AACF,YAAM,KAAK,OAAO,OAAO,WAAW;AAAA,QAClC;AAAA,QACA;AAAA,QACA,cAAc;AAAA,QACd,aAAa;AAAA,QACb,OAAO,SAAS;AAAA,MAClB,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,IAC5B,SAAS,KAAK;AACZ,WAAK,QAAQ,OAAO,8CAA8C,GAAU;AAAA,IAC9E;AAAA,EACF;AAAA,EAEA,MAAc,aAAa,MAAc,UAAuB,QAAgC;AAC9F,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,aACJ,SAAS,eAAe,SAAS,cAAc,OAAO,OAAO,SAAS,UAAU,IAAI,SAAS;AAC/F,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,KAAK,WAAW;AAAA,QACjD,OAAO,EAAE,KAAK;AAAA,QACd,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AACD,YAAM,MAAM,WAAW,CAAC;AACxB,UAAI,KAAK;AACP,cAAM,KAAK,OAAO,OAAO,WAAW;AAAA,UAClC,IAAI,IAAI;AAAA,UACR,eAAe,SAAS;AAAA,UACxB,qBAAqB,cAAc;AAAA,UACnC,UAAU,SAAS,YAAY;AAAA,UAC/B;AAAA,UACA,YAAY;AAAA,QACd,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,MAC5B,OAAO;AACL,cAAM,KAAK,OAAO,OAAO,WAAW;AAAA,UAClC,IAAI,IAAI,KAAK;AAAA,UACb;AAAA,UACA,eAAe,SAAS;AAAA,UACxB,qBAAqB,cAAc;AAAA,UACnC,UAAU,SAAS,YAAY;AAAA,UAC/B;AAAA,UACA,WAAW;AAAA,UACX,eAAe;AAAA,UACf,YAAY;AAAA,UACZ,YAAY;AAAA,QACd,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,MAC5B;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,QAAQ,OAAO,0CAA0C,GAAU;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,MAAc,UAAU,MAAc,QAAgC;AACpE,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,KAAK,WAAW;AAAA,QACjD,OAAO,EAAE,KAAK;AAAA,QACd,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AACD,YAAM,MAAM,WAAW,CAAC;AACxB,UAAI,CAAC,IAAK;AACV,YAAM,KAAK,OAAO,OAAO,WAAW;AAAA,QAClC,IAAI,IAAI;AAAA,QACR;AAAA,QACA,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACrC,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,IAC5B,SAAS,KAAK;AACZ,WAAK,QAAQ,OAAO,kCAAkC,GAAU;AAAA,IAClE;AAAA,EACF;AAAA,EAEA,MAAc,QAAQ,MAAc,aAAmC,YAAoC;AACzG,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,KAAK,WAAW;AAAA,QACjD,OAAO,EAAE,KAAK;AAAA,QACd,OAAO;AAAA,QACP,SAAS;AAAA,MACX,CAAC;AACD,YAAM,MAAM,WAAW,CAAC;AACxB,UAAI,CAAC,IAAK;AACV,YAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,YAAM,KAAK,OAAO,OAAO,WAAW;AAAA,QAClC,IAAI,IAAI;AAAA,QACR,aAAa;AAAA,QACb;AAAA,QACA,YAAY,gBAAgB,WAAY,cAAc,OAAQ;AAAA,QAC9D,YAAY,IAAI,aAAa,KAAK;AAAA,QAClC,gBAAgB,IAAI,iBAAiB,MAAM,gBAAgB,WAAW,IAAI;AAAA,QAC1E,YAAY;AAAA,MACd,GAAG,EAAE,SAAS,WAAW,CAAC;AAAA,IAC5B,SAAS,KAAK;AACZ,WAAK,QAAQ,OAAO,gCAAgC,GAAU;AAAA,IAChE;AAAA,EACF;AACF;;;ACtSA,IAAMA,aAAY;AAClB,IAAMC,cAAa,EAAE,UAAU,MAAM,OAAO,CAAC,GAAG,aAAa,CAAC,EAAE;AASzD,IAAM,iCAAiC;AAOvC,IAAM,2BAA2B,IAAI;AAiCrC,IAAM,kBAAN,MAAsB;AAAA,EAK3B,YAA6B,MAA8B;AAA9B;AAC3B,SAAK,MAAM,KAAK,QAAQ,MAAM,KAAK,IAAI;AACvC,SAAK,SAAS,KAAK,UAAUD;AAC7B,SAAK,UAAU,KAAK,WAAW;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAM,eAAoD;AAC9D,UAAM,SAAS,KAAK,KAAK,UAAU;AACnC,QAAI,CAAC,UAAU,OAAO,OAAO,WAAW,YAAY;AAClD,WAAK,KAAK,OAAO,KAAK,0DAA0D;AAChF,aAAO,EAAE,QAAQ,KAAK,QAAQ,SAAS,EAAE;AAAA,IAC3C;AACA,QAAI,EAAE,gBAAgB,IAAI;AACxB,WAAK,KAAK,OAAO,KAAK,0CAA0C,aAAa,iBAAiB;AAC9F,aAAO,EAAE,QAAQ,KAAK,QAAQ,SAAS,EAAE;AAAA,IAC3C;AAEA,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,gBAAgB,KAAU,EAAE,YAAY;AAChF,QAAI;AACF,YAAM,MAAM,MAAM,OAAO,OAAO,KAAK,QAAQ;AAAA,QAC3C,OAAO,EAAE,CAAC,KAAK,OAAO,GAAG,EAAE,KAAK,UAAU,EAAE;AAAA,QAC5C,OAAO;AAAA,QACP,SAASC;AAAA,MACX,CAAC;AACD,YAAM,UAAU,aAAa,GAAG;AAChC,UAAI,YAAY,UAAa,UAAU,GAAG;AACxC,aAAK,KAAK,OAAO;AAAA,UACf,2BAA2B,WAAW,GAAG,IAAI,KAAK,MAAM,oBAAoB,SAAS;AAAA,QACvF;AAAA,MACF;AACA,aAAO,EAAE,QAAQ,KAAK,QAAQ,QAAQ;AAAA,IACxC,SAAS,KAAK;AACZ,YAAM,MAAO,KAAe,WAAW,OAAO,GAAG;AACjD,WAAK,KAAK,OAAO,KAAK,6BAA6B,KAAK,MAAM,YAAY,GAAG,GAAG;AAChF,aAAO,EAAE,QAAQ,KAAK,QAAQ,OAAO,IAAI;AAAA,IAC3C;AAAA,EACF;AACF;AAGA,SAAS,aAAa,KAAkC;AACtD,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,MAAM,QAAQ,GAAG,EAAG,QAAO,IAAI;AACnC,MAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,UAAM,IAAI;AACV,eAAW,KAAK,CAAC,gBAAgB,WAAW,SAAS,YAAY,cAAc,GAAG;AAChF,UAAI,OAAO,EAAE,CAAC,MAAM,SAAU,QAAO,EAAE,CAAC;AAAA,IAC1C;AAAA,EACF;AACA,SAAO;AACT;;;AJ5DO,IAAM,mBAAN,MAAyC;AAAA,EAU9C,YAAY,UAAmC,CAAC,GAAG;AATnD,gBAAO;AACP,mBAAU;AACV,gBAAO;AAQL,SAAK,UAAU;AAAA,MACb,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,kBAAkB;AAAA,MAClB,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,KAAmC;AAE5C,QAAI;AACF,UAAI,WAAuC,UAAU,EAAE,SAAS;AAAA,QAC9D,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,SAAS;AAAA,QACT,MAAM;AAAA,QACN,OAAO;AAAA,QACP,mBAAmB;AAAA,QACnB,WAAW;AAAA,QACX,SAAS,CAAC,QAAQ,SAAS;AAAA,MAC7B,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,UAAI,OAAO,KAAK,sFAAsF,GAAU;AAAA,IAClH;AAEA,UAAM,SAAS,KAAK,QAAQ,WAAW;AAEvC,QAAI,WAAW,YAAY;AACzB,WAAK,kBAAkB,IAAI,mBAAmB,KAAK,QAAQ,QAAQ;AACnE,UAAI,gBAAgB,OAAO,KAAK,eAAe;AAC/C,UAAI,OAAO,KAAK,6DAA6D;AAC7E;AAAA,IACF;AAEA,QAAI,WAAW,QAAQ;AACrB,YAAM,OAAO,IAAI,eAAe,EAAE,UAAU,MAAM,CAAC;AACnD,UAAI,gBAAgB,OAAO,IAAI;AAC/B,UAAI,OAAO,KAAK,6CAA6C;AAC7D;AAAA,IACF;AAKA,SAAK,kBAAkB,IAAI,mBAAmB,KAAK,QAAQ,QAAQ;AACnE,QAAI,gBAAgB,OAAO,KAAK,eAAe;AAE/C,QAAI,KAAK,gBAAgB,YAAY;AACnC,UAAI,SAAc;AAClB,UAAI;AAAE,iBAAS,IAAI,WAAgB,UAAU;AAAA,MAAG,QAC1C;AAAE,YAAI;AAAE,mBAAS,IAAI,WAAgB,MAAM;AAAA,QAAG,QAAQ;AAAA,QAAe;AAAA,MAAE;AAE7E,UAAI,CAAC,QAAQ;AACX,YAAI,WAAW,MAAM;AACnB,cAAI,OAAO,KAAK,oGAA+F;AAAA,QACjH,OAAO;AACL,cAAI,OAAO,KAAK,2EAAsE;AAAA,QACxF;AACA;AAAA,MACF;AAGA,UAAI;AACJ,UAAI,KAAK,QAAQ,eAAe,OAAO;AACrC,YAAI;AACF,iBAAO,IAAI,eAAe,EAAE,UAAU,MAAM,CAAC;AAAA,QAC/C,SAAS,KAAK;AACZ,cAAI,OAAO,KAAK,2EAA2E,GAAU;AAAA,QACvG;AAAA,MACF;AAEA,WAAK,YAAY,IAAI,aAAa;AAAA,QAChC;AAAA,QACA,QAAQ,IAAI;AAAA,QACZ,SAAS,KAAK,QAAQ;AAAA,QACtB;AAAA,MACF,CAAC;AAED,UAAI;AACF,QAAC,IAAY,iBAAiB,OAAO,KAAK,SAAS;AACnD,YAAI,OAAO,KAAK,gFAAgF;AAAA,MAClG,SAAS,KAAK;AACZ,YAAI,OAAO,KAAK,0EAA0E,GAAU;AAAA,MACtG;AAOA,YAAM,gBAAgB,KAAK,QAAQ,iBAAiB;AACpD,UAAI,gBAAgB,GAAG;AACrB,cAAM,YAAY,IAAI,gBAAgB;AAAA,UACpC,WAAW,MAAM;AAAA,UACjB,QAAQ,IAAI;AAAA,QACd,CAAC;AACD,cAAM,UAAU,KAAK,QAAQ,oBAAoB;AACjD,cAAM,QAAQ,MAAM;AAClB,eAAK,UAAU,MAAM,aAAa,EAAE;AAAA,YAAM,CAAC,QACzC,IAAI,OAAO,KAAK,6CAA8C,KAAe,WAAW,GAAG,EAAE;AAAA,UAC/F;AAAA,QACF;AACA,cAAM;AACN,aAAK,iBAAiB,YAAY,OAAO,OAAO;AAChD,aAAK,eAAe,QAAQ;AAC5B,YAAI,OAAO;AAAA,UACT,uDAAuD,aAAa,WAAW,KAAK,MAAM,UAAU,GAAI,CAAC;AAAA,QAC3G;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,gBAAgB;AACvB,oBAAc,KAAK,cAAc;AACjC,WAAK,iBAAiB;AAAA,IACxB;AACA,UAAM,KAAK,WAAW,QAAQ;AAC9B,UAAM,KAAK,iBAAiB,QAAQ;AAAA,EACtC;AACF;","names":["RUN_TABLE","SYSTEM_CTX"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/service-job",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.5.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling",
|
|
6
6
|
"type": "module",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"croner": "^10.0.1",
|
|
18
|
-
"@objectstack/core": "9.
|
|
19
|
-
"@objectstack/platform-objects": "9.
|
|
20
|
-
"@objectstack/spec": "9.
|
|
18
|
+
"@objectstack/core": "9.5.1",
|
|
19
|
+
"@objectstack/platform-objects": "9.5.1",
|
|
20
|
+
"@objectstack/spec": "9.5.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^25.9.2",
|