Skip to content

Runtime Lifecycle

James Daley edited this page Jun 18, 2026 · 3 revisions

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.

Boot Pipeline

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
Loading

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.

Discovery and Registration

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
Loading

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.

Activation Logic

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
Loading

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.

Runtime Requirement Path

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
Loading

Runtime requirement validation happens after the registry returns a module and before start(context:). A failing requirement prevents lifecycle code from running.

Module State Model

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.

Entitlement Reconciliation

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
Loading

Entitlement reconciliation is conservative. It deactivates modules that are no longer unlocked and leaves still-unlocked modules active.

Shutdown

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"]
Loading

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 Handling

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.

Lifecycle Checklist

  • 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.

Clone this wiki locally