Architecture & epochs
BridgeKit is a single choke point with a fixed transport at its center. Everything above the transport is generated or written by you; the transport itself is the one Nitrogen-built part. The same architecture is implemented on both Android (Kotlin) and iOS (Swift), validated end-to-end on both platforms.
The layers
Section titled “The layers”┌────────────────────────── JS (per runtime epoch) ──────────────────────────┐│ contracts (pure) │ JS Registry │ typed proxies │ hooks │ state mirrors ││ JS Dispatcher (one per epoch) │└──────────────▲──────────────────────────────────────────┬──────────────────┘ long-held callbacks (epoch-tagged) invoke / stream / state ops │ Nitro generic transport (fixed, internal) │┌──────────────┴──────────────────────────────────────────▼──────────────────┐│ BridgeHost · BridgeStreams · BridgeState (3 hybrid objects) ││ Native core: Registry · Router · Epoch manager · Park buffer · State store ││ provide/consume · scope handles · (Android) ServiceLoader · lifecycle ext │└─────────────────────────────────────────────────────────────────────────────┘The native core
Section titled “The native core”The Kotlin and Swift cores mirror each other in responsibility. Where the concurrency primitives differ, the table notes both.
| Piece | Responsibility |
|---|---|
BridgeKit | Public entry point. BridgeKit.default installs the Router as the JNI delegate. provide / consume / tryConsume / isProvided / initialize / dump. |
Router | The routing engine. Async/sync dispatch, epoch swap on connectDispatcher, stream pumps, state channel, scope resolution (instance → feature → global). Thread-safe via ConcurrentHashMap + AtomicLong epoch. |
StateStore | Thread-safe (ConcurrentHashMap) bidirectional state, keyed by (contractId, stateKey, scope), holding MutableStateFlow<BridgeValue<Any?>>. |
StreamHub | Multiplexes one provider Flow<T> across N consumers via MutableSharedFlow(extraBufferCapacity=64, DROP_OLDEST). |
OutboundCallerImpl | The native → JS call engine behind generated outbound() proxies. Main-thread ANR guard throws before parking. |
| Park buffer | Holds durable native-side interests (consume subscriptions, observations) across epochs and replays them on reconnect. |
| Piece | Responsibility |
|---|---|
BridgeKitRuntime | Public entry point. BridgeKitRuntime.default installs the Router as the BridgeKitNative.delegate. provide / consume / isProvided / awaitProvided / dump. |
Router | The routing engine. Async/sync dispatch, epoch swap on connectDispatcher, stream pumps, state channel, scope resolution (instance → feature → global). Thread-safe via a single NSRecursiveLock (NOT an actor — an actor would deadlock sync specs on the JS thread). |
StateStore | Lock-guarded bidirectional state, keyed by (contractId, stateKey, scopeKey), holding BridgeValue<Any?>. Provider state iterated in background Tasks; JS writes via writeFromJs. |
StreamHub | Multiplexes one provider AsyncThrowingStream across N consumers; first consumer starts an upstream Task, others join. Fan-out runs outside the lock. |
OutboundCallerImpl | The native → JS call engine. Polls awaitDispatcher on a 10ms tick; wraps each continuation in OnceContinuation to guard against double-resume crashes; withBridgeTimeout races the op against Task.sleep. invokeSync throws NOT_SUPPORTED. |
OnceContinuation | Guards every CheckedContinuation — Swift crashes on double-resume. Used in ParkBuffer, OutboundCallerImpl, and stream ends. |
| Park buffer | Same role as Android. Re-parked and replayed on epoch swap. |
The fixed transport — three hybrid objects
Section titled “The fixed transport — three hybrid objects”These are the only Nitrogen specs in the system, and they are internal. The same three hybrid objects exist on both platforms:
BridgeHost—invoke(env): Promise<AnyMap>,invokeSync(env): AnyMap,connectDispatcher(epochInfo, onInvoke, onStreamOpen, onStreamClose, onStateWrite): AnyMap.BridgeStreams—open(env, onNext, onEnd): streamId,close(streamId),emitFromJs(streamId, value),endFromJs(streamId, end).BridgeState—read(env): AnyMap,observe(env, onChange): obsId,unobserve(obsId),write(env): AnyMap.
All callbacks the native side holds are reference-counted by Nitro and auto-dispatched to the JS thread. Every one is epoch-tagged and released on epoch end.
Epochs in one paragraph
Section titled “Epochs in one paragraph”The JS runtime is recreated as part of normal production lifecycle, so connectDispatcher is
an epoch swap. Each connect allocates a monotonic epoch id and atomically supersedes the
previous dispatcher: prior-epoch stream pumps and Flow / AsyncStream collections are
cancelled, in-flight native → JS calls fail with BRIDGE_NOT_READY, callbacks are released,
and JS-provided contracts go Unprovided until the new bundle re-registers. Durable native
interests are re-parked and replayed. The connect returns a state snapshot so JS mirrors
hydrate immediately. Full detail: Scopes, bindings & epochs.
iOS packaging
Section titled “iOS packaging”The iOS runtime ships as a single Axion.xcframework binary. All Swift modules
(BridgeKit, AxionContracts, AxionCore, and others) are statically linked in. Consumers
import BridgeKit and AxionContracts via synthetic thin framework aliases whose
.swiftinterface files expose a pure-Swift public API without exposing Nitro/C++ headers.
This packaging step runs once (make all in platforms/ios/Makefile); the resulting
xcframework is what app targets link against. See Installation for
setup instructions.
The wire protocol + epoch model are shared
Section titled “The wire protocol + epoch model are shared”The wire protocol — { "v": value } wrapping, { "status": "gone" } observer signals,
correlationId per call, AnyMap-only payloads — is identical on both platforms. The epoch
model (monotonic counter, stale-op rejection, 250ms grace for replacing, park-buffer replay)
is also shared. Full detail: Wire protocol.
Why a single choke point matters
Section titled “Why a single choke point matters”Because every op funnels through one place, observability is nearly free: every op carries a
correlationId, dev mode emits one structured trace format to both the JS console and
logcat/Xcode console, and dump() can print bindings, park-buffer contents, open streams,
mirror values, epoch and readiness on demand. See Observability & debugging.