@onekeyfe/react-native-range-downloader 3.0.81-alpha.2 → 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.
|
@@ -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
|
|
@@ -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,13 +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
402
|
override fun sweepFirmwareArtifactOrphans(): Promise<FirmwareArtifactSweepResult> {
|
|
399
|
-
return Promise.async {
|
|
403
|
+
return Promise.async(firmwareArtifactScope) {
|
|
400
404
|
val (deletedFiles, deletedBytes) = FirmwareArtifactStore.sweepOrphans()
|
|
401
405
|
FirmwareArtifactSweepResult(
|
|
402
406
|
deletedFiles = deletedFiles.toDouble(),
|
|
@@ -219,7 +219,11 @@ final class FirmwareArtifactStore {
|
|
|
219
219
|
transactionId: params.transactionId,
|
|
220
220
|
artifactRef: "fw:\(expectedSha256)"
|
|
221
221
|
)
|
|
222
|
-
let key =
|
|
222
|
+
let key = firmwareArtifactDownloadKey(
|
|
223
|
+
transactionId: params.transactionId,
|
|
224
|
+
expectedSize: Int64(params.expectedSize),
|
|
225
|
+
expectedSha256: expectedSha256
|
|
226
|
+
)
|
|
223
227
|
markDownloadActive(expectedSha256, delta: 1)
|
|
224
228
|
do {
|
|
225
229
|
let artifact = try await downloadCoordinator.run(
|
|
@@ -913,6 +917,29 @@ final class FirmwareArtifactStore {
|
|
|
913
917
|
_ params: FirmwareArtifactDownloadParams,
|
|
914
918
|
partialURL: URL,
|
|
915
919
|
resumeOffset: Int64
|
|
920
|
+
) async throws {
|
|
921
|
+
do {
|
|
922
|
+
try await FirmwareArtifactWallClockDeadline.run(
|
|
923
|
+
timeoutSeconds:
|
|
924
|
+
params.overallDeadlineSeconds ?? Self.defaultDownloadDeadline
|
|
925
|
+
) { [self] in
|
|
926
|
+
try await streamDownloadWithinDeadline(
|
|
927
|
+
params,
|
|
928
|
+
partialURL: partialURL,
|
|
929
|
+
resumeOffset: resumeOffset
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
} catch FirmwareArtifactDeadlineError.exceeded {
|
|
933
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
934
|
+
"ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
935
|
+
)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private func streamDownloadWithinDeadline(
|
|
940
|
+
_ params: FirmwareArtifactDownloadParams,
|
|
941
|
+
partialURL: URL,
|
|
942
|
+
resumeOffset: Int64
|
|
916
943
|
) async throws {
|
|
917
944
|
guard let url = URL(string: params.url), let hostname = url.host else {
|
|
918
945
|
throw FirmwareArtifactStoreError.invalidInput("Invalid firmware URL")
|
|
@@ -949,10 +976,10 @@ final class FirmwareArtifactStore {
|
|
|
949
976
|
}
|
|
950
977
|
}
|
|
951
978
|
|
|
952
|
-
let
|
|
979
|
+
let downloadedURL: URL
|
|
953
980
|
let response: URLResponse
|
|
954
981
|
do {
|
|
955
|
-
(
|
|
982
|
+
(downloadedURL, response) = try await session.download(for: request)
|
|
956
983
|
} catch {
|
|
957
984
|
if isCancellationError(
|
|
958
985
|
error,
|
|
@@ -971,9 +998,20 @@ final class FirmwareArtifactStore {
|
|
|
971
998
|
"ARTIFACT_NETWORK_FAILED: firmware request failed"
|
|
972
999
|
)
|
|
973
1000
|
}
|
|
1001
|
+
defer { try? fileManager.removeItem(at: downloadedURL) }
|
|
974
1002
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
975
1003
|
throw FirmwareArtifactStoreError.downloadFailed("Firmware response is not HTTP")
|
|
976
1004
|
}
|
|
1005
|
+
guard
|
|
1006
|
+
let responseURL = httpResponse.url,
|
|
1007
|
+
responseURL.scheme?.lowercased() == "https",
|
|
1008
|
+
responseURL.host?.lowercased() == hostname.lowercased(),
|
|
1009
|
+
responseURL.port == nil || responseURL.port == 443
|
|
1010
|
+
else {
|
|
1011
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1012
|
+
"ARTIFACT_REDIRECT_REJECTED: firmware response changed canonical identity"
|
|
1013
|
+
)
|
|
1014
|
+
}
|
|
977
1015
|
guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
|
|
978
1016
|
throw FirmwareArtifactStoreError.downloadFailed(
|
|
979
1017
|
"ARTIFACT_HTTP_\(httpResponse.statusCode): firmware request failed"
|
|
@@ -1003,21 +1041,25 @@ final class FirmwareArtifactStore {
|
|
|
1003
1041
|
}
|
|
1004
1042
|
|
|
1005
1043
|
var written = append ? resumeOffset : 0
|
|
1006
|
-
|
|
1007
|
-
|
|
1044
|
+
let source = try FileHandle(forReadingFrom: downloadedURL)
|
|
1045
|
+
defer { try? source.close() }
|
|
1008
1046
|
do {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
}
|
|
1018
|
-
try handle.write(contentsOf: buffer)
|
|
1019
|
-
buffer.removeAll(keepingCapacity: true)
|
|
1047
|
+
while true {
|
|
1048
|
+
try Task.checkCancellation()
|
|
1049
|
+
try rejectIfCancelled(transactionId: params.transactionId)
|
|
1050
|
+
guard
|
|
1051
|
+
let buffer = try source.read(upToCount: 64 * 1024),
|
|
1052
|
+
!buffer.isEmpty
|
|
1053
|
+
else {
|
|
1054
|
+
break
|
|
1020
1055
|
}
|
|
1056
|
+
written += Int64(buffer.count)
|
|
1057
|
+
guard written <= Int64(params.maxBytes) else {
|
|
1058
|
+
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1059
|
+
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
1060
|
+
)
|
|
1061
|
+
}
|
|
1062
|
+
try handle.write(contentsOf: buffer)
|
|
1021
1063
|
}
|
|
1022
1064
|
} catch let error as FirmwareArtifactStoreError {
|
|
1023
1065
|
throw error
|
|
@@ -1039,15 +1081,6 @@ final class FirmwareArtifactStore {
|
|
|
1039
1081
|
"ARTIFACT_NETWORK_FAILED: firmware response stream failed"
|
|
1040
1082
|
)
|
|
1041
1083
|
}
|
|
1042
|
-
if !buffer.isEmpty {
|
|
1043
|
-
written += Int64(buffer.count)
|
|
1044
|
-
guard written <= Int64(params.maxBytes) else {
|
|
1045
|
-
throw FirmwareArtifactStoreError.downloadFailed(
|
|
1046
|
-
"ARTIFACT_PROTOCOL_INVALID: firmware artifact exceeds maxBytes"
|
|
1047
|
-
)
|
|
1048
|
-
}
|
|
1049
|
-
try handle.write(contentsOf: buffer)
|
|
1050
|
-
}
|
|
1051
1084
|
try handle.synchronize()
|
|
1052
1085
|
try rejectIfCancelled(transactionId: params.transactionId)
|
|
1053
1086
|
}
|
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import CommonCrypto
|
|
3
3
|
|
|
4
|
+
enum FirmwareArtifactDeadlineError: Error {
|
|
5
|
+
case exceeded
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
enum FirmwareArtifactWallClockDeadline {
|
|
9
|
+
static func run<T>(
|
|
10
|
+
timeoutSeconds: TimeInterval,
|
|
11
|
+
operation: @escaping @Sendable () async throws -> T
|
|
12
|
+
) async throws -> T {
|
|
13
|
+
let timeoutNanoseconds = UInt64(max(0.001, timeoutSeconds) * 1_000_000_000.0)
|
|
14
|
+
return try await withThrowingTaskGroup(of: T.self) { group in
|
|
15
|
+
group.addTask {
|
|
16
|
+
try await operation()
|
|
17
|
+
}
|
|
18
|
+
group.addTask {
|
|
19
|
+
try await Task.sleep(nanoseconds: timeoutNanoseconds)
|
|
20
|
+
throw FirmwareArtifactDeadlineError.exceeded
|
|
21
|
+
}
|
|
22
|
+
defer { group.cancelAll() }
|
|
23
|
+
guard let result = try await group.next() else {
|
|
24
|
+
throw FirmwareArtifactDeadlineError.exceeded
|
|
25
|
+
}
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func firmwareArtifactDownloadKey(
|
|
32
|
+
transactionId: String,
|
|
33
|
+
expectedSize: Int64,
|
|
34
|
+
expectedSha256: String
|
|
35
|
+
) -> String {
|
|
36
|
+
"\(transactionId)|\(expectedSize)|\(expectedSha256)"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
enum FirmwareBackgroundDownloadError: LocalizedError, CustomStringConvertible {
|
|
40
|
+
case cancelled
|
|
41
|
+
case invalidTask
|
|
42
|
+
case deadlineExceeded
|
|
43
|
+
case redirectRejected
|
|
44
|
+
case responseRejected
|
|
45
|
+
case sizeRejected
|
|
46
|
+
case tlsRejected
|
|
47
|
+
case transferFailed
|
|
48
|
+
|
|
49
|
+
var errorDescription: String? {
|
|
50
|
+
switch self {
|
|
51
|
+
case .cancelled:
|
|
52
|
+
return "ARTIFACT_CANCELLED: firmware background download was cancelled"
|
|
53
|
+
case .invalidTask:
|
|
54
|
+
return "ARTIFACT_PROTOCOL_INVALID: firmware background task is invalid"
|
|
55
|
+
case .deadlineExceeded:
|
|
56
|
+
return "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
57
|
+
case .redirectRejected:
|
|
58
|
+
return "ARTIFACT_REDIRECT_REJECTED: firmware redirect changed canonical identity"
|
|
59
|
+
case .responseRejected:
|
|
60
|
+
return "ARTIFACT_PROTOCOL_INVALID: firmware response is invalid"
|
|
61
|
+
case .sizeRejected:
|
|
62
|
+
return "ARTIFACT_PROTOCOL_INVALID: firmware artifact size is invalid"
|
|
63
|
+
case .tlsRejected:
|
|
64
|
+
return "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
65
|
+
case .transferFailed:
|
|
66
|
+
return "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var description: String {
|
|
71
|
+
errorDescription ?? "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
4
75
|
// MARK: - Dependency-free RangeDownloader logic (OCDS §4 / §5)
|
|
5
76
|
//
|
|
6
77
|
// This file holds the DETERMINISTIC, dependency-light pieces of the range
|
|
@@ -323,49 +323,22 @@ private struct FirmwareBackgroundTaskDescriptor: Codable, Equatable {
|
|
|
323
323
|
let deadlineAt: TimeInterval
|
|
324
324
|
|
|
325
325
|
var key: String {
|
|
326
|
-
|
|
326
|
+
firmwareArtifactDownloadKey(
|
|
327
|
+
transactionId: transactionId,
|
|
328
|
+
expectedSize: expectedSize,
|
|
329
|
+
expectedSha256: expectedSha256
|
|
330
|
+
)
|
|
327
331
|
}
|
|
328
332
|
|
|
329
|
-
func
|
|
333
|
+
func hasSameDownloadIdentity(
|
|
330
334
|
as other: FirmwareBackgroundTaskDescriptor
|
|
331
335
|
) -> Bool {
|
|
332
|
-
|
|
336
|
+
transactionId == other.transactionId &&
|
|
337
|
+
expectedSize == other.expectedSize &&
|
|
333
338
|
expectedSha256 == other.expectedSha256
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
private enum FirmwareBackgroundDownloadError: LocalizedError {
|
|
338
|
-
case cancelled
|
|
339
|
-
case invalidTask
|
|
340
|
-
case deadlineExceeded
|
|
341
|
-
case redirectRejected
|
|
342
|
-
case responseRejected
|
|
343
|
-
case sizeRejected
|
|
344
|
-
case tlsRejected
|
|
345
|
-
case transferFailed
|
|
346
|
-
|
|
347
|
-
var errorDescription: String? {
|
|
348
|
-
switch self {
|
|
349
|
-
case .cancelled:
|
|
350
|
-
return "ARTIFACT_CANCELLED: firmware background download was cancelled"
|
|
351
|
-
case .invalidTask:
|
|
352
|
-
return "ARTIFACT_PROTOCOL_INVALID: firmware background task is invalid"
|
|
353
|
-
case .deadlineExceeded:
|
|
354
|
-
return "ARTIFACT_DEADLINE_EXCEEDED: firmware download exceeded its deadline"
|
|
355
|
-
case .redirectRejected:
|
|
356
|
-
return "ARTIFACT_REDIRECT_REJECTED: firmware redirect changed canonical identity"
|
|
357
|
-
case .responseRejected:
|
|
358
|
-
return "ARTIFACT_PROTOCOL_INVALID: firmware response is invalid"
|
|
359
|
-
case .sizeRejected:
|
|
360
|
-
return "ARTIFACT_PROTOCOL_INVALID: firmware artifact size is invalid"
|
|
361
|
-
case .tlsRejected:
|
|
362
|
-
return "ARTIFACT_TLS_FAILED: firmware TLS validation failed"
|
|
363
|
-
case .transferFailed:
|
|
364
|
-
return "ARTIFACT_NETWORK_FAILED: firmware background transfer failed"
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
342
|
public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
|
|
370
343
|
|
|
371
344
|
public static let shared = RangeDownloader()
|
|
@@ -836,9 +809,12 @@ public final class RangeDownloader: NSObject, URLSessionDownloadDelegate {
|
|
|
836
809
|
else {
|
|
837
810
|
continue
|
|
838
811
|
}
|
|
839
|
-
if candidate.
|
|
812
|
+
if candidate.hasSameDownloadIdentity(as: descriptor) {
|
|
840
813
|
matchingTaskFound = true
|
|
841
|
-
} else if
|
|
814
|
+
} else if
|
|
815
|
+
candidate.transactionId == descriptor.transactionId &&
|
|
816
|
+
candidate.taskId == descriptor.taskId
|
|
817
|
+
{
|
|
842
818
|
task.cancel()
|
|
843
819
|
}
|
|
844
820
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-range-downloader",
|
|
3
|
-
"version": "3.0.81-alpha.
|
|
3
|
+
"version": "3.0.81-alpha.3",
|
|
4
4
|
"description": "react-native-range-downloader",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"typescript": "^5.9.2"
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
|
-
"@onekeyfe/react-native-sni-connect": "3.0.81-alpha.
|
|
91
|
+
"@onekeyfe/react-native-sni-connect": "3.0.81-alpha.3",
|
|
92
92
|
"react": "*",
|
|
93
93
|
"react-native": "*",
|
|
94
94
|
"react-native-nitro-modules": "0.33.2"
|