SERVER ECOSYSTEM
Background Queues
@rasenganjs/queue is functional and tested, but its governing RFC (RFC-0004)
has a Phase 4 that hasn't landed yet. Expect rougher edges and possible API
changes before it's fully stable.
@rasenganjs/queue adds background job queues — the Controller equivalent for async work, mirroring @rasenganjs/ws's Gateway pattern exactly.
npm install @rasenganjs/queue
Registering the Plugin
import { bootstrap } from '@rasenganjs/server'; import { createQueuePlugin } from '@rasenganjs/queue'; import appModule from './app.module'; bootstrap((app) => { app.registerPlugin(createQueuePlugin()); // defaults to MemoryQueueAdapter app.registerModule(appModule); // may declare queues: [EmailQueue] });
Writing a Queue
import { Queue, JobRouter, type JobHandler } from '@rasenganjs/queue'; export class EmailQueue extends Queue { name = 'emails'; constructor(private mailer: MailerService) { super(); } jobs(router: JobRouter) { router.process('welcome', this.sendWelcome, { attempts: 3, backoff: 5_000, }); } sendWelcome: JobHandler<{ userId: string }> = async (job) => { await this.mailer.sendWelcome(job.data.userId); // resolve -> job completes; throw -> retried per backoff policy }; }
import { defineModule } from '@rasenganjs/server'; import { EmailQueue } from './email.queue'; export default defineModule({ queues: [EmailQueue], providers: [MailerService], });
Like Gateway, Queue extends Provider — constructor injection and lifecycle hooks work identically to any other provider.
Enqueuing Jobs
Inject the queue anywhere it's exported and visible, and call .add():
export class SignupController extends Controller { constructor(private emailQueue: EmailQueue) { super(); } routes(router: Router) { router.post('/signup', this.signup); } signup: RouteHandler = async (ctx) => { const user = await createUser(ctx.body); await this.emailQueue.add('welcome', { userId: user.id }); return ctx.res.json(user); }; }
Delayed and Recurring Jobs
await this.emailQueue.add('reminder', { userId }, { delay: 60_000 }); // fires in 1 minute
await this.reportQueue.add( 'dailyDigest', {}, { repeat: { every: 86_400_000 } } ); // every 24h
A { repeat } registration is idempotent by jobKey (derived from the job name + data, or an explicit repeat.key) — calling .add() again with the same spec doesn't create a duplicate schedule. It also resolves with the recurring job's jobKey instead of a fresh id.
Retry Semantics
At-least-once processing: resolving the handler completes the job, throwing retries with exponential backoff until attempts is exhausted, then the job moves to a dead-letter state.
const dead = await this.emailQueue.getDead(); await this.emailQueue.retryDead(dead[0].id); // resets attempt count to 1, back to waiting
At-least-once delivery means a handler can run more than once for the same logical job (e.g. after a stalled-reservation reclaim). Design job handlers to be safely re-runnable.
ProcessOptions
Scaling with Redis
MemoryQueueAdapter (the default) is in-process and loses jobs on restart — dev only. RedisQueueAdapter persists across restarts and is safe across multiple processes sharing one queue:
import { createQueuePlugin, RedisQueueAdapter } from '@rasenganjs/queue'; import Redis from 'ioredis'; const client = new Redis(); const adapter = new RedisQueueAdapter({ client, blockingClient: client.duplicate(), // reserve()'s BLMOVE needs its own connection }); app.registerPlugin(createQueuePlugin({ adapter }));
QueuePluginOptions
worker: false is a deployment-topology choice, not a code change — useful for splitting a producer-only web process from dedicated worker processes that actually run job handlers.
