@onekeyfe/react-native-range-downloader 6.7.0-alpha.311 → 6.7.0-alpha.313

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.
@@ -100,7 +100,10 @@ class ConcurrentRangeDownloader(
100
100
  * segment files, so no in-flight worker resurrects a deleted file.
101
101
  */
102
102
  class CancelHandle {
103
+ // Only an explicit caller cancellation sets aborted. Worker failures use
104
+ // workerStop to stop sibling segments without changing that intent.
103
105
  val aborted = AtomicBoolean(false)
106
+ internal val workerStop = AtomicBoolean(false)
104
107
 
105
108
  @Volatile
106
109
  private var pool: java.util.concurrent.ExecutorService? = null
@@ -108,7 +111,7 @@ class ConcurrentRangeDownloader(
108
111
  internal fun attach(pool: java.util.concurrent.ExecutorService) {
109
112
  this.pool = pool
110
113
  // If cancel() already raced in before the pool was attached, honor it.
111
- if (aborted.get()) pool.shutdownNow()
114
+ if (workerStop.get()) pool.shutdownNow()
112
115
  }
113
116
 
114
117
  /**
@@ -121,6 +124,7 @@ class ConcurrentRangeDownloader(
121
124
  */
122
125
  fun cancel() {
123
126
  aborted.set(true)
127
+ workerStop.set(true)
124
128
  pool?.let { p ->
125
129
  p.shutdownNow()
126
130
  try {
@@ -221,9 +225,9 @@ class ConcurrentRangeDownloader(
221
225
  )
222
226
  onProgress(transferred.get(), total)
223
227
 
224
- // Share the abort flag with the cancel handle so an external cancel() is
225
- // observed by the per-segment loops; default to a private flag otherwise.
226
- val aborted = cancelHandle?.aborted ?: AtomicBoolean(false)
228
+ // External cancel also stops workers; internal fallback/failure must not
229
+ // mark the caller's explicit-cancellation flag.
230
+ val aborted = cancelHandle?.workerStop ?: AtomicBoolean(false)
227
231
  val fallback = AtomicBoolean(false)
228
232
  val firstError = AtomicReference<Exception?>(null)
229
233
 
@@ -243,11 +247,11 @@ class ConcurrentRangeDownloader(
243
247
  }
244
248
  } catch (e: FallbackException) {
245
249
  fallback.set(true)
246
- aborted.set(true)
247
250
  firstError.compareAndSet(null, e)
248
- } catch (e: Exception) {
249
251
  aborted.set(true)
252
+ } catch (e: Exception) {
250
253
  firstError.compareAndSet(null, e)
254
+ aborted.set(true)
251
255
  }
252
256
  }
253
257
  }
@@ -293,6 +293,67 @@ class ConcurrentRangeDownloaderOcdsTest {
293
293
  assertNoArtifactsLeft(partial)
294
294
  }
295
295
 
296
+ @Test
297
+ fun internalFallbackDoesNotMarkCallerCancelled() {
298
+ val target = parts[2]
299
+ startServer(RangeFaultServer(content).arm(
300
+ RangeFaultServer.FaultMode.STATUS_200, target.start, target.end, times = Int.MAX_VALUE,
301
+ ))
302
+ val handle = ConcurrentRangeDownloader.CancelHandle()
303
+
304
+ val outcome = newDownloader().download(url(), partialPath(), handle) { _, _ -> }
305
+
306
+ assertEquals(ConcurrentRangeDownloader.Outcome.FALLBACK, outcome)
307
+ assertFalse(handle.aborted.get())
308
+ assertNoArtifactsLeft(partialPath())
309
+ }
310
+
311
+ @Test
312
+ fun segment403PreservesHttpStatusWithoutMarkingCallerCancelled() {
313
+ val target = parts[2]
314
+ val normal = RangeFaultServer(content)
315
+ server = MockWebServer()
316
+ server.dispatcher = object : Dispatcher() {
317
+ override fun dispatch(request: RecordedRequest): MockResponse =
318
+ if (request.getHeader("Range") == "bytes=${target.start}-${target.end}") {
319
+ MockResponse().setResponseCode(403)
320
+ } else {
321
+ normal.dispatcher.dispatch(request)
322
+ }
323
+ }
324
+ server.start()
325
+ val handle = ConcurrentRangeDownloader.CancelHandle()
326
+
327
+ val failure = runCatching {
328
+ newDownloader().download(url(), partialPath(), handle) { _, _ -> }
329
+ }.exceptionOrNull()
330
+
331
+ assertTrue(failure is ConcurrentRangeDownloader.PermanentHttpException)
332
+ assertEquals(403, (failure as ConcurrentRangeDownloader.PermanentHttpException).code)
333
+ assertFalse(handle.aborted.get())
334
+ }
335
+
336
+ @Test
337
+ fun exhaustedSocketTimeoutDoesNotMarkCallerCancelled() {
338
+ val target = parts[2]
339
+ startServer(RangeFaultServer(content).arm(
340
+ RangeFaultServer.FaultMode.STALL, target.start, target.end, times = Int.MAX_VALUE,
341
+ ))
342
+ val handle = ConcurrentRangeDownloader.CancelHandle()
343
+ val client = OkHttpClient.Builder().readTimeout(200, TimeUnit.MILLISECONDS).build()
344
+ val downloader = ConcurrentRangeDownloader(client, maxPartRetry = 0)
345
+
346
+ val failure = runCatching {
347
+ downloader.download(url(), partialPath(), handle) { _, _ -> }
348
+ }.exceptionOrNull()
349
+
350
+ assertTrue(
351
+ "expected a socket timeout, got ${failure?.javaClass?.name}: ${failure?.message}",
352
+ failure is java.net.SocketTimeoutException,
353
+ )
354
+ assertFalse(handle.aborted.get())
355
+ }
356
+
296
357
  // ------------------------------------------------------------------------
297
358
  // OCDS-T4: 429 (+Retry-After: 0) once on a segment → transient retry succeeds.
298
359
  // SHA correct; the affected segment requested twice; no from-0 restart.
@@ -532,6 +593,7 @@ class ConcurrentRangeDownloaderOcdsTest {
532
593
  worker.shutdownNow()
533
594
 
534
595
  assertTrue("cancel must abort promptly (took ${elapsed}ms)", elapsed < 12_000)
596
+ assertTrue("explicit cancellation must mark caller intent", handle.aborted.get())
535
597
 
536
598
  // cancel-then-delete: now that workers are terminated, deleting the artifacts
537
599
  // must STICK — no straggler worker resurrects a `.segN`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-range-downloader",
3
- "version": "6.7.0-alpha.311",
3
+ "version": "6.7.0-alpha.313",
4
4
  "description": "react-native-range-downloader",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",