@happyvertical/smrt-jobs

Background job execution with persistent queue, retry strategies, cron scheduling, and a fluent JobBuilder API.

v0.20.44Task RunnerSchedulerFluent API

Overview

smrt-jobs provides background job execution for any SmrtObject method. Jobs are persisted in the _smrt_jobs system table, processed by a polling-based TaskRunner with concurrency control, and can be scheduled via cron expressions through the ScheduleRunner.

Installation

bash
npm install @happyvertical/smrt-jobs

Quick Start

typescript
import { withBackgroundJobs, TaskRunner } from '@happyvertical/smrt-jobs';
import { Document } from './document.js';

// Mixin adds .bg() and .background() to any SmrtObject class
const BackgroundDocument = withBackgroundJobs(Document);
const doc = new BackgroundDocument({ db });
await doc.initialize();

// Quick enqueue -- runs when a TaskRunner picks it up
const handle = await doc.bg('generateSummary', { format: 'md' });

// Fluent builder for advanced options
const handle2 = await doc.background('generateSummary', { format: 'md' })
  .delay('5m')
  .priority('high')
  .retries(5)
  .queue('analysis')
  .timeout(600000)
  .enqueue();

// Wait for result (polling-based)
const result = await handle2.wait({ timeout: 60000, pollInterval: 100 });

Core Models

SmrtJob

typescript
class SmrtJob extends SmrtObject {
  queue: string               // 'default' by default
  objectType: string          // Class name for ObjectRegistry lookup
  objectId: string
  method: string              // Method to call on the object
  args: string                // JSON arguments
  runAt: Date
  priority: number            // Higher = sooner
  status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
  attempts: number
  maxAttempts: number
  timeout: number             // Default 5 minutes (ms)
  retryStrategy: string
  workerId?: string
  workerHeartbeat?: Date
  resultPointer?: string      // App-defined result storage
}

TaskRunner

typescript
const runner = new TaskRunner({
  concurrency: 5,             // Max parallel jobs
  pollInterval: 1000,         // Poll every 1 second
  heartbeatInterval: 30000,   // Heartbeat every 30 seconds
  shutdownTimeout: 30000,     // Graceful shutdown timeout
  queues: ['default', 'analysis'],
});
await runner.initialize(db);
await runner.start();

// Listen for events
runner.on('job:completed', (job, result) => { /* ... */ });
runner.on('job:failed', (job, error) => { /* ... */ });

// Graceful shutdown
process.on('SIGTERM', () => runner.stop());

Cron Scheduling

typescript
import { ScheduleRunner } from '@happyvertical/smrt-jobs';

// Polls _smrt_agent_schedules for due cron entries
const scheduleRunner = new ScheduleRunner({ pollInterval: 30000 });
await scheduleRunner.initialize(db);
await scheduleRunner.start();

// Connect TaskRunner events to update schedule state
taskRunner.on('job:completed', (job) => {
  const scheduleId = job.args?._scheduleId;
  if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, true);
});
taskRunner.on('job:failed', (job, error) => {
  const scheduleId = job.args?._scheduleId;
  if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, false, error.message);
});

// Cron format: 5-field (minute hour dom month dow)
// Supports: *, ranges, lists, steps
// All times are UTC (not timezone-aware)

Best Practices

DOs

  • Use withBackgroundJobs() mixin to add background capabilities
  • Use the fluent builder for complex job configuration
  • Wire ScheduleRunner events to TaskRunner for completion tracking
  • Implement graceful shutdown with runner.stop()
  • Use priority levels for job ordering (critical/high/normal/low)

DON'Ts

  • Don't forget to call .enqueue() on the builder (it's lazy)
  • Don't assume timezone support (cron is UTC only)
  • Don't expect a dead letter queue (failed jobs stay in DB)
  • Don't rely on resultPointer without implementing a result backend
  • Don't poll too aggressively with handle.wait() (default 100ms is reasonable)

Related Modules