@nolag/queue
Real-time job queues with lifecycle tracking, progress updates, and worker management.
Overview
Distribute work across real-time workers with full lifecycle visibility. Jobs follow a state machine: pending, claimed, active, then completed or failed. Every state transition is broadcast to queue participants. A failed job goes back to pending until maxAttempts is exhausted. Three roles participate in a queue: producers add jobs, workers claim and process them, and monitors observe queue state. A wrapper plays one role; run one wrapper per role. Your app owns one core NoLag client and injects it into NoLagQueue; the wrapper attaches its behaviour to that connection.
Key Features
- State machine lifecycle: pending, claimed, active, completed, failed
- Load-balanced delivery: each new job reaches exactly one worker in the group
- Priority label on every job and automatic re-queue on failure
- Real-time progress reporting (0-100) broadcast to all participants
- Ephemeral progress channel keeps durable storage lean
- Worker online/offline presence via lobby
How It Works
NoLagQueue attaches to an injected @nolag/js-sdk client and maintains a lobby for worker presence. Calling joinQueue(name) returns a QueueRoom that subscribes to two topics: jobs for lifecycle events and _progress for progress updates. Workers subscribe to jobs with loadBalance: true in the group queue-workers-<queue> (or the loadBalanceGroup you set), so the broker hands each job event to exactly one worker in the group. Producers and monitors subscribe without load balancing and see every event. _progress is never load balanced.
Exclusivity comes from that delivery, not from a server-side claim. claimJob() is a local state transition plus a broadcast of jobClaimed: the worker that received jobAdded is the only worker in its group that holds the job, and claimJob() returns null only when the job is unknown locally or is not pending. A retry is immediate: failJob() records the attempt, and while attempts < maxAttempts it moves the job back to pending and emits jobRetrying in the same call, with no delay or backoff. Nothing re-dispatches the job for you; the worker that failed it claims it again from its jobRetrying handler. priority is a label stored on the job and carried in every event; nothing reorders delivery by it. The app owns the socket lifecycle; the wrapper never opens or closes it.
| Topic | Purpose |
|---|---|
jobs | Job lifecycle events: added, claimed, completed, failed, retrying |
_progress | In-flight progress updates (0-100), not persisted |
Because workers subscribe with loadBalance: true, a worker group whose actors hold a persistent session (agent or orchestrator) is the one case where the hosted broker replays what was dispatched while the group was away; see Replay for the exact conditions. Replay progress is reported by the core client (client.on('replay:start'), client.on('replay:end')), not by the wrapper. Producers and monitors never trigger replay.
Before you start. Create an app from the nolag-queue-sdk blueprint. That seeds the rooms this SDK expects (image-processing, email-dispatch) and the online lobby, and returns an app slug with a random suffix. That slug is the appName you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.
import { NoLagApi } from "@nolag/js-sdk";
const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My App", blueprintId: "nolag-queue-sdk" });
console.log(app.slug); // e.g. "my-app-a3f9": this is your appName
const actor = await api.actors.create({ name: "web-client", actorType: "user" });
console.log(actor.accessToken); // shown once; keep itInstallation
npm install @nolag/queue @nolag/js-sdkShared connection. One core NoLag client can back several wrapper SDKs at once, for example a job queue, a dashboard, and notify on a single socket, as long as each wrapper uses a distinct appName. Each wrapper attaches its handlers on construction and releases them with detach(), and never touches the socket itself. Your app owns connect() and disconnect().
Quick Start
Each role is its own wrapper, typically its own process. All three join the seeded image-processing queue.
import { NoLag } from '@nolag/js-sdk'
import { NoLagQueue } from '@nolag/queue'
const client = NoLag(PRODUCER_TOKEN)
const producer = new NoLagQueue({
client,
appName: APP_SLUG, // the slug returned when you created the app
role: 'producer',
})
await client.connect() // the app owns the connection
await producer.ready() // wrapper setup complete
const queue = producer.joinQueue('image-processing')
// Enqueue a job. `type` is required; the id is generated for you.
const job = queue.addJob({
type: 'resize',
payload: { imageId: 'img_xyz', width: 1280 },
priority: 'high', // 'low' | 'normal' | 'high' | 'critical'; a label, not an ordering
maxAttempts: 3, // default 3
})
console.log('Added job:', job.id, 'status:', job.status) // 'pending'
// Producers receive every lifecycle event
queue.on('jobCompleted', (done) => console.log(`Job ${done.id} done:`, done.result))
queue.on('jobFailed', (failed) => {
console.warn(`Job ${failed.id} failed (attempt ${failed.attempts}/${failed.maxAttempts}):`, failed.error)
})
producer.detach()
client.disconnect()API Reference
NoLagQueue
Constructor Options
| Option | Type | Description |
|---|---|---|
client | NoLagSocket | Required. The injected core NoLag client the app owns and connects. |
appName | string | The app slug used as the topic prefix. Pass the suffixed slug returned when you created the app; the default 'queue' will not match a hosted app. |
workerId | string | Stable worker ID for this client (auto-generated if omitted). Recorded as createdBy and claimedBy on jobs. |
role | 'producer' | 'worker' | 'monitor' | Role this wrapper plays (default 'monitor'). Only workers subscribe with load balancing. |
concurrency | number | Advertised in worker presence as QueueWorker.concurrency (default 1). The wrapper does not limit claims by it. |
metadata | Record<string, unknown> | Optional custom data attached to worker presence. |
queues | string[] | Queues to join once the wrapper is ready. |
loadBalanceGroup | string | Subscribe-level load-balance group for workers (default 'queue-workers-<queueName>'). Set a custom group to partition workers by region or capability. |
maxJobCache | number | Max jobs cached in memory per queue (default 1000). |
debug | boolean | Enable wrapper debug logging (default false). |
| Method | Returns | Description |
|---|---|---|
ready() | Promise<void> | Resolves once wrapper setup completed. Join methods throw before this resolves. |
detach() | void | Release this wrapper's handlers and topics; terminal, never closes the socket. |
joinQueue(name, opts?) | QueueRoom | Subscribe to a named queue and return it. Synchronous; returns the existing instance if already joined. opts.filters declares the job filter values this participant wants (for a worker, its capabilities). |
leaveQueue(name) | void | Unsubscribe from a queue and release its resources. |
getQueues() | QueueRoom[] | All currently joined queues. |
getOnlineWorkers() | QueueWorker[] | Workers currently present in the online lobby. |
NoLagQueue Events
| Event | Payload | Description |
|---|---|---|
connected | none | Wrapper setup completed on a live connection. |
disconnected | reason: string | Connection closed. |
reconnecting | none | The core client is attempting to reconnect. |
reconnected | none | Connection restored; queue membership and presence are restored automatically. |
error | error: Error | A transport or protocol error occurred. |
workerOnline | worker: QueueWorker | A worker joined the lobby. |
workerOffline | worker: QueueWorker | A worker left the lobby. |
QueueRoom
| Method | Role | Returns | Description |
|---|---|---|---|
addJob(opts) | Producer | Job | Enqueue a job. opts is { type, payload?, priority?, maxAttempts?, filter?, filters? }; type is required, the id is generated. |
claimJob(jobId) | Worker | Job | null | Move a pending job to claimed and broadcast it. null if the job is unknown locally or not pending. |
reportProgress(jobId, progress) | Worker | void | Broadcast a progress value (0-100) on _progress. |
completeJob(jobId, result?) | Worker | Job | null | Mark the job completed with an optional result. null if the job is unknown locally or not in claimed or active. |
failJob(jobId, error?) | Worker | Job | null | Mark the job failed and increment attempts. While attempts < maxAttempts it returns to pending and jobRetrying fires immediately. |
getJob(id) | Any | Job | undefined | Read a job from the local cache. |
getJobs(filter?) | Any | Job[] | Read cached jobs, optionally filtered by { status?, type?, priority? }. Does not change what the server sends. |
pendingCount | Any | number | Getter: cached jobs in the pending state. |
activeCount | Any | number | Getter: cached jobs in the active state. |
getWorkers() | Any | QueueWorker[] | Workers present in this queue. |
setFilters(values) / addFilters(values) / removeFilters(values) | Any | void | Change which job filter values this participant receives. Filters compose with load balancing: one matching worker gets each job. |
QueueRoom Events
All lifecycle events carry the full Job. Producers and monitors receive every jobAdded; within a worker group only one worker does. The later events are emitted only on clients that already hold the job in their cache.
| Event | Payload | Description |
|---|---|---|
jobAdded | Job | A new job was added. Delivered to one worker per group, and to every producer and monitor. |
jobClaimed | Job | A worker claimed a job; claimedBy names it. |
jobProgress | JobProgress | A worker reported progress: { jobId, progress, workerId, timestamp }. |
jobCompleted | Job | A job completed; result carries the payload. |
jobFailed | Job | A job failed; error, attempts and maxAttempts describe the state. |
jobRetrying | Job | A failed job was returned to pending for another attempt. |
workerJoined | worker: QueueWorker | A worker joined this queue. |
workerLeft | worker: QueueWorker | A worker left this queue. |
Types
| Type | Shape |
|---|---|
Job | { id, type, payload?, priority, status, progress, result?, error?, attempts, maxAttempts, claimedBy?, createdBy, createdAt, updatedAt, completedAt?, filter?, isReplay } |
JobStatus | 'pending' | 'claimed' | 'active' | 'completed' | 'failed' |
JobPriority | 'low' | 'normal' | 'high' | 'critical' (default 'normal') |
QueueWorker | { workerId, actorTokenId, role, activeJobs, concurrency, metadata?, joinedAt, isLocal } |