Engine
The engine is the runtime assembly layer for TWIN capabilities. It takes structured type configuration, initialises connectors and components, registers named instances, manages lifecycle, and provides a stable runtime surface for server routes, processors, background tasks, and extension modules.
Core Responsibilities
At a high level, the engine performs six responsibilities:
- Load and maintain runtime state through pluggable state storage.
- Initialise configured connector and component types.
- Register created instances in shared factories.
- Track instance metadata such as defaults, features, and names.
- Run deterministic lifecycle phases (
bootstrap,start,stop). - Manage context IDs and context ID handlers for node, tenant, and other scoped execution keys.
EngineCore is the central implementation of this behaviour. The Engine package extends and
composes this core for full runtime use, but the operational mechanics are driven by EngineCore.
Runtime Data Model
IEngineCoreContext stores the runtime working set:
config: the active engine configuration.state: mutable persisted runtime state.stateDirty: a flag controlling state write-back.registeredInstances: per-type registry metadata (type,isDefault,features).componentInstances: concrete runtime instances plus initialisation status.
This separation is important. Factory registries provide lookup by name, while
componentInstances is the ordered list the engine uses for lifecycle execution.
Engine Runtime Model
Type Configuration and Instances
IEngineConfig extends IEngineCoreConfig and defines a strongly typed types map. Each map entry
is an array of IEngineCoreTypeConfig<T> values. Every entry can control:
type: concrete implementation selector.overrideInstanceType: explicit runtime instance name.isDefault: default instance marker for lookups that do not specify a name.isMultiInstance: per-call dynamic instance generation model.features: feature tags used for contextual resolution.- optional route metadata (
restPath,socketPath, options).
The engine records the resulting registrations in registeredInstances, then provides helper
resolution methods:
getRegisteredInstanceType(type, features?).getRegisteredInstanceTypeOptional(type, features?).
Default selection first honours isDefault, then falls back to first configured entry.
Initialisation Pipeline
On start(), the engine performs:
- Internal logging setup (
engine-logging-connector,engine-logging-service). - State load from configured state storage.
- Iteration over all registered type initialisers.
- Context ID handler registration.
- Bootstrap phase.
- Start phase (unless explicitly skipped).
- State save in finalisation.
If any step fails, the engine calls stop(), logs the error chain, and rethrows.
Type initialisers are registered with (type, module, method) metadata and loaded dynamically at
runtime. For each config entry in types[type], the initialiser returns:
- a
createComponentfunction, - an
instanceTypeName, - and the target factory for registration.
The engine then computes a final instance name (overrideInstanceType or returned type name),
creates the instance, pushes it into componentInstances, and registers it in the returned factory.
Component Workflow in Engine
Component initialisation follows the same engine type pipeline as connectors, but with component specific orchestration semantics.
Component Workflow Inside Engine Initialisation
- Type selection: the engine reads `types.<componentType>[]` and selects each entry in order.
- Factory-level type resolution: `createComponent` is chosen from the component type (for example service or rest-client).
- Options merge: default options and config options are merged, commonly injecting active connector instance types.
- Instance creation: the component is created once and pushed into `context.componentInstances`.
- Instance registration: the instance is registered in `ComponentFactory` under `overrideInstanceType` or generated default instance name.
- Runtime resolution: routes and processors call `ComponentFactory.get(...)` and execute business logic through the selected component instance.
A practical example is logging:
- logging connector initialiser selects
console,entity-storage, ormulticonnector type, then registers the connector inLoggingConnectorFactory. - logging component initialiser selects
serviceorrest-clienttype. - for
service, it injects defaultloggingConnectorTypefrom the currently registered connector instance and createsLoggingService. - the created component is registered in
ComponentFactoryand tracked for lifecycle.
This pattern repeats across domains, enabling consistent runtime composition while preserving domain specific implementation details.
Lifecycle Semantics
Lifecycle execution is deterministic and instance based:
bootstrap: executed first over all tracked instances. Any false result is treated as failure.start: executed after successful bootstrap for each uninitialised instance.stop: executed when shutting down for each initialised instance.
Each lifecycle method receives a logging component type parameter, so startup and shutdown logging can be routed consistently.
When skipComponentStart is used, components are marked initialised without calling start, which
allows subsequent stop logic to still run for cleanup.
This is used by TWIN Node during CLI command execution. In node-core command flow,
executeCommand starts the engine with engineCore.start(true), so the engine can be configured,
state and bootstrap logic can run, and command actions can execute without fully starting component
runtime services.
State and Cloning
The engine supports pluggable state backends and clone replication.
stateLoad()pulls persisted state into context.setStateDirty()marks state for persistence.stateSave()writes state only when dirty and non-empty.
getCloneData() captures configuration, state, registered type initialisers, context ID keys, and
entity schemas. populateClone() reconstructs an engine clone from this payload using in-memory
state storage, enabling worker-based or task-based engine replication.
Context IDs and Feature-Aware Handlers
The engine can register context ID keys with required handler features, then bind concrete handlers
through ComponentFactory into ContextIdHandlerFactory. This enables contextual execution for
scopes such as node or tenant, while keeping handler implementation selection configuration driven.
Feature tags in registeredInstances support selecting handlers and other instance types by
capability rather than only by static name.
Multi-Instance Mode
isMultiInstance changes registration semantics. Instead of registering a singleton instance,
engine registers a factory callback that creates instances from supplied params. This is useful for
cases such as per-endpoint rest clients or dynamically scoped component instances.
For non-multi-instance entries, the engine registers and reuses a singleton instance.
Operational Notes
- Keep
overrideInstanceTypestable where routes or processors reference explicit names. - Use
isDefaultfor expected fallback behaviour. - Use
featuresfor context-sensitive resolution rather than proliferating hard-coded instance names. - Prefer component-level business logic and connector-level infrastructure logic to preserve clean separation of concerns.
Further Reading
- Components: component behaviour and responsibilities.
- Connectors: connector architecture.
- Node Runtime: node startup and environment-driven configuration assembly.
- Context IDs: context key registration, handlers, and async propagation.