Skip to content

Refactor core native API: single Nitro HybridObject, remove legacy bridge module #11

Description

@V3RON

Summary

The @react-native-runtimes/core package currently exposes two parallel native APIs to JavaScript:

  1. Nitro HybridObject (ThreadedRuntimeFunctions) — partial migration for cross-runtime function calls (install, run)
  2. Legacy bridge Native Module (NativeModules.ThreadedRuntime) — classic RCTBridgeModule (iOS) / ReactContextBaseJavaModule (Android) for all lifecycle operations and a fallback runtime-function path

This is confusing, duplicates code paths (especially for ThreadedRuntime.run()), and blocks making react-native-nitro-modules a hard requirement. We should remove the bridge module entirely and consolidate everything into one Nitro HybridObject named ThreadedRuntime.

Note: The public ThreadedRuntime module is not a TurboModule — there is no codegen spec. Internal TurboModule delegate code (ThreadedRuntimeTurboModuleDelegate, DefaultTurboModuleManagerDelegate) is separate infrastructure for secondary RN runtimes and should stay.


Current state

Nitro (partial — keep and extend)

Piece Role
src/specs/ThreadedRuntimeFunctions.nitro.ts Nitrogen spec
cpp/HybridThreadedRuntimeFunctions.{hpp,cpp} C++ implementation
nitrogen/generated/** Generated registration/spec glue
nitro.json"ThreadedRuntimeFunctions" Autolinking config

JS access: NitroModules.createHybridObject('ThreadedRuntimeFunctions')

Methods:

  • install(runtimeName?) — raw JSI; installs __rnrRegisterRuntimeFunction / __rnrCallRuntimeFunction and registers a Nitro Dispatcher
  • run(runtimeName, functionId, argsJson) — cross-runtime calls via RuntimeFunctionScheduler (C++ JSI, no bridge round-trip)

Legacy bridge module (remove)

Platform Bridge entry Core implementation
iOS ThreadedRuntime implements RCTBridgeModule with RCT_EXPORT_METHOD(...) in ThreadedRuntime.mm Same file's + (void) static methods
Android ThreadedRuntimeBridgeModule.kt (@ReactModule(name = "ThreadedRuntime")) ThreadedRuntime.kt object

JS access: NativeModules.ThreadedRuntime in packages/core/src/ThreadedRuntime.tsx

Methods:

  • prewarmRuntime / preloadRuntime / prewarmRuntimeWithOptions
  • runHeadlessTask / dispatchHeadlessTask
  • callRuntimeFunction ← legacy fallback for ThreadedRuntime.run()
  • completeRuntimeFunctionCall ← callback leg of legacy path only
  • destroyRuntime / destroyAllRuntimes / getRuntimeNames
  • addListener / removeListeners (no-ops for NativeEventEmitter compat)

Out of scope (keep as-is for this refactor)

  • ThreadedRuntimeSurface — Fabric view via ViewGroupManager / requireNativeComponent
  • ThreadedRuntimeTurboModuleDelegate (iOS) / DefaultTurboModuleManagerDelegate (Android) — internal TurboModule resolution for secondary runtimes
  • @react-native-runtimes/state still has its own legacy SharedZustandStoreModule — separate follow-up

Dual runtime-function paths (why legacy code is bulky)

Nitro path (target):

JS ThreadedRuntime.run()
  → HybridThreadedRuntimeFunctions.run()
  → RuntimeFunctionScheduler (C++ dispatcher)
  → __rnrCallRuntimeFunction in target runtime (JSI)

Legacy bridge path (delete):

JS ThreadedRuntime.run()
  → NativeModules.callRuntimeFunction (Promise + callId)
  → native queue + flushRuntimeFunctionCalls
  → host.callFunctionOnJSModule("ThreadedRuntimeFunctionRunner", "run", ...)
  → JS runRegisteredRuntimeFunction()
  → NativeModules.completeRuntimeFunctionCall(callId, ...)

Legacy path adds substantial code on both platforms:

  • iOS: callRuntimeFunctionWithRuntimeName, completeRuntimeFunctionCallWithCallId, flushRuntimeFunctionCallsWithRuntimeName, promise maps
  • Android: pendingRuntimeFunctionCalls, pendingRuntimeFunctionPromises, invokeRuntimeFunctionCall, completeRuntimeFunctionCall
  • JS: runRegisteredRuntimeFunction, completeRuntimeFunctionCall, registerCallableModule(RUNTIME_FUNCTION_RUNNER_MODULE, ...)

Target state: one HybridObject ThreadedRuntime

Single Nitro entry point:

NitroModules.createHybridObject<ThreadedRuntime>('ThreadedRuntime')

Spec (src/specs/ThreadedRuntime.nitro.ts)

Merge existing functions spec with lifecycle methods. Drop bridge-only aliases.

import type { HybridObject } from 'react-native-nitro-modules';

export interface ThreadedRuntime
  extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
  // Cross-runtime functions (from ThreadedRuntimeFunctions)
  run(
    runtimeName: string,
    functionId: string,
    argsJson: string,
  ): Promise<string>;

  // Lifecycle (from bridge module)
  prewarmRuntime(runtimeName: string): Promise<void>;
  prewarmRuntimeWithOptions(
    runtimeName: string,
    kind: string,
    useMainNativeModules: boolean,
  ): Promise<void>;
  runHeadlessTask(
    runtimeName: string,
    taskName: string,
    payloadJson: string,
  ): Promise<void>;
  destroyRuntime(runtimeName: string): Promise<void>;
  destroyAllRuntimes(): Promise<void>;
  getRuntimeNames(): Promise<string[]>;
}

install(runtimeName?) stays out of the .nitro.ts file — registered as a raw JSI hybrid method in C++ via registerRawHybridMethod (same pattern as today). TypeScript adds it on the cached hybrid type:

type ThreadedRuntimeHybrid = ThreadedRuntime & {
  install: (runtimeName?: string) => void;
};

nitro.json

"autolinking": {
  "ThreadedRuntime": {
    "all": {
      "language": "c++",
      "implementationClassName": "HybridThreadedRuntime"
    }
  }
}

Remove the ThreadedRuntimeFunctions entry. Run npm run specs in packages/core to regenerate nitrogen output.

Native implementation: one C++ class on both platforms

Use C++ everywhere for the hybrid object. Lifecycle ops delegate to existing platform code through dispatcher shims.

Method Implementation
install RuntimeFunctionJsi + Nitro Dispatcher registration (existing)
run RuntimeFunctionScheduler::callRuntimeFunctionOnRuntime (existing)
prewarmRuntime* ThreadedRuntimeDispatcher[ThreadedRuntime prewarm…] / ThreadedRuntime.prewarmRuntimeWithOptions
runHeadlessTask ThreadedRuntimeDispatcher::dispatchHeadlessTask (existing)
destroyRuntime / destroyAllRuntimes / getRuntimeNames Extend ThreadedRuntimeDispatcher.h with JNI (Android) + .mm (iOS) wrappers

Rename/merge: HybridThreadedRuntimeFunctionsHybridThreadedRuntime (one .hpp/.cpp).

Android Context: extend ThreadedRuntimeDispatcher.h JNI helpers to accept/cache Application context (set once from NativeComposeThreadedRuntimeOnLoad / OnLoad.cpp, replacing init currently in ThreadedRuntimeBridgeModule.init).

iOS: C++ calls ObjC static methods via ThreadedRuntimeDispatcher.mm (no bridge context needed).


JS changes (packages/core/src/ThreadedRuntime.tsx)

  • Add getThreadedRuntimeNitro() — lazy cache, fail/warn loudly on native platforms if Nitro unavailable
  • Replace all nativeRuntime?.… calls with Nitro hybrid methods
  • Remove NativeModules import and ThreadedRuntimeNativeModule type
  • Remove legacy runtime-function fallback in ThreadedRuntime.run()
  • Remove completeRuntimeFunctionCall, runRegisteredRuntimeFunction, and RUNTIME_FUNCTION_RUNNER_MODULE callable module registration

Keep:

  • registerCallableModule(HEADLESS_TASK_RUNNER_MODULE, …) — headless dispatch still uses callFunctionOnJSModule, not the bridge

Make react-native-nitro-modules a hard peer dependency (already listed; today JS treats it as optional).


Delete

Layer Remove
Android ThreadedRuntimeBridgeModule.kt
Android Legacy fn path in ThreadedRuntime.kt: callRuntimeFunction, completeRuntimeFunctionCall, pending queues, flushRuntimeFunctionCalls, invokeRuntimeFunctionCall
iOS RCTBridgeModule conformance, RCT_EXPORT_MODULE, all RCT_EXPORT_METHOD
iOS Legacy fn path: callRuntimeFunctionWithRuntimeName, completeRuntimeFunctionCallWithCallId, flushRuntimeFunctionCallsWithRuntimeName, promise maps
Spec src/specs/ThreadedRuntimeFunctions.nitro.ts
C++ HybridThreadedRuntimeFunctions.{hpp,cpp} (merged into HybridThreadedRuntime)
Package ThreadedRuntimePackage.createNativeModules() — package registers ViewManager only

Keep unchanged

  • ThreadedRuntime.kt / ThreadedRuntime.mm host orchestration (minus bridge exports and legacy fn queue)
  • ThreadedRuntimeSurfaceManager / surface views
  • RuntimeFunctionJsi.*, RuntimeFunctionScheduler.*
  • TurboModule delegates inside secondary runtimes

Suggested file map after refactor

packages/core/
├── src/specs/
│   └── ThreadedRuntime.nitro.ts              # single spec (replaces ThreadedRuntimeFunctions)
├── cpp/
│   ├── HybridThreadedRuntime.{hpp,cpp}       # merged hybrid
│   └── nativecompose/threadedruntime/          # JSI + scheduler + dispatcher (extended)
├── android/.../
│   ├── ThreadedRuntime.kt                    # core engine (trim legacy fn path)
│   ├── ThreadedRuntimePackage.kt             # ViewManager only
│   └── ThreadedRuntimeSurface*.kt
├── ios/
│   ├── ThreadedRuntime.{h,mm}                # engine only, no RCTBridgeModule
│   ├── ThreadedRuntimeDispatcher.mm          # extended C++ shim
│   └── ThreadedRuntimeSurface*.mm
└── nitrogen/generated/                       # regenerated

Execution order

  1. Extend dispatcher — add destroyRuntime, destroyAllRuntimes, getRuntimeNames (+ Android context init) to ThreadedRuntimeDispatcher
  2. New spec + codegenThreadedRuntime.nitro.ts, update nitro.json, merge C++ hybrid class, delete old functions spec/class
  3. Wire JS — single getThreadedRuntimeNitro(), switch all call sites, remove legacy fn runner
  4. Delete bridge — bridge module, RCT exports, legacy fn queue machinery
  5. Trim packageThreadedRuntimePackage view-only; update README; verify example app

Test plan

After each phase, verify on iOS and Android:

  • ThreadedRuntime.prewarm() / prewarmBusinessRuntime()
  • Mount ThreadedRuntimeSurface / threaded component
  • ThreadedRuntime.runHeadlessTask()
  • runtimeFunction(...).runOn(otherRuntime, ...) cross-runtime call (requires install() in both runtimes)
  • ThreadedRuntime.destroy() / destroyAll() / getRuntimeNames()
  • Example app (example/) still runs with react-native-nitro-modules linked

Risks / breaking changes

  1. Breaking: Removing NativeModules.ThreadedRuntime breaks any direct consumer outside this package. Within the monorepo, only ThreadedRuntime.tsx uses it today.
  2. Android Context — confirm Nitro init timing vs first prewarm call; may need Application context holder set at startup.
  3. Surface view — remains old-style ViewManager; Nitro Views are a separate future effort.
  4. State package@react-native-runtimes/state still has dual Nitro + bridge API; not blocking this work but should be tracked separately.

References

  • Nitro spec (current): packages/core/src/specs/ThreadedRuntimeFunctions.nitro.ts
  • JS dual-path usage: packages/core/src/ThreadedRuntime.tsx (getRuntimeFunctionsNitro() + nativeRuntime)
  • Bridge module (Android): packages/core/android/.../ThreadedRuntimeBridgeModule.kt
  • Bridge module (iOS): packages/core/ios/ThreadedRuntime.mm (RCT_EXPORT_METHOD)
  • Prior art (Nitro-first, but still has bridge): packages/state/src/index.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions