expo-beacon 0.6.11 → 0.6.12

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.
@@ -459,8 +459,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
459
459
 
460
460
  private fun scheduleTimeoutIfConfigured(region: Region) {
461
461
  val seconds = beaconTimeouts[region.uniqueId] ?: return
462
- // Cancel any existing timer (shouldn't happen, but be safe)
463
- cancelTimeout(region.uniqueId)
462
+ // If a timer is already running, don't reset it. Miss-based exits clear
463
+ // enteredRegions without cancelling the timer, so a subsequent re-entry
464
+ // must not restart the clock — doing so would defer the timeout indefinitely.
465
+ if (timeoutRunnables.containsKey(region.uniqueId)) return
464
466
  val runnable = Runnable {
465
467
  timeoutRunnables.remove(region.uniqueId)
466
468
  // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
@@ -850,6 +850,7 @@ public class ExpoBeaconModule: Module {
850
850
  postBeaconNotification(identifier: identifier, eventType: "enter")
851
851
  scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
852
852
  case .exit:
853
+ smoothedDistances.removeValue(forKey: identifier)
853
854
  cancelEddystoneTimeout(identifier: identifier)
854
855
  sendLoggedEvent("onEddystoneExit", [
855
856
  "identifier": identifier,
@@ -1078,6 +1079,7 @@ public class ExpoBeaconModule: Module {
1078
1079
  eddystoneEnterCounters[identifier] = 0
1079
1080
  eddystoneExitCounters[identifier] = 0
1080
1081
  eddystoneLatestSeen.removeValue(forKey: identifier)
1082
+ smoothedDistances.removeValue(forKey: identifier)
1081
1083
  // Do NOT cancel the timeout here — same reason as iBeacon miss-based exit.
1082
1084
 
1083
1085
  let ns = paired["namespace"] as? String ?? ""
@@ -1098,9 +1100,10 @@ public class ExpoBeaconModule: Module {
1098
1100
  // MARK: - Timeout timer helpers
1099
1101
 
1100
1102
  private func scheduleBeaconTimeout(identifier: String, beacon: CLBeacon) {
1101
- // Cancel any existing timer for this identifier (shouldn't happen, but be safe)
1102
- beaconTimeoutTimers[identifier]?.cancel()
1103
- beaconTimeoutTimers.removeValue(forKey: identifier)
1103
+ // If a timer is already running, don't reset it. Miss-based exits clear
1104
+ // enteredRegions without cancelling the timer, so a subsequent re-entry
1105
+ // must not restart the clock — doing so would defer the timeout indefinitely.
1106
+ guard beaconTimeoutTimers[identifier] == nil else { return }
1104
1107
 
1105
1108
  let paired = loadPairedBeaconsRaw().first { ($0["identifier"] as? String) == identifier }
1106
1109
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1123,8 +1126,8 @@ public class ExpoBeaconModule: Module {
1123
1126
  }
1124
1127
 
1125
1128
  private func scheduleEddystoneTimeout(identifier: String, namespace: String, instance: String) {
1126
- eddystoneTimeoutTimers[identifier]?.cancel()
1127
- eddystoneTimeoutTimers.removeValue(forKey: identifier)
1129
+ // If a timer is already running, don't reset it — same reason as iBeacon timeout.
1130
+ guard eddystoneTimeoutTimers[identifier] == nil else { return }
1128
1131
 
1129
1132
  let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
1130
1133
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1197,16 +1200,21 @@ public class ExpoBeaconModule: Module {
1197
1200
  // MARK: - Distance smoothing + enter/exit hysteresis
1198
1201
 
1199
1202
  /// Apply exponential moving average (EMA) smoothing to a raw distance reading.
1200
- /// Returns nil if the reading is a jump outlier (raw differs from smoothed by > DISTANCE_JUMP_FACTOR).
1203
+ /// If the reading is a large jump (> DISTANCE_JUMP_FACTOR), resets the EMA to the new
1204
+ /// value instead of rejecting it — this ensures distance events keep flowing when the
1205
+ /// user moves away from a beacon, rather than freezing because the EMA is stuck at the
1206
+ /// old close-range value and every new far-range reading is rejected.
1201
1207
  private func smoothDistance(identifier: String, rawDistance: Double) -> Double? {
1202
1208
  guard let prev = smoothedDistances[identifier] else {
1203
1209
  smoothedDistances[identifier] = rawDistance
1204
1210
  return rawDistance
1205
1211
  }
1206
- // Jump guard: if the raw value is wildly different, treat as outlier
1212
+ // Jump guard: if the raw value is wildly different, reset EMA to the new reading
1213
+ // so the hysteresis pipeline keeps receiving data and can fire the exit event.
1207
1214
  let ratio = prev > 0.001 ? rawDistance / prev : rawDistance
1208
1215
  if ratio > Self.DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / Self.DISTANCE_JUMP_FACTOR) {
1209
- return nil
1216
+ smoothedDistances[identifier] = rawDistance
1217
+ return rawDistance
1210
1218
  }
1211
1219
  let smoothed = Self.DISTANCE_EMA_ALPHA * rawDistance + (1 - Self.DISTANCE_EMA_ALPHA) * prev
1212
1220
  smoothedDistances[identifier] = smoothed
@@ -1356,6 +1364,7 @@ public class ExpoBeaconModule: Module {
1356
1364
  postBeaconNotification(identifier: identifier, eventType: "enter")
1357
1365
  scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1358
1366
  case .exit:
1367
+ smoothedDistances.removeValue(forKey: identifier)
1359
1368
  cancelBeaconTimeout(identifier: identifier)
1360
1369
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
1361
1370
  postBeaconNotification(identifier: identifier, eventType: "exit")
@@ -1379,6 +1388,7 @@ public class ExpoBeaconModule: Module {
1379
1388
  missCounters[identifier] = 0
1380
1389
  enterCounters[identifier] = 0
1381
1390
  exitCounters[identifier] = 0
1391
+ smoothedDistances.removeValue(forKey: identifier)
1382
1392
  // Do NOT cancel the timeout here. A miss-based exit is triggered by
1383
1393
  // ranging gaps (e.g. accuracy == -1), not a confirmed physical departure.
1384
1394
  // Cancelling here prevents the timeout from firing when it is longer than
@@ -1429,6 +1439,7 @@ public class ExpoBeaconModule: Module {
1429
1439
  enterCounters.removeValue(forKey: identifier)
1430
1440
  exitCounters.removeValue(forKey: identifier)
1431
1441
  missCounters.removeValue(forKey: identifier)
1442
+ smoothedDistances.removeValue(forKey: identifier)
1432
1443
  if wasEntered {
1433
1444
  cancelBeaconTimeout(identifier: identifier)
1434
1445
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, region: beaconRegion, event: "exit"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",