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.
- package/ios/LocationManager.swift +109 -124
- package/ios/NitroGeolocation.swift +10 -0
- package/package.json +2 -2
- package/README.md +0 -598
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
if
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
171
|
+
initializeLocationManagerIfNeeded()
|
|
188
172
|
|
|
189
|
-
|
|
190
|
-
|
|
173
|
+
// Configure location manager
|
|
174
|
+
locationManager?.desiredAccuracy = parsedOptions.accuracy
|
|
175
|
+
locationManager?.distanceFilter = parsedOptions.distanceFilter
|
|
191
176
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
177
|
+
// Create request
|
|
178
|
+
var request = LocationRequest(
|
|
179
|
+
success: success,
|
|
180
|
+
error: error,
|
|
181
|
+
options: parsedOptions,
|
|
182
|
+
timer: nil
|
|
183
|
+
)
|
|
199
184
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
194
|
+
pendingRequests.append(request)
|
|
209
195
|
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
success: success,
|
|
233
|
-
error: error,
|
|
234
|
-
options: parsedOptions
|
|
235
|
-
)
|
|
217
|
+
activeWatches[watchId] = subscription
|
|
236
218
|
|
|
237
|
-
|
|
219
|
+
initializeLocationManagerIfNeeded()
|
|
238
220
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
221
|
+
// Configure location manager
|
|
222
|
+
locationManager?.desiredAccuracy = parsedOptions.accuracy
|
|
223
|
+
locationManager?.distanceFilter = parsedOptions.distanceFilter
|
|
242
224
|
|
|
243
|
-
|
|
244
|
-
}
|
|
225
|
+
startMonitoring()
|
|
245
226
|
|
|
246
|
-
return
|
|
227
|
+
return watchId
|
|
247
228
|
}
|
|
248
229
|
|
|
249
230
|
func clearWatch(watchId: Double) {
|
|
250
|
-
|
|
251
|
-
guard let self = self else { return }
|
|
252
|
-
|
|
253
|
-
self.activeWatches.removeValue(forKey: watchId)
|
|
231
|
+
activeWatches.removeValue(forKey: watchId)
|
|
254
232
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
guard let self = self else { return }
|
|
240
|
+
activeWatches.removeAll()
|
|
265
241
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
432
|
-
// Find and remove the request with this
|
|
433
|
-
if let index = pendingRequests.firstIndex(where: { $0.
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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`.
|