Skip to content

All versions since v0.9.4

v0.9.4

What’s Changed

Runtime Safety

  • Mutating client methods (startJobChain, startJobChains, deleteJobChains, triggerJob, completeJobChain) now enforce that a transaction context from runInTransaction is provided at runtime, throwing TransactionContextRequiredError if omitted. This matches the existing TypeScript type requirements and ensures consistent behavior for JavaScript callers.
  • New exported error class TransactionContextRequiredError for specific error handling.

Bug Fixes

  • Dashboard chain-delete and job-trigger routes now correctly wrap mutating calls in a transaction context.

Packages

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

v0.9.5

What’s Changed

Features

  • Deduplication: excludeJobChainIds option — Skip specific chain IDs during deduplication matching, useful for recurring self-scheduling jobs that would otherwise deduplicate against their own still-incomplete chain
  • Dashboard: Cascade delete for job chains — New “Cascade delete” checkbox in the delete dialog that removes all transitive blocker chains. Also available via ?cascade=true query parameter on DELETE /api/chains/:id
  • Client type covariance — Client<A | B> is now assignable to Client<A>, fixing type errors when passing multi-job-type clients to code that only cares about a subset
  • Compile-time validation for createJobTypeProcessorRegistry — TypeScript now produces a clear error naming missing job types if the client doesn’t cover all required types
  • New public exports — BaseTxContext, HookDef, and TransactionHooksSavepoint are now exported from queuert
  • Cleanup guide and example — New documentation and showcase example for cleaning up completed job chains

Breaking Changes

  • createDashboard() is now async — callers must await it
  • DeduplicationOptions is now generic (DeduplicationOptions<TJobId>)

Packages

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

v0.10.0

What’s Changed

Features

  • Postgres vacuum support: New vacuum() method on the Postgres state adapter for on-demand dead-tuple reclamation on job tables. A new vacuum_tuning migration (applied automatically by migrateToLatest()) configures fillfactor = 75 and aggressive per-table autovacuum settings to reduce table bloat automatically
  • SQLite vacuum support: New vacuum() method on the SQLite state adapter for on-demand page reclamation via PRAGMA incremental_vacuum. migrateToLatest() now validates that auto_vacuum = INCREMENTAL is set on the database — this pragma must be configured before table creation

Bug Fixes

  • NATS notification buffering: The NATS notify adapter now flushes the connection after each publish, ensuring timely delivery of job-scheduled, chain-completed, and ownership-lost notifications

Improvements

  • Relaxed OpenTelemetry peer dependency: @opentelemetry/api peer dependency relaxed from ^1.9.0 to ^1.0.0, supporting a wider range of OpenTelemetry versions

Packages

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

v0.11.0

What’s Changed

Features

  • Public in-process adapters. createInProcessStateAdapter and createInProcessNotifyAdapter moved from queuert/internal to the main queuert entry. The in-process state adapter was rewritten around per-type sorted sets and a journal-based rollback log.
  • New queuert/conformance subpath export. Framework-agnostic conformance runner (runStateAdapterConformance, runNotifyAdapterConformance, ConformanceError, ConformanceReport) with a built-in expect shim, for users authoring custom adapters. Documented under Advanced → Custom Adapters.
  • client.deleteJobChain(id, …) — new singular API, symmetric to deleteJobChains. Returns the deleted chain or undefined if missing; throws BlockerReferenceError if external jobs depend on it; supports cascade.
  • client.triggerJobs(ids, …) — new plural API. Atomic validation: throws JobNotFoundError / JobNotTriggerableError for the first invalid job before any job is triggered; returns jobs in input order; empty input returns [].
  • Redis Cluster support (@queuert/redis). SET+PUBLISH Lua script passes the pub/sub channel via ARGV instead of KEYS, avoiding CROSSSLOT rejection on Redis Cluster. New examples for ioredis and node-redis cluster clients.
  • Optional withSavepoint hook on PgStateProvider and SqliteStateProvider — drivers that track transaction state client-side (e.g. postgres.js) can override the default SAVEPOINT / RELEASE / ROLLBACK TO SQL path with native savepoint support.
  • Job<…> gained a TOutput generic parameter — the completed variant now carries output.
  • New guides and examples: Prioritization guide (with showcase-multiworker-prioritization example) and Processing Reliability / error recovery guide (with showcase-error-recovery example).

Breaking Changes

  • runInTransaction renamed to withTransaction on StateAdapter, PgStateProvider, SqliteStateProvider.
  • HookDef renamed to HookDefinition.
  • createPgNotifyAdapter / createRedisNotifyAdapter: option provider renamed to notifyProvider.
  • createInProcessStateAdapter and createInProcessNotifyAdapter are now async and must be await-ed.
  • In-process adapters moved from queuert/internal to queuert.
  • client.deleteJobChain no longer throws JobChainNotFoundError for missing chains — it returns undefined instead.
  • StateAdapter contract: triggerJob replaced by triggerJobs (bucketed triggered / notFound / notTriggerable result); deleteJobChains returns { deleted, blockerRefs }; the adapter enforces blocker validation in one all-or-nothing statement.
  • executeSql in PgStateProvider / SqliteStateProvider now takes typed SQL hints (paramTypes, columnTypes) instead of a boolean returns flag. Custom providers must update accordingly.
  • pgLiteral removed from @queuert/postgres entry; sqliteLiteral removed from @queuert/sqlite entry.

Packages

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

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.

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.