expo-background-tracking 1.0.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.
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/build/BackgroundGeolocation.d.ts +122 -0
- package/build/BackgroundGeolocation.js +901 -0
- package/build/Database.d.ts +30 -0
- package/build/Database.js +206 -0
- package/build/EventBus.d.ts +10 -0
- package/build/EventBus.js +37 -0
- package/build/GeofenceManager.d.ts +42 -0
- package/build/GeofenceManager.js +281 -0
- package/build/HttpService.d.ts +27 -0
- package/build/HttpService.js +265 -0
- package/build/Logger.d.ts +15 -0
- package/build/Logger.js +52 -0
- package/build/MotionDetector.d.ts +40 -0
- package/build/MotionDetector.js +141 -0
- package/build/Scheduler.d.ts +19 -0
- package/build/Scheduler.js +79 -0
- package/build/environment.d.ts +8 -0
- package/build/environment.js +26 -0
- package/build/geo.d.ts +10 -0
- package/build/geo.js +46 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +28 -0
- package/build/types.d.ts +275 -0
- package/build/types.js +56 -0
- package/package.json +351 -0
- package/src/BackgroundGeolocation.ts +1032 -0
- package/src/Database.ts +236 -0
- package/src/EventBus.ts +33 -0
- package/src/GeofenceManager.ts +269 -0
- package/src/HttpService.ts +232 -0
- package/src/Logger.ts +52 -0
- package/src/MotionDetector.ts +149 -0
- package/src/Scheduler.ts +90 -0
- package/src/environment.ts +22 -0
- package/src/geo.ts +57 -0
- package/src/index.ts +7 -0
- package/src/types.ts +410 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Geofence, Location, LogEntry, SQLQuery } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* SQLite persistence layer (locations queue, geofences, key/value state, logs).
|
|
4
|
+
* Mirrors the embedded database of transistorsoft's SDK.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Database {
|
|
7
|
+
private db;
|
|
8
|
+
private opening;
|
|
9
|
+
private open;
|
|
10
|
+
insertLocation(location: Location): Promise<void>;
|
|
11
|
+
getLocations(query?: SQLQuery): Promise<Location[]>;
|
|
12
|
+
getCount(): Promise<number>;
|
|
13
|
+
destroyLocations(): Promise<void>;
|
|
14
|
+
destroyLocation(uuid: string): Promise<void>;
|
|
15
|
+
deleteLocations(uuids: string[]): Promise<void>;
|
|
16
|
+
/** Purge by retention policy. */
|
|
17
|
+
prune(maxDays: number, maxRecords: number): Promise<void>;
|
|
18
|
+
upsertGeofence(geofence: Geofence): Promise<void>;
|
|
19
|
+
getGeofences(): Promise<Geofence[]>;
|
|
20
|
+
getGeofence(identifier: string): Promise<Geofence | null>;
|
|
21
|
+
removeGeofence(identifier: string): Promise<void>;
|
|
22
|
+
removeGeofences(identifiers?: string[]): Promise<void>;
|
|
23
|
+
setKV(key: string, value: unknown): Promise<void>;
|
|
24
|
+
getKV<T>(key: string): Promise<T | null>;
|
|
25
|
+
insertLog(level: string, message: string): Promise<void>;
|
|
26
|
+
getLogs(limit?: number): Promise<LogEntry[]>;
|
|
27
|
+
destroyLogs(): Promise<void>;
|
|
28
|
+
pruneLogs(maxDays: number): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export declare const database: Database;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.database = exports.Database = void 0;
|
|
37
|
+
const SQLite = __importStar(require("expo-sqlite"));
|
|
38
|
+
const DB_NAME = 'expo_background_tracking.db';
|
|
39
|
+
/**
|
|
40
|
+
* SQLite persistence layer (locations queue, geofences, key/value state, logs).
|
|
41
|
+
* Mirrors the embedded database of transistorsoft's SDK.
|
|
42
|
+
*/
|
|
43
|
+
class Database {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.db = null;
|
|
46
|
+
this.opening = null;
|
|
47
|
+
}
|
|
48
|
+
async open() {
|
|
49
|
+
if (this.db)
|
|
50
|
+
return this.db;
|
|
51
|
+
if (!this.opening) {
|
|
52
|
+
this.opening = (async () => {
|
|
53
|
+
const db = await SQLite.openDatabaseAsync(DB_NAME);
|
|
54
|
+
await db.execAsync(`
|
|
55
|
+
PRAGMA journal_mode = WAL;
|
|
56
|
+
CREATE TABLE IF NOT EXISTS locations (
|
|
57
|
+
uuid TEXT PRIMARY KEY,
|
|
58
|
+
timestamp INTEGER NOT NULL,
|
|
59
|
+
data TEXT NOT NULL,
|
|
60
|
+
synced INTEGER DEFAULT 0
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_locations_ts ON locations (timestamp);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS geofences (
|
|
64
|
+
identifier TEXT PRIMARY KEY,
|
|
65
|
+
data TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
68
|
+
key TEXT PRIMARY KEY,
|
|
69
|
+
value TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS logs (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
timestamp INTEGER NOT NULL,
|
|
74
|
+
level TEXT NOT NULL,
|
|
75
|
+
message TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
`);
|
|
78
|
+
this.db = db;
|
|
79
|
+
return db;
|
|
80
|
+
})();
|
|
81
|
+
}
|
|
82
|
+
return this.opening;
|
|
83
|
+
}
|
|
84
|
+
// --- locations ---------------------------------------------------------
|
|
85
|
+
async insertLocation(location) {
|
|
86
|
+
const db = await this.open();
|
|
87
|
+
await db.runAsync('INSERT OR REPLACE INTO locations (uuid, timestamp, data, synced) VALUES (?, ?, ?, 0)', location.uuid, Date.parse(location.timestamp), JSON.stringify(location));
|
|
88
|
+
}
|
|
89
|
+
async getLocations(query) {
|
|
90
|
+
const db = await this.open();
|
|
91
|
+
const where = [];
|
|
92
|
+
const args = [];
|
|
93
|
+
if (query?.start != null) {
|
|
94
|
+
where.push('timestamp >= ?');
|
|
95
|
+
args.push(query.start);
|
|
96
|
+
}
|
|
97
|
+
if (query?.end != null) {
|
|
98
|
+
where.push('timestamp <= ?');
|
|
99
|
+
args.push(query.end);
|
|
100
|
+
}
|
|
101
|
+
const order = query?.order === -1 ? 'DESC' : 'ASC';
|
|
102
|
+
const limit = query?.limit ? ` LIMIT ${Math.floor(query.limit)}` : '';
|
|
103
|
+
const sql = `SELECT data FROM locations ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY timestamp ${order}${limit}`;
|
|
104
|
+
const rows = await db.getAllAsync(sql, ...args);
|
|
105
|
+
return rows.map((r) => JSON.parse(r.data));
|
|
106
|
+
}
|
|
107
|
+
async getCount() {
|
|
108
|
+
const db = await this.open();
|
|
109
|
+
const row = await db.getFirstAsync('SELECT COUNT(*) as c FROM locations');
|
|
110
|
+
return row?.c ?? 0;
|
|
111
|
+
}
|
|
112
|
+
async destroyLocations() {
|
|
113
|
+
const db = await this.open();
|
|
114
|
+
await db.runAsync('DELETE FROM locations');
|
|
115
|
+
}
|
|
116
|
+
async destroyLocation(uuid) {
|
|
117
|
+
const db = await this.open();
|
|
118
|
+
await db.runAsync('DELETE FROM locations WHERE uuid = ?', uuid);
|
|
119
|
+
}
|
|
120
|
+
async deleteLocations(uuids) {
|
|
121
|
+
if (!uuids.length)
|
|
122
|
+
return;
|
|
123
|
+
const db = await this.open();
|
|
124
|
+
const placeholders = uuids.map(() => '?').join(',');
|
|
125
|
+
await db.runAsync(`DELETE FROM locations WHERE uuid IN (${placeholders})`, ...uuids);
|
|
126
|
+
}
|
|
127
|
+
/** Purge by retention policy. */
|
|
128
|
+
async prune(maxDays, maxRecords) {
|
|
129
|
+
const db = await this.open();
|
|
130
|
+
if (maxDays > 0) {
|
|
131
|
+
const cutoff = Date.now() - maxDays * 86400000;
|
|
132
|
+
await db.runAsync('DELETE FROM locations WHERE timestamp < ?', cutoff);
|
|
133
|
+
}
|
|
134
|
+
if (maxRecords > 0) {
|
|
135
|
+
await db.runAsync(`DELETE FROM locations WHERE uuid NOT IN (
|
|
136
|
+
SELECT uuid FROM locations ORDER BY timestamp DESC LIMIT ?
|
|
137
|
+
)`, maxRecords);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// --- geofences ----------------------------------------------------------
|
|
141
|
+
async upsertGeofence(geofence) {
|
|
142
|
+
const db = await this.open();
|
|
143
|
+
await db.runAsync('INSERT OR REPLACE INTO geofences (identifier, data) VALUES (?, ?)', geofence.identifier, JSON.stringify(geofence));
|
|
144
|
+
}
|
|
145
|
+
async getGeofences() {
|
|
146
|
+
const db = await this.open();
|
|
147
|
+
const rows = await db.getAllAsync('SELECT data FROM geofences');
|
|
148
|
+
return rows.map((r) => JSON.parse(r.data));
|
|
149
|
+
}
|
|
150
|
+
async getGeofence(identifier) {
|
|
151
|
+
const db = await this.open();
|
|
152
|
+
const row = await db.getFirstAsync('SELECT data FROM geofences WHERE identifier = ?', identifier);
|
|
153
|
+
return row ? JSON.parse(row.data) : null;
|
|
154
|
+
}
|
|
155
|
+
async removeGeofence(identifier) {
|
|
156
|
+
const db = await this.open();
|
|
157
|
+
await db.runAsync('DELETE FROM geofences WHERE identifier = ?', identifier);
|
|
158
|
+
}
|
|
159
|
+
async removeGeofences(identifiers) {
|
|
160
|
+
const db = await this.open();
|
|
161
|
+
if (!identifiers) {
|
|
162
|
+
await db.runAsync('DELETE FROM geofences');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!identifiers.length)
|
|
166
|
+
return;
|
|
167
|
+
const placeholders = identifiers.map(() => '?').join(',');
|
|
168
|
+
await db.runAsync(`DELETE FROM geofences WHERE identifier IN (${placeholders})`, ...identifiers);
|
|
169
|
+
}
|
|
170
|
+
// --- key/value (state, odometer, config) --------------------------------
|
|
171
|
+
async setKV(key, value) {
|
|
172
|
+
const db = await this.open();
|
|
173
|
+
await db.runAsync('INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)', key, JSON.stringify(value));
|
|
174
|
+
}
|
|
175
|
+
async getKV(key) {
|
|
176
|
+
const db = await this.open();
|
|
177
|
+
const row = await db.getFirstAsync('SELECT value FROM kv WHERE key = ?', key);
|
|
178
|
+
return row ? JSON.parse(row.value) : null;
|
|
179
|
+
}
|
|
180
|
+
// --- logs ----------------------------------------------------------------
|
|
181
|
+
async insertLog(level, message) {
|
|
182
|
+
const db = await this.open();
|
|
183
|
+
await db.runAsync('INSERT INTO logs (timestamp, level, message) VALUES (?, ?, ?)', Date.now(), level, message);
|
|
184
|
+
}
|
|
185
|
+
async getLogs(limit = 5000) {
|
|
186
|
+
const db = await this.open();
|
|
187
|
+
const rows = await db.getAllAsync('SELECT timestamp, level, message FROM logs ORDER BY id ASC LIMIT ?', limit);
|
|
188
|
+
return rows.map((r) => ({
|
|
189
|
+
timestamp: new Date(r.timestamp).toISOString(),
|
|
190
|
+
level: r.level,
|
|
191
|
+
message: r.message,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
async destroyLogs() {
|
|
195
|
+
const db = await this.open();
|
|
196
|
+
await db.runAsync('DELETE FROM logs');
|
|
197
|
+
}
|
|
198
|
+
async pruneLogs(maxDays) {
|
|
199
|
+
if (maxDays <= 0)
|
|
200
|
+
return;
|
|
201
|
+
const db = await this.open();
|
|
202
|
+
await db.runAsync('DELETE FROM logs WHERE timestamp < ?', Date.now() - maxDays * 86400000);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
exports.Database = Database;
|
|
206
|
+
exports.database = new Database();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EventName, Subscription } from './types';
|
|
2
|
+
type Listener = (event: unknown) => void;
|
|
3
|
+
/** Minimal dependency-free event emitter. */
|
|
4
|
+
export declare class EventBus {
|
|
5
|
+
private listeners;
|
|
6
|
+
on(name: EventName, fn: Listener): Subscription;
|
|
7
|
+
emit(name: EventName, event: unknown): void;
|
|
8
|
+
removeAll(name?: EventName): void;
|
|
9
|
+
}
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventBus = void 0;
|
|
4
|
+
/** Minimal dependency-free event emitter. */
|
|
5
|
+
class EventBus {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.listeners = new Map();
|
|
8
|
+
}
|
|
9
|
+
on(name, fn) {
|
|
10
|
+
if (!this.listeners.has(name))
|
|
11
|
+
this.listeners.set(name, new Set());
|
|
12
|
+
const set = this.listeners.get(name);
|
|
13
|
+
set.add(fn);
|
|
14
|
+
return { remove: () => set.delete(fn) };
|
|
15
|
+
}
|
|
16
|
+
emit(name, event) {
|
|
17
|
+
const set = this.listeners.get(name);
|
|
18
|
+
if (!set)
|
|
19
|
+
return;
|
|
20
|
+
for (const fn of [...set]) {
|
|
21
|
+
try {
|
|
22
|
+
fn(event);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
// listener errors must never break the tracking pipeline
|
|
26
|
+
console.warn(`[expo-background-tracking] listener error (${name})`, e);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
removeAll(name) {
|
|
31
|
+
if (name)
|
|
32
|
+
this.listeners.delete(name);
|
|
33
|
+
else
|
|
34
|
+
this.listeners.clear();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.EventBus = EventBus;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EventBus } from './EventBus';
|
|
2
|
+
import type { Geofence, Location } from './types';
|
|
3
|
+
export declare const GEOFENCE_TASK = "expo-background-tracking.geofence";
|
|
4
|
+
/**
|
|
5
|
+
* Unlimited geofencing (circular + polygon) with software proximity
|
|
6
|
+
* management, like transistorsoft: only nearby geofences are registered
|
|
7
|
+
* with the OS; the rest are monitored as the device approaches.
|
|
8
|
+
* In Expo Go, evaluation is fully software-based from the foreground stream.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GeofenceManager {
|
|
11
|
+
private bus;
|
|
12
|
+
private runtime;
|
|
13
|
+
private proximityRadius;
|
|
14
|
+
private started;
|
|
15
|
+
constructor(bus: EventBus);
|
|
16
|
+
setProximityRadius(metres: number): void;
|
|
17
|
+
add(geofence: Geofence): Promise<boolean>;
|
|
18
|
+
addMany(geofences: Geofence[]): Promise<boolean>;
|
|
19
|
+
private validate;
|
|
20
|
+
remove(identifier: string): Promise<boolean>;
|
|
21
|
+
removeAll(identifiers?: string[]): Promise<boolean>;
|
|
22
|
+
getGeofences(): Promise<Geofence[]>;
|
|
23
|
+
getGeofence(identifier: string): Promise<Geofence | null>;
|
|
24
|
+
exists(identifier: string): Promise<boolean>;
|
|
25
|
+
start(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Register nearby circular geofences with the OS (dev build only).
|
|
29
|
+
* Polygon geofences are always evaluated in software.
|
|
30
|
+
*/
|
|
31
|
+
private refreshNative;
|
|
32
|
+
/** Software evaluation — called for every location fix. */
|
|
33
|
+
evaluate(location: Location): Promise<void>;
|
|
34
|
+
/** Native OS geofence event (from TaskManager, dev build). */
|
|
35
|
+
onNativeEvent(identifier: string, action: 'ENTER' | 'EXIT', location: Location): Promise<void>;
|
|
36
|
+
private fire;
|
|
37
|
+
/** Useful for polygon geofences: approximate center for native registration. */
|
|
38
|
+
static centerOf(g: Geofence): {
|
|
39
|
+
latitude: number;
|
|
40
|
+
longitude: number;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GeofenceManager = exports.GEOFENCE_TASK = void 0;
|
|
37
|
+
const ExpoLocation = __importStar(require("expo-location"));
|
|
38
|
+
const Database_1 = require("./Database");
|
|
39
|
+
const Logger_1 = require("./Logger");
|
|
40
|
+
const geo_1 = require("./geo");
|
|
41
|
+
const environment_1 = require("./environment");
|
|
42
|
+
exports.GEOFENCE_TASK = 'expo-background-tracking.geofence';
|
|
43
|
+
/**
|
|
44
|
+
* Unlimited geofencing (circular + polygon) with software proximity
|
|
45
|
+
* management, like transistorsoft: only nearby geofences are registered
|
|
46
|
+
* with the OS; the rest are monitored as the device approaches.
|
|
47
|
+
* In Expo Go, evaluation is fully software-based from the foreground stream.
|
|
48
|
+
*/
|
|
49
|
+
class GeofenceManager {
|
|
50
|
+
constructor(bus) {
|
|
51
|
+
this.bus = bus;
|
|
52
|
+
this.runtime = new Map();
|
|
53
|
+
this.proximityRadius = 1000;
|
|
54
|
+
this.started = false;
|
|
55
|
+
}
|
|
56
|
+
setProximityRadius(metres) {
|
|
57
|
+
this.proximityRadius = metres;
|
|
58
|
+
}
|
|
59
|
+
async add(geofence) {
|
|
60
|
+
this.validate(geofence);
|
|
61
|
+
await Database_1.database.upsertGeofence(geofence);
|
|
62
|
+
this.runtime.set(geofence.identifier, {
|
|
63
|
+
inside: false,
|
|
64
|
+
enteredAt: null,
|
|
65
|
+
dwellFired: false,
|
|
66
|
+
active: false,
|
|
67
|
+
});
|
|
68
|
+
if (this.started)
|
|
69
|
+
await this.refreshNative(null);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
async addMany(geofences) {
|
|
73
|
+
for (const g of geofences) {
|
|
74
|
+
this.validate(g);
|
|
75
|
+
await Database_1.database.upsertGeofence(g);
|
|
76
|
+
this.runtime.set(g.identifier, {
|
|
77
|
+
inside: false,
|
|
78
|
+
enteredAt: null,
|
|
79
|
+
dwellFired: false,
|
|
80
|
+
active: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (this.started)
|
|
84
|
+
await this.refreshNative(null);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
validate(g) {
|
|
88
|
+
if (!g.identifier)
|
|
89
|
+
throw new Error('Geofence requires identifier');
|
|
90
|
+
const circular = g.latitude != null && g.longitude != null && g.radius != null;
|
|
91
|
+
const polygon = !!g.vertices?.length;
|
|
92
|
+
if (!circular && !polygon) {
|
|
93
|
+
throw new Error(`Geofence "${g.identifier}" requires latitude/longitude/radius or vertices`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async remove(identifier) {
|
|
97
|
+
await Database_1.database.removeGeofence(identifier);
|
|
98
|
+
this.runtime.delete(identifier);
|
|
99
|
+
if (this.started)
|
|
100
|
+
await this.refreshNative(null);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
async removeAll(identifiers) {
|
|
104
|
+
await Database_1.database.removeGeofences(identifiers);
|
|
105
|
+
if (identifiers)
|
|
106
|
+
identifiers.forEach((id) => this.runtime.delete(id));
|
|
107
|
+
else
|
|
108
|
+
this.runtime.clear();
|
|
109
|
+
if (this.started)
|
|
110
|
+
await this.refreshNative(null);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
getGeofences() {
|
|
114
|
+
return Database_1.database.getGeofences();
|
|
115
|
+
}
|
|
116
|
+
getGeofence(identifier) {
|
|
117
|
+
return Database_1.database.getGeofence(identifier);
|
|
118
|
+
}
|
|
119
|
+
async exists(identifier) {
|
|
120
|
+
return (await Database_1.database.getGeofence(identifier)) != null;
|
|
121
|
+
}
|
|
122
|
+
async start() {
|
|
123
|
+
this.started = true;
|
|
124
|
+
const geofences = await Database_1.database.getGeofences();
|
|
125
|
+
for (const g of geofences) {
|
|
126
|
+
if (!this.runtime.has(g.identifier)) {
|
|
127
|
+
this.runtime.set(g.identifier, {
|
|
128
|
+
inside: false,
|
|
129
|
+
enteredAt: null,
|
|
130
|
+
dwellFired: false,
|
|
131
|
+
active: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
await this.refreshNative(null);
|
|
136
|
+
}
|
|
137
|
+
async stop() {
|
|
138
|
+
this.started = false;
|
|
139
|
+
if ((0, environment_1.isBackgroundCapable)()) {
|
|
140
|
+
try {
|
|
141
|
+
const registered = await ExpoLocation.hasStartedGeofencingAsync(exports.GEOFENCE_TASK);
|
|
142
|
+
if (registered)
|
|
143
|
+
await ExpoLocation.stopGeofencingAsync(exports.GEOFENCE_TASK);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* ignore */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Register nearby circular geofences with the OS (dev build only).
|
|
152
|
+
* Polygon geofences are always evaluated in software.
|
|
153
|
+
*/
|
|
154
|
+
async refreshNative(position) {
|
|
155
|
+
if (!(0, environment_1.isBackgroundCapable)())
|
|
156
|
+
return; // Expo Go → software-only
|
|
157
|
+
try {
|
|
158
|
+
const geofences = await Database_1.database.getGeofences();
|
|
159
|
+
const circular = geofences.filter((g) => g.latitude != null && g.longitude != null && g.radius != null);
|
|
160
|
+
let selected = circular;
|
|
161
|
+
const changed = { on: [], off: [] };
|
|
162
|
+
if (position && circular.length > 19) {
|
|
163
|
+
selected = circular
|
|
164
|
+
.map((g) => ({
|
|
165
|
+
g,
|
|
166
|
+
d: (0, geo_1.haversine)(position.latitude, position.longitude, g.latitude, g.longitude),
|
|
167
|
+
}))
|
|
168
|
+
.filter(({ g, d }) => d <= this.proximityRadius + (g.radius ?? 0))
|
|
169
|
+
.sort((a, b) => a.d - b.d)
|
|
170
|
+
.slice(0, 19)
|
|
171
|
+
.map(({ g }) => g);
|
|
172
|
+
}
|
|
173
|
+
const selectedIds = new Set(selected.map((g) => g.identifier));
|
|
174
|
+
for (const g of circular) {
|
|
175
|
+
const rt = this.runtime.get(g.identifier);
|
|
176
|
+
if (!rt)
|
|
177
|
+
continue;
|
|
178
|
+
const nowActive = selectedIds.has(g.identifier);
|
|
179
|
+
if (nowActive && !rt.active)
|
|
180
|
+
changed.on.push(g);
|
|
181
|
+
if (!nowActive && rt.active)
|
|
182
|
+
changed.off.push(g.identifier);
|
|
183
|
+
rt.active = nowActive;
|
|
184
|
+
}
|
|
185
|
+
if (changed.on.length || changed.off.length) {
|
|
186
|
+
this.bus.emit('geofenceschange', changed);
|
|
187
|
+
}
|
|
188
|
+
if (selected.length) {
|
|
189
|
+
await ExpoLocation.startGeofencingAsync(exports.GEOFENCE_TASK, selected.map((g) => ({
|
|
190
|
+
identifier: g.identifier,
|
|
191
|
+
latitude: g.latitude,
|
|
192
|
+
longitude: g.longitude,
|
|
193
|
+
radius: g.radius,
|
|
194
|
+
notifyOnEnter: g.notifyOnEntry !== false,
|
|
195
|
+
notifyOnExit: g.notifyOnExit !== false,
|
|
196
|
+
})));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const registered = await ExpoLocation.hasStartedGeofencingAsync(exports.GEOFENCE_TASK);
|
|
200
|
+
if (registered)
|
|
201
|
+
await ExpoLocation.stopGeofencingAsync(exports.GEOFENCE_TASK);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
Logger_1.logger.warn(`Native geofencing unavailable: ${String(e)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Software evaluation — called for every location fix. */
|
|
209
|
+
async evaluate(location) {
|
|
210
|
+
const { latitude, longitude } = location.coords;
|
|
211
|
+
const geofences = await Database_1.database.getGeofences();
|
|
212
|
+
const now = Date.now();
|
|
213
|
+
for (const g of geofences) {
|
|
214
|
+
let rt = this.runtime.get(g.identifier);
|
|
215
|
+
if (!rt) {
|
|
216
|
+
rt = { inside: false, enteredAt: null, dwellFired: false, active: false };
|
|
217
|
+
this.runtime.set(g.identifier, rt);
|
|
218
|
+
}
|
|
219
|
+
let inside;
|
|
220
|
+
if (g.vertices?.length) {
|
|
221
|
+
inside = (0, geo_1.pointInPolygon)(latitude, longitude, g.vertices);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const d = (0, geo_1.haversine)(latitude, longitude, g.latitude, g.longitude);
|
|
225
|
+
inside = d <= (g.radius ?? 200);
|
|
226
|
+
}
|
|
227
|
+
if (inside && !rt.inside) {
|
|
228
|
+
rt.inside = true;
|
|
229
|
+
rt.enteredAt = now;
|
|
230
|
+
rt.dwellFired = false;
|
|
231
|
+
if (g.notifyOnEntry !== false)
|
|
232
|
+
this.fire(g, 'ENTER', location);
|
|
233
|
+
}
|
|
234
|
+
else if (!inside && rt.inside) {
|
|
235
|
+
rt.inside = false;
|
|
236
|
+
rt.enteredAt = null;
|
|
237
|
+
rt.dwellFired = false;
|
|
238
|
+
if (g.notifyOnExit !== false)
|
|
239
|
+
this.fire(g, 'EXIT', location);
|
|
240
|
+
}
|
|
241
|
+
else if (inside &&
|
|
242
|
+
rt.inside &&
|
|
243
|
+
g.notifyOnDwell &&
|
|
244
|
+
!rt.dwellFired &&
|
|
245
|
+
rt.enteredAt != null &&
|
|
246
|
+
now - rt.enteredAt >= (g.loiteringDelay ?? 300000)) {
|
|
247
|
+
rt.dwellFired = true;
|
|
248
|
+
this.fire(g, 'DWELL', location);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await this.refreshNative({ latitude, longitude });
|
|
252
|
+
}
|
|
253
|
+
/** Native OS geofence event (from TaskManager, dev build). */
|
|
254
|
+
async onNativeEvent(identifier, action, location) {
|
|
255
|
+
const g = await Database_1.database.getGeofence(identifier);
|
|
256
|
+
if (!g)
|
|
257
|
+
return;
|
|
258
|
+
const rt = this.runtime.get(identifier);
|
|
259
|
+
if (rt) {
|
|
260
|
+
rt.inside = action === 'ENTER';
|
|
261
|
+
rt.enteredAt = action === 'ENTER' ? Date.now() : null;
|
|
262
|
+
}
|
|
263
|
+
this.fire(g, action, location);
|
|
264
|
+
}
|
|
265
|
+
fire(g, action, location) {
|
|
266
|
+
Logger_1.logger.info(`Geofence ${action}: ${g.identifier}`);
|
|
267
|
+
this.bus.emit('geofence', {
|
|
268
|
+
identifier: g.identifier,
|
|
269
|
+
action,
|
|
270
|
+
location: { ...location, event: 'geofence' },
|
|
271
|
+
extras: g.extras,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Useful for polygon geofences: approximate center for native registration. */
|
|
275
|
+
static centerOf(g) {
|
|
276
|
+
if (g.vertices?.length)
|
|
277
|
+
return (0, geo_1.centroid)(g.vertices);
|
|
278
|
+
return { latitude: g.latitude, longitude: g.longitude };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
exports.GeofenceManager = GeofenceManager;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EventBus } from './EventBus';
|
|
2
|
+
import type { Config, Location } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* HTTP upload service: autoSync, batchSync, templates, JWT refresh,
|
|
5
|
+
* offline queue with re-sync on reconnect. Pure `fetch` — no Firebase.
|
|
6
|
+
*/
|
|
7
|
+
export declare class HttpService {
|
|
8
|
+
private bus;
|
|
9
|
+
private config;
|
|
10
|
+
private syncing;
|
|
11
|
+
private connectivityTimer;
|
|
12
|
+
private lastConnected;
|
|
13
|
+
constructor(bus: EventBus);
|
|
14
|
+
setConfig(config: Config): void;
|
|
15
|
+
startConnectivityMonitoring(intervalMs?: number): void;
|
|
16
|
+
stopConnectivityMonitoring(): void;
|
|
17
|
+
private checkConnectivity;
|
|
18
|
+
/** Called after each persisted location when autoSync is enabled. */
|
|
19
|
+
autoSync(): Promise<void>;
|
|
20
|
+
/** Upload all queued locations. Resolves with the uploaded records. */
|
|
21
|
+
sync(): Promise<Location[]>;
|
|
22
|
+
private renderTemplate;
|
|
23
|
+
private buildBody;
|
|
24
|
+
private authHeaders;
|
|
25
|
+
private refreshToken;
|
|
26
|
+
private upload;
|
|
27
|
+
}
|