Sync Queue
At-least-once work queue. Server version is Redis-backed and durable, browser version uses in-memory state. Messages are delivered to exactly one consumer per delivery attempt.
Consumer Pattern
for await (const msg of q.stream({ signal: ac.signal })) {
try {
await handle(msg.data);
await msg.ack();
} catch (error) {
await msg.nack({ delayMs: 3000, reason: "handler_error" });
}
}Decision Guide
ack(): processing succeeded. Message removed.nack({delayMs?}): processing failed, re-queue after delay. AftermaxDeliveries(default 10), moves to DLQ.touch({leaseMs?}): extend visibility timeout for long-running handlers. Call before lease expires.reader(): create additional independent blocking consumers within the same process.
Gotchas
stream({wait: true})auto-retries transient transport errors. Do not wrap withretry().ack()andnack()can returnfalseif lease already expired (another consumer may have reclaimed).maxDeliveriesdefault is 10. After that, message goes to DLQ with reasonmax_deliveries_exceeded.defaultLeaseMsis 30s. If handler takes longer, calltouch()or it will be reclaimed.maxMessageAgeMsdefault 7 days. Old unprocessed messages go to DLQ with reasonexpired.orderingconfig is accepted but not enforced at runtime —orderingKeyis stored as metadata only.- Maintenance (lease reclaim, delayed→ready promotion, DLQ moves) runs automatically during recv calls.
- Payload max 128KB. Validated at send and re-parsed at receive.
idempotencyKeywithidempotencyTtlMsprevents duplicate sends (default TTL 7 days).
Browser
import { queue } from "@valentinkolb/sync/browser";Same API (send/recv/stream/ack/nack/touch). Browser-specific notes:
- Queue state (ready list, delayed messages, deliveries, DLQ) lives in JS heap — lost on page refresh.
- Blocking
recv({wait: true})uses a Promise-based event emitter instead of RedisBLMOVE. - Maintenance (lease reclaim, delayed→ready promotion) runs lazily on each
recv()call, same as server. - No transport error retries wrapping — no network errors possible in-memory.
stream({wait: true})works identically but without the retry wrapper.
API Reference
Read full API/types/config/defaults in references/api.md.