Skip to main content

Publishing Jobs

Publishing

Use typed queue instances to publish jobs with full type safety. Each queue instance has a publish method that accepts the payload and optional provider-specific options.

Best Practice: Centralized Queue Registry

Recommended (but not enforced): Define all queues in a centralized location for better organization and reusability:

// src/queues/registry.ts
import { bullmqQueue, sqsQueue, pgbossQueue } from "balda";

// Define payload types
type UserEvent = { userId: string; action: string };
type EmailJob = { to: string; subject: string; body: string };
type NotificationJob = { userId: string; message: string };

// Export all queues from a single registry
export const queues = {
userEvents: bullmqQueue<UserEvent>("user-events"),
emails: sqsQueue<EmailJob>("emails", {
queueUrl: "https://sqs.us-east-1.amazonaws.com/123456789/emails",
}),
notifications: pgbossQueue<NotificationJob>("notifications"),
} as const;

Benefits:

  • Single source of truth for all queues
  • Easy to import: import { queues } from './queues/registry.js'
  • Consistent queue configuration across the application
  • Better discoverability of available queues

Usage:

import { queues } from "./queues/registry.js";

// Publishing
await queues.userEvents.publish({ userId: "123", action: "signup" });
await queues.emails.publish({
to: "user@example.com",
subject: "Welcome",
body: "Hello!",
});

// Subscribing
export class UserEventHandler {
@queues.userEvents.subscribe()
async handle(payload: UserEvent) {
console.log(`User ${payload.userId} performed ${payload.action}`);
}
}

This pattern is completely optional - you can also define queues inline or in separate files based on your preference.

Creating typed queues

Alternatively, create typed queue instances directly where needed using factory functions:

import { bullmqQueue, sqsQueue, pgbossQueue } from "balda";

// Define payload types
type UserEvent = { userId: string; action: string };
type EmailJob = { to: string; subject: string; body: string };
type NotificationJob = { userId: string; message: string };

// Create queue instances
export const userQueue = bullmqQueue<UserEvent>("user-events");
export const emailQueue = sqsQueue<EmailJob>("emails", {
queueUrl: "https://sqs.us-east-1.amazonaws.com/123456789/emails",
});
export const notificationQueue = pgbossQueue<NotificationJob>("notifications");

Basic publishing

// Publish to BullMQ
await userQueue.publish({ userId: "123", action: "signup" });

// Publish to SQS
await emailQueue.publish({
to: "user@example.com",
subject: "Welcome",
body: "Thanks for signing up!",
});

// Publish to PGBoss
await notificationQueue.publish({
userId: "123",
message: "Your order has shipped",
});

Publishing with provider options

Each provider supports specific options as the second parameter:

BullMQ options (maps to Queue.add third parameter):

await userQueue.publish(
{ userId: "123", action: "signup" },
{
delay: 5000, // Delay 5 seconds
priority: 1, // High priority
removeOnComplete: true, // Auto-remove on completion
attempts: 3, // Retry up to 3 times
backoff: {
type: "exponential",
delay: 2000,
},
},
);

SQS options:

await emailQueue.publish(
{ to: "user@example.com", subject: "Hello", body: "World" },
{
MessageGroupId: "emails", // For FIFO queues
MessageDeduplicationId: "123", // For FIFO queues
DelaySeconds: 10, // Delay 10 seconds
MessageAttributes: {
// Custom attributes
priority: {
DataType: "String",
StringValue: "high",
},
},
},
);

PGBoss options (maps to send third parameter):

await notificationQueue.publish(
{ userId: "123", message: "Hello!" },
{
retryLimit: 3, // Retry up to 3 times
retryDelay: 60, // Wait 60 seconds between retries
expireInSeconds: 3600, // Expire after 1 hour
priority: 10, // Higher number = higher priority
},
);

Subscribing to queues

The subscribe method supports two patterns:

1. Decorator-based subscription (recommended for class methods):

export class UserEventHandler {
@userQueue.subscribe()
async handle(payload: UserEvent) {
console.log(`User ${payload.userId} performed ${payload.action}`);
}
}

2. Callback-based subscription (recommended for standalone functions):

await userQueue.subscribe(async (payload) => {
console.log(`User ${payload.userId} performed ${payload.action}`);
});

Key differences:

  • Decorators are ideal for organized class-based handlers and work seamlessly with dependency injection patterns
  • Callbacks are perfect for simple handlers, scripts, or when you prefer a functional programming style
  • Both methods provide full type safety and identical runtime behavior
  • You can mix both approaches in the same application

Type safety benefits

The typed queue API provides full type safety:

// ✅ Type-safe: correct payload
await userQueue.publish({ userId: '123', action: 'signup' });

// ❌ TypeScript error: missing required fields
await userQueue.publish({ userId: '123' });

// ❌ TypeScript error: wrong field type
await userQueue.publish({ userId: 123, action: 'signup' });

// ✅ Type-safe handler
@userQueue.subscribe()
async handle(payload: UserEvent) {
// payload.userId is string
// payload.action is string
// Full autocomplete and type checking
}

Multiple queues with the same provider

You can create multiple queue instances for the same provider:

import { bullmqQueue } from 'balda';

// Different queues for different job types
export const emailQueue = bullmqQueue<EmailJob>('emails');
export const smsQueue = bullmqQueue<SMSJob>('sms');
export const pushQueue = bullmqQueue<PushJob>('push-notifications');

// Each has its own type-safe publish/subscribe
await emailQueue.publish({ to: 'user@example.com', ... });
await smsQueue.publish({ phone: '+1234567890', ... });
await pushQueue.publish({ deviceToken: 'abc123', ... });

Summary: Two ways to subscribe

Every typed queue instance (BullMQ, SQS, PGBoss, or custom) supports both subscription methods:

import { bullmqQueue, sqsQueue, pgbossQueue } from "balda";

const queue = bullmqQueue<MyPayload>("my-topic");

// Method 1: Decorator (for class methods)
class MyHandler {
@queue.subscribe()
async handle(payload: MyPayload) {}
}

// Method 2: Callback (for standalone functions)
await queue.subscribe(async (payload) => {});

Choose the method that best fits your application architecture. Both provide identical functionality and full TypeScript type safety.