@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 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.execute(record);
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.execute(record);
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.execute(record);
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
  }
@@ -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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,mBAAkC;;;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,oBAAqB;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,qBAAQ,sBAAS;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/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.execute(record);
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.execute(record);
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.execute(record);
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":"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","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.0.0",
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.0.0",
19
- "@objectstack/platform-objects": "10.0.0",
20
- "@objectstack/spec": "10.0.0"
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",