@onekeyfe/react-native-background-thread 3.0.16 → 3.0.18

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.
@@ -1,10 +1,17 @@
1
1
  #include <jni.h>
2
2
  #include <jsi/jsi.h>
3
3
  #include <android/log.h>
4
+ #include <atomic>
5
+ #include <chrono>
6
+ #include <condition_variable>
7
+ #include <deque>
8
+ #include <functional>
4
9
  #include <memory>
5
10
  #include <mutex>
6
11
  #include <string>
12
+ #include <thread>
7
13
  #include <unordered_map>
14
+ #include <unordered_set>
8
15
 
9
16
  #include "SharedStore.h"
10
17
  #include "SharedRPC.h"
@@ -86,6 +93,355 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeExecuteWork(
86
93
  } catch (const std::exception &e) {
87
94
  LOGE("Error in nativeExecuteWork: %s", e.what());
88
95
  }
96
+
97
+ // CRITICAL: Drain the Hermes microtask queue. React Native 0.74+ configures
98
+ // Hermes with an explicit microtask queue, which must be manually drained
99
+ // after each JS execution. Without this, Promise.then() / async-await
100
+ // continuations (including already-resolved promises) are never executed,
101
+ // causing all awaits to hang forever in the background runtime.
102
+ try {
103
+ rt->drainMicrotasks();
104
+ } catch (const jsi::JSError &e) {
105
+ LOGE("JSError draining microtasks: %s", e.getMessage().c_str());
106
+ } catch (const std::exception &e) {
107
+ LOGE("Error draining microtasks: %s", e.what());
108
+ }
109
+ }
110
+
111
+ // ── Timer support for background runtime ──────────────────────────────
112
+ // The background Hermes runtime does NOT have working setTimeout/setInterval
113
+ // out of the box (RN's timer module only wires into the main runtime). We
114
+ // install our own JSI-level setTimeout/setInterval/clearTimeout/clearInterval
115
+ // backed by a single C++ worker thread that dispatches callbacks back to the
116
+ // background JS queue via the same executor used by SharedRPC.
117
+
118
+ struct TimerEntry {
119
+ std::shared_ptr<jsi::Function> callback;
120
+ long long fireAtMs; // Absolute time in ms when the timer should fire.
121
+ long long intervalMs; // 0 if one-shot, >0 if setInterval period.
122
+ bool cancelled;
123
+ };
124
+
125
+ static std::mutex gTimerMutex;
126
+ static std::condition_variable gTimerCv;
127
+ static std::unordered_map<int64_t, TimerEntry> gTimers;
128
+ static std::atomic<int64_t> gNextTimerId{1};
129
+ static std::atomic<bool> gTimerWorkerStarted{false};
130
+ static std::atomic<bool> gTimerWorkerStop{false};
131
+ static RPCRuntimeExecutor gBgTimerExecutor;
132
+
133
+ static long long nowMs() {
134
+ using namespace std::chrono;
135
+ return duration_cast<milliseconds>(
136
+ steady_clock::now().time_since_epoch())
137
+ .count();
138
+ }
139
+
140
+ // Called on the bg JS thread. Executes the callback only; the worker has
141
+ // already erased (one-shot) or rescheduled (interval) the timer under lock.
142
+ static void fireTimerOnJsThread(
143
+ int64_t timerId,
144
+ std::shared_ptr<jsi::Function> cb,
145
+ jsi::Runtime &rt) {
146
+ if (!cb) return;
147
+ try {
148
+ cb->call(rt);
149
+ } catch (const jsi::JSError &e) {
150
+ LOGE("Timer %lld callback JSError: %s", (long long)timerId,
151
+ e.getMessage().c_str());
152
+ } catch (const std::exception &e) {
153
+ LOGE("Timer %lld callback error: %s", (long long)timerId, e.what());
154
+ }
155
+ }
156
+
157
+ static void timerWorkerLoop() {
158
+ while (!gTimerWorkerStop.load()) {
159
+ // Snapshot of timers that should be dispatched this iteration and
160
+ // their callbacks. Captured under the lock; callbacks are invoked on
161
+ // the JS thread (not here).
162
+ std::vector<std::pair<int64_t, std::shared_ptr<jsi::Function>>> toFire;
163
+ {
164
+ std::unique_lock<std::mutex> lock(gTimerMutex);
165
+ if (gTimers.empty()) {
166
+ gTimerCv.wait(lock, [] {
167
+ return gTimerWorkerStop.load() || !gTimers.empty();
168
+ });
169
+ if (gTimerWorkerStop.load()) return;
170
+ continue;
171
+ }
172
+
173
+ // Find the earliest fireAt among non-cancelled timers.
174
+ long long earliest = LLONG_MAX;
175
+ for (auto &kv : gTimers) {
176
+ if (!kv.second.cancelled && kv.second.fireAtMs < earliest) {
177
+ earliest = kv.second.fireAtMs;
178
+ }
179
+ }
180
+ long long now = nowMs();
181
+ if (earliest == LLONG_MAX) {
182
+ // Only cancelled timers remain; clean them up.
183
+ for (auto it = gTimers.begin(); it != gTimers.end();) {
184
+ if (it->second.cancelled) it = gTimers.erase(it);
185
+ else ++it;
186
+ }
187
+ continue;
188
+ }
189
+ if (earliest > now) {
190
+ gTimerCv.wait_for(
191
+ lock, std::chrono::milliseconds(earliest - now));
192
+ continue;
193
+ }
194
+
195
+ // Collect ready timers AND either erase (one-shot) or reschedule
196
+ // (interval) them RIGHT HERE under the lock. This is critical:
197
+ // if we wait to erase in fireTimerOnJsThread, the next worker
198
+ // iteration would immediately find the same timers still
199
+ // in-queue and re-dispatch them, causing an infinite flood of
200
+ // `scheduleOnJSThread` calls.
201
+ for (auto it = gTimers.begin(); it != gTimers.end();) {
202
+ if (it->second.cancelled) {
203
+ it = gTimers.erase(it);
204
+ continue;
205
+ }
206
+ if (it->second.fireAtMs <= now) {
207
+ toFire.emplace_back(it->first, it->second.callback);
208
+ if (it->second.intervalMs > 0) {
209
+ // Reschedule interval. Use `now + intervalMs` rather
210
+ // than `fireAtMs + intervalMs` so a slow fire path
211
+ // cannot produce an infinite backlog.
212
+ it->second.fireAtMs = now + it->second.intervalMs;
213
+ ++it;
214
+ } else {
215
+ it = gTimers.erase(it);
216
+ }
217
+ } else {
218
+ ++it;
219
+ }
220
+ }
221
+ }
222
+
223
+ RPCRuntimeExecutor executor = gBgTimerExecutor;
224
+ if (!executor) {
225
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
226
+ continue;
227
+ }
228
+ for (auto &pair : toFire) {
229
+ int64_t id = pair.first;
230
+ std::shared_ptr<jsi::Function> cb = pair.second;
231
+ executor([id, cb](jsi::Runtime &rt) {
232
+ fireTimerOnJsThread(id, cb, rt);
233
+ });
234
+ }
235
+ }
236
+ }
237
+
238
+ static void ensureTimerWorkerStarted() {
239
+ bool expected = false;
240
+ if (gTimerWorkerStarted.compare_exchange_strong(expected, true)) {
241
+ std::thread(timerWorkerLoop).detach();
242
+ LOGI("Timer worker thread started");
243
+ }
244
+ }
245
+
246
+ static int64_t scheduleTimer(
247
+ std::shared_ptr<jsi::Function> cb,
248
+ double ms,
249
+ bool isInterval) {
250
+ int64_t id = gNextTimerId.fetch_add(1);
251
+ long long intervalMs = isInterval ? static_cast<long long>(ms) : 0;
252
+ long long delay = static_cast<long long>(ms);
253
+ if (delay < 0) delay = 0;
254
+ {
255
+ std::lock_guard<std::mutex> lock(gTimerMutex);
256
+ gTimers[id] = TimerEntry{
257
+ std::move(cb),
258
+ nowMs() + delay,
259
+ intervalMs,
260
+ false,
261
+ };
262
+ }
263
+ gTimerCv.notify_all();
264
+ ensureTimerWorkerStarted();
265
+ return id;
266
+ }
267
+
268
+ static void cancelTimer(int64_t id) {
269
+ {
270
+ std::lock_guard<std::mutex> lock(gTimerMutex);
271
+ auto it = gTimers.find(id);
272
+ if (it != gTimers.end()) {
273
+ it->second.cancelled = true;
274
+ }
275
+ }
276
+ gTimerCv.notify_all();
277
+ }
278
+
279
+ static void installTimersOnRuntime(jsi::Runtime &rt) {
280
+ auto makeSetter = [](bool isInterval) {
281
+ return [isInterval](
282
+ jsi::Runtime &rt,
283
+ const jsi::Value &,
284
+ const jsi::Value *args,
285
+ size_t count) -> jsi::Value {
286
+ if (count < 1 || !args[0].isObject() ||
287
+ !args[0].getObject(rt).isFunction(rt)) {
288
+ return jsi::Value::undefined();
289
+ }
290
+ auto cb = std::make_shared<jsi::Function>(
291
+ args[0].getObject(rt).getFunction(rt));
292
+ double ms = 0;
293
+ if (count >= 2 && args[1].isNumber()) {
294
+ ms = args[1].asNumber();
295
+ }
296
+ int64_t id = scheduleTimer(std::move(cb), ms, isInterval);
297
+ return jsi::Value(static_cast<double>(id));
298
+ };
299
+ };
300
+ auto makeCanceller = []() {
301
+ return [](jsi::Runtime &rt,
302
+ const jsi::Value &,
303
+ const jsi::Value *args,
304
+ size_t count) -> jsi::Value {
305
+ if (count < 1 || !args[0].isNumber()) {
306
+ return jsi::Value::undefined();
307
+ }
308
+ int64_t id = static_cast<int64_t>(args[0].asNumber());
309
+ cancelTimer(id);
310
+ return jsi::Value::undefined();
311
+ };
312
+ };
313
+
314
+ // requestAnimationFrame(cb): fires after ~16ms (60fps) with high-resolution
315
+ // timestamp arg, matching the DOM contract. Background runtime has no
316
+ // rendering concept, so we just approximate via setTimeout(16ms).
317
+ auto rafFn = [](jsi::Runtime &rt,
318
+ const jsi::Value &,
319
+ const jsi::Value *args,
320
+ size_t count) -> jsi::Value {
321
+ if (count < 1 || !args[0].isObject() ||
322
+ !args[0].getObject(rt).isFunction(rt)) {
323
+ return jsi::Value::undefined();
324
+ }
325
+ // Wrap callback so it receives a DOMHighResTimeStamp-like arg.
326
+ auto userCb = std::make_shared<jsi::Function>(
327
+ args[0].getObject(rt).getFunction(rt));
328
+ auto wrapper = jsi::Function::createFromHostFunction(
329
+ rt,
330
+ jsi::PropNameID::forAscii(rt, "rafWrapper"),
331
+ 0,
332
+ [userCb](jsi::Runtime &rt2,
333
+ const jsi::Value &,
334
+ const jsi::Value *,
335
+ size_t) -> jsi::Value {
336
+ try {
337
+ userCb->call(rt2, jsi::Value(static_cast<double>(nowMs())));
338
+ } catch (const jsi::JSError &e) {
339
+ LOGE("rAF callback JSError: %s", e.getMessage().c_str());
340
+ } catch (const std::exception &e) {
341
+ LOGE("rAF callback error: %s", e.what());
342
+ }
343
+ return jsi::Value::undefined();
344
+ });
345
+ auto wrappedCb = std::make_shared<jsi::Function>(std::move(wrapper));
346
+ int64_t id = scheduleTimer(std::move(wrappedCb), 16.0, false);
347
+ return jsi::Value(static_cast<double>(id));
348
+ };
349
+
350
+ // requestIdleCallback(cb, {timeout?}): fires "soon" with an IdleDeadline-ish
351
+ // object. Background runtime has no render frames to be idle between, so
352
+ // we approximate via setTimeout(1ms) and provide a deadline stub whose
353
+ // timeRemaining() always returns 50 (reasonable budget).
354
+ auto ricFn = [](jsi::Runtime &rt,
355
+ const jsi::Value &,
356
+ const jsi::Value *args,
357
+ size_t count) -> jsi::Value {
358
+ if (count < 1 || !args[0].isObject() ||
359
+ !args[0].getObject(rt).isFunction(rt)) {
360
+ return jsi::Value::undefined();
361
+ }
362
+ auto userCb = std::make_shared<jsi::Function>(
363
+ args[0].getObject(rt).getFunction(rt));
364
+ auto wrapper = jsi::Function::createFromHostFunction(
365
+ rt,
366
+ jsi::PropNameID::forAscii(rt, "ricWrapper"),
367
+ 0,
368
+ [userCb](jsi::Runtime &rt2,
369
+ const jsi::Value &,
370
+ const jsi::Value *,
371
+ size_t) -> jsi::Value {
372
+ try {
373
+ // Build a minimal IdleDeadline: { didTimeout: false,
374
+ // timeRemaining: () => 50 }.
375
+ jsi::Object deadline(rt2);
376
+ deadline.setProperty(rt2, "didTimeout", jsi::Value(false));
377
+ deadline.setProperty(
378
+ rt2,
379
+ "timeRemaining",
380
+ jsi::Function::createFromHostFunction(
381
+ rt2,
382
+ jsi::PropNameID::forAscii(rt2, "timeRemaining"),
383
+ 0,
384
+ [](jsi::Runtime &,
385
+ const jsi::Value &,
386
+ const jsi::Value *,
387
+ size_t) -> jsi::Value {
388
+ return jsi::Value(50.0);
389
+ }));
390
+ userCb->call(rt2, jsi::Value(rt2, std::move(deadline)));
391
+ } catch (const jsi::JSError &e) {
392
+ LOGE("rIC callback JSError: %s", e.getMessage().c_str());
393
+ } catch (const std::exception &e) {
394
+ LOGE("rIC callback error: %s", e.what());
395
+ }
396
+ return jsi::Value::undefined();
397
+ });
398
+ auto wrappedCb = std::make_shared<jsi::Function>(std::move(wrapper));
399
+ int64_t id = scheduleTimer(std::move(wrappedCb), 1.0, false);
400
+ return jsi::Value(static_cast<double>(id));
401
+ };
402
+
403
+ auto global = rt.global();
404
+ global.setProperty(
405
+ rt, "setTimeout",
406
+ jsi::Function::createFromHostFunction(
407
+ rt, jsi::PropNameID::forAscii(rt, "setTimeout"), 2,
408
+ makeSetter(false)));
409
+ global.setProperty(
410
+ rt, "setInterval",
411
+ jsi::Function::createFromHostFunction(
412
+ rt, jsi::PropNameID::forAscii(rt, "setInterval"), 2,
413
+ makeSetter(true)));
414
+ global.setProperty(
415
+ rt, "clearTimeout",
416
+ jsi::Function::createFromHostFunction(
417
+ rt, jsi::PropNameID::forAscii(rt, "clearTimeout"), 1,
418
+ makeCanceller()));
419
+ global.setProperty(
420
+ rt, "clearInterval",
421
+ jsi::Function::createFromHostFunction(
422
+ rt, jsi::PropNameID::forAscii(rt, "clearInterval"), 1,
423
+ makeCanceller()));
424
+ global.setProperty(
425
+ rt, "requestAnimationFrame",
426
+ jsi::Function::createFromHostFunction(
427
+ rt, jsi::PropNameID::forAscii(rt, "requestAnimationFrame"), 1,
428
+ rafFn));
429
+ global.setProperty(
430
+ rt, "cancelAnimationFrame",
431
+ jsi::Function::createFromHostFunction(
432
+ rt, jsi::PropNameID::forAscii(rt, "cancelAnimationFrame"), 1,
433
+ makeCanceller()));
434
+ global.setProperty(
435
+ rt, "requestIdleCallback",
436
+ jsi::Function::createFromHostFunction(
437
+ rt, jsi::PropNameID::forAscii(rt, "requestIdleCallback"), 1,
438
+ ricFn));
439
+ global.setProperty(
440
+ rt, "cancelIdleCallback",
441
+ jsi::Function::createFromHostFunction(
442
+ rt, jsi::PropNameID::forAscii(rt, "cancelIdleCallback"), 1,
443
+ makeCanceller()));
444
+ LOGI("Timer + rAF + rIC polyfills installed on bg runtime");
89
445
  }
90
446
 
91
447
  // ── nativeInstallSharedBridge ───────────────────────────────────────────
@@ -145,9 +501,20 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
145
501
  };
146
502
 
147
503
  std::string runtimeId = isMain ? "main" : "background";
504
+ // Save the bg executor so our custom timer worker can dispatch callbacks
505
+ // back to the bg JS queue. We must do this BEFORE moving `executor` into
506
+ // SharedRPC::install (which will std::move it out).
507
+ if (!capturedIsMain) {
508
+ gBgTimerExecutor = executor;
509
+ }
148
510
  SharedRPC::install(*rt, std::move(executor), runtimeId);
149
511
  LOGI("SharedStore and SharedRPC installed (isMain=%d)", static_cast<int>(isMain));
150
512
  if (!capturedIsMain) {
513
+ // Install setTimeout/setInterval/clearTimeout/clearInterval on the
514
+ // background runtime. React Native's built-in timer module only wires
515
+ // into the main runtime, so without this, any `await wait(ms)` or
516
+ // setTimeout callback in the background thread would never fire.
517
+ installTimersOnRuntime(*rt);
151
518
  invokeOptionalGlobalFunction(*rt, "__setupBackgroundRPCHandler");
152
519
  }
153
520
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-background-thread",
3
- "version": "3.0.16",
3
+ "version": "3.0.18",
4
4
  "description": "react-native-background-thread",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",