@tokamakdev/plugin-location 0.1.0-beta.128 → 0.1.0-beta.130

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/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { Plugin, lifecycleStage, type JsonStream, type TokamakContext } from "@tokamakdev/plugin";
1
+ import { capabilitiesOf, lifecycleStage, platformApi, type JsonStream, type TokamakContext } from "@tokamakdev/plugin";
2
2
 
3
+ import { android } from "./android.generated.js";
4
+ import { apple } from "./apple.generated.js";
3
5
  import { backgroundDenied, permissionDenied, timedOut, unavailable } from "./errors.js";
4
6
  import type {
5
7
  LocationErrorEvent,
@@ -18,147 +20,120 @@ import type {
18
20
 
19
21
  export type * from "./types.js";
20
22
 
21
- class Location extends Plugin {
22
- constructor(ctx: TokamakContext) {
23
- super(ctx, "location");
24
- }
25
-
26
- /**
27
- * Applies long-term location awareness, replacing the previous configuration, which the plugin keeps
28
- * and applies again at each start. Rejects on Apple platforms with `NotSupportedError` where the
29
- * device cannot monitor significant changes.
30
- */
31
- setup(config: SetupConfig): Promise<void> {
32
- return this.call("setup", config);
33
- }
34
-
35
- /**
36
- * The device's position, asking for in-use permission where the user has not answered. Rejects
37
- * with `NotAllowedError` without it, `TimeoutError` after `timeout`, and `NotReadableError`
38
- * when location is unavailable. Asking off screen rejects with `NeedsUIError`, as
39
- * `requestPermission` does.
40
- */
41
- async getCurrentPosition({ maximumAge = 0, timeout }: PositionOptions = {}): Promise<Position> {
42
- await this.#authorize();
43
- // JSON has no Infinity, which accepts any recent position.
44
- const recent = await this.call<Position | null>("recentPosition", { maximumAge: Math.min(maximumAge, Number.MAX_VALUE) });
45
- if (recent) return recent;
46
- const located = new AbortController();
47
- const timer = timeout === undefined ? undefined : setTimeout(() => { located.abort(timedOut()); }, timeout);
48
- const position = await this.call<Position | null>("currentPosition", null, { signal: located.signal })
49
- .finally(() => { clearTimeout(timer); });
50
- if (position) return position;
51
- // Android finds no position in the background without background permission.
52
- const background = await lifecycleStage(this.ctx) === "background";
53
- throw background && (await this.permission()).background !== "granted" ? backgroundDenied() : unavailable("Location is unavailable");
54
- }
55
-
56
- /**
57
- * Positions as the device moves, after `getCurrentPosition`'s permission checks, which can reject
58
- * with `NeedsUIError` off screen. A slow reader receives the newest.
59
- */
60
- watchPosition(): JsonStream<Position> {
61
- return this.stream("watchPosition", this.#authorize().then(() => null), "dropOldest");
62
- }
23
+ export function location(ctx: TokamakContext) {
24
+ const native = platformApi(ctx, { apple, android });
63
25
 
64
26
  /** Requires location services and in-use permission, asking for it where the user has not answered. */
65
- async #authorize(): Promise<void> {
66
- if (!await this.servicesEnabled()) throw unavailable("Location services are disabled");
67
- let { inUse } = await this.permission();
68
- if (inUse === "prompt") ({ inUse } = await this.requestPermission());
27
+ async function authorize(): Promise<void> {
28
+ if (!await native.servicesEnabled()) throw unavailable("Location services are disabled");
29
+ let { inUse } = await native.permission();
30
+ if (inUse === "prompt") ({ inUse } = await native.requestPermission());
69
31
  if (inUse !== "granted") throw permissionDenied();
70
32
  }
71
33
 
72
- /** The app's in-use and background permission, and the precision it gets. Never asks. */
73
- permission(): Promise<LocationPermission> {
74
- return this.call("permission");
75
- }
76
-
77
- /**
78
- * Asks for in-use permission, and for what `options` names, where the user has not answered.
79
- * Android asks for background permission once in-use permission is granted. Rejects with
80
- * `NeedsUIError` when a prompt would show off screen.
81
- */
82
- requestPermission(options: PermissionRequest = {}): Promise<LocationPermission> {
83
- return this.call("requestPermission", options);
84
- }
85
-
86
- /** Whether location services are on for the device. While they are off, nothing locates. */
87
- servicesEnabled(): Promise<boolean> {
88
- return this.call("servicesEnabled");
89
- }
90
-
91
- /**
92
- * Opens the app's page in the system settings, where the user changes its location permission.
93
- * Rejects with `NeedsUIError` off screen on iOS and Android.
94
- */
95
- openSettings(): Promise<void> {
96
- return this.call("openSettings");
97
- }
98
-
99
- /**
100
- * Starts continuous tracking, replacing any running, until `stopUpdates`; `setup` gives long-term
101
- * awareness instead. The updates' positions reach `onLocationUpdate`, including after the system
102
- * ended the app. Rejects with `NotAllowedError` without in-use permission, and on iOS with
103
- * `NeedsUIError` off screen. Android's `foregroundService` rejects with `NotSupportedError`
104
- * without the `background-updates` build setting, and with `NeedsUIError` when Android does not
105
- * allow the service to start off screen.
106
- */
107
- startUpdates(options: UpdateOptions = {}): Promise<void> {
108
- return this.call("startUpdates", options);
109
- }
110
-
111
- stopUpdates(): Promise<void> {
112
- return this.call("stopUpdates");
113
- }
114
-
115
- /** The options of the running updates, or null when none are running. */
116
- getUpdates(): Promise<UpdateOptions | null> {
117
- return this.call("getUpdates");
118
- }
119
-
120
- /**
121
- * Monitors a circular region, replacing one with the same `id`, until `removeRegion`. Its entries
122
- * and exits reach `onRegionTransition`, including when the system launches the app for one. A
123
- * region starts outside, so a device already inside reports an entry. Rejects with
124
- * `NotAllowedError` without precise location, and with `QuotaExceededError` beyond Apple's 20.
125
- */
126
- addRegion(region: Region): Promise<void> {
127
- return this.call("addRegion", region);
128
- }
129
-
130
- /** The monitored regions. */
131
- getRegions(): Promise<MonitoredRegion[]> {
132
- return this.call("getRegions");
133
- }
134
-
135
- removeRegion(id: string): Promise<void> {
136
- return this.call("removeRegion", { id });
137
- }
138
-
139
- /** Receives the updates the Worker's `onLocationUpdate` receives, while the listening page or Worker runs. */
140
- onLocationUpdate(): JsonStream<LocationUpdate> {
141
- return this.stream("onLocationUpdate", null, "dropOldest");
142
- }
143
-
144
- /** Receives the transitions the Worker's `onRegionTransition` receives, while the listening page or Worker runs. */
145
- onRegionTransition(): JsonStream<RegionTransition> {
146
- return this.stream("onRegionTransition");
147
- }
148
-
149
- /**
150
- * Receives the visits the Worker's `onVisit` receives, while the listening page or Worker runs.
151
- * @platforms ios macos
152
- */
153
- onVisit(): JsonStream<Visit> {
154
- return this.stream("onVisit");
155
- }
156
-
157
- /** Receives the failures the Worker's `onLocationError` receives, while the listening page or Worker runs. */
158
- onLocationError(): JsonStream<LocationErrorEvent> {
159
- return this.stream("onLocationError");
160
- }
34
+ return {
35
+ /** Whether each operation is implemented where the app runs, without calling it. Permission is not reported. */
36
+ get capabilities(): Readonly<Record<string, boolean>> {
37
+ return capabilitiesOf(ctx, "location");
38
+ },
39
+
40
+ /**
41
+ * Applies long-term location awareness, replacing the previous configuration, which the plugin
42
+ * keeps and applies again at each start. Rejects on Apple platforms with `NotSupportedError` where
43
+ * the device cannot monitor significant changes.
44
+ */
45
+ setup: (config: SetupConfig): Promise<void> => native.setup(config),
46
+
47
+ /**
48
+ * The device's position, asking for in-use permission where the user has not answered. Rejects
49
+ * with `NotAllowedError` without it, `TimeoutError` after `timeout`, and `NotReadableError`
50
+ * when location is unavailable. Asking off screen rejects with `NeedsUIError`, as
51
+ * `requestPermission` does.
52
+ */
53
+ async getCurrentPosition({ maximumAge = 0, timeout }: PositionOptions = {}): Promise<Position> {
54
+ await authorize();
55
+ // JSON has no Infinity, which accepts any recent position.
56
+ const recent = await native.recentPosition(Math.min(maximumAge, Number.MAX_VALUE));
57
+ if (recent) return recent;
58
+ const located = new AbortController();
59
+ const timer = timeout === undefined ? undefined : setTimeout(() => { located.abort(timedOut()); }, timeout);
60
+ const position = await platformApi(ctx, { apple, android }, located.signal).currentPosition()
61
+ .finally(() => { clearTimeout(timer); });
62
+ if (position) return position;
63
+ // Android finds no position in the background without background permission.
64
+ const background = await lifecycleStage(ctx) === "background";
65
+ throw background && (await native.permission()).background !== "granted" ? backgroundDenied() : unavailable("Location is unavailable");
66
+ },
67
+
68
+ /**
69
+ * Positions as the device moves, after `getCurrentPosition`'s permission checks, which can reject
70
+ * with `NeedsUIError` off screen. A slow reader receives the newest.
71
+ */
72
+ async watchPosition(): Promise<JsonStream<Position>> {
73
+ await authorize();
74
+ return native.watchPosition();
75
+ },
76
+
77
+ /** The app's in-use and background permission, and the precision it gets. Never asks. */
78
+ permission: (): Promise<LocationPermission> => native.permission(),
79
+
80
+ /**
81
+ * Asks for in-use permission, and for what `options` names, where the user has not answered.
82
+ * Android asks for background permission once in-use permission is granted. Rejects with
83
+ * `NeedsUIError` when a prompt would show off screen.
84
+ */
85
+ requestPermission: (options?: PermissionRequest): Promise<LocationPermission> => native.requestPermission(options),
86
+
87
+ /** Whether location services are on for the device. While they are off, nothing locates. */
88
+ servicesEnabled: (): Promise<boolean> => native.servicesEnabled(),
89
+
90
+ /**
91
+ * Opens the app's page in the system settings, where the user changes its location permission.
92
+ * Rejects with `NeedsUIError` off screen on iOS and Android.
93
+ */
94
+ openSettings: (): Promise<void> => native.openSettings(),
95
+
96
+ /**
97
+ * Starts continuous tracking, replacing any running, until `stopUpdates`; `setup` gives long-term
98
+ * awareness instead. The updates' positions reach `onLocationUpdate`, including after the system
99
+ * ended the app. Rejects with `NotAllowedError` without in-use permission, and on iOS with
100
+ * `NeedsUIError` off screen. Android's `foregroundService` rejects with `NotSupportedError`
101
+ * without the `background-updates` build setting, and with `NeedsUIError` when Android does not
102
+ * allow the service to start off screen.
103
+ */
104
+ startUpdates: (options?: UpdateOptions): Promise<void> => native.startUpdates(options),
105
+
106
+ stopUpdates: (): Promise<void> => native.stopUpdates(),
107
+
108
+ /** The options of the running updates, or null when none are running. */
109
+ getUpdates: (): Promise<UpdateOptions | null> => native.getUpdates(),
110
+
111
+ /**
112
+ * Monitors a circular region, replacing one with the same `id`, until `removeRegion`. Its entries
113
+ * and exits reach `onRegionTransition`, including when the system launches the app for one. A
114
+ * region starts outside, so a device already inside reports an entry. Rejects with
115
+ * `NotAllowedError` without precise location, and with `QuotaExceededError` beyond Apple's 20.
116
+ */
117
+ addRegion: (region: Region): Promise<void> => native.addRegion(region),
118
+
119
+ /** The monitored regions. */
120
+ getRegions: (): Promise<MonitoredRegion[]> => native.getRegions(),
121
+
122
+ removeRegion: (id: string): Promise<void> => native.removeRegion(id),
123
+
124
+ /** Receives the updates the Worker's `onLocationUpdate` receives, while the listening page or Worker runs. */
125
+ onLocationUpdate: (): JsonStream<LocationUpdate> => native.onLocationUpdate(),
126
+
127
+ /** Receives the transitions the Worker's `onRegionTransition` receives, while the listening page or Worker runs. */
128
+ onRegionTransition: (): JsonStream<RegionTransition> => native.onRegionTransition(),
129
+
130
+ /**
131
+ * Receives the visits the Worker's `onVisit` receives, while the listening page or Worker runs.
132
+ * @platforms ios macos
133
+ */
134
+ onVisit: (): JsonStream<Visit> => apple(ctx).onVisit(),
135
+
136
+ /** Receives the failures the Worker's `onLocationError` receives, while the listening page or Worker runs. */
137
+ onLocationError: (): JsonStream<LocationErrorEvent> => native.onLocationError(),
138
+ };
161
139
  }
162
-
163
-
164
- export function location(ctx: TokamakContext): Location { return new Location(ctx); }
package/src/types.ts CHANGED
@@ -1,11 +1,12 @@
1
+ /** A position's coordinates, without those the device does not report. */
1
2
  export interface Coordinates {
2
3
  readonly latitude: number;
3
4
  readonly longitude: number;
4
5
  readonly accuracy: number;
5
- readonly altitude: number | null;
6
- readonly altitudeAccuracy: number | null;
7
- readonly heading: number | null;
8
- readonly speed: number | null;
6
+ readonly altitude?: number;
7
+ readonly altitudeAccuracy?: number;
8
+ readonly heading?: number;
9
+ readonly speed?: number;
9
10
  }
10
11
 
11
12
  export interface Position {
@@ -157,10 +158,10 @@ export interface Visit {
157
158
  readonly longitude: number;
158
159
  /** In metres. */
159
160
  readonly accuracy: number;
160
- /** Milliseconds since the epoch, or null when unknown. */
161
- readonly arrival: number | null;
162
- /** Milliseconds since the epoch, or null before the user leaves. */
163
- readonly departure: number | null;
161
+ /** Milliseconds since the epoch, absent when unknown. */
162
+ readonly arrival?: number;
163
+ /** Milliseconds since the epoch, absent before the user leaves. */
164
+ readonly departure?: number;
164
165
  }
165
166
 
166
167
  /** A failure of a background location service. */
@@ -174,15 +175,3 @@ export interface LocationErrorEvent {
174
175
 
175
176
  export type Listener<T> = (event: T) => void;
176
177
  export type ErrorListener = (error: DOMException) => void;
177
-
178
- declare global {
179
- // eslint-disable-next-line @typescript-eslint/no-namespace -- events are declared by merging into Tokamak.Events.
180
- namespace Tokamak {
181
- interface Events {
182
- "location.update": LocationUpdate;
183
- "location.region": RegionTransition;
184
- "location.visit": Visit;
185
- "location.error": LocationErrorEvent;
186
- }
187
- }
188
- }
@@ -38,36 +38,13 @@
38
38
  "build": "build/index.mjs",
39
39
  "platforms": {
40
40
  "macos": {
41
- "class": "TokamakLocationPlugin",
42
- "sources": [
43
- "apple/LocationEvents.swift",
44
- "apple/LocationPlugin.swift",
45
- "apple/LocationPositions.swift",
46
- "apple/LocationRegions.swift",
47
- "apple/LocationUpdates.swift"
48
- ]
41
+ "class": "TokamakLocationPlugin"
49
42
  },
50
43
  "ios": {
51
- "class": "TokamakLocationPlugin",
52
- "sources": [
53
- "apple/LocationEvents.swift",
54
- "apple/LocationPlugin.swift",
55
- "apple/LocationPositions.swift",
56
- "apple/LocationRegions.swift",
57
- "apple/LocationUpdates.swift"
58
- ]
44
+ "class": "TokamakLocationPlugin"
59
45
  },
60
46
  "android": {
61
- "class": "com.tokamak.plugins.location.TokamakLocationPlugin",
62
- "sources": [
63
- "android/LocationEvents.kt",
64
- "android/LocationPlugin.kt",
65
- "android/LocationPositions.kt",
66
- "android/LocationReceiver.kt",
67
- "android/LocationRegions.kt",
68
- "android/LocationService.kt",
69
- "android/LocationUpdates.kt"
70
- ]
47
+ "class": "com.tokamak.plugins.location.TokamakLocationPlugin"
71
48
  }
72
49
  },
73
50
  "api": {
package/web/index.ts CHANGED
@@ -59,19 +59,20 @@ function browserLocationApi(): Geolocation {
59
59
  throw new DOMException("Location is unavailable", "NotSupportedError");
60
60
  }
61
61
 
62
- function copyPosition(position: GeolocationPosition): Position {
63
- const { coords } = position;
62
+ /** `position` as native platforms give it: without the coordinates the device does not report. */
63
+ function copyPosition({ coords, timestamp }: GeolocationPosition): Position {
64
+ const { latitude, longitude, accuracy, altitude, altitudeAccuracy, heading, speed } = coords;
64
65
  return {
65
66
  coords: {
66
- latitude: coords.latitude,
67
- longitude: coords.longitude,
68
- accuracy: coords.accuracy,
69
- altitude: coords.altitude,
70
- altitudeAccuracy: coords.altitudeAccuracy,
71
- heading: coords.heading,
72
- speed: coords.speed,
67
+ latitude,
68
+ longitude,
69
+ accuracy,
70
+ ...(altitude === null ? {} : { altitude }),
71
+ ...(altitudeAccuracy === null ? {} : { altitudeAccuracy }),
72
+ ...(heading === null ? {} : { heading }),
73
+ ...(speed === null ? {} : { speed }),
73
74
  },
74
- timestamp: position.timestamp,
75
+ timestamp,
75
76
  };
76
77
  }
77
78
 
package/events/index.ts DELETED
@@ -1,18 +0,0 @@
1
- import { defineEvent } from "@tokamakdev/plugin/events";
2
-
3
- import "../src/types.js";
4
-
5
- /**
6
- * Receives the positions of continuous updates and significant changes in native builds: on
7
- * screen, off screen, or after the system launches the app for them.
8
- */
9
- export const onLocationUpdate = defineEvent("location.update");
10
-
11
- /** Receives each region entry or exit, including one that launches the app. */
12
- export const onRegionTransition = defineEvent("location.region");
13
-
14
- /** Apple only: receives each arrival at or departure from a place, including one that launches the app. */
15
- export const onVisit = defineEvent("location.visit");
16
-
17
- /** Receives failures of background location services, such as a region the system cannot monitor. */
18
- export const onLocationError = defineEvent("location.error");