react-native-nitro-geolocation 0.1.0 → 0.1.2

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.
@@ -10,10 +10,11 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
10
10
  }
11
11
 
12
12
  private struct LocationRequest {
13
+ let id: UUID = UUID()
13
14
  let success: (GeolocationResponse) -> Void
14
15
  let error: ((GeolocationError) -> Void)?
15
16
  let options: ParsedOptions
16
- var timer: Timer?
17
+ var timer: DispatchSourceTimer?
17
18
  }
18
19
 
19
20
  private struct WatchSubscription {
@@ -22,7 +23,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
22
23
  let options: ParsedOptions
23
24
  }
24
25
 
25
- private struct ParsedOptions {
26
+ struct ParsedOptions {
26
27
  let timeout: Double
27
28
  let maximumAge: Double
28
29
  let accuracy: CLLocationAccuracy
@@ -51,7 +52,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
51
52
  // MARK: - Properties
52
53
 
53
54
  private var locationManager: CLLocationManager?
54
- private var lastLocation: CLLocation?
55
+ internal private(set) var lastLocation: CLLocation?
55
56
  private var usingSignificantChanges: Bool = false
56
57
 
57
58
  // Authorization
@@ -84,36 +85,32 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
84
85
  success: (() -> Void)?,
85
86
  error: ((GeolocationError) -> Void)?
86
87
  ) {
87
- DispatchQueue.main.async { [weak self] in
88
- guard let self = self else { return }
89
-
90
- self.initializeLocationManagerIfNeeded()
91
- self.enqueueAuthorizationCallbacks(success: success, error: error)
92
-
93
- // Skip permission requests if configured
94
- if skipPermissionRequests {
95
- if enableBackgroundLocationUpdates {
96
- self.enableBackgroundLocationUpdatesIfNeeded()
97
- }
98
- self.handleAuthorizationSuccess()
99
- return
100
- }
88
+ initializeLocationManagerIfNeeded()
89
+ enqueueAuthorizationCallbacks(success: success, error: error)
101
90
 
102
- // Check if already authorized
103
- let currentStatus = CLLocationManager.authorizationStatus()
104
- if currentStatus == .authorizedAlways || currentStatus == .authorizedWhenInUse {
105
- self.handleAuthorizationSuccess()
106
- return
91
+ // Skip permission requests if configured
92
+ if skipPermissionRequests {
93
+ if enableBackgroundLocationUpdates {
94
+ enableBackgroundLocationUpdatesIfNeeded()
107
95
  }
96
+ handleAuthorizationSuccess()
97
+ return
98
+ }
108
99
 
109
- if currentStatus == .denied || currentStatus == .restricted {
110
- self.handleAuthorizationError(for: currentStatus)
111
- return
112
- }
100
+ // Check if already authorized
101
+ let currentStatus = CLLocationManager.authorizationStatus()
102
+ if currentStatus == .authorizedAlways || currentStatus == .authorizedWhenInUse {
103
+ handleAuthorizationSuccess()
104
+ return
105
+ }
113
106
 
114
- // Not determined yet, request permission
115
- self.requestPermission(for: authType)
107
+ if currentStatus == .denied || currentStatus == .restricted {
108
+ handleAuthorizationError(for: currentStatus)
109
+ return
116
110
  }
111
+
112
+ // Not determined yet, request permission
113
+ requestPermission(for: authType)
117
114
  }
118
115
 
119
116
  private func enqueueAuthorizationCallbacks(
@@ -153,63 +150,51 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
153
150
  error: ((GeolocationError) -> Void)?,
154
151
  options: GeolocationOptions?
155
152
  ) {
156
- DispatchQueue.main.async { [weak self] in
157
- guard let self = self else { return }
158
-
159
- let parsedOptions = ParsedOptions.parse(from: options)
160
-
161
- // Check authorization
162
- let status = CLLocationManager.authorizationStatus()
163
- if status == .denied || status == .restricted {
164
- let message =
165
- status == .restricted
166
- ? "This application is not authorized to use location services"
167
- : "User denied access to location services."
168
- error?(self.createError(code: self.PERMISSION_DENIED, message: message))
169
- return
170
- }
171
-
172
- if !CLLocationManager.locationServicesEnabled() {
173
- error?(
174
- self.createError(
175
- code: self.POSITION_UNAVAILABLE, message: "Location services disabled."))
176
- return
177
- }
153
+ let parsedOptions = ParsedOptions.parse(from: options)
154
+
155
+ // Check authorization
156
+ let status = CLLocationManager.authorizationStatus()
157
+ if status == .denied || status == .restricted {
158
+ let message =
159
+ status == .restricted
160
+ ? "This application is not authorized to use location services"
161
+ : "User denied access to location services."
162
+ error?(createError(code: PERMISSION_DENIED, message: message))
163
+ return
164
+ }
178
165
 
179
- // Check cached location
180
- if let cached = self.lastLocation,
181
- self.isCachedLocationValid(cached, options: parsedOptions)
182
- {
183
- success(self.locationToPosition(cached))
184
- return
185
- }
166
+ if !CLLocationManager.locationServicesEnabled() {
167
+ error?(createError(code: POSITION_UNAVAILABLE, message: "Location services disabled."))
168
+ return
169
+ }
186
170
 
187
- self.initializeLocationManagerIfNeeded()
171
+ initializeLocationManagerIfNeeded()
188
172
 
189
- // Configure location manager (use best accuracy from all pending requests)
190
- self.updateLocationManagerConfiguration()
173
+ // Configure location manager
174
+ locationManager?.desiredAccuracy = parsedOptions.accuracy
175
+ locationManager?.distanceFilter = parsedOptions.distanceFilter
191
176
 
192
- // Create request
193
- var request = LocationRequest(
194
- success: success,
195
- error: error,
196
- options: parsedOptions,
197
- timer: nil
198
- )
177
+ // Create request
178
+ var request = LocationRequest(
179
+ success: success,
180
+ error: error,
181
+ options: parsedOptions,
182
+ timer: nil
183
+ )
199
184
 
200
- // Setup timeout
201
- let timer = Timer.scheduledTimer(
202
- withTimeInterval: parsedOptions.timeout / 1000.0, repeats: false
203
- ) { [weak self] timer in
204
- self?.handleTimeout(for: timer)
205
- }
206
- request.timer = timer
185
+ // Setup timeout with DispatchSourceTimer (no run loop needed)
186
+ let timer = DispatchSource.makeTimerSource(queue: .main)
187
+ timer.schedule(deadline: .now() + parsedOptions.timeout / 1000.0)
188
+ timer.setEventHandler { [weak self] in
189
+ self?.handleTimeout(for: request.id)
190
+ }
191
+ timer.resume()
192
+ request.timer = timer
207
193
 
208
- self.pendingRequests.append(request)
194
+ pendingRequests.append(request)
209
195
 
210
- // Start location updates
211
- self.startMonitoring()
212
- }
196
+ // Start location updates
197
+ startMonitoring()
213
198
  }
214
199
 
215
200
  // MARK: - Watch Position
@@ -219,56 +204,44 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
219
204
  error: ((GeolocationError) -> Void)?,
220
205
  options: GeolocationOptions?
221
206
  ) -> Double {
222
- var resultWatchId: Double = 0
223
-
224
- DispatchQueue.main.sync { [weak self] in
225
- guard let self = self else { return }
226
-
227
- let parsedOptions = ParsedOptions.parse(from: options)
228
- let watchId = self.nextWatchId
229
- self.nextWatchId += 1
207
+ let parsedOptions = ParsedOptions.parse(from: options)
208
+ let watchId = nextWatchId
209
+ nextWatchId += 1
210
+
211
+ let subscription = WatchSubscription(
212
+ success: success,
213
+ error: error,
214
+ options: parsedOptions
215
+ )
230
216
 
231
- let subscription = WatchSubscription(
232
- success: success,
233
- error: error,
234
- options: parsedOptions
235
- )
217
+ activeWatches[watchId] = subscription
236
218
 
237
- self.activeWatches[watchId] = subscription
219
+ initializeLocationManagerIfNeeded()
238
220
 
239
- self.initializeLocationManagerIfNeeded()
240
- self.updateLocationManagerConfiguration()
241
- self.startMonitoring()
221
+ // Configure location manager
222
+ locationManager?.desiredAccuracy = parsedOptions.accuracy
223
+ locationManager?.distanceFilter = parsedOptions.distanceFilter
242
224
 
243
- resultWatchId = watchId
244
- }
225
+ startMonitoring()
245
226
 
246
- return resultWatchId
227
+ return watchId
247
228
  }
248
229
 
249
230
  func clearWatch(watchId: Double) {
250
- DispatchQueue.main.async { [weak self] in
251
- guard let self = self else { return }
252
-
253
- self.activeWatches.removeValue(forKey: watchId)
231
+ activeWatches.removeValue(forKey: watchId)
254
232
 
255
- // Stop monitoring if no more watches or pending requests
256
- if self.activeWatches.isEmpty && self.pendingRequests.isEmpty {
257
- self.stopMonitoring()
258
- }
233
+ // Stop monitoring if no more watches or pending requests
234
+ if activeWatches.isEmpty && pendingRequests.isEmpty {
235
+ stopMonitoring()
259
236
  }
260
237
  }
261
238
 
262
239
  func stopObserving() {
263
- DispatchQueue.main.async { [weak self] in
264
- guard let self = self else { return }
240
+ activeWatches.removeAll()
265
241
 
266
- self.activeWatches.removeAll()
267
-
268
- // Stop monitoring if no pending requests
269
- if self.pendingRequests.isEmpty {
270
- self.stopMonitoring()
271
- }
242
+ // Stop monitoring if no pending requests
243
+ if pendingRequests.isEmpty {
244
+ stopMonitoring()
272
245
  }
273
246
  }
274
247
 
@@ -298,7 +271,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
298
271
 
299
272
  // 1. Fire all pending getCurrentPosition requests
300
273
  for request in pendingRequests {
301
- request.timer?.invalidate()
274
+ request.timer?.cancel()
302
275
  request.success(position)
303
276
  }
304
277
  pendingRequests.removeAll()
@@ -338,7 +311,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
338
311
 
339
312
  // Fire all pending requests with error
340
313
  for request in pendingRequests {
341
- request.timer?.invalidate()
314
+ request.timer?.cancel()
342
315
  request.error?(geoError)
343
316
  }
344
317
  pendingRequests.removeAll()
@@ -355,8 +328,17 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
355
328
 
356
329
  private func initializeLocationManagerIfNeeded() {
357
330
  guard locationManager == nil else { return }
358
- locationManager = CLLocationManager()
359
- locationManager?.delegate = self
331
+
332
+ // CLLocationManager must be created on main thread for delegate callbacks
333
+ if Thread.isMainThread {
334
+ locationManager = CLLocationManager()
335
+ locationManager?.delegate = self
336
+ } else {
337
+ DispatchQueue.main.sync {
338
+ locationManager = CLLocationManager()
339
+ locationManager?.delegate = self
340
+ }
341
+ }
360
342
  }
361
343
 
362
344
  private func updateLocationManagerConfiguration() {
@@ -408,7 +390,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
408
390
  }
409
391
  }
410
392
 
411
- private func isCachedLocationValid(_ location: CLLocation, options: ParsedOptions) -> Bool {
393
+ func isCachedLocationValid(_ location: CLLocation, options: ParsedOptions) -> Bool {
412
394
  // Check if maximumAge is infinity
413
395
  if options.maximumAge.isInfinite {
414
396
  return true
@@ -428,13 +410,16 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
428
410
  return true
429
411
  }
430
412
 
431
- private func handleTimeout(for timer: Timer) {
432
- // Find and remove the request with this timer
433
- if let index = pendingRequests.firstIndex(where: { $0.timer === timer }) {
413
+ private func handleTimeout(for requestId: UUID) {
414
+ // Find and remove the request with this ID
415
+ if let index = pendingRequests.firstIndex(where: { $0.id == requestId }) {
434
416
  let request = pendingRequests[index]
435
417
  pendingRequests.remove(at: index)
436
418
 
437
- // Always return timeout error
419
+ // Cancel timer
420
+ request.timer?.cancel()
421
+
422
+ // Return timeout error
438
423
  let timeoutSeconds = request.options.timeout / 1000.0
439
424
  let message = String(format: "Unable to fetch location within %.1fs.", timeoutSeconds)
440
425
  request.error?(createError(code: TIMEOUT, message: message))
@@ -493,7 +478,7 @@ class LocationManager: NSObject, CLLocationManagerDelegate {
493
478
  queuedAuthorizationCallbacks.removeAll()
494
479
  }
495
480
 
496
- private func locationToPosition(_ location: CLLocation) -> GeolocationResponse {
481
+ func locationToPosition(_ location: CLLocation) -> GeolocationResponse {
497
482
  let altitude = location.verticalAccuracy < 0 ? 0.0 : location.altitude
498
483
  let altitudeAccuracy = location.verticalAccuracy < 0 ? 0.0 : location.verticalAccuracy
499
484
  let heading = location.course >= 0 ? location.course : -1.0
@@ -38,6 +38,16 @@ class NitroGeolocation: HybridNitroGeolocationSpec {
38
38
  success: @escaping (GeolocationResponse) -> Void, error: ((GeolocationError) -> Void)?,
39
39
  options: GeolocationOptions?
40
40
  ) throws {
41
+ // Fast path: check cached location immediately (no dispatch overhead!)
42
+ let parsedOptions = LocationManager.ParsedOptions.parse(from: options)
43
+
44
+ if let cached = locationManager.lastLocation,
45
+ locationManager.isCachedLocationValid(cached, options: parsedOptions) {
46
+ success(locationManager.locationToPosition(cached))
47
+ return
48
+ }
49
+
50
+ // Slow path: need GPS
41
51
  locationManager.getCurrentPosition(success: success, error: error, options: options)
42
52
  }
43
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-geolocation",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "⚡🚀Blazing-fast geolocation for React Native powered by Nitro Modules",
5
5
  "main": "src/index",
6
6
  "source": "src/index",
@@ -43,7 +43,7 @@
43
43
  "@types/react-test-renderer": "^18.0.0",
44
44
  "react": "19.1.0",
45
45
  "react-native": "0.81.1",
46
- "react-native-nitro-modules": "^0.29.7",
46
+ "react-native-nitro-modules": "^0.29.6",
47
47
  "typescript": "^5.9.3"
48
48
  },
49
49
  "peerDependencies": {
package/README.md DELETED
@@ -1,598 +0,0 @@
1
- # react-native-nitro-geolocation
2
-
3
- A high-performance geolocation library for React Native, powered by [Nitro Modules](https://github.com/mrousavy/nitro). This is a complete rewrite of `@react-native-community/geolocation` using modern architecture for superior performance and developer experience.
4
-
5
- ## Architecture Comparison
6
-
7
- ### Bridge vs JSI Architecture
8
-
9
- #### `@react-native-community/geolocation` (Old Bridge)
10
- ```
11
- ┌─────────────┐
12
- │ JS │
13
- └──────┬──────┘
14
- │ serialize JSON
15
-
16
- ┌─────────────┐
17
- │ Bridge │ ← Async message queue
18
- └──────┬──────┘
19
- │ deserialize JSON
20
-
21
- ┌─────────────┐
22
- │ Java/ObjC │
23
- └─────────────┘
24
- ```
25
-
26
- **Characteristics:**
27
- - **Asynchronous**: All calls go through message queue
28
- - **Serialization overhead**: Every data structure must be serialized/deserialized
29
- - **Event emitter pattern**: Uses `DeviceEventEmitter` for location updates
30
- - **Indirect callbacks**: Events broadcast to all JS listeners
31
-
32
- #### `react-native-nitro-geolocation` (Nitro/JSI)
33
- ```
34
- ┌─────────────┐
35
- │ JS │
36
- └──────┬──────┘
37
- │ direct memory access
38
-
39
- ┌─────────────┐
40
- │ C++ (JSI) │ ← Type-safe bindings
41
- └──────┬──────┘
42
- │ direct function call
43
-
44
- ┌─────────────┐
45
- │Kotlin/Swift │
46
- └─────────────┘
47
- ```
48
-
49
- **Characteristics:**
50
- - **Synchronous capable**: Direct C++ function calls (getCurrentPosition still async due to GPS)
51
- - **Zero serialization**: Shared memory between JS and native
52
- - **Direct callbacks**: Native directly invokes JS functions via JSI
53
- - **Type-safe**: Compile-time type checking in C++
54
-
55
- ---
56
-
57
- ## Method-by-Method Architecture Analysis
58
-
59
- ### 1. `setRNConfiguration(config)`
60
-
61
- #### Original (`@react-native-community/geolocation`)
62
- ```java
63
- // GeolocationModule.java
64
- @ReactMethod
65
- public void setConfiguration(ReadableMap config) {
66
- mConfiguration = Configuration.fromReactMap(config);
67
- // Bridge automatically handles JSON deserialization
68
- }
69
- ```
70
-
71
- **Architecture:**
72
- - `@ReactMethod` annotation → Bridge registers method
73
- - `ReadableMap` → Deserialized from JS object
74
- - Configuration stored in Java object
75
-
76
- #### Nitro Version
77
- ```kotlin
78
- // NitroGeolocation.kt
79
- override fun setRNConfiguration(config: RNConfigurationInternal) {
80
- configuration = config
81
- }
82
- ```
83
-
84
- **Architecture:**
85
- - Direct C++ → Kotlin call via HybridObject
86
- - `RNConfigurationInternal` is C++ struct, no serialization
87
- - Type-safe: Compile error if structure changes
88
-
89
- **Performance Difference:**
90
- - **Bridge**: ~0.5-2ms (JSON parse + bridge overhead)
91
- - **Nitro**: ~0.01-0.05ms (direct memory copy)
92
- - **Speedup**: ~50-200x faster
93
-
94
- ---
95
-
96
- ### 2. `requestAuthorization(success, error)`
97
-
98
- #### Original Architecture
99
- ```java
100
- // GeolocationModule.java
101
- @ReactMethod
102
- public void requestAuthorization(final Callback success, final Callback error) {
103
- // Bridge wraps JS callbacks as Java Callback objects
104
- PermissionsModule.requestPermissions(..., new PromiseImpl(success, error));
105
- }
106
- ```
107
-
108
- **Call flow:**
109
- 1. JS calls method → Bridge enqueues
110
- 2. Bridge deserializes callbacks → Creates Java `Callback` wrapper
111
- 3. Permission result → `success.invoke()`
112
- 4. Bridge serializes result → Enqueues back to JS
113
- 5. JS callback executed
114
-
115
- **Overhead per call**: ~1-3ms
116
-
117
- #### Nitro Architecture
118
- ```kotlin
119
- // NitroGeolocation.kt
120
- override fun requestAuthorization(
121
- success: (() -> Unit)?,
122
- error: ((error: GeolocationError) -> Unit)?
123
- ) {
124
- requestAuthorizationHandler.execute(success, error)
125
- }
126
- ```
127
-
128
- **Call flow:**
129
- 1. JS calls method → Direct C++ function call
130
- 2. C++ passes function references (no wrapping)
131
- 3. Permission result → `success?.invoke()`
132
- 4. JSI directly executes JS function (shared memory)
133
-
134
- **Overhead per call**: ~0.01-0.1ms
135
-
136
- **Key Difference:**
137
- - **Bridge**: Callbacks are serialized as IDs, invoked through message queue
138
- - **Nitro**: Callbacks are actual C++ function pointers to JS functions
139
-
140
- ---
141
-
142
- ### 3. `getCurrentPosition(success, error, options)`
143
-
144
- #### Original Architecture
145
- ```java
146
- // AndroidLocationManager.java
147
- public void getCurrentLocationData(ReadableMap options, Callback success, Callback error) {
148
- // Single callback instance for this request
149
- new SingleUpdateRequest(locationManager, provider, timeout, success, error).invoke(location);
150
- }
151
-
152
- private static class SingleUpdateRequest {
153
- private final Callback mSuccess;
154
- private final LocationListener mLocationListener = new LocationListener() {
155
- public void onLocationChanged(Location location) {
156
- mSuccess.invoke(locationToMap(location)); // Bridge serialization
157
- }
158
- };
159
- }
160
- ```
161
-
162
- **Data flow:**
163
- ```
164
- Android LocationManager
165
- → LocationListener.onLocationChanged(Location)
166
- → locationToMap(Location) // Create WritableMap
167
- → success.invoke(WritableMap) // Serialize to JSON
168
- → Bridge message queue
169
- → JS deserialize JSON
170
- → User callback
171
- ```
172
-
173
- **Overhead**: ~1-3ms per location update
174
-
175
- #### Nitro Architecture
176
- ```kotlin
177
- // GetCurrentPosition.kt
178
- fun execute(
179
- success: (position: GeolocationResponse) -> Unit,
180
- error: ((error: GeolocationError) -> Unit)?,
181
- options: GeolocationOptions?
182
- ) {
183
- val listener = object : LocationListener {
184
- override fun onLocationChanged(location: Location) {
185
- success(locationToPosition(location)) // Direct JSI call
186
- }
187
- }
188
- locationManager.requestLocationUpdates(provider, 100, 1f, listener, Looper.getMainLooper())
189
- }
190
- ```
191
-
192
- **Data flow:**
193
- ```
194
- Android LocationManager
195
- → LocationListener.onLocationChanged(Location)
196
- → locationToPosition(Location) // Create Kotlin data class
197
- → success(GeolocationResponse) // Direct JSI invocation
198
- → User callback (zero serialization)
199
- ```
200
-
201
- **Overhead**: ~0.01-0.1ms per location update
202
-
203
- **Additional Improvements:**
204
- 1. **Better location algorithm**: Implements `isBetterLocation()` from Android docs
205
- 2. **Modern API support**: Uses `getCurrentLocation()` API on Android 11+
206
- 3. **Timeout with fallback**: Returns last known location on timeout (configurable)
207
-
208
- ---
209
-
210
- ### 4. `watchPosition(success, error, options)`
211
-
212
- This is where architectural differences are most significant.
213
-
214
- #### Original Architecture
215
- ```java
216
- // AndroidLocationManager.java
217
- private final LocationListener mLocationListener = new LocationListener() {
218
- public void onLocationChanged(Location location) {
219
- // Broadcast to ALL JS listeners via event emitter
220
- mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
221
- .emit("geolocationDidChange", locationToMap(location));
222
- }
223
- };
224
-
225
- public void startObserving(ReadableMap options) {
226
- // Single global listener for ALL watches
227
- locationManager.requestLocationUpdates(provider, 1000, distanceFilter, mLocationListener);
228
- }
229
- ```
230
-
231
- **Architecture:**
232
- ```
233
- ┌──────────────────────────────┐
234
- │ Android LocationManager │
235
- └──────────────┬───────────────┘
236
- │ Single LocationListener
237
-
238
- ┌──────────────────────────────┐
239
- │ mLocationListener │
240
- └──────────────┬───────────────┘
241
- │ emit("geolocationDidChange", location)
242
-
243
- ┌──────────────────────────────┐
244
- │ DeviceEventEmitter │ ← Serialize location to JSON
245
- └──────────────┬───────────────┘
246
- │ Bridge
247
-
248
- ┌──────────────────────────────┐
249
- │ JS EventEmitter │
250
- └──────────────┬───────────────┘
251
- │ Broadcast to ALL listeners
252
- ├──────┬──────┬──────┐
253
- ▼ ▼ ▼ ▼
254
- watch1 watch2 watch3 watch4 (all receive same event)
255
- ```
256
-
257
- **Problems:**
258
- 1. **Single global listener**: Cannot have different options per watch
259
- 2. **Broadcast overhead**: All watches receive updates even if they have different filters
260
- 3. **JS-side filtering**: Each watch must filter events in JS
261
- 4. **Memory**: Event emitter maintains listener registry in JS
262
-
263
- **Data flow per update:**
264
- ```
265
- Location update (native)
266
- → serialize to JSON (1-2ms)
267
- → emit to bridge queue
268
- → deserialize in JS (1-2ms)
269
- → broadcast to N listeners (0.1ms × N)
270
- → each listener filters/processes
271
- Total: ~2-5ms + (0.1ms × N listeners)
272
- ```
273
-
274
- #### Nitro Architecture
275
- ```kotlin
276
- // WatchPosition.kt
277
- class WatchPosition(private val reactContext: ReactApplicationContext) {
278
- // Multiple watches, each with its own callback and options
279
- private val watchCallbacks = ConcurrentHashMap<Int, WatchCallback>()
280
- private val watchIdGenerator = AtomicInteger(0)
281
-
282
- data class WatchCallback(
283
- val success: (GeolocationResponse) -> Unit,
284
- val error: ((GeolocationError) -> Unit)?,
285
- val options: GeolocationOptions?
286
- )
287
-
288
- fun watch(success: ..., error: ..., options: ...): Int {
289
- val watchId = watchIdGenerator.incrementAndGet()
290
- watchCallbacks[watchId] = WatchCallback(success, error, options)
291
-
292
- // Start location updates only when first watch is added
293
- if (watchCallbacks.size == 1) {
294
- startObserving(options)
295
- }
296
- return watchId
297
- }
298
-
299
- private val locationListener = object : LocationListener {
300
- override fun onLocationChanged(location: Location) {
301
- val position = locationToPosition(location)
302
- // Direct callback to each watch
303
- watchCallbacks.values.forEach { callback ->
304
- callback.success(position)
305
- }
306
- }
307
- }
308
- }
309
- ```
310
-
311
- **Architecture:**
312
- ```
313
- ┌──────────────────────────────┐
314
- │ Android LocationManager │
315
- └──────────────┬───────────────┘
316
- │ Single LocationListener (lazy start)
317
-
318
- ┌──────────────────────────────┐
319
- │ locationListener │
320
- └──────────────┬───────────────┘
321
- │ Direct loop over watchCallbacks
322
- ├──────┬──────┬──────┐
323
- ▼ ▼ ▼ ▼
324
- watch1 watch2 watch3 watch4
325
- (direct JSI invocation to each callback)
326
- ```
327
-
328
- **Advantages:**
329
- 1. **Native watch management**: watchId and callbacks stored in native `ConcurrentHashMap`
330
- 2. **Direct callbacks**: Each watch callback invoked directly via JSI (no event emitter)
331
- 3. **Thread-safe**: `ConcurrentHashMap` + `AtomicInteger` for concurrent access
332
- 4. **Lazy lifecycle**: Start location updates on first watch, stop on last clearWatch
333
- 5. **Per-watch options**: Can support different options per watch (future enhancement)
334
-
335
- **Data flow per update:**
336
- ```
337
- Location update (native)
338
- → locationToPosition() (create Kotlin object, ~0.01ms)
339
- → forEach watch callback
340
- → Direct JSI call (0.01ms per watch)
341
- Total: ~0.01ms + (0.01ms × N watches)
342
- ```
343
-
344
- **Performance Comparison:**
345
-
346
- | Scenario | Bridge | Nitro | Speedup |
347
- |----------|--------|-------|---------|
348
- | 1 watch, 1 update/sec | ~3ms/update | ~0.02ms/update | **150x** |
349
- | 4 watches, 1 update/sec | ~5ms/update | ~0.05ms/update | **100x** |
350
- | 1 watch, 10 updates/sec | ~30ms/sec | ~0.2ms/sec | **150x** |
351
- | 4 watches, 10 updates/sec | ~50ms/sec | ~0.5ms/sec | **100x** |
352
-
353
- ---
354
-
355
- ### 5. `clearWatch(watchId)`
356
-
357
- #### Original Architecture
358
- ```javascript
359
- // JS side (GeolocationModule wraps this)
360
- let watchID = 0;
361
- const subscriptions = new Map();
362
-
363
- function watchPosition(success, error, options) {
364
- const watchID = ++watchID;
365
- const subscription = DeviceEventEmitter.addListener('geolocationDidChange', success);
366
- subscriptions.set(watchID, subscription);
367
- return watchID;
368
- }
369
-
370
- function clearWatch(watchID) {
371
- const subscription = subscriptions.get(watchID);
372
- subscription?.remove(); // Removes JS listener only
373
- subscriptions.delete(watchID);
374
- }
375
- ```
376
-
377
- **Architecture:**
378
- - watchId managed in JS
379
- - No native call to `clearWatch()`
380
- - Native listener keeps running until `stopObserving()` called
381
-
382
- #### Nitro Architecture
383
- ```kotlin
384
- // WatchPosition.kt
385
- fun clearWatch(watchId: Int) {
386
- watchCallbacks.remove(watchId)
387
-
388
- // Automatically stop observing if no more watches
389
- if (watchCallbacks.isEmpty()) {
390
- stopObserving()
391
- }
392
- }
393
- ```
394
-
395
- **Architecture:**
396
- - watchId managed in native (Kotlin)
397
- - Direct removal from `ConcurrentHashMap`
398
- - Automatic cleanup: stops LocationManager when last watch removed
399
-
400
- **Key Difference:**
401
- - **Bridge**: JS-only cleanup, native keeps running
402
- - **Nitro**: Native cleanup + automatic resource management
403
-
404
- ---
405
-
406
- ### 6. `stopObserving()`
407
-
408
- #### Original Architecture
409
- ```java
410
- // AndroidLocationManager.java
411
- public void stopObserving() {
412
- LocationManager locationManager = (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE);
413
- locationManager.removeUpdates(mLocationListener);
414
- mWatchedProvider = null;
415
- }
416
- ```
417
-
418
- - Single global listener removed
419
- - All watches stopped at once
420
- - No cleanup of watch callbacks (handled in JS)
421
-
422
- #### Nitro Architecture
423
- ```kotlin
424
- // WatchPosition.kt
425
- fun stopObserving() {
426
- val locationManager = reactContext.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
427
-
428
- locationListener?.let { listener ->
429
- locationManager?.removeUpdates(listener)
430
- }
431
-
432
- // Complete cleanup
433
- locationListener = null
434
- watchedProvider = null
435
- currentOptions = null
436
- watchCallbacks.clear()
437
- }
438
- ```
439
-
440
- **Additional cleanup:**
441
- - All watch callbacks cleared from `ConcurrentHashMap`
442
- - Explicit null assignment for GC
443
- - Complete resource release
444
-
445
- ---
446
-
447
- ## Overall Performance Summary
448
-
449
- ### Latency Comparison
450
-
451
- | Operation | Bridge Overhead | Nitro Overhead | Speedup |
452
- |-----------|----------------|----------------|---------|
453
- | Method call (e.g., setRNConfiguration) | 0.5-2ms | 0.01-0.05ms | **50-200x** |
454
- | Callback invocation | 1-3ms | 0.01-0.1ms | **30-100x** |
455
- | Location update (single) | 2-4ms | 0.02-0.15ms | **100-200x** |
456
- | Location update (4 watches) | 4-6ms | 0.05-0.4ms | **80-120x** |
457
-
458
- ### Memory Comparison
459
-
460
- | Component | Bridge | Nitro |
461
- |-----------|--------|-------|
462
- | Serialization buffers | ~1KB per call | 0 bytes (shared memory) |
463
- | Event emitter registry | ~100 bytes per listener | 0 bytes (no event emitter) |
464
- | Watch callback storage | JS Map (~50 bytes/watch) | ConcurrentHashMap (~40 bytes/watch) |
465
- | JSON parsing overhead | ~500 bytes per location | 0 bytes |
466
-
467
- **Total memory savings**: ~2-5KB per active watch session
468
-
469
- ### Thread Safety
470
-
471
- | Aspect | Bridge | Nitro |
472
- |--------|--------|-------|
473
- | Concurrent watches | ✅ Handled by React Native bridge | ✅ ConcurrentHashMap + AtomicInteger |
474
- | Callback invocation | ⚠️ Queued (serialized execution) | ✅ Thread-safe direct invocation |
475
- | Resource cleanup | ⚠️ JS GC dependent | ✅ Explicit native cleanup |
476
-
477
- ---
478
-
479
- ## Battery Impact
480
-
481
- ### Bridge Architecture
482
- ```
483
- Location update (every 1s)
484
- ├─ GPS wakes CPU (~5mW)
485
- ├─ Serialize to JSON (~2mW for 2ms)
486
- ├─ Bridge context switch (~1mW)
487
- ├─ Deserialize in JS (~2mW for 2ms)
488
- └─ JS callback execution (~1mW)
489
- Total: ~11mW per update = 39.6J per hour
490
- ```
491
-
492
- ### Nitro Architecture
493
- ```
494
- Location update (every 1s)
495
- ├─ GPS wakes CPU (~5mW)
496
- ├─ Direct callback (~0.1mW for 0.02ms)
497
- └─ JS callback execution (~1mW)
498
- Total: ~6.1mW per update = 22J per hour
499
- ```
500
-
501
- **Battery savings**: ~45% less power consumption for continuous tracking
502
-
503
- ---
504
-
505
- ## Build Size Comparison
506
-
507
- | Component | Bridge | Nitro |
508
- |-----------|--------|-------|
509
- | Java/Kotlin code | ~15KB | ~18KB |
510
- | C++ code | 0KB | ~25KB (Nitro runtime) |
511
- | JavaScript | ~8KB | ~6KB (less event handling) |
512
- | **Total overhead** | ~23KB | ~49KB |
513
-
514
- **Note**: Nitro adds ~26KB, but this is one-time cost shared across all Nitro modules in your app.
515
-
516
- ---
517
-
518
- ## Type Safety
519
-
520
- ### Bridge (Runtime Type Checking)
521
- ```typescript
522
- // ❌ No compile-time safety
523
- getCurrentPosition(
524
- (position) => {
525
- console.log(position.coords.latitude); // Could be undefined at runtime
526
- }
527
- );
528
- ```
529
-
530
- ### Nitro (Compile-Time Type Checking)
531
- ```typescript
532
- // ✅ Full TypeScript + C++ type safety
533
- getCurrentPosition(
534
- (position: GeolocationResponse) => {
535
- console.log(position.coords.latitude); // Guaranteed to exist
536
- }
537
- );
538
- ```
539
-
540
- C++ generates type-safe bindings:
541
- ```cpp
542
- // Generated by Nitrogen
543
- struct GeolocationResponse {
544
- GeolocationCoordinates coords;
545
- double timestamp;
546
- };
547
- ```
548
-
549
- **Benefit**: Catch type errors at compile time, not in production.
550
-
551
- ---
552
-
553
- ## Migration Guide
554
-
555
- Since this is a **drop-in replacement** for `@react-native-community/geolocation`, migration is trivial:
556
-
557
- ```diff
558
- - import Geolocation from '@react-native-community/geolocation';
559
- + import Geolocation from 'react-native-nitro-geolocation';
560
-
561
- // API is 100% identical
562
- Geolocation.watchPosition(
563
- position => console.log(position),
564
- error => console.error(error),
565
- { enableHighAccuracy: true }
566
- );
567
- ```
568
-
569
- **No code changes required!**
570
-
571
- ---
572
-
573
- ## Why Choose Nitro Geolocation?
574
-
575
- 1. **🚀 Performance**: 50-200x faster method calls, 100-200x faster location updates
576
- 2. **🔋 Battery**: ~45% less power consumption for continuous tracking
577
- 3. **🧵 Thread Safety**: Modern concurrent data structures (ConcurrentHashMap)
578
- 4. **🎯 Type Safety**: Compile-time type checking via C++
579
- 5. **🧹 Resource Management**: Automatic cleanup, explicit GC hints
580
- 6. **📦 Drop-in Replacement**: Zero migration cost
581
- 7. **🏗️ Modern Architecture**: JSI-based, ready for React Native's future
582
-
583
- ---
584
-
585
- ## Benchmarks
586
-
587
- See [BENCHMARKS.md](./BENCHMARKS.md) for detailed performance measurements.
588
-
589
- ---
590
-
591
- ## License
592
-
593
- MIT
594
-
595
- Original work Copyright (c) 2022-present, React Native Community
596
- Modified work Copyright (c) 2025, jingjing2222
597
-
598
- This project is a Nitro Modules port of `@react-native-community/geolocation`.