Skip to content

Latest commit

 

History

History
185 lines (146 loc) · 40.4 KB

File metadata and controls

185 lines (146 loc) · 40.4 KB

Migrating to 2.0

Every @node-ts/bus package is released as 2.0.0. The adapters peer on @node-ts/bus-core ^2.0.0, so upgrade all the @node-ts/bus-* packages you use together. Each package's CHANGELOG.md has the full list of changes.

All packages

  • Node.js 24 or later is required. Every package declares engines.node >=24 and is compiled for ES2024.
  • Import only from the package root. Each package now has an exports map. Deep imports such as @node-ts/bus-core/dist/service-bus/error fail at runtime with ERR_PACKAGE_PATH_NOT_EXPORTED, and TypeScript can't resolve them under moduleResolution node16, nodenext or bundler. Import from @node-ts/bus-core (or the adapter's root) instead. The errors the packages throw, such as BusAlreadyInitialized and bus-mongodb's WorkflowStateNotFound, are now exported there. If you need something else that isn't exported, please open an issue.
  • A bus creates nothing when it starts. initialize() used to create the queues, topics, subscriptions, exchanges, tables and indexes the bus needs. It now only checks they exist, with read-only calls, and throws ResourcesNotProvisioned naming what's missing (see Provisioning):
    • In production, create them at deploy time, with deploy credentials, by running bus provision <module> from @node-ts/bus-cli (or calling bus.provision()) on a module that exports your BusConfiguration, or a function that returns one. Then remove the create permissions from the service: each adapter's page lists the runtime permissions it needs, which bus provision --dry-run --permissions prints.
    • For local development and tests, add .withAutoProvision() to the configuration to keep creating everything at initialize().
    • If the service's credentials can't describe its resources, add .withResourceVerification(false).
    • A topic or exchange is now provisioned for every message in the bus' message types, not only those it handles or has sent, so services can be deployed in any order.
    • Pass a send-only bus the message types of what it sends with withMessageTypes(). Publishing no longer creates a topic or exchange the first time a message is sent, so without them a send-only service's topics aren't provisioned and its sends fail.
  • ES modules get their own entry point. import loads an ES module entry and require loads the CommonJS build. You don't need to change anything, and named imports like import { Command } from '@node-ts/bus-messages' work from ES modules. The ES module entry re-exports the CommonJS build, so code that mixes import and require still gets one copy of each class.

@node-ts/bus-class-serializer

The package is removed, along with class-transformer and reflect-metadata. The default JsonSerializer in @node-ts/bus-core now restores Dates, Maps, Sets, bigints and class instances at any depth from message types that bus generate-message-types (in the new @node-ts/bus-cli) generates from your TypeScript source. To move over:

  1. Remove @node-ts/bus-class-serializer, class-transformer and reflect-metadata from your dependencies, and the import 'reflect-metadata' line from your entry point.

  2. Remove the @Type(...) decorators (and any other class-transformer decorators) from your messages and workflow state. If nothing else uses decorators, also remove experimentalDecorators and emitDecoratorMetadata from your tsconfig.

  3. In the package that declares your messages, install @node-ts/bus-cli as a dev dependency, generate the message types, and export them:

    npm i --save-dev @node-ts/bus-cli typescript
    npx bus generate-message-types --entry 'src/**/*.ts' --out src/message-types.generated.ts
    export * from './message-types.generated'

    Then pass the generated messageTypes of every message library a service handles, and its own, to its bus:

    import { messageTypes as orderMessageTypes } from '@my-org/order-messages'
    import { messageTypes } from './message-types.generated'
    
    Bus.configure().withMessageTypes(orderMessageTypes, messageTypes)

    Add the command to your prebuild script, and --check to CI (see Generating message types). Include the files that declare your workflow state if you want their Dates and classes restored too.

  4. Remove .withSerializer(new ClassSerializer()). The default serializer uses the message types the bus was given. If the service uses several message libraries, generate the types in each and pass them all to withMessageTypes().

The wire format doesn't change: Dates are ISO strings, Maps are objects and Sets are arrays, as with class-transformer, so messages already in your queues and workflow state already persisted are read the same way. Fields that class-transformer silently left as strings because they had no @Type are now restored too.

With @node-ts/bus-mongodb, Dates in workflow state are now saved as ISO strings rather than BSON dates, as they already were without ClassSerializer. State saved with BSON dates is still read back as Dates.

Two things behave differently:

  • Constructors aren't run. class-transformer called the constructor with no arguments, so field initializers filled in fields missing from the payload. Restored objects are now created from the class' prototype, so those fields stay undefined. Don't rely on constructor logic or defaults in messages.
  • Unsupported field types fail generation instead of failing silently at runtime: functions, unions of types that are restored differently such as Date | string, generic classes, fields typed as an abstract class, classes that aren't exported, and so on. The generator lists each one.
  • Values are restored as their declared class, as with @Type without a discriminator: a CardPayment in a field declared as Payment comes back as a Payment.

@node-ts/bus-core

  • JsonSerializer no longer runs constructors. It creates the received message, or the workflow state read by a persistence adapter, from its class' prototype and copies the parsed fields onto it. Field initializers no longer fill in fields missing from the payload, and constructors that need arguments no longer throw.

  • Maps, Sets and bigints are now written as plain JSON (an object of the entries, an array and a string) rather than being lost or throwing: JSON.stringify wrote a Map or Set as {} and threw on a bigint. Without generated message types they're read back as that plain JSON.

  • Every bus that receives messages needs withMessageTypes(). Pass it the generated message types (see above). At initialize(), a bus with handlers or workflows throws MessageTypesMissing, naming each handled message and workflow state that has no entry, whichever serializer it uses. Send-only buses and buses with no handlers aren't checked, and messages handled by withCustomHandler are exempt. In a plain JavaScript project, write the entries by hand: { messages: { 'my-app/thing': 'Thing' }, types: { Thing: { fields: {} } } }.

  • Each bus is isolated from other buses in the same process. A bus used inside another bus' handler no longer inherits the correlationId or sticky attributes of the message being handled, and its failMessage() and returnMessage() throw FailMessageOutsideHandlingContext / ReturnMessageOutsideHandlingContext instead of acting on the other bus' message. Use the handler context (ctx.send, ctx.failMessage(), ...) or the bus that's handling the message.

  • messageHandlingContext is no longer exported. Each bus has its own. To read the message being handled outside a handler, e.g. in middleware or code a handler calls, use bus.getHandlingContext(). Handlers get the same details from their arguments and context.

  • A transport instance can only be used by one bus. It holds one queue and one connection, so build() throws TransportAlreadyInUse when another bus already uses it. Create a transport per bus, or use withConcurrency() for more consumers. Serializers and persistence can still be shared, and a shared persistence is disposed when the last bus that uses it is disposed.

  • Serializer.deserialize and toClass take the bus' message types as an optional last argument. A custom serializer can use them to restore nested types the way JsonSerializer does.

  • Persistence adapters store and return plain JSON values. saveWorkflowState gets workflow state already converted with toPlain, and getWorkflowState returns state as it was stored; the bus restores its classes with its own serializer and message types. A custom persistence should stop calling coreDependencies.serializer. CoreDependencies also gains messageTypes.

  • defaultLoggerFactory is replaced by createDefaultLoggerFactory(), which each bus calls for its own factory.

  • Configure the bus before build(). asSendOnly and every with* method on BusConfiguration now throw BusAlreadyInitialized when called after build(). withConcurrency, withContainer, withRetryStrategy (now withRecoverability), withReceiver, withMessageReadMiddleware (now withMiddleware) and withInterruptSignals (formerly withAdditionalInterruptSignal) used to be silently ignored at that point, so move any such calls before build().

  • Handlers get a HandlerContext. Function handlers and Handler.handle are called with (message, attributes, ctx), and class workflow handlers with (message, workflowState, attributes, ctx). Use ctx.send and ctx.publish instead of capturing the bus or injecting it. Handlers that declare fewer parameters don't need to change, but code that calls a FunctionHandler, Handler or CustomHandler directly, such as a unit test, must now pass a context; a plain object with correlationId, send, publish, failMessage and returnMessage will do.

  • WorkflowHandler parameters are (message, workflowState, attributes). The type used to say (message, attributes, state), but the bus always called handlers in the new order. If you typed a handler against the old order, swap the parameters.

  • Message classes need a static NAME equal to their $name. The bus reads the name from NAME without constructing the class, so constructors with required arguments or side effects are no longer run at registration. A class with only $name = 'my-app/thing' no longer type checks with handlerFor, startedBy, when or a class handler's messageType, and throws MessageNameMissing at registration in plain JavaScript. Add static NAME = 'my-app/thing' and set $name = Thing.NAME. A subclass needs its own NAME too: one that inherits its parent's throws MessageNameInherited, since it would otherwise be routed as the parent. Message types are now typed as MessageDeclaration from @node-ts/bus-messages (a message class or a defineCommand/defineEvent definition) rather than ClassConstructor.

  • SystemMessageMissingResolver is replaced by MessageNameMissing. It's thrown when a message type has no static NAME, or is undefined, which usually means a circular import. A message from another system that has no $name is still handled with withCustomHandler and a resolver.

  • withAdditionalInterruptSignal is replaced by withInterruptSignals(signals), which replaces the default SIGINT and SIGTERM rather than adding to them. Change .withAdditionalInterruptSignal('SIGUSR2') to .withInterruptSignals(['SIGINT', 'SIGTERM', 'SIGUSR2']). Pass [] to let your host own shutdown.

  • Class handlers with no constructor arguments no longer need a container. Without withContainer, the bus constructs them with new, as it does for class workflows. build() now throws ContainerNotRegistered only for a class handler whose constructor takes arguments, and the error names that class. If the constructor throws, the message fails with ClassHandlerNotResolved.

  • Handler errors say what failed. HandlerDispatchRejected's message now lists each handler's error and its cause is set, and ContainerNotRegistered and ClassHandlerNotResolved name the class handler (classHandlerName, the first constructor argument of both). The plain Errors thrown by the workflow registry are now WorkflowRegisteredAfterInitialization, WorkflowNameAlreadyRegistered and WorkflowStateNotProvided; code that matched their message text should check the class instead.

  • Workflow handler errors are wrapped in WorkflowHandlerFailed. A failed startedBy or when handler, or a failure to save the state it returned, now appears in HandlerDispatchRejected's rejections as a WorkflowHandlerFailed naming the workflow, the instance's $workflowId and the message, with the original error as its cause. A failed when handler used to be a nested HandlerDispatchRejected, and a failed startedBy handler the error itself, so code that checked those rejections should read cause instead.

  • Lifecycle hooks and read middleware are replaced by withMiddleware(). The eight emitters on BusInstance (beforeSend, beforePublish, afterSend, afterPublish, afterReceive, beforeDispatch, afterDispatch and onError), their payload types (BeforeSend, AfterSend, OnError and so on), withMessageReadMiddleware(), MiddlewareDispatcher and TypedEmitter are removed, and Middleware and Next are now the async middleware types below. Register a BusMiddleware with incoming, handler and outgoing stages instead (see Middleware):

    • withMessageReadMiddleware(fn) → withMiddleware({ incoming: (ctx, next) => fn(ctx.transportMessage, next) }). A middleware must now return a promise, so make a synchronous one async. An incoming middleware that doesn't call next() now has its message deleted, where read middleware left it in flight.
    • beforeSend / beforePublish → an outgoing middleware, before await next(). It gets { kind: 'send' | 'publish', message, attributes, headers }.
    • afterSend / afterPublish → an outgoing middleware, after await next(). Inside a handler that resolves once the message is buffered in the handler's outbox, not when it's sent.
    • afterReceive → an incoming middleware, before next(). afterDispatch → after await next().
    • beforeDispatch → a handler middleware, which wraps each handler and gets its handlerName.
    • onError → an incoming middleware that wraps await next() in a try/catch and rethrows. Not rethrowing marks the message handled.
  • Transport.send and Transport.publish take an optional third argument, TransportSendOptions, with the headers set by outgoing middleware. A custom transport keeps working without it; write the headers natively to support them. To reject a header name the transport uses itself, implement the new optional assertSendOptions(sendOptions), which the bus calls before buffering or sending a message, and throw TransportHeaderReserved there.

  • handlerFor takes the attributes type second. Its type parameters are now <TMessage, TAttributes, THandler>, so code that passed the handler type as the second type argument must move it to the third. Handlers may now return any value.

  • Class workflow handler names are type checked. startedBy(Message, 'handler') and when(Message, 'handler') only compile when the named method takes that message (and the state, attributes and HandlerContext it's called with) and returns changes to the workflow state or nothing, with no fields that aren't in the state. Most handlers the compiler now reports would have failed or saved the wrong fields at runtime, so fix them. Some code that worked is rejected too:

    • configureWorkflow must type its mapper with its own workflow class, such as WorkflowMapper<OrderState, OrderWorkflow>. With any or this as the workflow type no handler name compiles, and a mapper typed with a different workflow class doesn't compile.
    • Handler methods must be public. Make protected or private handlers public.
    • A generic workflow (class OrderWorkflow<TState extends OrderState> extends Workflow<TState>) must type its mapper with a concrete state, such as WorkflowMapper<OrderState, OrderWorkflow<OrderState>>, since a handler can't be checked against a state that's still a type parameter.

    WorkflowHandler's parameters are now required, completeWorkflow() and discardWorkflow() return WorkflowStateChange<TState>, and WorkflowMapper's onStartedBy and onWhen store handler names as string (OnWhenHandler no longer takes type arguments).

  • Class workflows are only created to handle a message. When the bus provisions or initializes, it reads configureWorkflow() from an instance created from the class' prototype, without running its constructor, instead of first resolving the workflow from the container, or constructing it. A configureWorkflow() that uses the workflow's fields or injected dependencies now finds them undefined, and if it throws or passes the mapper an undefined message, handler name or lookup, provision() or initialize() throws WorkflowConfigurationFailed naming the workflow. A configureWorkflow declared as an arrow function property must become a method. Map messages with only the mapper and values that don't come from the instance, and use dependencies in the handler methods. A workflow the container can't resolve now fails each message it handles with WorkflowHandlerFailed, as a class handler does, rather than failing initialize(). testWorkflow() reads configureWorkflow() the same way, so its createWorkflow is only called for each message.

  • Transport has a required endpointName, the name of the queue the bus receives from. A custom transport must add it, such as get endpointName() { return this.configuration.queueName }. InMemoryQueue takes an optional endpointName, in-memory by default.

  • Every message the bus sends has a messageId and a sentAt in its MessageAttributes: a new UUID and an ISO 8601 timestamp, unless you pass your own to send or publish. Unlike correlationId, they aren't copied from the message being handled. A custom transport must carry them with the message and keep them across retries and in the dead letter queue (see Custom transports). Tests that compare a handler's attributes with toEqual may need toMatchObject or expect.objectContaining.

  • Retry strategies are replaced by a recoverability policy. withRetryStrategy(), RetryStrategy, DefaultRetryStrategy and CoreDependencies.retryStrategy are removed. The bus now decides both the delay and when a message is out of attempts, with the policy passed to withRecoverability(): a function of the failure that returns retry(delay) or deadLetter() (see Recoverability). The default, defaultRecoverability(), makes 10 attempts with the same exponential delays as DefaultRetryStrategy, so a bus that didn't configure retries behaves as before.

    • withRetryStrategy(new DefaultRetryStrategy()) → remove it, or withRecoverability(defaultRecoverability()).
    • A custom RetryStrategy → withRecoverability(defaultRecoverability({ delay: failedAttempts => ... })). calculateRetryDelay was given the attempts that had failed before, starting from 0; delay is given the failures counting the current one, starting from 1, so calculateRetryDelay(n) is delay(n + 1).
    • maxRetries on InMemoryQueue or RabbitMqTransport → defaultRecoverability({ maxAttempts }). maxRetries: 0, to never retry, is withRecoverability(() => deadLetter()).
  • failMessage() and returnMessage() take effect once handling finishes. The bus now settles each message once, after the incoming middleware and handlers finish. failMessage() dead-letters the message even if the handler then throws, instead of also retrying it, and returnMessage() counts as a failed attempt, so the policy decides its delay and dead-letters it once it's out of attempts. Neither acts on the transport straight away, and code after them still runs, but the messages the message's handlers send are now dropped and a workflow handler's state changes aren't saved, as when it throws.

  • The handlers of a message share one outbox, which holds their workflow state too. The workflow state the handlers of a message save and the messages they send are held until they all resolve, and dropped if any of them throws or calls failMessage() or returnMessage(). Then the state is saved, checking its $version as before, and the messages are sent. Both used to happen as each handler resolved, so a handler that resolved kept its state and sent its messages even when another handler of the same message failed: the retry sent the messages again, started a second workflow instance, or skipped sending because the workflow's state said it already had. A handler middleware that throws now drops what every handler sent and saved, too, and handler middleware no longer sees the workflow state saved when next() resolves. If a handler's messages must be sent whatever its siblings do, give it a message of its own. When several workflows handle one message and saving one's state fails after another's was saved, the messages of the workflow whose state was saved are still sent, and may be sent again by the retry; configure withOutbox() for a bus where several workflows handle one message. To save the state and the messages in one transaction, so a crash or a broker outage between them loses nothing, configure the new transactional outbox with withOutbox().

  • Dead-lettered messages carry a bus-failure header with their failure metadata, as JSON. Transports reserve the name, so outgoing middleware that set a bus-failure header now throws TransportHeaderReserved.

  • The Transport interface changes for recoverability. A custom transport must:

    • set failedAttempts on each TransportMessage it reads: how many times handling it failed before, 0 on its first delivery.
    • take the delay as the second argument of returnMessage(message, delay), and stop dead-lettering messages there. The bus calls fail() when the policy dead-letters a message.
    • take the failure metadata as the second argument of fail(message, failure), write it on the dead-lettered copy as a bus-failure header with toFailureHeader(), and remove the message from the service queue. The bus no longer calls deleteMessage() after fail().
  • A Receiver applies the recoverability policy. A message that fails is returned to the transport with the policy's delay and reported to the host as failed, as before, or moved to the dead letter queue with transport.fail() and reported as handled. A custom receiver must set failedAttempts on the messages it returns.

  • A persistence that stores outgoing messages must keep each message's destination. The transactional outbox stores replies as OutgoingMessages of kind reply, with the address they're sent to in destination, so store it and return it when the message is claimed.

  • A persistence that stores outgoing messages is used by every bus. To support delayed delivery, a bus prepares its persistence even when it's send-only, and initialize() initializes it, even without workflows, when it implements the new optional storeOutgoingMessages, claimDueOutgoingMessages, deleteOutgoingMessages and releaseOutgoingMessages (as InMemoryPersistence, PostgresPersistence and MongodbPersistence do). A started bus checks it every second for messages that are due, and dispose() disposes it unless another bus still uses it. A send-only bus configured with withPersistence() now connects to the database at initialize() and disconnects at dispose(), which ends a pg Pool or MongoClient you passed in, so dispose that bus last or give it a persistence of its own. A bus without workflows that uses PostgresPersistence or MongodbPersistence now needs a reachable database at startup, and its outgoing messages table or collection, which bus provision creates (see "A bus creates nothing when it starts" above).

  • HandlerContext has a reply() method, which answers the message being handled by sending a message straight to its return address (see Request and reply). A plain object used as a HandlerContext in a test needs a reply too, such as reply: async () => {}. workflowContext() adds one.

  • Every message sent by a bus that receives messages has a replyTo attribute, its transport's returnAddress or endpointName. Send-only buses and schedulers don't set it. Pass replyTo: undefined to send() or publish() to leave it out. Received attributes include it, so a test that compares a handler's attributes with toEqual needs replyTo too, or toMatchObject.

  • OutgoingContext has a third kind, reply, for a message sent with ctx.reply(), with the return address it's sent to in destination. An outgoing middleware that tells sends from publishes with kind === 'send' and treats everything else as a publish now also gets replies there. OutgoingContext['message'] is now a Message rather than a Command | Event.

  • A custom transport implements sendToAddress(address, message, attributes, sendOptions) to support ctx.reply(): it sends the message straight to the queue at a return address, bypassing topic routing, and throws EndpointNotFound when there's no queue there. It's optional, but ctx.reply() throws TransportReplyNotSupported without it. Carry the new replyTo attribute with every message, and reserve the header name it's written under, if any. Give the transport a returnAddress if its queue names aren't enough for other services to reach its queue.

  • defaultRecoverability() dead-letters reply errors on the first failure: DelayedReplyNotSupported, ReturnAddressMissing, TransportReplyNotSupported and EndpointNotFound (ALWAYS_UNRECOVERABLE), whatever its unrecoverable option is.

  • Persistence.initializeWorkflow is removed. A custom persistence creates its tables, collections and indexes in the new optional provision({ workflows, dryRun }), which returns a ProvisioningPlan, and gets the bus' workflows in initialize({ workflows, verifyResources }), where it checks they exist, rather than creating them, when verifyResources is set. Each of workflows has the workflowStateType and messageWorkflowMappings that initializeWorkflow was called with.

  • Transport.initialize() gets messageNames, verifyResources and autoProvision. A custom transport creates its queues and subscriptions in the new optional provision({ handlerRegistry, sendOnly, messageNames, dryRun }), and initialize() checks they exist when verifyResources is set. Only create resources at runtime, such as the topic of a message being sent, when autoProvision is set.

  • Warnings and errors from the default logger now go to stderr even without DEBUG set. Pass your own logger with withLogger to change that.

@node-ts/bus-cli

  • bus provision <module> is new: it creates what the bus a module exports needs, at deploy time (see above).
  • bus generate-message-types also reads defineCommand/defineEvent definitions and interfaces or type aliases with a literal $name, and prints a warning for each declaration with a $name it skips. Regenerate your message types, and check the warnings: an interface that used to be skipped quietly may now be read, or be reported as a duplicate $name. A message class whose static NAME isn't its $name now fails generation.

@node-ts/bus-mongodb

  • The mongodb driver is now version 7 (MongoDB server 4.2 or later). MongodbPersistence takes a MongoClient from mongodb 7, so upgrade your own copy of the driver.
  • Workflow state keys use a new encoding, and existing data isn't migrated. Keys are now percent-encoded (% → %25, $ → %24, . → %2E) instead of using the old __ scheme. Workflow state saved by 1.x isn't found by 2.0. Before you upgrade, let running workflows finish, or migrate their documents yourself. Drop any existing index on the old key paths, or provisioning fails with an index conflict.
  • Collections and indexes are created by bus provision, not initialize(). The service now needs only find, insert, update and listIndexes on its collections (and remove on outgoingmessages and inbox), and listCollections on the database.
  • An inbox collection is provisioned, which records the messages each endpoint has handled with withOutbox(), so a copy of one is skipped. It has a unique index on { endpoint, messageId } and a TTL index that removes records after 7 days. initialize() checks it exists, like the other collections, so run bus provision before starting a service on this version.
  • withOutbox() is supported, on a replica set or a sharded cluster. On a standalone server, a bus configured with it throws ReplicaSetRequired from initialize().

@node-ts/bus-postgres

  • The schema, tables and indexes are created by bus provision, not initialize(). The service now needs only USAGE on the schema and SELECT, INSERT and UPDATE on its tables (and DELETE on outgoing_messages and inbox).

  • An outgoing_messages table is provisioned in the configured schema, which holds messages sent with deliverAfter or deliverAt. Delayed delivery needs Postgres 9.5 or later. If you drop the schema without cascade, for example in tests, drop this table first.

  • An inbox table is provisioned in the configured schema, which records the messages each endpoint has handled with withOutbox(), so a copy of one is skipped. The service needs SELECT, INSERT and DELETE on it. initialize() checks it exists, like the other tables, so run bus provision before starting a service on this version. If you drop the schema without cascade, drop it too.

  • Index names longer than 63 bytes are shortened with a hash, so they no longer truncate to the same name. Nothing is dropped or renamed: names that fit are unchanged, and an index 1.x created under its truncated name is reused. If 1.x skipped an index because its truncated name collided with another, provisioning creates it. That CREATE INDEX blocks writes to the table while it builds, so on a large table you may want to create it yourself first with CREATE INDEX CONCURRENTLY, using the name and definition that bus provision --dry-run --json lists.

  • Workflow state lookups also match the state's $name. Workflow states whose table names collide (the same first 63 bytes once invalid characters are stripped, or names that differ only in stripped characters) shared a table and could read each other's state. Table names don't change. If you changed a state's $name in a way that kept its table, for example only its case, rows saved under the old $name are no longer found: update them with update "<schema>"."<table>" set data = jsonb_set(data, '{$name}', '"<new name>"') where data->>'$name' = '<old name>'.

@node-ts/bus-sqs

  • autoProvision is removed. Queues, topics, subscriptions and the queue policy are created by bus provision, or at startup with withAutoProvision(), never by default. Remove autoProvision: false. If you relied on the default, see "A bus creates nothing when it starts" above. The queue policy is only set by provisioning, and a publish no longer creates its topic unless the bus is configured with withAutoProvision().
  • initialize() checks what the bus receives through: the queues (sqs:GetQueueUrl) and the subscription to each handled topic (sns:ListSubscriptionsByTopic), as it did with autoProvision: false, but no longer calls sns:GetTopicAttributes. A send-only bus checks nothing. Custom handler topics (topicIdentifier) are no longer created, only subscribed to, and a refusal to read their subscriptions is logged rather than failing startup.
  • A message that can't be parsed goes straight to the dead letter queue. It used to be made visible again until the queue's redrive policy moved it, which re-read it on every poll.
  • messageRetentionPeriod must be at least 60. An explicit 0 used to be silently replaced with 14 days. Now it's passed to SQS, which rejects it (the minimum is 60 seconds). The same applies to waitTimeSeconds: 0 and visibilityTimeout: 0, which now take effect.
  • Messages carry messageId and sentAt as two more top-level SNS message attributes, next to correlationId, and the return address as replyTo. replyTo is a reserved header name, so outgoing middleware that set it now throws TransportHeaderReserved.
  • The return address is the queue's URL, so a replier in another account or region can reach it. A reply from ctx.reply() is sent straight to it with SendMessage rather than through SNS. The replying service needs sqs:SendMessage on the queues of the services it replies to, and a requester in another account needs a queuePolicy that allows it, which must also keep the statement that lets SNS topics send to the queue, since it replaces the default policy.
  • maxReceiveCount defaults to 15, up from 10. The bus' recoverability policy now dead-letters failed messages itself, with their failure metadata, so the queue's redrive policy is only a backstop for messages that crash the process. Existing queues get the new value when they're provisioned. If you set maxReceiveCount, keep it above your policy's maxAttempts, or SQS dead-letters messages first, without failure metadata.
  • SqsTransport takes SQSClient/SNSClient from @aws-sdk/client-sqs/client-sns 3.1142.0 or a later 3.x release. Upgrade your own copies if you pass clients in.

@node-ts/bus-rabbitmq

  • A message that can't be parsed goes straight to the dead letter queue and is acked. It used to be left unacked, which held a prefetch slot until the connection closed.
  • The topology is declared by bus provision, not initialize(). The service now needs no configure permission: write on amq.default and its messages' exchanges, and read on its service, dead letter and retry queues (only the service queue before RabbitMQ 4.3.1, whose passive declares need no permission). Before the first send to an exchange or retry queue, the transport checks it exists with a passive declare, unless withResourceVerification(false) is set, and a send to a missing exchange throws ResourcesNotProvisioned. A check the broker refuses throws RabbitMqResourceCheckRefused.
  • A send-only bus no longer declares queues, only the exchanges of its messages, and a bus configured with asScheduler() declares nothing. Delete queues a send-only service no longer needs.
  • maxRetries is removed. The bus' recoverability policy decides when a message is out of attempts: use defaultRecoverability({ maxAttempts }).
  • Retries now wait for the delay the bus' recoverability policy chooses, using new durable <queue>-retry-<n>ms queues, one for each power of two from 1 to 2³² milliseconds, which are provisioned with the service queue. Existing queues are unchanged: the service queue keeps its arguments, and the legacy <queue>-retry queue is still declared so messages already in it drain. Messages returned by 1.x keep their attempt count.
  • Every message from a bus that receives messages has the AMQP replyTo property set to its queue name, the return address that ctx.reply() sends replies to through the default exchange. A consumer that isn't on @node-ts/bus and answers messages that have a replyTo, such as a listener that returns a value, now sends its answer to the bus' queue, where it's discarded unless the bus handles it. Stop such consumers from replying, or have the bus' outgoing middleware delete context.attributes.replyTo for the messages they receive.
  • The AMQP messageId property is now the bus' messageId, so it's the same for every message sent with the same id, rather than a new UUID per publish. sentAt is carried in a sentAt header.
  • amqplib is now version 2.2. It ships its own types, so remove @types/amqplib. heartbeat=0 in a connection string now disables heartbeats.

@node-ts/bus-redis

  • It's a new transport on Redis Streams, replacing the 0.x Redis lists transport from the node-ts/bus-redis repository: 0.1.8 (npm's latest, for bus-core 1.x, configured with new RedisTransport({ queueName, connectionString })), and 0.1.1–0.1.7 and 0.1.9 (the 0.x tag, for bus-core 0.6, loaded as the inversify BusRedisModule). Pass new RedisTransport({ queueName, connection: { url } }) to withTransport(): connectionString is now connection.url, and inversify isn't used. It needs Redis 7.0+ or Valkey 7.2+, and redis (node-redis) 6 as a peer dependency.
  • maxRetries is removed: use the bus' recoverability policy, defaultRecoverability({ maxAttempts }). visibilityTimeout is now visibilityTimeoutMs, subscriptionsKeyPrefix is now keyPrefix, and withScheduler is removed.
  • Messages in 0.x queues aren't moved, and the two versions can't send each other messages. Drain the old queues, then provision and deploy every service that exchanges messages together. See Migrating from @node-ts/bus-redis 0.x.

@node-ts/bus-sqs-lambda

  • The receiver applies the bus' recoverability policy. A record that's retried has its visibility timeout set to the policy's delay, rather than waiting out the queue's visibility timeout, and a record that's dead-lettered, by the policy or failMessage(), is moved to the dead letter queue with its failure metadata and reported to Lambda as handled.
  • Partial batch failures are opt-in. Pass new BusSqsLambdaReceiver({ reportBatchItemFailures: true }) and enable ReportBatchItemFailures on the event source mapping to retry only the failed records. Without it, a failure still fails the whole batch.
  • The aws-lambda CLI is no longer a dependency. Install @types/aws-lambda yourself if you use the typings.

@node-ts/bus-test

  • The package ships compiled JavaScript from dist instead of its TypeScript source. If you added @node-ts/bus-test to jest's transformIgnorePatterns exceptions so ts-jest would compile it, you can remove that. Import transportTests and the test messages from the package root, since paths such as @node-ts/bus-test/src/... no longer exist.
  • @node-ts/bus-core is a peer dependency. Install it next to @node-ts/bus-test (your transport already needs it). typescript is no longer installed with the suite, so add it to your own dev dependencies if you relied on getting it through the suite.
  • The suites pass @node-ts/bus-test's own generated message types (exported as messageTypes) to their buses. Other buses your tests build that receive messages need withMessageTypes() with their own fixtures' types, and each needs its own transport instance.
  • The suites provision their buses with bus.provision(), then initialize them without provisioning. A transport or persistence that needs resources must implement provision(), and its initialize() must create nothing.
  • transportTests checks recoverability: failedAttempts counts up on each delivery, a message is dead-lettered at the suite policy's maxAttempts of 5, an unrecoverable error is dead-lettered on its first failure, and every dead-lettered message has bus-failure metadata. readAllFromDeadLetterQueue must return each message's failure, read with fromFailureHeader(), and wait until a message has been dead-lettered.
  • transportTests checks replies: the transport must implement sendToAddress(), a message sent to its own return address arrives with its attributes, including replyTo, without a subscription, and a reply from ctx.reply() reaches the requester with its correlation id and sticky attributes.
  • transportTests checks messageId and sentAt: a message arrives with both, a messageId the caller passes is kept, and both are the same on every retry and in the dead letter queue. readAllFromDeadLetterQueue must return them in each message's attributes.
  • The suite checks that messages survive a round trip with their types restored: class instances several levels deep, Dates, Maps, Sets, bigints, optional and null fields, and attributes. It uses generated message types, so serialize and deserialize message bodies with coreDependencies.messageSerializer in your transport rather than calling JSON.stringify/JSON.parse on them yourself.