Migrating from 3.7.11 to 4.0.0
Both 3.7.11 and 4.0.0 are LTS versions. Upgrade framework packages together. Keep existing production applications on 3.7.11 until you are ready to adopt the breaking APIs.
Node.js and ESM
Use Node.js 24 or later. 4.0.0 publishes ESM JavaScript and TypeScript declarations only; CommonJS bundles, require exports and CommonJS worker entries are removed. Set "type": "module" in the application package, use ESM imports and compile worker bootstraps to ESM. The getting-started guide includes the TypeScript configuration.
Upgrade all @heronjs/* dependencies together to 4.0.0, including applications moving from a 4.0.0 alpha.
Runtime changes
- Module metadata is read into a snapshot, then a pure traversal produces a registration plan. Constructor identity handles cycles, repeated imports, and different classes with identical names. Explicit export visibility is preserved.
- Each application owns its DI container, registry, CQRS buses, events, subscriptions, services, cache, database clients, worker manager, HTTP host, and extension instances. Decorators write metadata; bootstrap performs registration and initialization.
- Bootstrap and startup track acquired resources before continuing. Concurrent and repeated close calls share completion. Shutdown waits for pending initialization, cancels streams, closes protocols and workers, and attempts remaining disposers after errors. SQL ownership includes the primary, all replicas, and the routing client.
- EBS adapters share handler discovery, parameter binding, receiver preservation, and reactive completion. Kafka, NATS, and AMQP retain their acknowledgement and connection behavior. Startup/publish failures propagate; AMQP publishing waits for writable backpressure and cleanup attempts every owned channel.
- HTTP verbs use one metadata factory. GraphQL awaits guard evaluation, resolver normalization, and resilience writes; unexpected resolver errors are masked at the response boundary. tRPC preserves both Observable and async-iterable subscriptions.
- Thread messages are direction-specific discriminated unions with runtime decoders. Receiver, argument-tuple, and result relationships are inferred for local reactive calls. Cross-process and reflective values remain
unknownuntil checked. - Framework errors use
Error, stable codes, andcause; aggregated cleanup failures remain inspectable. Explicit environment/configuration functions replace import-time environment snapshots and global registration switches.
API replacements
Update applications as a coordinated framework upgrade.
| Previous API or behavior | Replacement |
|---|---|
CoreContainer, CoreRegistry, cached HeronApp.app | Use the returned application’s container and registry; use createContainer() for standalone ownership. HeronApp.create constructs a fresh root. |
Import-time AUTO_REGISTER, autoInjectable, registry decorators | Declare providers/services/controllers/transporters in @Module(...), or register them explicitly on an owned container. Decorators declare metadata. |
Globals installed on globalThis, _, GlobalFunctions, global reflector | Import named functions and pass application/container dependencies explicitly. |
Caller-selected resolve<T>('name') | Use a constructor or new Token<T>('name'). String/symbol lookup returns unknown; validate it before use. Registration must match the token’s value type. |
| Untyped reflection keys and invented metadata result types | Use metadataKey<T>(), setMetadata(), and obtain()/obtain_prop(). Raw keys yield unknown. Typed mixin writers also enforce the key’s value type. |
Generic UseConfig<T>(...) | Pass a typed metadata key and its matching configuration to UseConfig(key, config). |
Caller-selected results on ThreadManager.run, run$, and dynamic CQRS lookup | Validate the returned unknown value, for example with a schema. Local Reactive.call infers the result from its callable. |
Loose ThreadMessage with optional fields | Use ParentMessage or WorkerMessage, include the fields required by its variant, and validate incoming data before dispatch. |
| Implicit datasource creation and synchronous tenant access | Await datasource initialization; tenant repository with(...) asynchronously returns its selected client. Application lifecycle owns cleanup. |
| Runtime error payload bags | Read code, message, cause, and isFatal; use ErrorOptions when wrapping errors. Do not return internal causes to clients. |
Global EBS lookup helpers (usePublisher, useStreamer, useKafka, useNats, useAMQP) | Declare messaging adapters and handlers in the module, then resolve/inject their application-owned registrations. |
| Arbitrary built-in extension strings | Use the closed built-in union, or {type: 'CUSTOM', name, event} for a custom extension declaration. |
| Fastify hook-name object | Use hooks(instance) { instance.addHook(...) }; native Fastify overloads check each hook signature. See the HTTP migration. |
Import-time PRODUCTION / HERON_CONTEXT values | Call readEnvironment() at the explicit configuration boundary. Configure logging explicitly with configureLogging(...). |
LimitedMap.get() assumed present | Handle T | undefined; falsy keys remain valid and are evicted correctly at capacity. |
Typed-token example:
import {Token, createContainer} from '@heronjs/common';
interface Clock {
now(): number;
}
const clock = new Token<Clock>('clock');
const container = createContainer();
container.registerInstance(clock, {now: () => Date.now()});
const timestamp: number = container.resolve(clock).now();
await container.dispose();Externally supplied instances remain externally owned. Use registerOwnedInstance or onDispose when the container should release a resource. GATEKEEPER and GATEKEEPER_RESOLVER are typed tokens; retain those exported identities when registering authentication dependencies.
Renamed contracts
Interface names describe their role without an I prefix. External SDK names such as KafkaJS.IHeaders are reused as supplied by that SDK; the framework does not duplicate them to rename a vendor contract.
| Former contract | Replacement |
|---|---|
ILogger | LogWriter |
ICollection | CollectionLike |
IStack | StackLike |
IQueue | QueueLike |
IMap | KeyValueStore |
ICreatable | Creatable |
IActivatable | Activatable |
IDisposable | DisposableResource |
IConnectable | Connectable |
IExtension | ExtensionLifecycle |
IRegistry | RegistryLike |
ITransform | Transform |
IEventContext | EventContextContract |
IMessageBroker | MessageBroker |
IResolver | ValueResolver |
ICommandHandler | CommandHandler |
IQueryHandler | QueryHandler |
ICommandBus | CommandBus |
IQueryBus | QueryBus |
IDomainEvent | DomainEventMessage |
IDomainEventHandler | DomainEventHandler |
IEventBus | EventBusContract |
IContext | Context |
IDispatcher | EventDispatcher |
IAMQPHandler | AMQPChannelHandler |
ArgumentReturn | TypeScript ParameterDecorator |
MethodReturn | TypeScript MethodDecorator |
Dictionary<T> | TypeScript Record<string, T> |
ICommand, IQuery | object; typed handler contracts retain their input relationships |
IFunction, isFunctionalSupported | The existing Interceptor contract and corrected isInterceptor predicate |
Duplicate Resolver<R> | AuthResolver<R>, extending the shared ValueResolver contract |
String alias MessageBroker | string for service broker identifiers; MessageBroker<T> now names the former IMessageBroker<T> contract |
Use the exported contract names in application code. Update type imports and implementations together, and validate dynamic values before assigning them to these contracts.
Logging applies mandatory redaction: arbitrary message/error text and payloads are redacted; reviewed static event labels and allowlisted operational fields are retained. The same policy covers explicit telemetry reports, worker stdout/stderr, and messaging log callbacks. JSON is the default output format, logger configuration applies to existing instances, and per-logger redaction can only remove more data. See logging privacy for configuration, examples, and redaction limits.
Based on 4.0.0 release source (4.0.0).