INFO
This page is MIGRATING.md from the repository, so edit it there. The 1.x documentation is still at node-ts.gitbook.io/bus, but it's no longer maintained.
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 >=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. - 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.
@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:
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:shnpm i --save-dev @node-ts/bus-cli typescript npx bus generate-message-types --entry 'src/**/*.ts' --out src/message-types.generated.tstsexport * from './message-types.generated'Then pass the generated
messageTypesof every message library a service handles, and its own, to its bus:tsimport { 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 the bus-cli README). 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.
@node-ts/bus-core
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 read 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,withReceiver,withMessageReadMiddlewareandwithInterruptSignals(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.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).Warnings and errors from the default logger now go to stderr even without
DEBUGset. Pass your own logger withwithLoggerto change that.
@node-ts/bus-cli
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.
@node-ts/bus-mongodb
- 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, orinitializeWorkflowfails with an index conflict.
@node-ts/bus-postgres
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,
initializeWorkflownow creates it on the next start. ThatCREATE 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 SQL thatinitializeWorkflowlogs at debug level.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>'.
@node-ts/bus-sqs
- 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.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.
@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.
- Retries now wait for the
RetryStrategydelay, using new durable<queue>-retry-<n>msqueues that are declared the first time they're needed. 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. amqplibis now version 2.2. It ships its own types, so remove@types/amqplib.heartbeat=0in a connection string now disables heartbeats.
@node-ts/bus-sqs-lambda
- 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.
@node-ts/bus-test
- 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 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.