Skip to content

Queue ​

Holo-JS ships queue support as a first-class server subsystem. New projects already scaffold config/queue.ts, create server/jobs, and default to the sync driver so dispatch works on day one.

Existing projects ​

Install queue support into an existing Holo-JS app with:

bash
npx holo install queue

Pick a starting driver during install when needed:

bash
npx holo install queue --driver redis
npx holo install queue --driver database

What queue owns ​

The queue subsystem gives you:

  • a typed config/queue.ts
  • auto-discovered jobs under server/jobs
  • dispatch helpers
  • long-lived worker commands for async drivers
  • failed-job management
  • first-party media integration for queued conversions

Queue does not own event contracts. Event contracts and listener orchestration are documented under Events. Queued listeners use queue internally.

Queue config ​

New apps start with sync:

ts
import { defineQueueConfig } from '@holo-js/queue'
export default defineQueueConfig({
  default: 'sync',
  failed: {
    driver: 'database',
    connection: 'default',
    table: 'failed_jobs',
  },
  connections: {
    sync: {
      driver: 'sync',
      queue: 'default',
    },
  },
})

sync runs the job inline. It is the simplest default and needs no worker process. Async drivers need a separate worker runtime in production. See Deployment for host selection and Workers for worker commands.

End-to-end driver examples ​

Sync ​

Use sync when you want immediate execution in local development, tests, or low-volume app flows.

ts
import SendDigest from '../jobs/reports/send-digest'

await SendDigest.dispatch({
  reportId: 'daily-summary',
})

No worker process is required.

Redis ​

Use redis for normal asynchronous work:

ts
// config/redis.ts
import { env } from '@holo-js/config'
import { defineRedisConfig } from '@holo-js/kernel'

export default defineRedisConfig({
  default: 'cache',
  connections: {
    cache: {
      url: env('REDIS_URL') || undefined,
      host: env('REDIS_HOST', '127.0.0.1'),
      port: env('REDIS_PORT', 6379),
      username: env('REDIS_USERNAME'),
      password: env('REDIS_PASSWORD'),
      db: env('REDIS_DB', 0),
    },
  },
})
ts
// config/queue.ts
import { defineQueueConfig } from '@holo-js/queue'
export default defineQueueConfig({
  default: 'redis',
  failed: {
    driver: 'database',
    connection: 'default',
    table: 'failed_jobs',
  },
  connections: {
    redis: {
      driver: 'redis',
      connection: 'cache',
      queue: 'default',
      retryAfter: 90,
      blockFor: 5,
    },
  },
})

Queue only needs the shared Redis connection name. The actual Redis target lives in config/redis.ts.

Shared Redis connections resolve in this order:

  1. url
  2. clusters
  3. host

See Configuration for examples of all three connection styles.

Run a worker:

bash
npx holo queue:work --connection redis

Database ​

Use database when you want queue portability and are comfortable polling from your app database:

ts
import { defineQueueConfig } from '@holo-js/queue'
export default defineQueueConfig({
  default: 'database',
  failed: {
    driver: 'database',
    connection: 'default',
    table: 'failed_jobs',
  },
  connections: {
    database: {
      driver: 'database',
      connection: 'default',
      table: 'jobs',
      queue: 'default',
      retryAfter: 90,
      sleep: 1,
    },
  },
})

Generate the required tables and migrate:

bash
npx holo queue:table
npx holo queue:failed-table
npx holo migrate

Then run a worker:

bash
npx holo queue:work --connection database

Queue names and connections ​

  • A connection selects the backend driver configuration such as sync, redis, or database.
  • A queue name lets you partition work inside that connection, such as default, emails, or media.
  • Jobs can set defaults, and dispatch can override them per call.

Dispatch overview ​

Use the job definition for typed queued execution:

ts
import SendDigest from '../jobs/reports/send-digest'

await SendDigest.dispatch({
  reportId: 'daily-summary',
})
  .onConnection('redis')
  .onQueue('emails')
  .delay(60)
  .onComplete((result) => {
    console.log(result.jobId)
  })
  .onFailed((error) => {
    console.error(error)
  })

Use the string-based dispatch helper when the job name is dynamic or when you prefer dispatching by name:

ts
import { dispatch } from '@holo-js/queue'

await dispatch('reports.send-digest', {
  reportId: 'daily-summary',
})
  .onConnection('redis')
  .onQueue('emails')

If dispatch must wait for a successful database commit, compose that explicitly with DB.afterCommit() from @holo-js/db.

Use dispatchSync() when the current code path must execute the job immediately:

ts
import SendDigest from '../jobs/reports/send-digest'
import { dispatchSync } from '@holo-js/queue'

await SendDigest.dispatchSync({
  reportId: 'daily-summary',
})

await dispatchSync('reports.send-digest', {
  reportId: 'daily-summary',
})

Server-only rules ​

Queue jobs are backend code. Keep them in server-owned files and pass only JSON-serializable payloads:

  • IDs
  • strings
  • numbers
  • booleans
  • arrays
  • plain objects

Do not queue model instances, functions, streams, or browser-only objects.

Continue ​

Holo owns backend runtime concerns. The host framework owns SSR and routing.