@akbeniwal/react-native-smart-netinfo 1.0.4 → 1.0.6

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.
@@ -14,7 +14,7 @@ RCT_EXPORT_MODULE()
14
14
  _queue = dispatch_queue_create("com.smartnetinfo.networkmonitor", NULL);
15
15
  _monitor = nw_path_monitor_create();
16
16
 
17
- __weak typeof(self) weakSelf = self;
17
+ __weak ReactNativeSmartNetinfo *weakSelf = self;
18
18
  nw_path_monitor_set_update_handler(_monitor, ^(nw_path_t path) {
19
19
  nw_path_status_t status = nw_path_get_status(path);
20
20
  BOOL isConnected = (status == nw_path_status_satisfied);
@@ -65,9 +65,4 @@ RCT_EXPORT_MODULE()
65
65
  return std::make_shared<facebook::react::NativeReactNativeSmartNetinfoSpecJSI>(params);
66
66
  }
67
67
 
68
- + (NSString *)moduleName
69
- {
70
- return @"ReactNativeSmartNetinfo";
71
- }
72
-
73
68
  @end
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@akbeniwal/react-native-smart-netinfo",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "A lightweight smart network monitoring library for React Native with internet reachability, latency monitoring, connection quality detection and speed testing.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
+ "src",
8
9
  "dist",
9
10
  "android",
10
11
  "ios",
@@ -0,0 +1,8 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+
3
+ export interface Spec extends TurboModule {
4
+ addListener(eventName: string): void;
5
+ removeListeners(count: number): void;
6
+ }
7
+
8
+ export default TurboModuleRegistry.getEnforcing<Spec>('ReactNativeSmartNetinfo');
@@ -0,0 +1,342 @@
1
+ import { AppState, AppStateStatus, NativeEventEmitter } from 'react-native';
2
+ import NativeReactNativeSmartNetinfo from './NativeReactNativeSmartNetinfo';
3
+ import { NetworkState, SmartNetInfoConfig, NetworkStateListener } from './types';
4
+ import { getConnectionQuality } from './utils/getConnectionQuality';
5
+ import { getLatency } from './utils/getLatency';
6
+ import { runSpeedTest as executeSpeedTest } from './utils/runSpeedTest';
7
+
8
+ const PING_URLS = [
9
+ 'https://clients3.google.com/generate_204',
10
+ 'https://www.apple.com/library/test/success.html',
11
+ 'https://cloudflare-dns.com/dns-query',
12
+ 'https://google.com/generate_204'
13
+ ];
14
+
15
+ const SPEED_TEST_URL = 'https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js';
16
+
17
+ class SmartNetInfoManager {
18
+ private pingUrlIndex = 0;
19
+ private config: Required<SmartNetInfoConfig> = {
20
+ pingIntervalMs: 30000,
21
+ timeoutMs: 5000,
22
+ speedTestFileSizeInBytes: 90000,
23
+ disableAutoSpeedTest: false,
24
+ };
25
+
26
+ private state: NetworkState = {
27
+ isConnected: null,
28
+ isInternetReachable: null,
29
+ latencyMs: null,
30
+ connectionQuality: null,
31
+ internetSpeed: null,
32
+ isTestingSpeed: false,
33
+ };
34
+
35
+ private listeners = new Set<NetworkStateListener>();
36
+ private timeoutId: ReturnType<typeof setTimeout> | null = null;
37
+ private appStateSubscription: { remove: () => void } | null = null;
38
+ private nativeEventEmitter: NativeEventEmitter | null = null;
39
+ private nativeEventSubscription: { remove: () => void } | null = null;
40
+ private isMonitoring = false;
41
+ private isChecking = false;
42
+
43
+ /**
44
+ * Configure the SmartNetInfo options.
45
+ * If already monitoring, it will restart the monitoring process to apply new intervals.
46
+ */
47
+ public configure(config: Partial<SmartNetInfoConfig>): void {
48
+ this.config = { ...this.config, ...config };
49
+ if (this.isMonitoring) {
50
+ this.stopMonitoring();
51
+ this.startMonitoring();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Fetches the current network state immediately by performing a connectivity check.
57
+ */
58
+ public async fetch(): Promise<NetworkState> {
59
+ await this.checkConnectivity();
60
+ return this.state;
61
+ }
62
+
63
+ /**
64
+ * Subscribe to network state changes.
65
+ * Automatically starts monitoring if it was not already running.
66
+ *
67
+ * @param listener Callback function receiving the updated NetworkState
68
+ * @returns A cleanup function to unsubscribe
69
+ */
70
+ public addEventListener(listener: NetworkStateListener): () => void {
71
+ this.listeners.add(listener);
72
+
73
+ // Provide the current state immediately to the new listener
74
+ listener(this.state);
75
+
76
+ if (!this.isMonitoring) {
77
+ this.startMonitoring();
78
+ }
79
+
80
+ return () => this.removeEventListener(listener);
81
+ }
82
+
83
+ /**
84
+ * Unsubscribe a listener from network state changes.
85
+ * Automatically stops monitoring if no listeners remain.
86
+ *
87
+ * @param listener Callback function to unsubscribe
88
+ */
89
+ public removeEventListener(listener: NetworkStateListener): void {
90
+ this.listeners.delete(listener);
91
+ if (this.listeners.size === 0 && this.isMonitoring) {
92
+ this.stopMonitoring();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Triggers an internet speed test manually.
98
+ *
99
+ * @returns Estimated download speed in Mbps, or null if test fails
100
+ */
101
+ public async runSpeedTest(): Promise<number | null> {
102
+ if (this.state.isTestingSpeed) {
103
+ return this.state.internetSpeed;
104
+ }
105
+
106
+ this.updateState({ isTestingSpeed: true });
107
+
108
+ const timeout = Math.max(this.config.timeoutMs * 3, 15000);
109
+ const speed = await executeSpeedTest(SPEED_TEST_URL, this.config.speedTestFileSizeInBytes, timeout);
110
+
111
+ this.updateState({
112
+ internetSpeed: speed,
113
+ isTestingSpeed: false,
114
+ });
115
+
116
+ return speed;
117
+ }
118
+
119
+ /**
120
+ * Start monitoring network status (polling, AppState transitions, and browser online/offline events).
121
+ */
122
+ public startMonitoring(): void {
123
+ if (this.isMonitoring) return;
124
+ this.isMonitoring = true;
125
+
126
+ // Set up Native Event Emitter if available
127
+ try {
128
+ if (NativeReactNativeSmartNetinfo) {
129
+ this.nativeEventEmitter = new NativeEventEmitter(NativeReactNativeSmartNetinfo as any);
130
+ this.nativeEventSubscription = this.nativeEventEmitter.addListener(
131
+ 'NetworkStatusChanged',
132
+ this.handleNativeNetworkChange
133
+ );
134
+ }
135
+ } catch (e) {
136
+ console.warn('SmartNetInfo native module not found, falling back to pure polling');
137
+ }
138
+
139
+ // Run initial check immediately and then schedule subsequent checks
140
+ this.checkConnectivity().then(() => {
141
+ // Auto run speed test on initial connect if online
142
+ if (
143
+ this.state.isInternetReachable &&
144
+ this.state.internetSpeed === null &&
145
+ !this.config.disableAutoSpeedTest
146
+ ) {
147
+ this.runSpeedTest();
148
+ }
149
+ this.scheduleNextCheck();
150
+ });
151
+
152
+ // React Native AppState listener to detect foreground transitions
153
+ try {
154
+ this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
155
+ } catch (error) {
156
+ console.warn('SmartNetInfo failed to register AppState listener:', error);
157
+ }
158
+
159
+ // Web browser window event listeners for react-native-web compatibility
160
+ const hasWindowListeners = typeof window !== 'undefined' && typeof window.addEventListener === 'function';
161
+ if (hasWindowListeners) {
162
+ window.addEventListener('online', this.handleWebOnline);
163
+ window.addEventListener('offline', this.handleWebOffline);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Stop monitoring network status and clean up timers and listeners.
169
+ */
170
+ public stopMonitoring(): void {
171
+ if (!this.isMonitoring) return;
172
+ this.isMonitoring = false;
173
+
174
+ if (this.timeoutId) {
175
+ clearTimeout(this.timeoutId);
176
+ this.timeoutId = null;
177
+ }
178
+
179
+ if (this.nativeEventSubscription) {
180
+ this.nativeEventSubscription.remove();
181
+ this.nativeEventSubscription = null;
182
+ }
183
+
184
+ if (this.appStateSubscription) {
185
+ if (typeof this.appStateSubscription.remove === 'function') {
186
+ this.appStateSubscription.remove();
187
+ } else {
188
+ // Fallback for older React Native versions
189
+ (AppState as any).removeEventListener?.('change', this.handleAppStateChange);
190
+ }
191
+ this.appStateSubscription = null;
192
+ }
193
+
194
+ const hasWindowListeners = typeof window !== 'undefined' && typeof window.removeEventListener === 'function';
195
+ if (hasWindowListeners) {
196
+ window.removeEventListener('online', this.handleWebOnline);
197
+ window.removeEventListener('offline', this.handleWebOffline);
198
+ }
199
+ }
200
+
201
+ private handleNativeNetworkChange = (event: { isConnected: boolean }) => {
202
+ if (!event.isConnected) {
203
+ this.updateState({
204
+ isConnected: false,
205
+ isInternetReachable: false,
206
+ latencyMs: null,
207
+ connectionQuality: null,
208
+ });
209
+ if (this.timeoutId) {
210
+ clearTimeout(this.timeoutId);
211
+ this.timeoutId = null;
212
+ }
213
+ } else {
214
+ this.updateState({ isConnected: true });
215
+ this.checkConnectivity().finally(() => {
216
+ this.scheduleNextCheck();
217
+ });
218
+ }
219
+ };
220
+
221
+ private handleAppStateChange = (nextAppState: AppStateStatus): void => {
222
+ if (nextAppState === 'active') {
223
+ this.checkConnectivity().finally(() => {
224
+ this.scheduleNextCheck();
225
+ });
226
+ }
227
+ };
228
+
229
+ private handleWebOnline = (): void => {
230
+ this.updateState({ isConnected: true, isInternetReachable: true });
231
+ this.checkConnectivity().finally(() => {
232
+ this.scheduleNextCheck();
233
+ });
234
+ };
235
+
236
+ private handleWebOffline = (): void => {
237
+ this.updateState({
238
+ isConnected: false,
239
+ isInternetReachable: false,
240
+ latencyMs: null,
241
+ connectionQuality: null,
242
+ });
243
+ };
244
+
245
+ private scheduleNextCheck(): void {
246
+ if (!this.isMonitoring) return;
247
+
248
+ if (this.timeoutId) {
249
+ clearTimeout(this.timeoutId);
250
+ this.timeoutId = null;
251
+ }
252
+
253
+ if (this.config.pingIntervalMs <= 0) return;
254
+
255
+ // If native module explicitly told us we are offline (isConnected: false),
256
+ // we DO NOT poll at all to save battery. We wait for native event.
257
+ if (this.state.isConnected === false && this.nativeEventEmitter) {
258
+ return;
259
+ }
260
+
261
+ // When offline (without native fallback), check more frequently (every 3 seconds) to detect recovery quickly.
262
+ // When online, use the configured pingIntervalMs.
263
+ const isOffline = this.state.isInternetReachable === false;
264
+ const interval = isOffline
265
+ ? Math.min(this.config.pingIntervalMs, 3000)
266
+ : this.config.pingIntervalMs;
267
+
268
+ this.timeoutId = setTimeout(async () => {
269
+ await this.checkConnectivity();
270
+ this.scheduleNextCheck();
271
+ }, interval);
272
+ }
273
+
274
+ private async checkConnectivity(): Promise<void> {
275
+ if (this.isChecking) return;
276
+ this.isChecking = true;
277
+
278
+ try {
279
+ // Add a buffer to timeout when we are currently offline to allow the radio to wake up
280
+ const isCurrentlyOffline = this.state.isInternetReachable === false;
281
+ const actualTimeout = isCurrentlyOffline
282
+ ? Math.max(this.config.timeoutMs, 4000)
283
+ : this.config.timeoutMs;
284
+
285
+ const currentPingUrl = PING_URLS[this.pingUrlIndex];
286
+ // Cycle to the next URL for the next check to avoid DNS caching issues if it fails
287
+ this.pingUrlIndex = (this.pingUrlIndex + 1) % PING_URLS.length;
288
+
289
+ const { isReachable, latencyMs } = await getLatency(currentPingUrl, actualTimeout);
290
+
291
+ const wasInternetReachable = this.state.isInternetReachable;
292
+
293
+ this.updateState({
294
+ isConnected: isReachable,
295
+ isInternetReachable: isReachable,
296
+ latencyMs,
297
+ connectionQuality: getConnectionQuality(latencyMs),
298
+ });
299
+
300
+ // If internet just became reachable and we haven't run speed test yet, trigger auto speed test
301
+ if (
302
+ isReachable &&
303
+ wasInternetReachable === false &&
304
+ this.state.internetSpeed === null &&
305
+ !this.config.disableAutoSpeedTest
306
+ ) {
307
+ this.runSpeedTest();
308
+ }
309
+ } catch (error) {
310
+ console.warn('SmartNetInfo failed during checkConnectivity:', error);
311
+ } finally {
312
+ this.isChecking = false;
313
+ }
314
+ }
315
+
316
+ private updateState(partialState: Partial<NetworkState>): void {
317
+ const nextState = { ...this.state, ...partialState };
318
+
319
+ // Check if anything actually changed
320
+ const hasChanged = Object.keys(nextState).some(
321
+ (key) => (nextState as any)[key] !== (this.state as any)[key]
322
+ );
323
+
324
+ if (hasChanged) {
325
+ this.state = nextState;
326
+ this.notifyListeners();
327
+ }
328
+ }
329
+
330
+ private notifyListeners(): void {
331
+ this.listeners.forEach((listener) => {
332
+ try {
333
+ listener(this.state);
334
+ } catch (error) {
335
+ console.error('Error in SmartNetInfo listener:', error);
336
+ }
337
+ });
338
+ }
339
+ }
340
+
341
+ export const SmartNetInfo = new SmartNetInfoManager();
342
+ export default SmartNetInfo;
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { SmartNetInfo, default } from './SmartNetInfo';
2
+ export * from './types';
package/src/types.ts ADDED
@@ -0,0 +1,29 @@
1
+ export type ConnectionQuality = 'poor' | 'good' | 'excellent';
2
+
3
+ export interface NetworkState {
4
+ /** True if the network check succeeded, false if it failed or timed out */
5
+ isConnected: boolean | null;
6
+ /** True if internet reachability check succeeded */
7
+ isInternetReachable: boolean | null;
8
+ /** Round-trip ping latency in milliseconds */
9
+ latencyMs: number | null;
10
+ /** Connection quality based on latency ('poor' | 'good' | 'excellent') */
11
+ connectionQuality: ConnectionQuality | null;
12
+ /** Estimated internet download speed in Mbps (automatically measured when online) */
13
+ internetSpeed: number | null;
14
+ /** True if a speed test is currently running */
15
+ isTestingSpeed: boolean;
16
+ }
17
+
18
+ export interface SmartNetInfoConfig {
19
+ /** The interval in milliseconds to poll the connection (defaults to 30000ms, set to 0 to disable polling) */
20
+ pingIntervalMs?: number;
21
+ /** The fetch timeout in milliseconds (defaults to 5000ms) */
22
+ timeoutMs?: number;
23
+ /** File size of the speed test URL in bytes (defaults to 90000 bytes for jQuery) */
24
+ speedTestFileSizeInBytes?: number;
25
+ /** If true, disables the automatic speed test on mount/online transitions (defaults to false) */
26
+ disableAutoSpeedTest?: boolean;
27
+ }
28
+
29
+ export type NetworkStateListener = (state: NetworkState) => void;
@@ -0,0 +1,20 @@
1
+ import { ConnectionQuality } from '../types';
2
+
3
+ /**
4
+ * Categorizes a round-trip connection latency (in milliseconds) into a descriptive rating.
5
+ *
6
+ * @param latencyMs Latency in milliseconds or null if offline/unknown
7
+ * @returns ConnectionQuality rating or null
8
+ */
9
+ export function getConnectionQuality(latencyMs: number | null): ConnectionQuality | null {
10
+ if (latencyMs === null || latencyMs < 0) {
11
+ return null;
12
+ }
13
+ if (latencyMs < 150) {
14
+ return 'excellent';
15
+ }
16
+ if (latencyMs < 400) {
17
+ return 'good';
18
+ }
19
+ return 'poor';
20
+ }
@@ -0,0 +1,61 @@
1
+ export interface LatencyResult {
2
+ /** True if the ping request responded with an HTTP status < 400 */
3
+ isReachable: boolean;
4
+ /** Latency in milliseconds, or null if the check timed out or failed */
5
+ latencyMs: number | null;
6
+ }
7
+
8
+ /**
9
+ * Pings a URL to verify internet reachability and measure round-trip latency.
10
+ * Uses an AbortController to support timeouts.
11
+ *
12
+ * @param pingUrl The target URL to ping
13
+ * @param timeoutMs Request timeout in milliseconds
14
+ * @returns A promise resolving to LatencyResult
15
+ */
16
+ export async function getLatency(pingUrl: string, timeoutMs: number): Promise<LatencyResult> {
17
+ const startTime = Date.now();
18
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
19
+ try {
20
+ const controller = new AbortController();
21
+ timeoutId = setTimeout(() => {
22
+ controller.abort();
23
+ }, timeoutMs);
24
+
25
+ // Append dynamic timestamp and random string to strictly bypass network/DNS caching
26
+ const uniqueUrl = `${pingUrl}${pingUrl.includes('?') ? '&' : '?'}t=${Date.now()}&r=${Math.random().toString().slice(2)}`;
27
+
28
+ // Perform GET request to verify connectivity.
29
+ // We use GET instead of HEAD for maximum compatibility across various CDNs and proxies.
30
+ // Since clients3.google.com/generate_204 returns a 204 No Content response,
31
+ // using GET has negligible bandwidth usage.
32
+ const response = await fetch(uniqueUrl, {
33
+ method: 'GET',
34
+ signal: controller.signal,
35
+ headers: {
36
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
37
+ 'Pragma': 'no-cache',
38
+ 'Expires': '0',
39
+ },
40
+ });
41
+
42
+ // Fully consume the response body to prevent connection leaks in React Native
43
+ try {
44
+ await response.text();
45
+ } catch (e) {
46
+ // ignore
47
+ }
48
+
49
+ const isReachable = response.ok || response.status < 400;
50
+ const latencyMs = Date.now() - startTime;
51
+ return { isReachable, latencyMs };
52
+ } catch (error) {
53
+ console.warn(`[SmartNetInfo] getLatency failed:`, error instanceof Error ? error.message : error);
54
+ // If it fails due to network/timeout, we are offline.
55
+ return { isReachable: false, latencyMs: null };
56
+ } finally {
57
+ if (timeoutId) {
58
+ clearTimeout(timeoutId);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Runs a download speed test by fetching a remote asset, reading the response
3
+ * completely, and calculating the throughput speed in Mbps.
4
+ *
5
+ * @param speedTestUrl URL of the remote asset to download
6
+ * @param speedTestFileSizeInBytes Known size of the remote asset in bytes
7
+ * @returns A promise resolving to the estimated download speed in Mbps, or null if the test fails
8
+ */
9
+ export async function runSpeedTest(
10
+ speedTestUrl: string,
11
+ speedTestFileSizeInBytes: number,
12
+ timeoutMs: number = 15000
13
+ ): Promise<number | null> {
14
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
15
+ try {
16
+ const startTime = Date.now();
17
+ const controller = new AbortController();
18
+
19
+ timeoutId = setTimeout(() => {
20
+ controller.abort();
21
+ }, timeoutMs);
22
+
23
+ const uniqueUrl = `${speedTestUrl}${speedTestUrl.includes('?') ? '&' : '?'}t=${Date.now()}&r=${Math.random().toString().slice(2)}`;
24
+
25
+ const response = await fetch(uniqueUrl, {
26
+ method: 'GET',
27
+ signal: controller.signal,
28
+ headers: {
29
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
30
+ 'Pragma': 'no-cache',
31
+ 'Expires': '0',
32
+ },
33
+ });
34
+
35
+ if (!response.ok) {
36
+ throw new Error(`Speed test download failed with status: ${response.status}`);
37
+ }
38
+
39
+ // Fully consume the response body so that we measure the complete download duration
40
+ await response.text();
41
+
42
+ const durationSec = (Date.now() - startTime) / 1000;
43
+ if (durationSec <= 0) {
44
+ return null;
45
+ }
46
+
47
+ const fileSizeBits = speedTestFileSizeInBytes * 8;
48
+ const speedMbps = fileSizeBits / durationSec / 1000000;
49
+
50
+ // Round to 2 decimal places (e.g. 15.45)
51
+ return Math.round(speedMbps * 100) / 100;
52
+ } catch (error) {
53
+ console.warn('Network speed test failed:', error);
54
+ return null;
55
+ } finally {
56
+ if (timeoutId) {
57
+ clearTimeout(timeoutId);
58
+ }
59
+ }
60
+ }