@kafitra/react-native-live-tracking 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/android/build.gradle +71 -0
  4. package/android/gradle.properties +7 -0
  5. package/android/src/main/AndroidManifest.xml +40 -0
  6. package/android/src/main/java/com/livetracking/LiveTrackingModuleImpl.kt +728 -0
  7. package/android/src/main/java/com/livetracking/LiveTrackingPackage.kt +16 -0
  8. package/android/src/main/java/com/livetracking/location/LocationEngine.kt +93 -0
  9. package/android/src/main/java/com/livetracking/network/NetworkListener.kt +127 -0
  10. package/android/src/main/java/com/livetracking/optimizer/ActivityRecognitionHandler.kt +248 -0
  11. package/android/src/main/java/com/livetracking/optimizer/MotionSleepManager.kt +130 -0
  12. package/android/src/main/java/com/livetracking/permissions/PermissionHandler.kt +145 -0
  13. package/android/src/main/java/com/livetracking/queue/QueueEngine.kt +167 -0
  14. package/android/src/main/java/com/livetracking/queue/QueuedLocation.kt +16 -0
  15. package/android/src/main/java/com/livetracking/queue/TrackingDatabase.kt +239 -0
  16. package/android/src/main/java/com/livetracking/receiver/BootReceiver.kt +53 -0
  17. package/android/src/main/java/com/livetracking/service/TrackingForegroundService.kt +145 -0
  18. package/android/src/main/java/com/livetracking/sync/FirebaseSyncEngine.kt +277 -0
  19. package/android/src/main/java/com/livetracking/sync/LocationDataPoint.kt +31 -0
  20. package/android/src/main/java/com/livetracking/sync/SyncEngineController.kt +220 -0
  21. package/android/src/main/java/com/livetracking/sync/SyncTargetConfig.kt +20 -0
  22. package/android/src/main/java/com/livetracking/sync/TargetHandler.kt +601 -0
  23. package/android/src/newarch/java/com/livetracking/LiveTrackingModule.kt +64 -0
  24. package/android/src/oldarch/java/com/livetracking/LiveTrackingModule.kt +70 -0
  25. package/android/src/test/java/com/livetracking/BackoffCalculationTest.kt +216 -0
  26. package/android/src/test/java/com/livetracking/BatchAccumulatorTest.kt +391 -0
  27. package/android/src/test/java/com/livetracking/BootReceiverTest.kt +247 -0
  28. package/android/src/test/java/com/livetracking/FirebaseSyncEngineTest.kt +337 -0
  29. package/android/src/test/java/com/livetracking/LocationEngineTest.kt +202 -0
  30. package/android/src/test/java/com/livetracking/MotionSleepManagerTest.kt +420 -0
  31. package/android/src/test/java/com/livetracking/OfflineQueueTest.kt +462 -0
  32. package/android/src/test/java/com/livetracking/PermissionHandlerTest.kt +200 -0
  33. package/android/src/test/java/com/livetracking/QueueEngineTest.kt +335 -0
  34. package/android/src/test/java/com/livetracking/SyncEngineControllerTest.kt +855 -0
  35. package/ios/ActivityRecognitionHandler.swift +196 -0
  36. package/ios/BackgroundModeHelper.swift +132 -0
  37. package/ios/FirebaseSyncEngine.swift +276 -0
  38. package/ios/LiveTracking-Bridging-Header.h +2 -0
  39. package/ios/LiveTracking.m +37 -0
  40. package/ios/LiveTracking.swift +773 -0
  41. package/ios/LocationDataPoint.swift +56 -0
  42. package/ios/LocationEngine.swift +160 -0
  43. package/ios/MotionSleepManager.swift +151 -0
  44. package/ios/NetworkListener.swift +105 -0
  45. package/ios/OfflineQueueManager.swift +503 -0
  46. package/ios/PermissionHandler.swift +148 -0
  47. package/ios/QueueEngine.swift +249 -0
  48. package/ios/SyncEngineController.swift +396 -0
  49. package/ios/SyncTargetConfig.swift +36 -0
  50. package/ios/TargetHandler.swift +715 -0
  51. package/ios/Tests/ActivityRecognitionHandlerTests.swift +259 -0
  52. package/ios/Tests/FirebaseSyncEngineTests.swift +303 -0
  53. package/ios/Tests/LocationEngineTests.swift +244 -0
  54. package/ios/Tests/MotionSleepManagerTests.swift +355 -0
  55. package/ios/Tests/NetworkListenerTests.swift +188 -0
  56. package/ios/Tests/OfflineQueueFlushTests.swift +375 -0
  57. package/ios/Tests/PermissionHandlerTests.swift +238 -0
  58. package/ios/Tests/QueueEngineTests.swift +346 -0
  59. package/ios/TrackingCleanup.swift +93 -0
  60. package/ios/TrackingNotificationManager.swift +187 -0
  61. package/lib/commonjs/EventEmitter.js +113 -0
  62. package/lib/commonjs/EventEmitter.js.map +1 -0
  63. package/lib/commonjs/LiveTracking.js +134 -0
  64. package/lib/commonjs/LiveTracking.js.map +1 -0
  65. package/lib/commonjs/NativeLiveTracking.js +21 -0
  66. package/lib/commonjs/NativeLiveTracking.js.map +1 -0
  67. package/lib/commonjs/filters/distanceTimeFilter.js +63 -0
  68. package/lib/commonjs/filters/distanceTimeFilter.js.map +1 -0
  69. package/lib/commonjs/index.js +103 -0
  70. package/lib/commonjs/index.js.map +1 -0
  71. package/lib/commonjs/serialization/locationSerializer.js +51 -0
  72. package/lib/commonjs/serialization/locationSerializer.js.map +1 -0
  73. package/lib/commonjs/types.js +77 -0
  74. package/lib/commonjs/types.js.map +1 -0
  75. package/lib/commonjs/utils/distance.js +63 -0
  76. package/lib/commonjs/utils/distance.js.map +1 -0
  77. package/lib/commonjs/utils/retry.js +80 -0
  78. package/lib/commonjs/utils/retry.js.map +1 -0
  79. package/lib/commonjs/validation.js +463 -0
  80. package/lib/commonjs/validation.js.map +1 -0
  81. package/lib/module/EventEmitter.js +105 -0
  82. package/lib/module/EventEmitter.js.map +1 -0
  83. package/lib/module/LiveTracking.js +127 -0
  84. package/lib/module/LiveTracking.js.map +1 -0
  85. package/lib/module/NativeLiveTracking.js +16 -0
  86. package/lib/module/NativeLiveTracking.js.map +1 -0
  87. package/lib/module/filters/distanceTimeFilter.js +58 -0
  88. package/lib/module/filters/distanceTimeFilter.js.map +1 -0
  89. package/lib/module/index.js +32 -0
  90. package/lib/module/index.js.map +1 -0
  91. package/lib/module/serialization/locationSerializer.js +45 -0
  92. package/lib/module/serialization/locationSerializer.js.map +1 -0
  93. package/lib/module/types.js +94 -0
  94. package/lib/module/types.js.map +1 -0
  95. package/lib/module/utils/distance.js +56 -0
  96. package/lib/module/utils/distance.js.map +1 -0
  97. package/lib/module/utils/retry.js +72 -0
  98. package/lib/module/utils/retry.js.map +1 -0
  99. package/lib/module/validation.js +456 -0
  100. package/lib/module/validation.js.map +1 -0
  101. package/lib/typescript/EventEmitter.d.ts +65 -0
  102. package/lib/typescript/EventEmitter.d.ts.map +1 -0
  103. package/lib/typescript/LiveTracking.d.ts +23 -0
  104. package/lib/typescript/LiveTracking.d.ts.map +1 -0
  105. package/lib/typescript/NativeLiveTracking.d.ts +25 -0
  106. package/lib/typescript/NativeLiveTracking.d.ts.map +1 -0
  107. package/lib/typescript/filters/distanceTimeFilter.d.ts +44 -0
  108. package/lib/typescript/filters/distanceTimeFilter.d.ts.map +1 -0
  109. package/lib/typescript/index.d.ts +21 -0
  110. package/lib/typescript/index.d.ts.map +1 -0
  111. package/lib/typescript/serialization/locationSerializer.d.ts +39 -0
  112. package/lib/typescript/serialization/locationSerializer.d.ts.map +1 -0
  113. package/lib/typescript/types.d.ts +217 -0
  114. package/lib/typescript/types.d.ts.map +1 -0
  115. package/lib/typescript/utils/distance.d.ts +38 -0
  116. package/lib/typescript/utils/distance.d.ts.map +1 -0
  117. package/lib/typescript/utils/retry.d.ts +60 -0
  118. package/lib/typescript/utils/retry.d.ts.map +1 -0
  119. package/lib/typescript/validation.d.ts +26 -0
  120. package/lib/typescript/validation.d.ts.map +1 -0
  121. package/package.json +126 -0
  122. package/react-native-live-tracking.podspec +47 -0
  123. package/src/EventEmitter.ts +118 -0
  124. package/src/LiveTracking.ts +159 -0
  125. package/src/NativeLiveTracking.ts +29 -0
  126. package/src/filters/distanceTimeFilter.ts +75 -0
  127. package/src/index.ts +51 -0
  128. package/src/serialization/locationSerializer.ts +57 -0
  129. package/src/types.ts +252 -0
  130. package/src/utils/distance.ts +68 -0
  131. package/src/utils/retry.ts +75 -0
  132. package/src/validation.ts +552 -0
@@ -0,0 +1,601 @@
1
+ package com.livetracking.sync
2
+
3
+ import com.google.firebase.database.FirebaseDatabase
4
+ import com.google.firebase.firestore.FirebaseFirestore
5
+ import com.livetracking.queue.QueuedLocation
6
+ import java.util.Timer
7
+ import java.util.TimerTask
8
+ import java.util.concurrent.ExecutorService
9
+ import java.util.concurrent.Executors
10
+ import java.util.concurrent.Future
11
+ import java.util.concurrent.atomic.AtomicBoolean
12
+ import java.util.concurrent.atomic.AtomicInteger
13
+ import kotlin.math.pow
14
+ import kotlin.random.Random
15
+
16
+ /**
17
+ * Interface for per-target offline queue operations.
18
+ * Implemented by QueueEngine (task 5.6) to provide target-scoped persistence.
19
+ */
20
+ interface OfflineQueueProvider {
21
+ fun enqueueForTarget(
22
+ targetPath: String,
23
+ latitude: Double,
24
+ longitude: Double,
25
+ timestamp: Long,
26
+ accuracy: Float,
27
+ speed: Float?,
28
+ altitude: Double?,
29
+ bearing: Float?
30
+ ): Future<Unit>
31
+
32
+ fun dequeueBatchForTarget(targetPath: String, size: Int): Future<List<QueuedLocation>>
33
+ fun countForTarget(targetPath: String): Future<Int>
34
+ fun evictOldestForTarget(targetPath: String): Future<Unit>
35
+ fun removeBatchForTarget(ids: List<String>): Future<Unit>
36
+ }
37
+
38
+ /**
39
+ * Callback interface for target write operations.
40
+ */
41
+ interface TargetWriteCallback {
42
+ fun onSuccess()
43
+ fun onError(errorCode: String, message: String)
44
+ }
45
+
46
+ /**
47
+ * Listener interface for error/warning events emitted by a TargetHandler.
48
+ */
49
+ interface TargetEventListener {
50
+ /**
51
+ * Called when a write operation fails after all retries are exhausted.
52
+ */
53
+ fun onWriteError(targetPath: String, method: String, errorCode: String, message: String)
54
+
55
+ /**
56
+ * Called when the offline queue overflows (10,000 cap reached).
57
+ */
58
+ fun onQueueOverflow(targetPath: String)
59
+ }
60
+
61
+ /**
62
+ * TargetHandler manages a single sync target's write lifecycle.
63
+ *
64
+ * Responsibilities:
65
+ * - Batch accumulation: buffers location data points until batchSize is reached
66
+ * - 30-second batch timeout: flushes partial batches after 30s of inactivity
67
+ * - Write execution: dispatches writes to Firebase using the configured method (set/push/update)
68
+ * - Retry cancellation: for set/update targets, cancels in-progress retries when newer data arrives
69
+ * - Offline queue delegation: enqueues data when offline and offlineQueue is enabled, discards otherwise
70
+ *
71
+ * Each TargetHandler operates independently and does not block other targets.
72
+ *
73
+ * @param config The sync target configuration
74
+ * @param firebaseService The Firebase service type ("RTDB" or "Firestore")
75
+ * @param networkChecker Function that returns current online status
76
+ * @param offlineQueueProvider Optional provider for offline persistence (null if offlineQueue is disabled)
77
+ * @param eventListener Listener for error/warning events
78
+ */
79
+ class TargetHandler(
80
+ private val config: SyncTargetConfig,
81
+ private val firebaseService: String,
82
+ private val networkChecker: () -> Boolean,
83
+ private val offlineQueueProvider: OfflineQueueProvider? = null,
84
+ private val eventListener: TargetEventListener? = null
85
+ ) {
86
+
87
+ /** The Firebase path this handler writes to. */
88
+ val targetPath: String get() = config.path
89
+
90
+ companion object {
91
+ private const val BATCH_TIMEOUT_MS = 30_000L
92
+ private const val BASE_DELAY_MS = 1000L
93
+ private const val MULTIPLIER = 2.0
94
+ private const val JITTER_MS = 200L
95
+ private const val MAX_QUEUE_SIZE = 10_000
96
+ }
97
+
98
+ private val executor: ExecutorService = Executors.newSingleThreadExecutor()
99
+ private val batch: MutableList<LocationDataPoint> = mutableListOf()
100
+ private var batchTimer: Timer? = null
101
+ private val retryGeneration = AtomicInteger(0)
102
+ private val isRetrying = AtomicBoolean(false)
103
+ private val isShutdown = AtomicBoolean(false)
104
+
105
+ private val maxRetries: Int
106
+ get() = if (config.method == "push") 5 else 3
107
+
108
+ /**
109
+ * Dispatch a new location data point to this target.
110
+ *
111
+ * If the device is offline and offlineQueue is enabled, the point is queued.
112
+ * If the device is offline and offlineQueue is disabled, the point is discarded.
113
+ * If online, the point is accumulated in the batch buffer.
114
+ * When the batch reaches batchSize, it is flushed (written to Firebase).
115
+ * For set/update targets, any in-progress retry is cancelled when new data arrives.
116
+ *
117
+ * @param location The location data point to dispatch
118
+ */
119
+ fun dispatch(location: LocationDataPoint) {
120
+ if (isShutdown.get()) return
121
+
122
+ executor.execute {
123
+ // For set/update targets, cancel any in-progress retry since newer data supersedes
124
+ if (config.method == "set" || config.method == "update") {
125
+ if (isRetrying.get()) {
126
+ retryGeneration.incrementAndGet()
127
+ isRetrying.set(false)
128
+ }
129
+ }
130
+
131
+ // Check if device is offline
132
+ if (!networkChecker()) {
133
+ if (config.offlineQueue && offlineQueueProvider != null) {
134
+ enqueueOffline(location)
135
+ }
136
+ // If offlineQueue is disabled, discard the data point
137
+ return@execute
138
+ }
139
+
140
+ // Accumulate in batch
141
+ batch.add(location)
142
+ resetBatchTimer()
143
+
144
+ // Check if batch is full
145
+ if (batch.size >= config.batchSize) {
146
+ flushBatch()
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Flush any partially-filled batch immediately.
153
+ * Called during stop() to ensure no data is lost.
154
+ * This method blocks until the flush is complete.
155
+ */
156
+ fun flush() {
157
+ if (isShutdown.get()) return
158
+
159
+ val future = executor.submit {
160
+ if (batch.isNotEmpty()) {
161
+ flushBatch()
162
+ }
163
+ }
164
+ try {
165
+ future.get()
166
+ } catch (e: Exception) {
167
+ // Ignore interruption during shutdown
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Shutdown this handler, cancelling timers and releasing resources.
173
+ */
174
+ fun shutdown() {
175
+ isShutdown.set(true)
176
+ cancelBatchTimer()
177
+ retryGeneration.incrementAndGet() // Cancel any in-progress retries
178
+ executor.shutdown()
179
+ }
180
+
181
+ /**
182
+ * Get the number of queued locations for this target's offline queue.
183
+ *
184
+ * @return The count of queued locations, or 0 if offlineQueue is disabled
185
+ */
186
+ fun getQueuedCount(): Int {
187
+ if (!config.offlineQueue || offlineQueueProvider == null) return 0
188
+ return try {
189
+ offlineQueueProvider.countForTarget(config.path).get()
190
+ } catch (e: Exception) {
191
+ 0
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Flush the offline queue for this target.
197
+ * Called when network connectivity is restored.
198
+ * Flushes in chronological order, respecting batchSize for batched writes.
199
+ */
200
+ fun flushOfflineQueue() {
201
+ if (isShutdown.get()) return
202
+ if (!config.offlineQueue || offlineQueueProvider == null) return
203
+
204
+ executor.execute {
205
+ doFlushOfflineQueue()
206
+ }
207
+ }
208
+
209
+ // --- Private implementation ---
210
+
211
+ private fun flushBatch() {
212
+ cancelBatchTimer()
213
+ if (batch.isEmpty()) return
214
+
215
+ val pointsToWrite = ArrayList(batch)
216
+ batch.clear()
217
+
218
+ executeWrite(pointsToWrite)
219
+ }
220
+
221
+ private fun executeWrite(points: List<LocationDataPoint>) {
222
+ val generation = retryGeneration.get()
223
+
224
+ when (config.method) {
225
+ "set" -> executeSet(points.last(), generation, points)
226
+ "push" -> executePush(points, generation)
227
+ "update" -> executeUpdate(points.last(), generation, points)
228
+ }
229
+ }
230
+
231
+ private fun executeSet(point: LocationDataPoint, generation: Int, allPoints: List<LocationDataPoint>) {
232
+ val data = locationToMap(point)
233
+ executeWithRetry(generation, allPoints) { callback ->
234
+ writeSet(data, callback)
235
+ }
236
+ }
237
+
238
+ private fun executePush(points: List<LocationDataPoint>, generation: Int) {
239
+ executeWithRetry(generation, points) { callback ->
240
+ writePush(points, callback)
241
+ }
242
+ }
243
+
244
+ private fun executeUpdate(point: LocationDataPoint, generation: Int, allPoints: List<LocationDataPoint>) {
245
+ val data = locationToMap(point)
246
+ executeWithRetry(generation, allPoints) { callback ->
247
+ writeUpdate(data, callback)
248
+ }
249
+ }
250
+
251
+ private fun writeSet(data: Map<String, Any?>, callback: TargetWriteCallback) {
252
+ when (firebaseService) {
253
+ "RTDB" -> {
254
+ val ref = FirebaseDatabase.getInstance().getReference(config.path)
255
+ ref.setValue(data)
256
+ .addOnSuccessListener { callback.onSuccess() }
257
+ .addOnFailureListener { e ->
258
+ callback.onError(classifyError(e), e.message ?: "RTDB set failed")
259
+ }
260
+ }
261
+ "Firestore" -> {
262
+ val doc = FirebaseFirestore.getInstance().document(config.path)
263
+ doc.set(data)
264
+ .addOnSuccessListener { callback.onSuccess() }
265
+ .addOnFailureListener { e ->
266
+ callback.onError(classifyError(e), e.message ?: "Firestore set failed")
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ private fun writePush(points: List<LocationDataPoint>, callback: TargetWriteCallback) {
273
+ when (firebaseService) {
274
+ "RTDB" -> {
275
+ val ref = FirebaseDatabase.getInstance().getReference(config.path)
276
+ val updates = hashMapOf<String, Any>()
277
+ for (point in points) {
278
+ val key = ref.push().key ?: continue
279
+ val map = locationToMap(point)
280
+ @Suppress("UNCHECKED_CAST")
281
+ updates[key] = map as Any
282
+ }
283
+ if (updates.isEmpty()) {
284
+ callback.onSuccess()
285
+ return
286
+ }
287
+ ref.updateChildren(updates)
288
+ .addOnSuccessListener { callback.onSuccess() }
289
+ .addOnFailureListener { e ->
290
+ callback.onError(classifyError(e), e.message ?: "RTDB push failed")
291
+ }
292
+ }
293
+ "Firestore" -> {
294
+ val firestore = FirebaseFirestore.getInstance()
295
+ val collection = firestore.collection(config.path)
296
+ val batch = firestore.batch()
297
+ for (point in points) {
298
+ val docRef = collection.document()
299
+ batch.set(docRef, locationToMap(point))
300
+ }
301
+ batch.commit()
302
+ .addOnSuccessListener { callback.onSuccess() }
303
+ .addOnFailureListener { e ->
304
+ callback.onError(classifyError(e), e.message ?: "Firestore push failed")
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ private fun writeUpdate(data: Map<String, Any?>, callback: TargetWriteCallback) {
311
+ when (firebaseService) {
312
+ "RTDB" -> {
313
+ val ref = FirebaseDatabase.getInstance().getReference(config.path)
314
+ @Suppress("UNCHECKED_CAST")
315
+ ref.updateChildren(data as Map<String, Any>)
316
+ .addOnSuccessListener { callback.onSuccess() }
317
+ .addOnFailureListener { e ->
318
+ callback.onError(classifyError(e), e.message ?: "RTDB update failed")
319
+ }
320
+ }
321
+ "Firestore" -> {
322
+ val doc = FirebaseFirestore.getInstance().document(config.path)
323
+ @Suppress("UNCHECKED_CAST")
324
+ doc.update(data as Map<String, Any>)
325
+ .addOnSuccessListener { callback.onSuccess() }
326
+ .addOnFailureListener { e ->
327
+ callback.onError(classifyError(e), e.message ?: "Firestore update failed")
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Execute a write operation with exponential backoff retry.
335
+ *
336
+ * For set/update targets, retries are cancelled if a newer generation arrives
337
+ * (i.e., new data supersedes the in-progress write).
338
+ *
339
+ * After all retries are exhausted, if offlineQueue is enabled, the data points
340
+ * are queued for later flush upon connectivity restoration (Requirement 7.5).
341
+ *
342
+ * @param generation The retry generation at the time of dispatch
343
+ * @param points The data points being written (used for offline queuing on failure)
344
+ * @param operation The write operation to execute
345
+ */
346
+ private fun executeWithRetry(generation: Int, points: List<LocationDataPoint>, operation: (TargetWriteCallback) -> Unit) {
347
+ isRetrying.set(true)
348
+ var attempt = 0
349
+
350
+ fun tryOperation() {
351
+ // Check if this retry has been superseded by newer data
352
+ if (retryGeneration.get() != generation) {
353
+ isRetrying.set(false)
354
+ return
355
+ }
356
+
357
+ attempt++
358
+ operation(object : TargetWriteCallback {
359
+ override fun onSuccess() {
360
+ isRetrying.set(false)
361
+ }
362
+
363
+ override fun onError(errorCode: String, message: String) {
364
+ // Don't retry non-transient errors
365
+ if (isNonTransientError(errorCode)) {
366
+ isRetrying.set(false)
367
+ eventListener?.onWriteError(config.path, config.method, errorCode, message)
368
+ return
369
+ }
370
+
371
+ // Check if superseded before retrying
372
+ if (retryGeneration.get() != generation) {
373
+ isRetrying.set(false)
374
+ return
375
+ }
376
+
377
+ if (attempt >= maxRetries) {
378
+ isRetrying.set(false)
379
+ eventListener?.onWriteError(
380
+ config.path,
381
+ config.method,
382
+ errorCode,
383
+ "Failed after $attempt attempts: $message"
384
+ )
385
+ // If offlineQueue is enabled, queue the data for later flush
386
+ if (config.offlineQueue && offlineQueueProvider != null) {
387
+ for (point in points) {
388
+ enqueueOffline(point)
389
+ }
390
+ }
391
+ return
392
+ }
393
+
394
+ // Exponential backoff with jitter
395
+ val delay = calculateBackoffDelay(attempt)
396
+ try {
397
+ Thread.sleep(delay)
398
+ } catch (e: InterruptedException) {
399
+ Thread.currentThread().interrupt()
400
+ isRetrying.set(false)
401
+ return
402
+ }
403
+
404
+ // Check again after sleep
405
+ if (retryGeneration.get() != generation) {
406
+ isRetrying.set(false)
407
+ return
408
+ }
409
+
410
+ tryOperation()
411
+ }
412
+ })
413
+ }
414
+
415
+ tryOperation()
416
+ }
417
+
418
+ /**
419
+ * Calculate exponential backoff delay with jitter.
420
+ *
421
+ * Formula: baseDelay × 2^(attempt-1) ± random jitter (±200ms)
422
+ * Result is never negative.
423
+ *
424
+ * @param attempt Current attempt number (1-indexed)
425
+ * @return Delay in milliseconds
426
+ */
427
+ internal fun calculateBackoffDelay(attempt: Int): Long {
428
+ val exponentialDelay = (BASE_DELAY_MS * MULTIPLIER.pow((attempt - 1).toDouble())).toLong()
429
+ val jitter = Random.nextLong(-JITTER_MS, JITTER_MS + 1)
430
+ return maxOf(0L, exponentialDelay + jitter)
431
+ }
432
+
433
+ private fun enqueueOffline(location: LocationDataPoint) {
434
+ if (offlineQueueProvider == null) return
435
+
436
+ // Check queue size cap
437
+ try {
438
+ val currentCount = offlineQueueProvider.countForTarget(config.path).get()
439
+ if (currentCount >= MAX_QUEUE_SIZE) {
440
+ // Evict oldest to make room
441
+ offlineQueueProvider.evictOldestForTarget(config.path).get()
442
+ eventListener?.onQueueOverflow(config.path)
443
+ }
444
+ } catch (e: Exception) {
445
+ // If we can't check count, try to enqueue anyway
446
+ }
447
+
448
+ offlineQueueProvider.enqueueForTarget(
449
+ targetPath = config.path,
450
+ latitude = location.latitude,
451
+ longitude = location.longitude,
452
+ timestamp = location.timestamp,
453
+ accuracy = location.accuracy,
454
+ speed = location.speed,
455
+ altitude = location.altitude,
456
+ bearing = location.bearing
457
+ )
458
+ }
459
+
460
+ private fun doFlushOfflineQueue() {
461
+ if (offlineQueueProvider == null) return
462
+
463
+ while (!isShutdown.get()) {
464
+ val batchSize = config.batchSize
465
+ val queuedBatch = try {
466
+ offlineQueueProvider.dequeueBatchForTarget(config.path, batchSize).get()
467
+ } catch (e: Exception) {
468
+ break
469
+ }
470
+
471
+ if (queuedBatch.isEmpty()) break
472
+
473
+ // Convert QueuedLocations to LocationDataPoints
474
+ val points = queuedBatch.map { queued ->
475
+ LocationDataPoint(
476
+ latitude = queued.latitude,
477
+ longitude = queued.longitude,
478
+ timestamp = queued.timestamp,
479
+ accuracy = queued.accuracy,
480
+ speed = queued.speed,
481
+ altitude = queued.altitude,
482
+ bearing = queued.bearing
483
+ )
484
+ }
485
+
486
+ // Write synchronously during flush
487
+ var writeSuccess = false
488
+ val generation = retryGeneration.get()
489
+
490
+ val latch = java.util.concurrent.CountDownLatch(1)
491
+ var writeError: String? = null
492
+
493
+ val callback = object : TargetWriteCallback {
494
+ override fun onSuccess() {
495
+ writeSuccess = true
496
+ latch.countDown()
497
+ }
498
+
499
+ override fun onError(errorCode: String, message: String) {
500
+ writeError = message
501
+ latch.countDown()
502
+ }
503
+ }
504
+
505
+ // Execute the write based on method
506
+ when (config.method) {
507
+ "set" -> writeSet(locationToMap(points.last()), callback)
508
+ "push" -> writePush(points, callback)
509
+ "update" -> writeUpdate(locationToMap(points.last()), callback)
510
+ }
511
+
512
+ try {
513
+ latch.await()
514
+ } catch (e: InterruptedException) {
515
+ Thread.currentThread().interrupt()
516
+ break
517
+ }
518
+
519
+ if (writeSuccess) {
520
+ // Remove successfully written points from queue
521
+ val ids = queuedBatch.map { it.id }
522
+ try {
523
+ offlineQueueProvider.removeBatchForTarget(ids).get()
524
+ } catch (e: Exception) {
525
+ // Log but continue
526
+ }
527
+ } else {
528
+ // Stop flushing on failure - retain data for retry later
529
+ break
530
+ }
531
+ }
532
+ }
533
+
534
+ private fun resetBatchTimer() {
535
+ cancelBatchTimer()
536
+ if (config.batchSize <= 1) return // No timer needed for immediate writes
537
+
538
+ batchTimer = Timer("BatchTimer-${config.path}", true).apply {
539
+ schedule(object : TimerTask() {
540
+ override fun run() {
541
+ if (!isShutdown.get()) {
542
+ executor.execute {
543
+ if (batch.isNotEmpty()) {
544
+ flushBatch()
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }, BATCH_TIMEOUT_MS)
550
+ }
551
+ }
552
+
553
+ private fun cancelBatchTimer() {
554
+ batchTimer?.cancel()
555
+ batchTimer = null
556
+ }
557
+
558
+ /**
559
+ * Convert a LocationDataPoint to a Firebase-compatible map.
560
+ * Null optional fields are included as null (not placeholder values).
561
+ */
562
+ private fun locationToMap(point: LocationDataPoint): Map<String, Any?> {
563
+ val map = hashMapOf<String, Any?>(
564
+ "latitude" to point.latitude,
565
+ "longitude" to point.longitude,
566
+ "timestamp" to point.timestamp,
567
+ "accuracy" to point.accuracy.toDouble()
568
+ )
569
+ // Include optional fields as null when unavailable (never use placeholders)
570
+ if (point.speed != null) {
571
+ map["speed"] = point.speed.toDouble()
572
+ }
573
+ if (point.altitude != null) {
574
+ map["altitude"] = point.altitude
575
+ }
576
+ if (point.bearing != null) {
577
+ map["bearing"] = point.bearing.toDouble()
578
+ }
579
+ return map
580
+ }
581
+
582
+ /**
583
+ * Classify a Firebase exception as transient or non-transient.
584
+ * Non-transient errors (permission denied, auth failures) should not be retried.
585
+ */
586
+ private fun classifyError(exception: Exception): String {
587
+ val message = exception.message?.lowercase() ?: ""
588
+ return when {
589
+ message.contains("permission") || message.contains("denied") -> "PERMISSION_DENIED"
590
+ message.contains("unauthenticated") || message.contains("auth") -> "PERMISSION_DENIED"
591
+ else -> "FIREBASE_WRITE_FAILED"
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Check if an error code represents a non-transient error that should not be retried.
597
+ */
598
+ private fun isNonTransientError(errorCode: String): Boolean {
599
+ return errorCode == "PERMISSION_DENIED"
600
+ }
601
+ }
@@ -0,0 +1,64 @@
1
+ package com.livetracking
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactMethod
5
+ import com.facebook.react.bridge.Promise
6
+ import com.facebook.react.module.annotations.ReactModule
7
+
8
+ /**
9
+ * New Architecture (TurboModule) implementation.
10
+ * This file is only compiled when the new architecture is enabled.
11
+ *
12
+ * Extends NativeLiveTrackingSpec (generated from TurboModule codegen) and delegates
13
+ * all logic to LiveTrackingModuleImpl.
14
+ * Exposes methods: configure, start, stop, getStatus, getQueuedLocations.
15
+ * Events are emitted directly from LiveTrackingModuleImpl via RCTDeviceEventEmitter.
16
+ *
17
+ * Requirements: 9.1, 9.2, 9.3, 9.4, 11.2
18
+ */
19
+ @ReactModule(name = LiveTrackingModuleImpl.NAME)
20
+ class LiveTrackingModule(reactContext: ReactApplicationContext) :
21
+ NativeLiveTrackingSpec(reactContext) {
22
+
23
+ private val impl = LiveTrackingModuleImpl(reactContext)
24
+
25
+ companion object {
26
+ const val NAME = LiveTrackingModuleImpl.NAME
27
+ }
28
+
29
+ override fun getName(): String = NAME
30
+
31
+ override fun configure(config: String, promise: Promise) {
32
+ impl.configure(config, promise)
33
+ }
34
+
35
+ override fun start(promise: Promise) {
36
+ impl.start(promise)
37
+ }
38
+
39
+ override fun stop(promise: Promise) {
40
+ impl.stop(promise)
41
+ }
42
+
43
+ override fun getStatus(promise: Promise) {
44
+ impl.getStatus(promise)
45
+ }
46
+
47
+ override fun getQueuedLocations(promise: Promise) {
48
+ impl.getQueuedLocations(promise)
49
+ }
50
+
51
+ override fun getQueuedLocationsByTarget(promise: Promise) {
52
+ impl.getQueuedLocationsByTarget(promise)
53
+ }
54
+
55
+ @ReactMethod
56
+ override fun addListener(eventName: String) {
57
+ // Required for RN event emitter - no-op
58
+ }
59
+
60
+ @ReactMethod
61
+ override fun removeListeners(count: Double) {
62
+ // Required for RN event emitter - no-op
63
+ }
64
+ }