Releases
- 0.5.0:
route(produce(Catalog, { events }), settings)routes several catalog events with shared settings; wire names declared on catalogs and routes reach the codec; transactions at an isolation level with automatic rerun of serialization failures - 0.4.1: PostgreSQL hands out payloads exactly as published, Prisma 5 support, jsonEnvelope fixes, a codec contract for any test framework
- 0.4.0: Prisma 7 on a real database, wake-up at the caller's commit, the Confluent Kafka client, codec contract helpers, record keys, legacy flat envelopes, an application-owned producer
- 0.3.1: internal readability refactor, same behaviour
- 0.3.0: code quality, per-key order across event types, codecs, transactions in NestJS, bounded shutdown, ESLint rules, OpenTelemetry
- 0.2.0: high-throughput delivery, event catalogs, decoding of foreign messages
- 0.1.x: first public release
- How versioning works
0.5.0 (2026-10-27)
A minor release with new API and nothing required when upgrading from 0.4.x: no export removed or renamed, every option additive, no database or default wire-format change, and per-event routes and jsonEnvelope wireTypes work as before. See Upgrading from 0.4.1 to 0.5.0.
- Catalog routes.
route(produce(Catalog, { events }), { transport, destination, retry, timeout })inroutesturns every selected event into a route with the shared settings, where a service publishing several catalog events to one topic used to write one route per event and repeat the destination, retry and timeout. Withoutdestination, each event goes to its own event type, as a plain route does; withoutevents, the whole catalog is routed. The routes it creates are ordinary routes: one handlerrelaybus.transport:<transport>:<destination>per destination (the per-key order and the ids of stored deliveries are the same as with routes written by hand), counted byproduce(...)boot checks and byRelayBusModule.forFeature({ produces }). See Routing a catalog production. - Wire names next to the catalog and the route.
defineCatalog(events, { wireTypes })androute(..., { wireTypes })name the types a wire format uses, by catalog event name (checked by the compiler; the route's names win). The bus hands the names of the events a transport sends to it when the bus is created: the new optionalTransport.declareWireTypesis implemented by the Kafka and RabbitMQ transports, which pass them to ajsonEnvelopecodec through its newwithWireTypes. The codec then writes and reads those events exactly as with its ownwireTypes; a contract test on both transports checks that the bytes are identical.catalogWireTypes(catalog)lists a catalog's names for a codec on the consuming side. See Wire names. - Boot checks with their own codes. An event routed twice to one destination through a catalog route (
RELAYBUS_ROUTE_DUPLICATE_EVENT), a wire name for an unknown event (RELAYBUS_CATALOG_UNKNOWN_EVENT), a clash of wire names on a transport or with the codec's own (RELAYBUS_WIRE_TYPE_COLLISION) and wire names for a transport or codec that cannot map type names (RELAYBUS_WIRE_TYPES_UNSUPPORTED) fail when the bus is created, before any transport connects. Every transport checks its names first and all apply them last, so a refused configuration changes no transport; a transport that two buses use, or that already started, refuses wire names. All four extendConfigurationError. Two plain routes to one destination keep failing with the plainConfigurationError. - Additive API: core
route,catalogWireTypes,CatalogRoute,CatalogRouteOptions,RouteDefinition,DuplicateRouteEventError,UnknownCatalogEventError,WireTypeCollisionError,WireTypesUnsupportedError,DefineCatalogOptions.wireTypes,Transport.declareWireTypes(checks the names and returns the function that applies them);RelayBusOptions.routesacceptsRouteDefinition(a plain route or a catalog route); Kafka and RabbitMQKafkaCodec.withWireTypes/RabbitMqCodec.withWireTypesanddeclareWireTypeson both transports;InMemoryTransport.declaredWireTypesin@relaybus/testing;mergeWireTypesandwithDeclaredWireTypesin@relaybus/core/internal. - Isolation levels with automatic rerun.
bus.transaction(work, { isolationLevel, maxAttempts })and@RelayBusTransaction({ isolationLevel, maxAttempts })run a transaction at'read committed','repeatable read'or'serializable'. At the last two, a serialization failure (SQLSTATE40001, PrismaP2034) rolls the attempt back and runsworkagain in a new transaction, up tomaxAttemptstimes (default 3), and the last failure is thrown unchanged. This lets PostgreSQL's optimistic SERIALIZABLE replacepg_advisory_xact_lockorSELECT ... FOR UPDATEfor write-skew checks such as "count the active rows, check a limit, insert".workmay run more than once, so keep external calls out of it; events of a failed attempt are rolled back with it, andonPublishedand the dispatcher wake-up wait for the final commit.PgExecutorsets the level in theBEGINround trip (no extra statement), TypeORM through its own isolation-level transaction, every other executor withSET TRANSACTION ISOLATION LEVELas the first statement; MongoDB and the in-memory store reject a level with aConfigurationError. A decorated method that asks for a level joins a running transaction only when relaybus opened it at the same level, else it throwsTransactionIsolationConflictError(RELAYBUS_TRANSACTION_ISOLATION_CONFLICT). WithoutisolationLevela transaction runs exactly as in 0.4.1: same statements, same round trips, no rerun. See Serializable transactions. - Additive API: core
TransactionOptions,TransactionIsolationLevel,TRANSACTION_ISOLATION_LEVELS,TransactionIsolationConflictError,RelayBus.currentIsolationLevel(),EventStoreTransactionOptions.isolationLevel, and the optionalEventStore.isolationLevelsandEventStore.isSerializationFailure;@RelayBusTransaction(options); postgresPostgresIsolatedTransactionOptionsand the optionalPostgresExecutor.transactionWithIsolationandPostgresExecutor.isSerializationFailure; a transaction-isolation section in the store contract of@relaybus/testing. - Benchmark against 0.4.1 (quick gate, interleaved; publish 6 pairs, dispatch 3 pairs per cell): publish 0.92–1.01x (batch 1 1.00x, batch 100 1.01x, batch 100 with 5 handlers 0.92x, every interval spans 1.0), dispatch 0.96–1.02x (PostgreSQL 1 instance 1.02x, 4 instances 1.01x, 10 hot keys 0.96x), so speed is unchanged; statements per event and per delivery are identical. MongoDB was not measured for this release: the harness could not load the 0.4.1 side's MongoDB template within its transaction budget. See Performance.
0.4.1 (2026-10-26)
A patch release: no export removed or renamed, every option additive. PostgreSQL users get one column conversion that setup() applies; see Upgrading from 0.4.0 to 0.4.1 for the DDL when you run your own migrations.
- PostgreSQL keeps the payload as published.
payloadandheaderswerejsonb, which re-orders object keys, and the insert also went throughjsonb_to_recordset, so an event read back serialized differently from what the producer published. Both columns are nowjsonand the event insert usesjson_to_recordsetend to end, so the transport and the handlers get the producer's exact text, key order included.setup()converts existing tables in place (idempotent, one table rewrite); rows stored before keep the orderjsonbgave them. The schema marker stays the one 0.4.0 writes, so 0.4.0 and 0.4.1 instances can run side by side and roll back with the defaultsetupStore: true: a 0.4.0setup()on a converted database runs nothing. The other batch parameters (deliveries, key counts, id lists) stay onjsonb_to_recordset. - Payload fidelity is part of the store contract. Every store (PostgreSQL through
pg, Kysely, Knex, TypeORM, Drizzle and Prisma, MongoDB, in-memory) must hand back a payload with unsorted keys at every depth, unicode,1e21,5e-324, the largest safe integers and empty containers exactly asJSON.stringifywrote it, for single and batched appends and for a handler added later. A bus-level test checks the same throughpublishandpublishAll. New tests also show that every built-in Kafka and RabbitMQ codec and both transports write and read payloads byte for byte; they did already. - Prisma 5. The optional
@prisma/clientpeer is^5.0.0 || ^6.0.0 || ^7.0.0(0.4.0:^6 || ^7, which made npm refuse the install next to Prisma 5 or add a second client).createPrismaExecutorworks unchanged on Prisma 5.22; aprisma compat (prisma 5)CI job runs the Prisma tests on it, and CI checks that installing@relaybus/postgresinto an app on Prisma 5.22 leaves its@prisma/clientandprisma generateuntouched. - jsonEnvelope keeps every top-level field type. New
otherFields: { payloadField }keeps the unknown top-level fields that are not read as event headers (numbers, booleans, null, objects, arrays, and text when headers do not live at the top level) unchanged under that payload field (an object payload always gets it,{}when there is nothing to keep), where the event's schema validates them; writing spreads them back over the top level, so the values survive a round trip, and the bytes do too whenbodyOrderlists the fields in wire order. Without it, decode leaves them out as before. - jsonEnvelope
bodyOrdercovers spread fields. Event headers spread byheaders: 'top-level-fields'(and fields kept byotherFields) now take theirbodyOrderposition; before they always came last. - jsonEnvelope
wireTypes. Maps event types to the names a format uses on the wire, so one route carries many types whose wire names are not relaybus event types, without afixedtype per transport. The event type grammar is unchanged; it already accepts snake_case such astransfer_auto_dispatched. - The codec contract runs under any test framework.
@relaybus/testing/codec-contractimports no test framework:assertKafkaCodecCase/assertRabbitMqCodecCasethrow aCodecContractError(codeRELAYBUS_TESTING_CODEC_CONTRACT), andregisterKafkaCodecContract/registerRabbitMqCodecContractregister tests with anydescribeandit.@relaybus/testing/jestregisters with Jest's globals.vitestis an optional peer now;kafkaCodecContractandrabbitMqCodecContractfrom the main entry work with Vitest as before. Compared byte for byte, the decoded payload must also keep the expected key order. - Additive API:
jsonEnvelopeoptionsotherFieldsandwireTypes;@relaybus/testingassertKafkaCodecCase,assertRabbitMqCodecCase,registerKafkaCodecContract,registerRabbitMqCodecContract,CodecContractError,TestFramework, and thecodec-contractandjestentry points. - Benchmark against 0.4.0 (quick gate, 3 interleaved pairs per cell): publish 0.98–1.10x (batch 1 0.98x, batch 100 1.04x, batch 100 with 5 handlers 1.10x, MongoDB 0.99x), dispatch 1.02–1.03x (PostgreSQL 1 instance 1.02x, 10 hot keys 1.02x, MongoDB 1.03x), so speed is unchanged. WAL bytes per event or delivery are 3–11% lower and
lanesupdates per delivery 0.88x on the PostgreSQL dispatch cell. The 4-instance PostgreSQL dispatch cell was not valid on either side (its template drains before the measurement window ends); that is a harness sizing issue, not a regression.
0.4.0 (2026-10-25)
Not breaking: the public API snapshot of every package changes by additions only (no export removed or renamed, checked with git diff of the public-api snapshots against 0.3.1), and nothing in the database, the SQL or the default wire format changes. Nothing to do when upgrading from 0.3; see Upgrading from 0.3 to 0.4 for the optional features.
- Prisma 7 runs on a real database. The Prisma adapter now passes the full event store contract against PostgreSQL with Prisma Client 7.10 on
@prisma/adapter-pg(6.19 is covered in CI too). This found one bug: Prisma raw queries reject avoidcolumn, sosetup()and the trailing notification failed; both statements now return a boolean column.@prisma/clientis an optional peer^6 || ^7. PostgresEventStoreacceptspoolnext toexecutor. Statements outside a transaction (claims, completions, renewals) run on it as prepared statements (a warm dispatch batch parses 0 statements instead of 2 for Prisma, Kysely and Drizzle), and the store listens on it.- Wake-up at the caller's commit. With a
pool, an event published in your ORM transaction is dispatched right after that transaction commits (handler start a median under 10 ms after a commit held back 200 ms). A publish in a transaction relaybus does not own wakes the dispatcher 25, 100 and 250 ms after the latest publish instead of once at 25 ms. Newbus.wake()for stores without notifications. - Kafka:
client: 'confluent'runs@confluentinc/kafka-javascript(its KafkaJS-compatible API), now the recommended client, tested on a real broker next tokafkajs. The default stays'kafkajs'. The peer range is^1.11.0. - Kafka:
producerpublishes and dead-letters through an already-connected producer the application owns (kafkajs or Confluent). The transport never connects or disconnects it. With it,kafkais optional and the transport publishes only;kafkaandgroupIdas well make it consume with its own consumer. - Kafka
jsonEnvelope:recordKey: 'event-key' | 'none' | (event) => string | null(default'event-key', unchanged) andcontentTypeHeader. Only the wire key changes; the outbox order of one event key stays. jsonEnvelope(Kafka and RabbitMQ) describes more in-house formats:occurredAtas'epoch-millis'or'epoch-seconds',extrabody values and headers (constants or functions of the event, written only),bodyOrderfor formats compared byte for byte, an id that falls back to the transport's own (whenMissing: 'record-position') andheaders: 'top-level-fields'. Together they write and read legacy flat envelopes (one topic, many types, no key, no headers) byte for byte.@relaybus/testing:kafkaCodecContractandrabbitMqCodecContract(and the purekafkaCodecMismatches,rabbitMqCodecMismatches) check a codec against captured messages in both directions. The transports' own decode path is exported asdecodeKafkaRecordanddecodeRabbitMqMessage.- Peer and test dependencies moved to the latest versions:
@opentelemetry/api1.10 with the OpenTelemetry SDK 3,@nestjs/*12.2. - Additive API: Kafka
KafkaInjectedProducer,KafkaRecordKey,KafkaRecordOptions,decodeKafkaRecord; RabbitMQdecodeRabbitMqMessage; testing the codec contract functions and their option types;RelayBus.wake();KafkaTransportOptions.kafkais now optional. - Benchmark against 0.3.1 (quick run, 3 interleaved pairs per cell): publish 0.98–1.03x, dispatch 0.99–1.06x, statements per delivery identical. See Performance.
- Known limits: on decode, unknown top-level fields are kept only when they are strings (0.4.1 adds
otherFields); there is no consumer injection (only a producer can be passed in); RabbitMQ has no routing-key override.
0.3.1 (2026-10-25)
A readability release: no behaviour, API, SQL or wire-format change. Nothing to do when upgrading from 0.3.0.
- Every source file is now at most 300 code lines and every function at most 40 (enforced by lint). The largest files shrank from 794 to 294 lines (
relay-bus.ts) and from 772 to 173 lines (dispatcher.ts). - Shared skeletons moved into abstract base classes: one lease settlement flow for the PostgreSQL, MongoDB and in-memory stores, one claim flow for PostgreSQL and MongoDB, and one subscription lifecycle for Kafka and RabbitMQ. Value objects replace repeated primitives with rules (durations, backoff, lease identity, composite keys, bounded integers).
- Proof of no behaviour change: the public API snapshot, the SQL snapshot of every statement and the performance budgets are unchanged; 2,798 tests with 100% coverage.
- Additive type change:
KafkaTransportandRabbitMqTransportnow extend the internalSubscribingTransportbase class. - Benchmark against 0.3.0 (quick gate, 3 interleaved pairs per cell): publish 0.99–1.02x, dispatch 0.98–1.04x, every interval spans 1.0, so speed is unchanged. The 4-instance PostgreSQL dispatch cell was not valid on either side (its template drains before the measurement window ends); that is a harness sizing issue, not a regression.
0.3.0 (2026-10-25)
Follow Upgrading from 0.2 to 0.3. Nothing has to be migrated in the database; read Breaking changes first. What stays stable between releases is described in Stability, what is tested in Compatibility.
What is new
| Area | Change |
|---|---|
| Code quality | The code now follows a strict set of limits: strict tsconfig flags (noUnusedLocals, noImplicitReturns, noPropertyAccessFromIndexSignature and more), lint rules for function size (60 lines), complexity (10), depth (3), at most two positional parameters, one class per file and a [ClassName](methodName) prefix on every log line. The two largest files, the PostgreSQL and MongoDB stores, are split into modules per concern with byte-identical SQL and unchanged filters. 2,773 tests, 100 % coverage unchanged. |
| Errors | Every error class carries a stable code (RELAYBUS_LEASE_LOST, ...), reported on dead letters, retries and observer events and listed on Error codes. Remaining bare throws became coded classes. Kafka and RabbitMQ dead letters carry the error name and code. |
| API surface | A snapshot test pins every export of every package, so a change of the public API is always deliberate. |
| Ordering | Per-key order now holds across event types routed to one destination: all routes to one destination are one handler. Ordering keys. |
| Codecs | Kafka and RabbitMQ take a symmetric codec (cloudEventsBinary, cloudEventsStructured, jsonEnvelope, or your own { encode, decode }) for the wire format both ways. The default stays byte for byte the same. Guide. |
| Typed ordering key | defineEvent(type, { schema, key: (payload) => payload.orderId }) derives the key from the payload; a wrong field fails to compile. |
| Transactions in NestJS | @RelayBusTransaction() opens a transaction around a method, RelayBusTransactionHost hands it to repositories, and every transaction is held to transactionTimeout (default '5s'). Guide. |
| Decorators | @RelayBusConsumer(Event) is the method decorator (default style) and @RelayBusCatalogConsumer(Catalog) the optional class decorator. RelayBusModule.forFeature({ consumes, produces }) fails the boot when a module lacks a consumer or a route. Event catalogs. |
| Bounded shutdown | bus.stop({ timeout }) covers the whole shutdown, force-closes what relaybus opened itself at the deadline and resolves with a StopResult { graceful, abandonedDeliveries, durationMs }. NestJS app.close() is bounded the same way. |
@relaybus/eslint-plugin | New package. no-external-call-in-transaction and publish-requires-transaction, both error in the recommended config. |
@relaybus/otel | New package. OpenTelemetry metrics (publish, claim, handler, end-to-end latency, retries, dead letters, backlog gauges, a counter of undecodable inbound messages) and traces across services. Observability. |
| Observer hooks | Timed hooks (onPublishFinished, onClaimFinished, onHandlerStarted, onHandlerFinished, onTransportSent, onReceiveFinished, onInvalidMessage, onBacklog), skipped entirely when no observer implements them. |
| Testing | Every timer of the packages runs through the Clock, so FakeClock controls them all. |
| Benchmarks | bench:full gains two routed cells (distinct:1000 and hot:10 keyed events sent to an in-process fake transport that acks after 5 ms). |
Breaking changes
| What | 0.2.0 | 0.3.0 | Action |
|---|---|---|---|
| Route handler ids | relaybus.transport:<transport>:<destination>:<eventType> | relaybus.transport:<transport>:<destination> | Update dashboards and code that match route ids (handlers(), listDeliveries({ handler }), HandlerContext.handler, observer events). Stored 0.2.0 deliveries are still sent. |
| Routes to one destination | Different retry or timeout were allowed | They must be equal, and an event type may be routed to a destination once; otherwise new RelayBus throws ConfigurationError | Give such routes the same settings. |
| Transaction time budget | No limit | transactionTimeout, default '5s': a longer bus.transaction or @RelayBusTransaction() method is rolled back with TransactionTimeoutError | Move slow calls out of the transaction, or raise the option. |
bus.stop() | Returns Promise<void>, waits without a deadline | Returns Promise<StopResult>, bounded by timeout (default dispatcher.shutdownTimeout, 30 s) | Only code that types the result as void needs a change. |
| Error names | Five Kafka and RabbitMQ errors reported the name RelayBusError | They report their own class name | Code that matched error.name === 'RelayBusError' on dead letters must use the class name or code. |
| Failure stage | onFailure stages without 'backlog' | New stage 'backlog' | An exhaustive switch over the stage needs a new case. |
Deprecations
Removed in 1.0.0; they still work and behave the same.
@Subscribeis the earlier name of@RelayBusConsumer;@EventConsumerof@RelayBusCatalogConsumer. Their option and metadata types are deprecated too.- The inbound-only
decodeoption ofKafkaTransportandRabbitMqTransport. Usecodec; giving both is a configuration error.
Benchmark: 0.2.0 vs 0.3.0
In short: 0.3.0 is as fast as 0.2.0 on publish and on MongoDB, and about 1.8 times faster for keyed PostgreSQL dispatch in two cells. No cell showed a lost, duplicated or out-of-order delivery. Most cells have one sample per side, so a difference below about 10 % is noise. The full tables and caveats are in Performance: 0.3.0 vs 0.2.0.
Method: pnpm bench:full --base v0.2.0, interleaved on one machine (Apple M4, Docker Desktop, PostgreSQL 17, MongoDB 7, Node 24.3.0), fixed windows, database counters and exactness validation, as described in the benchmark README. The full run (39 minutes, all cells) used commit a8587ff; every sample was valid except the two of the PostgreSQL fan-out cell on four instances, whose preloaded backlog was too small on both sides, so that cell is left out. The publish and mixed-load cells were measured again after two publish fixes, with 2 samples per side and the 0.2.0 side taken from stored 0.2.0 samples.
| Scenario | 0.2.0 per second | 0.3.0 per second | Ratio | Samples |
|---|---|---|---|---|
| PostgreSQL publish, 1 publisher, 1 event per call | 1,543 | 1,573 | 1.02 | 2 / 2 |
| PostgreSQL publish, 32 publishers, 1 event | 6,235 | 6,111 | 0.98 | 2 / 2 |
| PostgreSQL publish, 8 publishers, 1,000 keys | 3,789 | 4,145 | 1.09 | 2 / 2 |
| PostgreSQL mixed load, 5,000 per second, tuned | 4,133 | 4,032 | 0.98 | 2 / 2 |
| PostgreSQL routed keyed dispatch, 1,000 keys | 3,267 | 5,827 | 1.78 | 1 / 1 |
| PostgreSQL keyed dispatch, 1,000 keys, 4 workers | 4,453 | 7,931 | 1.78 | 1 / 1 |
| PostgreSQL and MongoDB publish, all other cells | - | - | 0.94 to 1.09 | 2 / 2 |
| PostgreSQL dispatch, all other cells | - | - | 0.88 to 1.19 | 1 / 1, one cell 3 / 3 |
| MongoDB dispatch, all cells | - | - | 0.98 to 1.06 | 1 / 1, one cell 3 / 3 |
Statements per publish are 4 in both versions (4.07 against 4.07 at one publisher), so the transaction budget costs no extra round trip. Mixed-load p99 ratios are not meaningful at 5,000 per second: both sides are saturated and p99 moved between 79 ms and 10.6 s from run to run.
Two things to know. First, PostgreSQL WAL bytes per delivery were 8 to 11 % higher in four dispatch cells and 5 to 8 % lower in five others (one sample each, except one cell with three), so there is no clear direction yet. Second, the first full run showed 25 % more statements per publish (5 against 4). That was the transaction budget and was fixed before this table was taken, in a13fb7e and 2fb3cde. The publish cells that were not measured again (8 publishers with batches, five handlers, hot keys) were 0.79 to 1.02 in the full run, before the fix.
Known issues
- p99 latency near saturation. Carried over from 0.2.0 and not changed by this release: at about 5,000 events per second on a laptop, end-to-end p99 on PostgreSQL was higher than 0.1.0's. Re-measure with
bench:fullfor your hardware. - Order across the upgrade. Deliveries stored under 0.2.0 route ids are a separate queue from new ones. See Upgrading.
- RabbitMQ and the shutdown deadline. A connection opened with
urlis cut at the deadline, even during the TCP or AMQP handshake. Aconnectfunction you supply getsconnect({ signal })and must honour it; a socket it keeps opening is not stopped by relaybus. Kafka has no forced disconnect, so past the deadline the bus stops waiting for it. TimeoutNegativeWarningon Node 23 and newer.kafkajscan print this warning; it comes from kafkajs, not from relaybus.- ORM adapters do not use prepared statements. Unchanged from 0.2.0; run dispatchers on a
pgpool (Tuning).
0.2.0 (2026-10-24)
Breaking. Completed deliveries are now deleted when they complete instead of being kept. To keep them for listDeliveries, pass completionHistory: true. The 0.1.0 PostgreSQL and MongoDB schemas are upgraded in place by setup(). Follow Upgrading from 0.1 to 0.2; stop every 0.1.0 worker first.
What is new
| Area | Change |
|---|---|
| Delivery path | Workers lease 256 lanes instead of rows, claim and complete in batches, and delete a delivery when it completes. The delivery row is written once and deleted once. Why |
| Throughput | On one laptop against 0.1.0: PostgreSQL publish 1.1 to 1.9x, PostgreSQL dispatch up to 18x, MongoDB dispatch 2 to 26x, about half the WAL per delivery. Numbers and method |
| Deadlock-free locking | One documented lock order for every statement, and permanent stress tests: 10 dispatchers on PostgreSQL and on MongoDB, concurrent publishers, takeovers and keyed ordering. |
| Wake-ups | PostgreSQL LISTEN/NOTIFY (wakeUp: 'notify') starts new work on other processes within milliseconds; at most one NOTIFY per 10 ms per store. wakeUp: 'poll' turns it off. |
| Prepared statements | The statements relaybus repeats are parsed once per connection (preparedStatements, on by default for pg pools). Why |
| Event catalogs | defineCatalog, HandlersOf, defineHandlers, bus.forCatalog, bus.subscribeCatalog and a boot-time coverage check. consume(...) and produce(...) roles let services share one catalog; @EventConsumer in @relaybus/nestjs. Guide |
| Foreign messages | Kafka and RabbitMQ decode option with plainJsonDecoder and decodeCloudEvent (structured mode). Default decoding is unchanged. Guide |
| MongoDB | Lane documents, bulk completion, smaller new documents. |
| Tuning | New options wakeUp, preparedStatements and completionHistory. Tuning guide |
| Tooling | Shared test containers (suite about 6 to 3.5 minutes), an interleaved benchmark harness (pnpm bench:quick, pnpm bench:full), performance budgets that can only go down. |
Verification: 2,292 tests with 100 % statement, branch, function and line coverage; typecheck, lint, build and publint green; 0 lost, 0 duplicate and 0 out-of-order deliveries in the full benchmark.
Known issues
- p99 latency near saturation. At 5,000 events per second on a laptop, end-to-end p99 on PostgreSQL is higher than 0.1.0's (524 ms against 114 ms in the single-sample run), although 0.2.0 accepts more load there. Below saturation (1,000 per second) p99 is far lower. To be addressed in a later release.
- Ordering scope. Order is guaranteed between deliveries of the same handler and the same ordering key. Unkeyed events, and deliveries to different handlers, have no relative order.
bus.stop()has no deadline yet. It waits for running handlers to finish; a handler that never returns holds shutdown until the process manager kills it. Give handlers their own timeouts (timeouton@Subscribe).- ORM adapters do not use prepared statements. Prisma, TypeORM, Drizzle, Knex and Kysely send every statement unprepared, so each publish and claim is parsed again (about 0.3 ms per publish and 1.3 ms per claim of 10 deliveries). Run dispatchers on a
pgpool; see Tuning.
0.1.x
First public release: the transactional outbox, per-handler deliveries, retries, dead letters and ordering keys for PostgreSQL and MongoDB, the Kafka and RabbitMQ transports and the NestJS module. Upgrade with Upgrading from 0.1 to 0.2.
How versioning works
Every push to main with releasable Conventional Commits publishes all seven @relaybus/* packages to npm with one shared version, computed by semantic-release. There is no committed changelog: the GitHub Releases page is the changelog.
- Current release line: 0.2.x. While the version is below 1.0.0, a breaking change bumps the minor version.
featcommits bump the minor version;fix,perf,refactorandrevertbump the patch version (a refactor changes the published code, never its behaviour).docs,chore,ci,test,buildandstyledo not release.- Prereleases are published from the
nextandbetabranches under the matching npm dist-tags. - Releases are published by CI with npm Trusted Publishing (OIDC) and provenance, so a tarball links back to the workflow run that built it.
Install a specific version
pnpm add @relaybus/core@0.3.0 @relaybus/nestjs@0.3.0 @relaybus/postgres@0.3.0Keep all @relaybus/* packages on the same version.