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.
- Node.js 24 or later is required. Every package declares
engines.node >=24and is compiled for ES2024. - Import only from the package root. Each package now has an
exportsmap. Deep imports such as@node-ts/bus-core/dist/service-bus/errorfail at runtime withERR_PACKAGE_PATH_NOT_EXPORTED, and TypeScript can't resolve them undermoduleResolutionnode16,nodenextorbundler. Import from@node-ts/bus-core(or the adapter's root) instead. The errors the packages throw, such asBusAlreadyInitializedand bus-mongodb'sWorkflowStateNotFound, 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 throwsResourcesNotProvisionednaming 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 callingbus.provision()) on a module that exports yourBusConfiguration, or a function that returns one. Then remove the create permissions from the service: each adapter's page lists the runtime permissions it needs, whichbus provision --dry-run --permissionsprints. - For local development and tests, add
.withAutoProvision()to the configuration to keep creating everything atinitialize(). - 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.
- In production, create them at deploy time, with deploy credentials, by running
- ES modules get their own entry point.
importloads an ES module entry andrequireloads the CommonJS build. You don't need to change anything, and named imports likeimport { Command } from '@node-ts/bus-messages'work from ES modules. The ES module entry re-exports the CommonJS build, so code that mixesimportandrequirestill gets one copy of each class.
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:
-
Remove
@node-ts/bus-class-serializer,class-transformerandreflect-metadatafrom your dependencies, and theimport 'reflect-metadata'line from your entry point. -
Remove the
@Type(...)decorators (and any other class-transformer decorators) from your messages and workflow state. If nothing else uses decorators, also removeexperimentalDecoratorsandemitDecoratorMetadatafrom your tsconfig. -
In the package that declares your messages, install
@node-ts/bus-clias 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.tsexport * from './message-types.generated'
Then pass the generated
messageTypesof 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
prebuildscript, and--checkto CI (see Generating message types). Include the files that declare your workflow state if you want their Dates and classes restored too. -
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 towithMessageTypes().
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
@Typewithout a discriminator: aCardPaymentin a field declared asPaymentcomes back as aPayment.
-
JsonSerializerno 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.stringifywrote aMaporSetas{}and threw on abigint. 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). Atinitialize(), a bus with handlers or workflows throwsMessageTypesMissing, 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 bywithCustomHandlerare 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
correlationIdor sticky attributes of the message being handled, and itsfailMessage()andreturnMessage()throwFailMessageOutsideHandlingContext/ReturnMessageOutsideHandlingContextinstead of acting on the other bus' message. Use the handler context (ctx.send,ctx.failMessage(), ...) or the bus that's handling the message. -
messageHandlingContextis 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, usebus.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()throwsTransportAlreadyInUsewhen another bus already uses it. Create a transport per bus, or usewithConcurrency()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.deserializeandtoClasstake the bus' message types as an optional last argument. A custom serializer can use them to restore nested types the wayJsonSerializerdoes. -
Persistence adapters store and return plain JSON values.
saveWorkflowStategets workflow state already converted withtoPlain, andgetWorkflowStatereturns state as it was stored; the bus restores its classes with its own serializer and message types. A custom persistence should stop callingcoreDependencies.serializer.CoreDependenciesalso gainsmessageTypes. -
defaultLoggerFactoryis replaced bycreateDefaultLoggerFactory(), which each bus calls for its own factory. -
Configure the bus before
build().asSendOnlyand everywith*method onBusConfigurationnow throwBusAlreadyInitializedwhen called afterbuild().withConcurrency,withContainer,withRetryStrategy(nowwithRecoverability),withReceiver,withMessageReadMiddleware(nowwithMiddleware) andwithInterruptSignals(formerlywithAdditionalInterruptSignal) used to be silently ignored at that point, so move any such calls beforebuild(). -
Handlers get a
HandlerContext. Function handlers andHandler.handleare called with(message, attributes, ctx), and class workflow handlers with(message, workflowState, attributes, ctx). Usectx.sendandctx.publishinstead of capturing the bus or injecting it. Handlers that declare fewer parameters don't need to change, but code that calls aFunctionHandler,HandlerorCustomHandlerdirectly, such as a unit test, must now pass a context; a plain object withcorrelationId,send,publish,failMessageandreturnMessagewill do. -
WorkflowHandlerparameters 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
NAMEequal to their$name. The bus reads the name fromNAMEwithout 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 withhandlerFor,startedBy,whenor a class handler'smessageType, and throwsMessageNameMissingat registration in plain JavaScript. Addstatic NAME = 'my-app/thing'and set$name = Thing.NAME. A subclass needs its ownNAMEtoo: one that inherits its parent's throwsMessageNameInherited, since it would otherwise be routed as the parent. Message types are now typed asMessageDeclarationfrom@node-ts/bus-messages(a message class or adefineCommand/defineEventdefinition) rather thanClassConstructor. -
SystemMessageMissingResolveris replaced byMessageNameMissing. It's thrown when a message type has no staticNAME, or isundefined, which usually means a circular import. A message from another system that has no$nameis still handled withwithCustomHandlerand a resolver. -
withAdditionalInterruptSignalis replaced bywithInterruptSignals(signals), which replaces the defaultSIGINTandSIGTERMrather 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 withnew, as it does for class workflows.build()now throwsContainerNotRegisteredonly for a class handler whose constructor takes arguments, and the error names that class. If the constructor throws, the message fails withClassHandlerNotResolved. -
Handler errors say what failed.
HandlerDispatchRejected's message now lists each handler's error and itscauseis set, andContainerNotRegisteredandClassHandlerNotResolvedname the class handler (classHandlerName, the first constructor argument of both). The plainErrors thrown by the workflow registry are nowWorkflowRegisteredAfterInitialization,WorkflowNameAlreadyRegisteredandWorkflowStateNotProvided; code that matched their message text should check the class instead. -
Workflow handler errors are wrapped in
WorkflowHandlerFailed. A failedstartedByorwhenhandler, or a failure to save the state it returned, now appears inHandlerDispatchRejected'srejectionsas aWorkflowHandlerFailednaming the workflow, the instance's$workflowIdand the message, with the original error as itscause. A failedwhenhandler used to be a nestedHandlerDispatchRejected, and a failedstartedByhandler the error itself, so code that checked those rejections should readcauseinstead. -
Lifecycle hooks and read middleware are replaced by
withMiddleware(). The eight emitters onBusInstance(beforeSend,beforePublish,afterSend,afterPublish,afterReceive,beforeDispatch,afterDispatchandonError), their payload types (BeforeSend,AfterSend,OnErrorand so on),withMessageReadMiddleware(),MiddlewareDispatcherandTypedEmitterare removed, andMiddlewareandNextare now the async middleware types below. Register aBusMiddlewarewithincoming,handlerandoutgoingstages instead (see Middleware):withMessageReadMiddleware(fn)→withMiddleware({ incoming: (ctx, next) => fn(ctx.transportMessage, next) }). A middleware must now return a promise, so make a synchronous oneasync. An incoming middleware that doesn't callnext()now has its message deleted, where read middleware left it in flight.beforeSend/beforePublish→ anoutgoingmiddleware, beforeawait next(). It gets{ kind: 'send' | 'publish', message, attributes, headers }.afterSend/afterPublish→ anoutgoingmiddleware, afterawait next(). Inside a handler that resolves once the message is buffered in the handler's outbox, not when it's sent.afterReceive→ anincomingmiddleware, beforenext().afterDispatch→ afterawait next().beforeDispatch→ ahandlermiddleware, which wraps each handler and gets itshandlerName.onError→ anincomingmiddleware that wrapsawait next()in atry/catchand rethrows. Not rethrowing marks the message handled.
-
Transport.sendandTransport.publishtake an optional third argument,TransportSendOptions, with theheadersset 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 optionalassertSendOptions(sendOptions), which the bus calls before buffering or sending a message, and throwTransportHeaderReservedthere. -
handlerFortakes 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')andwhen(Message, 'handler')only compile when the named method takes that message (and the state, attributes andHandlerContextit'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:configureWorkflowmust type its mapper with its own workflow class, such asWorkflowMapper<OrderState, OrderWorkflow>. Withanyorthisas 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 asWorkflowMapper<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()anddiscardWorkflow()returnWorkflowStateChange<TState>, andWorkflowMapper'sonStartedByandonWhenstore handler names asstring(OnWhenHandlerno 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. AconfigureWorkflow()that uses the workflow's fields or injected dependencies now finds themundefined, and if it throws or passes the mapper an undefined message, handler name or lookup,provision()orinitialize()throwsWorkflowConfigurationFailednaming the workflow. AconfigureWorkflowdeclared 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 withWorkflowHandlerFailed, as a class handler does, rather than failinginitialize().testWorkflow()readsconfigureWorkflow()the same way, so itscreateWorkflowis only called for each message. -
Transporthas a requiredendpointName, the name of the queue the bus receives from. A custom transport must add it, such asget endpointName() { return this.configuration.queueName }.InMemoryQueuetakes an optionalendpointName,in-memoryby default. -
Every message the bus sends has a
messageIdand asentAtin itsMessageAttributes: a new UUID and an ISO 8601 timestamp, unless you pass your own tosendorpublish. UnlikecorrelationId, 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 withtoEqualmay needtoMatchObjectorexpect.objectContaining. -
Retry strategies are replaced by a recoverability policy.
withRetryStrategy(),RetryStrategy,DefaultRetryStrategyandCoreDependencies.retryStrategyare removed. The bus now decides both the delay and when a message is out of attempts, with the policy passed towithRecoverability(): a function of the failure that returnsretry(delay)ordeadLetter()(see Recoverability). The default,defaultRecoverability(), makes 10 attempts with the same exponential delays asDefaultRetryStrategy, so a bus that didn't configure retries behaves as before.withRetryStrategy(new DefaultRetryStrategy())→ remove it, orwithRecoverability(defaultRecoverability()).- A custom
RetryStrategy→withRecoverability(defaultRecoverability({ delay: failedAttempts => ... })).calculateRetryDelaywas given the attempts that had failed before, starting from 0;delayis given the failures counting the current one, starting from 1, socalculateRetryDelay(n)isdelay(n + 1). maxRetriesonInMemoryQueueorRabbitMqTransport→defaultRecoverability({ maxAttempts }).maxRetries: 0, to never retry, iswithRecoverability(() => deadLetter()).
-
failMessage()andreturnMessage()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, andreturnMessage()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()orreturnMessage(). Then the state is saved, checking its$versionas 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 whennext()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; configurewithOutbox()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 withwithOutbox(). -
Dead-lettered messages carry a
bus-failureheader with their failure metadata, as JSON. Transports reserve the name, so outgoing middleware that set abus-failureheader now throwsTransportHeaderReserved. -
The
Transportinterface changes for recoverability. A custom transport must:- set
failedAttemptson eachTransportMessageit reads: how many times handling it failed before,0on its first delivery. - take the delay as the second argument of
returnMessage(message, delay), and stop dead-lettering messages there. The bus callsfail()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 abus-failureheader withtoFailureHeader(), and remove the message from the service queue. The bus no longer callsdeleteMessage()afterfail().
- set
-
A
Receiverapplies 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 withtransport.fail()and reported as handled. A custom receiver must setfailedAttemptson the messages it returns. -
A persistence that stores outgoing messages must keep each message's
destination. The transactional outbox stores replies asOutgoingMessages ofkindreply, with the address they're sent to indestination, 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 optionalstoreOutgoingMessages,claimDueOutgoingMessages,deleteOutgoingMessagesandreleaseOutgoingMessages(asInMemoryPersistence,PostgresPersistenceandMongodbPersistencedo). A started bus checks it every second for messages that are due, anddispose()disposes it unless another bus still uses it. A send-only bus configured withwithPersistence()now connects to the database atinitialize()and disconnects atdispose(), which ends apgPoolorMongoClientyou passed in, so dispose that bus last or give it a persistence of its own. A bus without workflows that usesPostgresPersistenceorMongodbPersistencenow needs a reachable database at startup, and its outgoing messages table or collection, whichbus provisioncreates (see "A bus creates nothing when it starts" above). -
HandlerContexthas areply()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 aHandlerContextin a test needs areplytoo, such asreply: async () => {}.workflowContext()adds one. -
Every message sent by a bus that receives messages has a
replyToattribute, its transport'sreturnAddressorendpointName. Send-only buses and schedulers don't set it. PassreplyTo: undefinedtosend()orpublish()to leave it out. Received attributes include it, so a test that compares a handler's attributes withtoEqualneedsreplyTotoo, ortoMatchObject. -
OutgoingContexthas a third kind,reply, for a message sent withctx.reply(), with the return address it's sent to indestination. An outgoing middleware that tells sends from publishes withkind === 'send'and treats everything else as a publish now also gets replies there.OutgoingContext['message']is now aMessagerather than aCommand | Event. -
A custom transport implements
sendToAddress(address, message, attributes, sendOptions)to supportctx.reply(): it sends the message straight to the queue at a return address, bypassing topic routing, and throwsEndpointNotFoundwhen there's no queue there. It's optional, butctx.reply()throwsTransportReplyNotSupportedwithout it. Carry the newreplyToattribute with every message, and reserve the header name it's written under, if any. Give the transport areturnAddressif its queue names aren't enough for other services to reach its queue. -
defaultRecoverability()dead-letters reply errors on the first failure:DelayedReplyNotSupported,ReturnAddressMissing,TransportReplyNotSupportedandEndpointNotFound(ALWAYS_UNRECOVERABLE), whatever itsunrecoverableoption is. -
Persistence.initializeWorkflowis removed. A custom persistence creates its tables, collections and indexes in the new optionalprovision({ workflows, dryRun }), which returns aProvisioningPlan, and gets the bus' workflows ininitialize({ workflows, verifyResources }), where it checks they exist, rather than creating them, whenverifyResourcesis set. Each ofworkflowshas theworkflowStateTypeandmessageWorkflowMappingsthatinitializeWorkflowwas called with. -
Transport.initialize()getsmessageNames,verifyResourcesandautoProvision. A custom transport creates its queues and subscriptions in the new optionalprovision({ handlerRegistry, sendOnly, messageNames, dryRun }), andinitialize()checks they exist whenverifyResourcesis set. Only create resources at runtime, such as the topic of a message being sent, whenautoProvisionis set. -
Warnings and errors from the default logger now go to stderr even without
DEBUGset. Pass your own logger withwithLoggerto change that.
bus provision <module>is new: it creates what the bus a module exports needs, at deploy time (see above).bus generate-message-typesalso readsdefineCommand/defineEventdefinitions and interfaces or type aliases with a literal$name, and prints a warning for each declaration with a$nameit 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 staticNAMEisn't its$namenow fails generation.
- The
mongodbdriver is now version 7 (MongoDB server 4.2 or later).MongodbPersistencetakes aMongoClientfrommongodb7, 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, notinitialize(). The service now needs onlyfind,insert,updateandlistIndexeson its collections (andremoveonoutgoingmessagesandinbox), andlistCollectionson the database. - An
inboxcollection is provisioned, which records the messages each endpoint has handled withwithOutbox(), 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 runbus provisionbefore 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 throwsReplicaSetRequiredfrominitialize().
-
The schema, tables and indexes are created by
bus provision, notinitialize(). The service now needs onlyUSAGEon the schema andSELECT,INSERTandUPDATEon its tables (andDELETEonoutgoing_messagesandinbox). -
An
outgoing_messagestable is provisioned in the configured schema, which holds messages sent withdeliverAfterordeliverAt. Delayed delivery needs Postgres 9.5 or later. If you drop the schema withoutcascade, for example in tests, drop this table first. -
An
inboxtable is provisioned in the configured schema, which records the messages each endpoint has handled withwithOutbox(), so a copy of one is skipped. The service needsSELECT,INSERTandDELETEon it.initialize()checks it exists, like the other tables, so runbus provisionbefore starting a service on this version. If you drop the schema withoutcascade, 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 INDEXblocks writes to the table while it builds, so on a large table you may want to create it yourself first withCREATE INDEX CONCURRENTLY, using the name and definition thatbus provision --dry-run --jsonlists. -
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$namein a way that kept its table, for example only its case, rows saved under the old$nameare no longer found: update them withupdate "<schema>"."<table>" set data = jsonb_set(data, '{$name}', '"<new name>"') where data->>'$name' = '<old name>'.
autoProvisionis removed. Queues, topics, subscriptions and the queue policy are created bybus provision, or at startup withwithAutoProvision(), never by default. RemoveautoProvision: 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 withwithAutoProvision().initialize()checks what the bus receives through: the queues (sqs:GetQueueUrl) and the subscription to each handled topic (sns:ListSubscriptionsByTopic), as it did withautoProvision: false, but no longer callssns: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.
messageRetentionPeriodmust be at least 60. An explicit0used to be silently replaced with 14 days. Now it's passed to SQS, which rejects it (the minimum is 60 seconds). The same applies towaitTimeSeconds: 0andvisibilityTimeout: 0, which now take effect.- Messages carry
messageIdandsentAtas two more top-level SNS message attributes, next tocorrelationId, and the return address asreplyTo.replyTois a reserved header name, so outgoing middleware that set it now throwsTransportHeaderReserved. - 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 withSendMessagerather than through SNS. The replying service needssqs:SendMessageon the queues of the services it replies to, and a requester in another account needs aqueuePolicythat allows it, which must also keep the statement that lets SNS topics send to the queue, since it replaces the default policy. maxReceiveCountdefaults 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 setmaxReceiveCount, keep it above your policy'smaxAttempts, or SQS dead-letters messages first, without failure metadata.SqsTransporttakesSQSClient/SNSClientfrom@aws-sdk/client-sqs/client-sns3.1142.0 or a later 3.x release. Upgrade your own copies if you pass clients in.
- 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, notinitialize(). The service now needs noconfigurepermission:writeonamq.defaultand its messages' exchanges, andreadon 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, unlesswithResourceVerification(false)is set, and a send to a missing exchange throwsResourcesNotProvisioned. A check the broker refuses throwsRabbitMqResourceCheckRefused. - 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. maxRetriesis removed. The bus' recoverability policy decides when a message is out of attempts: usedefaultRecoverability({ maxAttempts }).- Retries now wait for the delay the bus' recoverability policy chooses, using new durable
<queue>-retry-<n>msqueues, 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>-retryqueue 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
replyToproperty set to its queue name, the return address thatctx.reply()sends replies to through the default exchange. A consumer that isn't on @node-ts/bus and answers messages that have areplyTo, 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 deletecontext.attributes.replyTofor the messages they receive. - The AMQP
messageIdproperty 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.sentAtis carried in asentAtheader. amqplibis now version 2.2. It ships its own types, so remove@types/amqplib.heartbeat=0in a connection string now disables heartbeats.
- It's a new transport on Redis Streams, replacing the 0.x Redis lists transport from the
node-ts/bus-redisrepository: 0.1.8 (npm'slatest, for bus-core 1.x, configured withnew RedisTransport({ queueName, connectionString })), and 0.1.1–0.1.7 and 0.1.9 (the0.xtag, for bus-core 0.6, loaded as the inversifyBusRedisModule). Passnew RedisTransport({ queueName, connection: { url } })towithTransport():connectionStringis nowconnection.url, and inversify isn't used. It needs Redis 7.0+ or Valkey 7.2+, andredis(node-redis) 6 as a peer dependency. maxRetriesis removed: use the bus' recoverability policy,defaultRecoverability({ maxAttempts }).visibilityTimeoutis nowvisibilityTimeoutMs,subscriptionsKeyPrefixis nowkeyPrefix, andwithScheduleris 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.
- 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 enableReportBatchItemFailureson the event source mapping to retry only the failed records. Without it, a failure still fails the whole batch. - The
aws-lambdaCLI is no longer a dependency. Install@types/aws-lambdayourself if you use the typings.
- The package ships compiled JavaScript from
distinstead of its TypeScript source. If you added@node-ts/bus-testto jest'stransformIgnorePatternsexceptions so ts-jest would compile it, you can remove that. ImporttransportTestsand the test messages from the package root, since paths such as@node-ts/bus-test/src/...no longer exist. @node-ts/bus-coreis a peer dependency. Install it next to@node-ts/bus-test(your transport already needs it).typescriptis 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 asmessageTypes) to their buses. Other buses your tests build that receive messages needwithMessageTypes()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 implementprovision(), and itsinitialize()must create nothing. transportTestschecks recoverability:failedAttemptscounts up on each delivery, a message is dead-lettered at the suite policy'smaxAttemptsof 5, an unrecoverable error is dead-lettered on its first failure, and every dead-lettered message hasbus-failuremetadata.readAllFromDeadLetterQueuemust return each message'sfailure, read withfromFailureHeader(), and wait until a message has been dead-lettered.transportTestschecks replies: the transport must implementsendToAddress(), a message sent to its own return address arrives with its attributes, includingreplyTo, without a subscription, and a reply fromctx.reply()reaches the requester with its correlation id and sticky attributes.transportTestschecksmessageIdandsentAt: a message arrives with both, amessageIdthe caller passes is kept, and both are the same on every retry and in the dead letter queue.readAllFromDeadLetterQueuemust 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.messageSerializerin your transport rather than callingJSON.stringify/JSON.parseon them yourself.