expo-beacon 0.6.10 → 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,7 +1079,8 @@ public class ExpoBeaconModule: Module {
1078
1079
  eddystoneEnterCounters[identifier] = 0
1079
1080
  eddystoneExitCounters[identifier] = 0
1080
1081
  eddystoneLatestSeen.removeValue(forKey: identifier)
1081
- cancelEddystoneTimeout(identifier: identifier)
1082
+ smoothedDistances.removeValue(forKey: identifier)
1083
+ // Do NOT cancel the timeout here — same reason as iBeacon miss-based exit.
1082
1084
 
1083
1085
  let ns = paired["namespace"] as? String ?? ""
1084
1086
  let inst = paired["instance"] 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 }
@@ -1108,8 +1111,10 @@ public class ExpoBeaconModule: Module {
1108
1111
  let work = DispatchWorkItem { [weak self] in
1109
1112
  guard let self = self else { return }
1110
1113
  self.beaconTimeoutTimers.removeValue(forKey: identifier)
1111
- // Only fire if the beacon is still in range
1112
- guard self.enteredRegions.contains(identifier) else { return }
1114
+ // Fire unconditionally. A miss-based exit may have cleared enteredRegions before
1115
+ // the timer elapsed (ranging gaps can cause false exits), but the beacon may still
1116
+ // be physically present. Distance-based exits call cancelBeaconTimeout() so this
1117
+ // work item is cancelled before it runs on genuine out-of-range departures.
1113
1118
  self.sendLoggedEvent("onBeaconTimeout", self.makeBeaconEventParams(identifier: identifier, beacon: beacon))
1114
1119
  }
1115
1120
  beaconTimeoutTimers[identifier] = work
@@ -1121,8 +1126,8 @@ public class ExpoBeaconModule: Module {
1121
1126
  }
1122
1127
 
1123
1128
  private func scheduleEddystoneTimeout(identifier: String, namespace: String, instance: String) {
1124
- eddystoneTimeoutTimers[identifier]?.cancel()
1125
- 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 }
1126
1131
 
1127
1132
  let paired = loadPairedEddystonesRaw().first { ($0["identifier"] as? String) == identifier }
1128
1133
  guard let seconds = paired?["timeoutSeconds"] as? Int, seconds > 0 else { return }
@@ -1130,7 +1135,7 @@ public class ExpoBeaconModule: Module {
1130
1135
  let work = DispatchWorkItem { [weak self] in
1131
1136
  guard let self = self else { return }
1132
1137
  self.eddystoneTimeoutTimers.removeValue(forKey: identifier)
1133
- guard self.eddystoneEnteredRegions.contains(identifier) else { return }
1138
+ // Fire unconditionally same reason as iBeacon timeout.
1134
1139
  self.sendLoggedEvent("onEddystoneTimeout", [
1135
1140
  "identifier": identifier,
1136
1141
  "namespace": namespace,
@@ -1195,16 +1200,21 @@ public class ExpoBeaconModule: Module {
1195
1200
  // MARK: - Distance smoothing + enter/exit hysteresis
1196
1201
 
1197
1202
  /// Apply exponential moving average (EMA) smoothing to a raw distance reading.
1198
- /// 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.
1199
1207
  private func smoothDistance(identifier: String, rawDistance: Double) -> Double? {
1200
1208
  guard let prev = smoothedDistances[identifier] else {
1201
1209
  smoothedDistances[identifier] = rawDistance
1202
1210
  return rawDistance
1203
1211
  }
1204
- // 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.
1205
1214
  let ratio = prev > 0.001 ? rawDistance / prev : rawDistance
1206
1215
  if ratio > Self.DISTANCE_JUMP_FACTOR || (ratio > 0 && ratio < 1.0 / Self.DISTANCE_JUMP_FACTOR) {
1207
- return nil
1216
+ smoothedDistances[identifier] = rawDistance
1217
+ return rawDistance
1208
1218
  }
1209
1219
  let smoothed = Self.DISTANCE_EMA_ALPHA * rawDistance + (1 - Self.DISTANCE_EMA_ALPHA) * prev
1210
1220
  smoothedDistances[identifier] = smoothed
@@ -1354,6 +1364,7 @@ public class ExpoBeaconModule: Module {
1354
1364
  postBeaconNotification(identifier: identifier, eventType: "enter")
1355
1365
  scheduleBeaconTimeout(identifier: identifier, beacon: beacon)
1356
1366
  case .exit:
1367
+ smoothedDistances.removeValue(forKey: identifier)
1357
1368
  cancelBeaconTimeout(identifier: identifier)
1358
1369
  sendLoggedEvent("onBeaconExit", makeBeaconEventParams(identifier: identifier, beacon: beacon, event: "exit"))
1359
1370
  postBeaconNotification(identifier: identifier, eventType: "exit")
@@ -1377,7 +1388,12 @@ public class ExpoBeaconModule: Module {
1377
1388
  missCounters[identifier] = 0
1378
1389
  enterCounters[identifier] = 0
1379
1390
  exitCounters[identifier] = 0
1380
- cancelBeaconTimeout(identifier: identifier)
1391
+ smoothedDistances.removeValue(forKey: identifier)
1392
+ // Do NOT cancel the timeout here. A miss-based exit is triggered by
1393
+ // ranging gaps (e.g. accuracy == -1), not a confirmed physical departure.
1394
+ // Cancelling here prevents the timeout from firing when it is longer than
1395
+ // the miss window. Distance-based exits call cancelBeaconTimeout() so the
1396
+ // timer is still cancelled on genuine out-of-range events.
1381
1397
 
1382
1398
  // Look up region info for the exit event payload
1383
1399
  let region = monitoredRegions.first { $0.identifier == identifier }
@@ -1423,6 +1439,7 @@ public class ExpoBeaconModule: Module {
1423
1439
  enterCounters.removeValue(forKey: identifier)
1424
1440
  exitCounters.removeValue(forKey: identifier)
1425
1441
  missCounters.removeValue(forKey: identifier)
1442
+ smoothedDistances.removeValue(forKey: identifier)
1426
1443
  if wasEntered {
1427
1444
  cancelBeaconTimeout(identifier: identifier)
1428
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.10",
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",