@ccheever/exact-ibex-runtime 0.1.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.
Files changed (161) hide show
  1. package/package.json +63 -0
  2. package/src/abort/AbortController.ts +23 -0
  3. package/src/abort/AbortSignal.ts +152 -0
  4. package/src/abort/index.ts +2 -0
  5. package/src/accessibility.ts +12 -0
  6. package/src/arraybuffer-detach.ts +109 -0
  7. package/src/base64/base64.ts +168 -0
  8. package/src/base64/index.ts +1 -0
  9. package/src/blob/Blob.ts +259 -0
  10. package/src/blob/File.ts +59 -0
  11. package/src/blob/FormData.ts +323 -0
  12. package/src/blob/index.ts +3 -0
  13. package/src/bootstrap.ts +1946 -0
  14. package/src/broadcast/BroadcastChannel.ts +280 -0
  15. package/src/broadcast/index.ts +5 -0
  16. package/src/cache/Cache.ts +349 -0
  17. package/src/cache/CacheStorage.ts +89 -0
  18. package/src/cache/index.ts +27 -0
  19. package/src/camera/index.ts +6202 -0
  20. package/src/camera/processor.worker.ts +194 -0
  21. package/src/camera/scene.ts +195 -0
  22. package/src/clipboard/Clipboard.ts +129 -0
  23. package/src/clipboard/ClipboardItem.ts +97 -0
  24. package/src/clipboard/index.ts +6 -0
  25. package/src/clone/index.ts +1 -0
  26. package/src/clone/structuredClone.ts +389 -0
  27. package/src/clone/transferableSymbols.ts +2 -0
  28. package/src/compression/CompressionStream.ts +146 -0
  29. package/src/compression/DecompressionStream.ts +342 -0
  30. package/src/compression/index.ts +4 -0
  31. package/src/console/Console.ts +341 -0
  32. package/src/console/index.ts +2 -0
  33. package/src/core/accessibility-state.ts +263 -0
  34. package/src/core/accessibility.ts +184 -0
  35. package/src/core/agent-state.ts +37 -0
  36. package/src/core/diagnostics-logs.ts +144 -0
  37. package/src/core/host-call-bridge.ts +16 -0
  38. package/src/core/i18n-helpers.ts +189 -0
  39. package/src/core/locale-state.ts +253 -0
  40. package/src/core/locale.ts +95 -0
  41. package/src/crypto/Crypto.ts +2743 -0
  42. package/src/crypto/index.ts +1 -0
  43. package/src/diagnostics/logs.ts +7 -0
  44. package/src/encoding/TextDecoder.ts +1181 -0
  45. package/src/encoding/TextDecoderStream.ts +58 -0
  46. package/src/encoding/TextEncoder.ts +180 -0
  47. package/src/encoding/TextEncoderStream.ts +39 -0
  48. package/src/encoding/index.ts +8 -0
  49. package/src/events/CloseEvent.ts +91 -0
  50. package/src/events/DOMException.ts +409 -0
  51. package/src/events/ErrorEvent.ts +39 -0
  52. package/src/events/Event.ts +151 -0
  53. package/src/events/EventTarget.ts +280 -0
  54. package/src/events/FocusEvent.ts +27 -0
  55. package/src/events/KeyboardEvent.ts +46 -0
  56. package/src/events/MessageEvent.ts +61 -0
  57. package/src/events/ProgressEvent.ts +33 -0
  58. package/src/events/PromiseRejectionEvent.ts +31 -0
  59. package/src/events/index.ts +52 -0
  60. package/src/eventsource/EventSource.ts +371 -0
  61. package/src/eventsource/index.ts +2 -0
  62. package/src/fetch/Headers.ts +642 -0
  63. package/src/fetch/Request.ts +760 -0
  64. package/src/fetch/Response.ts +543 -0
  65. package/src/fetch/body.ts +1256 -0
  66. package/src/fetch/cookie-jar.ts +566 -0
  67. package/src/fetch/demo.ts +207 -0
  68. package/src/fetch/errors.ts +101 -0
  69. package/src/fetch/fetch.ts +2610 -0
  70. package/src/fetch/index.ts +101 -0
  71. package/src/fetch/native-bridge.ts +65 -0
  72. package/src/fetch/types.ts +258 -0
  73. package/src/filereader/FileReader.ts +236 -0
  74. package/src/filereader/index.ts +1 -0
  75. package/src/fs/Dirent.ts +39 -0
  76. package/src/fs/ExactFile.ts +450 -0
  77. package/src/fs/Stats.ts +80 -0
  78. package/src/fs/index.ts +944 -0
  79. package/src/fs/promises.ts +386 -0
  80. package/src/fs/shared.ts +328 -0
  81. package/src/http-server/index.js +697 -0
  82. package/src/http-server/index.ts +27 -0
  83. package/src/identity.generated.ts +14 -0
  84. package/src/index.ts +283 -0
  85. package/src/indexeddb/IDBCursor.ts +188 -0
  86. package/src/indexeddb/IDBDatabase.ts +343 -0
  87. package/src/indexeddb/IDBFactory.ts +269 -0
  88. package/src/indexeddb/IDBIndex.ts +194 -0
  89. package/src/indexeddb/IDBKeyRange.ts +109 -0
  90. package/src/indexeddb/IDBObjectStore.ts +468 -0
  91. package/src/indexeddb/IDBRequest.ts +163 -0
  92. package/src/indexeddb/IDBTransaction.ts +207 -0
  93. package/src/indexeddb/index.ts +34 -0
  94. package/src/indexeddb/utils.ts +52 -0
  95. package/src/inspect/index.ts +1 -0
  96. package/src/inspect/inspect.ts +465 -0
  97. package/src/internal/detect.ts +104 -0
  98. package/src/locale.ts +10 -0
  99. package/src/location/index.ts +1059 -0
  100. package/src/locks/LockManager.ts +460 -0
  101. package/src/locks/index.ts +12 -0
  102. package/src/media/VideoFrame.ts +58 -0
  103. package/src/messaging/MessageChannel.ts +31 -0
  104. package/src/messaging/MessagePort.ts +180 -0
  105. package/src/messaging/index.ts +2 -0
  106. package/src/messaging.ts +247 -0
  107. package/src/native/NativeModules.ts +354 -0
  108. package/src/native/index.ts +1 -0
  109. package/src/navigator/Navigator.ts +351 -0
  110. package/src/navigator/index.ts +1 -0
  111. package/src/node/Buffer.ts +1786 -0
  112. package/src/node/index.ts +4 -0
  113. package/src/node/path.ts +495 -0
  114. package/src/node/process.ts +2528 -0
  115. package/src/performance/Performance.ts +532 -0
  116. package/src/performance/index.ts +21 -0
  117. package/src/polyfills/array.ts +236 -0
  118. package/src/polyfills/arraybuffer.ts +172 -0
  119. package/src/polyfills/groupby.ts +85 -0
  120. package/src/polyfills/index.ts +85 -0
  121. package/src/polyfills/intl.ts +1956 -0
  122. package/src/polyfills/iterator.ts +479 -0
  123. package/src/polyfills/promise.ts +37 -0
  124. package/src/polyfills/set.ts +245 -0
  125. package/src/polyfills/string.ts +85 -0
  126. package/src/polyfills/typedarray.ts +110 -0
  127. package/src/promise-rejection-tracking.ts +464 -0
  128. package/src/react-native/index.ts +388 -0
  129. package/src/runtime-entry.ts +55 -0
  130. package/src/scheduling/AnimationFrame.ts +105 -0
  131. package/src/scheduling/IdleCallback.ts +167 -0
  132. package/src/scheduling/index.ts +13 -0
  133. package/src/security/Capabilities.ts +1146 -0
  134. package/src/security/Permissions.ts +392 -0
  135. package/src/security/capability-bits.generated.ts +63 -0
  136. package/src/security/index.ts +16 -0
  137. package/src/sqlite/Database.ts +456 -0
  138. package/src/sqlite/Statement.ts +206 -0
  139. package/src/sqlite/constants.ts +79 -0
  140. package/src/sqlite/errors.ts +25 -0
  141. package/src/sqlite/index.ts +34 -0
  142. package/src/sqlite/module.js +438 -0
  143. package/src/storage/Storage.ts +291 -0
  144. package/src/storage/StorageManager.ts +91 -0
  145. package/src/storage/index.ts +3 -0
  146. package/src/stream-compat.ts +47 -0
  147. package/src/streams/ReadableStream.ts +4131 -0
  148. package/src/streams/TransformStream.ts +375 -0
  149. package/src/streams/WritableStream.ts +866 -0
  150. package/src/streams/index.ts +41 -0
  151. package/src/timers/Timers.ts +296 -0
  152. package/src/timers/index.ts +11 -0
  153. package/src/url/URL.ts +656 -0
  154. package/src/url/URLPattern.ts +850 -0
  155. package/src/url/URLSearchParams.ts +244 -0
  156. package/src/url/index.ts +9 -0
  157. package/src/websocket/WebSocket.ts +770 -0
  158. package/src/websocket/WebSocketError.ts +52 -0
  159. package/src/websocket/WebSocketStream.ts +628 -0
  160. package/src/websocket/index.ts +7 -0
  161. package/src/window/index.ts +872 -0
@@ -0,0 +1,1059 @@
1
+ import { CapabilityDeniedError, DOMException } from '../events/DOMException.js';
2
+
3
+ const FOREGROUND_PERMISSION = 'device:location:whenInUse';
4
+
5
+ type LocationPermissionOperation =
6
+ | 'getCurrentPosition'
7
+ | 'watchPosition'
8
+ | 'clearWatch';
9
+
10
+ const LOCATION_PERMISSIONS: Readonly<Record<LocationPermissionOperation, readonly string[]>> =
11
+ Object.freeze({
12
+ getCurrentPosition: Object.freeze([FOREGROUND_PERMISSION]),
13
+ watchPosition: Object.freeze([FOREGROUND_PERMISSION]),
14
+ clearWatch: Object.freeze([]),
15
+ });
16
+
17
+ const UNSUPPORTED_MESSAGE = 'Location is not supported on this platform';
18
+
19
+ type LocationFeature =
20
+ | 'getCurrentPosition'
21
+ | 'watchPosition'
22
+ | 'highAccuracy'
23
+ | 'altitude';
24
+
25
+ type LegacyPermissionStatus =
26
+ | 'notDetermined'
27
+ | 'denied'
28
+ | 'authorizedWhenInUse'
29
+ | 'authorizedAlways'
30
+ | 'restricted';
31
+
32
+ type LegacyLocationAccuracy =
33
+ | 'best'
34
+ | 'nearestTenMeters'
35
+ | 'hundredMeters'
36
+ | 'kilometer'
37
+ | 'threeKilometers';
38
+
39
+ interface LegacyLocationResult {
40
+ latitude: number;
41
+ longitude: number;
42
+ altitude: number;
43
+ horizontalAccuracy: number;
44
+ verticalAccuracy: number;
45
+ timestamp: number;
46
+ }
47
+
48
+ interface LegacyLocationModule {
49
+ requestPermission(level: 'whenInUse' | 'always'): Promise<{ status: LegacyPermissionStatus }>;
50
+ getCurrentLocation(
51
+ accuracy: LegacyLocationAccuracy,
52
+ timeoutMs?: number,
53
+ ): Promise<LegacyLocationResult>;
54
+ startUpdates(accuracy: LegacyLocationAccuracy, distanceFilter?: number): void;
55
+ stopUpdates(): void;
56
+ getPermissionStatus(): LegacyPermissionStatus;
57
+ isLocationServicesEnabled?(): boolean;
58
+ onLocationUpdate(callback: (location: LegacyLocationResult) => void): () => void;
59
+ onError(callback: (error: { code: string; message: string }) => void): () => void;
60
+ }
61
+
62
+ interface HostGeolocationLike {
63
+ clearWatch(watchId: number): void;
64
+ getCurrentPosition(
65
+ success: PositionCallback,
66
+ error?: PositionErrorCallback | null,
67
+ options?: PositionOptions,
68
+ ): void;
69
+ watchPosition(
70
+ success: PositionCallback,
71
+ error?: PositionErrorCallback | null,
72
+ options?: PositionOptions,
73
+ ): number;
74
+ }
75
+
76
+ interface HostNavigatorLike {
77
+ geolocation?: HostGeolocationLike;
78
+ permissions?: {
79
+ query(descriptor: { name: string }): Promise<{ state?: string }>;
80
+ };
81
+ }
82
+
83
+ interface LocationBackend {
84
+ readonly features: ReadonlySet<LocationFeature>;
85
+
86
+ getCurrentPosition(options?: PositionOptions): Promise<GeolocationPosition>;
87
+ getCurrentPositionGlobal(
88
+ success: PositionCallback,
89
+ error?: PositionErrorCallback | null,
90
+ options?: PositionOptions,
91
+ ): void;
92
+ watchPosition(
93
+ success: PositionCallback,
94
+ error?: PositionErrorCallback | null,
95
+ options?: PositionOptions,
96
+ ): number;
97
+ clearWatch(watchId: number): void;
98
+ }
99
+
100
+ declare global {
101
+ var __exactHostNavigator: HostNavigatorLike | undefined;
102
+ var __exactAndroidLocation:
103
+ | {
104
+ getPermissionStatus?: () => LegacyPermissionStatus;
105
+ getCurrentLocation?: (
106
+ accuracy: LegacyLocationAccuracy,
107
+ timeoutMs?: number,
108
+ ) => LegacyLocationResult;
109
+ isLocationServicesEnabled?: () => boolean;
110
+ }
111
+ | undefined;
112
+ }
113
+
114
+ export type LocationPermissionState = 'granted' | 'denied' | 'prompt' | 'unavailable';
115
+
116
+ export interface LocationPermissionDetails {
117
+ state: LocationPermissionState;
118
+ canRequestAgain: boolean | null;
119
+ settingsUrl?: string;
120
+ platformDetail?: Record<string, unknown>;
121
+ }
122
+
123
+ export class ExactGeolocationPositionError implements GeolocationPositionError, Error {
124
+ declare static readonly PERMISSION_DENIED: 1;
125
+ declare static readonly POSITION_UNAVAILABLE: 2;
126
+ declare static readonly TIMEOUT: 3;
127
+
128
+ readonly PERMISSION_DENIED = ExactGeolocationPositionError.PERMISSION_DENIED;
129
+ readonly POSITION_UNAVAILABLE = ExactGeolocationPositionError.POSITION_UNAVAILABLE;
130
+ readonly TIMEOUT = ExactGeolocationPositionError.TIMEOUT;
131
+
132
+ readonly name = 'GeolocationPositionError';
133
+ readonly message: string;
134
+ readonly code: 1 | 2 | 3;
135
+ stack?: string;
136
+
137
+ constructor(code: 1 | 2 | 3, message?: string) {
138
+ this.message = message ?? defaultErrorMessage(code);
139
+ this.code = code;
140
+ this.stack = new Error(this.message).stack;
141
+ }
142
+ }
143
+
144
+ Object.setPrototypeOf(ExactGeolocationPositionError.prototype, Error.prototype);
145
+ Object.setPrototypeOf(ExactGeolocationPositionError, Error);
146
+ Object.defineProperties(ExactGeolocationPositionError, {
147
+ PERMISSION_DENIED: { value: 1, enumerable: true, configurable: true },
148
+ POSITION_UNAVAILABLE: { value: 2, enumerable: true, configurable: true },
149
+ TIMEOUT: { value: 3, enumerable: true, configurable: true },
150
+ });
151
+
152
+ class UnsupportedLocationBackend implements LocationBackend {
153
+ readonly features: ReadonlySet<LocationFeature> = new Set();
154
+
155
+ async getCurrentPosition(): Promise<GeolocationPosition> {
156
+ throw new DOMException(UNSUPPORTED_MESSAGE, 'NotSupportedError');
157
+ }
158
+
159
+ getCurrentPositionGlobal(
160
+ _success: PositionCallback,
161
+ error?: PositionErrorCallback | null,
162
+ ): void {
163
+ queueMicrotask(() => {
164
+ error?.(new ExactGeolocationPositionError(ExactGeolocationPositionError.POSITION_UNAVAILABLE));
165
+ });
166
+ }
167
+
168
+ watchPosition(
169
+ _success: PositionCallback,
170
+ error?: PositionErrorCallback | null,
171
+ ): number {
172
+ const watchId = nextUnsupportedWatchId++;
173
+ queueMicrotask(() => {
174
+ error?.(new ExactGeolocationPositionError(ExactGeolocationPositionError.POSITION_UNAVAILABLE));
175
+ });
176
+ return watchId;
177
+ }
178
+
179
+ clearWatch(_watchId: number): void {}
180
+ }
181
+
182
+ class BrowserLocationBackend implements LocationBackend {
183
+ readonly features: ReadonlySet<LocationFeature> = new Set([
184
+ 'getCurrentPosition',
185
+ 'watchPosition',
186
+ ]);
187
+
188
+ private readonly geolocation: HostGeolocationLike;
189
+
190
+ constructor(geolocation: HostGeolocationLike) {
191
+ this.geolocation = geolocation;
192
+ }
193
+
194
+ getCurrentPosition(options?: PositionOptions): Promise<GeolocationPosition> {
195
+ return new Promise((resolve, reject) => {
196
+ this.geolocation.getCurrentPosition(
197
+ (position) => resolve(position),
198
+ (error) => reject(mapModuleError(error)),
199
+ options,
200
+ );
201
+ });
202
+ }
203
+
204
+ getCurrentPositionGlobal(
205
+ success: PositionCallback,
206
+ error?: PositionErrorCallback | null,
207
+ options?: PositionOptions,
208
+ ): void {
209
+ this.geolocation.getCurrentPosition(
210
+ success,
211
+ error
212
+ ? (positionError) => {
213
+ error(normalizePositionError(positionError));
214
+ }
215
+ : undefined,
216
+ options,
217
+ );
218
+ }
219
+
220
+ watchPosition(
221
+ success: PositionCallback,
222
+ error?: PositionErrorCallback | null,
223
+ options?: PositionOptions,
224
+ ): number {
225
+ return this.geolocation.watchPosition(
226
+ success,
227
+ error
228
+ ? (positionError) => {
229
+ error(normalizePositionError(positionError));
230
+ }
231
+ : undefined,
232
+ options,
233
+ );
234
+ }
235
+
236
+ clearWatch(watchId: number): void {
237
+ this.geolocation.clearWatch(watchId);
238
+ }
239
+ }
240
+
241
+ class NativeLocationBackend implements LocationBackend {
242
+ readonly features: ReadonlySet<LocationFeature> = new Set([
243
+ 'getCurrentPosition',
244
+ 'watchPosition',
245
+ 'highAccuracy',
246
+ 'altitude',
247
+ ]);
248
+
249
+ private modulePromise: Promise<LegacyLocationModule> | null = null;
250
+ private nextWatchId = 1;
251
+ private watchers = new Map<number, NativeWatchRegistration>();
252
+ private unsubscribeLocationUpdates: (() => void) | null = null;
253
+ private unsubscribeErrors: (() => void) | null = null;
254
+ private activeAccuracy: LegacyLocationAccuracy | null = null;
255
+ private activeDistanceFilter: number | null = null;
256
+ private updatesStarted = false;
257
+
258
+ async getCurrentPosition(options?: PositionOptions): Promise<GeolocationPosition> {
259
+ const module = await this.loadModule();
260
+ await ensureNativePermission(module);
261
+
262
+ try {
263
+ const location = await withTimeout(
264
+ module.getCurrentLocation(mapAccuracy(options), options?.timeout),
265
+ options?.timeout,
266
+ () =>
267
+ new ExactGeolocationPositionError(
268
+ ExactGeolocationPositionError.TIMEOUT,
269
+ 'Location request timed out',
270
+ ),
271
+ );
272
+ return toGeolocationPosition(location);
273
+ } catch (error) {
274
+ throw mapNativeModuleError(error);
275
+ }
276
+ }
277
+
278
+ getCurrentPositionGlobal(
279
+ success: PositionCallback,
280
+ error?: PositionErrorCallback | null,
281
+ options?: PositionOptions,
282
+ ): void {
283
+ void this.getCurrentPosition(options)
284
+ .then((position) => {
285
+ success(position);
286
+ })
287
+ .catch((err) => {
288
+ error?.(toGlobalPositionError(err));
289
+ });
290
+ }
291
+
292
+ watchPosition(
293
+ success: PositionCallback,
294
+ error?: PositionErrorCallback | null,
295
+ options?: PositionOptions,
296
+ ): number {
297
+ const watchId = this.nextWatchId++;
298
+ this.watchers.set(watchId, { success, error, options });
299
+ void this.startWatch(watchId);
300
+ return watchId;
301
+ }
302
+
303
+ clearWatch(watchId: number): void {
304
+ this.watchers.delete(watchId);
305
+ void this.refreshUpdates();
306
+ }
307
+
308
+ private async startWatch(watchId: number): Promise<void> {
309
+ const registration = this.watchers.get(watchId);
310
+ if (!registration) return;
311
+
312
+ try {
313
+ const module = await this.loadModule();
314
+ await ensureNativePermission(module);
315
+ if (!this.watchers.has(watchId)) return;
316
+
317
+ this.ensureSubscriptions(module);
318
+ await this.refreshUpdates();
319
+ } catch (error) {
320
+ if (!this.watchers.has(watchId)) return;
321
+ this.watchers.delete(watchId);
322
+ registration.error?.(toGlobalPositionError(error));
323
+ void this.refreshUpdates();
324
+ }
325
+ }
326
+
327
+ private ensureSubscriptions(module: LegacyLocationModule): void {
328
+ if (!this.unsubscribeLocationUpdates) {
329
+ this.unsubscribeLocationUpdates = module.onLocationUpdate((location) => {
330
+ const position = toGeolocationPosition(location);
331
+ for (const watcher of this.watchers.values()) {
332
+ watcher.success(position);
333
+ }
334
+ });
335
+ }
336
+
337
+ if (!this.unsubscribeErrors) {
338
+ this.unsubscribeErrors = module.onError((nativeError) => {
339
+ const positionError = nativeErrorToPositionError(nativeError);
340
+ const shouldTerminate = positionError.code === ExactGeolocationPositionError.PERMISSION_DENIED;
341
+
342
+ for (const watcher of this.watchers.values()) {
343
+ watcher.error?.(positionError);
344
+ }
345
+
346
+ if (shouldTerminate) {
347
+ this.watchers.clear();
348
+ void this.refreshUpdates();
349
+ }
350
+ });
351
+ }
352
+ }
353
+
354
+ private async refreshUpdates(): Promise<void> {
355
+ const module = await this.loadModule().catch(() => null);
356
+ if (!module) return;
357
+
358
+ if (this.watchers.size === 0) {
359
+ if (this.updatesStarted) {
360
+ module.stopUpdates();
361
+ this.updatesStarted = false;
362
+ this.activeAccuracy = null;
363
+ this.activeDistanceFilter = null;
364
+ }
365
+ return;
366
+ }
367
+
368
+ const desiredAccuracy = computeWatchAccuracy(this.watchers.values());
369
+ const desiredDistanceFilter = computeDistanceFilter(this.watchers.values());
370
+
371
+ if (
372
+ this.updatesStarted &&
373
+ this.activeAccuracy === desiredAccuracy &&
374
+ this.activeDistanceFilter === desiredDistanceFilter
375
+ ) {
376
+ return;
377
+ }
378
+
379
+ if (this.updatesStarted) {
380
+ module.stopUpdates();
381
+ }
382
+
383
+ module.startUpdates(desiredAccuracy, desiredDistanceFilter);
384
+ this.updatesStarted = true;
385
+ this.activeAccuracy = desiredAccuracy;
386
+ this.activeDistanceFilter = desiredDistanceFilter;
387
+ }
388
+
389
+ private loadModule(): Promise<LegacyLocationModule> {
390
+ if (!this.modulePromise) {
391
+ this.modulePromise = loadLegacyLocationModule().catch((error: unknown) => {
392
+ this.modulePromise = null;
393
+ throw error;
394
+ });
395
+ }
396
+
397
+ return this.modulePromise;
398
+ }
399
+ }
400
+
401
+ interface NativeWatchRegistration {
402
+ error?: PositionErrorCallback | null;
403
+ options?: PositionOptions;
404
+ success: PositionCallback;
405
+ }
406
+
407
+ function defaultErrorMessage(code: 1 | 2 | 3): string {
408
+ switch (code) {
409
+ case ExactGeolocationPositionError.PERMISSION_DENIED:
410
+ return 'Location permission denied';
411
+ case ExactGeolocationPositionError.TIMEOUT:
412
+ return 'Location request timed out';
413
+ case ExactGeolocationPositionError.POSITION_UNAVAILABLE:
414
+ default:
415
+ return 'Location is unavailable';
416
+ }
417
+ }
418
+
419
+ function toGeolocationPosition(location: LegacyLocationResult): GeolocationPosition {
420
+ const coords = {
421
+ latitude: location.latitude,
422
+ longitude: location.longitude,
423
+ accuracy: location.horizontalAccuracy,
424
+ altitude: Number.isFinite(location.altitude) ? location.altitude : null,
425
+ altitudeAccuracy: Number.isFinite(location.verticalAccuracy)
426
+ ? location.verticalAccuracy
427
+ : null,
428
+ heading: null,
429
+ speed: null,
430
+ toJSON(): Omit<GeolocationCoordinates, 'toJSON'> {
431
+ return {
432
+ latitude: this.latitude,
433
+ longitude: this.longitude,
434
+ accuracy: this.accuracy,
435
+ altitude: this.altitude,
436
+ altitudeAccuracy: this.altitudeAccuracy,
437
+ heading: this.heading,
438
+ speed: this.speed,
439
+ };
440
+ },
441
+ } satisfies GeolocationCoordinates;
442
+
443
+ return {
444
+ coords,
445
+ timestamp: location.timestamp,
446
+ toJSON(): Omit<GeolocationPosition, 'toJSON'> {
447
+ return {
448
+ coords: this.coords,
449
+ timestamp: this.timestamp,
450
+ };
451
+ },
452
+ } satisfies GeolocationPosition;
453
+ }
454
+
455
+ function normalizePositionError(
456
+ error: GeolocationPositionError,
457
+ ): ExactGeolocationPositionError {
458
+ if (error instanceof ExactGeolocationPositionError) {
459
+ return error;
460
+ }
461
+
462
+ return new ExactGeolocationPositionError(
463
+ normalizePositionErrorCode(error?.code),
464
+ error?.message,
465
+ );
466
+ }
467
+
468
+ function normalizePositionErrorCode(code: number | undefined): 1 | 2 | 3 {
469
+ if (code === ExactGeolocationPositionError.PERMISSION_DENIED) {
470
+ return ExactGeolocationPositionError.PERMISSION_DENIED;
471
+ }
472
+ if (code === ExactGeolocationPositionError.TIMEOUT) {
473
+ return ExactGeolocationPositionError.TIMEOUT;
474
+ }
475
+ return ExactGeolocationPositionError.POSITION_UNAVAILABLE;
476
+ }
477
+
478
+ function mapModuleError(error: GeolocationPositionError): Error {
479
+ const normalized = normalizePositionError(error);
480
+ if (normalized.code === ExactGeolocationPositionError.PERMISSION_DENIED) {
481
+ return new CapabilityDeniedError(FOREGROUND_PERMISSION, 'os_denied', false);
482
+ }
483
+
484
+ return normalized;
485
+ }
486
+
487
+ function mapNativeModuleError(error: unknown): Error {
488
+ if (error instanceof CapabilityDeniedError) {
489
+ return error;
490
+ }
491
+
492
+ if (error instanceof DOMException && error.name === 'NotSupportedError') {
493
+ return error;
494
+ }
495
+
496
+ if (error instanceof ExactGeolocationPositionError) {
497
+ return error;
498
+ }
499
+
500
+ if (error instanceof Error) {
501
+ const message = error.message.toLowerCase();
502
+ if (message.includes('permission_denied')) {
503
+ return new CapabilityDeniedError(FOREGROUND_PERMISSION, 'os_denied', false);
504
+ }
505
+ if (message.includes('timed out')) {
506
+ return new ExactGeolocationPositionError(
507
+ ExactGeolocationPositionError.TIMEOUT,
508
+ error.message,
509
+ );
510
+ }
511
+ return new ExactGeolocationPositionError(
512
+ ExactGeolocationPositionError.POSITION_UNAVAILABLE,
513
+ error.message,
514
+ );
515
+ }
516
+
517
+ return new ExactGeolocationPositionError(ExactGeolocationPositionError.POSITION_UNAVAILABLE);
518
+ }
519
+
520
+ function toGlobalPositionError(error: unknown): GeolocationPositionError {
521
+ if (error instanceof CapabilityDeniedError) {
522
+ return new ExactGeolocationPositionError(
523
+ ExactGeolocationPositionError.PERMISSION_DENIED,
524
+ error.message,
525
+ );
526
+ }
527
+
528
+ if (error instanceof DOMException && error.name === 'NotSupportedError') {
529
+ return new ExactGeolocationPositionError(
530
+ ExactGeolocationPositionError.POSITION_UNAVAILABLE,
531
+ error.message,
532
+ );
533
+ }
534
+
535
+ if (error instanceof ExactGeolocationPositionError) {
536
+ return error;
537
+ }
538
+
539
+ if (error instanceof Error && error.message.toLowerCase().includes('timed out')) {
540
+ return new ExactGeolocationPositionError(
541
+ ExactGeolocationPositionError.TIMEOUT,
542
+ error.message,
543
+ );
544
+ }
545
+
546
+ return new ExactGeolocationPositionError(
547
+ ExactGeolocationPositionError.POSITION_UNAVAILABLE,
548
+ error instanceof Error ? error.message : undefined,
549
+ );
550
+ }
551
+
552
+ function nativeErrorToPositionError(error: { code?: string; message?: string }): GeolocationPositionError {
553
+ const code = error.code?.toUpperCase();
554
+ if (code === 'PERMISSION_DENIED') {
555
+ return new ExactGeolocationPositionError(
556
+ ExactGeolocationPositionError.PERMISSION_DENIED,
557
+ error.message,
558
+ );
559
+ }
560
+ if (code === 'TIMEOUT') {
561
+ return new ExactGeolocationPositionError(ExactGeolocationPositionError.TIMEOUT, error.message);
562
+ }
563
+ return new ExactGeolocationPositionError(
564
+ ExactGeolocationPositionError.POSITION_UNAVAILABLE,
565
+ error.message,
566
+ );
567
+ }
568
+
569
+ async function ensureNativePermission(module: LegacyLocationModule): Promise<void> {
570
+ const currentStatus = module.getPermissionStatus();
571
+ const immediateError = permissionStatusToError(currentStatus);
572
+ if (immediateError) {
573
+ throw immediateError;
574
+ }
575
+
576
+ if (isAuthorizedStatus(currentStatus)) {
577
+ return;
578
+ }
579
+
580
+ const result = await module.requestPermission('whenInUse');
581
+ const requestError = permissionStatusToError(result.status);
582
+ if (requestError) {
583
+ throw requestError;
584
+ }
585
+
586
+ if (!isAuthorizedStatus(result.status)) {
587
+ throw new CapabilityDeniedError(FOREGROUND_PERMISSION, 'os_denied', false);
588
+ }
589
+ }
590
+
591
+ function permissionStatusToError(status: LegacyPermissionStatus): CapabilityDeniedError | null {
592
+ if (status === 'denied') {
593
+ return new CapabilityDeniedError(FOREGROUND_PERMISSION, 'os_denied', false);
594
+ }
595
+ if (status === 'restricted') {
596
+ return new CapabilityDeniedError(FOREGROUND_PERMISSION, 'os_restricted', false);
597
+ }
598
+ return null;
599
+ }
600
+
601
+ function isAuthorizedStatus(status: LegacyPermissionStatus): boolean {
602
+ return status === 'authorizedWhenInUse' || status === 'authorizedAlways';
603
+ }
604
+
605
+ function mapAccuracy(options?: PositionOptions): LegacyLocationAccuracy {
606
+ return options?.enableHighAccuracy ? 'best' : 'hundredMeters';
607
+ }
608
+
609
+ function computeWatchAccuracy(
610
+ watchers: IterableIterator<NativeWatchRegistration>,
611
+ ): LegacyLocationAccuracy {
612
+ for (const watcher of watchers) {
613
+ if (watcher.options?.enableHighAccuracy) {
614
+ return 'best';
615
+ }
616
+ }
617
+ return 'hundredMeters';
618
+ }
619
+
620
+ function computeDistanceFilter(
621
+ watchers: IterableIterator<NativeWatchRegistration>,
622
+ ): number {
623
+ let desired = 10;
624
+ for (const watcher of watchers) {
625
+ if (typeof watcher.options?.maximumAge === 'number' && watcher.options.maximumAge <= 1000) {
626
+ desired = Math.min(desired, 1);
627
+ }
628
+ }
629
+ return desired;
630
+ }
631
+
632
+ function getHostNavigator(): HostNavigatorLike | undefined {
633
+ const globalValue = globalThis as typeof globalThis & {
634
+ navigator?: HostNavigatorLike & { __exactNavigator?: boolean };
635
+ };
636
+
637
+ const exactHostNavigator = globalThis.__exactHostNavigator as
638
+ | (HostNavigatorLike & { __exactNavigator?: boolean })
639
+ | undefined;
640
+ if (exactHostNavigator && exactHostNavigator.__exactNavigator !== true) {
641
+ return exactHostNavigator;
642
+ }
643
+
644
+ const navigatorValue = globalValue.navigator;
645
+ if (navigatorValue && navigatorValue.__exactNavigator !== true) {
646
+ return navigatorValue;
647
+ }
648
+
649
+ return undefined;
650
+ }
651
+
652
+ function getHostGeolocation(): HostGeolocationLike | null {
653
+ const hostNavigator = getHostNavigator();
654
+ if (!hostNavigator?.geolocation) {
655
+ return null;
656
+ }
657
+ return hostNavigator.geolocation;
658
+ }
659
+
660
+ function getHostPermissionsQuery():
661
+ | ((descriptor: { name: string }) => Promise<{ state?: string }>)
662
+ | null {
663
+ const hostNavigator = getHostNavigator();
664
+ if (typeof hostNavigator?.permissions?.query === 'function') {
665
+ return hostNavigator.permissions.query.bind(hostNavigator.permissions);
666
+ }
667
+
668
+ const navigatorValue = (globalThis as typeof globalThis & {
669
+ navigator?: {
670
+ permissions?: {
671
+ query(descriptor: { name: string }): Promise<{ state?: string }>;
672
+ };
673
+ };
674
+ }).navigator;
675
+
676
+ if (typeof navigatorValue?.permissions?.query === 'function') {
677
+ return navigatorValue.permissions.query.bind(navigatorValue.permissions);
678
+ }
679
+
680
+ return null;
681
+ }
682
+
683
+ function hasNativeLocationBridge(): boolean {
684
+ const bridge = globalThis.__exactAndroidLocation;
685
+ return !!(
686
+ bridge &&
687
+ typeof bridge.getPermissionStatus === 'function' &&
688
+ typeof bridge.getCurrentLocation === 'function'
689
+ );
690
+ }
691
+
692
+ let legacyLocationModulePromise: Promise<LegacyLocationModule> | null = null;
693
+
694
+ function normalizeLegacyPermissionStatus(value: unknown): LegacyPermissionStatus {
695
+ switch (value) {
696
+ case 'authorizedWhenInUse':
697
+ case 'authorizedAlways':
698
+ case 'denied':
699
+ case 'restricted':
700
+ case 'notDetermined':
701
+ return value;
702
+ default:
703
+ return 'denied';
704
+ }
705
+ }
706
+
707
+ function androidLocationError(error: unknown): { code: string; message: string } {
708
+ const message = error instanceof Error ? error.message : String(error ?? 'Android location failed');
709
+ const upper = message.toUpperCase();
710
+ if (upper.includes('PERMISSION_DENIED')) {
711
+ return { code: 'PERMISSION_DENIED', message };
712
+ }
713
+ if (upper.includes('TIMEOUT') || upper.includes('TIMED OUT')) {
714
+ return { code: 'TIMEOUT', message };
715
+ }
716
+ return { code: 'POSITION_UNAVAILABLE', message };
717
+ }
718
+
719
+ class AndroidLocationModule implements LegacyLocationModule {
720
+ private locationListeners = new Set<(location: LegacyLocationResult) => void>();
721
+ private errorListeners = new Set<(error: { code: string; message: string }) => void>();
722
+ private pollTimer: ReturnType<typeof setInterval> | null = null;
723
+ private pollInFlight = false;
724
+ private pollAccuracy: LegacyLocationAccuracy = 'hundredMeters';
725
+ private pollTimeoutMs = 10000;
726
+
727
+ async requestPermission(_level: 'whenInUse' | 'always'): Promise<{ status: LegacyPermissionStatus }> {
728
+ return { status: this.getPermissionStatus() };
729
+ }
730
+
731
+ async getCurrentLocation(
732
+ accuracy: LegacyLocationAccuracy,
733
+ timeoutMs?: number,
734
+ ): Promise<LegacyLocationResult> {
735
+ const bridge = globalThis.__exactAndroidLocation;
736
+ if (!bridge || typeof bridge.getCurrentLocation !== 'function') {
737
+ throw new ExactGeolocationPositionError(
738
+ ExactGeolocationPositionError.POSITION_UNAVAILABLE,
739
+ UNSUPPORTED_MESSAGE,
740
+ );
741
+ }
742
+ return bridge.getCurrentLocation(
743
+ accuracy,
744
+ typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0
745
+ ? timeoutMs
746
+ : 10000,
747
+ );
748
+ }
749
+
750
+ startUpdates(accuracy: LegacyLocationAccuracy, distanceFilter?: number): void {
751
+ this.stopUpdates();
752
+ this.pollAccuracy = accuracy;
753
+ this.pollTimeoutMs = 10000;
754
+ const intervalMs =
755
+ typeof distanceFilter === 'number' && Number.isFinite(distanceFilter) && distanceFilter <= 1
756
+ ? 1000
757
+ : 5000;
758
+
759
+ const poll = () => {
760
+ if (this.pollInFlight || this.locationListeners.size === 0) {
761
+ return;
762
+ }
763
+ this.pollInFlight = true;
764
+ void this.getCurrentLocation(this.pollAccuracy, this.pollTimeoutMs)
765
+ .then((location) => {
766
+ for (const listener of [...this.locationListeners]) {
767
+ listener(location);
768
+ }
769
+ })
770
+ .catch((error) => {
771
+ const nativeError = androidLocationError(error);
772
+ for (const listener of [...this.errorListeners]) {
773
+ listener(nativeError);
774
+ }
775
+ })
776
+ .finally(() => {
777
+ this.pollInFlight = false;
778
+ });
779
+ };
780
+
781
+ poll();
782
+ this.pollTimer = setInterval(poll, intervalMs);
783
+ }
784
+
785
+ stopUpdates(): void {
786
+ if (this.pollTimer != null) {
787
+ clearInterval(this.pollTimer);
788
+ this.pollTimer = null;
789
+ }
790
+ this.pollInFlight = false;
791
+ }
792
+
793
+ getPermissionStatus(): LegacyPermissionStatus {
794
+ return normalizeLegacyPermissionStatus(globalThis.__exactAndroidLocation?.getPermissionStatus?.());
795
+ }
796
+
797
+ isLocationServicesEnabled(): boolean {
798
+ const bridge = globalThis.__exactAndroidLocation;
799
+ return typeof bridge?.isLocationServicesEnabled === 'function'
800
+ ? bridge.isLocationServicesEnabled()
801
+ : true;
802
+ }
803
+
804
+ onLocationUpdate(callback: (location: LegacyLocationResult) => void): () => void {
805
+ this.locationListeners.add(callback);
806
+ return () => {
807
+ this.locationListeners.delete(callback);
808
+ };
809
+ }
810
+
811
+ onError(callback: (error: { code: string; message: string }) => void): () => void {
812
+ this.errorListeners.add(callback);
813
+ return () => {
814
+ this.errorListeners.delete(callback);
815
+ };
816
+ }
817
+ }
818
+
819
+ function loadLegacyLocationModule(): Promise<LegacyLocationModule> {
820
+ legacyLocationModulePromise ??= hasNativeLocationBridge()
821
+ ? Promise.resolve(new AndroidLocationModule())
822
+ : Promise.reject(
823
+ new ExactGeolocationPositionError(
824
+ ExactGeolocationPositionError.POSITION_UNAVAILABLE,
825
+ UNSUPPORTED_MESSAGE,
826
+ ),
827
+ );
828
+
829
+ return legacyLocationModulePromise;
830
+ }
831
+
832
+ export async function getLocationPermissionDetails(): Promise<LocationPermissionDetails> {
833
+ const hostGeolocation = getHostGeolocation();
834
+ if (hostGeolocation) {
835
+ const query = getHostPermissionsQuery();
836
+ if (query) {
837
+ try {
838
+ const result = await query({ name: 'geolocation' });
839
+ const state = result?.state;
840
+ if (state === 'granted') {
841
+ return {
842
+ state: 'granted',
843
+ canRequestAgain: false,
844
+ platformDetail: {
845
+ permissionState: 'granted',
846
+ },
847
+ };
848
+ }
849
+ if (state === 'denied') {
850
+ return {
851
+ state: 'denied',
852
+ canRequestAgain: false,
853
+ platformDetail: {
854
+ permissionState: 'denied',
855
+ },
856
+ };
857
+ }
858
+ if (state === 'prompt') {
859
+ return {
860
+ state: 'prompt',
861
+ canRequestAgain: true,
862
+ platformDetail: {
863
+ permissionState: 'prompt',
864
+ },
865
+ };
866
+ }
867
+ } catch {
868
+ // Fall through to the conservative browser fallback below.
869
+ }
870
+ }
871
+
872
+ return {
873
+ state: 'prompt',
874
+ canRequestAgain: null,
875
+ platformDetail: {
876
+ permissionState: 'unknown',
877
+ },
878
+ };
879
+ }
880
+
881
+ if (hasNativeLocationBridge()) {
882
+ const module = await loadLegacyLocationModule();
883
+ const nativeStatus = module.getPermissionStatus();
884
+ const isLocationServicesEnabled = module.isLocationServicesEnabled?.() ?? true;
885
+
886
+ if (!isLocationServicesEnabled) {
887
+ return {
888
+ state: 'denied',
889
+ canRequestAgain: false,
890
+ settingsUrl: 'app-settings:location',
891
+ platformDetail: {
892
+ nativeStatus,
893
+ isLocationServicesEnabled,
894
+ },
895
+ };
896
+ }
897
+
898
+ switch (nativeStatus) {
899
+ case 'authorizedWhenInUse':
900
+ case 'authorizedAlways':
901
+ return {
902
+ state: 'granted',
903
+ canRequestAgain: false,
904
+ platformDetail: {
905
+ nativeStatus,
906
+ isLocationServicesEnabled,
907
+ },
908
+ };
909
+ case 'denied':
910
+ return {
911
+ state: 'denied',
912
+ canRequestAgain: false,
913
+ settingsUrl: 'app-settings:location',
914
+ platformDetail: {
915
+ nativeStatus,
916
+ isLocationServicesEnabled,
917
+ },
918
+ };
919
+ case 'restricted':
920
+ return {
921
+ state: 'denied',
922
+ canRequestAgain: false,
923
+ settingsUrl: 'app-settings:location',
924
+ platformDetail: {
925
+ nativeStatus,
926
+ isLocationServicesEnabled,
927
+ },
928
+ };
929
+ case 'notDetermined':
930
+ default:
931
+ return {
932
+ state: 'prompt',
933
+ canRequestAgain: true,
934
+ platformDetail: {
935
+ nativeStatus,
936
+ isLocationServicesEnabled,
937
+ },
938
+ };
939
+ }
940
+ }
941
+
942
+ return {
943
+ state: 'unavailable',
944
+ canRequestAgain: false,
945
+ platformDetail: {
946
+ reason: 'location_unavailable',
947
+ },
948
+ };
949
+ }
950
+
951
+ const unsupportedBackend = new UnsupportedLocationBackend();
952
+ let nativeBackend: NativeLocationBackend | null = null;
953
+ let nextUnsupportedWatchId = 1;
954
+
955
+ function getLocationBackend(): LocationBackend {
956
+ const hostGeolocation = getHostGeolocation();
957
+ if (hostGeolocation) {
958
+ return new BrowserLocationBackend(hostGeolocation);
959
+ }
960
+
961
+ if (hasNativeLocationBridge()) {
962
+ if (!nativeBackend) {
963
+ nativeBackend = new NativeLocationBackend();
964
+ }
965
+ return nativeBackend;
966
+ }
967
+
968
+ return unsupportedBackend;
969
+ }
970
+
971
+ function withTimeout<T>(
972
+ promise: Promise<T>,
973
+ timeoutMs: number | undefined,
974
+ createTimeoutError: () => Error,
975
+ ): Promise<T> {
976
+ if (timeoutMs == null || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
977
+ return promise;
978
+ }
979
+
980
+ return new Promise<T>((resolve, reject) => {
981
+ let settled = false;
982
+ const timeoutId = setTimeout(() => {
983
+ if (settled) return;
984
+ settled = true;
985
+ reject(createTimeoutError());
986
+ }, timeoutMs);
987
+
988
+ void promise.then(
989
+ (value) => {
990
+ if (settled) return;
991
+ settled = true;
992
+ clearTimeout(timeoutId);
993
+ resolve(value);
994
+ },
995
+ (error) => {
996
+ if (settled) return;
997
+ settled = true;
998
+ clearTimeout(timeoutId);
999
+ reject(error);
1000
+ },
1001
+ );
1002
+ });
1003
+ }
1004
+
1005
+ class ExactGeolocation implements Geolocation {
1006
+ clearWatch(watchId: number): void {
1007
+ getLocationBackend().clearWatch(watchId);
1008
+ }
1009
+
1010
+ getCurrentPosition(
1011
+ success: PositionCallback,
1012
+ error?: PositionErrorCallback | null,
1013
+ options?: PositionOptions,
1014
+ ): void {
1015
+ getLocationBackend().getCurrentPositionGlobal(success, error, options);
1016
+ }
1017
+
1018
+ watchPosition(
1019
+ success: PositionCallback,
1020
+ error?: PositionErrorCallback | null,
1021
+ options?: PositionOptions,
1022
+ ): number {
1023
+ return getLocationBackend().watchPosition(success, error, options);
1024
+ }
1025
+ }
1026
+
1027
+ class ExactLocationModule {
1028
+ readonly permissions = LOCATION_PERMISSIONS;
1029
+
1030
+ get features(): ReadonlySet<LocationFeature> {
1031
+ return new Set(getLocationBackend().features);
1032
+ }
1033
+
1034
+ getCurrentPosition(options?: PositionOptions): Promise<GeolocationPosition> {
1035
+ return getLocationBackend().getCurrentPosition(options);
1036
+ }
1037
+
1038
+ watchPosition(
1039
+ success: PositionCallback,
1040
+ error?: PositionErrorCallback | null,
1041
+ options?: PositionOptions,
1042
+ ): number {
1043
+ return getLocationBackend().watchPosition(success, error, options);
1044
+ }
1045
+
1046
+ clearWatch(watchId: number): void {
1047
+ getLocationBackend().clearWatch(watchId);
1048
+ }
1049
+ }
1050
+
1051
+ const geolocation = new ExactGeolocation();
1052
+
1053
+ export function getGeolocation(): Geolocation {
1054
+ return geolocation;
1055
+ }
1056
+
1057
+ export const Location = new ExactLocationModule();
1058
+
1059
+ export type ExactLocationPermissions = typeof LOCATION_PERMISSIONS;