@wayq/beekon-rn 0.0.3 → 0.0.5

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 (77) hide show
  1. package/BeekonRn.podspec +1 -1
  2. package/README.md +91 -39
  3. package/android/build.gradle +9 -4
  4. package/android/src/main/java/in/wayq/beekonrn/BeekonRnModule.kt +306 -60
  5. package/ios/BeekonRn.mm +90 -24
  6. package/ios/BeekonRn.swift +360 -60
  7. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/BeekonKit +0 -0
  8. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Info.plist +0 -0
  9. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.abi.json +7521 -1312
  10. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  11. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios.swiftinterface +191 -40
  12. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/BeekonKit +0 -0
  13. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Info.plist +0 -0
  14. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.abi.json +7521 -1312
  15. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  16. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +191 -40
  17. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +7521 -1312
  18. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  19. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/Modules/BeekonKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +191 -40
  20. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64_x86_64-simulator/BeekonKit.framework/_CodeSignature/CodeResources +2 -80
  21. package/lib/module/NativeBeekonRn.js +22 -7
  22. package/lib/module/NativeBeekonRn.js.map +1 -1
  23. package/lib/module/beekon.js +198 -46
  24. package/lib/module/beekon.js.map +1 -1
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/internal/mappers.js +122 -28
  27. package/lib/module/internal/mappers.js.map +1 -1
  28. package/lib/module/types/config.js +2 -0
  29. package/lib/module/types/enums.js +2 -0
  30. package/lib/module/types/enums.js.map +1 -0
  31. package/lib/module/types/error.js +10 -4
  32. package/lib/module/types/error.js.map +1 -1
  33. package/lib/module/types/geofence.js +2 -0
  34. package/lib/module/types/geofence.js.map +1 -0
  35. package/lib/module/types/location.js +2 -0
  36. package/lib/module/types/sync.js +2 -0
  37. package/lib/module/types/sync.js.map +1 -0
  38. package/lib/typescript/src/NativeBeekonRn.d.ts +102 -20
  39. package/lib/typescript/src/NativeBeekonRn.d.ts.map +1 -1
  40. package/lib/typescript/src/beekon.d.ts +81 -33
  41. package/lib/typescript/src/beekon.d.ts.map +1 -1
  42. package/lib/typescript/src/index.d.ts +5 -2
  43. package/lib/typescript/src/index.d.ts.map +1 -1
  44. package/lib/typescript/src/internal/mappers.d.ts +12 -6
  45. package/lib/typescript/src/internal/mappers.d.ts.map +1 -1
  46. package/lib/typescript/src/types/config.d.ts +50 -20
  47. package/lib/typescript/src/types/config.d.ts.map +1 -1
  48. package/lib/typescript/src/types/enums.d.ts +48 -0
  49. package/lib/typescript/src/types/enums.d.ts.map +1 -0
  50. package/lib/typescript/src/types/error.d.ts +11 -5
  51. package/lib/typescript/src/types/error.d.ts.map +1 -1
  52. package/lib/typescript/src/types/geofence.d.ts +36 -0
  53. package/lib/typescript/src/types/geofence.d.ts.map +1 -0
  54. package/lib/typescript/src/types/location.d.ts +22 -8
  55. package/lib/typescript/src/types/location.d.ts.map +1 -1
  56. package/lib/typescript/src/types/state.d.ts +13 -4
  57. package/lib/typescript/src/types/state.d.ts.map +1 -1
  58. package/lib/typescript/src/types/sync.d.ts +27 -0
  59. package/lib/typescript/src/types/sync.d.ts.map +1 -0
  60. package/package.json +4 -5
  61. package/scripts/fetch-beekonkit.sh +5 -5
  62. package/src/NativeBeekonRn.ts +110 -20
  63. package/src/beekon.ts +219 -49
  64. package/src/index.tsx +21 -2
  65. package/src/internal/mappers.ts +187 -30
  66. package/src/types/config.ts +52 -20
  67. package/src/types/enums.ts +64 -0
  68. package/src/types/error.ts +11 -8
  69. package/src/types/geofence.ts +37 -0
  70. package/src/types/location.ts +28 -8
  71. package/src/types/state.ts +13 -3
  72. package/src/types/sync.ts +23 -0
  73. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeDirectory +0 -0
  74. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeRequirements +0 -0
  75. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeResources +0 -296
  76. package/ios/Frameworks/BeekonKit.xcframework/_CodeSignature/CodeSignature +0 -0
  77. package/ios/Frameworks/BeekonKit.xcframework/ios-arm64/BeekonKit.framework/_CodeSignature/CodeResources +0 -146
package/src/beekon.ts CHANGED
@@ -1,111 +1,281 @@
1
1
  import NativeBeekon from './NativeBeekonRn';
2
2
  import type { BeekonConfig } from './types/config';
3
- import type { BeekonState } from './types/state';
3
+ import type { BeekonGeofence, GeofenceEvent } from './types/geofence';
4
4
  import type { Location } from './types/location';
5
+ import type { BeekonState } from './types/state';
6
+ import type { SyncStatus } from './types/sync';
5
7
  import {
6
8
  configToWire,
9
+ geofenceToWire,
10
+ recordToEntries,
11
+ rethrowAsBeekonError,
12
+ wireToGeofence,
13
+ wireToGeofenceEvent,
7
14
  wireToLocation,
8
15
  wireToState,
9
- rethrowAsBeekonError,
16
+ wireToSyncStatus,
10
17
  } from './internal/mappers';
11
18
 
19
+ type Listener<T> = (value: T) => void;
20
+
21
+ /**
22
+ * Fans the four native EventEmitters out to multiple JS subscribers and adds
23
+ * replay-1 semantics for `state` / `syncStatus` (RN EventEmitters don't replay,
24
+ * so we cache the latest value and hand it to new subscribers immediately —
25
+ * matching the native StateFlow / AsyncStream contract). `location` /
26
+ * `geofenceEvent` are plain fan-out with no replay.
27
+ *
28
+ * Native subscriptions are opened once, on first use, and kept for the app
29
+ * lifetime — the native module collects its flows continuously regardless, so
30
+ * this only keeps the JS cache warm.
31
+ */
32
+ class EventHub {
33
+ private subscribed = false;
34
+ private readonly stateListeners = new Set<Listener<BeekonState>>();
35
+ private readonly locationListeners = new Set<Listener<Location>>();
36
+ private readonly geofenceListeners = new Set<Listener<GeofenceEvent>>();
37
+ private readonly syncListeners = new Set<Listener<SyncStatus>>();
38
+ private lastState: BeekonState | undefined;
39
+ private lastSyncStatus: SyncStatus | undefined;
40
+
41
+ /** Idempotent: opens the native subscriptions the first time it is called. */
42
+ ensureSubscribed(): void {
43
+ if (this.subscribed) return;
44
+ this.subscribed = true;
45
+ NativeBeekon.onState((w) => {
46
+ const s = wireToState(w);
47
+ this.lastState = s;
48
+ this.stateListeners.forEach((cb) => cb(s));
49
+ });
50
+ NativeBeekon.onLocation((w) => {
51
+ const l = wireToLocation(w);
52
+ this.locationListeners.forEach((cb) => cb(l));
53
+ });
54
+ NativeBeekon.onGeofenceEvent((w) => {
55
+ const g = wireToGeofenceEvent(w);
56
+ this.geofenceListeners.forEach((cb) => cb(g));
57
+ });
58
+ NativeBeekon.onSyncStatus((w) => {
59
+ const s = wireToSyncStatus(w);
60
+ this.lastSyncStatus = s;
61
+ this.syncListeners.forEach((cb) => cb(s));
62
+ });
63
+ }
64
+
65
+ onState(cb: Listener<BeekonState>): () => void {
66
+ this.ensureSubscribed();
67
+ this.stateListeners.add(cb);
68
+ if (this.lastState !== undefined) cb(this.lastState);
69
+ return () => {
70
+ this.stateListeners.delete(cb);
71
+ };
72
+ }
73
+
74
+ onLocation(cb: Listener<Location>): () => void {
75
+ this.ensureSubscribed();
76
+ this.locationListeners.add(cb);
77
+ return () => {
78
+ this.locationListeners.delete(cb);
79
+ };
80
+ }
81
+
82
+ onGeofenceEvent(cb: Listener<GeofenceEvent>): () => void {
83
+ this.ensureSubscribed();
84
+ this.geofenceListeners.add(cb);
85
+ return () => {
86
+ this.geofenceListeners.delete(cb);
87
+ };
88
+ }
89
+
90
+ onSyncStatus(cb: Listener<SyncStatus>): () => void {
91
+ this.ensureSubscribed();
92
+ this.syncListeners.add(cb);
93
+ if (this.lastSyncStatus !== undefined) cb(this.lastSyncStatus);
94
+ return () => {
95
+ this.syncListeners.delete(cb);
96
+ };
97
+ }
98
+ }
99
+
12
100
  /**
13
- * Public facade for the Beekon SDK. Mirrors the native APIs:
101
+ * Public facade for the Beekon SDK a 1:1 mirror of the native APIs:
14
102
  *
15
103
  * - Android: `in.wayq.beekon.Beekon` (object)
16
104
  * - iOS: `BeekonKit.Beekon.shared` (actor)
17
105
  *
18
- * Lifecycle: `configure(config)` `start()` ... `stop()` (idempotent).
19
- * Subscribe to `onState` / `onLocation` for live updates; query
20
- * `history(from, to)` for persisted fixes.
106
+ * There is **no `initialize()`** the native SDKs auto-initialize. Lifecycle:
107
+ * `configure(config)` (optional; defaults 30s / 100m) → `start()`
108
+ * `stop()`. `start()` / `stop()` **never throw** — `onState` is the single
109
+ * source of truth for whether tracking is active and why it stopped.
21
110
  *
22
- * **Threading:** Methods are safe to call from any JS context. Subscribers'
111
+ * **Threading:** methods are safe to call from any JS context; subscriber
23
112
  * callbacks fire on the JS thread.
24
113
  *
25
- * **Persistence invariant:** the SDK persists every gated fix natively; JS is
26
- * a passive observer. In background, the JS engine is not guaranteed to be
27
- * alive — for past fixes, use `history()`.
114
+ * **Persistence invariant:** the SDK persists every gated fix natively; JS is a
115
+ * passive observer that is not guaranteed to be alive in the background. For
116
+ * past fixes use `getLocations()`.
28
117
  */
29
118
  class BeekonImpl {
119
+ private readonly hub = new EventHub();
120
+
30
121
  /**
31
122
  * Set tracking parameters. Optional — `start()` falls back to the previously
32
- * persisted config or the SDK default (30s / 100m) if never configured.
33
- *
34
- * While tracking, the new gate values take effect on the next admitted fix
35
- * without restarting the underlying location subscription.
123
+ * persisted config, or the SDK defaults if never configured. While tracking,
124
+ * gate thresholds take effect on the next admitted fix without restarting the
125
+ * location subscription; `accuracyMode` applies on the next `start()`.
36
126
  */
37
127
  async configure(config: BeekonConfig): Promise<void> {
128
+ this.hub.ensureSubscribed();
129
+ await NativeBeekon.configure(configToWire(config));
130
+ }
131
+
132
+ /**
133
+ * Begin tracking. Never throws — observe `onState` for the outcome
134
+ * (`stopped(reason)` on a permission / services / system problem). Beekon
135
+ * ships no permission API: request permissions in the host app, then observe
136
+ * `onState`.
137
+ */
138
+ async start(): Promise<void> {
139
+ this.hub.ensureSubscribed();
140
+ await NativeBeekon.start();
141
+ }
142
+
143
+ /** Stop tracking. Idempotent. Registered geofences keep firing. */
144
+ async stop(): Promise<void> {
145
+ this.hub.ensureSubscribed();
146
+ await NativeBeekon.stop();
147
+ }
148
+
149
+ /**
150
+ * Resume tracking if a session was active before the app was terminated. A
151
+ * no-op if the user explicitly stopped. Call early in app startup.
152
+ *
153
+ * Note: on a background relaunch the JS engine may not be running, so true
154
+ * cold-launch resume also requires native host wiring (Android: call
155
+ * `Beekon.start()` from `Application.onCreate`; iOS: `registerBackgroundTasks`
156
+ * in the AppDelegate). See the README.
157
+ */
158
+ async resumeIfNeeded(): Promise<void> {
159
+ this.hub.ensureSubscribed();
160
+ await NativeBeekon.resumeIfNeeded();
161
+ }
162
+
163
+ /**
164
+ * Read persisted fixes in the inclusive range `[from, to]`, oldest first.
165
+ * Reads come from the SDK's local storage (Room on Android, GRDB on iOS) — the
166
+ * source of truth even when JS was asleep. With sync enabled, rows are deleted
167
+ * locally once the server accepts them, so this returns only un-uploaded fixes.
168
+ *
169
+ * Throws `BeekonError` with kind `'storage'` on a database read failure.
170
+ */
171
+ async getLocations(from: Date, to: Date): Promise<Location[]> {
38
172
  try {
39
- await NativeBeekon.configure(configToWire(config));
173
+ const wires = await NativeBeekon.getLocations(
174
+ from.getTime(),
175
+ to.getTime()
176
+ );
177
+ return wires.map(wireToLocation);
40
178
  } catch (e) {
41
179
  rethrowAsBeekonError(e);
42
180
  }
43
181
  }
44
182
 
45
183
  /**
46
- * Begin tracking. State transitions to `tracking`.
47
- *
48
- * Throws a `BeekonError` with kind:
49
- * - `'permissionDenied'` — location permission not granted.
50
- * - `'locationServicesDisabled'` — system location services off (or GMS
51
- * unavailable on Android).
184
+ * Delete stored locations captured at or before `before` (all of them when
185
+ * `before` is omitted). Returns the number of rows removed. Throws
186
+ * `BeekonError` with kind `'storage'` on a failure.
52
187
  */
53
- async start(): Promise<void> {
188
+ async deleteLocations(before?: Date): Promise<number> {
54
189
  try {
55
- await NativeBeekon.start();
190
+ // Negative is the wire sentinel for "delete all" (see NativeBeekonRn.ts).
191
+ return await NativeBeekon.deleteLocations(before ? before.getTime() : -1);
56
192
  } catch (e) {
57
193
  rethrowAsBeekonError(e);
58
194
  }
59
195
  }
60
196
 
61
- /** Stop tracking. Idempotent. State transitions to `stopped(user)`. */
62
- async stop(): Promise<void> {
197
+ /**
198
+ * The number of stored locations not yet uploaded. Throws `BeekonError` with
199
+ * kind `'storage'` on a failure.
200
+ */
201
+ async pendingUploadCount(): Promise<number> {
63
202
  try {
64
- await NativeBeekon.stop();
203
+ return await NativeBeekon.pendingUploadCount();
65
204
  } catch (e) {
66
205
  rethrowAsBeekonError(e);
67
206
  }
68
207
  }
69
208
 
70
209
  /**
71
- * Read persisted fixes in the time range [from, to]. Returns fixes in
72
- * chronological order. Reads come from the SDK's local storage (Room on
73
- * Android, GRDB on iOS) — the source of truth even when JS was asleep.
74
- *
75
- * Retention: fixes older than 7 days OR beyond the most recent 100K are
76
- * pruned automatically.
77
- *
78
- * Throws `BeekonError` with kind `'storageFailure'` on database read failure.
210
+ * Request an immediate upload. Best-effort and self-retrying; a no-op when
211
+ * sync is not configured.
79
212
  */
80
- async history(from: Date, to: Date): Promise<Location[]> {
213
+ async sync(): Promise<void> {
214
+ await NativeBeekon.sync();
215
+ }
216
+
217
+ /** Set custom key/value fields (e.g. `user_id`) included with every upload. */
218
+ async setExtras(extras: Record<string, string>): Promise<void> {
219
+ await NativeBeekon.setExtras(recordToEntries(extras));
220
+ }
221
+
222
+ /**
223
+ * Register geofences. Re-adding an `id` replaces the existing one. Throws
224
+ * `BeekonError` with kind `'invalidGeofence'` if any entry fails validation —
225
+ * none are added when one is invalid.
226
+ */
227
+ async addGeofences(geofences: BeekonGeofence[]): Promise<void> {
81
228
  try {
82
- const wires = await NativeBeekon.history(from.getTime(), to.getTime());
83
- return wires.map(wireToLocation);
229
+ await NativeBeekon.addGeofences(geofences.map(geofenceToWire));
84
230
  } catch (e) {
85
231
  rethrowAsBeekonError(e);
86
232
  }
87
233
  }
88
234
 
235
+ /** Unregister geofences by id. Unknown ids are ignored. */
236
+ async removeGeofences(ids: string[]): Promise<void> {
237
+ await NativeBeekon.removeGeofences(ids);
238
+ }
239
+
240
+ /** The currently registered geofences. */
241
+ async listGeofences(): Promise<BeekonGeofence[]> {
242
+ const wires = await NativeBeekon.listGeofences();
243
+ return wires.map(wireToGeofence);
244
+ }
245
+
89
246
  /**
90
- * Subscribe to state transitions. Returns an unsubscribe function. The
91
- * current state is delivered to new subscribers immediately (replay-1
92
- * semantics, matching the native `state` flow / AsyncStream).
247
+ * Subscribe to tracking-state transitions (`idle` / `tracking` /
248
+ * `stopped(reason)`). The current state is delivered to new subscribers
249
+ * immediately (replay-1). Returns an unsubscribe function.
93
250
  */
94
251
  onState(cb: (s: BeekonState) => void): () => void {
95
- const subscription = NativeBeekon.onState((wire) => cb(wireToState(wire)));
96
- return () => subscription.remove();
252
+ return this.hub.onState(cb);
97
253
  }
98
254
 
99
255
  /**
100
- * Subscribe to gated locations as they arrive. Returns an unsubscribe
101
- * function. Broadcast (no replay) only delivers while the JS engine is
102
- * alive. For fixes emitted while JS was asleep, use `history()`.
256
+ * Subscribe to gated locations as they arrive. Broadcast (no replay) — only
257
+ * delivers while the JS engine is alive. For fixes emitted while JS was
258
+ * asleep, use `getLocations()`. Returns an unsubscribe function.
103
259
  */
104
260
  onLocation(cb: (l: Location) => void): () => void {
105
- const subscription = NativeBeekon.onLocation((wire) =>
106
- cb(wireToLocation(wire))
107
- );
108
- return () => subscription.remove();
261
+ return this.hub.onLocation(cb);
262
+ }
263
+
264
+ /**
265
+ * Subscribe to geofence enter / exit crossings. Broadcast (no replay).
266
+ * Returns an unsubscribe function.
267
+ */
268
+ onGeofenceEvent(cb: (e: GeofenceEvent) => void): () => void {
269
+ return this.hub.onGeofenceEvent(cb);
270
+ }
271
+
272
+ /**
273
+ * Subscribe to upload health (`idle` / `pending` / `failed(reason)`). The
274
+ * current status is delivered to new subscribers immediately (replay-1).
275
+ * Returns an unsubscribe function.
276
+ */
277
+ onSyncStatus(cb: (s: SyncStatus) => void): () => void {
278
+ return this.hub.onSyncStatus(cb);
109
279
  }
110
280
  }
111
281
 
package/src/index.tsx CHANGED
@@ -1,5 +1,24 @@
1
1
  export { Beekon } from './beekon';
2
- export type { BeekonConfig, AndroidNotificationConfig } from './types/config';
3
- export type { BeekonState, StopReason } from './types/state';
2
+
3
+ export type {
4
+ BeekonConfig,
5
+ SyncConfig,
6
+ NotificationConfig,
7
+ } from './types/config';
8
+ export type {
9
+ AccuracyMode,
10
+ StationaryMode,
11
+ LocationTrigger,
12
+ LocationQuality,
13
+ MotionState,
14
+ ActivityType,
15
+ } from './types/enums';
4
16
  export type { Location } from './types/location';
17
+ export type {
18
+ BeekonGeofence,
19
+ GeofenceEvent,
20
+ Transition,
21
+ } from './types/geofence';
22
+ export type { BeekonState, StopReason } from './types/state';
23
+ export type { SyncStatus, SyncFailure } from './types/sync';
5
24
  export { BeekonError, type BeekonErrorKind } from './types/error';
@@ -1,43 +1,156 @@
1
1
  import type { BeekonConfig } from '../types/config';
2
- import type { BeekonState, StopReason } from '../types/state';
2
+ import type {
3
+ AccuracyMode,
4
+ ActivityType,
5
+ LocationQuality,
6
+ LocationTrigger,
7
+ MotionState,
8
+ StationaryMode,
9
+ } from '../types/enums';
10
+ import type {
11
+ BeekonGeofence,
12
+ GeofenceEvent,
13
+ Transition,
14
+ } from '../types/geofence';
3
15
  import type { Location } from '../types/location';
16
+ import type { BeekonState, StopReason } from '../types/state';
17
+ import type { SyncFailure, SyncStatus } from '../types/sync';
4
18
  import { BeekonError, type BeekonErrorKind } from '../types/error';
5
- import type { WireConfig, WireLocation, WireState } from '../NativeBeekonRn';
19
+ import type {
20
+ WireConfig,
21
+ WireGeofence,
22
+ WireGeofenceEvent,
23
+ WireKeyValue,
24
+ WireLocation,
25
+ WireState,
26
+ WireSyncStatus,
27
+ } from '../NativeBeekonRn';
28
+
29
+ // Config defaults. Kept here (not in the wire layer) so the native modules can
30
+ // treat every wire field as present.
31
+ const DEFAULTS = {
32
+ minTimeBetweenLocationsSeconds: 30,
33
+ minDistanceBetweenLocationsMeters: 100,
34
+ accuracyMode: 'balanced' as AccuracyMode,
35
+ whenStationary: 'pause' as StationaryMode,
36
+ stationaryRadiusMeters: 5,
37
+ detectActivity: false,
38
+ syncIntervalSeconds: 300,
39
+ syncBatchSize: 100,
40
+ };
6
41
 
7
- const DEFAULT_INTERVAL_SECONDS = 30;
8
- const DEFAULT_DISTANCE_METERS = 100;
42
+ // --- Public → wire ---------------------------------------------------------
43
+
44
+ export function recordToEntries(
45
+ record: Record<string, string> | undefined
46
+ ): WireKeyValue[] {
47
+ if (!record) return [];
48
+ return Object.keys(record).map((key) => ({ key, value: record[key]! }));
49
+ }
9
50
 
10
51
  export function configToWire(config: BeekonConfig): WireConfig {
52
+ const sync = config.sync;
11
53
  return {
12
- intervalSeconds: config.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS,
13
- distanceMeters: config.distanceMeters ?? DEFAULT_DISTANCE_METERS,
14
- androidNotification: config.androidNotification,
54
+ minTimeBetweenLocationsSeconds:
55
+ config.minTimeBetweenLocationsSeconds ??
56
+ DEFAULTS.minTimeBetweenLocationsSeconds,
57
+ minDistanceBetweenLocationsMeters:
58
+ config.minDistanceBetweenLocationsMeters ??
59
+ DEFAULTS.minDistanceBetweenLocationsMeters,
60
+ accuracyMode: config.accuracyMode ?? DEFAULTS.accuracyMode,
61
+ whenStationary: config.whenStationary ?? DEFAULTS.whenStationary,
62
+ stationaryRadiusMeters:
63
+ config.stationaryRadiusMeters ?? DEFAULTS.stationaryRadiusMeters,
64
+ detectActivity: config.detectActivity ?? DEFAULTS.detectActivity,
65
+ sync: sync
66
+ ? {
67
+ url: sync.url,
68
+ headers: recordToEntries(sync.headers),
69
+ intervalSeconds: sync.intervalSeconds ?? DEFAULTS.syncIntervalSeconds,
70
+ batchSize: sync.batchSize ?? DEFAULTS.syncBatchSize,
71
+ }
72
+ : undefined,
73
+ notification: config.notification
74
+ ? { title: config.notification.title, text: config.notification.text }
75
+ : undefined,
15
76
  };
16
77
  }
17
78
 
79
+ export function geofenceToWire(g: BeekonGeofence): WireGeofence {
80
+ return {
81
+ id: g.id,
82
+ lat: g.latitude,
83
+ lng: g.longitude,
84
+ radiusMeters: g.radiusMeters,
85
+ notifyOnEntry: g.notifyOnEntry ?? true,
86
+ notifyOnExit: g.notifyOnExit ?? true,
87
+ };
88
+ }
89
+
90
+ // --- Wire → public ---------------------------------------------------------
91
+
18
92
  export function wireToLocation(w: WireLocation): Location {
19
93
  return {
20
- lat: w.lat,
21
- lng: w.lng,
94
+ id: w.id,
95
+ latitude: w.lat,
96
+ longitude: w.lng,
97
+ timestamp: new Date(w.timestampMs),
22
98
  accuracy: w.accuracy,
23
99
  speed: w.speed,
24
100
  bearing: w.bearing,
25
101
  altitude: w.altitude,
26
- timestamp: new Date(w.timestampMs),
102
+ quality: oneOf<LocationQuality>(
103
+ w.quality,
104
+ ['ok', 'lowAccuracy', 'implausibleSpeed'],
105
+ 'ok'
106
+ ),
107
+ trigger: oneOf<LocationTrigger>(
108
+ w.trigger,
109
+ ['interval', 'motion', 'checkIn', 'geofence', 'manual'],
110
+ 'interval'
111
+ ),
112
+ motion: oneOf<MotionState>(
113
+ w.motion,
114
+ ['moving', 'stationary', 'unknown'],
115
+ 'unknown'
116
+ ),
117
+ activity:
118
+ w.activity == null
119
+ ? null
120
+ : oneOf<ActivityType>(
121
+ w.activity,
122
+ [
123
+ 'stationary',
124
+ 'walking',
125
+ 'running',
126
+ 'cycling',
127
+ 'automotive',
128
+ 'unknown',
129
+ ],
130
+ 'unknown'
131
+ ),
132
+ isMock: w.isMock,
27
133
  };
28
134
  }
29
135
 
30
- function toStopReason(s: string | undefined): StopReason {
31
- switch (s) {
32
- case 'user':
33
- case 'permissionDenied':
34
- case 'locationServicesDisabled':
35
- case 'system':
36
- return s;
37
- default:
38
- // Defensive — native should never emit an unknown reason.
39
- return 'user';
40
- }
136
+ export function wireToGeofence(w: WireGeofence): BeekonGeofence {
137
+ return {
138
+ id: w.id,
139
+ latitude: w.lat,
140
+ longitude: w.lng,
141
+ radiusMeters: w.radiusMeters,
142
+ notifyOnEntry: w.notifyOnEntry,
143
+ notifyOnExit: w.notifyOnExit,
144
+ };
145
+ }
146
+
147
+ export function wireToGeofenceEvent(w: WireGeofenceEvent): GeofenceEvent {
148
+ return {
149
+ id: w.id,
150
+ geofenceId: w.geofenceId,
151
+ type: oneOf<Transition>(w.type, ['enter', 'exit'], 'enter'),
152
+ timestamp: new Date(w.timestampMs),
153
+ };
41
154
  }
42
155
 
43
156
  export function wireToState(w: WireState): BeekonState {
@@ -54,11 +167,57 @@ export function wireToState(w: WireState): BeekonState {
54
167
  }
55
168
  }
56
169
 
170
+ export function wireToSyncStatus(w: WireSyncStatus): SyncStatus {
171
+ switch (w.type) {
172
+ case 'idle':
173
+ return { kind: 'idle' };
174
+ case 'pending':
175
+ return { kind: 'pending' };
176
+ case 'failed':
177
+ return { kind: 'failed', reason: toSyncFailure(w.failure) };
178
+ default:
179
+ return { kind: 'idle' };
180
+ }
181
+ }
182
+
183
+ function toStopReason(s: string | undefined): StopReason {
184
+ return oneOf<StopReason>(
185
+ s,
186
+ [
187
+ 'user',
188
+ 'permissionDenied',
189
+ 'locationServicesDisabled',
190
+ 'locationUnavailable',
191
+ 'system',
192
+ ],
193
+ // The native side always populates the reason for a `stopped` state; an
194
+ // unknown/missing value means the wire contract was violated — fall back to
195
+ // `system` rather than throwing on a subscriber.
196
+ 'system'
197
+ );
198
+ }
199
+
200
+ function toSyncFailure(s: string | undefined): SyncFailure {
201
+ return oneOf<SyncFailure>(s, ['auth', 'rejected'], 'rejected');
202
+ }
203
+
204
+ /** Validate a wire enum string against the known set, with a safe fallback. */
205
+ function oneOf<T extends string>(
206
+ value: string | null | undefined,
207
+ allowed: readonly T[],
208
+ fallback: T
209
+ ): T {
210
+ return (allowed as readonly string[]).includes(value ?? '')
211
+ ? (value as T)
212
+ : fallback;
213
+ }
214
+
215
+ // --- Errors ----------------------------------------------------------------
216
+
57
217
  /**
58
- * Re-throw a native promise rejection as a typed `BeekonError`. Native modules
59
- * encode the kind in the error code string (`PERMISSION_DENIED`,
60
- * `LOCATION_SERVICES_DISABLED`, `STORAGE_FAILURE`); anything else falls
61
- * through as the original error.
218
+ * Re-throw a native promise rejection as a typed {@link BeekonError}. The native
219
+ * modules encode the kind in the error-code string (`STORAGE_FAILURE`,
220
+ * `INVALID_GEOFENCE`); anything else falls through as the original error.
62
221
  */
63
222
  export function rethrowAsBeekonError(e: unknown): never {
64
223
  const code = (e as { code?: string } | undefined)?.code;
@@ -72,12 +231,10 @@ export function rethrowAsBeekonError(e: unknown): never {
72
231
 
73
232
  function codeToKind(code: string | undefined): BeekonErrorKind | undefined {
74
233
  switch (code) {
75
- case 'PERMISSION_DENIED':
76
- return 'permissionDenied';
77
- case 'LOCATION_SERVICES_DISABLED':
78
- return 'locationServicesDisabled';
79
234
  case 'STORAGE_FAILURE':
80
- return 'storageFailure';
235
+ return 'storage';
236
+ case 'INVALID_GEOFENCE':
237
+ return 'invalidGeofence';
81
238
  default:
82
239
  return undefined;
83
240
  }