Runtime
The PluginRuntime is the lifecycle engine. It owns the global scope, spawns sessions, holds the current RuntimeSettings snapshot, exposes a change stream, and applies updates across the runtime and its active sessions in one call.
This page covers PluginRuntime. For session-specific concerns (isolation, cross-scope routing, per-session contexts), see Sessions.
Constructing and initializing
Section titled “Constructing and initializing”/// Demonstrates the full construct → init → createSession lifecycle.Future<void> constructAndSession() async { final runtime = PluginRuntime(plugins: [CasualPlugin(), FormalPlugin()]);
runtime.init(settings: const RuntimeSettings());
final session = await runtime.createSession(); print('session registry keys: ${session.registry.listAllServiceIds()}'); await runtime.dispose();}PluginRuntime is generic over the global and session context types: PluginRuntime<G extends GlobalPluginContext, S extends SessionPluginContext>. Bare PluginRuntime infers the defaults; supply your own subtypes only when you need a custom context.
What init does
Section titled “What init does”“Set up the world, then attach everything that was supposed to be on.”
When you call runtime.init(...), this happens in order:
- The global registry and global bus are created.
- Global service overrides from settings are parsed into
LocalPluginOverrides. - The runtime walks the plugin list and decides which global plugins are enabled.
- Each enabled global plugin runs
registeragainst aScopedServiceRegistrythat already knows itspluginId. - The global context is built (default, or via your
globalContextFactory). - Wildcard winner-scoped overrides get materialized onto the winners.
- Each enabled global plugin runs
attach(context). The runtime currently visits plugins in the order they were added, but this ordering is not part of the contract; plugin code must not rely on a particular peer being attached first. Resolve services at point of use, and use events orresolveAfterto coordinate between plugins.
After that, the runtime is ready to spawn sessions. You can open one per user action, keep one long-lived session, or mix the two.
Plugins not listed in RuntimeSettings.plugins default on, unless they carry FeatureFlag.experimental, which defaults off. Listing only some plugin ids does not narrow the enabled set by itself. To narrow it, add explicit enabled: false entries for plugins you want off, then pass those settings to init (or, later, to updateSettings).
Settings reconciliation
Section titled “Settings reconciliation”Users toggle features. Config values change. Priorities shift. You want the runtime to converge on the new state without writing hand-rolled teardown code each time. Reconciliation is the runtime’s job.
Full reconciliation
Section titled “Full reconciliation”Future<void> disableAnalytics(PluginRuntime runtime) async { final next = runtime.settings.copyWith( plugins: { ...runtime.settings.plugins, const PluginId('analytics'): const PluginConfig(enabled: false), }, );
await runtime.updateSettings(next);}That reconciles the global scope, reconciles every active session, and publishes the new snapshot on settingsStream. If the analytics plugin was attached, it gets detached. If something was waiting on a capability that analytics provided, the ripples show up during this call.
Snapshot-only update
Section titled “Snapshot-only update”/// Demonstrates emitting optimistic snapshot then confirming with a full/// reconciliation.Future<void> snapshotThenReconcile( PluginRuntime runtime, RuntimeSettings pendingSettings,) async { // Optimistic UI: settingsStream emits immediately. runtime.updateSettingsSnapshot(pendingSettings);
// Later, if the user confirms, run the real reconciliation. await runtime.updateSettings(pendingSettings);}updateSettingsSnapshot replaces the stored settings and emits on the stream. It does not run reconciliation, and it does not inject new config into already-resolved services. Think of it as “tell listeners, leave the runtime alone.”
| Method | What it does |
|---|---|
updateSettings(...) | Full reconciliation across runtime and sessions, plus stream emission. |
updateSettingsSnapshot(...) | Stream emission only; runtime is untouched. |
updateSettingsSnapshot is for bookkeeping updates, preview states, optimistic UI, anything where you are confident the runtime does not need to converge. If the distinction feels unclear at a call site, reach for updateSettings. Reconciling more than you needed to is cheaper than skipping reconciliation you needed.
Enablement precedence
Section titled “Enablement precedence”When deciding which plugins are enabled for a given settings snapshot, the runtime applies these rules in order:
FeatureFlag.lockedwins immediately. Nothing can disable a locked plugin.- Explicit
RuntimeSettings.plugins[pluginId].enabledis respected next. - Otherwise the experimental heuristic: stable plugins default on, experimental plugins default off.
Dependency validation runs after that base pass. If a plugin depends on something disabled, it gets disabled too. The exception is locked plugins: they stay enabled and the runtime logs a configuration error for the missing dependency.
Reconciliation is symmetric across scopes
Section titled “Reconciliation is symmetric across scopes”Global and session scopes run the same lifecycle on a toggle.
- Disable →
plugin.detach(context)runs, then the plugin’s services are unregistered. - Enable →
plugin.register(scopedRegistry)runs, thenplugin.attach(context). - Survivor → every still-enabled plugin receives
onPluginSettingsChanged(oldContext, newContext)so it can re-read its config and adjust without being re-attached.
This page covers what the runtime does during reconciliation. The lifecycle hooks themselves are documented in Plugins and Plugin Services. attach and detach are pure user hooks; the framework runs orchestration (stateful service attach, subscription cleanup) around them, so no super call is needed.