@onekeyfe/react-native-split-bundle-loader 3.0.18 → 3.0.19

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.
@@ -30,10 +30,87 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
30
30
  // #18: Limit concurrent asset extractions to avoid I/O contention
31
31
  private const val MAX_CONCURRENT_EXTRACTS = 2
32
32
  private val extractSemaphore = Semaphore(MAX_CONCURRENT_EXTRACTS)
33
+
34
+ // Wipe-on-APK-replace: avoids stale extracted HBC after overwrite install.
35
+ // `lastUpdateTime` changes on every APK replacement (adb install -r,
36
+ // Play Store upgrade, sideload, TestFlight-equivalent). If it differs
37
+ // from what we persisted, nuke the whole extract tree so the new APK's
38
+ // assets get extracted fresh on first load.
39
+ private const val PREFS_NAME = "split_bundle_loader"
40
+ private const val KEY_LAST_INSTALL_STAMP = "last_install_stamp"
41
+ // Double-checked locking: loser threads must BLOCK until the wipe
42
+ // finishes, not just skip. AtomicBoolean.compareAndSet would let
43
+ // losers race ahead and read half-nuked state.
44
+ @Volatile private var wipeCheckDone: Boolean = false
45
+ private val wipeLock = Any()
33
46
  }
34
47
 
35
48
  override fun getName(): String = NAME
36
49
 
50
+ // -----------------------------------------------------------------------
51
+ // Wipe extract dir when APK install/upgrade detected.
52
+ //
53
+ // Without this, an overwrite install (adb install -r, Play Store upgrade,
54
+ // etc.) leaves last-run's extracted HBC files in /data/.../files/, and
55
+ // extractBuiltinSegmentIfNeeded reuses them because its size check can
56
+ // pass even when Metro module IDs drifted. Nuking the tree on every
57
+ // install-stamp change forces the new APK's assets to be re-extracted.
58
+ //
59
+ // Atomic gate means this runs at most once per process regardless of how
60
+ // many entry points call it.
61
+ // -----------------------------------------------------------------------
62
+
63
+ private fun ensureExtractDirFreshForCurrentInstall(context: Context) {
64
+ // Fast path: already done, no locking needed (volatile read).
65
+ if (wipeCheckDone) return
66
+ synchronized(wipeLock) {
67
+ // Re-check inside the lock: another thread may have finished
68
+ // the wipe while we were waiting to enter.
69
+ if (wipeCheckDone) return
70
+
71
+ val currentStamp = try {
72
+ context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
73
+ } catch (e: Exception) {
74
+ SBLLogger.warn("[install-stamp] failed to read lastUpdateTime: ${e.message}")
75
+ // Mark done even on failure: retrying every call won't help and
76
+ // would defeat the gate. Conservative default is to NOT wipe.
77
+ wipeCheckDone = true
78
+ return
79
+ }
80
+
81
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
82
+ val savedStamp = prefs.getLong(KEY_LAST_INSTALL_STAMP, -1L)
83
+ if (savedStamp != currentStamp) {
84
+ val baseDir = File(context.filesDir, BUILTIN_EXTRACT_DIR)
85
+ if (baseDir.exists()) {
86
+ SBLLogger.info(
87
+ "[install-stamp] changed saved=$savedStamp current=$currentStamp, wiping ${baseDir.absolutePath}"
88
+ )
89
+ // rename-then-delete so the baseDir is gone atomically
90
+ // before we release the lock; waiters never see a half-nuked dir.
91
+ val tomb = File(
92
+ baseDir.parentFile,
93
+ ".${BUILTIN_EXTRACT_DIR}.stale-${System.nanoTime()}"
94
+ )
95
+ if (baseDir.renameTo(tomb)) {
96
+ tomb.deleteRecursively()
97
+ } else {
98
+ baseDir.deleteRecursively()
99
+ }
100
+ } else {
101
+ SBLLogger.info(
102
+ "[install-stamp] first seen current=$currentStamp (no extract dir yet)"
103
+ )
104
+ }
105
+ // Persist synchronously (commit) so that a crash mid-wipe doesn't
106
+ // leave us with a stale stamp + still-wiped dir next launch.
107
+ prefs.edit().putLong(KEY_LAST_INSTALL_STAMP, currentStamp).commit()
108
+ }
109
+ // Publish via volatile write; fast path on other threads now sees true.
110
+ wipeCheckDone = true
111
+ }
112
+ }
113
+
37
114
  // -----------------------------------------------------------------------
38
115
  // getRuntimeBundleContext
39
116
  // -----------------------------------------------------------------------
@@ -41,6 +118,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
41
118
  override fun getRuntimeBundleContext(promise: Promise) {
42
119
  try {
43
120
  val context = reactApplicationContext
121
+ ensureExtractDirFreshForCurrentInstall(context)
44
122
  val runtimeKind = "main"
45
123
  var sourceKind = "builtin"
46
124
  var bundleRoot = ""
@@ -91,6 +169,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
91
169
 
92
170
  override fun resolveSegmentPath(relativePath: String, sha256: String, promise: Promise) {
93
171
  try {
172
+ ensureExtractDirFreshForCurrentInstall(reactApplicationContext)
94
173
  val absolutePath = resolveSegmentPath(relativePath, sha256)
95
174
  if (absolutePath != null) {
96
175
  promise.resolve(absolutePath)
@@ -121,6 +200,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
121
200
  // segment integrity. Builtin segments are signed as part of the APK/IPA.
122
201
  // If runtime SHA-256 verification is needed, add it here.
123
202
  try {
203
+ ensureExtractDirFreshForCurrentInstall(reactApplicationContext)
124
204
  val segId = segmentId.toInt()
125
205
 
126
206
  val absolutePath = resolveSegmentPath(relativePath, sha256)
@@ -132,54 +212,21 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
132
212
  return
133
213
  }
134
214
 
135
- // #19: Try CatalystInstance first (bridge mode), fall back to
136
- // ReactHost registerSegment if available (bridgeless / new arch).
215
+ // Use ReactContext.registerSegment which works in both bridge
216
+ // and bridgeless modes. In bridge mode it delegates to
217
+ // CatalystInstance; in bridgeless mode it delegates to ReactHost.
137
218
  val reactContext = reactApplicationContext
138
219
  val segStart = System.nanoTime()
139
- if (reactContext.hasCatalystInstance()) {
140
- reactContext.catalystInstance.registerSegment(segId, absolutePath)
220
+ reactContext.registerSegment(segId, absolutePath) {
141
221
  val segMs = (System.nanoTime() - segStart) / 1_000_000.0
142
222
  SBLLogger.info("[SplitBundle] segment $segmentKey (id=$segId) registered in ${String.format("%.1f", segMs)}ms")
143
223
  promise.resolve(null)
144
- } else {
145
- // Bridgeless: try ReactHost via reflection
146
- val registered = tryRegisterViaBridgeless(segId, absolutePath)
147
- val segMs = (System.nanoTime() - segStart) / 1_000_000.0
148
- if (registered) {
149
- SBLLogger.info("[SplitBundle] segment $segmentKey (id=$segId) registered via bridgeless in ${String.format("%.1f", segMs)}ms")
150
- promise.resolve(null)
151
- } else {
152
- promise.reject(
153
- "SPLIT_BUNDLE_NO_INSTANCE",
154
- "Neither CatalystInstance nor ReactHost available"
155
- )
156
- }
157
224
  }
158
225
  } catch (e: Exception) {
159
226
  promise.reject("SPLIT_BUNDLE_LOAD_ERROR", e.message, e)
160
227
  }
161
228
  }
162
229
 
163
- // -----------------------------------------------------------------------
164
- // Bridgeless support (#19)
165
- // -----------------------------------------------------------------------
166
-
167
- private fun tryRegisterViaBridgeless(segmentId: Int, path: String): Boolean {
168
- return try {
169
- val appContext = reactApplicationContext.applicationContext
170
- val appClass = appContext.javaClass
171
- val hostMethod = appClass.getMethod("getReactHost")
172
- val host = hostMethod.invoke(appContext) ?: return false
173
- val registerMethod = host.javaClass.getMethod(
174
- "registerSegment", Int::class.java, String::class.java
175
- )
176
- registerMethod.invoke(host, segmentId, path)
177
- true
178
- } catch (_: Exception) {
179
- false
180
- }
181
- }
182
-
183
230
  // -----------------------------------------------------------------------
184
231
  // Path resolution helpers
185
232
  // -----------------------------------------------------------------------
@@ -206,14 +253,27 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
206
253
  val otaRoot = File(otaBundlePath).parentFile
207
254
  if (otaRoot != null) {
208
255
  val candidate = File(otaRoot, relativePath)
209
- if (candidate.exists() && isPathWithinRoot(otaRoot, candidate)) {
256
+ val exists = candidate.exists()
257
+ val withinRoot = isPathWithinRoot(otaRoot, candidate)
258
+ SBLLogger.info("[resolveSeg] rel=$relativePath ota root=${otaRoot.absolutePath} cand=${candidate.absolutePath} exists=$exists withinRoot=$withinRoot")
259
+ if (exists && withinRoot) {
210
260
  return candidate.absolutePath
211
261
  }
262
+ } else {
263
+ SBLLogger.info("[resolveSeg] rel=$relativePath otaBundlePath=$otaBundlePath parentFile=null")
212
264
  }
265
+ } else {
266
+ SBLLogger.info("[resolveSeg] rel=$relativePath otaBundlePath=(empty) — skipping OTA")
213
267
  }
214
268
 
215
269
  // 2. Try builtin: extract from assets if needed
216
- return extractBuiltinSegmentIfNeeded(relativePath, expectedSha256)
270
+ val result = extractBuiltinSegmentIfNeeded(relativePath, expectedSha256)
271
+ if (result == null) {
272
+ SBLLogger.warn("[resolveSeg] rel=$relativePath → null (builtin extract failed)")
273
+ } else {
274
+ SBLLogger.info("[resolveSeg] rel=$relativePath → builtin $result")
275
+ }
276
+ return result
217
277
  }
218
278
 
219
279
  /**
@@ -225,6 +285,7 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
225
285
  */
226
286
  private fun extractBuiltinSegmentIfNeeded(relativePath: String, expectedSha256: String): String? {
227
287
  val context = reactApplicationContext
288
+ ensureExtractDirFreshForCurrentInstall(context)
228
289
  val nativeVersion = try {
229
290
  context.packageManager
230
291
  .getPackageInfo(context.packageName, 0).versionName ?: "unknown"
@@ -272,12 +333,15 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
272
333
  }
273
334
  // Atomic rename prevents partial file observation
274
335
  if (tempFile.renameTo(extractedFile)) {
336
+ SBLLogger.info("[extractBuiltin] extracted $relativePath → ${extractedFile.absolutePath} (${extractedFile.length()} bytes)")
275
337
  extractedFile.absolutePath
276
338
  } else {
339
+ SBLLogger.warn("[extractBuiltin] rename failed for $relativePath: ${tempFile.absolutePath} → ${extractedFile.absolutePath}")
277
340
  tempFile.delete()
278
341
  null
279
342
  }
280
- } catch (_: IOException) {
343
+ } catch (e: IOException) {
344
+ SBLLogger.warn("[extractBuiltin] IOException for $relativePath: ${e.javaClass.simpleName}: ${e.message}")
281
345
  null
282
346
  }
283
347
  } finally {
@@ -130,22 +130,32 @@
130
130
  // 1. Try OTA bundle root first
131
131
  NSString *otaPath = [SplitBundleLoader otaBundlePath];
132
132
  if (otaPath) {
133
- NSString *otaRoot = [otaPath stringByDeletingLastPathComponent];
133
+ // Standardize root so hasPrefix matches candidate (iOS resolves /private/var → /var).
134
+ NSString *otaRoot = [[otaPath stringByDeletingLastPathComponent] stringByStandardizingPath];
134
135
  NSString *candidate = [[otaRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
135
- if ([candidate hasPrefix:otaRoot] &&
136
- [[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
136
+ BOOL otaPrefixOk = [candidate hasPrefix:otaRoot];
137
+ BOOL otaExists = [[NSFileManager defaultManager] fileExistsAtPath:candidate];
138
+ [SBLLogger info:[NSString stringWithFormat:@"[resolveAbs] rel=%@ ota root=%@ cand=%@ prefixOk=%d exists=%d",
139
+ relativePath, otaRoot, candidate, otaPrefixOk, otaExists]];
140
+ if (otaPrefixOk && otaExists) {
137
141
  return candidate;
138
142
  }
143
+ } else {
144
+ [SBLLogger info:[NSString stringWithFormat:@"[resolveAbs] rel=%@ otaPath=(nil) — skipping OTA", relativePath]];
139
145
  }
140
146
 
141
147
  // 2. Fallback to builtin resource path
142
- NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
148
+ NSString *builtinRoot = [[[NSBundle mainBundle] resourcePath] stringByStandardizingPath];
143
149
  NSString *candidate = [[builtinRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
144
- if ([candidate hasPrefix:builtinRoot] &&
145
- [[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
150
+ BOOL builtinPrefixOk = [candidate hasPrefix:builtinRoot];
151
+ BOOL builtinExists = [[NSFileManager defaultManager] fileExistsAtPath:candidate];
152
+ [SBLLogger info:[NSString stringWithFormat:@"[resolveAbs] rel=%@ builtin root=%@ cand=%@ prefixOk=%d exists=%d",
153
+ relativePath, builtinRoot, candidate, builtinPrefixOk, builtinExists]];
154
+ if (builtinPrefixOk && builtinExists) {
146
155
  return candidate;
147
156
  }
148
157
 
158
+ [SBLLogger warn:[NSString stringWithFormat:@"[resolveAbs] rel=%@ → nil (not found in OTA nor builtin)", relativePath]];
149
159
  return nil;
150
160
  }
151
161
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-split-bundle-loader",
3
- "version": "3.0.18",
3
+ "version": "3.0.19",
4
4
  "description": "react-native-split-bundle-loader",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",