Skip to content

All versions since v0.13.0

v0.13.0

What’s Changed

Features

  • Worker defaults: createInProcessWorker accepts a new defaults: { backoffConfig?, leaseConfig? } option for fleet-wide fallbacks. Resolution order is processor → registry → worker defaults → library default. InProcessWorkerDefaults is exported from the package root.
  • Required cross-slice middleware: new requiredAttemptMiddleware option on createInProcessWorker enforces (at compile time against the merged slice middleware, and at runtime by reference identity) that every slice includes the listed AttemptMiddleware instances as an in-order subsequence. The worker does not execute them itself — this guarantees cross-cutting concerns (auth, tracing, logging) are wired into every slice without giving up the per-slice middleware model.
  • Multi-slice validation: validation adapters now accept either a single external slice or a readonly array, restoring the multi-slice composition path that was removed alongside mergeJobTypeRegistries in 0.12. New public JobTypesDefinitions<T> resolves either form to the merged BaseJobTypeDefinitions record.
  • RuntimeType re-export from @queuert/postgres and @queuert/sqlite so custom state-provider authors can type their serialization/parsing without reaching into @queuert/typed-sql.
  • New state-sqlite-bun example demonstrates @queuert/sqlite running on bun:sqlite.

Breaking Changes

  • workerId → workerName: createInProcessWorker’s identity option is renamed to workerName — an optional human-readable label restricted to /^[A-Za-z0-9._-]+$/. The runtime always appends a random UUID to produce the final worker id (${workerName}-${uuid}, or just ${uuid} when omitted), making duplicate worker ids impossible to express. Observability events and error fields continue to expose the full id under workerId.
  • Validation adapter signatures: adapters accept the “single slice or readonly array” shape createClient already takes. ExternalJobTypeDefinitions is no longer exported — use JobTypeDefinitions for a single slice or JobTypesDefinitions for either form. Adapter authors implementing runValidationAdapterConformance must add a buildWithExternalSlices fixture, and buildWithExternalSlice’s blockers tightens from a readonly array to a tuple — schemas under test must switch from array(...) to tuple([...]) (z.tuple, v.tuple, Type.Tuple, type([...])).
  • Stricter public error fields: contextual fields on JobNotFoundError, ChainNotFoundError, JobTakenByAnotherWorkerError, JobAlreadyCompletedError, JobNotTriggerableError, and WaitChainTimeoutError (jobId, chainTypeName, etc.) are now non-optional string / number, and their constructor options are required — caught errors no longer need null-checks. JobTakenByAnotherWorkerError.leasedBy is now string | null (an omitted constructor option is normalized to null); narrowings via === undefined should switch to === null.
  • @queuert/otel attribute renames: every metric attribute key is renamed to OpenTelemetry semantic-convention style (lowercase, dotted) to align with the span attributes already emitted by the tracing layer. Metric names are unchanged, so queries still resolve — but dashboards, alerts, and recording rules that group by or filter on the old keys silently return empty results until updated. workerId → queuert.worker.id, typeName → queuert.job.type, chainTypeName → queuert.chain.type, continued → queuert.job.continued, operation → queuert.adapter.operation. Two value encodings also changed on queuert.job.completed: queuert.job.continued is now a real boolean (not "true" / "false"), and queuert.worker.id is omitted for workerless completions (no longer the literal string "null").
  • executeSql.id uniqueness: in @queuert/postgres and @queuert/sqlite, executeSql’s id now uniquely identifies the resolved SQL within a provider — the template applier folds variants like schema / tablePrefix into a hashed suffix. The built-in pg.Pool provider drops its SHA1 name-hash (query.name = id); built-in better-sqlite3 and node:sqlite providers drop their per-database SQL→Statement maps and key the cache by id. Custom providers can do the same.

Bug Fixes

  • Relax the generic bound on AttemptMiddleware’s TStateAdapter from StateAdapter<BaseTxContext, any> to StateAdapter<any, any>. The previous bound was unusable in practice: because the transaction-context type parameter appears in contravariant positions, no concrete adapter with a non-empty txCtx could satisfy StateAdapter<BaseTxContext = {}, any>. Adapter-aware middleware and generic test helpers now type-check without any workarounds.

Packages

All packages released at 0.13.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.

v0.14.0

What’s Changed

Features

  • Caller-supplied job IDs (breaking): startChain, startChains, and the worker continueWith callback now accept an optional id that becomes the new chain root (or continuation) job ID. State adapters accept a new validateId predicate that runs on both adapter-generated and caller-supplied IDs; failures throw the new InvalidJobIdError. When deduplication fires, the existing row’s ID wins over a caller-supplied id (the returned chain carries deduplicated: true).
  • Aligned adapter ID-generation options (breaking): both PostgreSQL and SQLite adapters now expose a generateId function. PostgreSQL’s idDefault (SQL expression) is removed in favor of JS-side generation; SQLite’s idGenerator is renamed to generateId. A new PostgreSQL migration drops DEFAULT gen_random_uuid() from the id column. Existing UUID generation behavior is unchanged for default configurations.

Bug Fixes

  • scheduled_at is now an honest eligibility floor: state adapters clamp scheduled_at to MAX(requested, now()) on createJobs, rescheduleJob, and unblockJobs. Previously, jobs with a past scheduled_at (or blocked-since-creation jobs whose original scheduled_at went stale while they waited) would jump to the front of the acquisition queue ahead of jobs that genuinely became ready earlier. unblockJobs now preserves a future scheduled_at set at job creation (it previously reset to now() unconditionally); triggerJobs continues to re-anchor to now() as an explicit reset. Applies to PostgreSQL, SQLite, and in-process adapters; no schema migration or backfill required.
  • lastAttemptError cleared on success: when a job completes successfully after a previously failed attempt, completeJob now resets last_attempt_error to NULL alongside the existing status/output/lease updates. Completed rows no longer carry over an error string from a prior failed attempt.
  • requiredAttemptMiddleware type fix: a spurious type error from createInProcessWorker’s requiredAttemptMiddleware check is fixed when middleware is typed against a user-supplied StateAdapter alias (e.g. AttemptMiddleware<MyStateAdapter>). Valid processor slices were being flagged as missing required middleware. Runtime behavior was unaffected; this is a type-only fix.
  • BaseTxContext tightened: changed from {} to Record<string, unknown>. The previous {} constraint accepted any non-nullish value (string, number, function), letting custom StateAdapter authors pick a non-object TTxContext without a type error. All built-in adapters are already object-shaped, so no runtime behavior changes.

Migration Notes

  • PostgreSQL: replace idDefault: "gen_random_uuid()" with generateId: () => crypto.randomUUID() (this is the default and can be omitted).
  • PostgreSQL: replace idDefault: "'job.' || gen_random_uuid()::text" with generateId: () => `job.${crypto.randomUUID()}` .
  • SQLite: rename idGenerator option to generateId.
  • Custom state adapter implementations must accept an optional id per entry in createJobs and apply their own validateId predicate.

Packages

All packages released at 0.14.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.

v0.14.1

What’s Changed

Improvements

  • Adapter conformance suite: StateConformanceFixture now propagates generateId and generateInvalidId through to the cases. Previously the runner only forwarded stateAdapter and poisonTransaction, so adapters configured with a custom validateId could not exercise the caller-supplied id path in conformance tests.

Breaking Changes (testing helpers only)

  • Removed StateAdapterConformanceContext and NotifyAdapterConformanceContext types. Callers writing conformance suites for external adapters (e.g. it.extend<NotifyAdapterConformanceContext>(...) in vitest specs) should switch to StateConformanceFixture / NotifyConformanceFixture. No runtime API changes — this only affects adapter authors using the conformance harness.

Packages

All packages released at 0.14.1: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.

v0.15.0

What’s Changed

Breaking Changes

  • triggerJob/triggerJobs renamed to rescheduleJob/rescheduleJobs with an optional schedule parameter ({ at: Date } | { afterMs: number }). Omitting it reschedules to now (previous behavior). A past time clamps to now. Only pending jobs are reschedulable. Batch validation is atomic.
  • Error classes renamed: JobNotTriggerableError / JobsNotTriggerableError → JobNotReschedulableError / JobsNotReschedulableError. New batch error variants JobsNotFoundError and JobsNotReschedulableError for rescheduleJobs. The status property removed from the singular error; the batch variant exposes jobIds.
  • Observability event renamed: jobTriggered → jobRescheduled. Now also emitted on worker retry (not just client reschedules), making it the single signal for “scheduledAt changed.” jobAttemptFailed is now a pure failure event — rescheduledAt/rescheduledAfterMs removed. scheduleAfterMs dropped from jobCreated and jobRescheduled events.
  • StateProvider gains required transactionConcurrency field ("concurrent" | "serialized"). Custom provider authors must declare it.
  • Dashboard: POST /jobs/:id/trigger renamed to /reschedule; “Trigger now” button renamed to “Reschedule”.
  • OTEL: Counter queuert.job.triggered → queuert.job.rescheduled; log entry job_triggered → job_rescheduled.

Features

  • Blocker chain cap: A job can declare at most 100 blocker chains. startChain, startChains, and continueWith throw the new BlockerLimitExceededError when exceeded.
  • Dashboard UX improvements: Infinite scroll on list views (chains, jobs, chain detail), real links on cards (right-click/Cmd-click to open in new tab), and smart back navigation that preserves filters.

Bug Fixes

  • createProcessors type resolution: Previously resolved job type names from the local slice only; now resolves from all registered slices.
  • "incomplete" dedup scope: Multi-step chains that continued past the root job are now correctly matched. Previously, once the root completed, the chain was no longer matched even though it was still running.
  • Concurrent unblock race (@queuert/postgres, @queuert/sqlite): Row-level locks on blocked jobs during unblockJobs prevent a race where concurrent blocker completions could leave dependent jobs stranded in blocked status. job_blocker PK widened to support duplicate chain IDs.

Performance

  • Autovacuum tuning (@queuert/postgres): New migration pins autovacuum_vacuum_threshold = 5000 / scale_factor = 0 on job and job_blocker tables, replacing size-proportional 0.02. Gives predictable vacuum cadence regardless of table size.

Packages

All packages released at 0.15.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.

v0.15.1

What’s Changed

Bug Fixes

  • Saturated workers no longer busy-loop. A worker whose slots were all busy kept polling getNextJobAvailableInMs as fast as the event loop allowed: with a due job in the backlog the poll reported 0ms, so the wait returned immediately and the worker re-entered the loop without being able to take the work. Saturated in-process workers now wait for a slot to free (or for pollIntervalMs) instead of polling.

Performance

  • Postgres job acquisition now uses the acquisition index. acquireJob and getNextJobAvailableInMs matched jobs with type_name IN (...) ordered by scheduled_at, which Postgres cannot satisfy from the (type_name, scheduled_at) index for more than one job type — it fell back to scanning and sorting the entire pending backlog on every acquisition and every poll. Both queries are now per-job-type LATERAL lookups, turning a backlog scan into one index lookup per job type. Workers polling a large Postgres backlog should see a substantial drop in database load.
  • On Postgres, when several job types are polled together, acquisition now picks a job type at random and takes its oldest due job rather than always taking the globally oldest job. This keeps a backlogged job type from starving the others.
  • Postgres getNextJobAvailableInMs no longer takes row locks on jobs scheduled in the future.

Packages

All packages released at 0.15.1: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.

v0.16.0 Latest

What’s Changed

Queuert 0.16 rebuilds the job model around the chain’s head row and reworks attempt handling around a single finish outcome API, with a one-step schema migration from v0.15.1. It also brings faster listing queries, a redesigned dashboard, and a leaner, more consistent public API. This release contains many breaking changes — review the notes below before upgrading.

Breaking Changes

  • New job model — a chain’s head row is now the chain itself. ChainStatus is just running | completed; completed jobs carry continuedToId; running jobs expose attemptAt / attemptBy / attemptUntil. “Lease” is renamed to “attempt” throughout (leaseConfig → attemptConfig with timeoutMs / heartbeatMs, LeaseConfig → AttemptConfig, plus matching log events and metrics).
  • Attempt finalization — handlers end an attempt with complete(async ({ finish }) => finish({ output | continueWith | reschedule })). rescheduleJob() and RescheduleJobError are removed; rescheduling is now a non-error outcome. completeChain’s complete option is renamed to handler.
  • Renames — startChain / startChains → createChain / createChains.
  • Listing API reshaped — flat options with a single status, required typeName, status-dependent orderBy checked at compile time, root → independent; the chainId / jobId / chainTypeName filters are removed; CreatedAtCursor → TimestampWithIdCursor.
  • Deduplication — scope is now required and "incomplete" is renamed to "running" (missing or unknown scopes throw TypeError). windowMs and excludeChainIds are removed.
  • Caller-supplied id collisions now throw instead of being silently ignored.
  • cascade removed from deleteChain / deleteChains.
  • TransactionContextRequiredError removed — a plain Error is thrown instead.
  • wrapHandler now wraps the whole attempt, so a catch around next() observes handler failures.
  • Public type exports pruned — internal helper types are no longer exported; CompletedJob and AttemptConfig are newly exported.

Database Migrations (@queuert/postgres, @queuert/sqlite)

  • All migrations are collapsed into 001_initial_schema. You must be on v0.15.1 before upgrading.
  • PostgreSQL: the upgrade copies tables while v0.15.1 workers keep running; a migration_lock table makes concurrent migrateToLatest() calls safe.
  • SQLite: stop all workers and clients before running the migration.
  • vacuum() is removed, along with SQLite’s checkForeignKeys / checkAutoVacuum options.

Features

  • step transaction primitive inside attempt handlers, with a matching wrapStep middleware hook.
  • Batched lookups — getChains / getJobs, with ChainTypeMismatchError.
  • Type names & counts — listChainTypeNames / listJobTypeNames and countByChainTypeNames / countByJobTypeNames.
  • Worker shutdown signal — in-flight handlers receive "worker_stopping" as the abort reason when the worker stops.

Dashboard

  • Full UI redesign: Overview page, Find by ID (⌘K), status tabs, and themes.
  • New endpoints for type names, counts, and by-ID lookups. Malformed IDs and unknown /api paths return 404; uncaught errors return 500 with an { error } body.

Observability (@queuert/otel)

  • Attempt spans record an abort event; prepare / step / complete spans record exceptions and ERROR status.
  • Breaking: blocker-completion span renamed resolve chain.* → complete chain.*; queuert.attempt.result attribute removed (read the outcome from span status); metric names follow the lease → attempt rename.
  • @opentelemetry/api peer dependency now also accepts ^1.0.0.

Performance & Fixes

  • Faster listing queries via per-type anchoring.
  • PostgreSQL and SQLite skip the redundant RELEASE SAVEPOINT round-trip.
  • Type checking is roughly 20–40% cheaper.
  • AttemptMiddleware typed against a concrete adapter now works.
  • chain.input now correctly reflects the entry job’s input.
  • Fixed a race where a job created with blockers could start pending instead of blocked (#4).

Packages

All packages released at 0.16.0: queuert, @queuert/postgres, @queuert/sqlite, @queuert/redis, @queuert/nats, @queuert/otel, @queuert/dashboard.