v0.3.0
Migration
This pre-1.0 minor requires a database upgrade and caller changes. Follow the
0.3.0 upgrade steps: drain old pollers, apply the
unified SQL migration, move to Node 22/24, update async provider registrations,
replace nullable tenant overrides, handle structured admin mutation results,
and remove unsupported deep imports. Delivery remains at-least-once.
Added
-
Critical per-file coverage gates for poller transitions, admin CAS, and
listener reconnect/shutdown paths, with a documented PostgreSQL regression
contract. CI and release coverage artifacts now include the tested commit,
source/report hashes, and actual installed runtime versions. The Jest harness
also loads Nest 12 ESM dependencies for source unit and PostgreSQL tests. -
README local, async publisher, tenant provider, wakeup, and SQL examples now
compile and run from the installed tarball in strict consumers with and
without optionalpg. CI/release gates verify Nest dependency visibility,
PostgreSQL transactions and tenant restoration, and notification-only
delivery. Examples now register handler dependencies and import the async
publisher's configuration, tenant context, and broker provider modules. -
Runtime startup now validates the required outbox columns, indexes, and
constraints and throwsOutboxSchemaErrorwith stable code
OUTBOX_SCHEMA_MISMATCH, required/actual versions, and missing objects before
poller SQL can fail generically. A unified shipped
src/sql/upgrade-to-current.sqlupgrades exact v0.1.0 and v0.2.1 fixtures
idempotently. -
Admin APIs now expose
listPage()with deterministic
(created_at DESC, id DESC)traversal, an exclusive opaque versioned cursor,
andnextCursor. Invalid cursors throwOutboxCursorErrorwith stable code
OUTBOX_INVALID_CURSOR; the existinglist()Date-filter API remains
available for compatibility. -
Producer envelope failures now throw
OutboxEnvelopeErrorwith stable code
OUTBOX_INVALID_ENVELOPEand machine-readablefield/reasondetails.
Fixed
retryMany()now deduplicates ids and uses 10,000-id statements instead of
risking PostgreSQL's bind limit. Admin cursor, tenant/status, processing-age,
andSENTretention paths have purpose-built indexes, while exact stats use
status-specific aggregates instead of one monolithic history aggregate.- Release workflow authorization now permits real npm publication only for a
matchingv*.*.*tag whose commit is onmain. Manual dispatch is dry-run
only; npm OIDC and GitHubcontents: writelive in separate jobs, and every
action in the privileged workflow is pinned to a reviewed commit SHA. - Runtime options from both
forRoot()andforRootAsync()are now validated
as bounded safe integers and supported enum/transport combinations before
startup. Invalid configuration throws the typed
OutboxConfigurationError; polling-disabled configurations still require a
usable wakeup path at module initialization. - Poller and admin reads now fail closed with the typed
OutboxPersistedInvariantErrorwhen stored status, retry counters, dates, or
JSON object shapes are corrupt, so malformed records cannot reach delivery
callbacks or be exposed as valid admin records. - PostgreSQL wakeup initialization now degrades to polling after client,
connection, orLISTENfailures. Reconnect uses capped exponential backoff,
generation-fences stale callbacks, detaches supported listeners, closes the
replaced client, handles botherrorandend, and leaves no reconnect work
after shutdown. Disabling polling while wakeup is disabled or unavailable
instead throws the typedOutboxWakeupUnavailableErrorwith stable code
OUTBOX_WAKEUP_UNAVAILABLE. - Poll interval, PostgreSQL notification, and manual triggers now share a
single-flight coordinator. Concurrent triggers are coalesced into at most
one queued rerun, background failures are logged without becoming unhandled
rejections, and shutdown drops queued work while waiting for the active poll. emit()andemitMany()now reject invalid dates, BigInt, circular or
non-plain JSON, unsupported JSON values, oversized payloads/headers, blank or
overlong identifiers, and invalid headers before calling the database.
emitMany()prevalidates the complete input and chunks inserts at 1,000 rows
on the same caller-owned transaction, below PostgreSQL and JavaScript
argument limits.- Duplicate event entries in one
@OnOutboxEvent()and duplicate discovery of
the same provider instance/method/event tuple now fail fast, while distinct
handlers retain intentional fan-out.
Changed
-
Development NestJS 11 controls now use exact 11.2.3, with compatible
Jest/Babel and Express dependency refreshes for reported development-only
advisories. Prisma 5/6/7 controls and the production audit-zero gate remain;
the Prisma 7 CLI exception awaits an upstream supported fix (OUT-M22B). -
The package now declares an explicit export map for the CommonJS/type root
and the documented fresh/current SQL migration paths. Accidentaldist/**
and component-migration deep imports are intentionally blocked in 0.3.0;
consumers must import runtime/types from the root and resolve
onlycreate-outbox-table.sqlorupgrade-to-current.sql. -
The development lint toolchain now uses the supported ESLint 10 flat-config
line, TypeScript ESLint 8, and current Prettier compatibility rules. This
removes the legacy ESLint 8 dependency path without changing package runtime
dependencies or the production audit-zero gate. -
The minimum Node.js engine is now 22. Node 20 reached upstream EOL on
2026-03-24 and is removed in 0.3.0. Node 22 and 24 are required runtime
controls; Node 26 remains an allowed-failure pre-LTS canary. -
NestJS 12 is now included in the
@nestjs/commonand@nestjs/corepeer
ranges, paired with@nestjs/schedule12. The exact NestJS 12.0.1 + Schedule
12.0.1 + Prisma 7.10.0 candidate passed strict packed type/module and
PostgreSQL consumer verification before the peer ranges were widened. -
forRootAsync()now separates factory-owned runtime configuration from
top-level Nest registrations. Custom transports and tenant provider classes
are constructed by Nest with dependencies fromimports; factory-returned
transport,tenancy.provider, orisGlobalvalues are rejected instead
of being ignored or instantiated with a bare constructor. The public async
factory type is tightened in 0.3.0. -
Global admin access is now named
OutboxOperatorServiceand documented as a
privileged control-plane API;OutboxAdminServiceremains a deprecated
compatibility alias.OutboxTenantAdminService.forTenant()creates a fixed
tenant scope whose reads, stats, health checks, retries, failures, and purges
all include the expected tenant predicate without adding an RBAC dependency. -
Admin
retry()andmarkFailed()now use compare-and-set source-state
transitions and returnapplied,not_found,conflict, orlost_claim.
markFailed()accepts onlyPENDING; all admin mutations leave active
PROCESSINGclaims untouched. Retry, failure, and purge invariants are now
fixed by an explicit operation matrix. -
Tenant producer provenance is now controlled by
tenancy.policy: 'optional' | 'required' | 'require-match'. Undefined tenant
ids fall back to the configured provider; null, non-string, blank, and
non-canonical whitespace values fail before SQL. Global events use the
explicittenantScope: 'global'escape hatch, andrequire-matchrejects
explicit/provider mismatches. -
Retry failures now persist a PostgreSQL-clock
next_attempt_at; every
poller claims from that stored due time instead of recalculating eligibility
from its local backoff configuration.retry.maxDelaybounds exponential
delay safely, andOutboxRecord.nextAttemptAtexposes the schedule. -
Admin retry keeps
retry_count, clearslast_errorandprocessed_at, and
writesnext_attempt_at = NOW()so the row is explicitly due immediately. -
The README now defines polling, local-handler, and publisher delivery as
at-least-once, documents every known duplicate window, and clarifies that
idempotency_key,partition_key, and OutboxSENTdo not provide consumer
deduplication, FIFO, or downstream-completion guarantees. -
Hook contexts are readonly detached snapshots. The hook contract now states
thatonEmitobserves a staged attempt before caller transaction commit,
documents rollback/no-handler/hook-failure meaning, and directs durable
compliance audit facts to transactional rows or durable events. -
Admin ordering is deterministic for traversal only. Equal transaction
timestamps,UPDATE ... RETURNING, aggregate indexes, concurrent claims,
retries, and callback duration do not provide global, aggregate, or partition
FIFO; strict FIFO remains deferred toOUT-B01. -
Pollers now claim one record on demand and protect its active callback with a
renewable PostgreSQL lease. Recovery only requeues expired leases, does not
consume retry budget, and stale completions require both the original claim
token and an unexpired lease. -
stuckThresholdremains as a deprecated compatibility alias for
lease.duration. Newlease.heartbeatIntervaland
lease.heartbeatFailureToleranceoptions define heartbeat timing and loss. -
Poller claims now use a private PostgreSQL
claim_token. Every poller-owned
SENT, retry, andFAILEDtransition compares the event id,PROCESSING
status, and token; a zero-row compare-and-set is treated as a lost claim and
does not emit success, failure, retry, or dead-letter hooks. -
Publishers, hooks, and local handlers receive detached deep snapshots.
OutboxRecord, dispatch contexts, and handler contexts now expose readonly
properties at compile time. Runtime freezing is intentionally not part of
the contract.
Testing
- Release verification now packs one allowlisted tarball, records its SHA-512
SRI and SHA-256 digest, and passes those exact bytes through every packed
consumer, the Node 24 control, manual dry-run, and npm publish. Reruns skip
an existing version only when registry integrity is identical; post-publish
verification checks npm signatures plus the provenance subject, tag ref,
source commit, and release workflow before the GitHub Release is created. - CI requires Node 22 and 24 controls, including exact NestJS 12.0.1 + Schedule
12.0.1 + Prisma 7.10.0 strict packed PostgreSQL consumers. Release
verification requires the same Node controls, while Node 26 is isolated in
a non-blocking canary until it reaches LTS. - CI and release verification now include Node 22 + NestJS 10.4.22 + Schedule
4.1.2 + exact Prisma 5.22.0. The isolated strict consumer installs the packed
tarball, generates the legacy Prisma client, typechecks public declarations,
loads the shipped SQL asset, and exercises emit/poll/admin state against
PostgreSQL. Prisma 6.19.3 runs through the same packed legacy fixture, while
the existing Prisma 7.10.0 modern consumer remains in place. - A repository-local release policy fixture rejects mutable action refs,
manual real-publish paths, shared npm/GitHub release authority, and verify
jobs with write permissions. - Unit contracts cover invalid sync/async runtime configuration, delivery
transport mismatches, and corrupt persisted rows. PostgreSQL E2E verifies the
new retry, JSON-object, and non-processing-claim CHECK constraints and their
idempotent upgrade. - Unit contracts cover hook rollback/mutation isolation, stable envelope
failures, full-batch prevalidation, bulk chunking, duplicate discovery, and
cursor decoding. PostgreSQL E2E traverses rows with an identical
created_atwithout gaps or duplicates. - PostgreSQL E2E now gates concurrent two-poller initial claims, active lease
heartbeats, expired-lease recovery, stale completions, publisher acceptance
beforeSENTprocess loss, and notification/poll fallback coalescing. The
existing CI and release PostgreSQL jobs run this suite for their supported
runtime tuples. - PostgreSQL E2E now also covers publisher terminal
FAILED, provider-derived
tenant persistence with ambient handler-context restoration, real
LISTEN-before/after readiness, real notification burst coalescing, polling
fallback after notification loss, reconnect generations, shutdown claim
release, mixed retry configurations, and runtime delivery after exact
v0.1.0/v0.2.1 fixture upgrades.
Migration
- Existing 0.1.x and 0.2.x databases must drain old pollers and apply the
unifiedsrc/sql/upgrade-to-current.sqlbefore deploying this runtime. The
additive nullable columns and partial indexes are safe to apply more than
once. The unified upgrade is idempotent but validates the existing table and
intentionally fails on corrupt rows. It makes existing pending/processing
retries due at migration time and rebuilds the pending index around
next_attempt_at.
LegacyPROCESSINGrows with a null lease retain the configured duration as
their recovery threshold. Drain 0.2.x pollers before starting the new runtime
because older pollers neither heartbeat active claims nor persist due times. - Because the required schema migration and readonly public type tightening
affect consumers, and the admin single-record mutation result changed from a
boolean to a discriminated union, this change is targeted at the next
pre-1.0 minor release rather than a patch release. - The additive cursor API/errors and stricter producer envelope validation are
also targeted at the next pre-1.0 minor release. Existing Date filters are
source-compatible but remain range filters rather than pagination cursors. - Raising the Node engine floor and removing Node 20 are also intentionally
targeted at that next pre-1.0 minor release. Node 20 consumers must move to
Node 22 or remain on the 0.2.x release line.