Reaktor Core — the foundation layer
reaktor-core is the layer everything else stands on: the Feature slot registry that lets a graph node ask for Feature.Database without ever branching on if (isAndroid), the Capability AutoCloseable resource marker, the Adapter weak-reference bridge to platform controllers, and the cross-platform primitives (time, coroutine dispatchers, logging, atomics, serialization). It exists, in the words of the spec, "to eliminate every 'is this Android?' branch." This doc is the code-grounded foundation half of the merged core + capability entry; the capability runtime that normalizes device/network facts into tiers is a designed plane that installs on core via a Feature slot — covered in depth in reaktor-capability, which this doc bridges to rather than duplicates.
The honest framing up front: core is real and the most-used module in the framework — but lean; the capability runtime is a plan, not a module. Feature, Capability, Adapter, and the primitives all ship and are used across bestbuds and every reaktor module. What is not there: any auto-bootstrap of platform adapters (registration is manual), a finished iOS controller path, a thread-safe way to reconfigure the global JSON, and — entirely — reaktor-capability as a Gradle module (no EnvironmentSnapshot, no CapabilityTier, no normalized enums exist in code; the spec's "fully typed but design-stage" is aspirational). And one piece of real technical debt this doc names head-on: the word "capability" is overloaded four ways, none of which is the object-capability security model.
Featurethe slot registry —var Feature.X by CreateSlot<T>(); register at the boundary, read anywhere without a platform branchCapabilitya minimalAutoCloseableresource marker (AtomicCapability= idempotent CAS close) — not device tiers, not a security tokenAdapter<Controller>aWeakRefbridge so common code drives a platform controller without owning or leaking it- Primitives time · dispatchers · logging · atomics · serialization — the batteries that erase platform branches
- The capability runtime sits on core raw platform facts → normalized tiers (never raw scores) → a strategy decision; installed via
Feature.Capability(reaktor-capability) - The honest law expose tiers, not raw fingerprints — the same entropy-reduction the web platform now enforces (UA reduction,
deviceMemorypower-of-two rounding, bucketedeffectiveType)
1. Vision — erase the platform branch
Every cross-platform framework dies the same death: if (Platform.isAndroid) … else if (isIOS) … else if (isBrowser) … metastasizing through business logic until the "shared" code is a switch statement with shared imports. reaktor-core exists to make that branch impossible to write. A graph node asks for Feature.Database or Feature.Camera; whichever platform booted registered the right implementation into that slot; the node never learns which platform it is on. Platform-specific power lives behind an Adapter that common code drives through a weak handle; platform differences in time, threading, atomics, and serialization are absorbed by expect/actual primitives.
This is L0 — the foundation layer, below the graph runtime, that every other module imports. It is deliberately small: a registry, a resource marker, an adapter, and a handful of primitives. The discipline is "add nothing here that a higher layer could own." Concretely: core does not do dependency injection (Koin does, bridged through a slot), does not know about the graph, and does not grow the capability runtime inside itself — that installs on core, through a slot, as its own module.
2. Implementation state
Written against the live code in reaktor-core (and consumers in bestbuds + the other reaktor modules). Paths are module-relative.
| Capability | Where | Status |
|---|---|---|
Feature slot registry — object Feature, ConcurrentHashMap<Int,Any> + AtomicInt, createId/storeDependency/fetchDependency/close; var Feature.X by CreateSlot<T>() | reaktor-core/.../framework/Feature.kt | Implemented (~21 live slots) |
Capability : AutoCloseable marker + AtomicCapability (atomic(false) CAS close-guard + doClose()) | reaktor-core/.../capabilities/Capability.kt | Implemented |
Adapter<Controller> — WeakRef<Controller>, null-safe invoke/suspended/invokeResult/suspendedResult, handle(event) | reaktor-core/.../framework/Adapter.kt | Implemented |
WeakRef / AtomicInt / ConcurrentHashMap (456-line lock-free) — expect/actual per target (JS WeakRef is a no-op) | reaktor-core/.../framework/Expectations.kt, .../structs/ | Implemented |
Platform code in src/{android,ios,jvm,js}Main; Android registers via BaseActivity.connect(adapters) | reaktor-core/src/*Main | Partial (manual registration; no auto-bootstrap; iOS BaseController is a stub) |
Primitives: epochTime() (kotlinx-datetime), Dispatch.{Main,IO,Default} (+ Dispatchers.Async expect/actual), Kermit logging, atomicfu | reaktor-core/.../utils, .../framework/Dispatch.kt | Implemented |
JSON — kotlinx.serialization + JsonBuilder.defaults() (encodeDefaults, ignoreUnknownKeys, classDiscriminator="type") | reaktor-core/.../framework/Json.kt | Implemented (global var json not safe to reconfigure concurrently) |
| Dependency injection | Koin, via the Feature.Dependency slot → reaktor-graph DI adapter | Implemented (separate concern; Feature ≠ DI) |
reaktor-capability module + EnvironmentSnapshot/CapabilityTier/PerformanceClass/NetworkCost/ThermalClass/CapabilityStrategy | — | Absent (not in settings.gradle.kts; zero types in code) |
| Privacy normalization layer (no-raw-strings enforcement) | — | Absent (raw getShibasisUserAgent() exists on web) |
WindowWidthClass-style adaptive layout in reaktor-ui | — | Absent |
Takeaway: Feature is the framework's spine — used by bestbuds and by reaktor-db/-media/-auth/-ui/-io/-telemetry/-work for ~21 slots. Capability (the marker) is used lightly (as the base of the graph's behavioral mixins). The capability runtime — the thing that would turn raw device facts into tiers — does not exist yet. Core is "Stable" in the sense the spec means (the API is small, settled, and load-bearing), with honest rough edges on iOS/web and bootstrap.
3. The five concepts, and the four meanings of "capability"
The single most important thing this doc does is disambiguate. "Capability" is overloaded across the codebase, and a reviewer steeped in security will misread it. State the boundaries before anything else.
| Concept | Question it answers | Owner | Examples |
|---|---|---|---|
Feature slot | Which process-level service/adapter is installed? | reaktor-core | Feature.Auth, Feature.Database, Feature.Camera, Feature.Work |
Capability (resource) | Is this a closable resource with a lifecycle? | reaktor-core | an AtomicCapability owning a socket / scope / handle |
| Runtime-mixin capability | Which behavior does this graph object compose? | reaktor-graph, reaktor-graph-port | LifecycleCapability, DependencyScopeCapability, PortCapability |
| Permission | Has the OS/user granted a privileged action? | adapters | camera, location, notifications |
| Environment capability (tier) | What can the device/network do right now, and at what quality? | reaktor-capability (designed) | PerformanceClass.Low, NetworkCost.Metered, ThermalClass.Serious |
| Strategy | Which implementation should run under the current tier + policy? | reaktor-capability + owning module | remote push vs local notify; multipart vs queued upload |
The rest of this doc walks core's three primitives — Feature, Capability, Adapter — then the primitives, then how the capability runtime sits on top.
4. Feature — the slot registry
Reaktor primitive: object Feature, a process-level, thread-safe typed slot registry. A module declares a slot with a delegate; a platform/bootstrap fills it; any code reads it without knowing the platform.
// Declare a slot (in the owning module):
var Feature.Database by CreateSlot<ObjectDatabase>()
var Feature.Camera by CreateSlot<CameraAdapter<*>>()
// Fill it at the boundary (platform bootstrap / app start):
Feature.Database = sqlDelightDatabase() // the actual impl for this target
// Read it anywhere — no `if (isAndroid)`:
val db = Feature.Database
Under the hood Feature is a singleton over a lock-free ConcurrentHashMap<Int, Any> keyed by an AtomicInt-allocated slot id, with createId / storeDependency / fetchDependency<T> / close. ~21 slots ship today — Auth, Database, Sql, GraphQl, Camera, Gallery, Speech{Recognizer,Synthesizer}, GooglePubSub, Work, Theme, Location, Compression, File, Permission, Storage, Analytics, Crashlytics, Telemetry, Dependency — and bestbuds plus most modules read them. 80/20: reaktor owns the typed registry; platform bootstraps and modules populate the slots.
5. Capability — the resource marker
Reaktor primitive: interface Capability : AutoCloseable — a minimal marker for "a thing with a lifecycle you must close" — and AtomicCapability, which adds an idempotent close guard so a double-close() is a safe no-op.
interface Capability : AutoCloseable
abstract class AtomicCapability : Capability {
private val closed = atomic(false) // kotlinx-atomicfu, multiplatform
final override fun close() {
if (closed.compareAndSet(expect = false, update = true)) doClose()
}
protected abstract fun doClose() // runs exactly once
}
This is exactly the canonical pattern the Kotlin stdlib prescribes — "close should be idempotent" — realized with an atomic CAS so it is correct under concurrency. The companion discipline is scope ownership: a coroutine-scoped resource can be cancelled before its cleanup runs, so the owner of the CoroutineScope owns teardown (cancel the Job → resources close); use { } guarantees close on exception. 80/20: reaktor owns the close-guard contract; the resource's actual cleanup is doClose(). (Reminder: this Capability is a closable resource, not a device tier and not an authority token — §3.)
6. Adapter — the WeakRef platform bridge
Reaktor primitive: Adapter<Controller> — common code drives a platform controller (an Android ComponentActivity, an iOS view controller, a browser global) through a weak reference, so the framework can use platform power without owning the platform object or leaking it.
open class Adapter<Controller>(controller: Controller) : Component {
val ref: WeakRef<Controller> = WeakRef(controller)
val controller: Controller? get() = ref.get()
operator fun <R> invoke(fn: Controller.() -> R?): R? // null-safe: no controller → null
suspend fun <R> suspended(fn: suspend Controller.() -> R?): R?
// + invokeResult / suspendedResult returning Result<T>
open fun handle(event: ControllerEvent) { /* lifecycle */ }
}
WeakRef is expect/actual per target (JVM/Android → java.lang.ref.WeakReference, iOS → kotlin.native.ref.WeakReference, JS → a no-op, since the browser GC model differs). Concrete adapters — PermissionAdapter, StorageAdapter, CameraAdapter, AuthAdapter — subclass it and do the privileged platform work; common code holds only the weak handle and calls through invoke/suspended. This is the well-grounded KMP shape: a platform-agnostic interface with the actual injected at the boundary (the Essenty/Decompose model), preferred over expect class (still Beta in Kotlin 2.x). 80/20: reaktor owns the adapter contract + WeakRef discipline; the platform supplies the controller.
Honest gaps: adapters register manually (e.g. Android BaseActivity.connect(listOf(permissionAdapter, …)) wires them into the lifecycle) — there is no auto-bootstrap, so a forgotten registration fails at use-time; and the iOS BaseController is currently a stub. Auto-bootstrap + fail-fast registration is the core hardening item (§11).
7. Primitives — the batteries
The handful of cross-platform utilities that erase the most common platform branches:
- Time —
epochTime(): Longoverkotlinx-datetimeClock.System. - Dispatchers —
Dispatch.{Main, IO, Default}wrappers, withDispatchers.Asyncchosen per platform viaexpect/actual(Android/iOS → IO, JS → Default, JVM → IO). - Logging — a thin wrapper over Touchlab Kermit (
info/debug/warn/verbose). - Atomics & structures — kotlinx-atomicfu for multiplatform CAS, plus a hand-written 456-line lock-free
ConcurrentHashMap(open-addressing incremental resize, Herlihy & Shavit) — a deliberate primitive since the JS/Native worlds lack a shared one. (Comment in code: it trades ~2–5× the throughput ofjava.util.concurrentfor portability.) - Serialization —
kotlinx.serializationwithJsonBuilder.defaults():encodeDefaults = true,ignoreUnknownKeys = true,classDiscriminator = "type"(the discriminator the whole framework's sealed hierarchies rely on). Honest gap: the globalvar jsonis mutable and not safe to reconfigure concurrently — fine to read everywhere, risky to re-set at runtime.
These lean on the standard kotlinx ecosystem (atomicfu, datetime, and serialization), which is the right call — core wraps them with reaktor defaults rather than reinventing them.
8. The capability runtime sits on core
The foundation answers "which implementation runs here?" (via Feature + Adapter). The capability runtime extends the same idea to "at what quality tier, under the current conditions?" — and it installs on core through exactly one more slot:
// reaktor-capability (designed) installs itself as a Feature slot — it does NOT live in core.
var Feature.Capability by CreateSlot<CapabilityRuntime>()
interface CapabilityRuntime : AutoCloseable { // a Capability, fittingly
val environment: StateFlow<EnvironmentSnapshot>
suspend fun resolve(request: CapabilityRequest): CapabilityDecision
}
The runtime's job: collectors gather raw platform signals via the Adapter pattern → a normalizer folds them into one immutable EnvironmentSnapshot of normalized tiers (PerformanceClass, MemoryClass, NetworkQuality, NetworkCost, ThermalClass, PowerMode, FormFactor) → a StrategySelector picks an implementation by CapabilityTier and policy, with a traceable decision. Product code branches on CapabilityTier, never on a platform fact. This is the reaktor-capability plane in full — its collectors, normalizer, strategy engine, degradation paths, reaktorDesktop overlay, and build phases live in that doc; this section only establishes the seam (it installs via Feature.Capability, it is itself a Capability/AutoCloseable, it consumes Adapter-shaped collectors). Status: designed; the module does not exist yet.
9. The normalization & privacy law
Reaktor law: the public API exposes normalized tiers, never raw model strings, vendor IDs, user-agent platform strings, or raw hardware scores as business-logic switches. This is the load-bearing rule of the capability plane — and it is not a stylistic preference; it is the direction the platforms themselves now enforce:
- The web froze raw fingerprints. User-Agent reduction (Chrome, now GA) collapses the OS version and device model to anchor constants (
Android 10; K); User-Agent Client Hints make high-entropy values opt-in request-only.navigator.deviceMemoryis rounded to the nearest power of two by spec to cap entropy; the Network Information API exposes a bucketedeffectiveType(slow-2g|2g|3g|4g) and asaveDataboolean, not the raw radio. W3C fingerprinting guidance prescribes exactly three moves: clamp precision, enumerate over granularity, boolean-reduce. That is reaktor's tier model. - Android computes the tier for you. Media Performance Class (
Build.VERSION.MEDIA_PERFORMANCE_CLASS, via Jetpackandroidx.core:core-performance/ Play Services) is a platform-validated, OS-decoupled, forward-compatible integer tier — the exemplar reaktor'sPerformanceClassshould mirror (a tier that stays stable across OS upgrades, so strategy selection doesn't silently shift). - iOS hands you raw signals to normalize. There is no MPC equivalent — apps read
ProcessInfo(thermalState: nominal/fair/serious/critical,isLowPowerModeEnabled,processorCount,physicalMemory) and are expected to react to thermal/power pressure. So the iOSactualmust bucket those raw values into the same enums internally and never expose them — which is precisely where the privacy law earns its keep.
The companion stance is progressive enhancement, not graceful degradation: the default code path must be the one that runs on the lowest tier, so an unknown/unsupported capability (Android MPC 0, Network Info absent on Safari/Firefox, pre-API-30) resolves to the conservative floor, and richer tiers are opt-in grants the snapshot adds. 80/20: reaktor owns the tier enums + the floor-default contract; the platform computes the raw signals. Honest status: today this law is unenforced — a raw getShibasisUserAgent() exists on web and there is no normalization layer, because the capability module that would own it isn't built.
10. The reaktor-core module
A tiny foundation, kept tiny on purpose.
Adds: the Feature slot registry; the Capability/AtomicCapability resource markers; the Adapter<Controller> WeakRef bridge + base adapter subclasses; expect/actual primitives (WeakRef, AtomicInt, ConcurrentHashMap, Dispatch); and the standard-lib wrappers (Kermit logging, kotlinx-datetime time, kotlinx.serialization defaults). Platform code lives in src/{android,ios,jvm,js}Main — not as separate Gradle modules.
Consumes: only the kotlinx ecosystem (atomicfu, datetime, serialization, coroutines) + Kermit. Nothing reaktor-internal — it is the bottom of the stack.
Must not: become the DI container (Koin owns DI, bridged via Feature.Dependency); grow the capability runtime inside itself (it installs via a slot, as its own module); leak platform types or raw fingerprints into common APIs; or become a god-object of static helpers. Core is a registry, a marker, an adapter, and a few primitives — and stays that.
11. Phased plan
Core is largely done; the work is hardening it and building the capability runtime on top.
| Phase | Deliverable | Notes |
|---|---|---|
| C0 — Harden the registry | Fail-fast slot validation (missing slot errors at boot, not mid-call); document the "resolve at the boundary, constructor-inject" discipline | service-locator critique (§4) |
| C1 — Bootstrap & iOS | Per-platform auto-bootstrap so adapters register without manual connect; finish the iOS BaseController/Darwin adapters | closes the manual-registration gap (§6) |
| C2 — Primitive cleanup | Make JSON config immutable/per-scope (not a global mutable var); audit expect/actual (prefer interface+injected actual over expect class, still Beta) | §7 |
| C3 — Capability vocabulary | Land the normalized enums + EnvironmentSnapshot + golden fixtures + the privacy rule as the first slice of reaktor-capability | per the capability doc P0 |
| C4 — Capability runtime v0 | :reaktor-capability module, CapabilityRuntime, Feature.Capability, collectors, deterministic StrategySelector | per the capability doc P1–P2 |
C0–C2 harden what exists; C3–C4 are the capability runtime, sequenced and detailed in reaktor-capability — this doc does not duplicate that plan.
12. Open decisions
- The "capability" naming collision. Three in-code meanings + the ocap word (§3). Options: rename core's
Capability→Closeable/Resource; reserve "Capability" for device tiers; or keep names but enforce a documented disambiguation convention. A rename touches the graph mixins, so it is the user's call — flag, don't unilaterally rename. - Service-locator discipline for
Feature. Make resolution boundary-only + fail-fast a lint/convention, or leave it as guidance? (§4). expect classvs interface + injectedactual. Standardize on the latter (Beta-safe, matches the adapter pattern) across core?- Where capability normalization lives. Per the capability doc: keep v0 graph-free and core-light (install via slot), confirmed.
- The unknown → floor policy. Make "unsupported signal resolves to the conservative tier" a hard, tested contract on every capability axis (§9).
13. References
Verified June 2026. Core API claims are grounded in the live reaktor-core code; external claims against primary platform/standards docs.
reaktor-core (code)
Featureregistry —reaktor-core/.../framework/Feature.kt; slot delegateCreateSlot<T>.Capability/AtomicCapability—.../capabilities/Capability.kt.Adapter<Controller>+WeakRef—.../framework/Adapter.kt,.../framework/Expectations.kt.ConcurrentHashMap—.../structs/. Android bootstrap —src/androidMain/.../BaseActivity.kt.
Privacy-preserving capability detection
- User-Agent Client Hints — Chrome for Developers · UA reduction — Privacy Sandbox (opt-in hints; frozen UA).
- Network Information API — MDN ("Limited availability — not Baseline"; no Firefox/Safari → treat as optional, feature-detected);
effectiveTypebuckets +Save-Data. - Mitigating Browser Fingerprinting in Web Specifications — W3C TAG/PING (clamp / enumerate / boolean-reduce; the entropy-reduction toolkit).
Platform capability tiers
- Android Media Performance Class — developer.android.com (
MEDIA_PERFORMANCE_CLASS, Jetpackcore-performance1.0.0,PlayServicesDevicePerformance, OS-decoupled forward-compat tier). Thermal — Thermal API (getThermalHeadroom, status listener). - iOS
ProcessInfo— thermalState (nominal/fair/serious/critical) · isLowPowerModeEnabled · Respond to Thermal State Changes.
Patterns
- Adaptive loading — web.dev (Osmani & Schloss, CDS 2019) · Graceful degradation vs progressive enhancement — MDN.
- Object-capability model (what reaktor does not mean) — overview; Miller/Yee/Shapiro, "Capability Myths Demolished".
- KMP —
expect/actual(K2 stable since Kotlin 2.0;expect classstill Beta) ·AutoCloseable— "close should be idempotent" · kotlinx-atomicfu · Decompose/Essenty Lifecycle (platform-agnostic interface + injectedactual) · Koin — why constructor injection (the service-locator caution).
See also
- reaktor-capability — the runtime that sits on core: environment snapshots, normalized tiers, strategy selection, degradation, and the reaktorDesktop capability plane. The other half of this merged entry.
- Graph Kernel Analysis · Reaktor Graph Visitors — where the runtime-mixin
*Capabilitytypes (§3) live. - Reaktor Actor Model · Reaktor Distributed Actors — actors are
Capability-closing,Feature-resolving nodes. - Reaktor FlexBuffer performance guide — the binary wire format and runtime tiers that complement core's JSON defaults.
- Performance and Observability — consumes measured signals that feed capability decisions.
Related docs
- Capability Runtime - Use before branching behavior by platform, device, network, permission, or renderer state.
- Service Layer - Use before adding service APIs, splitting reaktor-service out of reaktor-graph, wiring handlers to Spring/Workers, or making modules graph-projectable without hard graph dependencies.
- FFI - Use before changing the native bridge, FlexBuffer-RPC protocol, Hermes runtime path, ABI surface, graph representation, or target/binding support.
- FlexBuffer - Use before changing binary serialization or reaktor-flexbuffer hot paths.
- Server - Use before adding any production backend primitive (resilience, messaging, data patterns) or designing the Spring/Workers server runtime.