@rn-org/react-native-thread 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @rn-org/react-native-thread
2
2
 
3
- Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses **JavaScriptCore** on iOS and **Mozilla Rhino** on Android, each on a dedicated OS-level thread. Built as a New Architecture **TurboModule**.
3
+ Run JavaScript on real background threads in React Native — no Workers, no Worklets. Uses **Hermes** on both iOS and Android, each on a dedicated OS-level thread. Built as a New Architecture **TurboModule**.
4
4
 
5
5
  ---
6
6
 
@@ -41,17 +41,18 @@ Run JavaScript on real background threads in React Native — no Workers, no Wor
41
41
 
42
42
  ## Features
43
43
 
44
- - **True background threads** — each thread runs its own JS engine on an OS-level thread; the main Hermes/JSC runtime is never blocked.
44
+ - **True background threads** — each thread runs its own isolated **Hermes** runtime on a dedicated OS-level thread; the main React Native runtime is never blocked. Works on both **iOS** and **Android**.
45
45
  - **Unlimited threads** — create as many threads as you need; each is isolated.
46
46
  - **Shared thread** (`runOnJS`) — fire-and-forget tasks on a single persistent background thread; no teardown required.
47
47
  - **Per-thread message passing** — call `resolveThreadMessage(data)` from any thread, receive it on the main thread with `onMessage` (callback or `await`).
48
48
  - **Parameter injection** — pass values from the main thread into the background function as a typed argument `(args) => { ... }`. Supports primitives, objects, arrays, **and functions**.
49
- - **Function params** — pass functions (including imported ones) alongside plain data in the params object. The Babel plugin extracts their source at compile time, captures closed-over variables transitively, and downlevels to ES5 for Rhino compatibility.
49
+ - **Function params** — pass functions (including imported ones) alongside plain data in the params object. The Babel plugin extracts their source at compile time and captures closed-over variables transitively.
50
50
  - **Cross-module function params** — functions imported from other files (e.g. `import { compute } from './math'`) are automatically resolved and inlined by the Babel plugin.
51
51
  - **Named threads** — give threads friendly names; list or destroy them by name.
52
52
  - **Error handling** — thread exceptions are automatically caught and forwarded to the main thread. Use `onError` or `.catch()` on the `onMessage()` promise.
53
53
  - **Full `console` support** — `console.log/info/warn/error/debug` work inside threads and appear in Logcat / Xcode logs.
54
- - **Hermes-safe** — ships a Babel plugin that extracts function source at compile time so Hermes bytecode never breaks serialisation.
54
+ - **Timer support** — `setTimeout`, `setInterval`, `clearTimeout`, and `clearInterval` work inside threads.
55
+ - **Hermes-safe** — ships a Babel plugin that extracts function source at compile time so Hermes bytecode never breaks serialisation. Required on both iOS and Android since both platforms use Hermes.
55
56
  - **New Architecture only** — built on the TurboModule / Codegen pipeline.
56
57
 
57
58
  ---
@@ -81,7 +82,9 @@ yarn add @rn-org/react-native-thread
81
82
  cd ios && pod install
82
83
  ```
83
84
 
84
- **Android** the Rhino dependency is declared in the library's `build.gradle`; no extra steps needed.
85
+ The `hermes-engine` pod is declared as a dependency in the library's podspec; no extra configuration needed.
86
+
87
+ **Android** — the Hermes native dependency is declared in the library's `build.gradle`; no extra steps needed.
85
88
 
86
89
  ---
87
90
 
@@ -93,8 +96,7 @@ Hermes compiles your JS to bytecode at build time. That means `fn.toString()` at
93
96
  2. Detects **function-valued properties** in the params object (second argument) and inlines their source.
94
97
  3. **Captures closed-over variables** transitively — if a function references outer `const`/`let`/`var` bindings (literals or other functions), those are bundled into a self-contained IIFE.
95
98
  4. **Resolves cross-module imports** — functions imported from relative paths (e.g. `import { fn } from './utils'`) are read from disk and inlined.
96
- 5. **Downlevels ES6+ to ES5** arrow functions, `let`/`const`, template literals, etc. are transpiled for Rhino (Android's JS engine).
97
- 6. **Strips TypeScript** annotations automatically when the source file is `.ts` or `.tsx`.
99
+ 5. **Strips TypeScript** annotations automatically when the source file is `.ts` or `.tsx`.
98
100
 
99
101
  Add the plugin to your app's `babel.config.js`:
100
102
 
@@ -109,7 +111,9 @@ module.exports = {
109
111
  };
110
112
  ```
111
113
 
112
- The plugin is a no-op on non-Hermes builds (JSC, V8, etc.).
114
+ The plugin is a no-op on non-Hermes builds (V8, etc.).
115
+
116
+ > **Note:** The plugin safely handles cases where `@react-native/babel-preset` transforms `async` functions before the plugin's visitor fires. It falls back to the original source text via Babel's preserved AST positions.
113
117
 
114
118
  ---
115
119
 
@@ -149,6 +153,24 @@ thread.run(
149
153
  { x: 6, y: 7, multiply }
150
154
  );
151
155
 
156
+ // Async function params work too — the Babel plugin transforms async/await
157
+ // to generator-based code before sending to the Hermes thread runtime:
158
+ const isPrime = async (num) => {
159
+ if (num <= 1) return 'Not Prime';
160
+ for (let i = 2; i < num; i++) {
161
+ if (num % i === 0) return 'Not Prime';
162
+ }
163
+ return 'Prime';
164
+ };
165
+
166
+ thread.run(
167
+ async (args) => {
168
+ const result = await args.isPrime(args.num);
169
+ resolveThreadMessage(`${args.num} is ${result}`);
170
+ },
171
+ { num: 17, isPrime }
172
+ );
173
+
152
174
  // Functions that close over outer variables work too:
153
175
  const factor = 10;
154
176
  const scale = (n) => n * factor;
@@ -404,11 +426,29 @@ console.log(data); // { status: 'done', value: 42 }
404
426
 
405
427
  `console.log`, `.info`, `.warn`, `.error`, and `.debug` are all available and route to:
406
428
 
407
- - **iOS** — `NSLog`, visible in Xcode / Console.app; tagged `[RNThread-<id>]`.
429
+ - **iOS** — `NSLog`, visible in Xcode / Console.app; tagged `[RNThread-<id>] [Hermes]`.
408
430
  - **Android** — `android.util.Log`, visible in Logcat; tagged `RNThread-<id>`.
409
431
 
410
432
  ---
411
433
 
434
+ ### Timers
435
+
436
+ `setTimeout`, `clearTimeout`, `setInterval`, and `clearInterval` are available inside threads.
437
+
438
+ Both iOS and Android use the same Hermes-based event loop: after the initial evaluation, the thread drains all pending timers (and microtasks) before returning. The thread blocks until all timers have fired or been cleared.
439
+
440
+ ```ts
441
+ thread.run(() => {
442
+ setTimeout(() => {
443
+ resolveThreadMessage('delayed hello');
444
+ }, 2000);
445
+ });
446
+ ```
447
+
448
+ > **Note:** The event loop blocks the thread until timers complete. Be careful with `setInterval` — the thread won't finish until the interval is cleared inside the thread.
449
+
450
+ ---
451
+
412
452
  ### `__params__`
413
453
 
414
454
  ```ts
@@ -469,16 +509,20 @@ The included Babel plugin runs at **compile time** — before Hermes touches the
469
509
 
470
510
  #### Task function (first argument)
471
511
 
472
- Arrow functions and function expressions are replaced with an ES5-downleveled string literal wrapped as an IIFE:
512
+ Arrow functions and function expressions (including `async`) are replaced with a string literal wrapped as an IIFE. `async`/`await` is transformed to generator-based code because Hermes' eval-mode compiler does not support async syntax:
473
513
 
474
514
  ```js
475
515
  // Input (your source)
476
- thread.run((args) => {
477
- resolveThreadMessage(`${args.num} is great`);
478
- }, { num: 42 });
479
-
480
- // Output (what Hermes compiles)
481
- thread.run("((function (args) {\n resolveThreadMessage(\"\".concat(args.num, \" is great\"));\n}))(__params__)", { num: 42 });
516
+ thread.run(async (args) => {
517
+ const result = await args.compute(args.num);
518
+ resolveThreadMessage(result);
519
+ }, { num: 42, compute });
520
+
521
+ // Output (what Hermes compiles) async transformed, compute inlined
522
+ thread.run("((function(){ ... _asyncToGenerator ... })())(__params__)", {
523
+ num: 42,
524
+ compute: { __rnThreadFn: "(function(){ ... })()" }
525
+ });
482
526
  ```
483
527
 
484
528
  #### Function params (second argument)
@@ -497,7 +541,7 @@ thread.run(fn, { multiply });
497
541
  // Output — transitive closures are captured in a self-contained IIFE
498
542
  thread.run(fn, {
499
543
  multiply: {
500
- __rnThreadFn: "(function(){ var b = 3;\nvar add = function(x) { return x + b; };\nreturn function multiply(a) { return add(a) * 2; };})()"
544
+ __rnThreadFn: "(function(){ var b = 3;\nvar add = (x) => x + b;\nreturn function multiply(a) { return add(a) * 2; };})()"
501
545
  }
502
546
  });
503
547
  ```
@@ -515,7 +559,7 @@ export function checkEvenOdd(num: number): string {
515
559
  // App.tsx
516
560
  import { checkEvenOdd } from './utils';
517
561
  thread.run(fn, { checkEvenOdd });
518
- // → checkEvenOdd is inlined with TypeScript stripped and ES5-downleveled
562
+ // → checkEvenOdd is inlined with TypeScript stripped
519
563
  ```
520
564
 
521
565
  ### Supported call sites
@@ -532,8 +576,10 @@ thread.run(fn, { checkEvenOdd });
532
576
  | Value type | Captured |
533
577
  |---|---|
534
578
  | Inline arrow / function expression | Yes |
579
+ | Inline `async` arrow / function expression | Yes (transformed to generator) |
535
580
  | Reference to local `function` declaration | Yes |
536
581
  | Reference to local `const fn = () => ...` | Yes |
582
+ | Reference to local `const fn = async () => ...` | Yes (transformed to generator) |
537
583
  | Imported function (`import { fn } from './mod'`) | Yes (relative paths only) |
538
584
  | Closed-over `const`/`let`/`var` with literal init | Yes (transitively) |
539
585
  | Closed-over function references | Yes (transitively) |
@@ -542,10 +588,10 @@ thread.run(fn, { checkEvenOdd });
542
588
  ### Limitations
543
589
 
544
590
  - **Captured closures must be statically resolvable**: only variables initialized with literals and functions (local or imported) are captured. Runtime-computed values (e.g. `const x = fetchValue()`) must be passed explicitly as params.
591
+ - **`async` functions in params are supported** — the plugin transforms them to generator-based code before extraction. `await` works inside both task functions and function params.
545
592
  - **Cross-module resolution** only follows **relative imports** (e.g. `'./utils'`). Package imports (e.g. `'lodash'`) are not resolved.
546
- - Thread function bodies must be **ASCII-safe**: Rhino (Android) does not support non-ASCII identifier characters in source mode.
547
593
  - `undefined`, `Map`, `Set`, and class instances are not serialisable as param values — use plain objects, arrays, strings, numbers, booleans, or `null`.
548
- - Thread functions run in an **isolated JS engine** with no access to the React tree, native modules, or the main thread's global scope.
594
+ - Thread functions run in an **isolated Hermes runtime** with no access to the React tree, native modules, or the main thread's global scope.
549
595
 
550
596
  ---
551
597
 
@@ -553,12 +599,13 @@ thread.run(fn, { checkEvenOdd });
553
599
 
554
600
  | Constraint | Reason |
555
601
  |---|---|
556
- | Thread functions run in isolation | They execute in a completely separate JS engine |
602
+ | Thread functions run in isolation | They execute in a completely separate Hermes runtime (iOS and Android) |
603
+ | Hermes eval-mode has no `async`/`await` | The Babel plugin transforms async to generators before extraction |
557
604
  | Function params must be statically resolvable | The Babel plugin extracts source at compile time |
558
605
  | Non-function `params` must be JSON-serialisable | Serialised via `JSON.stringify` at runtime |
559
606
  | `resolveThreadMessage` payload must be JSON-serialisable | Transported as a JSON string over the bridge |
560
607
  | Cross-module resolution is relative-only | The plugin reads files from disk using the import path |
561
- | Function body must be ASCII-safe | Rhino parser limitation |
608
+ | No `fetch` / `XMLHttpRequest` | Thread runtimes have no network stack |
562
609
  | New Architecture required | TurboModule / Codegen only; no bridge fallback |
563
610
 
564
611
  ---
@@ -16,5 +16,12 @@ Pod::Spec.new do |s|
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
17
  s.private_header_files = "ios/**/*.h"
18
18
 
19
+ s.pod_target_xcconfig = {
20
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
21
+ "HEADER_SEARCH_PATHS" => "$(PODS_ROOT)/hermes-engine/destroot/include $(PODS_CONFIGURATION_BUILD_DIR)/hermes-engine/destroot/include",
22
+ }
23
+
19
24
  install_modules_dependencies(s)
25
+
26
+ s.dependency "hermes-engine"
20
27
  end
@@ -40,10 +40,24 @@ android {
40
40
  defaultConfig {
41
41
  minSdkVersion getExtOrDefault("minSdkVersion")
42
42
  targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+
44
+ externalNativeBuild {
45
+ cmake {
46
+ cppFlags "-std=c++17"
47
+ arguments "-DANDROID_STL=c++_shared"
48
+ }
49
+ }
50
+ }
51
+
52
+ externalNativeBuild {
53
+ cmake {
54
+ path "src/main/cpp/CMakeLists.txt"
55
+ }
43
56
  }
44
57
 
45
58
  buildFeatures {
46
59
  buildConfig true
60
+ prefab true
47
61
  }
48
62
 
49
63
  buildTypes {
@@ -64,6 +78,5 @@ android {
64
78
 
65
79
  dependencies {
66
80
  implementation "com.facebook.react:react-android"
67
- // Rhino JS engine – provides a self-contained JS runtime for background threads
68
- implementation "org.mozilla:rhino:1.7.15"
81
+ implementation "com.facebook.react:hermes-android"
69
82
  }
@@ -0,0 +1,33 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(react_native_thread)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+
6
+ find_package(fbjni REQUIRED CONFIG)
7
+ find_package(ReactAndroid REQUIRED CONFIG)
8
+ find_package(hermes-engine REQUIRED CONFIG)
9
+
10
+ # RN 0.71+ renamed the hermes target from hermesvm to libhermes
11
+ if(TARGET hermes-engine::libhermes)
12
+ set(HERMES_TARGET hermes-engine::libhermes)
13
+ elseif(TARGET hermes-engine::hermesvm)
14
+ set(HERMES_TARGET hermes-engine::hermesvm)
15
+ else()
16
+ message(FATAL_ERROR "Neither hermes-engine::libhermes nor hermes-engine::hermesvm target found")
17
+ endif()
18
+
19
+ add_library(react_native_thread SHARED
20
+ HermesThreadEngine.cpp
21
+ )
22
+
23
+ target_include_directories(react_native_thread PRIVATE
24
+ ${CMAKE_CURRENT_SOURCE_DIR}
25
+ )
26
+
27
+ target_link_libraries(react_native_thread
28
+ fbjni::fbjni
29
+ ReactAndroid::jsi
30
+ ${HERMES_TARGET}
31
+ android
32
+ log
33
+ )
@@ -0,0 +1,388 @@
1
+ #include "HermesThreadEngine.h"
2
+
3
+ #include <android/log.h>
4
+ #include <string>
5
+ #include <thread>
6
+ #include <unordered_map>
7
+
8
+ #define LOG_TAG "RNThread"
9
+ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
10
+ #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
11
+ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
12
+
13
+ using namespace facebook;
14
+
15
+ namespace rnthread {
16
+
17
+ // ──────────────────────────────────────────────────────────────────
18
+ // HermesThreadRuntime
19
+ // ──────────────────────────────────────────────────────────────────
20
+
21
+ HermesThreadRuntime::HermesThreadRuntime(
22
+ JNIEnv *env,
23
+ jobject module,
24
+ long threadId)
25
+ : runtime_(facebook::hermes::makeHermesRuntime(
26
+ ::hermes::vm::RuntimeConfig::Builder()
27
+ .withES6Promise(true)
28
+ .withMicrotaskQueue(true)
29
+ .build())),
30
+ moduleRef_(nullptr), jvm_(nullptr) {
31
+ env->GetJavaVM(&jvm_);
32
+ moduleRef_ = env->NewWeakGlobalRef(module);
33
+
34
+ installConsole(threadId);
35
+ installTimers();
36
+ installResolveThreadMessage(env, module, threadId);
37
+ }
38
+
39
+ HermesThreadRuntime::~HermesThreadRuntime() {
40
+ runtime_.reset();
41
+ if (jvm_ && moduleRef_) {
42
+ JNIEnv *env = nullptr;
43
+ bool didAttach = false;
44
+ if (jvm_->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) ==
45
+ JNI_EDETACHED) {
46
+ jvm_->AttachCurrentThread(&env, nullptr);
47
+ didAttach = true;
48
+ }
49
+ if (env) {
50
+ env->DeleteWeakGlobalRef(moduleRef_);
51
+ }
52
+ if (didAttach) {
53
+ jvm_->DetachCurrentThread();
54
+ }
55
+ }
56
+ }
57
+
58
+ void HermesThreadRuntime::evaluate(
59
+ const std::string &code,
60
+ const std::string &sourceURL) {
61
+ auto buf = std::make_shared<jsi::StringBuffer>(code);
62
+ runtime_->evaluateJavaScript(buf, sourceURL);
63
+ // Run the event loop: drain microtasks and pending timers until
64
+ // both queues are empty.
65
+ drainTimerLoop();
66
+ }
67
+
68
+ // ──────────────────────────────────────────────────────────────────
69
+ // Timer event loop
70
+ // ──────────────────────────────────────────────────────────────────
71
+
72
+ void HermesThreadRuntime::drainTimerLoop() {
73
+ auto &rt = *runtime_;
74
+ // Keep a JS object that maps timerId → callback so we hold references.
75
+ auto callbacks =
76
+ rt.global()
77
+ .getPropertyAsObject(rt, "__rnTimerCallbacks__");
78
+
79
+ while (true) {
80
+ rt.drainMicrotasks();
81
+
82
+ if (timerQueue_.empty()) break;
83
+
84
+ auto now = std::chrono::steady_clock::now();
85
+ auto &next = timerQueue_.top();
86
+
87
+ if (next.fireAt > now) {
88
+ // Sleep until the next timer is due.
89
+ std::this_thread::sleep_until(next.fireAt);
90
+ }
91
+
92
+ // Pop and fire all timers that are now due.
93
+ bool fired = false;
94
+ while (!timerQueue_.empty()) {
95
+ now = std::chrono::steady_clock::now();
96
+ if (timerQueue_.top().fireAt > now) break;
97
+
98
+ auto entry = timerQueue_.top();
99
+ timerQueue_.pop();
100
+
101
+ // Skip cancelled timers.
102
+ if (cancelledTimers_.count(entry.id)) {
103
+ cancelledTimers_.erase(entry.id);
104
+ continue;
105
+ }
106
+
107
+ auto idStr = std::to_string(entry.id);
108
+ auto cb = callbacks.getProperty(rt, idStr.c_str());
109
+ if (cb.isObject() && cb.getObject(rt).isFunction(rt)) {
110
+ cb.getObject(rt).asFunction(rt).call(rt);
111
+ fired = true;
112
+ }
113
+
114
+ if (entry.repeating) {
115
+ // Re-enqueue for the next interval.
116
+ entry.fireAt = std::chrono::steady_clock::now() +
117
+ std::chrono::milliseconds(entry.intervalMs);
118
+ timerQueue_.push(entry);
119
+ } else {
120
+ // One-shot: remove the callback reference.
121
+ callbacks.setProperty(rt, idStr.c_str(), jsi::Value::undefined());
122
+ }
123
+ }
124
+
125
+ if (fired) {
126
+ // Drain microtasks generated by the callbacks.
127
+ rt.drainMicrotasks();
128
+ }
129
+
130
+ // If no more timers, we're done.
131
+ if (timerQueue_.empty()) break;
132
+ }
133
+ }
134
+
135
+ // ──────────────────────────────────────────────────────────────────
136
+ // setTimeout / setInterval / clearTimeout / clearInterval
137
+ // ──────────────────────────────────────────────────────────────────
138
+
139
+ void HermesThreadRuntime::installTimers() {
140
+ auto &rt = *runtime_;
141
+
142
+ // Hidden object to hold callback references so they aren't GC'd.
143
+ rt.global().setProperty(
144
+ rt, "__rnTimerCallbacks__", jsi::Object(rt));
145
+
146
+ // Pointer to `this` captured by the lambdas — safe because the
147
+ // lambdas only run while `evaluate()` is executing on the owning thread.
148
+ auto *self = this;
149
+
150
+ // ── setTimeout(fn, ms) → id ──
151
+ auto setTimeoutFn = jsi::Function::createFromHostFunction(
152
+ rt, jsi::PropNameID::forAscii(rt, "setTimeout"), 2,
153
+ [self](jsi::Runtime &rt, const jsi::Value &,
154
+ const jsi::Value *args, size_t count) -> jsi::Value {
155
+ if (count < 1 || !args[0].isObject() ||
156
+ !args[0].getObject(rt).isFunction(rt))
157
+ return jsi::Value::undefined();
158
+
159
+ int ms = count >= 2 ? static_cast<int>(args[1].asNumber()) : 0;
160
+ int id = self->nextTimerId_++;
161
+
162
+ // Store callback in the hidden map.
163
+ auto cbs = rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
164
+ cbs.setProperty(rt, std::to_string(id).c_str(),
165
+ args[0].getObject(rt));
166
+
167
+ TimerEntry entry;
168
+ entry.id = id;
169
+ entry.fireAt = std::chrono::steady_clock::now() +
170
+ std::chrono::milliseconds(ms);
171
+ entry.repeating = false;
172
+ entry.intervalMs = 0;
173
+ self->timerQueue_.push(entry);
174
+
175
+ return jsi::Value(id);
176
+ });
177
+ rt.global().setProperty(rt, "setTimeout", std::move(setTimeoutFn));
178
+
179
+ // ── setInterval(fn, ms) → id ──
180
+ auto setIntervalFn = jsi::Function::createFromHostFunction(
181
+ rt, jsi::PropNameID::forAscii(rt, "setInterval"), 2,
182
+ [self](jsi::Runtime &rt, const jsi::Value &,
183
+ const jsi::Value *args, size_t count) -> jsi::Value {
184
+ if (count < 1 || !args[0].isObject() ||
185
+ !args[0].getObject(rt).isFunction(rt))
186
+ return jsi::Value::undefined();
187
+
188
+ int ms = count >= 2 ? static_cast<int>(args[1].asNumber()) : 0;
189
+ if (ms <= 0) ms = 1; // Prevent tight infinite loop.
190
+ int id = self->nextTimerId_++;
191
+
192
+ auto cbs = rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
193
+ cbs.setProperty(rt, std::to_string(id).c_str(),
194
+ args[0].getObject(rt));
195
+
196
+ TimerEntry entry;
197
+ entry.id = id;
198
+ entry.fireAt = std::chrono::steady_clock::now() +
199
+ std::chrono::milliseconds(ms);
200
+ entry.repeating = true;
201
+ entry.intervalMs = ms;
202
+ self->timerQueue_.push(entry);
203
+
204
+ return jsi::Value(id);
205
+ });
206
+ rt.global().setProperty(rt, "setInterval", std::move(setIntervalFn));
207
+
208
+ // ── clearTimeout(id) / clearInterval(id) ──
209
+ auto clearTimerFn = jsi::Function::createFromHostFunction(
210
+ rt, jsi::PropNameID::forAscii(rt, "clearTimeout"), 1,
211
+ [self](jsi::Runtime &rt, const jsi::Value &,
212
+ const jsi::Value *args, size_t count) -> jsi::Value {
213
+ if (count >= 1 && args[0].isNumber()) {
214
+ int id = static_cast<int>(args[0].asNumber());
215
+ self->cancelledTimers_.insert(id);
216
+ // Also remove the callback reference.
217
+ auto cbs =
218
+ rt.global().getPropertyAsObject(rt, "__rnTimerCallbacks__");
219
+ cbs.setProperty(rt, std::to_string(id).c_str(),
220
+ jsi::Value::undefined());
221
+ }
222
+ return jsi::Value::undefined();
223
+ });
224
+ rt.global().setProperty(rt, "clearTimeout", clearTimerFn);
225
+ rt.global().setProperty(rt, "clearInterval", std::move(clearTimerFn));
226
+ }
227
+
228
+ // ──────────────────────────────────────────────────────────────────
229
+ // console.{log,info,warn,error,debug}
230
+ // ──────────────────────────────────────────────────────────────────
231
+
232
+ void HermesThreadRuntime::installConsole(long threadId) {
233
+ auto &rt = *runtime_;
234
+
235
+ auto makeLogFn = [&rt, threadId](int prio) {
236
+ return jsi::Function::createFromHostFunction(
237
+ rt,
238
+ jsi::PropNameID::forAscii(rt, "log"),
239
+ 1,
240
+ [threadId, prio](
241
+ jsi::Runtime &rt,
242
+ const jsi::Value &,
243
+ const jsi::Value *args,
244
+ size_t count) -> jsi::Value {
245
+ std::string msg;
246
+ for (size_t i = 0; i < count; ++i) {
247
+ if (i > 0) msg += ' ';
248
+ msg += args[i].toString(rt).utf8(rt);
249
+ }
250
+ std::string tag = "RNThread-" + std::to_string(threadId);
251
+ __android_log_print(prio, tag.c_str(), "%s", msg.c_str());
252
+ return jsi::Value::undefined();
253
+ });
254
+ };
255
+
256
+ auto console = jsi::Object(rt);
257
+ console.setProperty(rt, "log", makeLogFn(ANDROID_LOG_INFO));
258
+ console.setProperty(rt, "info", makeLogFn(ANDROID_LOG_INFO));
259
+ console.setProperty(rt, "warn", makeLogFn(ANDROID_LOG_WARN));
260
+ console.setProperty(rt, "error", makeLogFn(ANDROID_LOG_ERROR));
261
+ console.setProperty(rt, "debug", makeLogFn(ANDROID_LOG_DEBUG));
262
+ rt.global().setProperty(rt, "console", std::move(console));
263
+ }
264
+
265
+ // ──────────────────────────────────────────────────────────────────
266
+ // resolveThreadMessage(data)
267
+ // ──────────────────────────────────────────────────────────────────
268
+
269
+ void HermesThreadRuntime::installResolveThreadMessage(
270
+ JNIEnv *env,
271
+ jobject module,
272
+ long threadId) {
273
+ auto &rt = *runtime_;
274
+
275
+ // We need a shared pointer to the weak global ref and JVM pointer
276
+ // so the lambda can outlive this scope.
277
+ JavaVM *jvm = jvm_;
278
+ jobject weakModule = moduleRef_;
279
+
280
+ auto fn = jsi::Function::createFromHostFunction(
281
+ rt,
282
+ jsi::PropNameID::forAscii(rt, "resolveThreadMessage"),
283
+ 1,
284
+ [jvm, weakModule, threadId](
285
+ jsi::Runtime &rt,
286
+ const jsi::Value &,
287
+ const jsi::Value *args,
288
+ size_t count) -> jsi::Value {
289
+ // JSON.stringify(args[0])
290
+ auto json = rt.global().getPropertyAsObject(rt, "JSON");
291
+ auto stringify = json.getPropertyAsFunction(rt, "stringify");
292
+ std::string serialised;
293
+ if (count > 0) {
294
+ auto result = stringify.call(rt, args[0]);
295
+ if (result.isString()) {
296
+ serialised = result.getString(rt).utf8(rt);
297
+ } else {
298
+ serialised = "null";
299
+ }
300
+ } else {
301
+ serialised = "null";
302
+ }
303
+
304
+ // Call back to Java on whatever thread we're on.
305
+ JNIEnv *jniEnv = nullptr;
306
+ bool didAttach = false;
307
+ if (jvm->GetEnv(
308
+ reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6) ==
309
+ JNI_EDETACHED) {
310
+ jvm->AttachCurrentThread(&jniEnv, nullptr);
311
+ didAttach = true;
312
+ }
313
+ if (!jniEnv) return jsi::Value::undefined();
314
+
315
+ // Resolve the weak ref
316
+ jobject strongModule = jniEnv->NewLocalRef(weakModule);
317
+ if (!strongModule) {
318
+ if (didAttach) jvm->DetachCurrentThread();
319
+ return jsi::Value::undefined();
320
+ }
321
+
322
+ jclass cls = jniEnv->GetObjectClass(strongModule);
323
+ jmethodID mid = jniEnv->GetMethodID(
324
+ cls, "onThreadMessage", "(DLjava/lang/String;)V");
325
+ if (mid) {
326
+ jstring jData = jniEnv->NewStringUTF(serialised.c_str());
327
+ jniEnv->CallVoidMethod(
328
+ strongModule, mid, static_cast<jdouble>(threadId), jData);
329
+ jniEnv->DeleteLocalRef(jData);
330
+ }
331
+
332
+ jniEnv->DeleteLocalRef(cls);
333
+ jniEnv->DeleteLocalRef(strongModule);
334
+ if (didAttach) jvm->DetachCurrentThread();
335
+
336
+ return jsi::Value::undefined();
337
+ });
338
+
339
+ rt.global().setProperty(rt, "resolveThreadMessage", std::move(fn));
340
+ }
341
+
342
+ } // namespace rnthread
343
+
344
+ // ──────────────────────────────────────────────────────────────────
345
+ // JNI entry points
346
+ // ──────────────────────────────────────────────────────────────────
347
+
348
+ extern "C" {
349
+
350
+ JNIEXPORT jlong JNICALL
351
+ Java_com_rnorg_reactnativethread_HermesThreadEngine_nativeCreate(
352
+ JNIEnv *env,
353
+ jobject /* thiz */,
354
+ jobject module,
355
+ jlong threadId) {
356
+ auto *rt = new rnthread::HermesThreadRuntime(env, module, threadId);
357
+ return reinterpret_cast<jlong>(rt);
358
+ }
359
+
360
+ JNIEXPORT void JNICALL
361
+ Java_com_rnorg_reactnativethread_HermesThreadEngine_nativeEvaluate(
362
+ JNIEnv *env,
363
+ jobject /* thiz */,
364
+ jlong ptr,
365
+ jstring code,
366
+ jstring sourceURL) {
367
+ auto *rt = reinterpret_cast<rnthread::HermesThreadRuntime *>(ptr);
368
+ const char *cCode = env->GetStringUTFChars(code, nullptr);
369
+ const char *cUrl = env->GetStringUTFChars(sourceURL, nullptr);
370
+ try {
371
+ rt->evaluate(std::string(cCode), std::string(cUrl));
372
+ } catch (const std::exception &e) {
373
+ LOGE("JS exception: %s", e.what());
374
+ }
375
+ env->ReleaseStringUTFChars(code, cCode);
376
+ env->ReleaseStringUTFChars(sourceURL, cUrl);
377
+ }
378
+
379
+ JNIEXPORT void JNICALL
380
+ Java_com_rnorg_reactnativethread_HermesThreadEngine_nativeDestroy(
381
+ JNIEnv * /* env */,
382
+ jobject /* thiz */,
383
+ jlong ptr) {
384
+ auto *rt = reinterpret_cast<rnthread::HermesThreadRuntime *>(ptr);
385
+ delete rt;
386
+ }
387
+
388
+ } // extern "C"