From 425a46e4ba9edd9eac1c718df9cb329d9988caee Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2024 19:24:08 +0700 Subject: [PATCH 01/31] Add docs draft --- apps/website/docs/.vitepress/config.mjs | 4 + apps/website/docs/web-api/geolocation.md | 104 +++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 apps/website/docs/web-api/geolocation.md diff --git a/apps/website/docs/.vitepress/config.mjs b/apps/website/docs/.vitepress/config.mjs index 7eb6ae62..22255ff2 100644 --- a/apps/website/docs/.vitepress/config.mjs +++ b/apps/website/docs/.vitepress/config.mjs @@ -104,6 +104,10 @@ export default defineConfig({ text: 'Preferred languages', link: '/web-api/preferred_languages', }, + { + text: 'Geolocation', + link: '/web-api/geolocation', + }, ], }, ]), diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md new file mode 100644 index 00000000..2260abf0 --- /dev/null +++ b/apps/website/docs/web-api/geolocation.md @@ -0,0 +1,104 @@ +--- +title: Geolocation +--- + +# Geolocation + +::: info + +Uses [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) under the hood + +::: + +## Usage + +All you need to do is to create an integration by calling `trackGeolocation` with an integration options: + +- `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). +- `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. +- `options?`: an object with the following properties: + - `maximumAge?`: a positive `number` representing the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to `0`, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to `Infinity` the device must return a cached position regardless of its age. + - `timeout?`: a positive `number` representing the maximum length of time (in milliseconds) the device is allowed to take in order to return a position. The maximum value for this attribute is `Infinity`. + - `enableHighAccuracy?`: a `boolean` that indicates the application would like to receive the best possible results. + +```ts +import { trackGeolocation } from '@withease/web-api'; + +const { $location, $latitude, $longitude, request, watching, reporting } = + trackGeolocation({ + setup: appStarted, + options: { + maximumAge, + timeout, + enableHighAccuracy, + }, + }); +``` + +Returns an object with: + +- `$location` - [_Store_](https://effector.dev/docs/api/effector/store) with the current location in the format `{ latitude, longitude }` +- `$latitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current latitude +- `$longitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current longitude +- `request` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to get the current location +- `watching` - an object with the following properties: + - `start` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to start watching the current location + - `stop` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to stop watching the current location + - `$active` - [_Store_](https://effector.dev/docs/api/effector/store) with `true` if watching is started and `false` if watching is stopped + +### Regional restrictions + +In some countries and regions, the use of geolocation can be restricted. If you are aiming to provide a service in such locations, you use some local providers to get the location of the user. For example, in China, you can use [Baidu](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), [Autonavi](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), or [Tencent](https://lbs.qq.com/webApi/component/componentGuide/componentGeolocation). + +Geolocation integration of `@withease/web-api` allows to use any provider additionally to the default one provided by the browser. To do so, you need to pass an `additionalProviders` option to the `trackGeolocation` function. + +```ts +import { trackGeolocation } from '@withease/web-api'; + +const geo = trackGeolocation({ + /* ... */ + additionalProviders: [ + /* your custom providers */ + ], +}); +``` + +Any provider should conform to the following contract: + +```ts +type CustomProvider = { + getCurrentPosition: () => Promise<{ latitude; longitude }>; +}; +``` + +For example, in case of Baidu, you can write something like this: + +```ts +// Create a Baidu geolocation instance outside of the getCurrentPosition function +// to avoid creating a new instance every time the function is called +const geolocation = new BMap.Geolocation(); + +const baiduProvider = { + async getCurrentPosition() { + // getCurrentPosition function should return a Promise + return new Promise((resolve, reject) => { + geolocation.getCurrentPosition(function (r) { + if (this.getStatus() == BMAP_STATUS_SUCCESS) { + // in case of success, resolve with the coordinates + resolve({ latitude: r.point.lat, longitude: r.point.lng }); + } else { + // otherwise, reject with an error + reject(new Error(this.getStatus())); + } + }); + }); + }, +}; + +const geo = trackGeolocation({ + /* ... */ + additionalProviders: [baiduProvider], +}); +``` + +Array of `additionalProviders` would be used in the order they are passed to the `trackGeolocation` function. The first provider that returns the coordinates would be used. It is used only if the browser [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) is not available or fails. From f71882c71a4c9b49ef93583f582f578d5fa5f573 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2024 19:26:00 +0700 Subject: [PATCH 02/31] mark about options --- apps/website/docs/web-api/geolocation.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 2260abf0..1993d0a1 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -67,7 +67,10 @@ Any provider should conform to the following contract: ```ts type CustomProvider = { - getCurrentPosition: () => Promise<{ latitude; longitude }>; + getCurrentPosition: ( + /* All options would be passed from trackGeolocation call */ + { maximumAge, timeout, enableHighAccuracy } + ) => Promise<{ latitude; longitude }>; }; ``` @@ -79,7 +82,7 @@ For example, in case of Baidu, you can write something like this: const geolocation = new BMap.Geolocation(); const baiduProvider = { - async getCurrentPosition() { + async getCurrentPosition({ maximumAge, timeout, enableHighAccuracy }) { // getCurrentPosition function should return a Promise return new Promise((resolve, reject) => { geolocation.getCurrentPosition(function (r) { From 4afcece07044c7e99233384e8d5ce810a6f733e6 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2024 19:28:00 +0700 Subject: [PATCH 03/31] reporting --- apps/website/docs/web-api/geolocation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 1993d0a1..66b6d09b 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -45,6 +45,8 @@ Returns an object with: - `start` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to start watching the current location - `stop` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to stop watching the current location - `$active` - [_Store_](https://effector.dev/docs/api/effector/store) with `true` if watching is started and `false` if watching is stopped +- `reporting` - an object with the following properties: + - `failed` - [_Event_](https://effector.dev/en/api/effector/event) that fires when the location request fails ### Regional restrictions From 6b0a7e76d87fbebcc848e2e90bc247857ea45783 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2024 19:28:33 +0700 Subject: [PATCH 04/31] No setup --- apps/website/docs/web-api/geolocation.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 66b6d09b..dd989916 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -14,24 +14,18 @@ Uses [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocat All you need to do is to create an integration by calling `trackGeolocation` with an integration options: -- `setup`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be installed, and the integration will be ready to use; it is required because it is better to use [explicit initialization _Event_ in the application](/magazine/explicit_start). -- `teardown?`: after this [_Event_](https://effector.dev/en/api/effector/event/) all listeners will be removed, and the integration will be ready to be destroyed. -- `options?`: an object with the following properties: - - `maximumAge?`: a positive `number` representing the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to `0`, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to `Infinity` the device must return a cached position regardless of its age. - - `timeout?`: a positive `number` representing the maximum length of time (in milliseconds) the device is allowed to take in order to return a position. The maximum value for this attribute is `Infinity`. - - `enableHighAccuracy?`: a `boolean` that indicates the application would like to receive the best possible results. +- `maximumAge?`: a positive `number` representing the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to `0`, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to `Infinity` the device must return a cached position regardless of its age. +- `timeout?`: a positive `number` representing the maximum length of time (in milliseconds) the device is allowed to take in order to return a position. The maximum value for this attribute is `Infinity`. +- `enableHighAccuracy?`: a `boolean` that indicates the application would like to receive the best possible results. ```ts import { trackGeolocation } from '@withease/web-api'; const { $location, $latitude, $longitude, request, watching, reporting } = trackGeolocation({ - setup: appStarted, - options: { - maximumAge, - timeout, - enableHighAccuracy, - }, + maximumAge, + timeout, + enableHighAccuracy, }); ``` From 18948c055439d80db8ec50774b5a235cfd294c53 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2024 19:39:36 +0700 Subject: [PATCH 05/31] Add subtitle --- apps/website/docs/web-api/geolocation.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index dd989916..f16e0b23 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -4,6 +4,8 @@ title: Geolocation # Geolocation +Allows tracking geolocation with [_Events_](https://effector.dev/en/api/effector/event/) and [_Stores_](https://effector.dev/docs/api/effector/store). + ::: info Uses [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) under the hood From f882f886c90ccd47bb53238358eef77c9bd266c0 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 17 Apr 2024 19:46:13 +0700 Subject: [PATCH 06/31] Add `tractGeolocation` integration --- .changeset/dry-cats-wash.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dry-cats-wash.md diff --git a/.changeset/dry-cats-wash.md b/.changeset/dry-cats-wash.md new file mode 100644 index 00000000..4d11b575 --- /dev/null +++ b/.changeset/dry-cats-wash.md @@ -0,0 +1,5 @@ +--- +'@withease/web-api': minor +--- + +Add `tractGeolocation` integration From a28143fb35eab71274bbb782d28583b28d9916a0 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 12:17:42 +0700 Subject: [PATCH 07/31] Change API a bit --- apps/website/docs/web-api/geolocation.md | 54 ++++++++++++------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index f16e0b23..6e4ac184 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -37,10 +37,6 @@ Returns an object with: - `$latitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current latitude - `$longitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current longitude - `request` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to get the current location -- `watching` - an object with the following properties: - - `start` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to start watching the current location - - `stop` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to stop watching the current location - - `$active` - [_Store_](https://effector.dev/docs/api/effector/store) with `true` if watching is started and `false` if watching is stopped - `reporting` - an object with the following properties: - `failed` - [_Event_](https://effector.dev/en/api/effector/event) that fires when the location request fails @@ -64,11 +60,14 @@ const geo = trackGeolocation({ Any provider should conform to the following contract: ```ts -type CustomProvider = { - getCurrentPosition: ( - /* All options would be passed from trackGeolocation call */ - { maximumAge, timeout, enableHighAccuracy } - ) => Promise<{ latitude; longitude }>; +type CustomProvider = ( + /* All options would be passed from trackGeolocation call */ { + maximumAge, + timeout, + enableHighAccuracy, + } +) => { + getCurrentPosition: () => Promise<{ latitude; longitude }>; }; ``` @@ -77,24 +76,27 @@ For example, in case of Baidu, you can write something like this: ```ts // Create a Baidu geolocation instance outside of the getCurrentPosition function // to avoid creating a new instance every time the function is called -const geolocation = new BMap.Geolocation(); - -const baiduProvider = { - async getCurrentPosition({ maximumAge, timeout, enableHighAccuracy }) { - // getCurrentPosition function should return a Promise - return new Promise((resolve, reject) => { - geolocation.getCurrentPosition(function (r) { - if (this.getStatus() == BMAP_STATUS_SUCCESS) { - // in case of success, resolve with the coordinates - resolve({ latitude: r.point.lat, longitude: r.point.lng }); - } else { - // otherwise, reject with an error - reject(new Error(this.getStatus())); - } + +function baiduProvider({ maximumAge, timeout, enableHighAccuracy }) { + const geolocation = new BMap.Geolocation(); + + return { + async getCurrentPosition({ maximumAge, timeout, enableHighAccuracy }) { + // getCurrentPosition function should return a Promise + return new Promise((resolve, reject) => { + geolocation.getCurrentPosition(function (r) { + if (this.getStatus() == BMAP_STATUS_SUCCESS) { + // in case of success, resolve with the coordinates + resolve({ latitude: r.point.lat, longitude: r.point.lng }); + } else { + // otherwise, reject with an error + reject(new Error(this.getStatus())); + } + }); }); - }); - }, -}; + }, + }; +} const geo = trackGeolocation({ /* ... */ From aba6289c005a6e6c905d96ca6f84fcd9c088487c Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 12:23:50 +0700 Subject: [PATCH 08/31] Fix typo --- apps/website/docs/web-api/geolocation.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 6e4ac184..bb250f01 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -74,10 +74,9 @@ type CustomProvider = ( For example, in case of Baidu, you can write something like this: ```ts -// Create a Baidu geolocation instance outside of the getCurrentPosition function -// to avoid creating a new instance every time the function is called - function baiduProvider({ maximumAge, timeout, enableHighAccuracy }) { + // Create a Baidu geolocation instance outside of the getCurrentPosition function + // to avoid creating a new instance every time the function is called const geolocation = new BMap.Geolocation(); return { From e53bf27756d8079db6d38a570019c57d08d8943d Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 12:30:12 +0700 Subject: [PATCH 09/31] Add types --- apps/website/docs/web-api/geolocation.md | 2 +- packages/web-api/src/geolocation.ts | 29 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 packages/web-api/src/geolocation.ts diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index bb250f01..cac3cce9 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -23,7 +23,7 @@ All you need to do is to create an integration by calling `trackGeolocation` wit ```ts import { trackGeolocation } from '@withease/web-api'; -const { $location, $latitude, $longitude, request, watching, reporting } = +const { $location, $latitude, $longitude, request, reporting } = trackGeolocation({ maximumAge, timeout, diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts new file mode 100644 index 00000000..68635cb0 --- /dev/null +++ b/packages/web-api/src/geolocation.ts @@ -0,0 +1,29 @@ +import { Event, EventCallable, Store } from 'effector'; + +type GeolocationParams = { + maximumAge?: number; + timeout?: number; + enableHighAccuracy?: boolean; +}; + +type CustomProvider = (params: GeolocationParams) => { + getCurrentPosition: () => Promise<{ latitude: number; longitude: number }>; +}; + +type Geolocation = { + $location: Store; + $latitude: Store; + $longitude: Store<{ latitude: number; longitude: number } | null>; + request: EventCallable; + reporting: { + failed: Event; + }; +}; + +function trackGeolocation( + params: GeolocationParams & { + additionalProviders?: Array; + } +): Geolocation { + return {} as any; +} From 9a0402ecdf236fca1e84b0b89ece1cf060242a58 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 12:56:47 +0700 Subject: [PATCH 10/31] Add watching back, improve custom providers --- apps/website/docs/web-api/geolocation.md | 112 ++++++++++++++++++++--- packages/web-api/src/geolocation.ts | 5 + 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index cac3cce9..8e7a84d6 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -23,7 +23,7 @@ All you need to do is to create an integration by calling `trackGeolocation` wit ```ts import { trackGeolocation } from '@withease/web-api'; -const { $location, $latitude, $longitude, request, reporting } = +const { $location, $latitude, $longitude, request, reporting, watching } = trackGeolocation({ maximumAge, timeout, @@ -37,6 +37,10 @@ Returns an object with: - `$latitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current latitude - `$longitude` - [_Store_](https://effector.dev/docs/api/effector/store) with the current longitude - `request` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to get the current location +- `watching` - an object with the following properties: + - `start` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to start watching the current location + - `stop` - [_EventCallable_](https://effector.dev/en/api/effector/event/#eventCallable) that has to be called to stop watching the current location + - `$active` - [_Store_](https://effector.dev/docs/api/effector/store) with `true` if watching is started and `false` if watching is stopped - `reporting` - an object with the following properties: - `failed` - [_Event_](https://effector.dev/en/api/effector/event) that fires when the location request fails @@ -60,6 +64,37 @@ const geo = trackGeolocation({ Any provider should conform to the following contract: ```ts +/* This type mimics https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition */ +type CustomGeolocationPosition = { + timestamp: number; + coords: { + latitude: number; + longitude: number; + accuracy?: number; + altitude?: number; + altitudeAccuracy?: number; + heading?: number; + speed?: number; + }; +}; + +/* This type mimics https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError */ +type CustomGeolocationError = { + /* + * You have to map your error codes to the Geolocation API error codes + * In case of unknown error, you are free to skip this field + */ + code?: 'PERMISSION_DENIED' | 'POSITION_UNAVAILABLE' | 'TIMEOUT'; + /* + * You can provide a custom message for the error + */ + message?: string; + /* + * You can provide a raw error object from your provider + */ + raw?: unknown; +}; + type CustomProvider = ( /* All options would be passed from trackGeolocation call */ { maximumAge, @@ -67,7 +102,16 @@ type CustomProvider = ( enableHighAccuracy, } ) => { - getCurrentPosition: () => Promise<{ latitude; longitude }>; + /* This function can throw CustomGeolocationError in case of error */ + getCurrentPosition: () => Promise; + /* + * This function should call successCallback with the position or errorCallback with the error. + * Function should return an Unsubscribe function, which should stop watching the position. + */ + watchPosition?: ( + successCallback: (position: CustomGeolocationPosition) => void, + errorCallback: (error: CustomGeolocationError) => void + ) => Unsubscribe; }; ``` @@ -79,21 +123,59 @@ function baiduProvider({ maximumAge, timeout, enableHighAccuracy }) { // to avoid creating a new instance every time the function is called const geolocation = new BMap.Geolocation(); - return { - async getCurrentPosition({ maximumAge, timeout, enableHighAccuracy }) { - // getCurrentPosition function should return a Promise - return new Promise((resolve, reject) => { - geolocation.getCurrentPosition(function (r) { - if (this.getStatus() == BMAP_STATUS_SUCCESS) { - // in case of success, resolve with the coordinates - resolve({ latitude: r.point.lat, longitude: r.point.lng }); - } else { - // otherwise, reject with an error - reject(new Error(this.getStatus())); + const getCurrentPosition = ({ maximumAge, timeout, enableHighAccuracy }) => { + // getCurrentPosition function should return a Promise + return new Promise((resolve, reject) => { + geolocation.getCurrentPosition(function (r) { + if (this.getStatus() === BMAP_STATUS_SUCCESS) { + // in case of success, resolve with the result + resolve({ + timestamp: Date.now(), + coords: { latitude: r.point.lat, longitude: r.point.lng }, + }); + } else { + // map Baidu error codes to the Geolocation API error codes + let code; + switch (this.getStatus()) { + case BMAP_STATUS_PERMISSION_DENIED: + code = 'PERMISSION_DENIED'; + break; + case BMAP_STATUS_SERVICE_UNAVAILABLE: + code = 'POSITION_UNAVAILABLE'; + break; + case BMAP_STATUS_TIMEOUT: + code = 'TIMEOUT'; + break; } - }); + + // reject with the error object + reject({ code, raw: this.getStatus() }); + } }); - }, + }); + }; + + /* + * Bailu does not support watching the position + * so, we have to write an imitation of the watchPosition function + */ + const watchPosition = (successCallback, errorCallback) => { + const timerId = setInterval(async () => { + try { + const position = await getCurrentPosition(); + + successCallback(position); + } catch (error) { + errorCallback(error); + } + }, 1_000); + + return () => clearInterval(timerId); + }; + + return { + getCurrentPosition, + watchPosition, }; } diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 68635cb0..e0bb2193 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -15,6 +15,11 @@ type Geolocation = { $latitude: Store; $longitude: Store<{ latitude: number; longitude: number } | null>; request: EventCallable; + watching: { + start: EventCallable; + stop: EventCallable; + $active: Store; + }; reporting: { failed: Event; }; From 574c4ca4b2df449e531b8e900c433df248c4b569 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:00:14 +0700 Subject: [PATCH 11/31] Add a note about priority --- apps/website/docs/web-api/geolocation.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 8e7a84d6..aae4e58d 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -50,6 +50,10 @@ In some countries and regions, the use of geolocation can be restricted. If you Geolocation integration of `@withease/web-api` allows to use any provider additionally to the default one provided by the browser. To do so, you need to pass an `additionalProviders` option to the `trackGeolocation` function. +::: tip +`additionalProviders` will be used only if the browser Geolocation API is not available or fails. Otherwise, the browser Geolocation API will be used. +::: + ```ts import { trackGeolocation } from '@withease/web-api'; From 19030c032f5a09c5976085bfabb7e13e719c34ed Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:02:04 +0700 Subject: [PATCH 12/31] hide Baidu example --- apps/website/docs/web-api/geolocation.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index aae4e58d..5ff914a0 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -119,6 +119,8 @@ type CustomProvider = ( }; ``` +::: details Baidu example + For example, in case of Baidu, you can write something like this: ```ts @@ -189,4 +191,6 @@ const geo = trackGeolocation({ }); ``` +::: + Array of `additionalProviders` would be used in the order they are passed to the `trackGeolocation` function. The first provider that returns the coordinates would be used. It is used only if the browser [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) is not available or fails. From c82639f2e704507637dce3cd210a7f0523668152 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:13:40 +0700 Subject: [PATCH 13/31] Allow to exclude browser geolocation from providers --- apps/website/docs/web-api/geolocation.md | 37 +++++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 5ff914a0..c2c2d160 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -1,5 +1,7 @@ --- title: Geolocation + +outline: [2, 3] --- # Geolocation @@ -48,18 +50,16 @@ Returns an object with: In some countries and regions, the use of geolocation can be restricted. If you are aiming to provide a service in such locations, you use some local providers to get the location of the user. For example, in China, you can use [Baidu](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), [Autonavi](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), or [Tencent](https://lbs.qq.com/webApi/component/componentGuide/componentGeolocation). -Geolocation integration of `@withease/web-api` allows to use any provider additionally to the default one provided by the browser. To do so, you need to pass an `additionalProviders` option to the `trackGeolocation` function. - -::: tip -`additionalProviders` will be used only if the browser Geolocation API is not available or fails. Otherwise, the browser Geolocation API will be used. -::: +Geolocation integration of `@withease/web-api` allows to use any provider additionally to the default one provided by the browser. To do so, you need to pass an `providers` option to the `trackGeolocation` function. ```ts import { trackGeolocation } from '@withease/web-api'; const geo = trackGeolocation({ /* ... */ - additionalProviders: [ + providers: [ + /* default browser Geolocation API */ + trackGeolocation.browserProvider, /* your custom providers */ ], }); @@ -187,10 +187,31 @@ function baiduProvider({ maximumAge, timeout, enableHighAccuracy }) { const geo = trackGeolocation({ /* ... */ - additionalProviders: [baiduProvider], + providers: [ + /* default browser Geolocation API */ + trackGeolocation.browserProvider, + /* Baidu provider */ + baiduProvider, + ], }); ``` ::: -Array of `additionalProviders` would be used in the order they are passed to the `trackGeolocation` function. The first provider that returns the coordinates would be used. It is used only if the browser [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API) is not available or fails. +Array of `providers` would be used in the order they are passed to the `trackGeolocation` function. The first provider that returns the coordinates would be used. + +### React Native + +In case of React Native, it is recommended to use the [`@react-native-community/geolocation`](https://github.com/michalchudziak/react-native-geolocation) package and do not use `navigator.geolocation` directly. You can easily achieve this by excluding `trackGeolocation.browserProvider` from the list of providers. + +```ts +import ReactNativeGeolocation from '@react-native-community/geolocation'; + +const geo = trackGeolocation({ + /* ... */ + providers: [ + trackGeolocation.browserProvider, // [!code --] + ReactNativeGeolocation, // [!code ++] + ], +}); +``` \ No newline at end of file From 6af1cf6da12772682b113481873f59c034f8f482 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:20:32 +0700 Subject: [PATCH 14/31] update types --- apps/website/docs/web-api/geolocation.md | 4 +-- packages/web-api/src/geolocation.ts | 39 ++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index c2c2d160..7f018922 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -112,7 +112,7 @@ type CustomProvider = ( * This function should call successCallback with the position or errorCallback with the error. * Function should return an Unsubscribe function, which should stop watching the position. */ - watchPosition?: ( + watchPosition: ( successCallback: (position: CustomGeolocationPosition) => void, errorCallback: (error: CustomGeolocationError) => void ) => Unsubscribe; @@ -214,4 +214,4 @@ const geo = trackGeolocation({ ReactNativeGeolocation, // [!code ++] ], }); -``` \ No newline at end of file +``` diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index e0bb2193..ad34167b 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -6,8 +6,43 @@ type GeolocationParams = { enableHighAccuracy?: boolean; }; +/** + * This type mimics GeolocationPostion + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition} + */ +type CustomGeolocationPosition = { + timestamp: number; + coords: { + latitude: number; + longitude: number; + accuracy?: number; + altitude?: number; + altitudeAccuracy?: number; + heading?: number; + speed?: number; + }; +}; + +/** + * This type mimics GeolocationPositionError + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError} + */ +type CustomGeolocationError = { + code?: 'PERMISSION_DENIED' | 'POSITION_UNAVAILABLE' | 'TIMEOUT'; + message?: string; + raw?: unknown; +}; + +type Unsubscribe = () => void; + type CustomProvider = (params: GeolocationParams) => { - getCurrentPosition: () => Promise<{ latitude: number; longitude: number }>; + getCurrentPosition: () => Promise; + watchPosition?: ( + successCallback: (position: CustomGeolocationPosition) => void, + errorCallback: (error: CustomGeolocationError) => void + ) => Unsubscribe; }; type Geolocation = { @@ -27,7 +62,7 @@ type Geolocation = { function trackGeolocation( params: GeolocationParams & { - additionalProviders?: Array; + providers?: Array; } ): Geolocation { return {} as any; From 3a5d29d66931d519701f9c2b6f8ec1a713c88b51 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:22:29 +0700 Subject: [PATCH 15/31] Add browserProvider --- packages/web-api/src/geolocation.ts | 6 +++++- packages/web-api/src/index.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index ad34167b..f8520140 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -60,10 +60,14 @@ type Geolocation = { }; }; -function trackGeolocation( +const BrowserProvider = Symbol('BrowserProvider'); + +export function trackGeolocation( params: GeolocationParams & { providers?: Array; } ): Geolocation { return {} as any; } + +trackGeolocation.browserProvider = BrowserProvider; diff --git a/packages/web-api/src/index.ts b/packages/web-api/src/index.ts index 9d6c3e4c..cd17cc34 100644 --- a/packages/web-api/src/index.ts +++ b/packages/web-api/src/index.ts @@ -3,3 +3,4 @@ export { trackPageVisibility } from './page_visibility'; export { trackNetworkStatus } from './network_status'; export { trackMediaQuery } from './media_query'; export { trackPreferredLanguages } from './preferred_languages'; +export { trackGeolocation } from './geolocation'; From 7a48f6b2692c3ccd8132f718a60e00747e13895c Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:22:54 +0700 Subject: [PATCH 16/31] Fix typo --- .changeset/dry-cats-wash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/dry-cats-wash.md b/.changeset/dry-cats-wash.md index 4d11b575..c4d7ed78 100644 --- a/.changeset/dry-cats-wash.md +++ b/.changeset/dry-cats-wash.md @@ -2,4 +2,4 @@ '@withease/web-api': minor --- -Add `tractGeolocation` integration +Add `trackGeolocation` integration From a79d608cefc98c6e5e637fef26f698016fd8d317 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:40:57 +0700 Subject: [PATCH 17/31] Add real units --- packages/web-api/src/geolocation.ts | 60 ++++++++++++++++++++++++++--- packages/web-api/src/shared.ts | 12 +++++- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index f8520140..c7160999 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -1,4 +1,13 @@ -import { Event, EventCallable, Store } from 'effector'; +import { + type Event, + type EventCallable, + type Store, + combine, + createEvent, + createStore, +} from 'effector'; + +import { readonly } from './shared'; type GeolocationParams = { maximumAge?: number; @@ -46,9 +55,9 @@ type CustomProvider = (params: GeolocationParams) => { }; type Geolocation = { - $location: Store; + $location: Store<{ latitude: number; longitude: number } | null>; $latitude: Store; - $longitude: Store<{ latitude: number; longitude: number } | null>; + $longitude: Store; request: EventCallable; watching: { start: EventCallable; @@ -56,7 +65,7 @@ type Geolocation = { $active: Store; }; reporting: { - failed: Event; + failed: Event; }; }; @@ -64,10 +73,49 @@ const BrowserProvider = Symbol('BrowserProvider'); export function trackGeolocation( params: GeolocationParams & { - providers?: Array; + providers?: Array; } ): Geolocation { - return {} as any; + // In case of no providers, we will use the default one only + const providres = params.providers ?? [BrowserProvider]; + + const $location = createStore<{ + latitude: number; + longitude: number; + } | null>(null); + + const $longitude = combine( + $location, + (location) => location?.longitude ?? null + ); + + const $latitude = combine( + $location, + (location) => location?.latitude ?? null + ); + + const request = createEvent(); + + const startWatching = createEvent(); + const stopWatching = createEvent(); + const $watchingActive = createStore(false); + + const failed = createEvent(); + + return { + $location: readonly($location), + $longitude, + $latitude, + request, + watching: { + start: startWatching, + stop: stopWatching, + $active: readonly($watchingActive), + }, + reporting: { + failed: readonly(failed), + }, + }; } trackGeolocation.browserProvider = BrowserProvider; diff --git a/packages/web-api/src/shared.ts b/packages/web-api/src/shared.ts index e9eca7fa..f49213ca 100644 --- a/packages/web-api/src/shared.ts +++ b/packages/web-api/src/shared.ts @@ -1,5 +1,8 @@ import { - Event, + type Event, + type Store, + type EventCallable, + type StoreWritable, attach, createEffect, createEvent, @@ -104,3 +107,10 @@ export function setupListener( return event; } + +export function readonly(unit: StoreWritable): Store; +export function readonly(unit: EventCallable): Event; + +export function readonly(unit: any) { + return unit.map((v: any) => v); +} From 02b964b7ac0da0d149f7791bb58db4fb90201f7e Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Wed, 24 Apr 2024 13:54:40 +0700 Subject: [PATCH 18/31] Add basic relations --- packages/web-api/src/geolocation.ts | 68 ++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index c7160999..0f2c9d4a 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -5,6 +5,10 @@ import { combine, createEvent, createStore, + createEffect, + sample, + restore, + attach, } from 'effector'; import { readonly } from './shared'; @@ -65,7 +69,7 @@ type Geolocation = { $active: Store; }; reporting: { - failed: Event; + failed: Event; }; }; @@ -79,6 +83,8 @@ export function trackGeolocation( // In case of no providers, we will use the default one only const providres = params.providers ?? [BrowserProvider]; + // -- units + const $location = createStore<{ latitude: number; longitude: number; @@ -100,7 +106,65 @@ export function trackGeolocation( const stopWatching = createEvent(); const $watchingActive = createStore(false); - const failed = createEvent(); + const failed = createEvent< + CustomGeolocationError | globalThis.GeolocationPositionError + >(); + + // -- shared logic + + const newPosition = createEvent< + CustomGeolocationPosition | globalThis.GeolocationPosition + >(); + + sample({ + clock: newPosition, + fn: (r) => ({ latitude: r.coords.latitude, longitude: r.coords.longitude }), + target: $location, + }); + + // -- get current position + + const getCurrentPositionFx = createEffect< + void, + CustomGeolocationPosition | globalThis.GeolocationPosition, + CustomGeolocationError | globalThis.GeolocationPositionError + >(() => { + // TODO: real code + throw { code: 'POSITION_UNAVAILABLE', message: 'Not implemented' }; + }); + + sample({ clock: request, target: getCurrentPositionFx }); + sample({ + clock: getCurrentPositionFx.doneData, + target: newPosition, + }); + sample({ clock: getCurrentPositionFx.failData, target: failed }); + + // -- watch position + + const saveUnsubscribe = createEvent(); + const $unsubscribe = restore(saveUnsubscribe, null); + + const watchPositionFx = createEffect(() => { + // TODO: real code + newPosition({} as any); + failed({} as any); + saveUnsubscribe(() => null); + }); + + const unwatchPositionFx = attach({ + source: $unsubscribe, + effect(unwatch) { + unwatch?.(); + }, + }); + + sample({ clock: startWatching, target: watchPositionFx }); + sample({ clock: stopWatching, target: unwatchPositionFx }); + + $watchingActive.on(startWatching, () => true).on(stopWatching, () => false); + + // -- public API return { $location: readonly($location), From e411ae0bf58321009d9ca304ed5daba84670c98b Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 25 Apr 2024 12:36:56 +0700 Subject: [PATCH 19/31] Add basic geolocation request --- apps/web-api-demo/geolocation.html | 22 +++++++++++ apps/web-api-demo/index.html | 1 + apps/web-api-demo/src/geolocation.ts | 18 +++++++++ apps/web-api-demo/test/geolocation.spec.ts | 34 +++++++++++++++++ packages/web-api/src/geolocation.ts | 44 +++++++++++++++++++--- 5 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 apps/web-api-demo/geolocation.html create mode 100644 apps/web-api-demo/src/geolocation.ts create mode 100644 apps/web-api-demo/test/geolocation.spec.ts diff --git a/apps/web-api-demo/geolocation.html b/apps/web-api-demo/geolocation.html new file mode 100644 index 00000000..3d71f435 --- /dev/null +++ b/apps/web-api-demo/geolocation.html @@ -0,0 +1,22 @@ + + + + + web-api demo + + + + + + +
+

Geolocation

+ +

latitude:

+

longitude:

+ + +
+ + + diff --git a/apps/web-api-demo/index.html b/apps/web-api-demo/index.html index f08cf2f8..acb0ca2a 100644 --- a/apps/web-api-demo/index.html +++ b/apps/web-api-demo/index.html @@ -16,6 +16,7 @@

@withease/web-api

  • page-visibility
  • screen-orientation
  • preferred-languages
  • +
  • geolocation
  • diff --git a/apps/web-api-demo/src/geolocation.ts b/apps/web-api-demo/src/geolocation.ts new file mode 100644 index 00000000..6e825deb --- /dev/null +++ b/apps/web-api-demo/src/geolocation.ts @@ -0,0 +1,18 @@ +import { trackGeolocation } from '@withease/web-api'; + +const latitudeElement = document.querySelector('#latitude')!; +const longitudeElement = document.querySelector('#longitude')!; +const getLocationButton = document.querySelector('#get-location')!; + +const { $latitude, $longitude, request } = trackGeolocation({}); + +$latitude.watch((latitude) => { + console.log('latitude', latitude); + latitudeElement.textContent = JSON.stringify(latitude); +}); +$longitude.watch((longitude) => { + console.log('longitude', longitude); + longitudeElement.textContent = JSON.stringify(longitude); +}); + +getLocationButton.addEventListener('click', () => request()); diff --git a/apps/web-api-demo/test/geolocation.spec.ts b/apps/web-api-demo/test/geolocation.spec.ts new file mode 100644 index 00000000..8f24e654 --- /dev/null +++ b/apps/web-api-demo/test/geolocation.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; + +const GEOLOCATION_PAGE = '/geolocation.html'; + +test.use({ + geolocation: { longitude: 41.890221, latitude: 12.492348 }, + permissions: ['geolocation'], +}); + +test('should show geolocation', async ({ page, context }) => { + await page.goto(GEOLOCATION_PAGE); + + const latitudeContainer = await page.$('#latitude'); + const longitudeContainer = await page.$('#longitude'); + const getLocationButton = await page.$('#get-location'); + + // By default it should be null + expect(await latitudeContainer!.textContent()).toBe('null'); + expect(await longitudeContainer!.textContent()).toBe('null'); + + // After requesting the location, it should be updated + await getLocationButton!.click(); + expect(await latitudeContainer!.textContent()).toBe('12.492348'); + expect(await longitudeContainer!.textContent()).toBe('41.890221'); + + // Change geolocation, values should NOT be updated + await context.setGeolocation({ longitude: 22.492348, latitude: 32.890221 }); + expect(await latitudeContainer!.textContent()).toBe('12.492348'); + expect(await longitudeContainer!.textContent()).toBe('41.890221'); + // Request the location again, values should be updated + await getLocationButton!.click(); + expect(await latitudeContainer!.textContent()).toBe('32.890221'); + expect(await longitudeContainer!.textContent()).toBe('22.492348'); +}); diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 0f2c9d4a..5e7734a0 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -80,8 +80,17 @@ export function trackGeolocation( providers?: Array; } ): Geolocation { - // In case of no providers, we will use the default one only - const providres = params.providers ?? [BrowserProvider]; + const providres = ( + params.providers ?? /* In case of no providers, we will use the default one only */ [ + BrowserProvider, + ] + ).map( + /* BrowserProvider symbol means usage of navigator.geolocation */ + (provider) => + (provider === BrowserProvider ? navigator.geolocation : provider) as + | CustomProvider + | globalThis.Geolocation + ); // -- units @@ -128,9 +137,26 @@ export function trackGeolocation( void, CustomGeolocationPosition | globalThis.GeolocationPosition, CustomGeolocationError | globalThis.GeolocationPositionError - >(() => { - // TODO: real code - throw { code: 'POSITION_UNAVAILABLE', message: 'Not implemented' }; + >(async () => { + let geolocation: GeolocationPosition | null = null; + + for (const provider of providres) { + if (isDefaultProvider(provider)) { + geolocation = await new Promise( + (resolve, rejest) => + provider.getCurrentPosition(resolve, rejest, params) + ); + } + } + + if (!geolocation) { + throw { + code: 'POSITION_UNAVAILABLE', + message: 'No avaiable geolocation provider', + }; + } + + return geolocation; }); sample({ clock: request, target: getCurrentPositionFx }); @@ -183,3 +209,11 @@ export function trackGeolocation( } trackGeolocation.browserProvider = BrowserProvider; + +function isDefaultProvider(provider: any): provider is globalThis.Geolocation { + return ( + 'getCurrentPosition' in provider && + 'watchPosition' in provider && + 'clearWatch' in provider + ); +} From ebaf2d8c18b22eaea9f00852146ea1a0bbe1e83a Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 25 Apr 2024 12:45:38 +0700 Subject: [PATCH 20/31] Add basic geolocation watching --- apps/web-api-demo/geolocation.html | 3 +++ apps/web-api-demo/src/geolocation.ts | 6 ++++- apps/web-api-demo/test/geolocation.spec.ts | 31 +++++++++++++++++++++- packages/web-api/src/geolocation.ts | 29 +++++++++++++++++--- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/apps/web-api-demo/geolocation.html b/apps/web-api-demo/geolocation.html index 3d71f435..0c9e2869 100644 --- a/apps/web-api-demo/geolocation.html +++ b/apps/web-api-demo/geolocation.html @@ -16,6 +16,9 @@

    Geolocation

    longitude:

    + + + diff --git a/apps/web-api-demo/src/geolocation.ts b/apps/web-api-demo/src/geolocation.ts index 6e825deb..87971560 100644 --- a/apps/web-api-demo/src/geolocation.ts +++ b/apps/web-api-demo/src/geolocation.ts @@ -3,8 +3,10 @@ import { trackGeolocation } from '@withease/web-api'; const latitudeElement = document.querySelector('#latitude')!; const longitudeElement = document.querySelector('#longitude')!; const getLocationButton = document.querySelector('#get-location')!; +const startWatchingButton = document.querySelector('#start-watching')!; +const stopWatchingButton = document.querySelector('#stop-watching')!; -const { $latitude, $longitude, request } = trackGeolocation({}); +const { $latitude, $longitude, request, watching } = trackGeolocation({}); $latitude.watch((latitude) => { console.log('latitude', latitude); @@ -16,3 +18,5 @@ $longitude.watch((longitude) => { }); getLocationButton.addEventListener('click', () => request()); +startWatchingButton.addEventListener('click', () => watching.start()); +stopWatchingButton.addEventListener('click', () => watching.stop()); diff --git a/apps/web-api-demo/test/geolocation.spec.ts b/apps/web-api-demo/test/geolocation.spec.ts index 8f24e654..01c666b9 100644 --- a/apps/web-api-demo/test/geolocation.spec.ts +++ b/apps/web-api-demo/test/geolocation.spec.ts @@ -7,7 +7,7 @@ test.use({ permissions: ['geolocation'], }); -test('should show geolocation', async ({ page, context }) => { +test('request', async ({ page, context }) => { await page.goto(GEOLOCATION_PAGE); const latitudeContainer = await page.$('#latitude'); @@ -32,3 +32,32 @@ test('should show geolocation', async ({ page, context }) => { expect(await latitudeContainer!.textContent()).toBe('32.890221'); expect(await longitudeContainer!.textContent()).toBe('22.492348'); }); + +test('watch', async ({ page, context }) => { + await page.goto(GEOLOCATION_PAGE); + + const latitudeContainer = await page.$('#latitude'); + const longitudeContainer = await page.$('#longitude'); + const startWatchingButton = await page.$('#start-watching'); + const stopWatchingButton = await page.$('#stop-watching'); + + // By default it should be null + expect(await latitudeContainer!.textContent()).toBe('null'); + expect(await longitudeContainer!.textContent()).toBe('null'); + + // After watching the location, it should be updated immediately + await startWatchingButton!.click(); + expect(await latitudeContainer!.textContent()).toBe('12.492348'); + expect(await longitudeContainer!.textContent()).toBe('41.890221'); + + // Change geolocation, values should be updated immediately + await context.setGeolocation({ longitude: 22.492348, latitude: 32.890221 }); + expect(await latitudeContainer!.textContent()).toBe('32.890221'); + expect(await longitudeContainer!.textContent()).toBe('22.492348'); + + // Stop watching and change geolocation, values should NOT be updated + await stopWatchingButton!.click(); + await context.setGeolocation({ longitude: 42.492348, latitude: 52.890221 }); + expect(await latitudeContainer!.textContent()).toBe('32.890221'); + expect(await longitudeContainer!.textContent()).toBe('22.492348'); +}); diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 5e7734a0..0e7d5f30 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -9,6 +9,7 @@ import { sample, restore, attach, + scopeBind, } from 'effector'; import { readonly } from './shared'; @@ -172,10 +173,30 @@ export function trackGeolocation( const $unsubscribe = restore(saveUnsubscribe, null); const watchPositionFx = createEffect(() => { - // TODO: real code - newPosition({} as any); - failed({} as any); - saveUnsubscribe(() => null); + const boundNewPosition = scopeBind(newPosition, { safe: true }); + const boundFailed = scopeBind(failed, { safe: true }); + const boundSaveUnsubscribe = scopeBind(saveUnsubscribe, { safe: true }); + + const unwatchMap = new Map<(id: number) => void, number>(); + + for (const provider of providres) { + if (isDefaultProvider(provider)) { + const watchId = provider.watchPosition( + boundNewPosition, + boundFailed, + params + ); + + unwatchMap.set((id: number) => provider.clearWatch(id), watchId); + } + } + + boundSaveUnsubscribe(() => { + for (const [unwatch, id] of unwatchMap) { + unwatch(id); + unwatchMap.delete(unwatch); + } + }); }); const unwatchPositionFx = attach({ From 0366dccc123a40f8dd58c98659c4928bec667b6e Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 25 Apr 2024 12:48:20 +0700 Subject: [PATCH 21/31] Refactoring --- packages/web-api/src/geolocation.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 0e7d5f30..a8b68b09 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -169,13 +169,11 @@ export function trackGeolocation( // -- watch position - const saveUnsubscribe = createEvent(); - const $unsubscribe = restore(saveUnsubscribe, null); + const $unsubscribe = createStore(null); const watchPositionFx = createEffect(() => { const boundNewPosition = scopeBind(newPosition, { safe: true }); const boundFailed = scopeBind(failed, { safe: true }); - const boundSaveUnsubscribe = scopeBind(saveUnsubscribe, { safe: true }); const unwatchMap = new Map<(id: number) => void, number>(); @@ -191,12 +189,12 @@ export function trackGeolocation( } } - boundSaveUnsubscribe(() => { + return () => { for (const [unwatch, id] of unwatchMap) { unwatch(id); unwatchMap.delete(unwatch); } - }); + }; }); const unwatchPositionFx = attach({ @@ -207,7 +205,9 @@ export function trackGeolocation( }); sample({ clock: startWatching, target: watchPositionFx }); + sample({ clock: watchPositionFx.doneData, target: $unsubscribe }); sample({ clock: stopWatching, target: unwatchPositionFx }); + sample({ clock: unwatchPositionFx.finally, target: $unsubscribe.reinit }); $watchingActive.on(startWatching, () => true).on(stopWatching, () => false); From 137b48e63a9a4cade2d1740c44c64a9740e22b19 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 25 Apr 2024 13:19:12 +0700 Subject: [PATCH 22/31] Add custom request and watch --- packages/web-api/src/geolocation.test.ts | 106 +++++++++++++++++++++++ packages/web-api/src/geolocation.ts | 31 +++++-- 2 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 packages/web-api/src/geolocation.test.ts diff --git a/packages/web-api/src/geolocation.test.ts b/packages/web-api/src/geolocation.test.ts new file mode 100644 index 00000000..afd92beb --- /dev/null +++ b/packages/web-api/src/geolocation.test.ts @@ -0,0 +1,106 @@ +/* + * This file contains tests only for custom geolocation providers. + * e2e-tests for built-in browser navigator.geolocation in apps/web-api-demo/test/geolocation.spec.ts + */ + +import { allSettled, createStore, fork } from 'effector'; +import { trackGeolocation } from 'geolocation'; +import { describe, expect, test } from 'vitest'; + +describe('trackGeolocation', () => { + test('request', async () => { + let lat = 41.890221; + let lon = 12.492348; + const myCustomProvider = () => ({ + async getCurrentPosition() { + return { + coords: { latitude: lat, longitude: lon }, + timestamp: Date.now(), + }; + }, + watchPosition(success: any, error: any) { + return () => {}; + }, + }); + + const geo = trackGeolocation({ providers: [myCustomProvider] }); + + const scope = fork(); + + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(`null`); + + await allSettled(geo.request, { scope }); + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 41.890221, + "longitude": 12.492348, + } + `); + + lat = 42.890221; + lon = 13.492348; + + await allSettled(geo.request, { scope }); + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 42.890221, + "longitude": 13.492348, + } + `); + }); + + test('watching', async () => { + const $externalLocation: any = createStore({ + latitude: 41.890221, + longitude: 12.492348, + }); + + const myCustomProvider = () => ({ + async getCurrentPosition() { + return { + coords: $externalLocation.getState(), + timestamp: Date.now(), + }; + }, + watchPosition(success: any, error: any) { + return $externalLocation.watch((location: any) => + success({ + coords: location, + timestamp: Date.now(), + }) + ); + }, + }); + + const geo = trackGeolocation({ providers: [myCustomProvider] }); + + const scope = fork(); + + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(`null`); + + await allSettled(geo.watching.start, { scope }); + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 41.890221, + "longitude": 12.492348, + } + `); + + $externalLocation.setState({ latitude: 42.890221, longitude: 13.492348 }); + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 42.890221, + "longitude": 13.492348, + } + `); + + await allSettled(geo.watching.stop, { scope }); + $externalLocation.setState({ latitude: 43.890221, longitude: 14.492348 }); + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 42.890221, + "longitude": 13.492348, + } + `); + }); +}); diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index a8b68b09..775fc1f8 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -7,7 +7,6 @@ import { createStore, createEffect, sample, - restore, attach, scopeBind, } from 'effector'; @@ -53,7 +52,7 @@ type Unsubscribe = () => void; type CustomProvider = (params: GeolocationParams) => { getCurrentPosition: () => Promise; - watchPosition?: ( + watchPosition: ( successCallback: (position: CustomGeolocationPosition) => void, errorCallback: (error: CustomGeolocationError) => void ) => Unsubscribe; @@ -139,7 +138,10 @@ export function trackGeolocation( CustomGeolocationPosition | globalThis.GeolocationPosition, CustomGeolocationError | globalThis.GeolocationPositionError >(async () => { - let geolocation: GeolocationPosition | null = null; + let geolocation: + | globalThis.GeolocationPosition + | CustomGeolocationPosition + | null = null; for (const provider of providres) { if (isDefaultProvider(provider)) { @@ -147,6 +149,8 @@ export function trackGeolocation( (resolve, rejest) => provider.getCurrentPosition(resolve, rejest, params) ); + } else { + geolocation = await provider(params).getCurrentPosition(); } } @@ -175,7 +179,8 @@ export function trackGeolocation( const boundNewPosition = scopeBind(newPosition, { safe: true }); const boundFailed = scopeBind(failed, { safe: true }); - const unwatchMap = new Map<(id: number) => void, number>(); + const defaultUnwatchMap = new Map<(id: number) => void, number>(); + const customUnwatchSet = new Set(); for (const provider of providres) { if (isDefaultProvider(provider)) { @@ -185,14 +190,26 @@ export function trackGeolocation( params ); - unwatchMap.set((id: number) => provider.clearWatch(id), watchId); + defaultUnwatchMap.set((id: number) => provider.clearWatch(id), watchId); + } else { + const unwatch = provider(params).watchPosition( + boundNewPosition, + boundFailed + ); + + customUnwatchSet.add(unwatch); } } return () => { - for (const [unwatch, id] of unwatchMap) { + for (const [unwatch, id] of defaultUnwatchMap) { unwatch(id); - unwatchMap.delete(unwatch); + defaultUnwatchMap.delete(unwatch); + } + + for (const unwatch of customUnwatchSet) { + unwatch(); + customUnwatchSet.delete(unwatch); } }; }); From 08e9937898cf3f138ed30e2343956d9624c598e6 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 25 Apr 2024 13:23:47 +0700 Subject: [PATCH 23/31] Size limit --- packages/web-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-api/package.json b/packages/web-api/package.json index 4e97bba4..cb3f433b 100644 --- a/packages/web-api/package.json +++ b/packages/web-api/package.json @@ -34,7 +34,7 @@ "size-limit": [ { "path": "./dist/web-api.js", - "limit": "1.58 kB" + "limit": "2.36 kB" } ] } From 82993b44c55deb715fe1f07e3de6fa45812fde22 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 25 Apr 2024 13:37:01 +0700 Subject: [PATCH 24/31] Add live demo --- .../website/docs/web-api/geolocation.live.vue | 19 +++++++++++++++++++ apps/website/docs/web-api/geolocation.md | 12 ++++++++++++ packages/web-api/src/geolocation.ts | 8 ++++---- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 apps/website/docs/web-api/geolocation.live.vue diff --git a/apps/website/docs/web-api/geolocation.live.vue b/apps/website/docs/web-api/geolocation.live.vue new file mode 100644 index 00000000..34a155c8 --- /dev/null +++ b/apps/website/docs/web-api/geolocation.live.vue @@ -0,0 +1,19 @@ + + + diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 7f018922..37e2369d 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -46,6 +46,18 @@ Returns an object with: - `reporting` - an object with the following properties: - `failed` - [_Event_](https://effector.dev/en/api/effector/event) that fires when the location request fails +## Live demo + +Let us show you a live demo of how it works. The following demo displays `$latitude` and `$longitude` values. _Click "Request geolocation" button to retrieve it._ + + + + + +## Additional capabilities + ### Regional restrictions In some countries and regions, the use of geolocation can be restricted. If you are aiming to provide a service in such locations, you use some local providers to get the location of the user. For example, in China, you can use [Baidu](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), [Autonavi](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), or [Tencent](https://lbs.qq.com/webApi/component/componentGuide/componentGeolocation). diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 775fc1f8..6e9c9f19 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -76,12 +76,12 @@ type Geolocation = { const BrowserProvider = Symbol('BrowserProvider'); export function trackGeolocation( - params: GeolocationParams & { + params?: GeolocationParams & { providers?: Array; } ): Geolocation { const providres = ( - params.providers ?? /* In case of no providers, we will use the default one only */ [ + params?.providers ?? /* In case of no providers, we will use the default one only */ [ BrowserProvider, ] ).map( @@ -150,7 +150,7 @@ export function trackGeolocation( provider.getCurrentPosition(resolve, rejest, params) ); } else { - geolocation = await provider(params).getCurrentPosition(); + geolocation = await provider(params ?? {}).getCurrentPosition(); } } @@ -192,7 +192,7 @@ export function trackGeolocation( defaultUnwatchMap.set((id: number) => provider.clearWatch(id), watchId); } else { - const unwatch = provider(params).watchPosition( + const unwatch = provider(params ?? {}).watchPosition( boundNewPosition, boundFailed ); From 17bd97b216040705c79d37270acbc2dbdd62376f Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Apr 2024 12:30:04 +0700 Subject: [PATCH 25/31] Final touches --- packages/web-api/src/geolocation.test.ts | 51 +++++++++++++++++++++--- packages/web-api/src/geolocation.ts | 32 +++++++++------ 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/packages/web-api/src/geolocation.test.ts b/packages/web-api/src/geolocation.test.ts index afd92beb..4d543e36 100644 --- a/packages/web-api/src/geolocation.test.ts +++ b/packages/web-api/src/geolocation.test.ts @@ -3,15 +3,15 @@ * e2e-tests for built-in browser navigator.geolocation in apps/web-api-demo/test/geolocation.spec.ts */ -import { allSettled, createStore, fork } from 'effector'; +import { allSettled, createStore, createWatch, fork } from 'effector'; import { trackGeolocation } from 'geolocation'; -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, vi } from 'vitest'; describe('trackGeolocation', () => { test('request', async () => { let lat = 41.890221; let lon = 12.492348; - const myCustomProvider = () => ({ + const myCustomProvider = vi.fn(() => ({ async getCurrentPosition() { return { coords: { latitude: lat, longitude: lon }, @@ -21,7 +21,7 @@ describe('trackGeolocation', () => { watchPosition(success: any, error: any) { return () => {}; }, - }); + })); const geo = trackGeolocation({ providers: [myCustomProvider] }); @@ -47,6 +47,8 @@ describe('trackGeolocation', () => { "longitude": 13.492348, } `); + + expect(myCustomProvider).toBeCalledTimes(1); }); test('watching', async () => { @@ -55,7 +57,7 @@ describe('trackGeolocation', () => { longitude: 12.492348, }); - const myCustomProvider = () => ({ + const myCustomProvider = vi.fn(() => ({ async getCurrentPosition() { return { coords: $externalLocation.getState(), @@ -70,7 +72,7 @@ describe('trackGeolocation', () => { }) ); }, - }); + })); const geo = trackGeolocation({ providers: [myCustomProvider] }); @@ -102,5 +104,42 @@ describe('trackGeolocation', () => { "longitude": 13.492348, } `); + + expect(myCustomProvider).toBeCalledTimes(1); + }); + + test('reporting', async () => { + const myCustomProvider = () => ({ + async getCurrentPosition() { + throw { + code: 'PERMISSION_DENIED', + message: 'User denied the request for Geolocation.', + raw: '用户拒绝了地理位置请求。', + }; + }, + watchPosition(success: any, error: any) { + return () => {}; + }, + }); + + const failedListener = vi.fn(); + + const geo = trackGeolocation({ providers: [myCustomProvider] }); + + const scope = fork(); + + createWatch({ unit: geo.reporting.failed, fn: failedListener, scope }); + + await allSettled(geo.request, { scope }); + + expect(failedListener).toBeCalledWith({ + code: 'PERMISSION_DENIED', + message: 'User denied the request for Geolocation.', + raw: '用户拒绝了地理位置请求。', + }); + }); + + test('do not throw if default provider is not available', async () => { + expect(() => trackGeolocation()).not.toThrow(); }); }); diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 6e9c9f19..a5492857 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -84,13 +84,24 @@ export function trackGeolocation( params?.providers ?? /* In case of no providers, we will use the default one only */ [ BrowserProvider, ] - ).map( - /* BrowserProvider symbol means usage of navigator.geolocation */ - (provider) => - (provider === BrowserProvider ? navigator.geolocation : provider) as - | CustomProvider - | globalThis.Geolocation - ); + ) + .map((provider) => { + /* BrowserProvider symbol means usage of navigator.geolocation */ + if (provider === BrowserProvider) { + const browserGeolocationAvailable = + globalThis.navigator && 'geolocation' in globalThis.navigator; + if (!browserGeolocationAvailable) { + return null; + } + + return globalThis.navigator.geolocation; + } + + return (provider as CustomProvider)(params ?? {}); + }) + .filter(Boolean) as Array< + ReturnType | globalThis.Geolocation + >; // -- units @@ -150,7 +161,7 @@ export function trackGeolocation( provider.getCurrentPosition(resolve, rejest, params) ); } else { - geolocation = await provider(params ?? {}).getCurrentPosition(); + geolocation = await provider.getCurrentPosition(); } } @@ -192,10 +203,7 @@ export function trackGeolocation( defaultUnwatchMap.set((id: number) => provider.clearWatch(id), watchId); } else { - const unwatch = provider(params ?? {}).watchPosition( - boundNewPosition, - boundFailed - ); + const unwatch = provider.watchPosition(boundNewPosition, boundFailed); customUnwatchSet.add(unwatch); } From 98bb6ff131537900288fd06053816c2ad9bc33a8 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Apr 2024 12:32:17 +0700 Subject: [PATCH 26/31] Size --- packages/web-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-api/package.json b/packages/web-api/package.json index cb3f433b..9609a285 100644 --- a/packages/web-api/package.json +++ b/packages/web-api/package.json @@ -34,7 +34,7 @@ "size-limit": [ { "path": "./dist/web-api.js", - "limit": "2.36 kB" + "limit": "2.38 kB" } ] } From 347f78ee7f77e4029eda13bcd051556ce593d06d Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Apr 2024 12:39:43 +0700 Subject: [PATCH 27/31] Fix usage of external default providers --- packages/web-api/src/geolocation.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index a5492857..4cec286c 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -77,7 +77,9 @@ const BrowserProvider = Symbol('BrowserProvider'); export function trackGeolocation( params?: GeolocationParams & { - providers?: Array; + providers?: Array< + typeof BrowserProvider | CustomProvider | globalThis.Geolocation + >; } ): Geolocation { const providres = ( @@ -97,7 +99,11 @@ export function trackGeolocation( return globalThis.navigator.geolocation; } - return (provider as CustomProvider)(params ?? {}); + if (isDefaultProvider(provider)) { + return provider; + } + + return provider(params ?? {}); }) .filter(Boolean) as Array< ReturnType | globalThis.Geolocation From a00daf01135edad5f718e4eebf61dd661aed2f97 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 1 Aug 2024 18:07:10 +0700 Subject: [PATCH 28/31] Allow to override geolocation providers via Fork API --- apps/website/docs/web-api/geolocation.md | 38 +++++ packages/web-api/package.json | 1 + packages/web-api/src/geolocation.test.ts | 56 ++++++- packages/web-api/src/geolocation.ts | 205 ++++++++++++++--------- 4 files changed, 218 insertions(+), 82 deletions(-) diff --git a/apps/website/docs/web-api/geolocation.md b/apps/website/docs/web-api/geolocation.md index 37e2369d..8d2f42ce 100644 --- a/apps/website/docs/web-api/geolocation.md +++ b/apps/website/docs/web-api/geolocation.md @@ -58,6 +58,21 @@ import demoFile from './geolocation.live.vue?raw'; ## Additional capabilities +While creating an integration, you can override the default geolocation provider with your custom one. It can be done by passing an array of providers to the `trackGeolocation` function. + +```ts +import { trackGeolocation } from '@withease/web-api'; + +const geo = trackGeolocation({ + /* ... */ + // by default providers field contains trackGeolocation.browserProvider + // which represents the browser built-in Geolocation API + providers: [trackGeolocation.browserProvider], +}); +``` + +The logic is quite straightforward: integration will call providers one by one until one of them returns the location. The first provider that returns the location will be used. + ### Regional restrictions In some countries and regions, the use of geolocation can be restricted. If you are aiming to provide a service in such locations, you use some local providers to get the location of the user. For example, in China, you can use [Baidu](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), [Autonavi](https://lbsyun.baidu.com/index.php?title=jspopular/guide/geolocation), or [Tencent](https://lbs.qq.com/webApi/component/componentGuide/componentGeolocation). @@ -227,3 +242,26 @@ const geo = trackGeolocation({ ], }); ``` + +### Testing + +You can pass a [_Store_](https://effector.dev/docs/api/effector/store) to `providers` option to get a way to mock the geolocation provider during testing via [Fork API](/magazine/fork_api_rules). + +```ts +import { createStore, fork } from 'effector'; +import { trackGeolocation } from '@withease/web-api'; + +// Create a store with the default provider +const $geolocationProviders = createStore([trackGeolocation.browserProvider]); + +// Create an integration with the store +const geo = trackGeolocation({ + /* ... */ + providers: $geolocationProviders, +}); + +// during testing, you can replace the provider with your mock +const scope = fork({ values: [[$geolocationProviders, myFakeProvider]] }); +``` + +That is it, any calculations on the created [_Scope_](https://effector.dev/docs/api/effector/scope) will use the `myFakeProvider` instead of the default one. diff --git a/packages/web-api/package.json b/packages/web-api/package.json index 9609a285..77341919 100644 --- a/packages/web-api/package.json +++ b/packages/web-api/package.json @@ -7,6 +7,7 @@ }, "scripts": { "test:run": "vitest run --typecheck", + "test:watch": "vitest --typecheck", "build": "vite build", "size": "size-limit", "publint": "node ../../tools/publint.mjs", diff --git a/packages/web-api/src/geolocation.test.ts b/packages/web-api/src/geolocation.test.ts index 4d543e36..a652c865 100644 --- a/packages/web-api/src/geolocation.test.ts +++ b/packages/web-api/src/geolocation.test.ts @@ -4,9 +4,10 @@ */ import { allSettled, createStore, createWatch, fork } from 'effector'; -import { trackGeolocation } from 'geolocation'; import { describe, expect, test, vi } from 'vitest'; +import { trackGeolocation } from './geolocation'; + describe('trackGeolocation', () => { test('request', async () => { let lat = 41.890221; @@ -143,3 +144,56 @@ describe('trackGeolocation', () => { expect(() => trackGeolocation()).not.toThrow(); }); }); + +describe('trackGeolocation, providers as a Store', () => { + const firstProvider = () => ({ + name: 'firstProvider', + async getCurrentPosition() { + return { + coords: { latitude: 1, longitude: 1 }, + timestamp: Date.now(), + }; + }, + watchPosition(success: any, error: any) { + return () => {}; + }, + }); + + const secondProvider = () => ({ + name: 'secondProvider', + async getCurrentPosition() { + return { + coords: { latitude: 2, longitude: 2 }, + timestamp: Date.now(), + }; + }, + watchPosition(success: any, error: any) { + return () => {}; + }, + }); + + const $providers = createStore([firstProvider]); + + const geo = trackGeolocation({ providers: $providers }); + + test('request', async () => { + const scopeWithOriginal = fork(); + const scopeWithReplace = fork({ values: [[$providers, [secondProvider]]] }); + + await allSettled(geo.request, { scope: scopeWithReplace }); + expect(scopeWithReplace.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 2, + "longitude": 2, + } + `); + + await allSettled(geo.request, { scope: scopeWithOriginal }); + expect(scopeWithOriginal.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 1, + "longitude": 1, + } + `); + }); +}); diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 4cec286c..aded3fde 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -2,13 +2,14 @@ import { type Event, type EventCallable, type Store, + type Effect, combine, createEvent, createStore, - createEffect, sample, attach, scopeBind, + is, } from 'effector'; import { readonly } from './shared'; @@ -77,37 +78,41 @@ const BrowserProvider = Symbol('BrowserProvider'); export function trackGeolocation( params?: GeolocationParams & { - providers?: Array< - typeof BrowserProvider | CustomProvider | globalThis.Geolocation - >; + providers?: + | Array + | Store< + Array< + typeof BrowserProvider | CustomProvider | globalThis.Geolocation + > + >; } ): Geolocation { - const providres = ( - params?.providers ?? /* In case of no providers, we will use the default one only */ [ - BrowserProvider, - ] - ) - .map((provider) => { - /* BrowserProvider symbol means usage of navigator.geolocation */ - if (provider === BrowserProvider) { - const browserGeolocationAvailable = - globalThis.navigator && 'geolocation' in globalThis.navigator; - if (!browserGeolocationAvailable) { - return null; - } - - return globalThis.navigator.geolocation; - } + let $providers: Store< + Array + >; + if (is.store(params?.providers)) { + $providers = params.providers; + } else { + $providers = createStore(params?.providers ?? [BrowserProvider]); + } - if (isDefaultProvider(provider)) { - return provider; - } + const initializeAllProvidersFx = attach({ + source: $providers, + effect(providers) { + return providers + .map((provider) => initializeProvider(provider, params)) + .filter(Boolean) as Array< + ReturnType | globalThis.Geolocation + >; + }, + }); - return provider(params ?? {}); - }) - .filter(Boolean) as Array< + const $initializedProviders = createStore | globalThis.Geolocation - >; + > | null>(null, { serialize: 'ignore' }).on( + initializeAllProvidersFx.doneData, + (_, providers) => providers + ); // -- units @@ -138,6 +143,13 @@ export function trackGeolocation( // -- shared logic + sample({ + clock: [request, startWatching], + source: $initializedProviders, + filter: (providers) => !providers, + target: initializeAllProvidersFx, + }); + const newPosition = createEvent< CustomGeolocationPosition | globalThis.GeolocationPosition >(); @@ -150,35 +162,38 @@ export function trackGeolocation( // -- get current position - const getCurrentPositionFx = createEffect< + const getCurrentPositionFx: Effect< void, CustomGeolocationPosition | globalThis.GeolocationPosition, CustomGeolocationError | globalThis.GeolocationPositionError - >(async () => { - let geolocation: - | globalThis.GeolocationPosition - | CustomGeolocationPosition - | null = null; - - for (const provider of providres) { - if (isDefaultProvider(provider)) { - geolocation = await new Promise( - (resolve, rejest) => - provider.getCurrentPosition(resolve, rejest, params) - ); - } else { - geolocation = await provider.getCurrentPosition(); + > = attach({ + source: $initializedProviders, + async effect(providers) { + let geolocation: + | globalThis.GeolocationPosition + | CustomGeolocationPosition + | null = null; + + for (const provider of providers ?? []) { + if (isDefaultProvider(provider)) { + geolocation = await new Promise( + (resolve, reject) => + provider.getCurrentPosition(resolve, reject, params) + ); + } else { + geolocation = await provider.getCurrentPosition(); + } } - } - if (!geolocation) { - throw { - code: 'POSITION_UNAVAILABLE', - message: 'No avaiable geolocation provider', - }; - } + if (!geolocation) { + throw { + code: 'POSITION_UNAVAILABLE', + message: 'No available geolocation provider', + }; + } - return geolocation; + return geolocation; + }, }); sample({ clock: request, target: getCurrentPositionFx }); @@ -192,40 +207,46 @@ export function trackGeolocation( const $unsubscribe = createStore(null); - const watchPositionFx = createEffect(() => { - const boundNewPosition = scopeBind(newPosition, { safe: true }); - const boundFailed = scopeBind(failed, { safe: true }); - - const defaultUnwatchMap = new Map<(id: number) => void, number>(); - const customUnwatchSet = new Set(); - - for (const provider of providres) { - if (isDefaultProvider(provider)) { - const watchId = provider.watchPosition( - boundNewPosition, - boundFailed, - params - ); - - defaultUnwatchMap.set((id: number) => provider.clearWatch(id), watchId); - } else { - const unwatch = provider.watchPosition(boundNewPosition, boundFailed); - - customUnwatchSet.add(unwatch); + const watchPositionFx = attach({ + source: $initializedProviders, + effect(providers) { + const boundNewPosition = scopeBind(newPosition, { safe: true }); + const boundFailed = scopeBind(failed, { safe: true }); + + const defaultUnwatchMap = new Map<(id: number) => void, number>(); + const customUnwatchSet = new Set(); + + for (const provider of providers ?? []) { + if (isDefaultProvider(provider)) { + const watchId = provider.watchPosition( + boundNewPosition, + boundFailed, + params + ); + + defaultUnwatchMap.set( + (id: number) => provider.clearWatch(id), + watchId + ); + } else { + const unwatch = provider.watchPosition(boundNewPosition, boundFailed); + + customUnwatchSet.add(unwatch); + } } - } - return () => { - for (const [unwatch, id] of defaultUnwatchMap) { - unwatch(id); - defaultUnwatchMap.delete(unwatch); - } + return () => { + for (const [unwatch, id] of defaultUnwatchMap) { + unwatch(id); + defaultUnwatchMap.delete(unwatch); + } - for (const unwatch of customUnwatchSet) { - unwatch(); - customUnwatchSet.delete(unwatch); - } - }; + for (const unwatch of customUnwatchSet) { + unwatch(); + customUnwatchSet.delete(unwatch); + } + }; + }, }); const unwatchPositionFx = attach({ @@ -262,6 +283,28 @@ export function trackGeolocation( trackGeolocation.browserProvider = BrowserProvider; +function initializeProvider( + provider: typeof BrowserProvider | CustomProvider | globalThis.Geolocation, + params?: GeolocationParams +) { + /* BrowserProvider symbol means usage of navigator.geolocation */ + if (provider === BrowserProvider) { + const browserGeolocationAvailable = + globalThis.navigator && 'geolocation' in globalThis.navigator; + if (!browserGeolocationAvailable) { + return null; + } + + return globalThis.navigator.geolocation; + } + + if (isDefaultProvider(provider)) { + return provider; + } + + return provider(params ?? {}); +} + function isDefaultProvider(provider: any): provider is globalThis.Geolocation { return ( 'getCurrentPosition' in provider && From fce73a454f07c6b73813147257eaaa8f52c280b6 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 1 Aug 2024 18:19:58 +0700 Subject: [PATCH 29/31] Add correct safe calls --- packages/web-api/src/geolocation.test.ts | 96 ++++++++++++++++++++++++ packages/web-api/src/geolocation.ts | 57 ++++++++------ 2 files changed, 131 insertions(+), 22 deletions(-) diff --git a/packages/web-api/src/geolocation.test.ts b/packages/web-api/src/geolocation.test.ts index a652c865..7bc5ac4c 100644 --- a/packages/web-api/src/geolocation.test.ts +++ b/packages/web-api/src/geolocation.test.ts @@ -197,3 +197,99 @@ describe('trackGeolocation, providers as a Store', () => { `); }); }); + +describe('trackGeolocation, failure of provider', () => { + const brokenProvider = () => ({ + async getCurrentPosition() { + throw new Error('Wow, government do not want you to use this provider!'); + }, + watchPosition(success: any, error: any) { + throw new Error('Wow, government do not want you to use this provider!'); + }, + }); + + const slightlyBrokenProvider = () => ({ + async getCurrentPosition() { + throw new Error('Not this time, buddy!'); + }, + watchPosition(success: any, error: any) { + error(new Error('Not this time, buddy!')); + + return () => {}; + }, + }); + + const okProvider = () => ({ + async getCurrentPosition() { + return { + coords: { latitude: 2, longitude: 2 }, + timestamp: Date.now(), + }; + }, + watchPosition(success: any, error: any) { + success({ + coords: { latitude: 2, longitude: 2 }, + timestamp: Date.now(), + }); + return () => {}; + }, + }); + + const geo = trackGeolocation({ + providers: [brokenProvider, slightlyBrokenProvider, okProvider], + }); + + test('request', async () => { + const scope = fork(); + + const failedWatcher = vi.fn(); + createWatch({ unit: geo.reporting.failed, fn: failedWatcher, scope }); + + await allSettled(geo.request, { scope }); + + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 2, + "longitude": 2, + } + `); + + expect(failedWatcher.mock.calls).toMatchInlineSnapshot(` + [ + [ + [Error: Wow, government do not want you to use this provider!], + ], + [ + [Error: Not this time, buddy!], + ], + ] + `); + }); + + test('watching', async () => { + const scope = fork(); + + const failedWatcher = vi.fn(); + createWatch({ unit: geo.reporting.failed, fn: failedWatcher, scope }); + + await allSettled(geo.watching.start, { scope }); + + expect(scope.getState(geo.$location)).toMatchInlineSnapshot(` + { + "latitude": 2, + "longitude": 2, + } + `); + + expect(failedWatcher.mock.calls).toMatchInlineSnapshot(` + [ + [ + [Error: Wow, government do not want you to use this provider!], + ], + [ + [Error: Not this time, buddy!], + ], + ] + `); + }); +}); diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index aded3fde..72400b8c 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -174,14 +174,20 @@ export function trackGeolocation( | CustomGeolocationPosition | null = null; + const boundFailed = scopeBind(failed, { safe: true }); + for (const provider of providers ?? []) { - if (isDefaultProvider(provider)) { - geolocation = await new Promise( - (resolve, reject) => - provider.getCurrentPosition(resolve, reject, params) - ); - } else { - geolocation = await provider.getCurrentPosition(); + try { + if (isDefaultProvider(provider)) { + geolocation = await new Promise( + (resolve, reject) => + provider.getCurrentPosition(resolve, reject, params) + ); + } else { + geolocation = await provider.getCurrentPosition(); + } + } catch (e: any) { + boundFailed(e); } } @@ -217,21 +223,28 @@ export function trackGeolocation( const customUnwatchSet = new Set(); for (const provider of providers ?? []) { - if (isDefaultProvider(provider)) { - const watchId = provider.watchPosition( - boundNewPosition, - boundFailed, - params - ); - - defaultUnwatchMap.set( - (id: number) => provider.clearWatch(id), - watchId - ); - } else { - const unwatch = provider.watchPosition(boundNewPosition, boundFailed); - - customUnwatchSet.add(unwatch); + try { + if (isDefaultProvider(provider)) { + const watchId = provider.watchPosition( + boundNewPosition, + boundFailed, + params + ); + + defaultUnwatchMap.set( + (id: number) => provider.clearWatch(id), + watchId + ); + } else { + const unwatch = provider.watchPosition( + boundNewPosition, + boundFailed + ); + + customUnwatchSet.add(unwatch); + } + } catch (e: any) { + boundFailed(e); } } From 4fd7080dbbff9f45494f89224e988d2c92155e44 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 1 Aug 2024 18:23:39 +0700 Subject: [PATCH 30/31] Build fixes --- packages/web-api/package.json | 2 +- packages/web-api/src/geolocation.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/web-api/package.json b/packages/web-api/package.json index 77341919..bc6a2466 100644 --- a/packages/web-api/package.json +++ b/packages/web-api/package.json @@ -35,7 +35,7 @@ "size-limit": [ { "path": "./dist/web-api.js", - "limit": "2.38 kB" + "limit": "2.49 kB" } ] } diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index 72400b8c..ccadf724 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -90,10 +90,11 @@ export function trackGeolocation( let $providers: Store< Array >; - if (is.store(params?.providers)) { - $providers = params.providers; + const providersFromParams = params?.providers; + if (is.store(providersFromParams)) { + $providers = providersFromParams; } else { - $providers = createStore(params?.providers ?? [BrowserProvider]); + $providers = createStore(providersFromParams ?? [BrowserProvider]); } const initializeAllProvidersFx = attach({ From bca44eb53dac3a04bb366a801418712e0184d0ab Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Thu, 1 Aug 2024 18:47:48 +0700 Subject: [PATCH 31/31] Get red of race condition in old effector --- packages/web-api/src/geolocation.ts | 43 +++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/packages/web-api/src/geolocation.ts b/packages/web-api/src/geolocation.ts index ccadf724..405dfc5f 100644 --- a/packages/web-api/src/geolocation.ts +++ b/packages/web-api/src/geolocation.ts @@ -14,6 +14,8 @@ import { import { readonly } from './shared'; +type InitializedProvider = ReturnType | globalThis.Geolocation; + type GeolocationParams = { maximumAge?: number; timeout?: number; @@ -108,12 +110,10 @@ export function trackGeolocation( }, }); - const $initializedProviders = createStore | globalThis.Geolocation - > | null>(null, { serialize: 'ignore' }).on( - initializeAllProvidersFx.doneData, - (_, providers) => providers - ); + const $initializedProviders = createStore | null>( + null, + { serialize: 'ignore' } + ).on(initializeAllProvidersFx.doneData, (_, providers) => providers); // -- units @@ -144,13 +144,6 @@ export function trackGeolocation( // -- shared logic - sample({ - clock: [request, startWatching], - source: $initializedProviders, - filter: (providers) => !providers, - target: initializeAllProvidersFx, - }); - const newPosition = createEvent< CustomGeolocationPosition | globalThis.GeolocationPosition >(); @@ -169,7 +162,7 @@ export function trackGeolocation( CustomGeolocationError | globalThis.GeolocationPositionError > = attach({ source: $initializedProviders, - async effect(providers) { + async effect(initializedProviders) { let geolocation: | globalThis.GeolocationPosition | CustomGeolocationPosition @@ -177,7 +170,15 @@ export function trackGeolocation( const boundFailed = scopeBind(failed, { safe: true }); - for (const provider of providers ?? []) { + let providers: InitializedProvider[]; + + if (initializedProviders) { + providers = initializedProviders; + } else { + providers = await initializeAllProvidersFx(); + } + + for (const provider of providers) { try { if (isDefaultProvider(provider)) { geolocation = await new Promise( @@ -216,14 +217,22 @@ export function trackGeolocation( const watchPositionFx = attach({ source: $initializedProviders, - effect(providers) { + async effect(initializedProviders) { const boundNewPosition = scopeBind(newPosition, { safe: true }); const boundFailed = scopeBind(failed, { safe: true }); + let providers: InitializedProvider[]; + + if (initializedProviders) { + providers = initializedProviders; + } else { + providers = await initializeAllProvidersFx(); + } + const defaultUnwatchMap = new Map<(id: number) => void, number>(); const customUnwatchSet = new Set(); - for (const provider of providers ?? []) { + for (const provider of providers) { try { if (isDefaultProvider(provider)) { const watchId = provider.watchPosition(