@gamitolab/bridgekit
v0.3.0-alpha.4
Published
Typed bidirectional bridge between React Native and native (Android/iOS)
Maintainers
Readme
@gamitolab/bridgekit
Typed, bidirectional communication between React Native and native code via a Marker API. One TypeScript contract definition drives JS types, runtime metadata, and generated Kotlin/Swift.
Built on Nitro Modules for the native transport.
Platform support
| Platform | Status | Transport | | -------- | ------ | --------- | | Android | ✅ Supported | Nitro (Kotlin runtime) | | iOS | ✅ Supported | Nitro (Swift runtime) | | Web | ✅ Supported | In-memory loopback (no native code) |
Alpha. The wire protocol is locked and the Android/iOS runtimes are at parity, but the public API may still change before
1.0.
Installation
# while in alpha, install the @alpha tag
pnpm add @gamitolab/bridgekit@alpha react-native-nitro-modules
# codegen CLI (dev dependency)
pnpm add -D @gamitolab/bridgekit-cli@alphaBridgeKit is a Nitro module. After installing:
- iOS —
cd ios && pod install - Android — autolinking handles it; no manual steps.
Peer dependencies: react, react-native (>= 0.86), react-native-nitro-modules (^0.37).
The host app pins RN and Nitro; this package does not. Dev builds in this repo
use React Native 0.86.3 and react-native-nitro-modules 0.37.1.
Brownfield
The public module is pure Swift (import BridgeKit). Nitro/C++ lives in
BridgeKitNitro and is what autolinking installs. A packaged RN host never
imports Nitro and never puts provide() inside the RN framework.
BRIDGEKIT_HOST_PROVIDES_RUNTIME=1 is a build mode: the host already
links public BridgeKit (Swift package / xcframework), so CocoaPods must not
inject a second copy into the RN target. In that mode BridgeKitNitro also
skips BKTransportWeakStubs.m (stubs in the same image as Nitro swallow
provide()) and the RN target must link with -Wl,-undefined,dynamic_lookup
so BKTransport* stay unbound until the host image loads.
pnpm add @gamitolab/bridgekit@alpha react-native-nitro-modules- Generate host contracts:
bridgekit generate --platform swift --out-dir ios/HostApp/Generated - Package RN:
pnpm package:ios(setBRIDGEKIT_HOST_PROVIDES_RUNTIME=1in the RN Podfile) - In the host Xcode project, add the remote package. No local path. URL
https://github.com/gamitolab/bridgekit.git, exact tag0.3.0-alpha.4, productBridgeKit, packagebridgekit:
.package(url: "https://github.com/gamitolab/bridgekit.git", exact: "0.3.0-alpha.4")
.product(name: "BridgeKit", package: "bridgekit")Then:
import BridgeKit
func application(...) -> Bool {
BridgekitDemoInitializer.configure() // your provide()s, generated contracts
ReactNativeBrownfield.shared.startReactNative(...)
return true
}No Podfile in the host. No NSClassFromString. Integrated brownfield (pods in
the same app target) autolinks Nitro and injects public BridgeKit in-process.
Entry points
| Import | Contents |
| ------ | -------- |
| @gamitolab/bridgekit | Runtime + React hooks + contract layer (everything) |
| @gamitolab/bridgekit/contract | Contract layer only — zero side effects, safe in Node/Jest/web |
| @gamitolab/bridgekit/react | React hooks (useBridge, useBridgeState, …) |
| @gamitolab/bridgekit/test | Testing helpers (createTestBridge, mockBridge) |
Quick Start — Marker API
1. Define a contract (TypeScript, single source of truth)
// connect-host.contract.ts — provided by NATIVE, consumed by JS
import { defineContract, Async, Void, Stream, State } from '@gamitolab/bridgekit/contract';
export const ConnectHost = defineContract('connect.host', {
methods: {
isLoggedIn: Async<boolean>(),
installEsim: Async<{ url: string; iccId: string }, 'success' | 'cancelled' | 'error'>(),
showLogin: Void(),
},
streams: {
otpCodes: Stream<string>(),
},
state: {
connectivity: State<{ online: boolean }>({ online: false }),
},
});Marker reference:
Async<Result>()/Async<Params, Result>()— async request/responseSync<Result>()/Sync<Params, Result>()— sync read (native-provided only)Void()/Void<Params>()— fire-and-forgetStream<Value>()/Stream<Value, Params>()— typed observable flowState<Value>(initial)— observable bidirectional state
2. Generate native bindings
The codegen lives in @gamitolab/bridgekit-cli. Emit Kotlin and/or Swift from the
same contract:
# Android (Kotlin) — default platform
bridgekit generate \
--contracts 'src/**/*.contract.ts' \
--out-dir android/src/main/java/com/myapp/bridgekit/generated
# iOS (Swift)
bridgekit generate --platform swift \
--contracts 'src/**/*.contract.ts' \
--out-dir ios/MyApp/bridgekit/generatedThis produces typed interfaces, data classes, codecs, and a contract object per platform.
3. Provide from native (Kotlin)
class ConnectHostProvider : ConnectHost {
override suspend fun isLoggedIn(): Boolean = session.isLoggedIn()
override suspend fun installEsim(params: InstallEsimParams): InstallEsimResult = /* ... */
override fun showLogin() { /* ... */ }
override fun otpCodes(): Flow<String> = smsRetriever.codes()
override val connectivity = MutableStateFlow(Connectivity(online = true))
}
BridgeKit.default.provide(ConnectHostContract) { ConnectHostProvider() }4. Consume from React Native
import { useBridge, useBridgeState } from '@gamitolab/bridgekit/react';
import { ConnectHost } from './connect-host.contract';
function LoginButton() {
const connect = useBridge(ConnectHost);
const { value, status } = useBridgeState(ConnectHost, 'connectivity');
return (
<Button
title={value?.online ? 'Connected' : 'Offline'}
onPress={() => connect.showLogin()}
/>
);
}5. Provide from React Native (JS → native direction)
import { useProvideBridge } from '@gamitolab/bridgekit/react';
import { LiaFeature } from './lia-feature.contract';
function LiaProvider({ children }) {
useProvideBridge(LiaFeature, {
getUnreadCount: () => store.unread,
});
return children;
}val lia = bridgeKit.consume(LiaFeatureContract)
val count = lia.getUnreadCount()ContractHook (React integration)
ContractHook is the React-first way to consume a contract. It returns a derived consumer
that re-renders automatically when any subscribed state changes.
import { defineContract, Async, State } from '@gamitolab/bridgekit/contract';
const UserContract = defineContract('user.contract', {
methods: {
getProfile: Async<{ userId: string }, { name: string; email: string }>(),
},
state: {
loginStatus: State<'idle' | 'active' | 'error'>('idle'),
},
});
// In a React component:
const user = UserContract.hook();
// user.loginStatus.value — reactive, re-renders on change
// user.loginStatus.status — 'provided' | 'unprovided' | 'stale'
// user.getProfile({ userId }) — returns Promise<{ name, email }>Scoping
Use BridgeScopeProvider to isolate contracts to a feature or instance:
import { BridgeScopeProvider } from '@gamitolab/bridgekit/react';
// Feature scope — multiple instances of the same feature do not cross-talk
<BridgeScopeProvider feature="checkout" instance={cartId}>
<CheckoutFlow />
</BridgeScopeProvider>Auto-discovery (native feature modules)
class ConnectBridgeModule : BridgeKitModule {
override fun register(bridgekit: BridgeKit, host: BridgeKitHost) {
bridgekit.provide(ConnectHostContract) { ConnectHostProvider(host.locate()) }
}
}
// Register via META-INF/services/com.bridgekit.discovery.BridgeKitModuleTesting
import { createTestBridge } from '@gamitolab/bridgekit/test';
const { bridgekit } = createTestBridge();
await bridgekit.provide(ConnectHost, { isLoggedIn: async () => true });
const proxy = bridgekit.bridge(ConnectHost);
expect(await proxy.isLoggedIn()).toBe(true);val fake = object : ConnectHost { /* ... */ }
val bridgekit = BridgeKit(testTransport)Diagnostics
BridgeKit.default.dump() returns live state:
const state = BridgeKit.default.dump();
// state.bindings — registered providers per contract+scope
// state.mirrors — observable state mirrors per contract+scope
// state.openStreams — current count of open stream subscriptions
// state.streamDrops — cumulative items dropped by bounded consumer queues
// state.counters — { calls, errors, firesDropped }
// state.epoch — current connection generationOn Android: BridgeKit.default.dump() logs to Logcat under the BridgeKit tag.
License
MIT © malopezr7
