@onekeyfe/react-native-range-downloader 3.0.81-alpha.1 → 3.0.81-alpha.3

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.
Files changed (23) hide show
  1. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactStore.kt +59 -127
  2. package/android/src/main/java/com/margelo/nitro/reactnativerangedownloader/ReactNativeRangeDownloader.kt +16 -20
  3. package/android/src/test/java/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactDeadlineTest.kt +26 -0
  4. package/ios/FirmwareArtifactStore.swift +125 -170
  5. package/ios/RangeDownloadLogic.swift +71 -0
  6. package/ios/ReactNativeRangeDownloader.swift +22 -60
  7. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts +0 -4
  8. package/lib/typescript/src/ReactNativeRangeDownloader.nitro.d.ts.map +1 -1
  9. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.cpp +0 -19
  10. package/nitrogen/generated/android/c++/JHybridReactNativeRangeDownloaderSpec.hpp +0 -1
  11. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/HybridReactNativeRangeDownloaderSpec.kt +0 -4
  12. package/nitrogen/generated/ios/ReactNativeRangeDownloader-Swift-Cxx-Umbrella.hpp +0 -3
  13. package/nitrogen/generated/ios/c++/HybridReactNativeRangeDownloaderSpecSwift.hpp +0 -11
  14. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec.swift +0 -1
  15. package/nitrogen/generated/ios/swift/HybridReactNativeRangeDownloaderSpec_cxx.swift +0 -19
  16. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.cpp +0 -1
  17. package/nitrogen/generated/shared/c++/HybridReactNativeRangeDownloaderSpec.hpp +0 -4
  18. package/package.json +2 -2
  19. package/src/ReactNativeRangeDownloader.nitro.ts +1 -8
  20. package/nitrogen/generated/android/c++/JFirmwareArtifactLeaseReconcileParams.hpp +0 -76
  21. package/nitrogen/generated/android/kotlin/com/margelo/nitro/reactnativerangedownloader/FirmwareArtifactLeaseReconcileParams.kt +0 -38
  22. package/nitrogen/generated/ios/swift/FirmwareArtifactLeaseReconcileParams.swift +0 -48
  23. package/nitrogen/generated/shared/c++/FirmwareArtifactLeaseReconcileParams.hpp +0 -76
@@ -6,7 +6,6 @@ import com.sniconnect.SniPinnedTransport
6
6
  import java.io.BufferedInputStream
7
7
  import java.io.File
8
8
  import java.io.FileInputStream
9
- import java.io.FileOutputStream
10
9
  import java.io.IOException
11
10
  import java.io.RandomAccessFile
12
11
  import java.security.MessageDigest
@@ -14,14 +13,29 @@ import java.util.UUID
14
13
  import java.util.concurrent.ConcurrentHashMap
15
14
  import java.util.concurrent.TimeUnit
16
15
  import java.util.zip.ZipInputStream
16
+ import javax.net.ssl.SSLException
17
+ import kotlin.math.ceil
17
18
  import okhttp3.Call
18
19
  import okhttp3.OkHttpClient
19
20
  import okhttp3.Protocol
20
21
  import okhttp3.Request
21
22
  import okhttp3.Response
22
23
  import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
23
- import org.json.JSONArray
24
- import org.json.JSONObject
24
+
25
+ private const val DEFAULT_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS = 180.0
26
+ private const val MAX_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS = 24.0 * 60 * 60
27
+
28
+ internal fun validateFirmwareDownloadDeadlineSeconds(value: Double?): Double {
29
+ val deadline = value ?: DEFAULT_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS
30
+ require(
31
+ deadline.isFinite() &&
32
+ deadline > 0 &&
33
+ deadline <= MAX_FIRMWARE_DOWNLOAD_DEADLINE_SECONDS
34
+ ) {
35
+ "Invalid firmware download deadline"
36
+ }
37
+ return deadline
38
+ }
25
39
 
26
40
  internal data class StoredFirmwareArtifact(
27
41
  val artifactRef: String,
@@ -43,23 +57,26 @@ private data class StagedFirmwareArchiveEntry(
43
57
  )
44
58
 
45
59
  private data class FirmwareDownloadKey(
46
- val transactionId: String,
47
60
  val expectedSha256: String,
48
61
  )
49
62
 
63
+ private class FirmwareDownloadLock {
64
+ val monitor = Any()
65
+ var references = 0
66
+ }
67
+
50
68
  internal object FirmwareArtifactStore {
51
69
  const val MAX_READ_BYTES = 256 * 1024
52
70
 
53
71
  private const val MAX_ARTIFACT_BYTES = 512L * 1024 * 1024
54
- private const val MAX_LEASE_METADATA_BYTES = 1024L * 1024
55
- private const val MAX_TOTAL_LEASE_REFS = 8192
56
72
  private const val FINAL_ARTIFACT_GRACE_MS = 24L * 60 * 60 * 1000
57
73
  private const val PARTIAL_ARTIFACT_GRACE_MS = 7L * 24 * 60 * 60 * 1000
58
74
  private val sha256Pattern = Regex("^[a-fA-F0-9]{64}$")
59
75
  private val artifactRefPattern = Regex("^fw:[a-f0-9]{64}$")
60
76
  private val leaseRefPattern = Regex("^fwlease:[a-f0-9-]{36}$")
61
77
  private val identifierPattern = Regex("^[A-Za-z0-9._:-]{1,160}$")
62
- private val downloadLocks = ConcurrentHashMap<FirmwareDownloadKey, Any>()
78
+ private val downloadLocks =
79
+ ConcurrentHashMap<FirmwareDownloadKey, FirmwareDownloadLock>()
63
80
  private val activeCalls =
64
81
  ConcurrentHashMap<String, MutableSet<Call>>()
65
82
  private val cancelledTransactions =
@@ -69,6 +86,7 @@ internal object FirmwareArtifactStore {
69
86
  private val leaseLock = Any()
70
87
  private val readerLock = Any()
71
88
  private val readers = mutableMapOf<String, OpenReader>()
89
+ private val leases = mutableMapOf<String, LeaseState>()
72
90
 
73
91
  private data class LeaseState(
74
92
  val transactionId: String,
@@ -102,14 +120,15 @@ internal object FirmwareArtifactStore {
102
120
  transactionId = params.transactionId,
103
121
  artifactRef = "fw:${validated.expectedSha256}",
104
122
  )
105
- val lockKey = FirmwareDownloadKey(
106
- params.transactionId,
107
- validated.expectedSha256,
108
- )
109
- val lock = downloadLocks.computeIfAbsent(lockKey) { Any() }
123
+ val lockKey = FirmwareDownloadKey(validated.expectedSha256)
124
+ val downloadLock = downloadLocks.compute(lockKey) { _, current ->
125
+ (current ?: FirmwareDownloadLock()).also {
126
+ it.references += 1
127
+ }
128
+ } ?: error("Firmware artifact lock is unavailable")
110
129
  markDownloadActive(validated.expectedSha256, 1)
111
130
  try {
112
- return synchronized(lock) {
131
+ return synchronized(downloadLock.monitor) {
113
132
  check(!cancelledTransactions.contains(params.transactionId)) {
114
133
  "ARTIFACT_CANCELLED: firmware artifact download was cancelled"
115
134
  }
@@ -117,6 +136,14 @@ internal object FirmwareArtifactStore {
117
136
  }
118
137
  } finally {
119
138
  markDownloadActive(validated.expectedSha256, -1)
139
+ downloadLocks.compute(lockKey) { _, current ->
140
+ if (current !== downloadLock) {
141
+ current
142
+ } else {
143
+ current.references -= 1
144
+ current.takeIf { it.references > 0 }
145
+ }
146
+ }
120
147
  }
121
148
  }
122
149
 
@@ -133,7 +160,7 @@ internal object FirmwareArtifactStore {
133
160
  fun discard(artifactRef: String) {
134
161
  val file = resolveArtifactFile(artifactRef)
135
162
  synchronized(leaseLock) {
136
- require(loadLeasesLocked().values.none { artifactRef in it.artifactRefs }) {
163
+ require(leases.values.none { artifactRef in it.artifactRefs }) {
137
164
  "ARTIFACT_LEASED: firmware artifact is retained"
138
165
  }
139
166
  }
@@ -284,6 +311,7 @@ internal object FirmwareArtifactStore {
284
311
  val maxBytes: Long,
285
312
  val expectedSha256: String,
286
313
  val hostname: String,
314
+ val overallDeadlineSeconds: Double,
287
315
  )
288
316
 
289
317
  private fun validateDownloadParams(
@@ -324,6 +352,8 @@ internal object FirmwareArtifactStore {
324
352
  require(sha256Pattern.matches(params.expectedSha256)) {
325
353
  "Invalid firmware artifact SHA-256"
326
354
  }
355
+ val overallDeadlineSeconds =
356
+ validateFirmwareDownloadDeadlineSeconds(params.overallDeadlineSeconds)
327
357
  require(params.routeType == "domain" || params.routeType == "pinnedIp") {
328
358
  "Invalid firmware route type"
329
359
  }
@@ -341,6 +371,7 @@ internal object FirmwareArtifactStore {
341
371
  maxBytes = maxBytes,
342
372
  expectedSha256 = params.expectedSha256.lowercase(),
343
373
  hostname = url.host,
374
+ overallDeadlineSeconds = overallDeadlineSeconds,
344
375
  )
345
376
  }
346
377
 
@@ -400,12 +431,10 @@ internal object FirmwareArtifactStore {
400
431
  .build()
401
432
  }
402
433
  val call = client.newCall(requestBuilder.build())
403
- params.overallDeadlineSeconds?.let { deadline ->
404
- require(deadline.isFinite() && deadline > 0) {
405
- "Invalid firmware download deadline"
406
- }
407
- call.timeout().timeout(deadline.toLong().coerceAtLeast(1), TimeUnit.SECONDS)
408
- }
434
+ call.timeout().timeout(
435
+ ceil(validated.overallDeadlineSeconds * 1000).toLong(),
436
+ TimeUnit.MILLISECONDS,
437
+ )
409
438
 
410
439
  registerCall(params.transactionId, call)
411
440
  try {
@@ -428,6 +457,12 @@ internal object FirmwareArtifactStore {
428
457
  error,
429
458
  )
430
459
  }
460
+ if (generateSequence<Throwable>(error) { it.cause }.any { it is SSLException }) {
461
+ throw IllegalStateException(
462
+ "ARTIFACT_TLS_FAILED: firmware TLS validation failed",
463
+ error,
464
+ )
465
+ }
431
466
  throw IllegalStateException(
432
467
  "ARTIFACT_NETWORK_FAILED: firmware request failed",
433
468
  error,
@@ -563,13 +598,11 @@ internal object FirmwareArtifactStore {
563
598
  "Invalid firmware transactionId"
564
599
  }
565
600
  return synchronized(leaseLock) {
566
- val leases = loadLeasesLocked()
567
601
  require(leases.size < 32) {
568
602
  "Too many firmware artifact leases"
569
603
  }
570
604
  val leaseRef = "fwlease:${UUID.randomUUID()}"
571
605
  leases[leaseRef] = LeaseState(transactionId, mutableSetOf())
572
- saveLeasesLocked(leases)
573
606
  leaseRef
574
607
  }
575
608
  }
@@ -592,41 +625,18 @@ internal object FirmwareArtifactStore {
592
625
  "Invalid firmware lease disposition"
593
626
  }
594
627
  val transactionId = synchronized(leaseLock) {
595
- val leases = loadLeasesLocked()
596
628
  val removed = leases.remove(validateLeaseRef(leaseRef))
597
629
  require(removed != null) {
598
630
  "Firmware artifact lease is unavailable"
599
631
  }
600
- saveLeasesLocked(leases)
601
632
  removed.transactionId
602
633
  }
603
634
  cancelledTransactions.remove(transactionId)
604
- downloadLocks.keys.removeIf { it.transactionId == transactionId }
605
- }
606
-
607
- fun reconcileLeases(activeLeaseRefs: Array<String>) {
608
- require(activeLeaseRefs.size <= 32) {
609
- "Too many active firmware artifact leases"
610
- }
611
- val active = activeLeaseRefs.mapTo(mutableSetOf()) {
612
- validateLeaseRef(it)
613
- }
614
- require(active.size == activeLeaseRefs.size) {
615
- "Duplicate active firmware artifact lease"
616
- }
617
- synchronized(leaseLock) {
618
- val leases = loadLeasesLocked()
619
- require(active.all { leases.containsKey(it) }) {
620
- "Firmware artifact lease reconciliation is incomplete"
621
- }
622
- leases.keys.retainAll(active)
623
- saveLeasesLocked(leases)
624
- }
625
635
  }
626
636
 
627
637
  fun sweepOrphans(): Pair<Int, Long> {
628
638
  val retainedSha256 = synchronized(leaseLock) {
629
- loadLeasesLocked().values
639
+ leases.values
630
640
  .flatMap { it.artifactRefs }
631
641
  .mapTo(mutableSetOf()) { it.removePrefix("fw:") }
632
642
  }
@@ -640,7 +650,7 @@ internal object FirmwareArtifactStore {
640
650
  var deletedFiles = 0
641
651
  var deletedBytes = 0L
642
652
  root.listFiles()?.forEach { file ->
643
- if (!file.isFile || file.name == "leases.json") return@forEach
653
+ if (!file.isFile) return@forEach
644
654
  val sha256 = file.name.take(64)
645
655
  if (
646
656
  !sha256Pattern.matches(sha256) ||
@@ -669,7 +679,7 @@ internal object FirmwareArtifactStore {
669
679
 
670
680
  private fun requireLease(leaseRef: String) {
671
681
  synchronized(leaseLock) {
672
- require(loadLeasesLocked().containsKey(validateLeaseRef(leaseRef))) {
682
+ require(leases.containsKey(validateLeaseRef(leaseRef))) {
673
683
  "Firmware artifact lease is unavailable"
674
684
  }
675
685
  }
@@ -684,7 +694,6 @@ internal object FirmwareArtifactStore {
684
694
  "Invalid firmware artifactRef"
685
695
  }
686
696
  synchronized(leaseLock) {
687
- val leases = loadLeasesLocked()
688
697
  val lease = leases[validateLeaseRef(leaseRef)]
689
698
  ?: error("Firmware artifact lease is unavailable")
690
699
  if (transactionId != null) {
@@ -692,9 +701,7 @@ internal object FirmwareArtifactStore {
692
701
  "Firmware artifact lease transaction mismatch"
693
702
  }
694
703
  }
695
- if (lease.artifactRefs.add(artifactRef)) {
696
- saveLeasesLocked(leases)
697
- }
704
+ lease.artifactRefs.add(artifactRef)
698
705
  }
699
706
  }
700
707
 
@@ -705,81 +712,6 @@ internal object FirmwareArtifactStore {
705
712
  return leaseRef
706
713
  }
707
714
 
708
- private fun loadLeasesLocked(): MutableMap<String, LeaseState> {
709
- val file = File(root, "leases.json")
710
- if (!file.exists()) return mutableMapOf()
711
- require(file.length() in 1..MAX_LEASE_METADATA_BYTES) {
712
- "Firmware lease metadata is too large"
713
- }
714
- val envelope = JSONObject(file.readText(Charsets.UTF_8))
715
- require(envelope.optInt("schemaVersion") == 1) {
716
- "Unsupported firmware lease schema"
717
- }
718
- val result = mutableMapOf<String, LeaseState>()
719
- val jsonLeases = envelope.getJSONObject("leases")
720
- val keys = jsonLeases.keys()
721
- while (keys.hasNext()) {
722
- val leaseRef = validateLeaseRef(keys.next())
723
- val jsonLease = jsonLeases.getJSONObject(leaseRef)
724
- val transactionId = jsonLease.getString("transactionId")
725
- require(identifierPattern.matches(transactionId)) {
726
- "Invalid persisted firmware transactionId"
727
- }
728
- val jsonRefs = jsonLease.getJSONArray("artifactRefs")
729
- require(jsonRefs.length() <= 4096) {
730
- "Too many persisted firmware artifact refs"
731
- }
732
- val refs = mutableSetOf<String>()
733
- for (index in 0 until jsonRefs.length()) {
734
- val artifactRef = jsonRefs.getString(index)
735
- require(artifactRefPattern.matches(artifactRef) && refs.add(artifactRef)) {
736
- "Invalid persisted firmware artifact ref"
737
- }
738
- }
739
- result[leaseRef] = LeaseState(transactionId, refs)
740
- }
741
- require(result.size <= 32) {
742
- "Too many persisted firmware artifact leases"
743
- }
744
- require(result.values.sumOf { it.artifactRefs.size } <= MAX_TOTAL_LEASE_REFS) {
745
- "Too many persisted firmware artifact refs"
746
- }
747
- return result
748
- }
749
-
750
- private fun saveLeasesLocked(leases: Map<String, LeaseState>) {
751
- require(
752
- leases.size <= 32 &&
753
- leases.values.sumOf { it.artifactRefs.size } <= MAX_TOTAL_LEASE_REFS
754
- ) {
755
- "Firmware lease metadata is too large"
756
- }
757
- val jsonLeases = JSONObject()
758
- leases.toSortedMap().forEach { (leaseRef, lease) ->
759
- jsonLeases.put(
760
- leaseRef,
761
- JSONObject()
762
- .put("transactionId", lease.transactionId)
763
- .put("artifactRefs", JSONArray(lease.artifactRefs.sorted())),
764
- )
765
- }
766
- val bytes = JSONObject()
767
- .put("schemaVersion", 1)
768
- .put("leases", jsonLeases)
769
- .toString()
770
- .toByteArray(Charsets.UTF_8)
771
- require(bytes.size.toLong() <= MAX_LEASE_METADATA_BYTES) {
772
- "Firmware lease metadata is too large"
773
- }
774
- val destination = File(root, "leases.json")
775
- val temporary = File(root, ".leases-${UUID.randomUUID()}.tmp")
776
- FileOutputStream(temporary).use { output ->
777
- output.write(bytes)
778
- output.fd.sync()
779
- }
780
- Os.rename(temporary.absolutePath, destination.absolutePath)
781
- }
782
-
783
715
  private fun markDownloadActive(sha256: String, delta: Int) {
784
716
  synchronized(activeDownloadLock) {
785
717
  val count = (activeDownloadCounts[sha256] ?: 0) + delta
@@ -9,6 +9,9 @@ import java.io.File
9
9
  import java.security.MessageDigest
10
10
  import java.util.concurrent.CopyOnWriteArrayList
11
11
  import java.util.concurrent.atomic.AtomicLong
12
+ import kotlinx.coroutines.CoroutineScope
13
+ import kotlinx.coroutines.Dispatchers
14
+ import kotlinx.coroutines.SupervisorJob
12
15
 
13
16
  // P1: Nitro adapter for the Android concurrent multi-range downloader.
14
17
  //
@@ -34,6 +37,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
34
37
 
35
38
  private val listeners = CopyOnWriteArrayList<Listener>()
36
39
  private val nextListenerId = AtomicLong(1)
40
+ private val firmwareArtifactScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
37
41
 
38
42
  // Active downloads keyed by "channel|taskId" so cancel/discardArtifacts can
39
43
  // flip the abort flag + stop the worker pool BEFORE deleting files, instead of
@@ -268,7 +272,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
268
272
 
269
273
  override fun getFirmwareArtifactCapabilities(): FirmwareArtifactCapabilities {
270
274
  return FirmwareArtifactCapabilities(
271
- firmwareArtifactProtocolVersion = 1.0,
275
+ firmwareArtifactProtocolVersion = 2.0,
272
276
  supportedRouteTypes = arrayOf("domain", "pinnedIp"),
273
277
  supportsArchiveMaterialization = true,
274
278
  maxReadBytes = FirmwareArtifactStore.MAX_READ_BYTES.toDouble(),
@@ -278,7 +282,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
278
282
  override fun downloadFirmwareArtifact(
279
283
  params: FirmwareArtifactDownloadParams,
280
284
  ): Promise<FirmwareArtifactReceipt> {
281
- return Promise.async {
285
+ return Promise.async(firmwareArtifactScope) {
282
286
  val artifact = FirmwareArtifactStore.download(params)
283
287
  FirmwareArtifactReceipt(
284
288
  artifactRef = artifact.artifactRef,
@@ -291,7 +295,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
291
295
  override fun cancelFirmwareArtifactDownloads(
292
296
  params: FirmwareArtifactCancelParams,
293
297
  ): Promise<Unit> {
294
- return Promise.async {
298
+ return Promise.async(firmwareArtifactScope) {
295
299
  FirmwareArtifactStore.cancelDownloads(params.transactionId)
296
300
  }
297
301
  }
@@ -299,7 +303,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
299
303
  override fun discardFirmwareArtifact(
300
304
  params: FirmwareArtifactRefParams,
301
305
  ): Promise<Unit> {
302
- return Promise.async {
306
+ return Promise.async(firmwareArtifactScope) {
303
307
  FirmwareArtifactStore.discard(params.artifactRef)
304
308
  }
305
309
  }
@@ -307,7 +311,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
307
311
  override fun openFirmwareArtifact(
308
312
  params: FirmwareArtifactRefParams,
309
313
  ): Promise<FirmwareArtifactReaderInfo> {
310
- return Promise.async {
314
+ return Promise.async(firmwareArtifactScope) {
311
315
  val (readerId, size) = FirmwareArtifactStore.open(params.artifactRef)
312
316
  FirmwareArtifactReaderInfo(readerId = readerId, size = size.toDouble())
313
317
  }
@@ -316,7 +320,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
316
320
  override fun readFirmwareArtifact(
317
321
  params: FirmwareArtifactReaderReadParams,
318
322
  ): Promise<ArrayBuffer> {
319
- return Promise.async {
323
+ return Promise.async(firmwareArtifactScope) {
320
324
  require(
321
325
  params.offset.isFinite() &&
322
326
  params.offset >= 0 &&
@@ -340,7 +344,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
340
344
  override fun closeFirmwareArtifact(
341
345
  params: FirmwareArtifactReaderCloseParams,
342
346
  ): Promise<Unit> {
343
- return Promise.async {
347
+ return Promise.async(firmwareArtifactScope) {
344
348
  FirmwareArtifactStore.close(params.readerId)
345
349
  }
346
350
  }
@@ -348,7 +352,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
348
352
  override fun materializeFirmwareArchive(
349
353
  params: FirmwareArchiveMaterializeParams,
350
354
  ): Promise<FirmwareArchiveMaterializeResult> {
351
- return Promise.async {
355
+ return Promise.async(firmwareArtifactScope) {
352
356
  val artifacts = FirmwareArtifactStore.materializeArchive(
353
357
  params.leaseRef,
354
358
  params.archiveArtifactRef,
@@ -372,7 +376,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
372
376
  override fun createFirmwareArtifactLease(
373
377
  params: FirmwareArtifactLeaseCreateParams,
374
378
  ): Promise<FirmwareArtifactLease> {
375
- return Promise.async {
379
+ return Promise.async(firmwareArtifactScope) {
376
380
  FirmwareArtifactLease(
377
381
  leaseRef = FirmwareArtifactStore.createLease(params.transactionId),
378
382
  )
@@ -382,7 +386,7 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
382
386
  override fun retainFirmwareArtifact(
383
387
  params: FirmwareArtifactLeaseRetainParams,
384
388
  ): Promise<Unit> {
385
- return Promise.async {
389
+ return Promise.async(firmwareArtifactScope) {
386
390
  FirmwareArtifactStore.retain(params.leaseRef, params.artifactRef)
387
391
  }
388
392
  }
@@ -390,21 +394,13 @@ class ReactNativeRangeDownloader : HybridReactNativeRangeDownloaderSpec() {
390
394
  override fun releaseFirmwareArtifactLease(
391
395
  params: FirmwareArtifactLeaseReleaseParams,
392
396
  ): Promise<Unit> {
393
- return Promise.async {
397
+ return Promise.async(firmwareArtifactScope) {
394
398
  FirmwareArtifactStore.releaseLease(params.leaseRef, params.disposition)
395
399
  }
396
400
  }
397
401
 
398
- override fun reconcileFirmwareArtifactLeases(
399
- params: FirmwareArtifactLeaseReconcileParams,
400
- ): Promise<Unit> {
401
- return Promise.async {
402
- FirmwareArtifactStore.reconcileLeases(params.activeLeaseRefs)
403
- }
404
- }
405
-
406
402
  override fun sweepFirmwareArtifactOrphans(): Promise<FirmwareArtifactSweepResult> {
407
- return Promise.async {
403
+ return Promise.async(firmwareArtifactScope) {
408
404
  val (deletedFiles, deletedBytes) = FirmwareArtifactStore.sweepOrphans()
409
405
  FirmwareArtifactSweepResult(
410
406
  deletedFiles = deletedFiles.toDouble(),
@@ -0,0 +1,26 @@
1
+ package com.margelo.nitro.reactnativerangedownloader
2
+
3
+ import org.junit.Assert.assertEquals
4
+ import org.junit.Assert.assertThrows
5
+ import org.junit.Test
6
+
7
+ class FirmwareArtifactDeadlineTest {
8
+ @Test
9
+ fun defaultsToABoundedThreeMinuteDeadline() {
10
+ assertEquals(180.0, validateFirmwareDownloadDeadlineSeconds(null), 0.0)
11
+ }
12
+
13
+ @Test
14
+ fun preservesFractionalDeadlines() {
15
+ assertEquals(2.75, validateFirmwareDownloadDeadlineSeconds(2.75), 0.0)
16
+ }
17
+
18
+ @Test
19
+ fun rejectsUnboundedOrInvalidDeadlines() {
20
+ listOf(0.0, -1.0, Double.NaN, Double.POSITIVE_INFINITY, 86_400.1).forEach {
21
+ assertThrows(IllegalArgumentException::class.java) {
22
+ validateFirmwareDownloadDeadlineSeconds(it)
23
+ }
24
+ }
25
+ }
26
+ }