-
Notifications
You must be signed in to change notification settings - Fork 0
Runtime Lifecycle
The Forsetti runtime lifecycle has five phases: construct, boot, discover, activate, and reconcile. Shutdown reverses activation without erasing persisted activation state unless the caller explicitly changes activation state before shutdown.
flowchart TD
A["Create ForsettiRuntime"] --> B["Build CompatibilityChecker"]
B --> C["Build ForsettiContext"]
C --> D["Build ModuleManager"]
D --> E["boot(bundle, subdirectory)"]
E --> F["ManifestLoader.loadManifests"]
F --> R["Create or refresh ModuleRegistrationRecord entries"]
R --> G["Refresh entitlements"]
G --> H["Start entitlement observation"]
H --> I{"restoreActivationState?"}
I -- yes --> J["Reconcile persisted desired activation"]
I -- no --> K["Return discovered manifests"]
J --> K
ForsettiRuntime owns the event bus, UI surface manager, module manager, overlay router, entitlement provider, and runtime version. The module manager receives the configured manifest loader, registry, capability policy, activation store, registration store, entitlement provider, UI surface manager, and scoped context.
sequenceDiagram
participant Runtime as ForsettiRuntime
participant Loader as ManifestLoader
participant Manager as ModuleManager
participant Store as ModuleRegistrationStore
Runtime->>Manager: discoverModules(bundle, subdirectory)
Manager->>Loader: loadManifests
Loader-->>Manager: manifestsByID
loop each discovered manifest
Manager->>Store: loadRecord(moduleID)
alt matching record exists
Store-->>Manager: no change
else missing or stale
Manager->>Store: saveRecord(ModuleRegistrationRecord)
end
end
Manager-->>Runtime: discovered manifests
Registration records capture a manifest hash and core identity fields. They are refreshed during discovery and validated again during activation so stale or missing registration data fails clearly.
flowchart TD
A["activateModule(moduleID)"] --> B{"Manifest discovered?"}
B -- no --> X["moduleNotDiscovered"]
B -- yes --> C["Evaluate compatibility"]
C --> D{"Any error severity issue?"}
D -- yes --> Y["incompatible(report)"]
D -- no --> Reg{"Registration record matches?"}
Reg -- no --> RM["registrationMissing or registrationMismatch"]
Reg -- yes --> E["Check entitlement provider"]
E --> F{"Unlocked?"}
F -- no --> Z["moduleLocked"]
F -- yes --> G{"moduleType"}
G -- service --> H["Resolve module factory"]
G -- ui/app --> I["Resolve module factory and require ForsettiUIModule"]
H --> V["Validate descriptor and manifest identity"]
I --> V
V --> RR["Validate runtime requirements"]
RR --> UI{"UI/app module?"}
UI -- yes --> U["Validate UI contribution capabilities and declarations"]
UI -- no --> J["start(scoped context)"]
U --> J
J --> L{"start throws?"}
L -- yes --> R["Report module error and roll back loaded module"]
L -- no --> M["Record active state"]
M --> N{"UI or app module?"}
N -- yes --> O["Apply sanitized UI contributions"]
N -- no --> P["Persist activation state"]
O --> P
Activation uses a capability-scoped ForsettiContext. Service resolution inside the module is filtered by the manifest's granted capabilities, and module identity is bound to the activated module ID.
flowchart LR
Manifest["ModuleManifest"] --> Role["defaultModuleRole"]
Manifest --> IO["runtimeRequirements.io"]
Manifest --> Data["runtimeRequirements.dataIsolation"]
Manifest --> UIReq["runtimeRequirements.ui"]
Role --> RoleValid["Role valid for moduleType and capability"]
IO --> IOValid["Capabilities and providers available"]
Data --> DataValid["Isolation mode and provider roles available"]
UIReq --> UIValid["Contributed UI IDs declared"]
RoleValid --> Pass["Activation can enter lifecycle"]
IOValid --> Pass
DataValid --> Pass
UIValid --> Pass
Runtime requirement validation happens after the registry returns a module and before start(context:). A failing requirement prevents lifecycle code from running.
| State | Meaning | Stored In |
|---|---|---|
| Discovered | Manifest was loaded and indexed by moduleID. |
manifestsByID |
| Registered | Manifest identity and hash were stored for later drift detection. | ModuleRegistrationStore |
| Loaded | Module instance was created from the registry. | loadedModules |
| Enabled service | Service module is active and started. | enabledServiceModuleIDs |
| Enabled UI | Compatibility state for the active UI/app module. The default model keeps this empty or one ID. | enabledUIModuleIDs |
| Active UI | UI/app module currently active for presentation. Activating another UI/app module deactivates the previous one. |
activeUIModuleID / selectedUIModuleID
|
Persisted activation state is desired state. Restore activates persisted IDs through the same compatibility, registration, entitlement, identity, runtime requirement, lifecycle, and UI contribution path used for explicit activation. Failed restores do not leave false active state behind.
sequenceDiagram
participant Provider as EntitlementProvider
participant Runtime as ForsettiRuntime
participant Manager as ModuleManager
participant Module as Active Module
participant Store as ActivationStore
Provider-->>Runtime: entitlementsDidChangeStream event
Runtime->>Manager: inspect enabled service and UI modules
loop each active module
Runtime->>Provider: isUnlocked(moduleID, productID)
alt no longer unlocked
Runtime->>Manager: deactivateModule(moduleID)
Manager->>Module: stop(context)
Manager->>Store: save updated activation state
else still unlocked
Runtime-->>Runtime: leave active
end
end
Entitlement reconciliation is conservative. It deactivates modules that are no longer unlocked and leaves still-unlocked modules active.
flowchart LR
A["runtime.shutdown()"] --> B["Cancel entitlement observation"]
B --> C["deactivateAllModules(persistState: false)"]
C --> D["Call stop(context) on loaded modules"]
D --> E["Clear loaded and enabled runtime state"]
E --> F["Leave persisted activation state unchanged"]
Shutdown is suitable for app lifecycle teardown. If a product decision should disable a module permanently, call deactivateModule(moduleID:) before shutdown so the activation store is updated.
| Failure | Result |
|---|---|
| Missing manifest directory | Boot throws manifestsDirectoryNotFound. |
| Invalid manifest JSON | Discovery throws validationFailed. |
Duplicate moduleID
|
Discovery throws duplicateModuleID. |
| Unsupported schema or manifest template | Discovery or compatibility fails with a schema/template issue. |
| Missing registration record | Activation throws registrationMissing. |
| Stale registration record | Activation throws registrationMismatch. |
| Denied capability | Activation throws incompatible(report:). |
| Locked module | Activation throws moduleLocked. |
| Factory returns mismatched descriptor or manifest identity | Activation throws moduleIdentityMismatch. |
Factory returns a non-UI module for ui or app manifest after identity validation |
Activation throws notUIModule. |
| Missing service or UI capability | Activation throws missingCapability or unsatisfiedRuntimeRequirement. |
| Required provider unavailable | Activation throws unsatisfiedRuntimeRequirement. |
| UI contribution lacks a manifest declaration | Activation throws unsatisfiedRuntimeRequirement. |
start(context:) throws |
Module instance is removed from loadedModules, the error is logged, and activation fails. |
- Discover manifests before activation.
- Confirm registration records are present and match the current manifest.
- Evaluate compatibility reports before entitlement checks.
- Keep entitlement providers platform-appropriate.
- Register module factories for every manifest
entryPoint. - Keep descriptor and manifest identity fields aligned.
- Validate runtime requirements before lifecycle side effects.
- Keep
start(context:)fast, deterministic, and idempotent. - Keep
stop(context:)safe to call during shutdown or entitlement reconciliation.