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.
- package/LICENSE +20 -0
- package/README.md +899 -0
- package/android/build.gradle +60 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/rnnetworkquality/CellularInfo.kt +34 -0
- package/android/src/main/java/com/rnnetworkquality/DownloadPolicy.kt +26 -0
- package/android/src/main/java/com/rnnetworkquality/Mappers.kt +106 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkMonitor.kt +263 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkProbe.kt +668 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkQualityModule.kt +287 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkQualityPackage.kt +25 -0
- package/android/src/main/java/com/rnnetworkquality/NetworkSnapshot.kt +83 -0
- package/android/src/main/java/com/rnnetworkquality/SnapshotBuilder.kt +92 -0
- package/android/src/main/java/com/rnnetworkquality/Throttler.kt +100 -0
- package/ios/CellularInfo.swift +47 -0
- package/ios/NetworkProbe.swift +391 -0
- package/ios/NetworkQuality.h +8 -0
- package/ios/NetworkQuality.mm +88 -0
- package/ios/NetworkQualityImpl.swift +371 -0
- package/ios/PathSnapshot.swift +109 -0
- package/ios/PrivacyInfo.xcprivacy +23 -0
- package/ios/Throttler.swift +174 -0
- package/lib/module/NativeNetworkQuality.js +5 -0
- package/lib/module/NativeNetworkQuality.js.map +1 -0
- package/lib/module/classify.js +126 -0
- package/lib/module/classify.js.map +1 -0
- package/lib/module/constants.js +52 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/errors.js +18 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/hooks.js +78 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +19 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/manager.js +595 -0
- package/lib/module/manager.js.map +1 -0
- package/lib/module/normalize.js +35 -0
- package/lib/module/normalize.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeNetworkQuality.d.ts +50 -0
- package/lib/typescript/src/NativeNetworkQuality.d.ts.map +1 -0
- package/lib/typescript/src/classify.d.ts +12 -0
- package/lib/typescript/src/classify.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +15 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +13 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts +18 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/manager.d.ts +98 -0
- package/lib/typescript/src/manager.d.ts.map +1 -0
- package/lib/typescript/src/normalize.d.ts +5 -0
- package/lib/typescript/src/normalize.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +171 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/mock.js +318 -0
- package/package.json +161 -0
- package/rn-network-quality.podspec +32 -0
- package/src/NativeNetworkQuality.ts +58 -0
- package/src/classify.ts +208 -0
- package/src/constants.ts +48 -0
- package/src/errors.ts +23 -0
- package/src/hooks.ts +110 -0
- package/src/index.tsx +25 -0
- package/src/manager.ts +891 -0
- package/src/normalize.ts +81 -0
- package/src/types.ts +207 -0
package/README.md
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
# rn-network-quality
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/rn-network-quality)
|
|
4
|
+
[](https://github.com/kruthikRgowda/rn-network-quality/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://reactnative.dev/architecture/landing-page)
|
|
7
|
+
|
|
8
|
+
Live network quality signals for React Native—bandwidth, link quality, and
|
|
9
|
+
connectivity through a New Architecture TurboModule.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
An online/offline flag only says that a route may exist. It cannot tell an app
|
|
14
|
+
whether that route is validated, behind a captive portal, constrained by a data
|
|
15
|
+
saving mode, or too slow for the experience it is about to start.
|
|
16
|
+
|
|
17
|
+
`rn-network-quality` combines event-driven operating-system signals with an
|
|
18
|
+
optional active probe and a deterministic TypeScript classifier. Your app gets
|
|
19
|
+
one actionable tier:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
offline | poor | moderate | good | excellent | unknown
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Use it to choose a video rendition, reduce image quality, postpone a large
|
|
26
|
+
upload, or explain why an operation may be slow. The raw measurements remain
|
|
27
|
+
available when your product needs a different policy.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Live default-network updates from Android `ConnectivityManager` and iOS
|
|
32
|
+
`NWPathMonitor`
|
|
33
|
+
- Transport, validation, captive-portal, metered/expensive, constrained,
|
|
34
|
+
roaming, IP-family, DNS, signal, and cellular-generation signals
|
|
35
|
+
- Android OS bandwidth estimates and an opt-in cross-platform latency and
|
|
36
|
+
throughput probe
|
|
37
|
+
- Pure, configurable quality classification with human-readable reasons
|
|
38
|
+
- Ref-counted monitoring, native de-duplication, and trailing-edge throttling
|
|
39
|
+
- Hooks, imperative functions, typed errors, and a consumer Jest mock
|
|
40
|
+
- No third-party runtime dependencies, polling, analytics, or telemetry
|
|
41
|
+
- New Architecture TurboModule implementations in Kotlin and Swift/Objective-C++
|
|
42
|
+
|
|
43
|
+
## Requirements and platform support
|
|
44
|
+
|
|
45
|
+
| Requirement | Support |
|
|
46
|
+
| ------------------- | ---------------------------------------------------------------------- |
|
|
47
|
+
| React Native | `>=0.80.0`; React Native 0.87.1 is used by this repository |
|
|
48
|
+
| Architecture | New Architecture only |
|
|
49
|
+
| Android | API 24 or newer |
|
|
50
|
+
| iOS | iOS 15.1 or newer, matching React Native 0.87.1 |
|
|
51
|
+
| Expo | Development builds and prebuild projects are supported; Expo Go is not |
|
|
52
|
+
| Web, Windows, macOS | Not supported |
|
|
53
|
+
|
|
54
|
+
React Native 0.80 is the peer-dependency floor because its Codegen parser
|
|
55
|
+
supports the `CodegenTypes.EventEmitter` property syntax used by this module's
|
|
56
|
+
TurboModule spec. The package has no old-architecture fallback.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
With Yarn:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
yarn add rn-network-quality
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or with npm:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm install rn-network-quality
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Install iOS pods from your application's iOS directory, then rebuild the app:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
cd ios
|
|
76
|
+
pod install
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Android autolinking requires no extra setup. This is native code, so adding the
|
|
80
|
+
package always requires a new native build.
|
|
81
|
+
|
|
82
|
+
## Quick start
|
|
83
|
+
|
|
84
|
+
`useNetworkQuality()` starts monitoring when the component subscribes and stops
|
|
85
|
+
it after the final subscriber unmounts. It returns `null` until the first native
|
|
86
|
+
snapshot arrives.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Text, View } from 'react-native';
|
|
90
|
+
import { useNetworkQuality } from 'rn-network-quality';
|
|
91
|
+
|
|
92
|
+
export function ConnectionSummary() {
|
|
93
|
+
const network = useNetworkQuality();
|
|
94
|
+
|
|
95
|
+
if (network === null) {
|
|
96
|
+
return <Text>Checking connection…</Text>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<View>
|
|
101
|
+
<Text>Quality: {network.quality}</Text>
|
|
102
|
+
<Text>Transport: {network.transport}</Text>
|
|
103
|
+
<Text>Source: {network.qualitySource}</Text>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Here is a small adaptive-video policy. `unknown` is deliberately unranked, so
|
|
110
|
+
it does not satisfy any minimum tier.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { isQualityAtLeast, type NetworkQuality } from 'rn-network-quality';
|
|
114
|
+
|
|
115
|
+
export function videoHeightFor(quality: NetworkQuality): number | null {
|
|
116
|
+
if (isQualityAtLeast(quality, 'excellent')) return 1080;
|
|
117
|
+
if (isQualityAtLeast(quality, 'good')) return 720;
|
|
118
|
+
if (isQualityAtLeast(quality, 'moderate')) return 480;
|
|
119
|
+
if (quality === 'poor') return 240;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## API reference
|
|
125
|
+
|
|
126
|
+
All runtime functions and types below are exported from `rn-network-quality`.
|
|
127
|
+
|
|
128
|
+
### `isSupported()`
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
function isSupported(): boolean;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Returns whether the `NetworkQuality` TurboModule is linked on this platform. It
|
|
135
|
+
never throws and is useful before rendering a feature on an optional platform.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { isSupported } from 'rn-network-quality';
|
|
139
|
+
|
|
140
|
+
const canShowNetworkDiagnostics = isSupported();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `configure(config)`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
function configure(config: DeepPartial<NetworkQualityConfig>): void;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Deep-merges the supplied values with the current configuration, reclassifies
|
|
150
|
+
the cached state, and updates active native monitoring. `autoProbe.intervalMs`
|
|
151
|
+
is clamped to at least 15 seconds. Negative durations, invalid numeric values,
|
|
152
|
+
and non-monotonic thresholds throw `TypeError`. An unlinked native module throws
|
|
153
|
+
`NetworkQualityError` with `E_UNSUPPORTED`.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { configure } from 'rn-network-quality';
|
|
157
|
+
|
|
158
|
+
configure({
|
|
159
|
+
throttleMs: 500,
|
|
160
|
+
thresholds: {
|
|
161
|
+
good: { minDownlinkKbps: 8_000, maxRttMs: 120 },
|
|
162
|
+
},
|
|
163
|
+
autoProbe: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
intervalMs: 120_000,
|
|
166
|
+
allowOnExpensive: false,
|
|
167
|
+
allowOnConstrained: false,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `getConfig()`
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
function getConfig(): NetworkQualityConfig;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Returns the current fully merged configuration. It throws `E_UNSUPPORTED` if
|
|
179
|
+
the native module is unavailable.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { getConfig } from 'rn-network-quality';
|
|
183
|
+
|
|
184
|
+
const probeTimeout = getConfig().probe.timeoutMs;
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### `getNetworkQuality()`
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
function getNetworkQuality(): Promise<NetworkQualityState>;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Reads and classifies the current state whether continuous monitoring is active
|
|
194
|
+
or not. If the native module is not linked, it throws `E_UNSUPPORTED` before
|
|
195
|
+
returning a promise. Platform failures that prevent a snapshot reject the
|
|
196
|
+
promise and are passed through by the native implementation.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { getNetworkQuality } from 'rn-network-quality';
|
|
200
|
+
|
|
201
|
+
const state = await getNetworkQuality();
|
|
202
|
+
console.log(state.quality, state.reasons);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `addNetworkQualityListener(listener)`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
function addNetworkQualityListener(
|
|
209
|
+
listener: (state: NetworkQualityState) => void
|
|
210
|
+
): { remove(): void };
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Adds a live-state listener. The first listener starts native monitoring and the
|
|
214
|
+
last removal stops it. A cached state is sent to a new listener in a microtask.
|
|
215
|
+
Calling `remove()` more than once is safe. Registration throws `E_UNSUPPORTED`
|
|
216
|
+
if the module is not linked. Because native monitoring owns active-probe
|
|
217
|
+
resources, removing the last listener also cancels an in-flight probe; its
|
|
218
|
+
promise rejects with `E_PROBE_FAILED`.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { addNetworkQualityListener } from 'rn-network-quality';
|
|
222
|
+
|
|
223
|
+
const subscription = addNetworkQualityListener((state) => {
|
|
224
|
+
console.log(state.quality, state.effectiveDownlinkKbps);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
subscription.remove();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `probeNetwork(options?)`
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
function probeNetwork(options?: Partial<ProbeConfig>): Promise<ProbeResult>;
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Runs one opt-in active probe. Options override the configured probe defaults.
|
|
237
|
+
Concurrent calls share the same in-flight promise. The result becomes
|
|
238
|
+
`lastProbe` and influences classification while it is fresh and its transport
|
|
239
|
+
still matches. When calls overlap, the first call's options apply to the shared
|
|
240
|
+
probe.
|
|
241
|
+
|
|
242
|
+
Invalid option values throw a synchronous `TypeError`. An unavailable native
|
|
243
|
+
module similarly throws `E_UNSUPPORTED` before returning a promise. Once native
|
|
244
|
+
work begins, the promise can reject with `E_INVALID_URL`, `E_OFFLINE`,
|
|
245
|
+
`E_PROBE_TIMEOUT`, or `E_PROBE_FAILED`. A failed throughput phase does not
|
|
246
|
+
reject the probe when latency succeeded; inspect `downloadError` instead.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { probeNetwork } from 'rn-network-quality';
|
|
250
|
+
|
|
251
|
+
const result = await probeNetwork({
|
|
252
|
+
latencyUrl: 'https://network.example.com/204',
|
|
253
|
+
downloadUrl: 'https://network.example.com/probe-3mb.bin',
|
|
254
|
+
timeoutMs: 6_000,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
console.log(result.rttMs, result.downlinkKbps);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `getLastProbeResult()`
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
function getLastProbeResult(): ProbeResult | null;
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Returns the most recent successful probe, or `null` before one succeeds. This
|
|
267
|
+
does not start network traffic. It throws `E_UNSUPPORTED` if the native module
|
|
268
|
+
is unavailable.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { getLastProbeResult } from 'rn-network-quality';
|
|
272
|
+
|
|
273
|
+
const lastMeasuredAt = getLastProbeResult()?.timestamp ?? null;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### `classifyNetworkQuality(snapshot, probe, config?, now?)`
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
function classifyNetworkQuality(
|
|
280
|
+
snapshot: NetworkSnapshot,
|
|
281
|
+
probe: ProbeResult | null,
|
|
282
|
+
config?: Pick<NetworkQualityConfig, 'thresholds' | 'probe'>,
|
|
283
|
+
now?: number
|
|
284
|
+
): Pick<
|
|
285
|
+
NetworkQualityState,
|
|
286
|
+
| 'quality'
|
|
287
|
+
| 'qualitySource'
|
|
288
|
+
| 'effectiveDownlinkKbps'
|
|
289
|
+
| 'effectiveRttMs'
|
|
290
|
+
| 'reasons'
|
|
291
|
+
>;
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Purely classifies input values without native access or side effects. `now`
|
|
295
|
+
defaults to `Date.now()` and can be injected for deterministic tests. It works
|
|
296
|
+
on unsupported platforms and does not throw for a valid typed input.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import {
|
|
300
|
+
classifyNetworkQuality,
|
|
301
|
+
type NetworkSnapshot,
|
|
302
|
+
} from 'rn-network-quality';
|
|
303
|
+
|
|
304
|
+
const snapshot: NetworkSnapshot = {
|
|
305
|
+
isConnected: true,
|
|
306
|
+
isValidated: true,
|
|
307
|
+
isCaptivePortal: false,
|
|
308
|
+
transport: 'wifi',
|
|
309
|
+
isVpn: false,
|
|
310
|
+
isExpensive: false,
|
|
311
|
+
isConstrained: false,
|
|
312
|
+
isRoaming: false,
|
|
313
|
+
downlinkKbps: 12_000,
|
|
314
|
+
uplinkKbps: 2_000,
|
|
315
|
+
signalStrength: null,
|
|
316
|
+
cellularGeneration: null,
|
|
317
|
+
supportsIPv4: true,
|
|
318
|
+
supportsIPv6: true,
|
|
319
|
+
supportsDNS: true,
|
|
320
|
+
unsatisfiedReason: null,
|
|
321
|
+
timestamp: 1_795_027_200_000,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const classification = classifyNetworkQuality(snapshot, null);
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### `isQualityAtLeast(quality, minimum)`
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
function isQualityAtLeast(
|
|
331
|
+
quality: NetworkQuality,
|
|
332
|
+
minimum: NetworkQuality
|
|
333
|
+
): boolean;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Compares ranked tiers from `offline` through `excellent`. Because `unknown` has
|
|
337
|
+
no rank, the function returns `false` when either argument is `unknown`. It is
|
|
338
|
+
pure and works without the native module.
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
import { isQualityAtLeast } from 'rn-network-quality';
|
|
342
|
+
|
|
343
|
+
const canAutoplayHd = isQualityAtLeast('good', 'moderate'); // true
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### `useNetworkQuality()`
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
function useNetworkQuality(): NetworkQualityState | null;
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
A `useSyncExternalStore`-based hook for live state. It returns `null` before the
|
|
353
|
+
first event and subscribes/unsubscribes with the component lifecycle. Linking
|
|
354
|
+
problems surface as `E_UNSUPPORTED` during render.
|
|
355
|
+
|
|
356
|
+
```tsx
|
|
357
|
+
import { Text } from 'react-native';
|
|
358
|
+
import { useNetworkQuality } from 'rn-network-quality';
|
|
359
|
+
|
|
360
|
+
export function TransportLabel() {
|
|
361
|
+
const state = useNetworkQuality();
|
|
362
|
+
return <Text>{state?.transport ?? 'detecting'}</Text>;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### `useNetworkProbe()`
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
function useNetworkProbe(): {
|
|
370
|
+
probe: (options?: Partial<ProbeConfig>) => Promise<ProbeResult>;
|
|
371
|
+
isProbing: boolean;
|
|
372
|
+
result: ProbeResult | null;
|
|
373
|
+
error: NetworkQualityError | null;
|
|
374
|
+
};
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Tracks one component's probe status, last result, and typed error. Results that
|
|
378
|
+
arrive after unmount are ignored. The returned `probe` preserves known
|
|
379
|
+
`NetworkQualityError` codes; unexpected failures, including invalid options,
|
|
380
|
+
reject with `E_PROBE_FAILED` and populate `error`. If the native module is
|
|
381
|
+
unavailable, the hook throws `E_UNSUPPORTED` during render.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
import { Button, Text } from 'react-native';
|
|
385
|
+
import { useNetworkProbe } from 'rn-network-quality';
|
|
386
|
+
|
|
387
|
+
export function ProbeButton() {
|
|
388
|
+
const { probe, isProbing, result, error } = useNetworkProbe();
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<>
|
|
392
|
+
<Button
|
|
393
|
+
title={isProbing ? 'Measuring…' : 'Measure network'}
|
|
394
|
+
disabled={isProbing}
|
|
395
|
+
onPress={() => void probe().catch(() => undefined)}
|
|
396
|
+
/>
|
|
397
|
+
<Text>{result?.rttMs ?? '—'} ms</Text>
|
|
398
|
+
{error ? (
|
|
399
|
+
<Text>
|
|
400
|
+
{error.code}: {error.message}
|
|
401
|
+
</Text>
|
|
402
|
+
) : null}
|
|
403
|
+
</>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### `NetworkQualityError`
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
class NetworkQualityError extends Error {
|
|
412
|
+
readonly code: NetworkQualityErrorCode;
|
|
413
|
+
readonly cause?: unknown;
|
|
414
|
+
constructor(
|
|
415
|
+
code: NetworkQualityErrorCode,
|
|
416
|
+
message: string,
|
|
417
|
+
options?: { cause?: unknown }
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`NetworkQualityError` exposes a stable `code`. The exported codes are
|
|
423
|
+
`E_UNSUPPORTED`, `E_OFFLINE`, `E_INVALID_URL`, `E_PROBE_TIMEOUT`,
|
|
424
|
+
`E_PROBE_FAILED`, and `E_PROBE_SKIPPED`. The final code is reserved for caller
|
|
425
|
+
or policy layers; the package's built-in scheduled auto-probe skips are silent.
|
|
426
|
+
|
|
427
|
+
| Code | Meaning |
|
|
428
|
+
| ----------------- | ----------------------------------------------------------------------- |
|
|
429
|
+
| `E_UNSUPPORTED` | The TurboModule is absent, unlinked, or unavailable on this platform |
|
|
430
|
+
| `E_OFFLINE` | A probe was requested without a connected default network |
|
|
431
|
+
| `E_INVALID_URL` | A probe endpoint is malformed or is not HTTP(S) |
|
|
432
|
+
| `E_PROBE_TIMEOUT` | Latency did not finish before the whole-probe deadline |
|
|
433
|
+
| `E_PROBE_FAILED` | Latency, cancellation, cookie isolation, or probe infrastructure failed |
|
|
434
|
+
| `E_PROBE_SKIPPED` | Reserved for a JavaScript policy that elects not to probe |
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { NetworkQualityError, probeNetwork } from 'rn-network-quality';
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
await probeNetwork();
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error instanceof NetworkQualityError && error.code === 'E_OFFLINE') {
|
|
443
|
+
console.log('Connect before measuring');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Constants
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
const DEFAULT_CONFIG: NetworkQualityConfig;
|
|
452
|
+
const QUALITY_ORDER: NetworkQuality[];
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
`DEFAULT_CONFIG` contains the documented defaults. `QUALITY_ORDER` is
|
|
456
|
+
`['offline', 'poor', 'moderate', 'good', 'excellent']`; `unknown` is omitted
|
|
457
|
+
because it is not ranked.
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
import { DEFAULT_CONFIG, QUALITY_ORDER } from 'rn-network-quality';
|
|
461
|
+
|
|
462
|
+
console.log(DEFAULT_CONFIG.throttleMs, QUALITY_ORDER.indexOf('good'));
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Exported types
|
|
466
|
+
|
|
467
|
+
| Type | Purpose | Example value |
|
|
468
|
+
| ------------------------- | ------------------------------------------- | ------------------------------------------ |
|
|
469
|
+
| `Transport` | Default route kind | `'wifi'` |
|
|
470
|
+
| `CellularGeneration` | Coarse radio generation | `'5g'` |
|
|
471
|
+
| `NetworkQuality` | Classified tier | `'good'` |
|
|
472
|
+
| `QualitySource` | Evidence used by the classifier | `'probe'` |
|
|
473
|
+
| `UnsatisfiedReason` | iOS unsatisfied-path reason | `'wifiDenied'` |
|
|
474
|
+
| `NetworkSnapshot` | Raw normalized OS signals | `{ ...snapshot }` |
|
|
475
|
+
| `ProbeResult` | Active-probe measurements | `{ rttMs: 42, ... }` |
|
|
476
|
+
| `ProbeFailure` | Failed-probe code, message, transport, time | `{ code: 'E_PROBE_TIMEOUT', ... }` |
|
|
477
|
+
| `NetworkQualityState` | Snapshot plus classification | `{ quality: 'good', ... }` |
|
|
478
|
+
| `TierThreshold` | One tier's bandwidth and RTT bounds | `{ minDownlinkKbps: 5000, maxRttMs: 150 }` |
|
|
479
|
+
| `QualityThresholds` | Excellent, good, and moderate bounds | `{ excellent, good, moderate }` |
|
|
480
|
+
| `ProbeConfig` | Probe endpoints, samples, timeout, and TTL | `DEFAULT_CONFIG.probe` |
|
|
481
|
+
| `AutoProbeConfig` | Automatic-probe policy | `DEFAULT_CONFIG.autoProbe` |
|
|
482
|
+
| `NetworkQualityConfig` | Complete library configuration | `DEFAULT_CONFIG` |
|
|
483
|
+
| `NetworkQualityErrorCode` | Stable error-code union | `'E_OFFLINE'` |
|
|
484
|
+
| `DeepPartial<T>` | Recursive optional form used by `configure` | `{ autoProbe: { enabled: true } }` |
|
|
485
|
+
|
|
486
|
+
Import types with `import type`, for example:
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
import type { NetworkQualityState, ProbeConfig } from 'rn-network-quality';
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Configuration reference
|
|
493
|
+
|
|
494
|
+
`configure()` accepts a recursive partial value, so changing one nested field
|
|
495
|
+
does not reset its siblings. `getConfig()` returns a defensive copy.
|
|
496
|
+
|
|
497
|
+
| Field | Default | Meaning |
|
|
498
|
+
| -------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------- |
|
|
499
|
+
| `throttleMs` | `1000` | Minimum interval for minor native numeric updates |
|
|
500
|
+
| `bandwidthChangeThresholdPct` | `10` | Percentage movement required for bandwidth or signal updates |
|
|
501
|
+
| `validationGraceMs` | `10000` | Grace time before a newly connected unvalidated network is `poor` |
|
|
502
|
+
| `thresholds.excellent.minDownlinkKbps` | `20000` | Excellent minimum downstream rate |
|
|
503
|
+
| `thresholds.excellent.maxRttMs` | `50` | Excellent maximum round-trip time |
|
|
504
|
+
| `thresholds.good.minDownlinkKbps` | `5000` | Good minimum downstream rate |
|
|
505
|
+
| `thresholds.good.maxRttMs` | `150` | Good maximum round-trip time |
|
|
506
|
+
| `thresholds.moderate.minDownlinkKbps` | `1000` | Moderate minimum downstream rate |
|
|
507
|
+
| `thresholds.moderate.maxRttMs` | `400` | Moderate maximum round-trip time |
|
|
508
|
+
| `probe.latencyUrl` | `https://www.gstatic.com/generate_204` | Latency endpoint |
|
|
509
|
+
| `probe.downloadUrl` | `https://speed.cloudflare.com/__down?bytes=3000000` | Throughput payload endpoint; `null` disables this phase |
|
|
510
|
+
| `probe.latencySamples` | `3` | Retained requests after one warm-up |
|
|
511
|
+
| `probe.timeoutMs` | `8000` | Whole-probe time budget |
|
|
512
|
+
| `probe.downloadMaxDurationMs` | `3000` | Download time cap after the first response byte |
|
|
513
|
+
| `probe.downloadMaxBytes` | `3000000` | Maximum response-body bytes consumed by one probe |
|
|
514
|
+
| `probe.resultTtlMs` | `60000` | How long same-transport probe data influences quality |
|
|
515
|
+
| `autoProbe.enabled` | `false` | Whether scheduled probing is active |
|
|
516
|
+
| `autoProbe.intervalMs` | `60000` | Scheduled interval, clamped to at least `15000` |
|
|
517
|
+
| `autoProbe.onTransportChange` | `true` | Schedule a debounced probe after a transport change |
|
|
518
|
+
| `autoProbe.allowOnExpensive` | `false` | Permit automatic traffic on an OS-designated expensive network |
|
|
519
|
+
| `autoProbe.allowOnConstrained` | `false` | Permit automatic traffic while Data Saver or Low Data Mode is active |
|
|
520
|
+
|
|
521
|
+
Durations and thresholds must be finite and non-negative;
|
|
522
|
+
`probe.timeoutMs` and `probe.downloadMaxDurationMs` must be positive and no
|
|
523
|
+
greater than `2_147_483_647`; `probe.downloadMaxBytes` must be a positive
|
|
524
|
+
integer no greater than that value;
|
|
525
|
+
`probe.latencySamples` must be an integer from 1 through 100; probe URLs must be
|
|
526
|
+
non-empty strings; and automatic-probe flags must be booleans. URL scheme and
|
|
527
|
+
parseability are checked when a probe is started.
|
|
528
|
+
|
|
529
|
+
## State reference
|
|
530
|
+
|
|
531
|
+
`null` means the signal is not available on the current platform, OS version,
|
|
532
|
+
network, or permission state. It is different from `false` and from a zero
|
|
533
|
+
measurement.
|
|
534
|
+
|
|
535
|
+
| Field | Type | Meaning |
|
|
536
|
+
| ----------------------- | ---------------------------- | --------------------------------------------------------------------------------- |
|
|
537
|
+
| `isConnected` | `boolean` | The default path can carry internet traffic |
|
|
538
|
+
| `isValidated` | `boolean \| null` | The OS validated access beyond the local link |
|
|
539
|
+
| `isCaptivePortal` | `boolean \| null` | The OS detected a sign-in/captive portal |
|
|
540
|
+
| `transport` | `Transport` | Default route: Wi-Fi, cellular, Ethernet, Bluetooth, VPN, other, none, or unknown |
|
|
541
|
+
| `isVpn` | `boolean \| null` | Whether a VPN transport can be identified |
|
|
542
|
+
| `isExpensive` | `boolean` | The OS considers data usage potentially costly |
|
|
543
|
+
| `isConstrained` | `boolean` | Data Saver or Low Data Mode is active |
|
|
544
|
+
| `isRoaming` | `boolean \| null` | The cellular network is roaming |
|
|
545
|
+
| `downlinkKbps` | `number \| null` | OS downstream estimate in kilobits per second |
|
|
546
|
+
| `uplinkKbps` | `number \| null` | OS upstream estimate in kilobits per second |
|
|
547
|
+
| `signalStrength` | `number \| null` | Bearer-dependent Android signal value; units are not normalized across transports |
|
|
548
|
+
| `cellularGeneration` | `CellularGeneration \| null` | Coarse 2G/3G/4G/5G radio generation |
|
|
549
|
+
| `supportsIPv4` | `boolean \| null` | The route supports IPv4 |
|
|
550
|
+
| `supportsIPv6` | `boolean \| null` | The route supports IPv6 |
|
|
551
|
+
| `supportsDNS` | `boolean \| null` | DNS service is available on the route |
|
|
552
|
+
| `unsatisfiedReason` | `UnsatisfiedReason \| null` | Why iOS reports an unsatisfied path |
|
|
553
|
+
| `timestamp` | `number` | Snapshot time in Unix-epoch milliseconds |
|
|
554
|
+
| `quality` | `NetworkQuality` | Derived quality tier |
|
|
555
|
+
| `qualitySource` | `QualitySource` | Probe, OS estimate, heuristic, or none |
|
|
556
|
+
| `effectiveDownlinkKbps` | `number \| null` | Downlink value actually used for classification |
|
|
557
|
+
| `effectiveRttMs` | `number \| null` | RTT value actually used for classification |
|
|
558
|
+
| `lastProbe` | `ProbeResult \| null` | Most recent successful active probe |
|
|
559
|
+
| `lastProbeFailure` | `ProbeFailure \| null` | Most recent failed probe, cleared by success or a transport change |
|
|
560
|
+
| `reasons` | `string[]` | Human-readable classifier decisions |
|
|
561
|
+
|
|
562
|
+
`ProbeResult` has its own measurement metadata:
|
|
563
|
+
|
|
564
|
+
| Field | Type | Meaning |
|
|
565
|
+
| --------------- | ---------------- | ----------------------------------------------------------------- |
|
|
566
|
+
| `rttMs` | `number \| null` | Median retained latency in milliseconds |
|
|
567
|
+
| `downlinkKbps` | `number \| null` | Measured downstream throughput in kilobits per second |
|
|
568
|
+
| `bytesReceived` | `number` | Throughput response bytes actually consumed |
|
|
569
|
+
| `durationMs` | `number` | Total probe duration in milliseconds |
|
|
570
|
+
| `transport` | `Transport` | Transport captured when the JS layer started the probe |
|
|
571
|
+
| `downloadError` | `string \| null` | Non-fatal throughput-phase error after a successful latency phase |
|
|
572
|
+
| `timestamp` | `number` | Completion time in Unix-epoch milliseconds |
|
|
573
|
+
|
|
574
|
+
### Platform availability and mapping
|
|
575
|
+
|
|
576
|
+
| Field | Android | iOS |
|
|
577
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
578
|
+
| `isConnected` | Default network exists and has `NET_CAPABILITY_INTERNET` | `NWPath.status == .satisfied` |
|
|
579
|
+
| `isValidated` | `NET_CAPABILITY_VALIDATED` | Always `null`; no equivalent public API |
|
|
580
|
+
| `isCaptivePortal` | `NET_CAPABILITY_CAPTIVE_PORTAL` | Always `null`; no equivalent public API |
|
|
581
|
+
| `transport` | Wi-Fi, cellular, Ethernet/USB, Bluetooth, VPN-only, or other; `none` without a default network | Wi-Fi, cellular, wired Ethernet, or other; `none` when unsatisfied |
|
|
582
|
+
| `isVpn` | `TRANSPORT_VPN` is present, including VPN over another transport | Always `null`; the implementation does not guess from `utun` interfaces |
|
|
583
|
+
| `isExpensive` | Inverse of `NOT_METERED`; temporarily-not-metered is respected on API 30+ | `NWPath.isExpensive` |
|
|
584
|
+
| `isConstrained` | Data Saver enabled | `NWPath.isConstrained` (Low Data Mode) |
|
|
585
|
+
| `isRoaming` | Inverse of `NOT_ROAMING` on API 28+; otherwise `null` | Always `null` |
|
|
586
|
+
| `downlinkKbps` | `linkDownstreamBandwidthKbps`, or `null` when non-positive | Always `null` from the OS; a probe can populate `effectiveDownlinkKbps` |
|
|
587
|
+
| `uplinkKbps` | `linkUpstreamBandwidthKbps`, or `null` when non-positive | Always `null` |
|
|
588
|
+
| `signalStrength` | Available on API 29+ when specified | Always `null` |
|
|
589
|
+
| `cellularGeneration` | Available on cellular only when the host app has `READ_PHONE_STATE` | Available on cellular devices; `null` in the simulator |
|
|
590
|
+
| `supportsIPv4` / `supportsIPv6` | Derived from link-address families without exposing addresses | `NWPath.supportsIPv4` / `supportsIPv6` |
|
|
591
|
+
| `supportsDNS` | Whether DNS servers are present, without exposing their addresses | `NWPath.supportsDNS` |
|
|
592
|
+
| `unsatisfiedReason` | Always `null` | Mapped from `NWPath.unsatisfiedReason` when the path is unsatisfied |
|
|
593
|
+
| `timestamp` | `System.currentTimeMillis()` | Unix time from `Date` |
|
|
594
|
+
|
|
595
|
+
Android evaluates transport in this order: Wi-Fi, cellular, Ethernet (including
|
|
596
|
+
USB on API 31+), Bluetooth, VPN when it is the only recognized transport, then
|
|
597
|
+
other. A VPN over Wi-Fi therefore reports `transport: 'wifi'` and `isVpn: true`.
|
|
598
|
+
iOS evaluates Wi-Fi, cellular, wired Ethernet, then other/loopback; it does not
|
|
599
|
+
infer VPN state from interface names.
|
|
600
|
+
|
|
601
|
+
Android maps GPRS, EDGE, CDMA, 1xRTT, IDEN, and GSM to `2g`; UMTS, EVDO variants,
|
|
602
|
+
HSDPA, HSUPA, HSPA, EHRPD, HSPAP, and TD-SCDMA to `3g`; LTE and IWLAN to `4g`;
|
|
603
|
+
and NR to `5g`. iOS maps GPRS, Edge, and CDMA1x to `2g`; WCDMA, HSDPA, HSUPA,
|
|
604
|
+
CDMA EVDO variants, and eHRPD to `3g`; LTE to `4g`; and NR/NRNSA to `5g`.
|
|
605
|
+
Unknown radio technologies remain `null`.
|
|
606
|
+
|
|
607
|
+
When an iOS path is unsatisfied, `unsatisfiedReason` can be `notAvailable`,
|
|
608
|
+
`cellularDenied`, `wifiDenied`, `localNetworkDenied`, `vpnInactive`, or
|
|
609
|
+
`unknown`. It is `null` for a satisfied path.
|
|
610
|
+
|
|
611
|
+
Android bandwidth figures are OS/carrier estimates, not speed-test results.
|
|
612
|
+
Wi-Fi estimates are commonly derived from link speed and can be optimistic.
|
|
613
|
+
iOS exposes no public bandwidth estimate, so run an active probe when a measured
|
|
614
|
+
value is important.
|
|
615
|
+
|
|
616
|
+
Android 5G non-standalone connections often report LTE and therefore appear as
|
|
617
|
+
`4g`. Signal-strength units are bearer-dependent and should be displayed as a
|
|
618
|
+
diagnostic value rather than compared across transport types.
|
|
619
|
+
|
|
620
|
+
## How quality is computed
|
|
621
|
+
|
|
622
|
+
Classification is deterministic and follows this order:
|
|
623
|
+
|
|
624
|
+
1. A disconnected snapshot is `offline` with source `none`.
|
|
625
|
+
2. A captive portal is `poor` immediately.
|
|
626
|
+
3. A fresh same-transport probe failure newer than the last success is `poor`
|
|
627
|
+
with source `probe`. A successful probe or transport change clears it.
|
|
628
|
+
4. A newly connected but unvalidated network is `unknown` with reason
|
|
629
|
+
`validating` for `validationGraceMs`; it becomes `poor` if validation does
|
|
630
|
+
not arrive before the grace period ends.
|
|
631
|
+
5. A probe is fresh only while it is within `resultTtlMs` and was measured on
|
|
632
|
+
the current transport. A stale probe is ignored and the reason is recorded.
|
|
633
|
+
6. Fresh probe downlink takes priority over Android's OS estimate. Probe RTT is
|
|
634
|
+
used when present.
|
|
635
|
+
7. Each available metric is tiered independently. When bandwidth and RTT
|
|
636
|
+
disagree, the worse tier wins.
|
|
637
|
+
8. If no measurements exist, 4G/5G maps to `good`, 3G to `moderate`, and 2G to
|
|
638
|
+
`poor`, with source `heuristic`.
|
|
639
|
+
9. If no measurement or cellular heuristic is available, quality is `unknown`.
|
|
640
|
+
|
|
641
|
+
When a fresh probe has no usable downlink value, Android's OS downlink estimate
|
|
642
|
+
is used as a fallback while the fresh RTT is still considered. `qualitySource`
|
|
643
|
+
is `probe` when either probe metric contributes, `os-estimate` when only the OS
|
|
644
|
+
downlink contributes, `heuristic` for cellular-generation fallback, and `none`
|
|
645
|
+
otherwise. `reasons` records decisions such as
|
|
646
|
+
`downlink 12000 kbps → good`, `rtt 180 ms → moderate`, and
|
|
647
|
+
`probe stale (transport changed)`.
|
|
648
|
+
|
|
649
|
+
`isExpensive` and `isConstrained` do not lower the tier. They describe user and
|
|
650
|
+
OS policy, so apps should respect them independently—for example, by deferring
|
|
651
|
+
a background upload even when measured quality is excellent.
|
|
652
|
+
|
|
653
|
+
Default thresholds are inclusive:
|
|
654
|
+
|
|
655
|
+
| Tier | Minimum downlink | Maximum RTT |
|
|
656
|
+
| --------- | ---------------: | -----------: |
|
|
657
|
+
| Excellent | 20,000 kbps | 50 ms |
|
|
658
|
+
| Good | 5,000 kbps | 150 ms |
|
|
659
|
+
| Moderate | 1,000 kbps | 400 ms |
|
|
660
|
+
| Poor | Below 1,000 kbps | Above 400 ms |
|
|
661
|
+
|
|
662
|
+
Customize the thresholds with `configure()`. Bounds must remain monotonic:
|
|
663
|
+
excellent requires at least as much bandwidth and no more latency than good,
|
|
664
|
+
and good must be at least as strict as moderate.
|
|
665
|
+
|
|
666
|
+
## Active probing
|
|
667
|
+
|
|
668
|
+
Probing is opt-in. Calling `probeNetwork()` performs:
|
|
669
|
+
|
|
670
|
+
1. One warm-up request followed by three retained latency requests by default.
|
|
671
|
+
The reported RTT is the median of retained samples.
|
|
672
|
+
2. One optional download that stops after the first configured limit: 3 MB or
|
|
673
|
+
three seconds after the first byte by default. Both platforms require at
|
|
674
|
+
least 64 KB over at least 100 ms; otherwise `downlinkKbps` is `null` and
|
|
675
|
+
`downloadError` is `too-little-data` or `too-fast-to-measure`.
|
|
676
|
+
3. A whole-operation timeout, eight seconds by default.
|
|
677
|
+
|
|
678
|
+
Requests use random `_nq` cache-busting query values, `Cache-Control: no-cache`,
|
|
679
|
+
and isolated native sessions/connections without the host app's cookies or
|
|
680
|
+
cache. Latency failure rejects the operation. Download failure preserves the
|
|
681
|
+
latency result and sets `downloadError`.
|
|
682
|
+
|
|
683
|
+
Android's `HttpURLConnection` exposes only a process-wide `CookieHandler`. To
|
|
684
|
+
avoid reading or racing the host app's cookie store, Android fails the probe
|
|
685
|
+
with `E_PROBE_FAILED` when such a handler is installed; it never replaces or
|
|
686
|
+
mutates that handler.
|
|
687
|
+
|
|
688
|
+
### Default hosts and data use
|
|
689
|
+
|
|
690
|
+
The default latency endpoint is
|
|
691
|
+
`https://www.gstatic.com/generate_204`. The default throughput endpoint is
|
|
692
|
+
`https://speed.cloudflare.com/__down?bytes=3000000`. A default probe downloads
|
|
693
|
+
up to about 3 MB plus four small latency responses and protocol overhead.
|
|
694
|
+
Endpoint operators can observe ordinary request metadata such as source IP and
|
|
695
|
+
headers; the library adds no user identifier or telemetry.
|
|
696
|
+
|
|
697
|
+
For full control, self-host an HTTP(S) endpoint that returns an empty `204`
|
|
698
|
+
response and a static download of known size, then configure both URLs:
|
|
699
|
+
|
|
700
|
+
```ts
|
|
701
|
+
import { configure } from 'rn-network-quality';
|
|
702
|
+
|
|
703
|
+
configure({
|
|
704
|
+
probe: {
|
|
705
|
+
latencyUrl: 'https://network.example.com/204',
|
|
706
|
+
downloadUrl: 'https://network.example.com/probe-3mb.bin',
|
|
707
|
+
downloadMaxDurationMs: 3_000,
|
|
708
|
+
downloadMaxBytes: 3_000_000,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Set `downloadUrl: null` to run latency only and avoid the throughput payload.
|
|
714
|
+
Only parseable `http` and `https` URLs are accepted.
|
|
715
|
+
|
|
716
|
+
### Automatic probes
|
|
717
|
+
|
|
718
|
+
Automatic probing is disabled by default. When explicitly enabled, it runs only
|
|
719
|
+
while at least one listener exists and the app is active. The interval is
|
|
720
|
+
clamped to 15 seconds or longer. A transport change can schedule a probe after a
|
|
721
|
+
two-second debounce. Offline runs are skipped, as are expensive or constrained
|
|
722
|
+
connections unless their corresponding allow flags are enabled. Moving to the
|
|
723
|
+
background pauses the interval; returning to the foreground resumes it.
|
|
724
|
+
|
|
725
|
+
## Permissions and privacy
|
|
726
|
+
|
|
727
|
+
The library declares only Android's normal `ACCESS_NETWORK_STATE` permission.
|
|
728
|
+
It requests no runtime permission and never reads SSID, BSSID, MAC addresses,
|
|
729
|
+
location, or phone identity. It never collects, stores, or exposes IP address
|
|
730
|
+
values; Android only checks each address family to report IPv4/IPv6 support. It
|
|
731
|
+
includes no analytics or telemetry and collects no data.
|
|
732
|
+
|
|
733
|
+
Android cellular generation is the sole optional permission-dependent field.
|
|
734
|
+
If the **host app** already declares and receives `READ_PHONE_STATE`, the library
|
|
735
|
+
uses it to map the cellular radio technology. The library does not declare or
|
|
736
|
+
request that permission; without it, `cellularGeneration` is `null`.
|
|
737
|
+
|
|
738
|
+
The iOS pod includes a privacy manifest declaring no tracking and no collected
|
|
739
|
+
data. It declares Apple's System Boot Time required-reason API category with
|
|
740
|
+
reason `35F9.1` because monotonic uptime is used only to measure elapsed probe
|
|
741
|
+
and throttling intervals.
|
|
742
|
+
|
|
743
|
+
Active probes contact the configured endpoints as described above. They run
|
|
744
|
+
only after an explicit call or after the app explicitly enables auto-probing.
|
|
745
|
+
|
|
746
|
+
## Battery and performance
|
|
747
|
+
|
|
748
|
+
Monitoring is callback-driven; there is no native polling loop. Native updates
|
|
749
|
+
are de-duplicated, minor bandwidth/signal changes are thresholded, and the
|
|
750
|
+
default trailing-edge throttle is one second. Significant changes such as
|
|
751
|
+
connectivity or transport transitions emit immediately. Native I/O and snapshot
|
|
752
|
+
processing run away from the main thread, and all callbacks, monitors, timers,
|
|
753
|
+
sessions, and executors are released when monitoring stops or the module is
|
|
754
|
+
invalidated during reload.
|
|
755
|
+
|
|
756
|
+
Active probing is the only operation that intentionally generates network
|
|
757
|
+
traffic. Choose intervals conservatively and keep the expensive/constrained
|
|
758
|
+
safeguards enabled unless the product explicitly requires otherwise.
|
|
759
|
+
|
|
760
|
+
## Testing in your app
|
|
761
|
+
|
|
762
|
+
The package ships a Jest mock with the same public surface and a fixed good
|
|
763
|
+
Wi-Fi state:
|
|
764
|
+
|
|
765
|
+
```js
|
|
766
|
+
jest.mock('rn-network-quality', () => require('rn-network-quality/mock'));
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
For a reusable manual mock, create `__mocks__/rn-network-quality.js` in the host
|
|
770
|
+
app with the same export:
|
|
771
|
+
|
|
772
|
+
```js
|
|
773
|
+
module.exports = require('rn-network-quality/mock');
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
The mock performs no native I/O. Its listener supplies the stable fixed state
|
|
777
|
+
on a microtask, and its probe API resolves a fixed Wi-Fi result, making it
|
|
778
|
+
suitable for component tests.
|
|
779
|
+
|
|
780
|
+
## Comparison with `@react-native-community/netinfo`
|
|
781
|
+
|
|
782
|
+
The packages are complementary rather than drop-in replacements.
|
|
783
|
+
|
|
784
|
+
| Capability | `rn-network-quality` | `@react-native-community/netinfo` |
|
|
785
|
+
| --------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
786
|
+
| Primary purpose | Actionable link-quality signals and tiering | Broad connectivity and connection metadata |
|
|
787
|
+
| Quality classifier | Built in and configurable | Application-defined |
|
|
788
|
+
| Active RTT/throughput probe | Built in and opt-in | Reachability-oriented checks, not a throughput benchmark |
|
|
789
|
+
| Raw platform signals | Focused on quality, validation, constraints, and IP/DNS support | Broad connection details across its supported targets |
|
|
790
|
+
| Architecture | New Architecture only | Choose according to NetInfo's current compatibility matrix |
|
|
791
|
+
| Supported targets here | Android and iOS | Useful when an app also needs additional targets |
|
|
792
|
+
|
|
793
|
+
Keep NetInfo if your app relies on its broader platform coverage or existing
|
|
794
|
+
state model. Add `rn-network-quality` when you need measured/estimated link
|
|
795
|
+
quality and a consistent decision tier.
|
|
796
|
+
|
|
797
|
+
## Example app
|
|
798
|
+
|
|
799
|
+
The included app demonstrates the quality badge, adaptive-video recommendation,
|
|
800
|
+
every state field, a 60-sample bandwidth sparkline, manual probing, runtime
|
|
801
|
+
settings, event history, immediate snapshots, and Android's optional phone-state
|
|
802
|
+
permission flow.
|
|
803
|
+
|
|
804
|
+
From the repository root:
|
|
805
|
+
|
|
806
|
+
```sh
|
|
807
|
+
yarn install --immutable
|
|
808
|
+
yarn example start
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
In another terminal, run one platform:
|
|
812
|
+
|
|
813
|
+
```sh
|
|
814
|
+
yarn example android
|
|
815
|
+
yarn example ios
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
For iOS, run `bundle install` in `example/` and
|
|
819
|
+
`bundle exec pod install --project-directory=ios` after native dependency
|
|
820
|
+
changes. See [`example/README.md`](example/README.md) for network-condition
|
|
821
|
+
simulation instructions.
|
|
822
|
+
|
|
823
|
+
## Troubleshooting
|
|
824
|
+
|
|
825
|
+
### `E_UNSUPPORTED` or `NetworkQuality` is unavailable
|
|
826
|
+
|
|
827
|
+
- Confirm React Native's New Architecture is enabled and rebuild the native app.
|
|
828
|
+
- Confirm the app uses React Native 0.80 or newer.
|
|
829
|
+
- On iOS, run `pod install` in the app's `ios` directory, open the generated
|
|
830
|
+
`.xcworkspace`, clean the build folder if needed, and rebuild.
|
|
831
|
+
- On Android, clean the app's Gradle build after changing native dependencies.
|
|
832
|
+
- Confirm autolinking includes `rn-network-quality`.
|
|
833
|
+
- Expo Go cannot load custom native modules; use an Expo development build or a
|
|
834
|
+
prebuilt native project.
|
|
835
|
+
|
|
836
|
+
Importing the package is safe on an unsupported platform. `isSupported()`
|
|
837
|
+
returns `false`; native-dependent functions throw a helpful `E_UNSUPPORTED`
|
|
838
|
+
error.
|
|
839
|
+
|
|
840
|
+
### iOS Swift generated-header errors
|
|
841
|
+
|
|
842
|
+
The pod defines the Swift module `rn_network_quality`, while the package name
|
|
843
|
+
contains hyphens. Do not copy native source files into the host target. Re-run
|
|
844
|
+
Pods installation, ensure the app builds the workspace, and remove stale Xcode
|
|
845
|
+
Derived Data if an old generated header remains cached. The library's
|
|
846
|
+
Objective-C++ adapter already handles modular and local generated-header forms.
|
|
847
|
+
|
|
848
|
+
### Values are `null` on iOS
|
|
849
|
+
|
|
850
|
+
iOS does not publish validation, captive-portal, VPN, roaming, bandwidth,
|
|
851
|
+
signal-strength, or upstream-bandwidth values through `NWPathMonitor`. These
|
|
852
|
+
fields intentionally remain `null`; use `probeNetwork()` for measured RTT and
|
|
853
|
+
downlink throughput.
|
|
854
|
+
|
|
855
|
+
### Cellular generation is `null`
|
|
856
|
+
|
|
857
|
+
The iOS simulator has no cellular radio. On Android, the host app must opt into
|
|
858
|
+
`READ_PHONE_STATE` and receive runtime approval before this field is read. 5G
|
|
859
|
+
NSA may still be reported by Android as LTE/`4g`.
|
|
860
|
+
|
|
861
|
+
### Emulator results look unrealistic
|
|
862
|
+
|
|
863
|
+
Virtual devices often report synthetic link speeds, no cellular generation, or
|
|
864
|
+
instant transport transitions. Use Android Emulator network speed/delay controls
|
|
865
|
+
or Apple's Network Link Conditioner, then validate important behavior on real
|
|
866
|
+
hardware.
|
|
867
|
+
|
|
868
|
+
### A probe has RTT but no downlink result
|
|
869
|
+
|
|
870
|
+
Read `downloadError` for a timeout, transport error, or rejected endpoint. If it
|
|
871
|
+
is `too-little-data`, fewer than 64 KB arrived; `too-fast-to-measure` means the
|
|
872
|
+
configured byte cap arrived in under 100 ms. These cases are not treated as
|
|
873
|
+
request failures, and latency remains valid by design. Confirm the URL is
|
|
874
|
+
HTTP(S), returns at least `downloadMaxBytes`, and is accessible from the device.
|
|
875
|
+
|
|
876
|
+
### State is not emitted for every tiny signal change
|
|
877
|
+
|
|
878
|
+
This is expected. Identical snapshots are removed and small numeric changes are
|
|
879
|
+
filtered by `bandwidthChangeThresholdPct`. Larger numeric changes use the
|
|
880
|
+
configured trailing-edge throttle. Set `throttleMs: 0` and lower the percentage
|
|
881
|
+
only when the extra render and callback volume is acceptable.
|
|
882
|
+
|
|
883
|
+
## Contributing
|
|
884
|
+
|
|
885
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, validation commands, native
|
|
886
|
+
tests, manual QA, commit conventions, and release administration. Community
|
|
887
|
+
participation follows the [Code of Conduct](CODE_OF_CONDUCT.md). Security issues
|
|
888
|
+
should be reported through the private process in [SECURITY.md](SECURITY.md).
|
|
889
|
+
|
|
890
|
+
## Versioning
|
|
891
|
+
|
|
892
|
+
Releases follow Semantic Versioning and Conventional Commits. `fix:` changes
|
|
893
|
+
drive patch releases, `feat:` changes drive minor releases, and breaking changes
|
|
894
|
+
drive major releases. Before 1.0, breaking changes increase the minor version.
|
|
895
|
+
Release notes and `CHANGELOG.md` are generated by `release-it`.
|
|
896
|
+
|
|
897
|
+
## License
|
|
898
|
+
|
|
899
|
+
[MIT](LICENSE) © 2026 kruthik
|