Guide · @staticflux/runtime
Lifecycles & the runtime
Every lifecycle in StaticFlux is a typed finite state machine. The runtime supervises the app, its subsystems and its features — and the state you are in decides which operations even exist.
#The app is a supervisor
starting → core-ready → running ⇄ degraded
running ⇄ paused
running|degraded|paused → reinitializing → core-ready
any → shutting-down → terminated
bootApp({ units, telemetry, subsystems }) resolves critical units, starts critical subsystems and returns a Result<App>. The supervisor broadcasts app-wide commands in dependency order (reverse for teardown): pause, resume, abort (one root signal cancels everything in flight), refresh (an epoch bump — stale data is revalidated and late responses dropped, no reload), reinitialize (back to core and re-boot, no reload) and shutdown.
Subsystems declare dependsOn and a restart policy (backoff, maxRestarts per windowMs). A subsystem that gives up moves the app to degraded and its dependents are told they are impaired. Features may request commands (supervisor.request); a command policy decides.
import { defineSubsystem } from '@staticflux/runtime';
export const syncSubsystem = defineSubsystem({
id: 'app/background-sync',
dependsOn: ['router/navigation'],
restart: { maxRestarts: 3, windowMs: 60_000 },
start: async (ctx) => queue.connect(ctx),
pause: () => queue.hold(),
resume: () => queue.release(),
refresh: async (epoch) => queue.resetFor(epoch),
stop: () => queue.close(),
});
#Features: logic and UI live separately
declared → loading → loaded → activating → activated
activated → mounting → mounted ⇄ hidden
mounted|hidden → unmounting → activated (logic stays warm)
activated → deactivating → loaded (logic disposed)
loaded → archived (heavy: snapshot persisted) | evicted
A feature is defineFeature({ id, weight, retention, modes, budget, readiness, load }) plus a module from defineFeatureModule({ units, activate, mount, visible, hide, unmount, snapshot, restore }). The logic scope contains the UI scope, which contains the visible scope — so a mounted feature always has live logic (I-5), and hidden keeps DOM and state while dropping effects.
Weight classes set the default demotion: cheap features are recreated, warm keep logic alive, heavy ones archive a snapshot (to any AsyncStore) and rehydrate fast. The resource manager enforces budgets on memory pressure and device tier — framework budgets win over feature preferences — and never demotes a feature in use. Dependencies between features are ref-counted (I-6).
#Frame scheduler
createFrameScheduler() coalesces updates into one flush per frame, separates measure and mutate phases (no layout thrashing), runs prioritized postTask work (interaction | critical | visible | idle), offers yieldToMain and idleUntilUrgent, and falls back where scheduler.postTask / yield are missing (Safari).
#Workers: scheduling and memory ledgers
Inside workers, @staticflux/runtime/worker gives the same priorities without frames: createWorkerScheduler() slices synchronous work into 8 ms tasks (postTask, yield, shouldYield), and createLongTaskDetector reports tasks over 50 ms. @staticflux/runtime/memory is a per-thread cache memory ledger for render, media and export workers: caches register with an evict callback and report their bytes; over budget — or when memory pressure rises — the ledger evicts lower priorities first, synchronously, before the resource manager demotes any feature.
import { createMemoryBudgets, MiB } from '@staticflux/runtime/memory';
const memory = createMemoryBudgets({ budgets: { gpu: 512 * MiB }, pressure });
const tiles = memory.register({ id: 'render/tiles', kind: 'gpu', evict: (bytes) => cache.evict(bytes) });
if (!tiles.ok) return tiles;
tiles.value.report(cache.bytes); // after every change; headroom() before a big allocation
All lifecycles, transitions and invariants
AGENTS.md on GitHub: @staticflux/runtime · @staticflux/core