Skip to content

Changelog

Version v0.12.0

What’s Changed

Features

  • Onion-style attempt middleware. New AttemptMiddleware API with wrapHandler / wrapPrepare / wrapComplete hooks that wrap each phase of an attempt and inject typed context. Middleware now lives per-Processors slice (createProcessors({ attemptMiddleware })) instead of running worker-wide.
  • Concurrent SELECT reads. New AsyncRwLock lets pure-SELECT queries run in parallel while writers stay serialized. End-to-end throughput improves on every adapter — for example, SQLite better-sqlite3 ~7,177 → ~8,396 jobs/s and Postgres pg notify ~1,536 → ~1,948 jobs/s.
  • Concurrent transaction-hook flush. createTransactionHooks dispatches flush and discard via Promise.allSettled instead of sequentially. Cross-hook ordering is no longer guaranteed; in-tree hooks are unaffected.
  • Cached prepared statements. Pass an optional id to sql() and built-in providers cache the prepared statement (postgres.js prepare: true, pg query.name, SQLite db.prepare cache). Dynamic SQL still runs unprepared.
  • Adapter close() lifecycle. NotifyAdapter and StateAdapter now expose an idempotent close() for explicit teardown.
  • Validation-adapter conformance suite. New runValidationAdapterConformance runner exported from queuert/conformance for validation-adapter authors. Mirrors runStateAdapterConformance in shape.
  • UnknownJobTypeError. Raised by merged JobTypes slices when a referenced type name isn’t owned by any slice.

Breaking Changes

Slice-based registries

  • JobTypeRegistry / JobTypeProcessorRegistry → JobTypes / Processors.
  • createClient and createInProcessWorker now accept a single slice or a readonly array of slices and merge them inline.
  • Standalone merge helpers (mergeJobTypeRegistries, mergeJobTypeProcessorRegistries) removed.
  • createJobTypeRegistry → createJobTypes; defineJobTypeRegistry → defineJobTypes; DefineJobTypes → JobTypeDefs; JobAttemptMiddleware → AttemptMiddleware.

Worker option restructuring

  • pollIntervalMs is now a top-level option on createInProcessWorker.
  • Per-type backoffConfig / leaseConfig defaults move onto createProcessors.
  • The old worker-loop backoffConfig (controlling crash recovery, not retries) is renamed to recoveryBackoffConfig.
  • jobTypeProcessorDefaults is gone.

Chain naming standardization

  • Drop Job from JobChain everywhere: types (Chain, ChainStatus, CompletedChain, ResolvedChain, ChainData, ChainNotFoundError), client methods (startChain(s), getChain, awaitChain, listChains, listChainJobs, completeChain, deleteChain(s)).
  • StateAdapter getters drop ById: getJob, getChain, listChains, listChainJobs, deleteChains.
  • NotifyAdapter events: notifyChainCompleted, listenChainCompleted.
  • ObservabilityAdapter events: chainCreated, chainCompleted, chainDeleted, chainDuration.
  • Filter parameters: excludeJobChainIds → excludeChainIds; jobChainId → chainId; jobChainTypeName → chainTypeName.
  • Log entry types and messages: "chain_created" / "chain_completed" / "chain_deleted".
  • OTEL metric names renamed: queuert.job_chain.{created,completed,deleted,duration} → queuert.chain.{...}. Update dashboards and alerts that reference the old names. Metric attributes (chainTypeName) are unchanged.
  • Schema migration 20260430000000_rename_chain_indexes runs automatically via migrateToLatest(). Postgres uses ALTER INDEX … RENAME TO; SQLite drops + recreates. Three indexes drop the job_ prefix.

Adapter lifecycle and wake hints

  • NotifyAdapter and StateAdapter now require an idempotent close() that cascades into the underlying provider when one is owned. After close(), notify/listen and most state methods reject.
  • Provider-level close() is optional and only present on resource-owning providers (e.g. createPgPoolNotifyProvider). Pass-through providers over caller-owned pools/clients omit it.
  • PgPoolNotifyProvider type removed — createPgPoolNotifyProvider now returns plain PgNotifyProvider.
  • notifyJobScheduled(typeName) no longer takes a count. Wake-hint budgets move to provideWakeHint(typeName, count) and consumeWakeHint(typeName), keyed by type name and additive across concurrent publishers.
  • Application code using built-in adapters should explicitly close them before tearing down caller-owned pools/clients.

Concurrent reads

  • createAsyncLock / AsyncLock → createAsyncRwLock / AsyncRwLock with acquireRead() / acquireWrite() returning a Disposable-compatible LockHandle. Idiomatic usage: using h = await lock.acquireWrite();.
  • SqliteStateProvider.executeSql and PgStateProvider.executeSql signature changes: params is now required (was optional); new required paramTypes: Record<number, RuntimeType> and readOnly: boolean fields.
  • Custom SQLite providers must consult readOnly to choose between acquireRead() and acquireWrite() — wiring everything to an exclusive lock silently disables concurrent reads.
  • @queuert/typed-sql’s sql() now takes readOnly via the options object instead of as a second positional argument.

Lock option replaces FOR UPDATE getters

  • getJobForUpdate and getLatestChainJobForUpdate removed.
  • Pass lock: "exclusive" to getJob / getChain instead.
  • getChain({ lock: "exclusive" }) now locks only the latest job in the chain (the rootJob if no continuation, otherwise the last continuation). The rootJob’s chainTypeName is immutable, so locking it earned nothing. Return shape unchanged.

Cached prepared statements (custom providers)

  • Custom PgStateProvider / SqliteStateProvider see an optional id?: string on executeSql. They continue to type-check unchanged and execute every statement unprepared. To opt into caching, accept id and use it as a cache key / pg query.name / postgres.js prepare toggle.
  • PgBouncer / Supavisor users (transaction-mode poolers) should write a custom provider that ignores id, since server-side prepared statements break under those proxies.

Smaller surface tweaks

  • RescheduleJobError no longer carries cause: undefined when constructed without an explicit cause. 'cause' in err is now false when not supplied (matching every other queuert error). Value-level (err.cause) is still undefined.
  • BaseTxContext is now exported only from queuert. The duplicate re-export from queuert/internal is removed. @queuert/postgres now imports it from queuert to match @queuert/sqlite.

TypeScript flags every wrong call site at compile time across all of these — there is no runtime fallback or deprecation period.

Bug Fixes

  • @queuert/postgres: addJobsBlockersSql now acquires FOR UPDATE on the latest job of each blocker chain (via a new locked_blocker_chain_latest CTE) before inserting blocker rows. Closes a race where a blocker chain could complete between the status read and the blocker insert and leave the blocked job stuck. No schema change.

Packages

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