react-native-mytatva-rn-sdk 1.2.66 → 1.2.68

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.
@@ -49,6 +49,8 @@ import kotlinx.coroutines.SupervisorJob
49
49
  import kotlinx.coroutines.delay
50
50
  import kotlinx.coroutines.launch
51
51
  import kotlinx.coroutines.withContext
52
+ import kotlinx.coroutines.sync.Mutex
53
+ import kotlinx.coroutines.sync.withLock
52
54
  import org.json.JSONObject
53
55
  import java.io.File
54
56
  import java.text.SimpleDateFormat
@@ -56,6 +58,9 @@ import java.util.Date
56
58
  import java.util.Locale
57
59
  import java.util.Timer
58
60
  import java.util.TimerTask
61
+ import java.util.concurrent.ConcurrentHashMap
62
+ import java.util.concurrent.ConcurrentLinkedQueue
63
+ import java.util.concurrent.atomic.AtomicBoolean
59
64
  import kotlin.coroutines.resume
60
65
  import kotlin.coroutines.suspendCoroutine
61
66
 
@@ -69,11 +74,14 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
69
74
  var prefsHelper: SharedPreferencesLibraryUtil? = null
70
75
  private val apiScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
71
76
  private var debounceJob: Job? = null
72
- private var isBatchProcessing = false
77
+ private val isBatchProcessing = AtomicBoolean(false)
73
78
  private var lastDeviceStatus: String? = null
74
79
  private var glucoseObserver: Observer<PocGlucose?>? = null
75
80
  private var isObserving = false
76
- private var lastProcessedSyncTimeInMillis: Long? = null
81
+ private val processedGlucoseIds = ConcurrentHashMap.newKeySet<Int>()
82
+ private val pendingDataQueue = ConcurrentLinkedQueue<PocGlucose>()
83
+ private val metadataLock = Mutex()
84
+ private var uploadJob: Job? = null
77
85
 
78
86
  init {
79
87
  mReactContext = reactContext
@@ -412,56 +420,42 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
412
420
  )
413
421
  return
414
422
  }
415
- if (isBatchProcessing) {
423
+
424
+ // Don't prevent observation during batch processing - they can coexist
425
+ // Just log it for monitoring
426
+ if (isBatchProcessing.get()) {
416
427
  Log.d(
417
- "observeAllGlucoseData",
418
- "Batch processing in progress, skipping duplicate call"
428
+ "observeGlucoseData",
429
+ "Batch processing in progress, but starting observer anyway"
419
430
  )
420
- return
421
431
  }
422
432
 
423
433
  // Remove existing observer if any to prevent memory leaks
424
- stopObservingGlucoseData()
434
+ if (glucoseObserver != null) {
435
+ stopObservingGlucoseDataInternal()
436
+ }
425
437
 
426
- // Wait a bit for cleanup to complete
427
- Handler(Looper.getMainLooper()).postDelayed({
438
+ // Create new observer immediately without delay to prevent data loss
439
+ Handler(Looper.getMainLooper()).post {
428
440
  // Create new observer with explicit nullable parameter type
429
441
  glucoseObserver = Observer<PocGlucose?> { pocGlucose ->
430
- val currentTime = System.currentTimeMillis()
431
- var dataAge = System.currentTimeMillis()
432
-
433
- val lastSyncData = prefsHelper?.lastSyncData
434
- val currentSyncTimeInMillis = lastSyncData?.timeInMillis
435
-
436
- if (lastSyncData != null) {
437
- dataAge = currentTime - lastSyncData.timeInMillis
442
+ if (pocGlucose == null) {
443
+ Log.d("observeGlucoseData", "Received null glucose data - skipping")
444
+ return@Observer
438
445
  }
439
446
 
440
- // Only proceed if timeInMillis is different from last processed
441
- if (currentSyncTimeInMillis != null && lastProcessedSyncTimeInMillis != null && currentSyncTimeInMillis == lastProcessedSyncTimeInMillis) {
442
- Log.d("SyncTimeCheck", "Duplicate sync time, skipping function calls")
447
+ // Check for duplicate using glucoseId instead of timestamp
448
+ val glucoseId = pocGlucose.glucoseId ?: run {
449
+ Log.w("observeGlucoseData", "Glucose ID is null, skipping")
443
450
  return@Observer
444
451
  }
445
452
 
446
- if ((pocGlucose != null && lastSyncData == null) || (pocGlucose != null && lastSyncData != null && dataAge <= 4 * 60 * 1000L)) {
447
- handleGlucoseData(pocGlucose, envType)
448
- Log.d(
449
- "observeGlucoseData",
450
- "Received glucose data - processing"
451
- )
452
- // Update last processed sync time
453
- lastProcessedSyncTimeInMillis = currentSyncTimeInMillis
453
+ // Add to queue for processing - ensures no data is lost
454
+ if (pendingDataQueue.offer(pocGlucose)) {
455
+ Log.d("observeGlucoseData", "Added glucose data to queue: $glucoseId")
456
+ triggerDataUpload(envType)
454
457
  } else {
455
-
456
- Handler(Looper.getMainLooper()).postDelayed({
457
- observeAllGlucoseData(userToken, isForClear, envType)
458
- }, 100)
459
- Log.d(
460
- "observeGlucoseData",
461
- "Received null glucose data - skipping processing"
462
- )
463
- // Update last processed sync time
464
- lastProcessedSyncTimeInMillis = currentSyncTimeInMillis
458
+ Log.e("observeGlucoseData", "Failed to add data to queue: $glucoseId")
465
459
  }
466
460
  }
467
461
 
@@ -477,7 +471,7 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
477
471
  isObserving = false
478
472
  }
479
473
  }
480
- }, 50) // Small delay to ensure previous cleanup is complete
474
+ }
481
475
 
482
476
  } catch (e: Exception) {
483
477
  Log.e("observeGlucoseData", "observeGlucoseData: ${e.message}")
@@ -487,6 +481,56 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
487
481
  }
488
482
  }
489
483
 
484
+ // New method to handle data upload from queue
485
+ private fun triggerDataUpload(envType: String) {
486
+ // If upload is already in progress, just return
487
+ if (uploadJob?.isActive == true) {
488
+ return
489
+ }
490
+
491
+ uploadJob = apiScope.launch {
492
+ while (pendingDataQueue.isNotEmpty()) {
493
+ val pocGlucose = pendingDataQueue.poll() ?: continue
494
+
495
+ // Check for duplicate using glucoseId
496
+ val glucoseId = pocGlucose.glucoseId ?: continue
497
+
498
+ if (!processedGlucoseIds.add(glucoseId)) {
499
+ Log.d("triggerDataUpload", "Already processed: $glucoseId, skipping")
500
+ continue
501
+ }
502
+
503
+ val currentTime = System.currentTimeMillis()
504
+ val lastSyncData = prefsHelper?.lastSyncData
505
+ val dataAge = if (lastSyncData != null) {
506
+ currentTime - lastSyncData.timeInMillis
507
+ } else {
508
+ 0L
509
+ }
510
+
511
+ // If data is recent (within 4 minutes), upload immediately
512
+ if (lastSyncData == null || dataAge <= 4 * 60 * 1000L) {
513
+ handleGlucoseData(pocGlucose, envType)
514
+ Log.d("triggerDataUpload", "Uploaded recent data: $glucoseId")
515
+ } else {
516
+ // Data is old, need batch processing
517
+ // Put it back in queue for batch to handle
518
+ pendingDataQueue.offer(pocGlucose)
519
+ Log.d("triggerDataUpload", "Old data detected, triggering batch: $glucoseId")
520
+
521
+ // Trigger batch process on main thread
522
+ withContext(Dispatchers.Main) {
523
+ observeAllGlucoseData(userToken, false, envType)
524
+ }
525
+ break // Exit loop to let batch take over
526
+ }
527
+
528
+ // Small delay to avoid overwhelming the API
529
+ delay(100)
530
+ }
531
+ }
532
+ }
533
+
490
534
  private fun handleGlucoseData(pocGlucose: PocGlucose, envType: String) {
491
535
  try {
492
536
  // Additional safety check
@@ -592,8 +636,9 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
592
636
  fun observeAllGlucoseData(token: String, isForClear: Boolean = false, envType: String) {
593
637
  Log.d("function call", "observeAllGlucoseData")
594
638
 
595
- if (isBatchProcessing) {
596
- Log.d("observeAllGlucoseData", "Batch processing in progress, skipping duplicate call")
639
+ // Use atomic compareAndSet to prevent concurrent batch processing
640
+ if (!isBatchProcessing.compareAndSet(false, true)) {
641
+ Log.d("observeAllGlucoseData", "Batch processing already in progress, skipping duplicate call")
597
642
  return
598
643
  }
599
644
 
@@ -601,60 +646,94 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
601
646
  env = envType
602
647
 
603
648
  CoroutineScope(Dispatchers.IO).launch {
604
- // Step 1: Clear DB first if needed
605
- /* if (isForClear) {
606
- Log.d("delete1111-observe", "observe delete")
607
- mModel.clearAllGlucoseAndDeviceData() // suspend or blocking DB operation
608
- } */
649
+ try {
650
+ // Process any pending data from queue first to ensure nothing is lost
651
+ val pendingData = mutableListOf<PocGlucose>()
652
+ while (pendingDataQueue.isNotEmpty()) {
653
+ pendingDataQueue.poll()?.let { pendingData.add(it) }
654
+ }
655
+
656
+ if (pendingData.isNotEmpty()) {
657
+ Log.d("observeAllGlucoseData", "Found ${pendingData.size} items in pending queue")
658
+ }
609
659
 
610
- // Step 2: Back to main thread to stop observers and proceed
611
- withContext(Dispatchers.Main) {
612
- stopObservingGlucoseData()
660
+ val lastSyncData = prefsHelper?.lastSyncData
661
+ Log.d("lastSyncData: ", Gson().toJson(lastSyncData).toString())
613
662
 
614
- Handler(Looper.getMainLooper()).postDelayed({
615
- try {
616
- val lastSyncData = prefsHelper?.lastSyncData
617
- Log.d("lastSyncData: ", Gson().toJson(lastSyncData).toString())
618
-
619
- val currentTime = System.currentTimeMillis()
620
- val dataAge =
621
- if (lastSyncData != null) currentTime - lastSyncData.timeInMillis else 0L
622
-
623
- if (lastSyncData != null && dataAge > 3 * 60 * 1000L) {
624
- isBatchProcessing = true
625
- CoroutineScope(Dispatchers.IO).launch {
626
- val glucoseData =
627
- mModel.getGlucoseBetweenTime(lastSyncData.timeInMillis)
628
-
629
- Log.d("observeAllGlucoseData: ", glucoseData.toString())
630
- Log.d("Last sync time: ", lastSyncData.lastSyncTime.toString())
631
- Log.d("current time: ", System.currentTimeMillis().toString())
632
-
633
- if (!glucoseData.isNullOrEmpty()) {
634
- processBatchDataAndStartObserver(
635
- glucoseData,
636
- isForClear,
637
- envType
638
- )
639
- } else {
640
- Log.d(
641
- "observeAllGlucoseData",
642
- "No historical data found, starting live observation"
643
- )
644
- withContext(Dispatchers.Main) {
645
- observeGlucoseData(userToken, isForClear, envType)
646
- }
647
- }
663
+ val currentTime = System.currentTimeMillis()
664
+ val dataAge = if (lastSyncData != null) currentTime - lastSyncData.timeInMillis else 0L
665
+
666
+ if (lastSyncData != null && dataAge > 3 * 60 * 1000L) {
667
+ val glucoseData = mModel.getGlucoseBetweenTime(lastSyncData.timeInMillis)
668
+
669
+ Log.d("observeAllGlucoseData", "Retrieved ${glucoseData?.size ?: 0} records from DB")
670
+
671
+ // Combine database data with pending queue data
672
+ val allData = mutableListOf<PocGlucose>()
673
+ glucoseData?.let { allData.addAll(it) }
674
+ allData.addAll(pendingData)
675
+
676
+ // Log BEFORE sorting to see database order
677
+ allData.take(5).forEachIndexed { i, data ->
678
+ Log.d("BEFORE Sort [$i]", "timeInMillis: ${data.timeInMillis}, glucoseId: ${data.glucoseId}")
679
+ }
680
+
681
+ // Sort by timestamp to ensure chronological order (oldest first)
682
+ // This is critical when device reconnects after being out of range
683
+ val sortedGlucoseData = allData.sortedBy { it.timeInMillis }
684
+
685
+ // Log AFTER sorting to verify chronological order
686
+ sortedGlucoseData.take(5).forEachIndexed { i, data ->
687
+ Log.d("AFTER Sort [$i]", "timeInMillis: ${data.timeInMillis}, glucoseId: ${data.glucoseId}")
688
+ }
689
+
690
+ if (sortedGlucoseData.isNotEmpty()) {
691
+ Log.d("observeAllGlucoseData", "✅ Sorted ${sortedGlucoseData.size} records chronologically")
692
+ Log.d("observeAllGlucoseData", "Oldest: ${sortedGlucoseData.first().timeInMillis}")
693
+ Log.d("observeAllGlucoseData", "Newest: ${sortedGlucoseData.last().timeInMillis}")
694
+ }
695
+
696
+ if (sortedGlucoseData.isNotEmpty()) {
697
+ processBatchDataAndStartObserver(
698
+ sortedGlucoseData,
699
+ isForClear,
700
+ envType
701
+ )
702
+ } else {
703
+ Log.d(
704
+ "observeAllGlucoseData",
705
+ "No historical data found, starting live observation"
706
+ )
707
+ withContext(Dispatchers.Main) {
708
+ if (!isObserving) {
709
+ observeGlucoseData(userToken, isForClear, envType)
648
710
  }
649
- } else {
650
- isBatchProcessing = false
711
+ }
712
+ }
713
+ } else {
714
+ // No batch needed, but process pending data if any
715
+ if (pendingData.isNotEmpty()) {
716
+ Log.d("observeAllGlucoseData", "Processing ${pendingData.size} pending items")
717
+ pendingData.forEach { data ->
718
+ val glucoseId = data.glucoseId
719
+ if (glucoseId != null && processedGlucoseIds.add(glucoseId)) {
720
+ handleGlucoseData(data, envType)
721
+ }
722
+ }
723
+ }
724
+
725
+ withContext(Dispatchers.Main) {
726
+ if (!isObserving) {
651
727
  observeGlucoseData(userToken, isForClear, envType)
652
728
  }
653
- } catch (e: Exception) {
654
- isBatchProcessing = false
655
- observeGlucoseData(userToken, isForClear, envType)
656
729
  }
657
- }, 100)
730
+ }
731
+ } catch (e: Exception) {
732
+ Log.e("observeAllGlucoseData", "Error in batch processing: ${e.message}", e)
733
+ } finally {
734
+ // Always reset the flag
735
+ isBatchProcessing.set(false)
736
+ Log.d("observeAllGlucoseData", "Batch processing completed, flag reset")
658
737
  }
659
738
  }
660
739
  }
@@ -679,8 +758,9 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
679
758
  Log.w("processBatchDataAndStartObserver", "Batch processing had failures")
680
759
  }
681
760
 
761
+ // Don't reset flag here - it's reset in observeAllGlucoseData's finally block
762
+
682
763
  // Ensure we're on main thread and not already observing
683
- isBatchProcessing = false
684
764
  withContext(Dispatchers.Main) {
685
765
  if (!isObserving) {
686
766
  Log.d(
@@ -699,7 +779,6 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
699
779
  } catch (e: Exception) {
700
780
  Log.e("processBatchDataAndStartObserver", "Error in batch processing: ${e.message}")
701
781
  // Start live observation even on error
702
- isBatchProcessing = false
703
782
  withContext(Dispatchers.Main) {
704
783
  if (!isObserving) {
705
784
  observeGlucoseData(userToken, isForClear, envType)
@@ -733,6 +812,9 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
733
812
  // Cancel all coroutines
734
813
  debounceJob?.cancel()
735
814
  debounceJob = null
815
+
816
+ uploadJob?.cancel()
817
+ uploadJob = null
736
818
 
737
819
  // Reset user token
738
820
  userToken = ""
@@ -740,12 +822,16 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
740
822
  // Reset device status
741
823
  lastDeviceStatus = null
742
824
 
825
+ // Clear processed IDs and pending queue
826
+ processedGlucoseIds.clear()
827
+ pendingDataQueue.clear()
828
+
829
+ // Reset batch processing flag
830
+ isBatchProcessing.set(false)
831
+
743
832
  // Clear any cached data if needed
744
833
  prefsHelper?.clearQRInformation() // if you have such method
745
834
 
746
- // Reset last processed sync time
747
- lastProcessedSyncTimeInMillis = null
748
-
749
835
  } catch (e: Exception) {
750
836
  Log.e("resetCgmState", "Error resetting CGM state: ${e.message}")
751
837
  }
@@ -814,13 +900,34 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
814
900
  continue
815
901
  }
816
902
 
903
+ // Log the actual timestamps being sent to verify order
904
+ transformedLogs.forEachIndexed { i, log ->
905
+ Log.d(
906
+ "Batch $index Record $i",
907
+ "timeInMillis: ${log.timeInMillis}, glucoseId: ${log.glucoseId}"
908
+ )
909
+ }
910
+
817
911
  val allResult = AllCGMLogRequest(vendor = "GoodFlip", logs = transformedLogs)
818
912
  val json = Gson().toJson(allResult)
819
913
 
914
+ // Check if logs array is empty - skip API call if so
915
+ if (transformedLogs.isEmpty()) {
916
+ Log.d(
917
+ "processBatchDataSynchronously",
918
+ "Batch $index skipped - logs array is empty, not hitting API"
919
+ )
920
+ continue
921
+ }
922
+
820
923
  Log.d(
821
924
  "Batch Upload",
822
925
  "Processing batch $index with ${transformedLogs.size} records"
823
926
  )
927
+ Log.d(
928
+ "Batch Upload",
929
+ "First timestamp: ${transformedLogs.firstOrNull()?.timeInMillis}, Last timestamp: ${transformedLogs.lastOrNull()?.timeInMillis}"
930
+ )
824
931
  logLongJson("Batch $index JSON=>>> ", json)
825
932
 
826
933
  val uploadSuccessful = uploadBatchSynchronously(json, index, envType)
@@ -864,6 +971,11 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
864
971
 
865
972
  @ReactMethod
866
973
  fun stopObservingGlucoseData() {
974
+ stopObservingGlucoseDataInternal()
975
+ }
976
+
977
+ // Internal method that doesn't clear the queue (used during restarts)
978
+ private fun stopObservingGlucoseDataInternal() {
867
979
  Log.d("stopObservingGlucoseData", "Stopping glucose data observation")
868
980
 
869
981
  try {
@@ -890,9 +1002,6 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
890
1002
  // Reset last device status
891
1003
  lastDeviceStatus = null
892
1004
 
893
- // Reset last processed sync time
894
- lastProcessedSyncTimeInMillis = null
895
-
896
1005
  } catch (e: Exception) {
897
1006
  Log.e("stopObservingGlucoseData", "Error stopping observer: ${e.message}")
898
1007
  }
@@ -929,21 +1038,28 @@ class CgmTrackyLibModule(reactContext: ReactApplicationContext) :
929
1038
 
930
1039
  private fun updateSyncMetadata(lastRecord: PocGlucose?) {
931
1040
  lastRecord?.let {
932
- try {
933
- val syncData = SyncMeta(
934
- System.currentTimeMillis(),
935
- it.timeInMillis,
936
- it.deviceId,
937
- it.glucoseId
938
- )
939
- prefsHelper?.lastSyncData = syncData
940
- lastProcessedSyncTimeInMillis = null
941
- Log.d(
942
- "Sync Metadata",
943
- "Sync metadata updated: glucoseId=${it.glucoseId}, time=${it.timeInMillis}"
944
- )
945
- } catch (e: Exception) {
946
- Log.e("updateSyncMetadata", "Error updating sync metadata: ${e.message}")
1041
+ apiScope.launch {
1042
+ metadataLock.withLock {
1043
+ try {
1044
+ val syncData = SyncMeta(
1045
+ System.currentTimeMillis(),
1046
+ it.timeInMillis,
1047
+ it.deviceId,
1048
+ it.glucoseId
1049
+ )
1050
+ prefsHelper?.lastSyncData = syncData
1051
+
1052
+ // Mark this glucose ID as processed
1053
+ it.glucoseId?.let { id -> processedGlucoseIds.add(id) }
1054
+
1055
+ Log.d(
1056
+ "Sync Metadata",
1057
+ "Sync metadata updated: glucoseId=${it.glucoseId}, time=${it.timeInMillis}"
1058
+ )
1059
+ } catch (e: Exception) {
1060
+ Log.e("updateSyncMetadata", "Error updating sync metadata: ${e.message}")
1061
+ }
1062
+ }
947
1063
  }
948
1064
  }
949
1065
  }
@@ -489,6 +489,24 @@ RCT_EXPORT_METHOD(reconnectCgmTracky:(NSString *)token envType: (NSString *)env
489
489
  });
490
490
  }
491
491
 
492
+ RCT_REMAP_METHOD(getTrackLibDbPath,
493
+ getTrackLibDbPathWithResolver:(RCTPromiseResolveBlock)resolve
494
+ rejecter:(RCTPromiseRejectBlock)reject)
495
+ {
496
+ @try {
497
+ NSArray<NSURL *> *urls = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
498
+ NSURL *documentsURL = [urls lastObject];
499
+ NSURL *storeURL = [documentsURL URLByAppendingPathComponent:@"KaiLiTe.sqlite"];
500
+ if (storeURL.path.length > 0) {
501
+ resolve(storeURL.path);
502
+ } else {
503
+ reject(@"ERROR_DB_PATH", @"Failed to construct DB path", nil);
504
+ }
505
+ } @catch (NSException *exception) {
506
+ NSError *error = [NSError errorWithDomain:@"com.mytatvarnsdk" code:500 userInfo:@{NSLocalizedDescriptionKey: exception.reason ?: @"Unknown error"}];
507
+ reject(@"ERROR_DB_PATH", @"Exception while fetching DB path", error);
508
+ }
509
+ }
492
510
 
493
511
  RCT_EXPORT_METHOD(openHelpSupport)
494
512
  {
@@ -429,6 +429,14 @@ class FinalViewModel: NSObject {
429
429
  let cgmLogs = filteredBatch.map { ReceiveDataToLog(data: $0) }
430
430
  let payload = Payload(logs: cgmLogs)
431
431
 
432
+ // Check if logs array is empty - skip API call if so
433
+ if cgmLogs.isEmpty {
434
+ print("Batch \(index + 1) skipped - logs array is empty, not hitting API")
435
+ // Move to next batch
436
+ self.uploadBatch(batches: batches, index: index + 1)
437
+ return
438
+ }
439
+
432
440
  print("====================================> called uploadBatch")
433
441
  API.shared.postCGMData(data: payload, environment: .stage) {
434
442
  print("====================================> uploaded successfully")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-mytatva-rn-sdk",
3
- "version": "1.2.66",
3
+ "version": "1.2.68",
4
4
  "description": "a package to inject data into visit health pwa",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",