Lifecycle
Lifecycle β @equinor/fusion-framework-module
This document describes the full module lifecycle: what phases exist, what runs in each phase, what order things happen, and how to hook into each phase as a module author or as a consumer.
Overview
Calling ModulesConfigurator.initialize() runs a deterministic lifecycle pipeline. The key ordering guarantee is:
Every module's configure phase completes before any module's initialize phase begins.
This means it is always safe to inspect another module's config inside postConfigure, and always safe to access another module's provider inside postInitialize. The framework enforces these boundaries so you do not have to reason about timing.
Phase 1 β Configure
The configure phase builds config for every module before anything initializes. The framework calls module.configure(ref?) to create a fresh ConfigBuilder for each module, then invites consumers to mutate it.
What runs and in what order
module.configure(ref?)β The module creates and returns a new config builder instance. This is a synchronous factory call. Therefparameter carries whatever was passed toinitialize(ref)β typically a parent framework instance.addConfig({ configure })callback β The consumer's callback receives the config builder and can call typed setter methods on it (builder.setBaseUrl(...),builder.setCredentials(...), etc.). This is where application-level configuration lives.module.postConfigure(configMap)β Runs after all consumerconfigurecallbacks have settled. Receives the full map of all modules' resolved configs. Use this to apply defaults that depend on what other modules configured, or to validate your own config.addConfig({ afterConfig })callback β Consumer hook that runs afterpostConfigure. Rarely needed; most configuration belongs in step 2.onConfigured(cb)callback β Global hook that runs once all per-module configure phases are done. Use it to inspect the full config map, log configuration, or run cross-module config validation.
When to put configuration here
Put everything the module needs to know before it can create a provider in the configure phase. URLs, credentials, feature flags, timeouts β all of these belong in config builder setters called inside the configure callback.
Do not put async work that depends on the network or on other providers here. That belongs in initialize.
Phase 2 β Initialize
Once all modules are configured, the framework initializes them concurrently. Every module's initialize starts at roughly the same time. This maximises startup speed, but it requires care when modules depend on each other.
What runs
module.initialize({ config, requireInstance, hasModule, ref })β The module receives its resolved config and creates the provider. UserequireInstance(name, timeout?)to await a peer module's provider. See Cross-Module Dependencies.module.postInitialize({ instance, modules, ref })β Runs after the module's owninitializeresolves AND after all other modules have initialized. The fullmodulesmap is available here. Use this for cross-module wiring β subscribing to event streams, injecting references, setting up reactive pipelines between providers.addConfig({ afterInit })callback β Consumer hook that runs afterpostInitialize. Useful for one-shot wiring that the framework does not know about.onInitialized(cb)callback β Global hook that runs once all modules have fully initialized. TheModulesInstanceis complete at this point.Plugin phase begins β The configurator runs
registerPlugincallbacks before resolvinginitialize().
The ref parameter
initialize(ref?) accepts an optional ref object that the framework threads through every lifecycle hook. In Fusion Framework, ref is typically the parent framework instance β it gives modules a back-reference to the environment they are running inside. Module authors should type ref as unknown or a narrow interface and treat it as optional.
Phase 3 - Plugins
The plugin phase runs after postInitialize, afterInit, and onInitialized callbacks have settled, but before initialize() resolves. Plugins are registered with IModulesConfigurator.registerPlugin and receive the sealed module instance map plus the optional ref object.
Use plugins for host-level side effects that need multiple initialized providers, such as telemetry bridges, global listeners, feature instrumentation, or subscriptions owned by the application shell.
import { createPlugin } from '@equinor/fusion-framework-module/plugins';
const telemetryPlugin = createPlugin<[typeof eventModule, typeof telemetryModule]>(
'contextTelemetry',
(modules) => modules.event.addEventListener('context:changed', (event) => {
modules.telemetry.track('context.changed', event.detail);
}),
);
configurator.registerPlugin(telemetryPlugin);Plugins can return a teardown function or an object with a dispose() method. Those teardowns run during configurator.dispose() before module dispose hooks. Plugin registration and teardown failures are isolated and reported as warning events, so one failing plugin does not prevent the remaining lifecycle work from running.
After plugins have settled, the promise returned by configurator.initialize() resolves with the sealed ModulesInstance.
Phase 4 β Dispose
Dispose is not called automatically. It is your responsibility to call configurator.dispose(instance, ref?) when the application tears down β for example, when a React application unmounts, or when a service worker is replaced.
The framework first runs plugin teardowns returned by registerPlugin callbacks. It then calls module.dispose({ instance, modules, ref }) for each registered module. This is the right place to cancel subscriptions, close WebSocket connections, clear caches, and release any other resources the provider holds.
BaseModuleProvider exposes a subscription: Subscription property (RxJS Subscription) that collects disposables. Adding to this.subscription inside initialize means they are automatically cleaned up when the provider's dispose() method is called β you do not need to track them separately.
class MyProvider extends BaseModuleProvider {
constructor(args: BaseModuleProviderCtorArgs) {
super(args);
// Any RxJS subscription added here is automatically cancelled on dispose.
this.subscription.add(
someObservable$.subscribe((value) => this.#handleValue(value)),
);
}
}Hook Reference
| Hook | Where it lives | Called by | When |
|---|---|---|---|
module.configure(ref?) | Module definition | Framework | Start of configure phase, once per module |
addConfig({ configure }) | Consumer | Framework | After module.configure(), before postConfigure |
module.postConfigure(configMap) | Module definition | Framework | After all consumer configure callbacks settle |
addConfig({ afterConfig }) | Consumer | Framework | After module.postConfigure() |
onConfigured(cb) | Consumer | Framework | After all modules' configure phases complete |
module.initialize(args) | Module definition | Framework | Start of initialize phase, concurrently |
module.postInitialize(args) | Module definition | Framework | After all modules have initialized |
addConfig({ afterInit }) | Consumer | Framework | After module.postInitialize() |
onInitialized(cb) | Consumer | Framework | After all modules' initialize phases complete |
registerPlugin(cb) | Consumer | Framework | After post-initialize callbacks, before initialize() resolves |
| plugin teardown | Consumer | Framework | During configurator.dispose(instance), before module dispose hooks |
module.dispose(args) | Module definition | Framework | On configurator.dispose(instance) |
Next Steps
- Authoring Modules β implement each hook step by step
- Cross-Module Dependencies β how
requireInstanceandpostInitializework together - Plugins β register host-level side effects after modules are initialized
- Events β observe the lifecycle with
event$