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,371 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ private enum PathLookupResult {
5
+ case path(NWPath)
6
+ case timedOut
7
+ case cancelled
8
+ }
9
+
10
+ @objc(NetworkQualityImpl)
11
+ public final class NetworkQualityImpl: NSObject {
12
+ @objc public var onChange: (([String: Any]) -> Void)?
13
+
14
+ private let queue = DispatchQueue(label: "com.rnnetworkquality.monitor")
15
+ private let queueKey = DispatchSpecificKey<UInt8>()
16
+ private let cellularInfo = CellularInfo()
17
+ private var monitor: NWPathMonitor?
18
+ private var throttler: Throttler?
19
+ private var latestSnapshot: [String: Any]?
20
+ private var activeProbes: [UUID: NetworkProbe] = [:]
21
+ private var activeOneShots: [UUID: OneShotRequest] = [:]
22
+ private var invalidated = false
23
+
24
+ public override init() {
25
+ super.init()
26
+ queue.setSpecific(key: queueKey, value: 1)
27
+ }
28
+
29
+ @objc(getCurrentStateWithResolve:reject:)
30
+ public func getCurrentState(
31
+ resolve: @escaping (Any?) -> Void,
32
+ reject: @escaping (String?, String?, Error?) -> Void
33
+ ) {
34
+ queue.async { [weak self] in
35
+ guard let self else { return }
36
+ guard !invalidated else {
37
+ reject("E_PROBE_FAILED", "NetworkQuality has been invalidated.", nil)
38
+ return
39
+ }
40
+ if let monitor {
41
+ let snapshot = latestSnapshot
42
+ ?? PathSnapshot.make(path: monitor.currentPath, cellularInfo: cellularInfo)
43
+ latestSnapshot = snapshot
44
+ resolve(snapshot)
45
+ return
46
+ }
47
+
48
+ withOneShotPath(timeoutMs: 2_000) { [weak self] result in
49
+ guard let self else { return }
50
+ switch result {
51
+ case let .path(path):
52
+ resolve(PathSnapshot.make(path: path, cellularInfo: cellularInfo))
53
+ case .timedOut:
54
+ resolve(PathSnapshot.unknownDisconnected())
55
+ case .cancelled:
56
+ reject(
57
+ "E_PROBE_FAILED",
58
+ "The network state request was cancelled because monitoring stopped.",
59
+ nil
60
+ )
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ @objc(startMonitoringWithThrottleMs:bandwidthChangeThresholdPct:)
67
+ public func startMonitoring(
68
+ throttleMs: NSNumber,
69
+ bandwidthChangeThresholdPct: NSNumber
70
+ ) {
71
+ queue.async { [weak self] in
72
+ guard let self else { return }
73
+ guard !invalidated else { return }
74
+ let throttle = max(0, throttleMs.doubleValue)
75
+ let threshold = max(0, bandwidthChangeThresholdPct.doubleValue)
76
+
77
+ if let throttler {
78
+ throttler.update(
79
+ throttleMs: throttle,
80
+ bandwidthChangeThresholdPct: threshold
81
+ )
82
+ } else {
83
+ throttler = makeThrottler(
84
+ throttleMs: throttle,
85
+ bandwidthChangeThresholdPct: threshold
86
+ )
87
+ }
88
+
89
+ guard monitor == nil else { return }
90
+ let pathMonitor = NWPathMonitor()
91
+ monitor = pathMonitor
92
+ pathMonitor.pathUpdateHandler = { [weak self] path in
93
+ self?.handle(path: path)
94
+ }
95
+ pathMonitor.start(queue: queue)
96
+ handle(path: pathMonitor.currentPath)
97
+ }
98
+ }
99
+
100
+ @objc public func stopMonitoring() {
101
+ queue.async { [weak self] in
102
+ self?.stopMonitoringOnQueue()
103
+ }
104
+ }
105
+
106
+ @objc(probeWithLatencyUrl:downloadUrl:latencySamples:timeoutMs:downloadMaxDurationMs:downloadMaxBytes:resolve:reject:)
107
+ public func probe(
108
+ latencyUrl: String,
109
+ downloadUrl: String?,
110
+ latencySamples: NSNumber,
111
+ timeoutMs: NSNumber,
112
+ downloadMaxDurationMs: NSNumber,
113
+ downloadMaxBytes: NSNumber,
114
+ resolve: @escaping (Any?) -> Void,
115
+ reject: @escaping (String?, String?, Error?) -> Void
116
+ ) {
117
+ let probeStartedAt = NetworkProbe.monotonicMilliseconds()
118
+ guard let latencyURL = NetworkProbe.validatedURL(latencyUrl) else {
119
+ reject("E_INVALID_URL", "latencyUrl must be a valid HTTP or HTTPS URL.", nil)
120
+ return
121
+ }
122
+
123
+ let downloadURL: URL?
124
+ if let downloadUrl {
125
+ guard let validated = NetworkProbe.validatedURL(downloadUrl) else {
126
+ reject("E_INVALID_URL", "downloadUrl must be a valid HTTP or HTTPS URL.", nil)
127
+ return
128
+ }
129
+ downloadURL = validated
130
+ } else {
131
+ downloadURL = nil
132
+ }
133
+
134
+ let latencySampleValue = latencySamples.doubleValue
135
+ guard
136
+ latencySampleValue.isFinite,
137
+ latencySampleValue.rounded(.towardZero) == latencySampleValue,
138
+ latencySampleValue >= 1,
139
+ latencySampleValue <= 100
140
+ else {
141
+ reject(
142
+ "E_PROBE_FAILED",
143
+ "latencySamples must be an integer between 1 and 100.",
144
+ nil
145
+ )
146
+ return
147
+ }
148
+ let timeoutValue = timeoutMs.doubleValue
149
+ guard timeoutValue.isFinite, timeoutValue > 0, timeoutValue <= 2_147_483_647
150
+ else {
151
+ reject(
152
+ "E_PROBE_FAILED",
153
+ "timeoutMs must be greater than 0 and at most 2147483647.",
154
+ nil
155
+ )
156
+ return
157
+ }
158
+ let downloadMaxDurationValue = downloadMaxDurationMs.doubleValue
159
+ guard
160
+ downloadMaxDurationValue.isFinite,
161
+ downloadMaxDurationValue > 0,
162
+ downloadMaxDurationValue <= 2_147_483_647
163
+ else {
164
+ reject(
165
+ "E_PROBE_FAILED",
166
+ "downloadMaxDurationMs must be greater than 0 and at most 2147483647.",
167
+ nil
168
+ )
169
+ return
170
+ }
171
+ let downloadMaxBytesValue = downloadMaxBytes.doubleValue
172
+ guard
173
+ downloadMaxBytesValue.isFinite,
174
+ downloadMaxBytesValue.rounded(.towardZero) == downloadMaxBytesValue,
175
+ downloadMaxBytesValue > 0,
176
+ downloadMaxBytesValue <= 2_147_483_647
177
+ else {
178
+ reject(
179
+ "E_PROBE_FAILED",
180
+ "downloadMaxBytes must be an integer between 1 and 2147483647.",
181
+ nil
182
+ )
183
+ return
184
+ }
185
+
186
+ queue.async { [weak self] in
187
+ guard let self else { return }
188
+ guard !invalidated else {
189
+ reject("E_PROBE_FAILED", "NetworkQuality has been invalidated.", nil)
190
+ return
191
+ }
192
+ let pathBudgetMs = timeoutValue
193
+ - (NetworkProbe.monotonicMilliseconds() - probeStartedAt)
194
+ guard pathBudgetMs > 0 else {
195
+ reject("E_PROBE_TIMEOUT", "The network path check exceeded the probe timeout.", nil)
196
+ return
197
+ }
198
+ currentPath(timeoutMs: pathBudgetMs) { [weak self] result in
199
+ guard let self else { return }
200
+ guard !invalidated else {
201
+ reject("E_PROBE_FAILED", "NetworkQuality has been invalidated.", nil)
202
+ return
203
+ }
204
+
205
+ let path: NWPath
206
+ switch result {
207
+ case let .path(value):
208
+ path = value
209
+ case .timedOut:
210
+ reject(
211
+ "E_PROBE_TIMEOUT",
212
+ "The network path check exceeded the probe timeout.",
213
+ nil
214
+ )
215
+ return
216
+ case .cancelled:
217
+ reject("E_PROBE_FAILED", "The network probe was cancelled.", nil)
218
+ return
219
+ }
220
+
221
+ guard
222
+ NetworkProbe.monotonicMilliseconds() - probeStartedAt < timeoutValue
223
+ else {
224
+ reject("E_PROBE_TIMEOUT", "The network path check exceeded the probe timeout.", nil)
225
+ return
226
+ }
227
+ guard path.status == .satisfied else {
228
+ reject("E_OFFLINE", "No connected network is available for probing.", nil)
229
+ return
230
+ }
231
+
232
+ let identifier = UUID()
233
+ let networkProbe = NetworkProbe(
234
+ latencyURL: latencyURL,
235
+ downloadURL: downloadURL,
236
+ latencySamples: Int(latencySampleValue),
237
+ timeoutMs: timeoutValue,
238
+ downloadMaxDurationMs: downloadMaxDurationValue,
239
+ downloadMaxBytes: Int(downloadMaxBytesValue),
240
+ startedAt: probeStartedAt
241
+ ) { [weak self] result in
242
+ self?.queue.async {
243
+ self?.activeProbes.removeValue(forKey: identifier)
244
+ }
245
+ switch result {
246
+ case let .success(value):
247
+ resolve(value)
248
+ case let .failure(failure):
249
+ reject(failure.code, failure.message, failure.error)
250
+ }
251
+ }
252
+ activeProbes[identifier] = networkProbe
253
+ networkProbe.start()
254
+ }
255
+ }
256
+ }
257
+
258
+ @objc public func invalidate() {
259
+ performSynchronouslyOnQueue { [weak self] in
260
+ guard let self else { return }
261
+ guard !invalidated else { return }
262
+ invalidated = true
263
+ stopMonitoringOnQueue()
264
+ onChange = nil
265
+ }
266
+ }
267
+
268
+ private func makeThrottler(
269
+ throttleMs: Double,
270
+ bandwidthChangeThresholdPct: Double
271
+ ) -> Throttler {
272
+ Throttler(
273
+ throttleMs: throttleMs,
274
+ bandwidthChangeThresholdPct: bandwidthChangeThresholdPct,
275
+ clock: {
276
+ Double(DispatchTime.now().uptimeNanoseconds) / 1_000_000
277
+ },
278
+ scheduler: { [weak self] delayMs, action in
279
+ let item = DispatchWorkItem(block: action)
280
+ self?.queue.asyncAfter(deadline: .now() + delayMs / 1_000, execute: item)
281
+ return item
282
+ },
283
+ emit: { [weak self] snapshot in
284
+ self?.onChange?(snapshot)
285
+ }
286
+ )
287
+ }
288
+
289
+ private func handle(path: NWPath) {
290
+ let snapshot = PathSnapshot.make(path: path, cellularInfo: cellularInfo)
291
+ latestSnapshot = snapshot
292
+ throttler?.submit(snapshot)
293
+ }
294
+
295
+ private func stopMonitoringOnQueue() {
296
+ monitor?.pathUpdateHandler = nil
297
+ monitor?.cancel()
298
+ monitor = nil
299
+ latestSnapshot = nil
300
+ throttler?.reset()
301
+ throttler = nil
302
+ activeProbes.values.forEach { $0.cancel() }
303
+ activeProbes.removeAll()
304
+ Array(activeOneShots.keys).forEach {
305
+ finishOneShot($0, result: .cancelled)
306
+ }
307
+ }
308
+
309
+ private func currentPath(
310
+ timeoutMs: Double,
311
+ completion: @escaping (PathLookupResult) -> Void
312
+ ) {
313
+ if let monitor {
314
+ completion(.path(monitor.currentPath))
315
+ } else {
316
+ withOneShotPath(timeoutMs: timeoutMs, completion: completion)
317
+ }
318
+ }
319
+
320
+ private func withOneShotPath(
321
+ timeoutMs: Double,
322
+ completion: @escaping (PathLookupResult) -> Void
323
+ ) {
324
+ let oneShot = NWPathMonitor()
325
+ let identifier = UUID()
326
+ let timeoutItem = DispatchWorkItem { [weak self] in
327
+ self?.finishOneShot(identifier, result: .timedOut)
328
+ }
329
+ activeOneShots[identifier] = OneShotRequest(
330
+ monitor: oneShot,
331
+ timeoutItem: timeoutItem,
332
+ completion: completion
333
+ )
334
+
335
+ oneShot.pathUpdateHandler = { [weak self] path in
336
+ self?.finishOneShot(identifier, result: .path(path))
337
+ }
338
+ oneShot.start(queue: queue)
339
+ queue.asyncAfter(
340
+ deadline: .now() + max(0, timeoutMs) / 1_000,
341
+ execute: timeoutItem
342
+ )
343
+ }
344
+
345
+ private func finishOneShot(
346
+ _ identifier: UUID,
347
+ result: PathLookupResult
348
+ ) {
349
+ guard let request = activeOneShots.removeValue(forKey: identifier) else {
350
+ return
351
+ }
352
+ request.monitor.pathUpdateHandler = nil
353
+ request.monitor.cancel()
354
+ request.timeoutItem.cancel()
355
+ request.completion(result)
356
+ }
357
+
358
+ private func performSynchronouslyOnQueue(_ action: () -> Void) {
359
+ if DispatchQueue.getSpecific(key: queueKey) != nil {
360
+ action()
361
+ } else {
362
+ queue.sync(execute: action)
363
+ }
364
+ }
365
+ }
366
+
367
+ private struct OneShotRequest {
368
+ let monitor: NWPathMonitor
369
+ let timeoutItem: DispatchWorkItem
370
+ let completion: (PathLookupResult) -> Void
371
+ }
@@ -0,0 +1,109 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ enum PathSnapshot {
5
+ static let comparableKeys = [
6
+ "isConnected",
7
+ "isValidated",
8
+ "isCaptivePortal",
9
+ "transport",
10
+ "isVpn",
11
+ "isExpensive",
12
+ "isConstrained",
13
+ "isRoaming",
14
+ "downlinkKbps",
15
+ "uplinkKbps",
16
+ "signalStrength",
17
+ "cellularGeneration",
18
+ "supportsIPv4",
19
+ "supportsIPv6",
20
+ "supportsDNS",
21
+ "unsatisfiedReason",
22
+ ]
23
+
24
+ static func make(path: NWPath, cellularInfo: CellularInfo) -> [String: Any] {
25
+ let isConnected = path.status == .satisfied
26
+ let transport = transport(path: path, isConnected: isConnected)
27
+ let isCellular = transport == "cellular"
28
+
29
+ return [
30
+ "isConnected": isConnected,
31
+ "isValidated": NSNull(),
32
+ "isCaptivePortal": NSNull(),
33
+ "transport": transport,
34
+ "isVpn": NSNull(),
35
+ "isExpensive": path.isExpensive,
36
+ "isConstrained": path.isConstrained,
37
+ "isRoaming": NSNull(),
38
+ "downlinkKbps": NSNull(),
39
+ "uplinkKbps": NSNull(),
40
+ "signalStrength": NSNull(),
41
+ "cellularGeneration": nullable(cellularInfo.generation(isCellular: isCellular)),
42
+ "supportsIPv4": path.supportsIPv4,
43
+ "supportsIPv6": path.supportsIPv6,
44
+ "supportsDNS": path.supportsDNS,
45
+ "unsatisfiedReason": nullable(unsatisfiedReason(path: path)),
46
+ "timestamp": Date().timeIntervalSince1970 * 1_000,
47
+ ]
48
+ }
49
+
50
+ static func unknownDisconnected() -> [String: Any] {
51
+ [
52
+ "isConnected": false,
53
+ "isValidated": NSNull(),
54
+ "isCaptivePortal": NSNull(),
55
+ "transport": "unknown",
56
+ "isVpn": NSNull(),
57
+ "isExpensive": false,
58
+ "isConstrained": false,
59
+ "isRoaming": NSNull(),
60
+ "downlinkKbps": NSNull(),
61
+ "uplinkKbps": NSNull(),
62
+ "signalStrength": NSNull(),
63
+ "cellularGeneration": NSNull(),
64
+ "supportsIPv4": NSNull(),
65
+ "supportsIPv6": NSNull(),
66
+ "supportsDNS": NSNull(),
67
+ "unsatisfiedReason": "unknown",
68
+ "timestamp": Date().timeIntervalSince1970 * 1_000,
69
+ ]
70
+ }
71
+
72
+ private static func transport(path: NWPath, isConnected: Bool) -> String {
73
+ guard isConnected else { return "none" }
74
+ if path.usesInterfaceType(.wifi) { return "wifi" }
75
+ if path.usesInterfaceType(.cellular) { return "cellular" }
76
+ if path.usesInterfaceType(.wiredEthernet) { return "ethernet" }
77
+ if path.usesInterfaceType(.other) || path.usesInterfaceType(.loopback) {
78
+ return "other"
79
+ }
80
+ return "other"
81
+ }
82
+
83
+ private static func unsatisfiedReason(path: NWPath) -> String? {
84
+ guard path.status != .satisfied else { return nil }
85
+
86
+ if #available(iOS 17.0, *), path.unsatisfiedReason == .vpnInactive {
87
+ return "vpnInactive"
88
+ }
89
+
90
+ switch path.unsatisfiedReason {
91
+ case .notAvailable:
92
+ return "notAvailable"
93
+ case .cellularDenied:
94
+ return "cellularDenied"
95
+ case .wifiDenied:
96
+ return "wifiDenied"
97
+ case .localNetworkDenied:
98
+ return "localNetworkDenied"
99
+ case .vpnInactive:
100
+ return "vpnInactive"
101
+ @unknown default:
102
+ return "unknown"
103
+ }
104
+ }
105
+
106
+ private static func nullable(_ value: String?) -> Any {
107
+ value ?? NSNull()
108
+ }
109
+ }
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSPrivacyTracking</key>
6
+ <false/>
7
+ <key>NSPrivacyTrackingDomains</key>
8
+ <array/>
9
+ <key>NSPrivacyCollectedDataTypes</key>
10
+ <array/>
11
+ <key>NSPrivacyAccessedAPITypes</key>
12
+ <array>
13
+ <dict>
14
+ <key>NSPrivacyAccessedAPIType</key>
15
+ <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
16
+ <key>NSPrivacyAccessedAPITypeReasons</key>
17
+ <array>
18
+ <string>35F9.1</string>
19
+ </array>
20
+ </dict>
21
+ </array>
22
+ </dict>
23
+ </plist>
@@ -0,0 +1,174 @@
1
+ import Foundation
2
+
3
+ protocol NetworkQualityScheduledTask: AnyObject {
4
+ func cancel()
5
+ }
6
+
7
+ extension DispatchWorkItem: NetworkQualityScheduledTask {}
8
+
9
+ final class Throttler {
10
+ typealias Clock = () -> Double
11
+ typealias Scheduler = (
12
+ _ delayMs: Double,
13
+ _ action: @escaping () -> Void
14
+ ) -> NetworkQualityScheduledTask
15
+ typealias Emitter = ([String: Any]) -> Void
16
+
17
+ private static let numericKeys = [
18
+ "downlinkKbps",
19
+ "uplinkKbps",
20
+ "signalStrength",
21
+ ]
22
+
23
+ private var throttleMs: Double
24
+ private var bandwidthChangeThresholdPct: Double
25
+ private let clock: Clock
26
+ private let scheduler: Scheduler
27
+ private let emit: Emitter
28
+
29
+ private var lastEmitted: [String: Any]?
30
+ private var lastEmitTime = -Double.infinity
31
+ private var pending: [String: Any]?
32
+ private var timer: NetworkQualityScheduledTask?
33
+
34
+ init(
35
+ throttleMs: Double,
36
+ bandwidthChangeThresholdPct: Double,
37
+ clock: @escaping Clock,
38
+ scheduler: @escaping Scheduler,
39
+ emit: @escaping Emitter
40
+ ) {
41
+ self.throttleMs = max(0, throttleMs)
42
+ self.bandwidthChangeThresholdPct = max(0, bandwidthChangeThresholdPct)
43
+ self.clock = clock
44
+ self.scheduler = scheduler
45
+ self.emit = emit
46
+ }
47
+
48
+ func update(throttleMs: Double, bandwidthChangeThresholdPct: Double) {
49
+ self.throttleMs = max(0, throttleMs)
50
+ self.bandwidthChangeThresholdPct = max(0, bandwidthChangeThresholdPct)
51
+ guard let pending else { return }
52
+ cancelPending()
53
+ submit(pending)
54
+ }
55
+
56
+ func submit(_ snapshot: [String: Any]) {
57
+ guard let previous = lastEmitted else {
58
+ emitNow(snapshot)
59
+ return
60
+ }
61
+
62
+ if Self.equalsIgnoringTimestamp(previous, snapshot) {
63
+ cancelPending()
64
+ return
65
+ }
66
+
67
+ if Self.isSignificantChange(previous, snapshot) {
68
+ cancelPending()
69
+ emitNow(snapshot)
70
+ return
71
+ }
72
+
73
+ let changedNumericKeys = Self.numericKeys.filter {
74
+ !Self.valuesEqual(previous[$0], snapshot[$0])
75
+ }
76
+ let crossesThreshold = changedNumericKeys.contains {
77
+ Self.exceedsThreshold(
78
+ oldValue: previous[$0],
79
+ newValue: snapshot[$0],
80
+ thresholdPct: bandwidthChangeThresholdPct
81
+ )
82
+ }
83
+ guard crossesThreshold else {
84
+ cancelPending()
85
+ return
86
+ }
87
+
88
+ let now = clock()
89
+ let elapsed = now - lastEmitTime
90
+ if elapsed >= throttleMs {
91
+ cancelPending()
92
+ emitNow(snapshot)
93
+ return
94
+ }
95
+
96
+ pending = snapshot
97
+ guard timer == nil else { return }
98
+ timer = scheduler(max(0, throttleMs - elapsed)) { [weak self] in
99
+ guard let self else { return }
100
+ self.timer = nil
101
+ guard let pending = self.pending else { return }
102
+ self.pending = nil
103
+ self.emitNow(pending)
104
+ }
105
+ }
106
+
107
+ func reset() {
108
+ cancelPending()
109
+ lastEmitted = nil
110
+ lastEmitTime = -Double.infinity
111
+ }
112
+
113
+ static func equalsIgnoringTimestamp(
114
+ _ first: [String: Any],
115
+ _ second: [String: Any]
116
+ ) -> Bool {
117
+ PathSnapshot.comparableKeys.allSatisfy {
118
+ valuesEqual(first[$0], second[$0])
119
+ }
120
+ }
121
+
122
+ static func isSignificantChange(
123
+ _ first: [String: Any],
124
+ _ second: [String: Any]
125
+ ) -> Bool {
126
+ PathSnapshot.comparableKeys
127
+ .filter { !numericKeys.contains($0) }
128
+ .contains { !valuesEqual(first[$0], second[$0]) }
129
+ }
130
+
131
+ static func exceedsThreshold(
132
+ oldValue: Any?,
133
+ newValue: Any?,
134
+ thresholdPct: Double
135
+ ) -> Bool {
136
+ let oldNumber = numericValue(oldValue)
137
+ let newNumber = numericValue(newValue)
138
+ if oldNumber == nil || newNumber == nil {
139
+ return oldNumber != nil || newNumber != nil
140
+ }
141
+ guard let oldNumber, let newNumber else { return false }
142
+ if oldNumber == newNumber { return false }
143
+ if oldNumber == 0 { return true }
144
+ return abs(newNumber - oldNumber) / abs(oldNumber) * 100 >= thresholdPct
145
+ }
146
+
147
+ private func emitNow(_ snapshot: [String: Any]) {
148
+ lastEmitted = snapshot
149
+ lastEmitTime = clock()
150
+ emit(snapshot)
151
+ }
152
+
153
+ private func cancelPending() {
154
+ timer?.cancel()
155
+ timer = nil
156
+ pending = nil
157
+ }
158
+
159
+ private static func numericValue(_ value: Any?) -> Double? {
160
+ guard !(value is NSNull) else { return nil }
161
+ return (value as? NSNumber)?.doubleValue
162
+ }
163
+
164
+ private static func valuesEqual(_ first: Any?, _ second: Any?) -> Bool {
165
+ switch (first, second) {
166
+ case (nil, nil):
167
+ return true
168
+ case let (first as NSObject, second as NSObject):
169
+ return first.isEqual(second)
170
+ default:
171
+ return false
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.get('NetworkQuality');
5
+ //# sourceMappingURL=NativeNetworkQuality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"..\\..\\src","sources":["NativeNetworkQuality.ts"],"mappings":";;AAAA,SACEA,mBAAmB,QAGd,cAAc;AAqDrB,eAAeA,mBAAmB,CAACC,GAAG,CAAO,gBAAgB,CAAC","ignoreList":[]}