@objectstack/service-job 10.0.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +39 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +39 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -123,6 +123,8 @@ var CronJobAdapter = class {
|
|
|
123
123
|
this.jobs = /* @__PURE__ */ new Map();
|
|
124
124
|
this.defaultTimezone = options.timezone ?? "UTC";
|
|
125
125
|
this.maxExecutions = options.maxExecutions ?? 100;
|
|
126
|
+
this.cluster = options.cluster;
|
|
127
|
+
this.leaseMs = options.leaseMs ?? 6e4;
|
|
126
128
|
}
|
|
127
129
|
async schedule(name, schedule, handler) {
|
|
128
130
|
await this.cancel(name);
|
|
@@ -135,13 +137,13 @@ var CronJobAdapter = class {
|
|
|
135
137
|
schedule.expression,
|
|
136
138
|
{ timezone: schedule.timezone ?? this.defaultTimezone, name },
|
|
137
139
|
async () => {
|
|
138
|
-
await this.
|
|
140
|
+
await this.runScheduled(name);
|
|
139
141
|
}
|
|
140
142
|
);
|
|
141
143
|
record.task = task;
|
|
142
144
|
} else if (schedule.type === "interval" && schedule.intervalMs) {
|
|
143
145
|
const handle = setInterval(() => {
|
|
144
|
-
void this.
|
|
146
|
+
void this.runScheduled(name);
|
|
145
147
|
}, schedule.intervalMs);
|
|
146
148
|
handle?.unref?.();
|
|
147
149
|
record.task = { stop: () => clearInterval(handle) };
|
|
@@ -149,7 +151,7 @@ var CronJobAdapter = class {
|
|
|
149
151
|
const delay = new Date(schedule.at).getTime() - Date.now();
|
|
150
152
|
if (delay > 0) {
|
|
151
153
|
const handle = setTimeout(() => {
|
|
152
|
-
void this.
|
|
154
|
+
void this.runScheduled(name);
|
|
153
155
|
}, delay);
|
|
154
156
|
handle?.unref?.();
|
|
155
157
|
record.task = { stop: () => clearTimeout(handle) };
|
|
@@ -190,6 +192,31 @@ var CronJobAdapter = class {
|
|
|
190
192
|
}
|
|
191
193
|
this.jobs.clear();
|
|
192
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Run a SCHEDULED fire of `name` under cluster leader-election: only the node
|
|
197
|
+
* that acquires the per-job lock runs the handler; peers skip. No cluster /
|
|
198
|
+
* in-memory driver => lock always granted => single-node unchanged. Manual
|
|
199
|
+
* `trigger()` bypasses this.
|
|
200
|
+
*/
|
|
201
|
+
async runScheduled(name) {
|
|
202
|
+
const record = this.jobs.get(name);
|
|
203
|
+
if (!record) return;
|
|
204
|
+
const lock = this.cluster?.lock;
|
|
205
|
+
if (!lock) {
|
|
206
|
+
await this.execute(record);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const handle = await lock.acquire(`job:${name}`, { ttlMs: this.leaseMs, waitMs: 0 });
|
|
210
|
+
if (!handle) return;
|
|
211
|
+
try {
|
|
212
|
+
await this.execute(record);
|
|
213
|
+
} finally {
|
|
214
|
+
try {
|
|
215
|
+
await handle.release();
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
193
220
|
async execute(record, data) {
|
|
194
221
|
const execution = {
|
|
195
222
|
jobId: record.name,
|
|
@@ -487,6 +514,13 @@ function countDeleted(res) {
|
|
|
487
514
|
}
|
|
488
515
|
|
|
489
516
|
// src/job-service-plugin.ts
|
|
517
|
+
function getClusterSafe(ctx) {
|
|
518
|
+
try {
|
|
519
|
+
return ctx.getService("cluster");
|
|
520
|
+
} catch {
|
|
521
|
+
return void 0;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
490
524
|
var JobServicePlugin = class {
|
|
491
525
|
constructor(options = {}) {
|
|
492
526
|
this.name = "com.objectstack.service.job";
|
|
@@ -523,7 +557,7 @@ var JobServicePlugin = class {
|
|
|
523
557
|
return;
|
|
524
558
|
}
|
|
525
559
|
if (choice === "cron") {
|
|
526
|
-
const cron = new CronJobAdapter({ timezone: "UTC" });
|
|
560
|
+
const cron = new CronJobAdapter({ timezone: "UTC", cluster: getClusterSafe(ctx) });
|
|
527
561
|
ctx.registerService("job", cron);
|
|
528
562
|
ctx.logger.info("JobServicePlugin: registered CronJobAdapter");
|
|
529
563
|
return;
|
|
@@ -551,7 +585,7 @@ var JobServicePlugin = class {
|
|
|
551
585
|
let cron;
|
|
552
586
|
if (this.options.enableCron !== false) {
|
|
553
587
|
try {
|
|
554
|
-
cron = new CronJobAdapter({ timezone: "UTC" });
|
|
588
|
+
cron = new CronJobAdapter({ timezone: "UTC", cluster: getClusterSafe(ctx) });
|
|
555
589
|
} catch (err) {
|
|
556
590
|
ctx.logger.warn("JobServicePlugin: cron adapter init failed; cron jobs will not auto-run", err);
|
|
557
591
|
}
|
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","../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"]}
|
|
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 */\n/** Resolve the cluster service if present; undefined on single-node. */\nfunction getClusterSafe(ctx: any): any {\n try { return ctx.getService('cluster'); } catch { return undefined; }\n}\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', cluster: getClusterSafe(ctx) });\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', cluster: getClusterSafe(ctx) });\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/** Minimal cluster lock surface for scheduler leader-election (structural — no hard dep on the cluster contract). */\ninterface SchedulerCluster {\n lock?: {\n acquire(key: string, opts?: { ttlMs?: number; waitMs?: number }): Promise<{ release(): Promise<void> } | null>;\n };\n}\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 /** Cluster service for scheduler leader-election. With a remote driver only ONE\n * node fires each scheduled job; with the in-memory driver the lock always\n * succeeds so single-node behaviour is unchanged. */\n cluster?: SchedulerCluster;\n /** Lease TTL (ms) held while a scheduled fire runs. Default 60000. */\n leaseMs?: 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 private readonly cluster?: SchedulerCluster;\n private readonly leaseMs: number;\n\n constructor(options: CronJobAdapterOptions = {}) {\n this.defaultTimezone = options.timezone ?? 'UTC';\n this.maxExecutions = options.maxExecutions ?? 100;\n this.cluster = options.cluster;\n this.leaseMs = options.leaseMs ?? 60_000;\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.runScheduled(name); },\n );\n record.task = task;\n } else if (schedule.type === 'interval' && schedule.intervalMs) {\n const handle = setInterval(() => { void this.runScheduled(name); }, 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.runScheduled(name); }, 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 /**\n * Run a SCHEDULED fire of `name` under cluster leader-election: only the node\n * that acquires the per-job lock runs the handler; peers skip. No cluster /\n * in-memory driver => lock always granted => single-node unchanged. Manual\n * `trigger()` bypasses this.\n */\n private async runScheduled(name: string): Promise<void> {\n const record = this.jobs.get(name);\n if (!record) return;\n const lock = this.cluster?.lock;\n if (!lock) { await this.execute(record); return; }\n const handle = await lock.acquire(`job:${name}`, { ttlMs: this.leaseMs, waitMs: 0 });\n if (!handle) return; // another node is the leader for this fire\n try {\n await this.execute(record);\n } finally {\n try { await handle.release(); } catch { /* ignore */ }\n }\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
|
@@ -94,9 +94,6 @@ declare class DbJobAdapter implements IJobService {
|
|
|
94
94
|
private bumpJob;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/**
|
|
98
|
-
* Configuration options for the JobServicePlugin.
|
|
99
|
-
*/
|
|
100
97
|
interface JobServicePluginOptions {
|
|
101
98
|
/**
|
|
102
99
|
* Job adapter type.
|
|
@@ -145,6 +142,17 @@ declare class JobServicePlugin implements Plugin {
|
|
|
145
142
|
destroy(): Promise<void>;
|
|
146
143
|
}
|
|
147
144
|
|
|
145
|
+
/** Minimal cluster lock surface for scheduler leader-election (structural — no hard dep on the cluster contract). */
|
|
146
|
+
interface SchedulerCluster {
|
|
147
|
+
lock?: {
|
|
148
|
+
acquire(key: string, opts?: {
|
|
149
|
+
ttlMs?: number;
|
|
150
|
+
waitMs?: number;
|
|
151
|
+
}): Promise<{
|
|
152
|
+
release(): Promise<void>;
|
|
153
|
+
} | null>;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
148
156
|
/**
|
|
149
157
|
* Configuration for the cron-based job adapter.
|
|
150
158
|
*/
|
|
@@ -153,6 +161,12 @@ interface CronJobAdapterOptions {
|
|
|
153
161
|
timezone?: string;
|
|
154
162
|
/** Maximum execution history per job (default: 100) */
|
|
155
163
|
maxExecutions?: number;
|
|
164
|
+
/** Cluster service for scheduler leader-election. With a remote driver only ONE
|
|
165
|
+
* node fires each scheduled job; with the in-memory driver the lock always
|
|
166
|
+
* succeeds so single-node behaviour is unchanged. */
|
|
167
|
+
cluster?: SchedulerCluster;
|
|
168
|
+
/** Lease TTL (ms) held while a scheduled fire runs. Default 60000. */
|
|
169
|
+
leaseMs?: number;
|
|
156
170
|
}
|
|
157
171
|
/**
|
|
158
172
|
* Cron-based job adapter implementing IJobService using the `croner`
|
|
@@ -165,6 +179,8 @@ declare class CronJobAdapter implements IJobService {
|
|
|
165
179
|
private readonly defaultTimezone;
|
|
166
180
|
private readonly maxExecutions;
|
|
167
181
|
private readonly jobs;
|
|
182
|
+
private readonly cluster?;
|
|
183
|
+
private readonly leaseMs;
|
|
168
184
|
constructor(options?: CronJobAdapterOptions);
|
|
169
185
|
schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;
|
|
170
186
|
cancel(name: string): Promise<void>;
|
|
@@ -173,6 +189,13 @@ declare class CronJobAdapter implements IJobService {
|
|
|
173
189
|
listJobs(): Promise<string[]>;
|
|
174
190
|
/** Stop all timers — call from plugin destroy. */
|
|
175
191
|
destroy(): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Run a SCHEDULED fire of `name` under cluster leader-election: only the node
|
|
194
|
+
* that acquires the per-job lock runs the handler; peers skip. No cluster /
|
|
195
|
+
* in-memory driver => lock always granted => single-node unchanged. Manual
|
|
196
|
+
* `trigger()` bypasses this.
|
|
197
|
+
*/
|
|
198
|
+
private runScheduled;
|
|
176
199
|
private execute;
|
|
177
200
|
}
|
|
178
201
|
|
package/dist/index.d.ts
CHANGED
|
@@ -94,9 +94,6 @@ declare class DbJobAdapter implements IJobService {
|
|
|
94
94
|
private bumpJob;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
/**
|
|
98
|
-
* Configuration options for the JobServicePlugin.
|
|
99
|
-
*/
|
|
100
97
|
interface JobServicePluginOptions {
|
|
101
98
|
/**
|
|
102
99
|
* Job adapter type.
|
|
@@ -145,6 +142,17 @@ declare class JobServicePlugin implements Plugin {
|
|
|
145
142
|
destroy(): Promise<void>;
|
|
146
143
|
}
|
|
147
144
|
|
|
145
|
+
/** Minimal cluster lock surface for scheduler leader-election (structural — no hard dep on the cluster contract). */
|
|
146
|
+
interface SchedulerCluster {
|
|
147
|
+
lock?: {
|
|
148
|
+
acquire(key: string, opts?: {
|
|
149
|
+
ttlMs?: number;
|
|
150
|
+
waitMs?: number;
|
|
151
|
+
}): Promise<{
|
|
152
|
+
release(): Promise<void>;
|
|
153
|
+
} | null>;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
148
156
|
/**
|
|
149
157
|
* Configuration for the cron-based job adapter.
|
|
150
158
|
*/
|
|
@@ -153,6 +161,12 @@ interface CronJobAdapterOptions {
|
|
|
153
161
|
timezone?: string;
|
|
154
162
|
/** Maximum execution history per job (default: 100) */
|
|
155
163
|
maxExecutions?: number;
|
|
164
|
+
/** Cluster service for scheduler leader-election. With a remote driver only ONE
|
|
165
|
+
* node fires each scheduled job; with the in-memory driver the lock always
|
|
166
|
+
* succeeds so single-node behaviour is unchanged. */
|
|
167
|
+
cluster?: SchedulerCluster;
|
|
168
|
+
/** Lease TTL (ms) held while a scheduled fire runs. Default 60000. */
|
|
169
|
+
leaseMs?: number;
|
|
156
170
|
}
|
|
157
171
|
/**
|
|
158
172
|
* Cron-based job adapter implementing IJobService using the `croner`
|
|
@@ -165,6 +179,8 @@ declare class CronJobAdapter implements IJobService {
|
|
|
165
179
|
private readonly defaultTimezone;
|
|
166
180
|
private readonly maxExecutions;
|
|
167
181
|
private readonly jobs;
|
|
182
|
+
private readonly cluster?;
|
|
183
|
+
private readonly leaseMs;
|
|
168
184
|
constructor(options?: CronJobAdapterOptions);
|
|
169
185
|
schedule(name: string, schedule: JobSchedule, handler: JobHandler): Promise<void>;
|
|
170
186
|
cancel(name: string): Promise<void>;
|
|
@@ -173,6 +189,13 @@ declare class CronJobAdapter implements IJobService {
|
|
|
173
189
|
listJobs(): Promise<string[]>;
|
|
174
190
|
/** Stop all timers — call from plugin destroy. */
|
|
175
191
|
destroy(): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* Run a SCHEDULED fire of `name` under cluster leader-election: only the node
|
|
194
|
+
* that acquires the per-job lock runs the handler; peers skip. No cluster /
|
|
195
|
+
* in-memory driver => lock always granted => single-node unchanged. Manual
|
|
196
|
+
* `trigger()` bypasses this.
|
|
197
|
+
*/
|
|
198
|
+
private runScheduled;
|
|
176
199
|
private execute;
|
|
177
200
|
}
|
|
178
201
|
|
package/dist/index.js
CHANGED
|
@@ -91,6 +91,8 @@ var CronJobAdapter = class {
|
|
|
91
91
|
this.jobs = /* @__PURE__ */ new Map();
|
|
92
92
|
this.defaultTimezone = options.timezone ?? "UTC";
|
|
93
93
|
this.maxExecutions = options.maxExecutions ?? 100;
|
|
94
|
+
this.cluster = options.cluster;
|
|
95
|
+
this.leaseMs = options.leaseMs ?? 6e4;
|
|
94
96
|
}
|
|
95
97
|
async schedule(name, schedule, handler) {
|
|
96
98
|
await this.cancel(name);
|
|
@@ -103,13 +105,13 @@ var CronJobAdapter = class {
|
|
|
103
105
|
schedule.expression,
|
|
104
106
|
{ timezone: schedule.timezone ?? this.defaultTimezone, name },
|
|
105
107
|
async () => {
|
|
106
|
-
await this.
|
|
108
|
+
await this.runScheduled(name);
|
|
107
109
|
}
|
|
108
110
|
);
|
|
109
111
|
record.task = task;
|
|
110
112
|
} else if (schedule.type === "interval" && schedule.intervalMs) {
|
|
111
113
|
const handle = setInterval(() => {
|
|
112
|
-
void this.
|
|
114
|
+
void this.runScheduled(name);
|
|
113
115
|
}, schedule.intervalMs);
|
|
114
116
|
handle?.unref?.();
|
|
115
117
|
record.task = { stop: () => clearInterval(handle) };
|
|
@@ -117,7 +119,7 @@ var CronJobAdapter = class {
|
|
|
117
119
|
const delay = new Date(schedule.at).getTime() - Date.now();
|
|
118
120
|
if (delay > 0) {
|
|
119
121
|
const handle = setTimeout(() => {
|
|
120
|
-
void this.
|
|
122
|
+
void this.runScheduled(name);
|
|
121
123
|
}, delay);
|
|
122
124
|
handle?.unref?.();
|
|
123
125
|
record.task = { stop: () => clearTimeout(handle) };
|
|
@@ -158,6 +160,31 @@ var CronJobAdapter = class {
|
|
|
158
160
|
}
|
|
159
161
|
this.jobs.clear();
|
|
160
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Run a SCHEDULED fire of `name` under cluster leader-election: only the node
|
|
165
|
+
* that acquires the per-job lock runs the handler; peers skip. No cluster /
|
|
166
|
+
* in-memory driver => lock always granted => single-node unchanged. Manual
|
|
167
|
+
* `trigger()` bypasses this.
|
|
168
|
+
*/
|
|
169
|
+
async runScheduled(name) {
|
|
170
|
+
const record = this.jobs.get(name);
|
|
171
|
+
if (!record) return;
|
|
172
|
+
const lock = this.cluster?.lock;
|
|
173
|
+
if (!lock) {
|
|
174
|
+
await this.execute(record);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const handle = await lock.acquire(`job:${name}`, { ttlMs: this.leaseMs, waitMs: 0 });
|
|
178
|
+
if (!handle) return;
|
|
179
|
+
try {
|
|
180
|
+
await this.execute(record);
|
|
181
|
+
} finally {
|
|
182
|
+
try {
|
|
183
|
+
await handle.release();
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
161
188
|
async execute(record, data) {
|
|
162
189
|
const execution = {
|
|
163
190
|
jobId: record.name,
|
|
@@ -455,6 +482,13 @@ function countDeleted(res) {
|
|
|
455
482
|
}
|
|
456
483
|
|
|
457
484
|
// src/job-service-plugin.ts
|
|
485
|
+
function getClusterSafe(ctx) {
|
|
486
|
+
try {
|
|
487
|
+
return ctx.getService("cluster");
|
|
488
|
+
} catch {
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
458
492
|
var JobServicePlugin = class {
|
|
459
493
|
constructor(options = {}) {
|
|
460
494
|
this.name = "com.objectstack.service.job";
|
|
@@ -491,7 +525,7 @@ var JobServicePlugin = class {
|
|
|
491
525
|
return;
|
|
492
526
|
}
|
|
493
527
|
if (choice === "cron") {
|
|
494
|
-
const cron = new CronJobAdapter({ timezone: "UTC" });
|
|
528
|
+
const cron = new CronJobAdapter({ timezone: "UTC", cluster: getClusterSafe(ctx) });
|
|
495
529
|
ctx.registerService("job", cron);
|
|
496
530
|
ctx.logger.info("JobServicePlugin: registered CronJobAdapter");
|
|
497
531
|
return;
|
|
@@ -519,7 +553,7 @@ var JobServicePlugin = class {
|
|
|
519
553
|
let cron;
|
|
520
554
|
if (this.options.enableCron !== false) {
|
|
521
555
|
try {
|
|
522
|
-
cron = new CronJobAdapter({ timezone: "UTC" });
|
|
556
|
+
cron = new CronJobAdapter({ timezone: "UTC", cluster: getClusterSafe(ctx) });
|
|
523
557
|
} catch (err) {
|
|
524
558
|
ctx.logger.warn("JobServicePlugin: cron adapter init failed; cron jobs will not auto-run", err);
|
|
525
559
|
}
|
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","../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"]}
|
|
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 */\n/** Resolve the cluster service if present; undefined on single-node. */\nfunction getClusterSafe(ctx: any): any {\n try { return ctx.getService('cluster'); } catch { return undefined; }\n}\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', cluster: getClusterSafe(ctx) });\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', cluster: getClusterSafe(ctx) });\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/** Minimal cluster lock surface for scheduler leader-election (structural — no hard dep on the cluster contract). */\ninterface SchedulerCluster {\n lock?: {\n acquire(key: string, opts?: { ttlMs?: number; waitMs?: number }): Promise<{ release(): Promise<void> } | null>;\n };\n}\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 /** Cluster service for scheduler leader-election. With a remote driver only ONE\n * node fires each scheduled job; with the in-memory driver the lock always\n * succeeds so single-node behaviour is unchanged. */\n cluster?: SchedulerCluster;\n /** Lease TTL (ms) held while a scheduled fire runs. Default 60000. */\n leaseMs?: 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 private readonly cluster?: SchedulerCluster;\n private readonly leaseMs: number;\n\n constructor(options: CronJobAdapterOptions = {}) {\n this.defaultTimezone = options.timezone ?? 'UTC';\n this.maxExecutions = options.maxExecutions ?? 100;\n this.cluster = options.cluster;\n this.leaseMs = options.leaseMs ?? 60_000;\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.runScheduled(name); },\n );\n record.task = task;\n } else if (schedule.type === 'interval' && schedule.intervalMs) {\n const handle = setInterval(() => { void this.runScheduled(name); }, 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.runScheduled(name); }, 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 /**\n * Run a SCHEDULED fire of `name` under cluster leader-election: only the node\n * that acquires the per-job lock runs the handler; peers skip. No cluster /\n * in-memory driver => lock always granted => single-node unchanged. Manual\n * `trigger()` bypasses this.\n */\n private async runScheduled(name: string): Promise<void> {\n const record = this.jobs.get(name);\n if (!record) return;\n const lock = this.cluster?.lock;\n if (!lock) { await this.execute(record); return; }\n const handle = await lock.acquire(`job:${name}`, { ttlMs: this.leaseMs, waitMs: 0 });\n if (!handle) return; // another node is the leader for this fire\n try {\n await this.execute(record);\n } finally {\n try { await handle.release(); } catch { /* ignore */ }\n }\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/service-job",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
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": "10.
|
|
19
|
-
"@objectstack/platform-objects": "10.
|
|
20
|
-
"@objectstack/spec": "10.
|
|
18
|
+
"@objectstack/core": "10.2.0",
|
|
19
|
+
"@objectstack/platform-objects": "10.2.0",
|
|
20
|
+
"@objectstack/spec": "10.2.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^26.0.0",
|