rn-network-quality 0.1.0

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 (72) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +899 -0
  3. package/android/build.gradle +60 -0
  4. package/android/src/main/AndroidManifest.xml +3 -0
  5. package/android/src/main/java/com/rnnetworkquality/CellularInfo.kt +34 -0
  6. package/android/src/main/java/com/rnnetworkquality/DownloadPolicy.kt +26 -0
  7. package/android/src/main/java/com/rnnetworkquality/Mappers.kt +106 -0
  8. package/android/src/main/java/com/rnnetworkquality/NetworkMonitor.kt +263 -0
  9. package/android/src/main/java/com/rnnetworkquality/NetworkProbe.kt +668 -0
  10. package/android/src/main/java/com/rnnetworkquality/NetworkQualityModule.kt +287 -0
  11. package/android/src/main/java/com/rnnetworkquality/NetworkQualityPackage.kt +25 -0
  12. package/android/src/main/java/com/rnnetworkquality/NetworkSnapshot.kt +83 -0
  13. package/android/src/main/java/com/rnnetworkquality/SnapshotBuilder.kt +92 -0
  14. package/android/src/main/java/com/rnnetworkquality/Throttler.kt +100 -0
  15. package/ios/CellularInfo.swift +47 -0
  16. package/ios/NetworkProbe.swift +391 -0
  17. package/ios/NetworkQuality.h +8 -0
  18. package/ios/NetworkQuality.mm +88 -0
  19. package/ios/NetworkQualityImpl.swift +371 -0
  20. package/ios/PathSnapshot.swift +109 -0
  21. package/ios/PrivacyInfo.xcprivacy +23 -0
  22. package/ios/Throttler.swift +174 -0
  23. package/lib/module/NativeNetworkQuality.js +5 -0
  24. package/lib/module/NativeNetworkQuality.js.map +1 -0
  25. package/lib/module/classify.js +126 -0
  26. package/lib/module/classify.js.map +1 -0
  27. package/lib/module/constants.js +52 -0
  28. package/lib/module/constants.js.map +1 -0
  29. package/lib/module/errors.js +18 -0
  30. package/lib/module/errors.js.map +1 -0
  31. package/lib/module/hooks.js +78 -0
  32. package/lib/module/hooks.js.map +1 -0
  33. package/lib/module/index.js +19 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/manager.js +595 -0
  36. package/lib/module/manager.js.map +1 -0
  37. package/lib/module/normalize.js +35 -0
  38. package/lib/module/normalize.js.map +1 -0
  39. package/lib/module/package.json +1 -0
  40. package/lib/module/types.js +2 -0
  41. package/lib/module/types.js.map +1 -0
  42. package/lib/typescript/package.json +1 -0
  43. package/lib/typescript/src/NativeNetworkQuality.d.ts +50 -0
  44. package/lib/typescript/src/NativeNetworkQuality.d.ts.map +1 -0
  45. package/lib/typescript/src/classify.d.ts +12 -0
  46. package/lib/typescript/src/classify.d.ts.map +1 -0
  47. package/lib/typescript/src/constants.d.ts +15 -0
  48. package/lib/typescript/src/constants.d.ts.map +1 -0
  49. package/lib/typescript/src/errors.d.ts +13 -0
  50. package/lib/typescript/src/errors.d.ts.map +1 -0
  51. package/lib/typescript/src/hooks.d.ts +18 -0
  52. package/lib/typescript/src/hooks.d.ts.map +1 -0
  53. package/lib/typescript/src/index.d.ts +13 -0
  54. package/lib/typescript/src/index.d.ts.map +1 -0
  55. package/lib/typescript/src/manager.d.ts +98 -0
  56. package/lib/typescript/src/manager.d.ts.map +1 -0
  57. package/lib/typescript/src/normalize.d.ts +5 -0
  58. package/lib/typescript/src/normalize.d.ts.map +1 -0
  59. package/lib/typescript/src/types.d.ts +171 -0
  60. package/lib/typescript/src/types.d.ts.map +1 -0
  61. package/mock.js +318 -0
  62. package/package.json +161 -0
  63. package/rn-network-quality.podspec +32 -0
  64. package/src/NativeNetworkQuality.ts +58 -0
  65. package/src/classify.ts +208 -0
  66. package/src/constants.ts +48 -0
  67. package/src/errors.ts +23 -0
  68. package/src/hooks.ts +110 -0
  69. package/src/index.tsx +25 -0
  70. package/src/manager.ts +891 -0
  71. package/src/normalize.ts +81 -0
  72. package/src/types.ts +207 -0
@@ -0,0 +1,391 @@
1
+ import Foundation
2
+
3
+ final class NetworkProbe: NSObject, URLSessionDataDelegate, URLSessionTaskDelegate {
4
+ struct Failure {
5
+ let code: String
6
+ let message: String
7
+ let error: Error?
8
+ }
9
+
10
+ typealias Completion = (Result<[String: Any], Failure>) -> Void
11
+
12
+ private enum Phase {
13
+ case latency(index: Int)
14
+ case download
15
+ case finished
16
+ }
17
+
18
+ private static let minimumDownloadBytes = 64 * 1_024
19
+ private static let minimumDownloadDurationMs = 100.0
20
+
21
+ private let latencyURL: URL
22
+ private let downloadURL: URL?
23
+ private let latencySamples: Int
24
+ private let timeoutMs: Double
25
+ private let downloadMaxDurationMs: Double
26
+ private let downloadMaxBytes: Int
27
+ private let completion: Completion
28
+ private let queue = DispatchQueue(label: "com.rnnetworkquality.probe")
29
+ private let delegateQueue: OperationQueue
30
+
31
+ private lazy var session: URLSession = {
32
+ let configuration = URLSessionConfiguration.ephemeral
33
+ configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
34
+ configuration.urlCache = nil
35
+ configuration.httpCookieStorage = nil
36
+ configuration.httpShouldSetCookies = false
37
+ configuration.waitsForConnectivity = false
38
+ return URLSession(
39
+ configuration: configuration,
40
+ delegate: self,
41
+ delegateQueue: delegateQueue
42
+ )
43
+ }()
44
+
45
+ private var phase: Phase = .latency(index: 0)
46
+ private var activeTask: URLSessionDataTask?
47
+ private var requestStartedAt = 0.0
48
+ private var responseHeadersAt: Double?
49
+ private var metricRttByTask: [Int: Double] = [:]
50
+ private var latencyResults: [Double] = []
51
+ private var receivedBytes = 0
52
+ private var downloadFirstByteAt: Double?
53
+ private var downloadFinishedAt: Double?
54
+ private var reachedDownloadLimit = false
55
+ private let startedAt: Double
56
+ private var timeoutItem: DispatchWorkItem?
57
+ private var downloadTimeoutItem: DispatchWorkItem?
58
+
59
+ init(
60
+ latencyURL: URL,
61
+ downloadURL: URL?,
62
+ latencySamples: Int,
63
+ timeoutMs: Double,
64
+ downloadMaxDurationMs: Double,
65
+ downloadMaxBytes: Int,
66
+ startedAt: Double,
67
+ completion: @escaping Completion
68
+ ) {
69
+ self.latencyURL = latencyURL
70
+ self.downloadURL = downloadURL
71
+ self.latencySamples = max(1, latencySamples)
72
+ self.timeoutMs = timeoutMs
73
+ self.downloadMaxDurationMs = downloadMaxDurationMs
74
+ self.downloadMaxBytes = downloadMaxBytes
75
+ self.startedAt = startedAt
76
+ self.completion = completion
77
+ let delegateQueue = OperationQueue()
78
+ delegateQueue.name = "com.rnnetworkquality.probe.delegate"
79
+ delegateQueue.maxConcurrentOperationCount = 1
80
+ delegateQueue.underlyingQueue = queue
81
+ self.delegateQueue = delegateQueue
82
+ super.init()
83
+ }
84
+
85
+ static func validatedURL(_ value: String) -> URL? {
86
+ guard
87
+ let components = URLComponents(string: value),
88
+ let scheme = components.scheme?.lowercased(),
89
+ scheme == "http" || scheme == "https",
90
+ components.host?.isEmpty == false,
91
+ let url = components.url
92
+ else {
93
+ return nil
94
+ }
95
+ return url
96
+ }
97
+
98
+ func start() {
99
+ queue.async { [weak self] in
100
+ guard let self else { return }
101
+ let remainingMs = timeoutMs - elapsedMilliseconds
102
+ guard remainingMs > 0 else {
103
+ handleWholeProbeTimeout()
104
+ return
105
+ }
106
+ let timeout = DispatchWorkItem { [weak self] in
107
+ self?.handleWholeProbeTimeout()
108
+ }
109
+ timeoutItem = timeout
110
+ queue.asyncAfter(deadline: .now() + remainingMs / 1_000, execute: timeout)
111
+ startLatencyRequest(index: 0)
112
+ }
113
+ }
114
+
115
+ func cancel() {
116
+ queue.async { [self] in
117
+ guard !isFinished else { return }
118
+ fail(
119
+ code: "E_PROBE_FAILED",
120
+ message: "The network probe was cancelled.",
121
+ error: nil
122
+ )
123
+ }
124
+ }
125
+
126
+ func urlSession(
127
+ _ session: URLSession,
128
+ dataTask: URLSessionDataTask,
129
+ didReceive response: URLResponse,
130
+ completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
131
+ ) {
132
+ responseHeadersAt = Self.monotonicMilliseconds()
133
+ completionHandler(.allow)
134
+ }
135
+
136
+ func urlSession(
137
+ _ session: URLSession,
138
+ dataTask: URLSessionDataTask,
139
+ didReceive data: Data
140
+ ) {
141
+ guard case .download = phase else { return }
142
+ let receivedAt = Self.monotonicMilliseconds()
143
+ if downloadFirstByteAt == nil {
144
+ downloadFirstByteAt = receivedAt
145
+ let timeout = DispatchWorkItem { [weak self] in
146
+ self?.handleDownloadTimeLimit()
147
+ }
148
+ downloadTimeoutItem = timeout
149
+ queue.asyncAfter(
150
+ deadline: .now() + downloadMaxDurationMs / 1_000,
151
+ execute: timeout
152
+ )
153
+ }
154
+
155
+ let remaining = downloadMaxBytes - receivedBytes
156
+ if remaining <= 0 {
157
+ reachedDownloadLimit = true
158
+ downloadFinishedAt = receivedAt
159
+ dataTask.cancel()
160
+ return
161
+ }
162
+ receivedBytes += min(remaining, data.count)
163
+ downloadFinishedAt = receivedAt
164
+ if receivedBytes >= downloadMaxBytes {
165
+ reachedDownloadLimit = true
166
+ dataTask.cancel()
167
+ }
168
+ }
169
+
170
+ func urlSession(
171
+ _ session: URLSession,
172
+ task: URLSessionTask,
173
+ didFinishCollecting metrics: URLSessionTaskMetrics
174
+ ) {
175
+ guard
176
+ let transaction = metrics.transactionMetrics.last,
177
+ let requestStart = transaction.requestStartDate,
178
+ let responseStart = transaction.responseStartDate
179
+ else {
180
+ return
181
+ }
182
+ metricRttByTask[task.taskIdentifier] =
183
+ responseStart.timeIntervalSince(requestStart) * 1_000
184
+ }
185
+
186
+ func urlSession(
187
+ _ session: URLSession,
188
+ task: URLSessionTask,
189
+ didCompleteWithError error: Error?
190
+ ) {
191
+ guard !isFinished else { return }
192
+
193
+ switch phase {
194
+ case let .latency(index):
195
+ if let error {
196
+ let timedOut = (error as? URLError)?.code == .timedOut
197
+ fail(
198
+ code: timedOut ? "E_PROBE_TIMEOUT" : "E_PROBE_FAILED",
199
+ message: timedOut
200
+ ? "The latency phase exceeded the probe timeout."
201
+ : "The latency request failed: \(error.localizedDescription)",
202
+ error: error
203
+ )
204
+ return
205
+ }
206
+
207
+ if index > 0 {
208
+ let fallbackRtt = max(
209
+ 0,
210
+ (responseHeadersAt ?? Self.monotonicMilliseconds()) - requestStartedAt
211
+ )
212
+ latencyResults.append(metricRttByTask[task.taskIdentifier] ?? fallbackRtt)
213
+ }
214
+
215
+ if index < latencySamples {
216
+ startLatencyRequest(index: index + 1)
217
+ } else if downloadURL != nil {
218
+ startDownloadRequest()
219
+ } else {
220
+ finish(downloadError: nil)
221
+ }
222
+
223
+ case .download:
224
+ if let error, !reachedDownloadLimit {
225
+ finish(downloadError: error.localizedDescription)
226
+ } else {
227
+ finish(downloadError: nil)
228
+ }
229
+ case .finished:
230
+ break
231
+ }
232
+ }
233
+
234
+ private var isFinished: Bool {
235
+ if case .finished = phase { return true }
236
+ return false
237
+ }
238
+
239
+ private func startLatencyRequest(index: Int) {
240
+ phase = .latency(index: index)
241
+ startRequest(url: latencyURL)
242
+ }
243
+
244
+ private func startDownloadRequest() {
245
+ guard let downloadURL else {
246
+ finish(downloadError: nil)
247
+ return
248
+ }
249
+ phase = .download
250
+ receivedBytes = 0
251
+ downloadFirstByteAt = nil
252
+ downloadFinishedAt = nil
253
+ reachedDownloadLimit = false
254
+ startRequest(url: downloadURL)
255
+ }
256
+
257
+ private func startRequest(url: URL) {
258
+ let remainingMs = timeoutMs - elapsedMilliseconds
259
+ guard remainingMs > 0 else {
260
+ handleWholeProbeTimeout()
261
+ return
262
+ }
263
+
264
+ guard let requestURL = Self.cacheBustedURL(url) else {
265
+ fail(
266
+ code: "E_INVALID_URL",
267
+ message: "Could not add a cache-busting query parameter to the probe URL.",
268
+ error: nil
269
+ )
270
+ return
271
+ }
272
+
273
+ var request = URLRequest(url: requestURL)
274
+ request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
275
+ request.timeoutInterval = max(0.001, remainingMs / 1_000)
276
+ request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
277
+
278
+ requestStartedAt = Self.monotonicMilliseconds()
279
+ responseHeadersAt = nil
280
+ let task = session.dataTask(with: request)
281
+ activeTask = task
282
+ task.resume()
283
+ }
284
+
285
+ private func handleWholeProbeTimeout() {
286
+ guard !isFinished else { return }
287
+ switch phase {
288
+ case .download:
289
+ activeTask?.cancel()
290
+ finish(downloadError: "The throughput phase exceeded the probe timeout.")
291
+ case .latency:
292
+ fail(
293
+ code: "E_PROBE_TIMEOUT",
294
+ message: "The latency phase exceeded the probe timeout.",
295
+ error: nil
296
+ )
297
+ case .finished:
298
+ break
299
+ }
300
+ }
301
+
302
+ private func handleDownloadTimeLimit() {
303
+ guard case .download = phase, !isFinished else { return }
304
+ reachedDownloadLimit = true
305
+ downloadFinishedAt = Self.monotonicMilliseconds()
306
+ activeTask?.cancel()
307
+ }
308
+
309
+ private func finish(downloadError: String?) {
310
+ guard !isFinished else { return }
311
+ downloadTimeoutItem?.cancel()
312
+ let finishedAt = downloadFinishedAt ?? Self.monotonicMilliseconds()
313
+ let downloadDuration = max(0, finishedAt - (downloadFirstByteAt ?? finishedAt))
314
+ var resolvedDownloadError = downloadError
315
+ if resolvedDownloadError == nil, downloadURL != nil {
316
+ if receivedBytes < Self.minimumDownloadBytes {
317
+ resolvedDownloadError = "too-little-data"
318
+ } else if downloadDuration < Self.minimumDownloadDurationMs {
319
+ resolvedDownloadError = "too-fast-to-measure"
320
+ }
321
+ }
322
+ let downlinkKbps: Double?
323
+ if resolvedDownloadError == nil,
324
+ downloadURL != nil,
325
+ downloadDuration > 0
326
+ {
327
+ downlinkKbps = Double(receivedBytes) * 8 / downloadDuration
328
+ } else {
329
+ downlinkKbps = nil
330
+ }
331
+
332
+ let result: [String: Any] = [
333
+ "rttMs": Self.nullable(Self.median(latencyResults)),
334
+ "downlinkKbps": Self.nullable(downlinkKbps),
335
+ "bytesReceived": receivedBytes,
336
+ "durationMs": elapsedMilliseconds,
337
+ "downloadError": Self.nullable(resolvedDownloadError),
338
+ "timestamp": Date().timeIntervalSince1970 * 1_000,
339
+ ]
340
+ complete(.success(result))
341
+ }
342
+
343
+ private func fail(code: String, message: String, error: Error?) {
344
+ complete(.failure(Failure(code: code, message: message, error: error)))
345
+ }
346
+
347
+ private func complete(_ result: Result<[String: Any], Failure>) {
348
+ guard !isFinished else { return }
349
+ phase = .finished
350
+ timeoutItem?.cancel()
351
+ downloadTimeoutItem?.cancel()
352
+ activeTask?.cancel()
353
+ session.invalidateAndCancel()
354
+ completion(result)
355
+ }
356
+
357
+ private var elapsedMilliseconds: Double {
358
+ Self.monotonicMilliseconds() - startedAt
359
+ }
360
+
361
+ private static func cacheBustedURL(_ url: URL) -> URL? {
362
+ guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
363
+ else {
364
+ return nil
365
+ }
366
+ var items = components.queryItems ?? []
367
+ items.append(URLQueryItem(name: "_nq", value: UUID().uuidString))
368
+ components.queryItems = items
369
+ return components.url
370
+ }
371
+
372
+ private static func median(_ values: [Double]) -> Double? {
373
+ guard !values.isEmpty else { return nil }
374
+ let sorted = values.sorted()
375
+ let middle = sorted.count / 2
376
+ if sorted.count.isMultiple(of: 2) {
377
+ return (sorted[middle - 1] + sorted[middle]) / 2
378
+ }
379
+ return sorted[middle]
380
+ }
381
+
382
+ private static func nullable(_ value: Any?) -> Any {
383
+ value ?? NSNull()
384
+ }
385
+
386
+ static func monotonicMilliseconds() -> Double {
387
+ Double(DispatchTime.now().uptimeNanoseconds) / 1_000_000
388
+ }
389
+ }
390
+
391
+ extension NetworkProbe.Failure: Error {}
@@ -0,0 +1,8 @@
1
+ #import <RNNetworkQualitySpec/RNNetworkQualitySpec.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ @interface NetworkQuality : NativeNetworkQualitySpecBase <NativeNetworkQualitySpec>
6
+ @end
7
+
8
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,88 @@
1
+ #import "NetworkQuality.h"
2
+
3
+ #if __has_include(<rn_network_quality/rn_network_quality-Swift.h>)
4
+ #import <rn_network_quality/rn_network_quality-Swift.h>
5
+ #else
6
+ #import "rn_network_quality-Swift.h"
7
+ #endif
8
+
9
+ @interface NetworkQuality ()
10
+ @property(nonatomic, strong) NetworkQualityImpl *impl;
11
+ @end
12
+
13
+ @implementation NetworkQuality
14
+
15
+ - (instancetype)init
16
+ {
17
+ self = [super init];
18
+ if (self) {
19
+ _impl = [NetworkQualityImpl new];
20
+ __weak NetworkQuality *weakSelf = self;
21
+ _impl.onChange = ^(NSDictionary<NSString *, id> *snapshot) {
22
+ NetworkQuality *strongSelf = weakSelf;
23
+ if (strongSelf != nil) {
24
+ [strongSelf emitOnNetworkStateChange:snapshot];
25
+ }
26
+ };
27
+ }
28
+ return self;
29
+ }
30
+
31
+ + (NSString *)moduleName
32
+ {
33
+ return @"NetworkQuality";
34
+ }
35
+
36
+ + (BOOL)requiresMainQueueSetup
37
+ {
38
+ return NO;
39
+ }
40
+
41
+ - (void)getCurrentState:(RCTPromiseResolveBlock)resolve
42
+ reject:(RCTPromiseRejectBlock)reject
43
+ {
44
+ [_impl getCurrentStateWithResolve:resolve reject:reject];
45
+ }
46
+
47
+ - (void)startMonitoring:(JS::NativeNetworkQuality::NativeMonitorOptions &)options
48
+ {
49
+ [_impl startMonitoringWithThrottleMs:@(options.throttleMs())
50
+ bandwidthChangeThresholdPct:@(options.bandwidthChangeThresholdPct())];
51
+ }
52
+
53
+ - (void)stopMonitoring
54
+ {
55
+ [_impl stopMonitoring];
56
+ }
57
+
58
+ - (void)probe:(JS::NativeNetworkQuality::NativeProbeOptions &)options
59
+ resolve:(RCTPromiseResolveBlock)resolve
60
+ reject:(RCTPromiseRejectBlock)reject
61
+ {
62
+ [_impl probeWithLatencyUrl:options.latencyUrl()
63
+ downloadUrl:options.downloadUrl()
64
+ latencySamples:@(options.latencySamples())
65
+ timeoutMs:@(options.timeoutMs())
66
+ downloadMaxDurationMs:@(options.downloadMaxDurationMs())
67
+ downloadMaxBytes:@(options.downloadMaxBytes())
68
+ resolve:resolve
69
+ reject:reject];
70
+ }
71
+
72
+ - (void)invalidate
73
+ {
74
+ [_impl invalidate];
75
+ }
76
+
77
+ - (void)dealloc
78
+ {
79
+ [_impl invalidate];
80
+ }
81
+
82
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
83
+ (const facebook::react::ObjCTurboModule::InitParams &)params
84
+ {
85
+ return std::make_shared<facebook::react::NativeNetworkQualitySpecJSI>(params);
86
+ }
87
+
88
+ @end