Files
suaveplan/openspec/archive/2026-05-12-enterprise-server-job-scheduler/proposal.md
2026-05-27 23:29:53 +01:00

4.5 KiB

OpenSpec Change: enterprise-server-job-scheduler

  • Change ID: enterprise-server-job-scheduler
  • Package: @suaveplan/job-scheduler
  • Category: server/infra
  • Target path: server/infra/job-scheduler
  • Status: PROPOSED (amended 2026-05-11)
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/job-scheduler.md
  • Amendment 2026-05-11: Cron-expression parsing extracted to standalone @suaveplan/cron primitive (see openspec/changes/primitives-cron/). This package now depends on it instead of bundling its own cron/cron-parser module.

Summary

Deliver @suaveplan/job-scheduler on the genesis branch from scratch at enterprise quality. The package provides background job scheduling with cron expressions and immediate/delayed queuing. It exposes a JobScheduler with schedule(cronExpr, handler), queue(handler, options), cancel(jobId), pause / resume, and a JobRegistry for named jobs. Jobs run with configurable concurrency, retry-with-backoff via @suaveplan/retry, AbortSignal cancellation, missed-execution detection (catchup strategy), and OTel span per execution. Injectable Clock enables deterministic testing.


Current-state audit (genesis)

Does not exist on genesis — fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Typed JOB_SCHEDULER.* error codes
@suaveplan/types (workspace:^) Clock, LoggerLike
@suaveplan/events (workspace:^) Job lifecycle events
@suaveplan/retry (workspace:^) Per-job retry + backoff
@suaveplan/logger (workspace:^) Structured logs
@suaveplan/cron (workspace:^) Cron-expression parsing + nextRun(tz) (split out 2026-05-11)
zod (catalog:forms) Options validation
@opentelemetry/api (catalog:observability) Spans + counters + histograms
tslib (catalog:build) Runtime helpers

Public API surface

class JobScheduler {
  constructor(options?: JobSchedulerOptions)
  schedule(cronExpr: string, handler: JobHandler, options?: JobOptions): string   // returns jobId
  queue(handler: JobHandler, options?: QueueJobOptions): string
  cancel(jobId: string): boolean
  pause(jobId?: string): void      // pause one or all
  resume(jobId?: string): void
  jobs(): JobStatus[]
  start(): void
  stop(): Promise<void>
}

function createJobScheduler(options?: JobSchedulerOptions): JobScheduler

type JobHandler = (ctx: JobContext) => Promise<void>

interface JobContext {
  jobId: string
  signal: AbortSignal
  attempt: number
  scheduledAt: number
  logger: LoggerLike
}

interface JobSchedulerOptions {
  concurrency?: number        // default: 5
  clock?: Clock
  logger?: LoggerLike
  catchupMissed?: boolean     // default: false
  maxMissedRuns?: number      // default: 1 when catchupMissed
}

interface JobOptions {
  id?: string
  name?: string
  retries?: number
  retryDelayMs?: number
  timeout?: number
  tz?: string                 // IANA timezone for cron
}

interface QueueJobOptions extends JobOptions {
  delayMs?: number
  runAt?: number              // epoch ms
}

interface JobStatus {
  id: string
  name?: string
  cronExpr?: string
  status: 'pending' | 'running' | 'paused' | 'cancelled'
  lastRun?: number
  nextRun?: number
  runs: number
  failures: number
}

type JobSchedulerErrorCode =
  | 'JOB_SCHEDULER_JOB_NOT_FOUND'
  | 'JOB_SCHEDULER_EXECUTION_FAILED'
  | 'JOB_SCHEDULER_TIMEOUT'

// Cron parse errors surface as @suaveplan/cron's CRON.* codes — no JOB_SCHEDULER_INVALID_CRON wrapper.

function registerJobSchedulerErrorCodes(): void

File layout

server/infra/job-scheduler/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/index.ts
    types/
      job-scheduler.types.ts
      job-scheduler.types.md
    scheduler/
      job-scheduler/
        job-scheduler.ts
        job-scheduler.md
        job-scheduler.types.ts
        job-scheduler.test.ts
        index.ts
    queue/
      job-queue/
        job-queue.ts
        job-queue.md
        job-queue.test.ts
        index.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Downstream: any server package requiring periodic tasks (cache warm-up, report generation, cleanup). No genesis package currently depends on it.

Prerequisites on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/retry, @suaveplan/logger, @suaveplan/cron.