-
Notifications
You must be signed in to change notification settings - Fork 0
feat: network-awarness [rn] #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cc80e16
dc22d23
24119d1
d859cf0
9c3abf3
602c45d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| package="com.metarouter.reactnative"> | ||
| <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||
| </manifest> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| package com.metarouter.reactnative | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. native kotlin monitor |
||
|
|
||
| import android.net.ConnectivityManager | ||
| import android.net.Network | ||
| import android.net.NetworkCapabilities | ||
| import android.net.NetworkRequest | ||
| import android.content.Context | ||
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
| import com.facebook.react.bridge.ReactMethod | ||
| import com.facebook.react.bridge.Promise | ||
| import com.facebook.react.bridge.Arguments | ||
| import com.facebook.react.modules.core.DeviceEventManagerModule | ||
|
|
||
| class MetaRouterNetworkMonitorModule( | ||
| private val reactContext: ReactApplicationContext | ||
| ) : ReactContextBaseJavaModule(reactContext) { | ||
|
|
||
| override fun getName(): String = "MetaRouterNetworkMonitor" | ||
|
|
||
| @Volatile | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ensure that reads + writes go to main memory, ensures that if two threads are on diff cores we aren't reading stale cached copy. Helps to ensure we don't run into subtle caching issues. |
||
| private var isConnected: Boolean = true | ||
|
|
||
| private var connectivityManager: ConnectivityManager? = null | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. allow null for graceful degradation if not avail |
||
| private var networkCallback: ConnectivityManager.NetworkCallback? = null | ||
|
|
||
| init { | ||
| connectivityManager = try { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. grab reference to connectivity service |
||
| reactContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager | ||
| } catch (_: Exception) { | ||
| null | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log here for debugging or intentionally swallowing exception when setting |
||
| } | ||
|
|
||
| // Snapshot initial state | ||
| connectivityManager?.let { cm -> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this .let means skip entire block if null |
||
| isConnected = try { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sets initial state |
||
| val network = cm.activeNetwork | ||
| val caps = network?.let { cm.getNetworkCapabilities(it) } | ||
| caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true | ||
| } catch (_: Exception) { | ||
| true // fallback | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, wasn't sure if you wanted to log the exception at lest. |
||
| } | ||
| } | ||
|
|
||
| // Register callback for changes | ||
| connectivityManager?.let { cm -> | ||
| val request = NetworkRequest.Builder() | ||
| .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) | ||
| .build() | ||
|
|
||
| val cb = object : ConnectivityManager.NetworkCallback() { | ||
| override fun onAvailable(network: Network) { | ||
| if (!isConnected) { | ||
| isConnected = true | ||
| sendEvent(true) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this sendEvent is sending the event across the react native bridge 💪 🌉 . |
||
| } | ||
| } | ||
|
|
||
| override fun onLost(network: Network) { | ||
| val activeNet = cm.activeNetwork | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a bit smarter bc if wifi drops we can't just respond to the onLost event we need to check if cellular connection still exists via the activeNet |
||
| val caps = activeNet?.let { cm.getNetworkCapabilities(it) } | ||
| val hasInternet = caps?.hasCapability( | ||
| NetworkCapabilities.NET_CAPABILITY_INTERNET | ||
| ) == true | ||
| if (!hasInternet && isConnected) { | ||
| isConnected = false | ||
| sendEvent(false) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. send false, no longer connected |
||
| } | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| cm.registerNetworkCallback(request, cb) | ||
| networkCallback = cb | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. register with the service then save callback for cleanup |
||
| } catch (_: SecurityException) { | ||
| // Missing ACCESS_NETWORK_STATE — fallback to always connected | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log? |
||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun sendEvent(connected: Boolean) { | ||
| val params = Arguments.createMap().apply { | ||
| putBoolean("isConnected", connected) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. defining shape of JS bridge event {isConnected: boolean} |
||
| } | ||
| try { | ||
| reactContext | ||
| .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) | ||
| .emit("onConnectivityChange", params) | ||
| } catch (_: Exception) { | ||
| // JS runtime not ready yet — ignore | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Log? |
||
| } | ||
| } | ||
|
|
||
| @ReactMethod | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. react methods we expose |
||
| fun getCurrentStatus(promise: Promise) { | ||
| promise.resolve(isConnected) | ||
| } | ||
|
|
||
| @ReactMethod | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. need to have these events otherwise there are warnings, no-op |
||
| fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) { | ||
| // Required for RN event emitter | ||
| } | ||
|
|
||
| @ReactMethod | ||
| fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Int) { | ||
| // Required for RN event emitter | ||
| } | ||
|
|
||
| override fun invalidate() { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unregister callback on termination |
||
| networkCallback?.let { cb -> | ||
| try { | ||
| connectivityManager?.unregisterNetworkCallback(cb) | ||
| } catch (_: Exception) { | ||
| // ignore | ||
| } | ||
| } | ||
| networkCallback = null | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,11 +5,14 @@ import com.facebook.react.bridge.NativeModule | |
| import com.facebook.react.bridge.ReactApplicationContext | ||
| import com.facebook.react.uimanager.ViewManager | ||
|
|
||
| class MetaRouterQueueStoragePackage : ReactPackage { | ||
| class MetaRouterPackage : ReactPackage { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. renaming bc now the native modules include more than just queue |
||
| override fun createNativeModules( | ||
| reactContext: ReactApplicationContext | ||
| ): List<NativeModule> { | ||
| return listOf(MetaRouterQueueStorageModule(reactContext)) | ||
| return listOf( | ||
| MetaRouterQueueStorageModule(reactContext), | ||
| MetaRouterNetworkMonitorModule(reactContext) | ||
| ) | ||
| } | ||
|
|
||
| override fun createViewManagers( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| #import <React/RCTBridgeModule.h> | ||
| #import <React/RCTEventEmitter.h> | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. objective c implentation |
||
| #import <Network/Network.h> | ||
|
|
||
| @interface MetaRouterNetworkMonitor : RCTEventEmitter <RCTBridgeModule> | ||
| @end | ||
|
|
||
| @implementation MetaRouterNetworkMonitor { | ||
| nw_path_monitor_t _monitor; | ||
| dispatch_queue_t _monitorQueue; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for iOS we need to set up this queue so _isConnected is only ever read or written from one thread on a time. Objective c concurrency pattern |
||
| BOOL _isConnected; | ||
| BOOL _hasListeners; | ||
| } | ||
|
|
||
| RCT_EXPORT_MODULE() | ||
|
|
||
| + (BOOL)requiresMainQueueSetup { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't initialize or block main thread while setting up |
||
| return NO; | ||
| } | ||
|
|
||
| - (instancetype)init { | ||
| self = [super init]; | ||
| if (self) { | ||
| _isConnected = YES; // optimistic default | ||
| _hasListeners = NO; | ||
| _monitorQueue = dispatch_queue_create("com.metarouter.network.monitor", DISPATCH_QUEUE_SERIAL); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. info on the queue: https://en.wikipedia.org/wiki/Grand_Central_Dispatch |
||
| _monitor = nw_path_monitor_create(); | ||
|
|
||
| __weak typeof(self) weakSelf = self; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. using weak reference here so that this can be properly deallocated |
||
| nw_path_monitor_set_update_handler(_monitor, ^(nw_path_t path) { | ||
| __strong typeof(weakSelf) strongSelf = weakSelf; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if this is nil (so reference was deallocated) then we go ahead and return. |
||
| if (!strongSelf) return; | ||
|
|
||
| BOOL connected = (nw_path_get_status(path) == nw_path_status_satisfied); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nw path gives us overall status. not wifi vs. cellular |
||
| BOOL changed = (connected != strongSelf->_isConnected); | ||
| strongSelf->_isConnected = connected; | ||
|
|
||
| if (changed && strongSelf->_hasListeners) { | ||
| [strongSelf sendEventWithName:@"onConnectivityChange" | ||
| body:@{@"isConnected": @(connected)}]; | ||
| } | ||
| }); | ||
|
|
||
| nw_path_monitor_set_queue(_monitor, _monitorQueue); | ||
| nw_path_monitor_start(_monitor); | ||
| } | ||
| return self; | ||
| } | ||
|
|
||
| - (NSArray<NSString *> *)supportedEvents { | ||
| return @[@"onConnectivityChange"]; | ||
| } | ||
|
|
||
| - (void)startObserving { | ||
| _hasListeners = YES; | ||
| } | ||
|
|
||
| - (void)stopObserving { | ||
| _hasListeners = NO; | ||
| } | ||
|
|
||
| RCT_EXPORT_METHOD(getCurrentStatus:(RCTPromiseResolveBlock)resolve | ||
| rejecter:(RCTPromiseRejectBlock)reject) | ||
| { | ||
| dispatch_async(_monitorQueue, ^{ | ||
| resolve(@(self->_isConnected)); | ||
| }); | ||
| } | ||
|
|
||
| - (void)dealloc { | ||
| if (_monitor) { | ||
| nw_path_monitor_cancel(_monitor); | ||
| } | ||
| } | ||
|
|
||
| @end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need this in the sdk now since we are accessing network state, this gets merged with the consuming app's manifest