@onekeyfe/react-native-background-thread 3.0.30 → 3.0.32
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/android/src/main/cpp/cpp-adapter.cpp +22 -0
- package/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt +211 -0
- package/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt +4 -0
- package/cpp/SharedRPC.cpp +88 -13
- package/cpp/SharedRPC.h +14 -0
- package/ios/BackgroundRunnerReactNativeDelegate.mm +12 -4
- package/ios/BackgroundThread.h +4 -0
- package/ios/BackgroundThread.mm +16 -0
- package/ios/BackgroundThreadManager.h +77 -3
- package/ios/BackgroundThreadManager.mm +285 -5
- package/lib/module/NativeBackgroundThread.js +26 -0
- package/lib/typescript/src/NativeBackgroundThread.d.ts +76 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/NativeBackgroundThread.ts +79 -4
- package/src/SharedRPC.ts +0 -1
- package/src/SharedStore.ts +0 -1
- package/src/index.tsx +1 -0
|
@@ -586,6 +586,28 @@ Java_com_backgroundthread_BackgroundThreadManager_nativeSetupErrorHandler(
|
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
// ── nativeInvalidateSharedRpc ───────────────────────────────────────────
|
|
590
|
+
// Synchronously quiesce the SharedRPC listener for a given runtimeId before
|
|
591
|
+
// the underlying JS runtime is torn down. Called from
|
|
592
|
+
// BackgroundThreadManager.restart() ahead of process restart / soft reload
|
|
593
|
+
// so any cross-runtime executor lambda still in flight short-circuits
|
|
594
|
+
// instead of dispatching onto a dying runtime (parity with iOS).
|
|
595
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
596
|
+
Java_com_backgroundthread_BackgroundThreadManager_nativeInvalidateSharedRpc(
|
|
597
|
+
JNIEnv *env, jobject thiz, jstring runtimeId) {
|
|
598
|
+
|
|
599
|
+
const char *idChars = env->GetStringUTFChars(runtimeId, nullptr);
|
|
600
|
+
if (!idChars) {
|
|
601
|
+
return JNI_FALSE;
|
|
602
|
+
}
|
|
603
|
+
std::string id(idChars);
|
|
604
|
+
env->ReleaseStringUTFChars(runtimeId, idChars);
|
|
605
|
+
|
|
606
|
+
bool found = SharedRPC::invalidate(id);
|
|
607
|
+
LOGI("nativeInvalidateSharedRpc: id=%s found=%d", id.c_str(), found ? 1 : 0);
|
|
608
|
+
return found ? JNI_TRUE : JNI_FALSE;
|
|
609
|
+
}
|
|
610
|
+
|
|
589
611
|
// ── nativeDestroy ───────────────────────────────────────────────────────
|
|
590
612
|
// Clean up native resources.
|
|
591
613
|
// Called from BackgroundThreadManager.destroy().
|
|
@@ -5,6 +5,8 @@ import android.content.Intent
|
|
|
5
5
|
import android.net.Uri
|
|
6
6
|
import android.os.Handler
|
|
7
7
|
import android.os.Looper
|
|
8
|
+
import com.facebook.react.ReactApplication
|
|
9
|
+
import com.facebook.react.ReactHost
|
|
8
10
|
import com.facebook.react.ReactPackage
|
|
9
11
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
10
12
|
import com.facebook.react.ReactInstanceEventListener
|
|
@@ -21,6 +23,7 @@ import com.facebook.react.runtime.hermes.HermesInstance
|
|
|
21
23
|
import com.facebook.react.shell.MainReactPackage
|
|
22
24
|
import java.io.File
|
|
23
25
|
import java.lang.ref.WeakReference
|
|
26
|
+
import java.util.concurrent.TimeUnit
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Singleton manager for the background React Native runtime.
|
|
@@ -47,6 +50,14 @@ class BackgroundThreadManager private constructor() {
|
|
|
47
50
|
@Volatile
|
|
48
51
|
private var mainRuntimePtr: Long = 0
|
|
49
52
|
private var mainReactContext: ReactApplicationContext? = null
|
|
53
|
+
|
|
54
|
+
// Captured lazily on the first installSharedBridgeInMainRuntime() call.
|
|
55
|
+
// Used by restart(mode='ui') to soft-reload the main runtime via
|
|
56
|
+
// ReactHost.reload(reason) so the bg runtime stays hot. Null when the
|
|
57
|
+
// host app is not on bridgeless / NewArch — restart() falls back to a
|
|
58
|
+
// full process restart in that case.
|
|
59
|
+
@Volatile
|
|
60
|
+
private var mainReactHost: ReactHost? = null
|
|
50
61
|
private var isStarted = false
|
|
51
62
|
|
|
52
63
|
companion object {
|
|
@@ -74,6 +85,14 @@ class BackgroundThreadManager private constructor() {
|
|
|
74
85
|
private external fun nativeDestroy()
|
|
75
86
|
private external fun nativeExecuteWork(runtimePtr: Long, workId: Long)
|
|
76
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Synchronously mark the SharedRPC listener for `runtimeId` as dead
|
|
90
|
+
* before the underlying JS runtime is torn down. See
|
|
91
|
+
* SharedRPC::invalidate (cpp/SharedRPC.cpp) for the cross-runtime
|
|
92
|
+
* correctness rationale.
|
|
93
|
+
*/
|
|
94
|
+
private external fun nativeInvalidateSharedRpc(runtimeId: String): Boolean
|
|
95
|
+
|
|
77
96
|
// ── SharedBridge ────────────────────────────────────────────────────────
|
|
78
97
|
|
|
79
98
|
/**
|
|
@@ -86,6 +105,23 @@ class BackgroundThreadManager private constructor() {
|
|
|
86
105
|
|
|
87
106
|
fun installSharedBridgeInMainRuntime(context: ReactApplicationContext) {
|
|
88
107
|
mainReactContext = context
|
|
108
|
+
// Capture the host the first time we see it; reload() needs it. We
|
|
109
|
+
// resolve via ReactApplication.reactHost — non-null only on bridgeless
|
|
110
|
+
// / NewArch builds. Bound here (not in restart()) because installShared
|
|
111
|
+
// Bridge is invoked from JS bootstrap, which is also when ReactContext
|
|
112
|
+
// is guaranteed live. Restart('ui') falls back to process restart if
|
|
113
|
+
// this stays null.
|
|
114
|
+
if (mainReactHost == null) {
|
|
115
|
+
try {
|
|
116
|
+
val app = context.applicationContext as? ReactApplication
|
|
117
|
+
mainReactHost = app?.reactHost
|
|
118
|
+
if (mainReactHost == null) {
|
|
119
|
+
BTLogger.warn("ReactHost unavailable (non-bridgeless host?); restart('ui') will fall back to process restart")
|
|
120
|
+
}
|
|
121
|
+
} catch (t: Throwable) {
|
|
122
|
+
BTLogger.error("Failed to resolve ReactHost: ${t.message}")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
89
125
|
context.runOnJSQueueThread {
|
|
90
126
|
try {
|
|
91
127
|
val ptr = context.javaScriptContextHolder?.get() ?: 0L
|
|
@@ -754,6 +790,180 @@ class BackgroundThreadManager private constructor() {
|
|
|
754
790
|
}
|
|
755
791
|
}
|
|
756
792
|
|
|
793
|
+
// ── Restart ─────────────────────────────────────────────────────────────
|
|
794
|
+
//
|
|
795
|
+
// Replaces direct use of `react-native-restart` from JS. The native module
|
|
796
|
+
// is the only piece that has visibility into BOTH the main and background
|
|
797
|
+
// ReactHost lifecycles, so it can sequence:
|
|
798
|
+
// 1. SharedRPC quiesce (mark listener alive=false, leak callback, drop
|
|
799
|
+
// executor) — synchronous, holds SharedRPC mutex internally
|
|
800
|
+
// 2. JS runtime teardown — see per-mode behaviour below
|
|
801
|
+
//
|
|
802
|
+
// mode='ui':
|
|
803
|
+
// Soft-reload the main runtime via ReactHost.reload(reason). bg runtime
|
|
804
|
+
// keeps running. After reload, JS bootstrap re-invokes installShared
|
|
805
|
+
// Bridge() (the TurboModule entry point), which re-installs the "main"
|
|
806
|
+
// SharedRPC listener and refreshes mainRuntimePtr. Requires bridgeless /
|
|
807
|
+
// NewArch — if ReactHost was never captured (old arch host), falls back
|
|
808
|
+
// to a process restart so callers get consistent reload semantics.
|
|
809
|
+
//
|
|
810
|
+
// mode='all':
|
|
811
|
+
// Process-level restart (Runtime.exit + makeRestartActivityTask). OTA
|
|
812
|
+
// install/switch and resetData want a clean slate on disk; a process
|
|
813
|
+
// relaunch trivially guarantees both runtimes bootstrap from fresh
|
|
814
|
+
// bundle content without us having to sequence "tear down bg → reload
|
|
815
|
+
// main → wait for new main → restart bg". iOS goes through soft reload
|
|
816
|
+
// for mode='all' because abrupt termination is App Store-unfriendly and
|
|
817
|
+
// iOS has no clean self-relaunch; Android does, so we keep using it.
|
|
818
|
+
|
|
819
|
+
fun restart(context: ReactApplicationContext, mode: String, reason: String, promise: com.facebook.react.bridge.Promise) {
|
|
820
|
+
val isUi = mode == "ui"
|
|
821
|
+
val isAll = mode == "all"
|
|
822
|
+
if (!isUi && !isAll) {
|
|
823
|
+
promise.reject(
|
|
824
|
+
"BG_RESTART_ERROR",
|
|
825
|
+
"BackgroundThread.restart: unsupported mode '$mode', expected 'ui' or 'all'"
|
|
826
|
+
)
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
BTLogger.info("restart: mode=$mode reason=$reason")
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
nativeInvalidateSharedRpc("main")
|
|
834
|
+
if (isAll) {
|
|
835
|
+
nativeInvalidateSharedRpc("background")
|
|
836
|
+
}
|
|
837
|
+
} catch (t: Throwable) {
|
|
838
|
+
// Invalidate is best-effort; not fatal if the JNI call somehow
|
|
839
|
+
// throws (e.g. so library not loaded yet). Continue to restart.
|
|
840
|
+
BTLogger.error("restart: SharedRPC invalidate failed: ${t.message}")
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
val host = mainReactHost
|
|
844
|
+
if (isUi && host != null) {
|
|
845
|
+
BTLogger.info("restart(ui): soft reload via ReactHost.reload")
|
|
846
|
+
// Post to the UI thread to keep the call off the JS thread that
|
|
847
|
+
// is about to be torn down; ReactHost.reload is itself thread-
|
|
848
|
+
// safe but the work it kicks off (JNI teardown of the old
|
|
849
|
+
// ReactInstance) is cleaner when not initiated from inside the
|
|
850
|
+
// outgoing runtime's callback frame.
|
|
851
|
+
//
|
|
852
|
+
// Promise resolution is sequenced after reload() returns. The
|
|
853
|
+
// actual *delivery* of that resolve to JS is best-effort: the
|
|
854
|
+
// CallInvoker that would route it back is tied to the outgoing
|
|
855
|
+
// ReactInstance which reload() is about to invalidate, and the
|
|
856
|
+
// typical caller is `await restart(...)` whose continuation
|
|
857
|
+
// never runs because the reload supersedes it. The position
|
|
858
|
+
// matters only for the synchronous-throw case — if reload()
|
|
859
|
+
// throws inline, the catch block reaches the fallback / reject
|
|
860
|
+
// path instead of misreporting success.
|
|
861
|
+
//
|
|
862
|
+
// ReactHost.reload(reason) returns a TaskInterface<Void> that
|
|
863
|
+
// completes when the new ReactInstance is up; an async fault
|
|
864
|
+
// means reload failed AFTER we already invalidated the
|
|
865
|
+
// SharedRPC main listener — main runtime is torn down without
|
|
866
|
+
// rebuild, SharedRPC is permanently dead for "main". Watch the
|
|
867
|
+
// task on a daemon thread and fall back to a process restart
|
|
868
|
+
// on fault / cancellation / timeout so we converge on a known
|
|
869
|
+
// good state instead of leaving the app wedged. TaskInterface
|
|
870
|
+
// only exposes waitForCompletion + isFaulted/isCancelled/
|
|
871
|
+
// getError (no continueWith/onError callback in RN 0.83's
|
|
872
|
+
// public surface), so polling on a worker thread is what we
|
|
873
|
+
// have. Timeout is generous (15s) — observed worst case
|
|
874
|
+
// reload on a low-end Android is ~3s.
|
|
875
|
+
Handler(Looper.getMainLooper()).post {
|
|
876
|
+
val task = try {
|
|
877
|
+
host.reload("BackgroundThread.restart(ui): $reason").also {
|
|
878
|
+
promise.resolve(null)
|
|
879
|
+
}
|
|
880
|
+
} catch (t: Throwable) {
|
|
881
|
+
BTLogger.error("restart(ui): ReactHost.reload threw synchronously: ${t.message}")
|
|
882
|
+
if (!triggerProcessRestart(context)) {
|
|
883
|
+
promise.reject(
|
|
884
|
+
"BG_RESTART_ERROR",
|
|
885
|
+
"restart(ui): reload failed and process restart fallback also failed: ${t.message}",
|
|
886
|
+
t,
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
// if triggerProcessRestart returned true, Runtime.exit(0)
|
|
890
|
+
// ran and this reject is unreachable
|
|
891
|
+
null
|
|
892
|
+
}
|
|
893
|
+
if (task != null) {
|
|
894
|
+
Thread {
|
|
895
|
+
try {
|
|
896
|
+
val completed = task.waitForCompletion(15, TimeUnit.SECONDS)
|
|
897
|
+
if (!completed || task.isFaulted() || task.isCancelled()) {
|
|
898
|
+
val err = task.getError()
|
|
899
|
+
BTLogger.error(
|
|
900
|
+
"restart(ui): reload task did not complete cleanly " +
|
|
901
|
+
"(completed=$completed, faulted=${task.isFaulted()}, " +
|
|
902
|
+
"cancelled=${task.isCancelled()}): ${err?.message} — " +
|
|
903
|
+
"falling back to process restart"
|
|
904
|
+
)
|
|
905
|
+
Handler(Looper.getMainLooper()).post {
|
|
906
|
+
triggerProcessRestart(context)
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
BTLogger.info("restart(ui): reload task completed successfully")
|
|
910
|
+
}
|
|
911
|
+
} catch (t: Throwable) {
|
|
912
|
+
BTLogger.error(
|
|
913
|
+
"restart(ui): reload-task watch thread errored: ${t.message} — " +
|
|
914
|
+
"falling back to process restart"
|
|
915
|
+
)
|
|
916
|
+
Handler(Looper.getMainLooper()).post {
|
|
917
|
+
triggerProcessRestart(context)
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}.apply {
|
|
921
|
+
isDaemon = true
|
|
922
|
+
name = "OneKey-BgThread-ReloadWatch"
|
|
923
|
+
start()
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (isUi) {
|
|
931
|
+
BTLogger.warn("restart(ui): ReactHost unavailable, falling back to process restart")
|
|
932
|
+
}
|
|
933
|
+
if (!triggerProcessRestart(context)) {
|
|
934
|
+
promise.reject(
|
|
935
|
+
"BG_RESTART_ERROR",
|
|
936
|
+
"Failed to trigger process restart (intent resolution or startActivity failed)"
|
|
937
|
+
)
|
|
938
|
+
}
|
|
939
|
+
// if triggerProcessRestart returned true, Runtime.exit(0) ran and
|
|
940
|
+
// any further code here is unreachable
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Returns true if Runtime.exit(0) was reached (in which case the process
|
|
945
|
+
* is now terminating and any code after the call site is unreachable).
|
|
946
|
+
* Returns false if intent resolution failed or startActivity threw
|
|
947
|
+
* before exit() — caller is responsible for propagating that failure
|
|
948
|
+
* to the JS Promise so callers don't observe a false success.
|
|
949
|
+
*/
|
|
950
|
+
private fun triggerProcessRestart(context: ReactApplicationContext): Boolean {
|
|
951
|
+
try {
|
|
952
|
+
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
953
|
+
if (intent == null) {
|
|
954
|
+
BTLogger.error("triggerProcessRestart: launch intent not found for ${context.packageName}")
|
|
955
|
+
return false
|
|
956
|
+
}
|
|
957
|
+
val mainIntent = Intent.makeRestartActivityTask(intent.component)
|
|
958
|
+
context.startActivity(mainIntent)
|
|
959
|
+
Runtime.getRuntime().exit(0)
|
|
960
|
+
return true // unreachable; exit() does not return
|
|
961
|
+
} catch (t: Throwable) {
|
|
962
|
+
BTLogger.error("triggerProcessRestart: ${t.message}")
|
|
963
|
+
return false
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
757
967
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
758
968
|
|
|
759
969
|
val isBackgroundStarted: Boolean get() = isStarted
|
|
@@ -763,6 +973,7 @@ class BackgroundThreadManager private constructor() {
|
|
|
763
973
|
bgRuntimePtr = 0
|
|
764
974
|
mainRuntimePtr = 0
|
|
765
975
|
mainReactContext = null
|
|
976
|
+
mainReactHost = null
|
|
766
977
|
bgReactHost?.destroy("BackgroundThreadManager destroyed", null)
|
|
767
978
|
bgReactHost = null
|
|
768
979
|
isStarted = false
|
|
@@ -38,4 +38,8 @@ class BackgroundThreadModule(reactContext: ReactApplicationContext) :
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
override fun restart(mode: String, reason: String, promise: Promise) {
|
|
43
|
+
BackgroundThreadManager.getInstance().restart(reactApplicationContext, mode, reason, promise)
|
|
44
|
+
}
|
|
41
45
|
}
|
package/cpp/SharedRPC.cpp
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#include "SharedRPC.h"
|
|
2
2
|
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
|
|
3
5
|
#ifdef __ANDROID__
|
|
4
6
|
#include <android/log.h>
|
|
5
7
|
#define RPC_LOG(...) __android_log_print(ANDROID_LOG_INFO, "SharedRPC", __VA_ARGS__)
|
|
@@ -23,24 +25,68 @@ void SharedRPC::install(jsi::Runtime &rt, RPCRuntimeExecutor executor,
|
|
|
23
25
|
auto obj = jsi::Object::createFromHostObject(rt, rpc);
|
|
24
26
|
rt.global().setProperty(rt, "sharedRPC", std::move(obj));
|
|
25
27
|
|
|
28
|
+
auto alive = std::make_shared<std::atomic<bool>>(true);
|
|
29
|
+
|
|
30
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
31
|
+
// Defensive dedup: under the normal restart flow, invalidate() has already
|
|
32
|
+
// run for this runtimeId and erased the entry, so this loop matches
|
|
33
|
+
// nothing. The branch survives as a fallback for any path that re-installs
|
|
34
|
+
// without first calling invalidate (e.g. legacy host integrations, partial
|
|
35
|
+
// teardown). Same correctness invariants as invalidate(): flip alive=false
|
|
36
|
+
// so any executor lambda already in flight short-circuits, and leak the
|
|
37
|
+
// jsi::Function callback because destroying it on a wrong/dying thread
|
|
38
|
+
// crashes (null deref in Pointer::~Pointer).
|
|
39
|
+
for (auto &listener : listeners_) {
|
|
40
|
+
if (listener.runtimeId == runtimeId) {
|
|
41
|
+
if (listener.alive) {
|
|
42
|
+
listener.alive->store(false);
|
|
43
|
+
}
|
|
44
|
+
if (listener.callback) {
|
|
45
|
+
new std::shared_ptr<jsi::Function>(std::move(listener.callback));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
listeners_.erase(
|
|
50
|
+
std::remove_if(listeners_.begin(), listeners_.end(),
|
|
51
|
+
[&runtimeId](const RuntimeListener &l) {
|
|
52
|
+
return l.runtimeId == runtimeId;
|
|
53
|
+
}),
|
|
54
|
+
listeners_.end());
|
|
55
|
+
listeners_.push_back(
|
|
56
|
+
{runtimeId, &rt, std::move(executor), nullptr, std::move(alive)});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
bool SharedRPC::invalidate(const std::string &runtimeId) {
|
|
26
60
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
27
|
-
|
|
28
|
-
// IMPORTANT: The old listener's callback is a jsi::Function tied to the old
|
|
29
|
-
// runtime. On reload, that runtime is already destroyed, so calling
|
|
30
|
-
// ~jsi::Function() would crash (null deref in Pointer::~Pointer).
|
|
31
|
-
// We intentionally leak the callback to avoid this.
|
|
61
|
+
bool found = false;
|
|
32
62
|
for (auto &listener : listeners_) {
|
|
33
|
-
if (listener.runtimeId
|
|
63
|
+
if (listener.runtimeId != runtimeId) continue;
|
|
64
|
+
if (listener.alive) {
|
|
65
|
+
listener.alive->store(false);
|
|
66
|
+
}
|
|
67
|
+
if (listener.callback) {
|
|
68
|
+
// Same rationale as install(): destroying a jsi::Function tied to a
|
|
69
|
+
// torn-down runtime crashes. Leak it; the runtime is going away anyway.
|
|
34
70
|
new std::shared_ptr<jsi::Function>(std::move(listener.callback));
|
|
35
71
|
}
|
|
72
|
+
// Drop the executor closure so nothing tries to dispatch via the dying
|
|
73
|
+
// RCTInstance/CallInvoker after this point.
|
|
74
|
+
listener.executor = nullptr;
|
|
75
|
+
found = true;
|
|
36
76
|
}
|
|
77
|
+
// Erase the dead entries. Already-dispatched executor lambdas hold their
|
|
78
|
+
// own shared_ptr<alive> snapshot, so erasing here does not affect them —
|
|
79
|
+
// it only prevents NEW notifyOtherRuntime() snapshots from picking up the
|
|
80
|
+
// dead listener. Without the erase, a mode='all' restart whose post-reload
|
|
81
|
+
// re-install never fires would leave a permanently-dead entry in the
|
|
82
|
+
// vector. The next install() for the same runtimeId pushes a fresh entry.
|
|
37
83
|
listeners_.erase(
|
|
38
84
|
std::remove_if(listeners_.begin(), listeners_.end(),
|
|
39
85
|
[&runtimeId](const RuntimeListener &l) {
|
|
40
86
|
return l.runtimeId == runtimeId;
|
|
41
87
|
}),
|
|
42
88
|
listeners_.end());
|
|
43
|
-
|
|
89
|
+
return found;
|
|
44
90
|
}
|
|
45
91
|
|
|
46
92
|
void SharedRPC::reset() {
|
|
@@ -48,7 +94,12 @@ void SharedRPC::reset() {
|
|
|
48
94
|
slots_.clear();
|
|
49
95
|
// Intentionally leak jsi::Function callbacks to avoid destroying them on the
|
|
50
96
|
// wrong thread (same rationale as the leak in install() for reload scenarios).
|
|
97
|
+
// Also flip alive=false so any executor lambda still in flight short-circuits
|
|
98
|
+
// before touching a torn-down runtime.
|
|
51
99
|
for (auto &listener : listeners_) {
|
|
100
|
+
if (listener.alive) {
|
|
101
|
+
listener.alive->store(false);
|
|
102
|
+
}
|
|
52
103
|
if (listener.callback) {
|
|
53
104
|
new std::shared_ptr<jsi::Function>(std::move(listener.callback));
|
|
54
105
|
}
|
|
@@ -59,25 +110,49 @@ void SharedRPC::reset() {
|
|
|
59
110
|
void SharedRPC::notifyOtherRuntime(jsi::Runtime &callerRt, const std::string &callId) {
|
|
60
111
|
// Collect executors and callbacks under lock, then invoke outside lock
|
|
61
112
|
// to avoid deadlock (executor may schedule work that also acquires mutex_).
|
|
62
|
-
|
|
113
|
+
//
|
|
114
|
+
// Each snapshot carries the listener's shared `alive` flag. The flag is
|
|
115
|
+
// checked twice — once here (so an already-invalidated listener is never
|
|
116
|
+
// even scheduled) and once again inside the dispatched lambda (so a
|
|
117
|
+
// listener invalidated AFTER snapshot but BEFORE the lambda runs is also
|
|
118
|
+
// short-circuited before touching the dying runtime).
|
|
119
|
+
struct Snapshot {
|
|
120
|
+
RPCRuntimeExecutor executor;
|
|
121
|
+
std::shared_ptr<jsi::Function> callback;
|
|
122
|
+
std::shared_ptr<std::atomic<bool>> alive;
|
|
123
|
+
};
|
|
124
|
+
std::vector<Snapshot> toNotify;
|
|
63
125
|
{
|
|
64
126
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
65
127
|
RPC_LOG("notifyOtherRuntime: callId=%s, listeners=%zu, callerRt=%p",
|
|
66
128
|
callId.c_str(), listeners_.size(), &callerRt);
|
|
67
129
|
for (auto &listener : listeners_) {
|
|
68
|
-
RPC_LOG(" listener: id=%s, rt=%p, hasCallback=%d",
|
|
69
|
-
listener.runtimeId.c_str(), listener.runtime,
|
|
130
|
+
RPC_LOG(" listener: id=%s, rt=%p, hasCallback=%d, alive=%d",
|
|
131
|
+
listener.runtimeId.c_str(), listener.runtime,
|
|
132
|
+
listener.callback != nullptr,
|
|
133
|
+
listener.alive ? listener.alive->load() : 0);
|
|
70
134
|
if (listener.runtime == &callerRt) continue;
|
|
71
135
|
if (!listener.callback) continue;
|
|
72
|
-
|
|
136
|
+
if (!listener.executor) continue;
|
|
137
|
+
if (!listener.alive || !listener.alive->load()) continue;
|
|
138
|
+
toNotify.push_back({listener.executor, listener.callback, listener.alive});
|
|
73
139
|
}
|
|
74
140
|
RPC_LOG(" toNotify count: %zu", toNotify.size());
|
|
75
141
|
}
|
|
76
142
|
|
|
77
|
-
for (auto &
|
|
143
|
+
for (auto &snap : toNotify) {
|
|
78
144
|
auto id = callId;
|
|
79
145
|
RPC_LOG(" invoking executor for callId=%s", id.c_str());
|
|
80
|
-
|
|
146
|
+
auto cb = snap.callback;
|
|
147
|
+
auto alive = snap.alive;
|
|
148
|
+
snap.executor([cb, alive, id](jsi::Runtime &rt) {
|
|
149
|
+
// Listener was invalidated between snapshot and dispatch — bail
|
|
150
|
+
// before calling into a runtime that may already be torn down.
|
|
151
|
+
if (!alive || !alive->load()) {
|
|
152
|
+
RPC_LOG(" executor work skipped (listener invalidated) for callId=%s",
|
|
153
|
+
id.c_str());
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
81
156
|
RPC_LOG(" executor work running for callId=%s", id.c_str());
|
|
82
157
|
try {
|
|
83
158
|
cb->call(rt, jsi::String::createFromUtf8(rt, id));
|
package/cpp/SharedRPC.h
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include <jsi/jsi.h>
|
|
4
|
+
#include <atomic>
|
|
4
5
|
#include <functional>
|
|
5
6
|
#include <memory>
|
|
6
7
|
#include <mutex>
|
|
@@ -22,6 +23,11 @@ struct RuntimeListener {
|
|
|
22
23
|
jsi::Runtime *runtime;
|
|
23
24
|
RPCRuntimeExecutor executor;
|
|
24
25
|
std::shared_ptr<jsi::Function> callback; // JS onWrite callback
|
|
26
|
+
// Liveness flag, shared with any executor lambda already in flight.
|
|
27
|
+
// Flipped to false by invalidate() (or by a follow-up install() that
|
|
28
|
+
// replaces this listener) so notifyOtherRuntime and any already-enqueued
|
|
29
|
+
// lambda can short-circuit before touching a torn-down runtime.
|
|
30
|
+
std::shared_ptr<std::atomic<bool>> alive;
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
class SharedRPC : public jsi::HostObject {
|
|
@@ -37,6 +43,14 @@ public:
|
|
|
37
43
|
static void install(jsi::Runtime &rt, RPCRuntimeExecutor executor,
|
|
38
44
|
const std::string &runtimeId);
|
|
39
45
|
|
|
46
|
+
/// Synchronously quiesce the listener for runtimeId before the underlying
|
|
47
|
+
/// JS runtime is torn down. Marks alive=false (so any executor lambda
|
|
48
|
+
/// already in flight short-circuits), leaks the jsi::Function callback
|
|
49
|
+
/// (destroying it on a wrong/dying thread crashes), and clears the
|
|
50
|
+
/// executor closure (drops the lambda's captured RCTInstance/CallInvoker).
|
|
51
|
+
/// Safe to call from any thread. Returns true if a listener was found.
|
|
52
|
+
static bool invalidate(const std::string &runtimeId);
|
|
53
|
+
|
|
40
54
|
static void reset();
|
|
41
55
|
|
|
42
56
|
private:
|
|
@@ -368,10 +368,18 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
|
|
|
368
368
|
// Install SharedStore into background runtime
|
|
369
369
|
SharedStore::install(runtime);
|
|
370
370
|
|
|
371
|
-
// Install SharedRPC with executor for cross-runtime notifications
|
|
372
|
-
RCTInstance
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
// Install SharedRPC with executor for cross-runtime notifications.
|
|
372
|
+
// Capture the bg RCTInstance __weak so executor lambdas still in flight
|
|
373
|
+
// when the bg host is torn down (BackgroundThread.restart mode=all,
|
|
374
|
+
// OTA bundle install) no-op cleanly instead of dispatching onto a
|
|
375
|
+
// freed instance and crashing.
|
|
376
|
+
__weak RCTInstance *weakBgInstance = _rctInstance;
|
|
377
|
+
RPCRuntimeExecutor bgExecutor = [weakBgInstance](std::function<void(jsi::Runtime &)> work) {
|
|
378
|
+
RCTInstance *strongBgInstance = weakBgInstance;
|
|
379
|
+
if (!strongBgInstance) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
[strongBgInstance callFunctionOnBufferedRuntimeExecutor:[work](jsi::Runtime &rt) {
|
|
375
383
|
work(rt);
|
|
376
384
|
}];
|
|
377
385
|
};
|
package/ios/BackgroundThread.h
CHANGED
|
@@ -9,5 +9,9 @@
|
|
|
9
9
|
path:(NSString *)path
|
|
10
10
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
11
11
|
reject:(RCTPromiseRejectBlock)reject;
|
|
12
|
+
- (void)restart:(NSString *)mode
|
|
13
|
+
reason:(NSString *)reason
|
|
14
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
15
|
+
reject:(RCTPromiseRejectBlock)reject;
|
|
12
16
|
|
|
13
17
|
@end
|
package/ios/BackgroundThread.mm
CHANGED
|
@@ -45,6 +45,22 @@
|
|
|
45
45
|
}];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
- (void)restart:(NSString *)mode
|
|
49
|
+
reason:(NSString *)reason
|
|
50
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
51
|
+
reject:(RCTPromiseRejectBlock)reject {
|
|
52
|
+
BackgroundThreadManager *manager = [BackgroundThreadManager sharedInstance];
|
|
53
|
+
[manager restartWithMode:mode
|
|
54
|
+
reason:reason
|
|
55
|
+
completion:^(NSError * _Nullable error) {
|
|
56
|
+
if (error) {
|
|
57
|
+
reject(@"BG_RESTART_ERROR", error.localizedDescription, error);
|
|
58
|
+
} else {
|
|
59
|
+
resolve(nil);
|
|
60
|
+
}
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
+ (NSString *)moduleName
|
|
49
65
|
{
|
|
50
66
|
return @"BackgroundThread";
|
|
@@ -12,7 +12,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
12
12
|
+ (instancetype)sharedInstance;
|
|
13
13
|
|
|
14
14
|
/// Install SharedBridge HostObject into the main (UI) runtime.
|
|
15
|
-
///
|
|
15
|
+
///
|
|
16
|
+
/// MUST be invoked from the host app's RCTReactNativeFactoryDelegate
|
|
17
|
+
/// hostDidStart: callback on EVERY main RCTHost lifecycle start — including
|
|
18
|
+
/// after a `BackgroundThread.restart` (both modes) reloads the main bridge.
|
|
19
|
+
/// The module cannot self-invoke this because it does not own the RCTHost
|
|
20
|
+
/// reference; restartWithMode: emits a loud error log if it detects this
|
|
21
|
+
/// contract was violated post-reload.
|
|
22
|
+
///
|
|
16
23
|
/// @param host The RCTHost for the main runtime
|
|
17
24
|
+ (void)installSharedBridgeInMainRuntime:(RCTHost *)host;
|
|
18
25
|
|
|
@@ -23,8 +30,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
23
30
|
/// @param entryURL The custom entry URL for the background runner
|
|
24
31
|
- (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL;
|
|
25
32
|
|
|
26
|
-
/// Check if background runner is started
|
|
27
|
-
|
|
33
|
+
/// Check if background runner is started.
|
|
34
|
+
///
|
|
35
|
+
/// Atomic to match the implementation's redeclaration (the post-reload
|
|
36
|
+
/// health-check reads this on the main thread while the start path writes
|
|
37
|
+
/// it from whichever thread the caller is on). Keeping the header
|
|
38
|
+
/// `nonatomic` while the impl was `atomic` tripped Clang's
|
|
39
|
+
/// -Wproperty-attribute-mismatch and breaks CI builds under -Werror.
|
|
40
|
+
@property (atomic, readonly) BOOL isStarted;
|
|
28
41
|
|
|
29
42
|
/// Register a HBC segment in the background runtime (Phase 2.5 spike)
|
|
30
43
|
/// @param segmentId The segment ID to register
|
|
@@ -34,6 +47,67 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
34
47
|
path:(NSString *)path
|
|
35
48
|
completion:(void (^)(NSError * _Nullable error))completion;
|
|
36
49
|
|
|
50
|
+
/// Restart the JS runtime(s). Replaces direct use of `react-native-restart`.
|
|
51
|
+
/// Coordinates SharedRPC quiesce + RCTReloadCommand so cross-runtime traffic
|
|
52
|
+
/// in flight during reload is silently dropped instead of crashing on a
|
|
53
|
+
/// torn-down RCTInstance / dangling jsi::Function callback.
|
|
54
|
+
///
|
|
55
|
+
/// ## Post-reload contract (host responsibility)
|
|
56
|
+
///
|
|
57
|
+
/// After `RCTTriggerReloadCommandListeners` rebuilds the main RCTHost, the
|
|
58
|
+
/// host app's `RCTReactNativeFactoryDelegate.hostDidStart:` is responsible
|
|
59
|
+
/// for re-arming BOTH halves of the integration on the new host:
|
|
60
|
+
/// 1. `+[BackgroundThreadManager installSharedBridgeInMainRuntime:newHost]`
|
|
61
|
+
/// — re-arms the "main" SharedRPC listener; without this main→bg RPC
|
|
62
|
+
/// silently breaks even though both runtimes are alive.
|
|
63
|
+
/// 2. (mode='all' only) `[BackgroundThreadManager.sharedInstance
|
|
64
|
+
/// startBackgroundRunner]` — recreates the bg RCTHost since the
|
|
65
|
+
/// previous one was released and `isStarted` was reset to NO.
|
|
66
|
+
///
|
|
67
|
+
/// The module defends against host integration omissions with a two-stage
|
|
68
|
+
/// post-reload health-check (`dispatch_after` on the main queue scheduled
|
|
69
|
+
/// before the reload is triggered — does NOT depend on
|
|
70
|
+
/// `RCTJavaScriptDidLoadNotification`, which is unreliable in bridgeless /
|
|
71
|
+
/// NewArch). Stage 1 fires ~3s after `restartWithMode:` returns; stage 2
|
|
72
|
+
/// (if needed) ~3s later. The check decides:
|
|
73
|
+
/// - Both halves healthy at stage 1 → log success, done.
|
|
74
|
+
/// - Anything missing at stage 1 → reschedule stage 2 unconditionally.
|
|
75
|
+
/// Earlier designs short-circuited the "main ready, bg not ready"
|
|
76
|
+
/// case at stage 1 as a stable signal, but hosts that gate
|
|
77
|
+
/// startBackgroundRunner on async work (feature flag, login, network)
|
|
78
|
+
/// can be mainReady=YES while the real start call is still inflight;
|
|
79
|
+
/// short-circuiting would race them and the host's late start would
|
|
80
|
+
/// be silently dropped (now warn-logged on the early-return path).
|
|
81
|
+
/// - Stage 2 → final verdict; whatever's missing is logged and self-
|
|
82
|
+
/// healed where possible.
|
|
83
|
+
///
|
|
84
|
+
/// For mode='all' self-respawn, the module replays the host's last entry
|
|
85
|
+
/// URL — cached in `startBackgroundRunnerWithEntryURL:` — instead of the
|
|
86
|
+
/// default `background.bundle`. This is critical on OTA-equipped hosts:
|
|
87
|
+
/// without the cache, self-respawn would load the bundled bg bundle from
|
|
88
|
+
/// the IPA while main runs the OTA-updated main bundle, and the next
|
|
89
|
+
/// cross-runtime RPC would moduleId-mismatch and crash. The cache makes
|
|
90
|
+
/// self-respawn a true safety net for the broken-AppDelegate-wiring case.
|
|
91
|
+
/// The bundled default name `"background.bundle"` is intentionally treated
|
|
92
|
+
/// as "no real cache" by the self-respawn fallback so the OTA-mismatch
|
|
93
|
+
/// warning still fires for hosts that initially bootstrapped with the
|
|
94
|
+
/// default URL and never swapped to an OTA-resolved path.
|
|
95
|
+
///
|
|
96
|
+
/// Concurrent restart() calls are made safe via a monotonic generation
|
|
97
|
+
/// counter: each invocation captures its own generation and each health-
|
|
98
|
+
/// check stage bails if a newer restart() has superseded it.
|
|
99
|
+
///
|
|
100
|
+
/// @param mode `@"ui"` to reload only the main runtime (bg stays hot);
|
|
101
|
+
/// `@"all"` to reload both. Any other value invokes completion
|
|
102
|
+
/// with NSError domain `BackgroundThread` code 10.
|
|
103
|
+
/// @param reason Free-form attribution string forwarded to
|
|
104
|
+
/// RCTTriggerReloadCommandListeners and host logs.
|
|
105
|
+
/// @param completion Invoked once the reload has been triggered. nil error
|
|
106
|
+
/// on success.
|
|
107
|
+
- (void)restartWithMode:(NSString *)mode
|
|
108
|
+
reason:(NSString *)reason
|
|
109
|
+
completion:(void (^)(NSError * _Nullable error))completion;
|
|
110
|
+
|
|
37
111
|
@end
|
|
38
112
|
|
|
39
113
|
NS_ASSUME_NONNULL_END
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
#include "SharedStore.h"
|
|
19
19
|
#include "SharedRPC.h"
|
|
20
|
+
#import <React/RCTReloadCommand.h>
|
|
20
21
|
#import <ReactCommon/RCTHost.h>
|
|
21
22
|
#import <objc/runtime.h>
|
|
22
23
|
|
|
@@ -24,9 +25,50 @@
|
|
|
24
25
|
@property (nonatomic, strong) BackgroundReactNativeDelegate *reactNativeFactoryDelegate;
|
|
25
26
|
@property (nonatomic, strong) RCTReactNativeFactory *reactNativeFactory;
|
|
26
27
|
@property (nonatomic, assign) BOOL hasListeners;
|
|
27
|
-
|
|
28
|
+
// Atomic because the post-reload health-check reads it on the main thread
|
|
29
|
+
// while startBackgroundRunnerWithEntryURL: writes it from whichever thread
|
|
30
|
+
// the caller is on (the module's public surface does not constrain caller
|
|
31
|
+
// thread). Atomic gives us the memory-barrier needed for cross-thread
|
|
32
|
+
// reads to see the latest store. The set+dispatch pattern in start... is
|
|
33
|
+
// still TOCTOU-racy for concurrent first-time starts, but that hazard
|
|
34
|
+
// pre-exists this PR and is out of scope here.
|
|
35
|
+
@property (atomic, assign, readwrite) BOOL isStarted;
|
|
36
|
+
// Flipped to YES inside the installSharedBridgeInMainRuntime: lambda and to
|
|
37
|
+
// NO at the start of restartWithMode:. Read by the post-reload health-check
|
|
38
|
+
// to detect when the host app's hostDidStart: failed to re-invoke the
|
|
39
|
+
// install on the new RCTHost — which would silently break main→bg RPC.
|
|
40
|
+
@property (atomic, assign) BOOL mainSharedBridgeInstalled;
|
|
41
|
+
// Monotonic counter bumped on every restartWithMode: call. The post-reload
|
|
42
|
+
// health-check captures its own generation and bails if a newer restart()
|
|
43
|
+
// supersedes it, preventing the second restart's flag reset from making
|
|
44
|
+
// the first restart's check misreport. Touched only on the main thread
|
|
45
|
+
// (the dispatch_block_t `work` runs there), so non-atomic is correct.
|
|
46
|
+
@property (nonatomic, assign) NSUInteger restartGeneration;
|
|
47
|
+
// Cached from the most recent startBackgroundRunnerWithEntryURL: call. Used
|
|
48
|
+
// by the mode='all' self-respawn fallback to preserve the host's last
|
|
49
|
+
// chosen entry URL (typically an OTA-resolved bundle path) across restart;
|
|
50
|
+
// `reactNativeFactoryDelegate` is released for mode='all', so without this
|
|
51
|
+
// cache we'd fall back to the bundled "background.bundle" — which on OTA
|
|
52
|
+
// devices is a moduleId-mismatch crash waiting to happen. Atomic for the
|
|
53
|
+
// same reason as isStarted: written from the caller's thread (public API
|
|
54
|
+
// doesn't pin start... to main) and read on main by the health-check.
|
|
55
|
+
@property (atomic, copy) NSString *lastEntryURL;
|
|
56
|
+
|
|
57
|
+
// Forward declaration so restartWithMode: can call it; definition lives
|
|
58
|
+
// after restartWithMode: for readability (the call site is the natural
|
|
59
|
+
// place to start reading the restart flow).
|
|
60
|
+
- (void)scheduleHealthCheckForRestart:(NSString *)mode
|
|
61
|
+
isAll:(BOOL)isAll
|
|
62
|
+
generation:(NSUInteger)myGen
|
|
63
|
+
retried:(BOOL)retried;
|
|
28
64
|
@end
|
|
29
65
|
|
|
66
|
+
// First-stage delay for the post-reload health-check. Picked so the host's
|
|
67
|
+
// hostDidStart: chain has time to run on a typical device. On slower
|
|
68
|
+
// devices the chain can exceed this; that's why the check is structured as
|
|
69
|
+
// two stages — see scheduleHealthCheckForRestart:isAll:generation:retried:.
|
|
70
|
+
static const NSTimeInterval kRestartHealthCheckDelaySeconds = 3.0;
|
|
71
|
+
|
|
30
72
|
@implementation BackgroundThreadManager
|
|
31
73
|
|
|
32
74
|
static BackgroundThreadManager *_sharedInstance = nil;
|
|
@@ -71,14 +113,28 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
|
|
|
71
113
|
[instance callFunctionOnBufferedRuntimeExecutor:^(facebook::jsi::Runtime &runtime) {
|
|
72
114
|
SharedStore::install(runtime);
|
|
73
115
|
|
|
74
|
-
// Install SharedRPC with executor for cross-runtime notifications
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
116
|
+
// Install SharedRPC with executor for cross-runtime notifications.
|
|
117
|
+
// Capture the RCTInstance __weak: when the main host is torn down on
|
|
118
|
+
// reload (RNRestart / RCTReloadCommand / BackgroundThread.restart) the
|
|
119
|
+
// instance is dealloc'd, and any executor lambda still in flight will
|
|
120
|
+
// see a nil strong reference and bail out — instead of dispatching
|
|
121
|
+
// callFunctionOnBufferedRuntimeExecutor on a freed instance and
|
|
122
|
+
// crashing (EXC_BAD_ACCESS / use-after-free).
|
|
123
|
+
__weak id weakInstance = instance;
|
|
124
|
+
RPCRuntimeExecutor mainExecutor = [weakInstance](std::function<void(jsi::Runtime &)> work) {
|
|
125
|
+
id strongInstance = weakInstance;
|
|
126
|
+
if (!strongInstance) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
[strongInstance callFunctionOnBufferedRuntimeExecutor:[work](jsi::Runtime &rt) {
|
|
78
130
|
work(rt);
|
|
79
131
|
}];
|
|
80
132
|
};
|
|
81
133
|
SharedRPC::install(runtime, std::move(mainExecutor), "main");
|
|
134
|
+
// Set the integration-health flag from inside the JS-thread lambda so
|
|
135
|
+
// it only flips true AFTER the listener is actually live; the post-
|
|
136
|
+
// reload observer reads this to detect host hostDidStart: omissions.
|
|
137
|
+
[BackgroundThreadManager sharedInstance].mainSharedBridgeInstalled = YES;
|
|
82
138
|
[BTLogger info:@"SharedStore and SharedRPC installed in main runtime"];
|
|
83
139
|
}];
|
|
84
140
|
}
|
|
@@ -96,9 +152,27 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
|
|
|
96
152
|
|
|
97
153
|
- (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL {
|
|
98
154
|
if (self.isStarted) {
|
|
155
|
+
// Surface the URL mismatch when a second start... call would
|
|
156
|
+
// otherwise silently drop the host's intent — most common in the
|
|
157
|
+
// self-respawn-vs-host-start race after restart('all'): self-
|
|
158
|
+
// respawn boots with cached URL, then the host's async
|
|
159
|
+
// hostDidStart chain (gated on feature flag / login / network)
|
|
160
|
+
// finally calls start with a different URL and gets short-
|
|
161
|
+
// circuited. The log lets that race be diagnosed from production
|
|
162
|
+
// traces instead of "why is bg on the old URL".
|
|
163
|
+
if (entryURL.length > 0 && ![entryURL isEqualToString:self.lastEntryURL]) {
|
|
164
|
+
[BTLogger warn:[NSString stringWithFormat:
|
|
165
|
+
@"startBackgroundRunnerWithEntryURL: ignored (already started); requested=%@ active=%@",
|
|
166
|
+
entryURL, self.lastEntryURL ?: @"<nil>"]];
|
|
167
|
+
}
|
|
99
168
|
return;
|
|
100
169
|
}
|
|
101
170
|
self.isStarted = YES;
|
|
171
|
+
// Cache the URL before we even start so the mode='all' self-respawn
|
|
172
|
+
// path can preserve it across a restart that releases the delegate.
|
|
173
|
+
// Updated unconditionally — first-time and re-start with a new URL
|
|
174
|
+
// both reflect into lastEntryURL.
|
|
175
|
+
self.lastEntryURL = entryURL;
|
|
102
176
|
[BTLogger info:[NSString stringWithFormat:@"Starting background runner with entryURL: %@", entryURL]];
|
|
103
177
|
|
|
104
178
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
@@ -156,4 +230,210 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
|
|
|
156
230
|
}
|
|
157
231
|
}
|
|
158
232
|
|
|
233
|
+
#pragma mark - Restart
|
|
234
|
+
|
|
235
|
+
- (void)restartWithMode:(NSString *)mode
|
|
236
|
+
reason:(NSString *)reason
|
|
237
|
+
completion:(void (^)(NSError * _Nullable error))completion
|
|
238
|
+
{
|
|
239
|
+
BOOL isUI = [mode isEqualToString:@"ui"];
|
|
240
|
+
BOOL isAll = [mode isEqualToString:@"all"];
|
|
241
|
+
|
|
242
|
+
if (!isUI && !isAll) {
|
|
243
|
+
NSError *error = [NSError errorWithDomain:@"BackgroundThread"
|
|
244
|
+
code:10
|
|
245
|
+
userInfo:@{NSLocalizedDescriptionKey:
|
|
246
|
+
[NSString stringWithFormat:@"BackgroundThread.restart: unsupported mode '%@', expected 'ui' or 'all'", mode]}];
|
|
247
|
+
if (completion) completion(error);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
NSString *attributedReason = reason.length > 0
|
|
252
|
+
? [NSString stringWithFormat:@"BackgroundThread.restart(%@): %@", mode, reason]
|
|
253
|
+
: [NSString stringWithFormat:@"BackgroundThread.restart(%@)", mode];
|
|
254
|
+
|
|
255
|
+
[BTLogger info:[NSString stringWithFormat:@"restart: mode=%@ reason=%@", mode, reason ?: @"<none>"]];
|
|
256
|
+
|
|
257
|
+
// Bridge teardown must happen on the main thread; everything before
|
|
258
|
+
// RCTTriggerReloadCommandListeners runs synchronously on that thread so
|
|
259
|
+
// the quiesce is provably done before any reload-driven instance dealloc.
|
|
260
|
+
dispatch_block_t work = ^{
|
|
261
|
+
// (1) Quiesce SharedRPC listener(s). Synchronous, holds the SharedRPC
|
|
262
|
+
// mutex internally — any concurrent notifyOtherRuntime that gets past
|
|
263
|
+
// the lock will see alive=false.
|
|
264
|
+
SharedRPC::invalidate("main");
|
|
265
|
+
if (isAll) {
|
|
266
|
+
SharedRPC::invalidate("background");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// (2) For mode=all, drop our strong refs to the bg host so ARC can
|
|
270
|
+
// dealloc it. We also reset isStarted so the post-reload hostDidStart
|
|
271
|
+
// callback (which is invoked from AppDelegate after the main reload
|
|
272
|
+
// completes) re-enters startBackgroundRunner instead of short-circuiting.
|
|
273
|
+
if (isAll) {
|
|
274
|
+
[BTLogger info:@"restart(all): releasing bg factory + resetting isStarted"];
|
|
275
|
+
self.reactNativeFactory = nil;
|
|
276
|
+
self.reactNativeFactoryDelegate = nil;
|
|
277
|
+
self.isStarted = NO;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// (3) Schedule a defensive post-reload health-check BEFORE triggering
|
|
281
|
+
// the reload. The check uses pure dispatch_after on our own state
|
|
282
|
+
// (mainSharedBridgeInstalled, isStarted) instead of an
|
|
283
|
+
// RCTJavaScriptDidLoadNotification observer — that notification's
|
|
284
|
+
// post timing is unreliable in bridgeless / NewArch, and an
|
|
285
|
+
// observer-based design introduces observer-leak + cross-generation
|
|
286
|
+
// misreporting hazards that dispatch_after + restartGeneration
|
|
287
|
+
// sidesteps entirely. See scheduleHealthCheckForRestart:... for the
|
|
288
|
+
// two-stage retry that tolerates slow devices.
|
|
289
|
+
self.mainSharedBridgeInstalled = NO;
|
|
290
|
+
NSUInteger myGen = ++self.restartGeneration;
|
|
291
|
+
[self scheduleHealthCheckForRestart:mode isAll:isAll generation:myGen retried:NO];
|
|
292
|
+
|
|
293
|
+
// (4) Trigger the main bridge reload. Equivalent to what
|
|
294
|
+
// react-native-restart did, but now sequenced AFTER quiesce so the
|
|
295
|
+
// SharedRPC race window is closed.
|
|
296
|
+
RCTTriggerReloadCommandListeners(attributedReason);
|
|
297
|
+
|
|
298
|
+
// (5) For mode=all, the new main host's hostDidStart will call
|
|
299
|
+
// BackgroundThreadBridge.startBackgroundRunner again (isStarted==NO
|
|
300
|
+
// now), recreating the bg host and re-installing the "background"
|
|
301
|
+
// listener via BackgroundRunnerReactNativeDelegate.hostDidStart.
|
|
302
|
+
// If the host fails to do so, the health-check above self-heals
|
|
303
|
+
// (with the default-entry-URL caveat noted there).
|
|
304
|
+
// For mode=ui, the bg host stays as-is; only the "main" listener
|
|
305
|
+
// gets re-installed in installSharedBridgeInMainRuntime.
|
|
306
|
+
|
|
307
|
+
if (completion) completion(nil);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
if ([NSThread isMainThread]) {
|
|
311
|
+
work();
|
|
312
|
+
} else {
|
|
313
|
+
dispatch_async(dispatch_get_main_queue(), work);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#pragma mark - Restart Health Check
|
|
318
|
+
|
|
319
|
+
// Two-stage post-reload health-check.
|
|
320
|
+
//
|
|
321
|
+
// Stage 1 fires at +kRestartHealthCheckDelaySeconds (~3s) from restart()
|
|
322
|
+
// dispatch. Reads `mainSharedBridgeInstalled` and (for mode='all')
|
|
323
|
+
// `isStarted` — both signals the module owns. Decides:
|
|
324
|
+
// - Both healthy → log success, done.
|
|
325
|
+
// - Anything missing → reschedule stage 2 (+~3s more, total ~6s) and
|
|
326
|
+
// defer the final verdict. This is intentional: even the "main ready,
|
|
327
|
+
// bg not ready" case is rescheduled rather than self-respawned at
|
|
328
|
+
// stage 1, because hosts that gate startBackgroundRunner on async
|
|
329
|
+
// work (feature-flag fetch, login, network callback) can be
|
|
330
|
+
// mainReady=YES while their async start is still in flight — see the
|
|
331
|
+
// in-line comment in the stage-1 branch below.
|
|
332
|
+
//
|
|
333
|
+
// Stage 2 fires at total ~6s. Whatever state we see is the final verdict;
|
|
334
|
+
// any remaining gap is logged as an integration failure (with self-heal
|
|
335
|
+
// where possible). 6s comfortably covers a low-end iPhone OTA reload chain
|
|
336
|
+
// (observed worst case ~2.5s); past that, integration is more likely
|
|
337
|
+
// broken than slow.
|
|
338
|
+
//
|
|
339
|
+
// Generation check at the top of each stage block bails out if a newer
|
|
340
|
+
// restart() has bumped restartGeneration in the meantime — its own check
|
|
341
|
+
// will run, and reading flags now would mix cycles.
|
|
342
|
+
- (void)scheduleHealthCheckForRestart:(NSString *)mode
|
|
343
|
+
isAll:(BOOL)isAll
|
|
344
|
+
generation:(NSUInteger)myGen
|
|
345
|
+
retried:(BOOL)retried
|
|
346
|
+
{
|
|
347
|
+
__weak BackgroundThreadManager *weakSelf = self;
|
|
348
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
|
|
349
|
+
(int64_t)(kRestartHealthCheckDelaySeconds * NSEC_PER_SEC)),
|
|
350
|
+
dispatch_get_main_queue(), ^{
|
|
351
|
+
BackgroundThreadManager *strongSelf = weakSelf;
|
|
352
|
+
if (!strongSelf) return;
|
|
353
|
+
if (strongSelf.restartGeneration != myGen) {
|
|
354
|
+
[BTLogger info:[NSString stringWithFormat:
|
|
355
|
+
@"restart(%@) health-check stage%@ superseded by newer restart (gen %lu → %lu)",
|
|
356
|
+
mode, retried ? @"2" : @"1",
|
|
357
|
+
(unsigned long)myGen, (unsigned long)strongSelf.restartGeneration]];
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
BOOL mainReady = strongSelf.mainSharedBridgeInstalled;
|
|
362
|
+
BOOL bgReady = !isAll || strongSelf.isStarted;
|
|
363
|
+
NSString *stage = retried ? @"2" : @"1";
|
|
364
|
+
|
|
365
|
+
if (mainReady && bgReady) {
|
|
366
|
+
[BTLogger info:[NSString stringWithFormat:
|
|
367
|
+
@"restart(%@): post-reload health-check stage%@ OK (mainReady=YES, bgReady=YES)",
|
|
368
|
+
mode, stage]];
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Stage 1: anything missing → reschedule stage 2.
|
|
373
|
+
//
|
|
374
|
+
// Earlier rounds short-circuited the "mainReady=YES, bgReady=NO"
|
|
375
|
+
// case as a stable signal that the host wasn't going to call
|
|
376
|
+
// startBackgroundRunner. That assumption holds only for hosts
|
|
377
|
+
// whose hostDidStart: synchronously calls both install AND start.
|
|
378
|
+
// Hosts that gate startBackgroundRunner on async work (feature
|
|
379
|
+
// flag fetch, login state, network callback) can have
|
|
380
|
+
// mainReady=YES while their async start call is still in flight;
|
|
381
|
+
// self-respawning at stage 1 would race them and the host's
|
|
382
|
+
// later (real) start would be silently dropped by the early
|
|
383
|
+
// return in startBackgroundRunnerWithEntryURL: (now logged as a
|
|
384
|
+
// warn, see #2). Stage 2 (+~3s) is sized to give that async path
|
|
385
|
+
// time to land; if it still hasn't by then, the host is broken
|
|
386
|
+
// and self-respawn is correct.
|
|
387
|
+
if (!retried) {
|
|
388
|
+
[BTLogger info:[NSString stringWithFormat:
|
|
389
|
+
@"restart(%@): stage1 incomplete (mainReady=%@, bgReady=%@) — rescheduling stage2 to cover slow hostDidStart chains and async host start calls",
|
|
390
|
+
mode, mainReady ? @"YES" : @"NO", bgReady ? @"YES" : @"NO"]];
|
|
391
|
+
[strongSelf scheduleHealthCheckForRestart:mode isAll:isAll generation:myGen retried:YES];
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Stage 2 final verdict.
|
|
396
|
+
|
|
397
|
+
if (isAll && !strongSelf.isStarted) {
|
|
398
|
+
NSString *cachedURL = strongSelf.lastEntryURL;
|
|
399
|
+
// Treat the bundled default name as "no real cache" so the
|
|
400
|
+
// OTA warn still fires for hosts that initially bootstrapped
|
|
401
|
+
// with the default URL and haven't yet swapped to an OTA-
|
|
402
|
+
// resolved path. Such hosts have OTA risk but no signal in
|
|
403
|
+
// lastEntryURL that distinguishes them.
|
|
404
|
+
BOOL hasCustomCachedURL = cachedURL.length > 0
|
|
405
|
+
&& ![cachedURL isEqualToString:@"background.bundle"];
|
|
406
|
+
if (hasCustomCachedURL) {
|
|
407
|
+
// Preferred path: replay the host's last entry URL. On OTA
|
|
408
|
+
// devices this is the OTA-resolved bundle, which keeps the
|
|
409
|
+
// bg moduleId table aligned with the new main bundle and
|
|
410
|
+
// avoids the silent → crash regression of falling back to
|
|
411
|
+
// the bundled `background.bundle`.
|
|
412
|
+
[BTLogger info:[NSString stringWithFormat:
|
|
413
|
+
@"restart(%@): bg not respawned by host AppDelegate; self-respawning with cached entryURL=%@",
|
|
414
|
+
mode, cachedURL]];
|
|
415
|
+
[strongSelf startBackgroundRunnerWithEntryURL:cachedURL];
|
|
416
|
+
} else {
|
|
417
|
+
// Cache is empty or holds the bundled default. Fall back
|
|
418
|
+
// and warn — on an OTA-equipped host this is the path
|
|
419
|
+
// that historically produces a moduleId-mismatch crash on
|
|
420
|
+
// the next cross-runtime RPC.
|
|
421
|
+
[BTLogger warn:[NSString stringWithFormat:
|
|
422
|
+
@"restart(%@): bg not respawned and no custom cached entryURL (cache=%@); falling back to default. If host uses OTA bundles, wire AppDelegate.hostDidStart: to call startBackgroundRunner explicitly to avoid moduleId-mismatch.",
|
|
423
|
+
mode, cachedURL ?: @"<nil>"]];
|
|
424
|
+
[strongSelf startBackgroundRunner];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!strongSelf.mainSharedBridgeInstalled) {
|
|
429
|
+
[BTLogger error:[NSString stringWithFormat:
|
|
430
|
+
@"restart(%@): SharedBridge not re-installed in main runtime within ~%.1fs after reload (stage%@). "
|
|
431
|
+
@"Host AppDelegate's hostDidStart: must invoke "
|
|
432
|
+
@"+[BackgroundThreadManager installSharedBridgeInMainRuntime:newHost] "
|
|
433
|
+
@"on the new RCTHost or main→bg RPC will silently fail.",
|
|
434
|
+
mode, kRestartHealthCheckDelaySeconds * 2, stage]];
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
159
439
|
@end
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { TurboModuleRegistry } from 'react-native';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Allowed values for `restart()`'s `mode` argument.
|
|
7
|
+
*
|
|
8
|
+
* Defined as a string literal union (not a TypeScript `enum`) on purpose:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Zero runtime cost.** A regular `enum` compiles to a JS object that
|
|
11
|
+
* ships with the bundle. A literal union erases to nothing.
|
|
12
|
+
* 2. **Codegen-friendly.** React Native's TurboModule codegen reads this
|
|
13
|
+
* spec to generate native types; its supported set is primitives,
|
|
14
|
+
* Object/Array, Promise, and string-literal unions. Plain TS `enum`
|
|
15
|
+
* is not in that set — behaviour varies by RN version and best
|
|
16
|
+
* avoided in spec files.
|
|
17
|
+
* 3. **Callsite type-safety without a runtime import.** Consumers can
|
|
18
|
+
* `import type { RestartMode } from '@onekeyfe/react-native-background-thread'`
|
|
19
|
+
* and pass a plain string; TypeScript narrows it without a runtime
|
|
20
|
+
* dependency on this module's value space.
|
|
21
|
+
*
|
|
22
|
+
* Migration note: the JSDoc on `restart()` historically said callers
|
|
23
|
+
* should use the `EAppRestartMode` enum from `@onekeyhq/shared`. If that
|
|
24
|
+
* is a regular TS string enum, its values aren't assignable to this union
|
|
25
|
+
* directly — either cast at the call site (`mode as RestartMode`) or
|
|
26
|
+
* migrate `EAppRestartMode` to a string-literal union / `as const` object
|
|
27
|
+
* so the two stay aligned.
|
|
28
|
+
*/
|
|
29
|
+
|
|
4
30
|
export default TurboModuleRegistry.getEnforcing('BackgroundThread');
|
|
5
31
|
//# sourceMappingURL=NativeBackgroundThread.js.map
|
|
@@ -1,8 +1,84 @@
|
|
|
1
1
|
import type { TurboModule } from 'react-native';
|
|
2
|
+
/**
|
|
3
|
+
* Allowed values for `restart()`'s `mode` argument.
|
|
4
|
+
*
|
|
5
|
+
* Defined as a string literal union (not a TypeScript `enum`) on purpose:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Zero runtime cost.** A regular `enum` compiles to a JS object that
|
|
8
|
+
* ships with the bundle. A literal union erases to nothing.
|
|
9
|
+
* 2. **Codegen-friendly.** React Native's TurboModule codegen reads this
|
|
10
|
+
* spec to generate native types; its supported set is primitives,
|
|
11
|
+
* Object/Array, Promise, and string-literal unions. Plain TS `enum`
|
|
12
|
+
* is not in that set — behaviour varies by RN version and best
|
|
13
|
+
* avoided in spec files.
|
|
14
|
+
* 3. **Callsite type-safety without a runtime import.** Consumers can
|
|
15
|
+
* `import type { RestartMode } from '@onekeyfe/react-native-background-thread'`
|
|
16
|
+
* and pass a plain string; TypeScript narrows it without a runtime
|
|
17
|
+
* dependency on this module's value space.
|
|
18
|
+
*
|
|
19
|
+
* Migration note: the JSDoc on `restart()` historically said callers
|
|
20
|
+
* should use the `EAppRestartMode` enum from `@onekeyhq/shared`. If that
|
|
21
|
+
* is a regular TS string enum, its values aren't assignable to this union
|
|
22
|
+
* directly — either cast at the call site (`mode as RestartMode`) or
|
|
23
|
+
* migrate `EAppRestartMode` to a string-literal union / `as const` object
|
|
24
|
+
* so the two stay aligned.
|
|
25
|
+
*/
|
|
26
|
+
export type RestartMode = 'ui' | 'all';
|
|
2
27
|
export interface Spec extends TurboModule {
|
|
3
28
|
startBackgroundRunnerWithEntryURL(entryURL: string): void;
|
|
4
29
|
installSharedBridge(): void;
|
|
5
30
|
loadSegmentInBackground(segmentId: number, path: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Reload one or both JS runtimes. Replaces `react-native-restart`.
|
|
33
|
+
*
|
|
34
|
+
* `mode` (see {@link RestartMode}):
|
|
35
|
+
* - `'ui'`: restart only the main JS runtime (the one driving the UI).
|
|
36
|
+
* The background runtime stays hot, preserving its state and avoiding
|
|
37
|
+
* the SharedRPC reload race that crashed iOS on language switch.
|
|
38
|
+
* - `'all'`: restart both main and background JS runtimes. Required when
|
|
39
|
+
* the JS bundle on disk has changed (OTA install/switch) so the two
|
|
40
|
+
* runtimes cannot keep running mismatched moduleId tables.
|
|
41
|
+
*
|
|
42
|
+
* Any other value rejects the promise — callers should pass a
|
|
43
|
+
* `RestartMode` literal. The native validation still runs at runtime
|
|
44
|
+
* (it's the source of truth for what the platforms accept), so a string
|
|
45
|
+
* cast that smuggles in an unknown value will be rejected there.
|
|
46
|
+
*
|
|
47
|
+
* `reason` is forwarded to RCTTriggerReloadCommandListeners and to host
|
|
48
|
+
* logs / Sentry breadcrumbs so production restarts are attributable.
|
|
49
|
+
*
|
|
50
|
+
* ## Platform behaviour for `mode='all'`
|
|
51
|
+
*
|
|
52
|
+
* iOS and Android tear down the JS runtimes the same way, but the OS
|
|
53
|
+
* process around them differs — code that runs natively (timers,
|
|
54
|
+
* singletons, in-memory caches, scheduled jobs, foreground services)
|
|
55
|
+
* cannot assume cross-platform survival semantics:
|
|
56
|
+
*
|
|
57
|
+
* - **iOS**: process survives. The main RCTHost is rebuilt in-place via
|
|
58
|
+
* `RCTTriggerReloadCommandListeners`; the bg RCTHost is released and
|
|
59
|
+
* re-spawned by the host AppDelegate's `hostDidStart:`, with the
|
|
60
|
+
* module's two-stage `dispatch_after` health-check as a fallback if
|
|
61
|
+
* the host integration fails to re-arm (intentionally not driven by
|
|
62
|
+
* `RCTJavaScriptDidLoadNotification`, whose timing is unreliable in
|
|
63
|
+
* bridgeless / NewArch). Any process-level state (NSUserDefaults cache,
|
|
64
|
+
* live URLSession tasks, GCD timers attached to the app process)
|
|
65
|
+
* survives. iOS goes through soft reload here because abrupt
|
|
66
|
+
* termination is App Store-unfriendly and iOS lacks a clean self-
|
|
67
|
+
* relaunch path.
|
|
68
|
+
* - **Android**: process is killed. `BackgroundThreadManager` invokes
|
|
69
|
+
* `Runtime.exit(0)` after queuing `makeRestartActivityTask`, so the
|
|
70
|
+
* JVM is replaced wholesale. Any process-level state is lost; Activity
|
|
71
|
+
* stack is rebuilt from the launch intent. The choice is intentional
|
|
72
|
+
* — `mode='all'` is meant for OTA install/switch where a fresh re-
|
|
73
|
+
* read of both bundles from disk is desirable, and Android has a
|
|
74
|
+
* clean self-relaunch path that iOS doesn't.
|
|
75
|
+
*
|
|
76
|
+
* Callers that depend on process-level state (anything held in native
|
|
77
|
+
* singletons, background services, JNI handles) must not rely on it
|
|
78
|
+
* surviving `mode='all'` cross-platform. For UI-only resets that should
|
|
79
|
+
* preserve process state on both platforms, use `mode='ui'`.
|
|
80
|
+
*/
|
|
81
|
+
restart(mode: RestartMode, reason: string): Promise<void>;
|
|
6
82
|
}
|
|
7
83
|
declare const _default: Spec;
|
|
8
84
|
export default _default;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const BackgroundThread: import("./NativeBackgroundThread").Spec;
|
|
2
|
+
export type { RestartMode } from './NativeBackgroundThread';
|
|
2
3
|
export { getSharedStore } from './SharedStore';
|
|
3
4
|
export type { ISharedStore } from './SharedStore';
|
|
4
5
|
export { getSharedRPC } from './SharedRPC';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-background-thread",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.32",
|
|
4
4
|
"description": "react-native-background-thread",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"typescript": "^5.9.2"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@onekeyfe/react-native-bundle-update": "
|
|
87
|
+
"@onekeyfe/react-native-bundle-update": "*",
|
|
88
88
|
"react": "*",
|
|
89
89
|
"react-native": "*"
|
|
90
90
|
},
|
|
@@ -1,13 +1,88 @@
|
|
|
1
1
|
import { TurboModuleRegistry } from 'react-native';
|
|
2
2
|
|
|
3
3
|
import type { TurboModule } from 'react-native';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Allowed values for `restart()`'s `mode` argument.
|
|
7
|
+
*
|
|
8
|
+
* Defined as a string literal union (not a TypeScript `enum`) on purpose:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Zero runtime cost.** A regular `enum` compiles to a JS object that
|
|
11
|
+
* ships with the bundle. A literal union erases to nothing.
|
|
12
|
+
* 2. **Codegen-friendly.** React Native's TurboModule codegen reads this
|
|
13
|
+
* spec to generate native types; its supported set is primitives,
|
|
14
|
+
* Object/Array, Promise, and string-literal unions. Plain TS `enum`
|
|
15
|
+
* is not in that set — behaviour varies by RN version and best
|
|
16
|
+
* avoided in spec files.
|
|
17
|
+
* 3. **Callsite type-safety without a runtime import.** Consumers can
|
|
18
|
+
* `import type { RestartMode } from '@onekeyfe/react-native-background-thread'`
|
|
19
|
+
* and pass a plain string; TypeScript narrows it without a runtime
|
|
20
|
+
* dependency on this module's value space.
|
|
21
|
+
*
|
|
22
|
+
* Migration note: the JSDoc on `restart()` historically said callers
|
|
23
|
+
* should use the `EAppRestartMode` enum from `@onekeyhq/shared`. If that
|
|
24
|
+
* is a regular TS string enum, its values aren't assignable to this union
|
|
25
|
+
* directly — either cast at the call site (`mode as RestartMode`) or
|
|
26
|
+
* migrate `EAppRestartMode` to a string-literal union / `as const` object
|
|
27
|
+
* so the two stay aligned.
|
|
28
|
+
*/
|
|
29
|
+
export type RestartMode = 'ui' | 'all';
|
|
30
|
+
|
|
4
31
|
export interface Spec extends TurboModule {
|
|
5
32
|
startBackgroundRunnerWithEntryURL(entryURL: string): void;
|
|
6
33
|
installSharedBridge(): void;
|
|
7
|
-
loadSegmentInBackground(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
34
|
+
loadSegmentInBackground(segmentId: number, path: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Reload one or both JS runtimes. Replaces `react-native-restart`.
|
|
37
|
+
*
|
|
38
|
+
* `mode` (see {@link RestartMode}):
|
|
39
|
+
* - `'ui'`: restart only the main JS runtime (the one driving the UI).
|
|
40
|
+
* The background runtime stays hot, preserving its state and avoiding
|
|
41
|
+
* the SharedRPC reload race that crashed iOS on language switch.
|
|
42
|
+
* - `'all'`: restart both main and background JS runtimes. Required when
|
|
43
|
+
* the JS bundle on disk has changed (OTA install/switch) so the two
|
|
44
|
+
* runtimes cannot keep running mismatched moduleId tables.
|
|
45
|
+
*
|
|
46
|
+
* Any other value rejects the promise — callers should pass a
|
|
47
|
+
* `RestartMode` literal. The native validation still runs at runtime
|
|
48
|
+
* (it's the source of truth for what the platforms accept), so a string
|
|
49
|
+
* cast that smuggles in an unknown value will be rejected there.
|
|
50
|
+
*
|
|
51
|
+
* `reason` is forwarded to RCTTriggerReloadCommandListeners and to host
|
|
52
|
+
* logs / Sentry breadcrumbs so production restarts are attributable.
|
|
53
|
+
*
|
|
54
|
+
* ## Platform behaviour for `mode='all'`
|
|
55
|
+
*
|
|
56
|
+
* iOS and Android tear down the JS runtimes the same way, but the OS
|
|
57
|
+
* process around them differs — code that runs natively (timers,
|
|
58
|
+
* singletons, in-memory caches, scheduled jobs, foreground services)
|
|
59
|
+
* cannot assume cross-platform survival semantics:
|
|
60
|
+
*
|
|
61
|
+
* - **iOS**: process survives. The main RCTHost is rebuilt in-place via
|
|
62
|
+
* `RCTTriggerReloadCommandListeners`; the bg RCTHost is released and
|
|
63
|
+
* re-spawned by the host AppDelegate's `hostDidStart:`, with the
|
|
64
|
+
* module's two-stage `dispatch_after` health-check as a fallback if
|
|
65
|
+
* the host integration fails to re-arm (intentionally not driven by
|
|
66
|
+
* `RCTJavaScriptDidLoadNotification`, whose timing is unreliable in
|
|
67
|
+
* bridgeless / NewArch). Any process-level state (NSUserDefaults cache,
|
|
68
|
+
* live URLSession tasks, GCD timers attached to the app process)
|
|
69
|
+
* survives. iOS goes through soft reload here because abrupt
|
|
70
|
+
* termination is App Store-unfriendly and iOS lacks a clean self-
|
|
71
|
+
* relaunch path.
|
|
72
|
+
* - **Android**: process is killed. `BackgroundThreadManager` invokes
|
|
73
|
+
* `Runtime.exit(0)` after queuing `makeRestartActivityTask`, so the
|
|
74
|
+
* JVM is replaced wholesale. Any process-level state is lost; Activity
|
|
75
|
+
* stack is rebuilt from the launch intent. The choice is intentional
|
|
76
|
+
* — `mode='all'` is meant for OTA install/switch where a fresh re-
|
|
77
|
+
* read of both bundles from disk is desirable, and Android has a
|
|
78
|
+
* clean self-relaunch path that iOS doesn't.
|
|
79
|
+
*
|
|
80
|
+
* Callers that depend on process-level state (anything held in native
|
|
81
|
+
* singletons, background services, JNI handles) must not rely on it
|
|
82
|
+
* surviving `mode='all'` cross-platform. For UI-only resets that should
|
|
83
|
+
* preserve process state on both platforms, use `mode='ui'`.
|
|
84
|
+
*/
|
|
85
|
+
restart(mode: RestartMode, reason: string): Promise<void>;
|
|
11
86
|
}
|
|
12
87
|
|
|
13
88
|
export default TurboModuleRegistry.getEnforcing<Spec>('BackgroundThread');
|
package/src/SharedRPC.ts
CHANGED
package/src/SharedStore.ts
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import NativeBackgroundThread from './NativeBackgroundThread';
|
|
2
2
|
|
|
3
3
|
export const BackgroundThread = NativeBackgroundThread;
|
|
4
|
+
export type { RestartMode } from './NativeBackgroundThread';
|
|
4
5
|
export { getSharedStore } from './SharedStore';
|
|
5
6
|
export type { ISharedStore } from './SharedStore';
|
|
6
7
|
export { getSharedRPC } from './SharedRPC';
|