@onekeyfe/react-native-bundle-update 3.0.65 → 3.0.66

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.
@@ -20,6 +20,7 @@ import java.nio.file.Path
20
20
  import java.nio.file.Paths
21
21
  import java.util.concurrent.CopyOnWriteArrayList
22
22
  import java.util.concurrent.atomic.AtomicBoolean
23
+ import java.util.concurrent.atomic.AtomicInteger
23
24
  import java.util.concurrent.atomic.AtomicLong
24
25
  import java.util.zip.ZipEntry
25
26
  import java.util.zip.ZipInputStream
@@ -1083,6 +1084,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1083
1084
 
1084
1085
  companion object {
1085
1086
  private const val PREFS_NAME = "BundleUpdatePrefs"
1087
+
1088
+ // Number of concurrent segment files (`<partial>.seg0..seg{N-1}`) the
1089
+ // ConcurrentRangeDownloader produces. MUST equal the segmentCount passed
1090
+ // to ConcurrentRangeDownloader (currently the default 8). Every place
1091
+ // that cleans up `.segN` files must iterate `0 until CONCURRENT_SEGMENT_COUNT`.
1092
+ private const val CONCURRENT_SEGMENT_COUNT = 8
1086
1093
  }
1087
1094
 
1088
1095
  private val listeners = CopyOnWriteArrayList<BundleListener>()
@@ -1234,9 +1241,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1234
1241
  if (verifyBundleSHA256(filePath, sha256)) {
1235
1242
  OneKeyLog.info("BundleUpdate", "downloadBundle: existing file SHA256 valid, skipping download")
1236
1243
  // Final file is authoritative — drop any stale concurrent
1237
- // partial/manifest left by an earlier interrupted attempt.
1244
+ // partial + segment files left by an earlier interrupted
1245
+ // attempt. (".progress" is a legacy manifest from the old
1246
+ // pre-allocated model; deleting it is a harmless no-op now.)
1238
1247
  if (partialFile.exists()) partialFile.delete()
1239
1248
  File("$partialFilePath.progress").delete()
1249
+ for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1240
1250
  // Keep isDownloading held across the skip delay below. Clearing
1241
1251
  // it before the sleep opens a ~1s window where a second
1242
1252
  // downloadBundle could pass the getAndSet guard and run
@@ -1248,8 +1258,11 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1248
1258
  } else {
1249
1259
  OneKeyLog.warn("BundleUpdate", "downloadBundle: existing file SHA256 mismatch, re-downloading")
1250
1260
  downloadedFile.delete()
1251
- // Stale completed file invalidates any partial too.
1261
+ // Stale completed file invalidates any partial too — including
1262
+ // the concurrent segment files, otherwise the next resume would
1263
+ // pick up bytes belonging to the rejected build.
1252
1264
  if (partialFile.exists()) partialFile.delete()
1265
+ for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1253
1266
  }
1254
1267
  }
1255
1268
 
@@ -1261,16 +1274,22 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1261
1274
  // downloader keeps its partial + manifest for resume).
1262
1275
  sendEvent("update/start")
1263
1276
  run {
1264
- var concurrentProgress = -1
1277
+ // onProgress is invoked concurrently by all 8 worker threads, so
1278
+ // a plain `var` read-compare-write races (duplicate/out-of-order
1279
+ // progress events). Use AtomicInteger + CAS: only the thread that
1280
+ // wins the compareAndSet to a strictly higher value emits, which
1281
+ // also keeps progress monotonic. (Worst case a race only affects
1282
+ // progress eventing, never file bytes.)
1283
+ val concurrentProgress = AtomicInteger(-1)
1265
1284
  val concurrentOutcome = ConcurrentRangeDownloader(
1266
1285
  httpClient = httpClient,
1267
1286
  log = { msg -> OneKeyLog.info("BundleUpdate", msg) },
1268
1287
  ).download(downloadUrl, partialFilePath) { transferred, total ->
1269
1288
  if (total > 0) {
1270
1289
  val p = ((transferred * 100) / total).toInt().coerceIn(0, 100)
1271
- if (p != concurrentProgress) {
1290
+ val prev = concurrentProgress.get()
1291
+ if (p > prev && concurrentProgress.compareAndSet(prev, p)) {
1272
1292
  sendEvent("update/downloading", progress = p)
1273
- concurrentProgress = p
1274
1293
  }
1275
1294
  }
1276
1295
  }
@@ -1304,7 +1323,14 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1304
1323
  // before discarding so we save a full re-download.
1305
1324
  val expectedSize = if (params.fileSize > 0) params.fileSize.toLong() else 0L
1306
1325
  var partialBytes = 0L
1307
- if (partialFile.exists()) {
1326
+ // If any concurrent `.segN` files survive, the `.partial` here is the
1327
+ // concurrent committed cursor, not a single-stream partial. Defer to
1328
+ // the concurrent downloader (which already ran above and may resume on
1329
+ // a later attempt) and skip size-based promote/discard, which would
1330
+ // otherwise misjudge a bare `.partial` when the concurrent path
1331
+ // returned FALLBACK but left `.segN` residue. Mirrors app-update.
1332
+ val hasConcurrentSegments = (0 until CONCURRENT_SEGMENT_COUNT).any { File("$partialFilePath.seg$it").exists() }
1333
+ if (partialFile.exists() && !hasConcurrentSegments) {
1308
1334
  val partialSize = partialFile.length()
1309
1335
  when {
1310
1336
  expectedSize > 0 && partialSize == expectedSize -> {
@@ -1325,6 +1351,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1325
1351
  expectedSize > 0 && partialSize > expectedSize -> {
1326
1352
  OneKeyLog.warn("BundleUpdate", "downloadBundle: stale partial (>expected), discarding: $partialSize/$expectedSize")
1327
1353
  partialFile.delete()
1354
+ for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1328
1355
  }
1329
1356
  partialSize > 0 -> {
1330
1357
  partialBytes = partialSize
@@ -1369,6 +1396,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1369
1396
  }
1370
1397
  OneKeyLog.warn("BundleUpdate", "downloadBundle: HTTP 416 (range not satisfiable), discarding partial and failing this attempt")
1371
1398
  if (partialFile.exists()) partialFile.delete()
1399
+ for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1372
1400
  // Don't pre-emit update/error here; the outer catch is the
1373
1401
  // single source of error events. sanitizeErrorMessageForEvent
1374
1402
  // recognizes "HTTP " prefix and forwards this string verbatim.
@@ -1376,7 +1404,7 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1376
1404
  }
1377
1405
 
1378
1406
  val expectsResume = partialBytes > 0
1379
- val isPartialResponse = response.code == 206
1407
+ var isPartialResponse = response.code == 206
1380
1408
 
1381
1409
  if (!response.isSuccessful || (response.code != 200 && response.code != 206)) {
1382
1410
  OneKeyLog.error("BundleUpdate", "downloadBundle: HTTP error, statusCode=${response.code}")
@@ -1390,9 +1418,36 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1390
1418
  if (expectsResume && !isPartialResponse) {
1391
1419
  OneKeyLog.warn("BundleUpdate", "downloadBundle: requested Range but server returned 200, restarting from scratch")
1392
1420
  if (partialFile.exists()) partialFile.delete()
1421
+ for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1393
1422
  partialBytes = 0L
1394
1423
  }
1395
1424
 
1425
+ // On a 206 the server's `Content-Range` start MUST equal the offset
1426
+ // we asked to resume from (`partialBytes`). A misconfigured server or
1427
+ // proxy can return a 206 whose range starts somewhere else; appending
1428
+ // that slice onto our `.partial` would splice mismatched bytes and the
1429
+ // final SHA256 would fail only after a full download. Guard here: if
1430
+ // the start is missing or != partialBytes, drop the partial+segments
1431
+ // and abort this attempt. We must NOT reuse this body as a 200-style
1432
+ // full rewrite: a mismatched 206 body is still a range slice, not the
1433
+ // whole file, so writing it would produce a corrupt bundle. Close the
1434
+ // response and throw a retryable error — with partial+segN already
1435
+ // gone, the next attempt naturally restarts from 0.
1436
+ if (isPartialResponse && partialBytes > 0) {
1437
+ val contentRangeStart = response.header("Content-Range")
1438
+ ?.let { Regex("""bytes\s+(\d+)-\d+/\d+""").find(it)?.groupValues?.getOrNull(1)?.toLongOrNull() }
1439
+ if (contentRangeStart == null || contentRangeStart != partialBytes) {
1440
+ OneKeyLog.warn(
1441
+ "BundleUpdate",
1442
+ "downloadBundle: 206 Content-Range start=$contentRangeStart != partialBytes=$partialBytes, discarding partial and aborting attempt"
1443
+ )
1444
+ if (partialFile.exists()) partialFile.delete()
1445
+ for (i in 0 until CONCURRENT_SEGMENT_COUNT) File("$partialFilePath.seg$i").delete()
1446
+ response.close()
1447
+ throw java.io.IOException("206 Content-Range start mismatch (got=$contentRangeStart, want=$partialBytes); discarded partial, retry from scratch")
1448
+ }
1449
+ }
1450
+
1396
1451
  // Close the response before throwing on a null body — OkHttp
1397
1452
  // holds connection resources on the response wrapper itself,
1398
1453
  // and `throw` here exits the function before any byteStream()
@@ -1844,8 +1899,12 @@ class ReactNativeBundleUpdate : HybridReactNativeBundleUpdateSpec() {
1844
1899
  downloadDir.listFiles()?.forEach { file ->
1845
1900
  val name = file.name
1846
1901
  // Strip the trailing extension chain to recover the
1847
- // "{appV}-{bV}" stem (e.g. "6.3.0-123.zip.partial").
1902
+ // "{appV}-{bV}" stem (e.g. "6.3.0-123.zip.partial", or a
1903
+ // concurrent segment file "6.3.0-123.zip.partial.seg3").
1848
1904
  var stem = name
1905
+ // Concurrent segment files end in ".segN" — peel that off
1906
+ // first so the rest of the chain strips as usual.
1907
+ stem = stem.replace(Regex("""\.seg\d+$"""), "")
1849
1908
  for (suffix in listOf(".resume", ".progress", ".partial", ".zip")) {
1850
1909
  if (stem.endsWith(suffix)) {
1851
1910
  stem = stem.substring(0, stem.length - suffix.length)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-bundle-update",
3
- "version": "3.0.65",
3
+ "version": "3.0.66",
4
4
  "description": "react-native-bundle-update",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",