Changelog
Version v0.12.0
What’s Changed
Features
- Onion-style attempt middleware. New
AttemptMiddlewareAPI withwrapHandler/wrapPrepare/wrapCompletehooks that wrap each phase of an attempt and inject typed context. Middleware now lives per-Processorsslice (createProcessors({ attemptMiddleware })) instead of running worker-wide. - Concurrent SELECT reads. New
AsyncRwLocklets pure-SELECTqueries 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.
createTransactionHooksdispatchesflushanddiscardviaPromise.allSettledinstead of sequentially. Cross-hook ordering is no longer guaranteed; in-tree hooks are unaffected. - Cached prepared statements. Pass an optional
idtosql()and built-in providers cache the prepared statement (postgres.jsprepare: true, pgquery.name, SQLitedb.preparecache). Dynamic SQL still runs unprepared. - Adapter
close()lifecycle.NotifyAdapterandStateAdapternow expose an idempotentclose()for explicit teardown. - Validation-adapter conformance suite. New
runValidationAdapterConformancerunner exported fromqueuert/conformancefor validation-adapter authors. MirrorsrunStateAdapterConformancein shape. UnknownJobTypeError. Raised by mergedJobTypesslices when a referenced type name isn’t owned by any slice.
Breaking Changes
Slice-based registries
JobTypeRegistry/JobTypeProcessorRegistry→JobTypes/Processors.createClientandcreateInProcessWorkernow accept a single slice or areadonlyarray of slices and merge them inline.- Standalone merge helpers (
mergeJobTypeRegistries,mergeJobTypeProcessorRegistries) removed. createJobTypeRegistry→createJobTypes;defineJobTypeRegistry→defineJobTypes;DefineJobTypes→JobTypeDefs;JobAttemptMiddleware→AttemptMiddleware.
Worker option restructuring
pollIntervalMsis now a top-level option oncreateInProcessWorker.- Per-type
backoffConfig/leaseConfigdefaults move ontocreateProcessors. - The old worker-loop
backoffConfig(controlling crash recovery, not retries) is renamed torecoveryBackoffConfig. jobTypeProcessorDefaultsis gone.
Chain naming standardization
- Drop
JobfromJobChaineverywhere: types (Chain,ChainStatus,CompletedChain,ResolvedChain,ChainData,ChainNotFoundError), client methods (startChain(s),getChain,awaitChain,listChains,listChainJobs,completeChain,deleteChain(s)). StateAdaptergetters dropById:getJob,getChain,listChains,listChainJobs,deleteChains.NotifyAdapterevents:notifyChainCompleted,listenChainCompleted.ObservabilityAdapterevents: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_indexesruns automatically viamigrateToLatest(). Postgres usesALTER INDEX … RENAME TO; SQLite drops + recreates. Three indexes drop thejob_prefix.
Adapter lifecycle and wake hints
NotifyAdapterandStateAdapternow require an idempotentclose()that cascades into the underlying provider when one is owned. Afterclose(), 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. PgPoolNotifyProvidertype removed —createPgPoolNotifyProvidernow returns plainPgNotifyProvider.notifyJobScheduled(typeName)no longer takes a count. Wake-hint budgets move toprovideWakeHint(typeName, count)andconsumeWakeHint(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/AsyncRwLockwithacquireRead()/acquireWrite()returning aDisposable-compatibleLockHandle. Idiomatic usage:using h = await lock.acquireWrite();.SqliteStateProvider.executeSqlandPgStateProvider.executeSqlsignature changes:paramsis now required (was optional); new requiredparamTypes: Record<number, RuntimeType>andreadOnly: booleanfields.- Custom SQLite providers must consult
readOnlyto choose betweenacquireRead()andacquireWrite()— wiring everything to an exclusive lock silently disables concurrent reads. @queuert/typed-sql’ssql()now takesreadOnlyvia the options object instead of as a second positional argument.
Lock option replaces FOR UPDATE getters
getJobForUpdateandgetLatestChainJobForUpdateremoved.- Pass
lock: "exclusive"togetJob/getChaininstead. getChain({ lock: "exclusive" })now locks only the latest job in the chain (the rootJob if no continuation, otherwise the last continuation). The rootJob’schainTypeNameis immutable, so locking it earned nothing. Return shape unchanged.
Cached prepared statements (custom providers)
- Custom
PgStateProvider/SqliteStateProvidersee an optionalid?: stringonexecuteSql. They continue to type-check unchanged and execute every statement unprepared. To opt into caching, acceptidand use it as a cache key / pgquery.name/ postgres.jspreparetoggle. - 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
RescheduleJobErrorno longer carriescause: undefinedwhen constructed without an explicit cause.'cause' in erris nowfalsewhen not supplied (matching every other queuert error). Value-level (err.cause) is stillundefined.BaseTxContextis now exported only fromqueuert. The duplicate re-export fromqueuert/internalis removed.@queuert/postgresnow imports it fromqueuertto 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:addJobsBlockersSqlnow acquiresFOR UPDATEon the latest job of each blocker chain (via a newlocked_blocker_chain_latestCTE) 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.