Skip to content

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.

┌────────────────────────── 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 Kotlin and Swift cores mirror each other in responsibility. Where the concurrency primitives differ, the table notes both.

PieceResponsibility
BridgeKitPublic entry point. BridgeKit.default installs the Router as the JNI delegate. provide / consume / tryConsume / isProvided / initialize / dump.
RouterThe routing engine. Async/sync dispatch, epoch swap on connectDispatcher, stream pumps, state channel, scope resolution (instance → feature → global). Thread-safe via ConcurrentHashMap + AtomicLong epoch.
StateStoreThread-safe (ConcurrentHashMap) bidirectional state, keyed by (contractId, stateKey, scope), holding MutableStateFlow<BridgeValue<Any?>>.
StreamHubMultiplexes one provider Flow<T> across N consumers via MutableSharedFlow(extraBufferCapacity=64, DROP_OLDEST).
OutboundCallerImplThe native → JS call engine behind generated outbound() proxies. Main-thread ANR guard throws before parking.
Park bufferHolds durable native-side interests (consume subscriptions, observations) across epochs and replays them on reconnect.

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:

  • BridgeHostinvoke(env): Promise<AnyMap>, invokeSync(env): AnyMap, connectDispatcher(epochInfo, onInvoke, onStreamOpen, onStreamClose, onStateWrite): AnyMap.
  • BridgeStreamsopen(env, onNext, onEnd): streamId, close(streamId), emitFromJs(streamId, value), endFromJs(streamId, end).
  • BridgeStateread(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.

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.

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.

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.