@chaosity/location-client-react 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -8,6 +8,16 @@ React bindings for [@chaosity/location-client](https://www.npmjs.com/package/@ch
|
|
|
8
8
|
npm install @chaosity/location-client-react @chaosity/location-client
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`maplibre-gl` is an optional peer, needed by `useMapLanguage` and the map
|
|
12
|
+
examples, at 6.4.1 or a later 6.x release. Earlier releases carry
|
|
13
|
+
[GHSA-jrc7-96c5-q579](https://github.com/advisories/GHSA-jrc7-96c5-q579), an
|
|
14
|
+
XSS in the attribution control, and `@chaosity/location-client` 0.11.0 is the
|
|
15
|
+
first core whose own peer admits 6. Under a bundler, MapLibre 6 runs its worker
|
|
16
|
+
from a file your application serves, so a map needs `setWorkerUrl` once before
|
|
17
|
+
it is built. The examples below serve it from `public/maplibre/`, and
|
|
18
|
+
[The MapLibre worker](https://github.com/chaosity-io/location-service-client#the-maplibre-worker)
|
|
19
|
+
in `@chaosity/location-client`'s README has the copy script and the Vite form.
|
|
20
|
+
|
|
11
21
|
## Quick Start
|
|
12
22
|
|
|
13
23
|
### 1. Create a Server Action to fetch config
|
|
@@ -86,6 +96,9 @@ React hook that keeps map label language in sync. Automatically reapplies after
|
|
|
86
96
|
|
|
87
97
|
```tsx
|
|
88
98
|
import { useMapLanguage } from '@chaosity/location-client-react'
|
|
99
|
+
import * as maplibregl from 'maplibre-gl'
|
|
100
|
+
|
|
101
|
+
maplibregl.setWorkerUrl('/maplibre/maplibre-gl-worker.mjs')
|
|
89
102
|
|
|
90
103
|
function MapComponent() {
|
|
91
104
|
const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
|
|
@@ -128,7 +141,8 @@ Provides the location client and automatic token refresh to all child components
|
|
|
128
141
|
|
|
129
142
|
**Props:**
|
|
130
143
|
|
|
131
|
-
- `getConfig` — Async function that returns `{ apiUrl: string, token: string, expiresAt?: number }`. Called on
|
|
144
|
+
- `getConfig` — Async function that returns `{ apiUrl: string, token: string, expiresAt?: number }`. Called on mount, whenever the token needs refreshing, and to retry a call that failed (see [Token Refresh](#token-refresh)).
|
|
145
|
+
- `configKey` — Optional. What `getConfig` answers for, such as an organisation or application id. When it changes, the provider drops the old client, token and `apiUrl`, calls `getConfig` again and hands out a new client, without remounting its children.
|
|
132
146
|
- `children` — Child components.
|
|
133
147
|
|
|
134
148
|
There is no `refreshBuffer` prop. It was removed in `0.3.0` — a value shorter
|
|
@@ -137,20 +151,47 @@ the server would not yet replace, and the two spun against each other. Both
|
|
|
137
151
|
sides now apply the same buffer to the token's own `exp`. Passing it does
|
|
138
152
|
nothing.
|
|
139
153
|
|
|
154
|
+
#### Switching organisation or application
|
|
155
|
+
|
|
156
|
+
Either of these is correct:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// Keeps the children mounted: only the client is replaced.
|
|
160
|
+
<LocationClientProvider configKey={orgId} getConfig={getLocationConfig}>
|
|
161
|
+
|
|
162
|
+
// Remounts everything below the provider, and its state with it.
|
|
163
|
+
<LocationClientProvider key={orgId} getConfig={getLocationConfig}>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Without either, the provider goes on using the old configuration until its
|
|
167
|
+
next token refresh, up to 14 minutes later. A new `getConfig` function is not
|
|
168
|
+
a signal on its own, because it is usually a new function on every render.
|
|
169
|
+
|
|
170
|
+
After a switch, a client or `getToken` kept from before it refuses: `send()`
|
|
171
|
+
and `verifyAddress()` reject, `getToken()` returns `undefined`, and
|
|
172
|
+
`getAppConfig()` answers `{}`. So one configuration's token never reaches
|
|
173
|
+
another's URL. Build anything that holds
|
|
174
|
+
on to them, such as a MapLibre map or a geocoder, from the context's current
|
|
175
|
+
values, and rebuild it when `client`, `getToken` or `apiUrl` changes, as the
|
|
176
|
+
[complete example](#complete-example-with-maplibre) does. The same happens
|
|
177
|
+
without a `configKey` when `getConfig` starts answering with a different
|
|
178
|
+
`apiUrl`.
|
|
179
|
+
|
|
140
180
|
### useLocationClient
|
|
141
181
|
|
|
142
182
|
Hook to access the location client in any component.
|
|
143
183
|
|
|
144
184
|
```tsx
|
|
145
|
-
const { client, getToken, loading, error } = useLocationClient()
|
|
185
|
+
const { client, getToken, apiUrl, loading, error } = useLocationClient()
|
|
146
186
|
```
|
|
147
187
|
|
|
148
188
|
**Returns:**
|
|
149
189
|
|
|
150
190
|
- `client` (`LocationClient | null`) — The location client. Not a bare `GeoPlacesClient`: the provider wraps it so `send()` and `verifyAddress()` refresh the token first when they need to, and retry once if the API rejects it.
|
|
151
|
-
- `getToken` (`() => string | undefined`) — Returns the current token
|
|
152
|
-
- `
|
|
153
|
-
- `
|
|
191
|
+
- `getToken` (`() => string | undefined`) — Returns the current token, for requests the client does not make itself, such as a map's style, tiles and glyphs. It is available before the first token arrives and stays the same function afterwards, so a map built early reads the token once it lands. It returns `undefined` once `configKey` changes.
|
|
192
|
+
- `apiUrl` (`string | null`) — The API `client` talks to, from the same `getConfig` answer as the token. Build map URLs from it rather than restating the URL. `null` until a configuration has loaded.
|
|
193
|
+
- `loading` (`boolean`) — Whether the client is initializing. `true` again while a new `configKey` loads.
|
|
194
|
+
- `error` (`string | null`) — Error message if initialization or a token refresh failed. The provider keeps retrying, and `error` returns to `null` on the first success (see [Token Refresh](#token-refresh)).
|
|
154
195
|
|
|
155
196
|
**Throws:** Error if used outside `LocationClientProvider`.
|
|
156
197
|
|
|
@@ -176,11 +217,46 @@ The provider owns the token lifecycle. There is nothing to manage manually.
|
|
|
176
217
|
package's peer range admits has it.
|
|
177
218
|
6. Concurrent refreshes are deduplicated — everything waiting shares one call to
|
|
178
219
|
`getConfig`.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
220
|
+
7. A failed `getConfig` is retried on the provider's own timer, backing off
|
|
221
|
+
exponentially from 1 to 30 seconds, with jitter. When `getConfig` rejects
|
|
222
|
+
with an error that has `retryAfterMs` (milliseconds), it waits that long
|
|
223
|
+
instead. Until then nothing else asks, neither a map reading its token for
|
|
224
|
+
every tile nor a `send()`. The tab coming back into view, or the browser
|
|
225
|
+
coming back online, retries at once, unless the wait was a `retryAfterMs`.
|
|
226
|
+
|
|
227
|
+
A Server Action, as in the Quick Start, cannot pass `retryAfterMs` on:
|
|
228
|
+
React sends a thrown error to the browser without its own fields. To honour
|
|
229
|
+
a token route's `Retry-After`, call the route from the browser and throw it
|
|
230
|
+
there:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
async function getConfig() {
|
|
234
|
+
const res = await fetch('/api/location-token')
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
const retryAfter = Number(res.headers.get('retry-after'))
|
|
237
|
+
throw Object.assign(new Error(`token route answered ${res.status}`), {
|
|
238
|
+
retryAfterMs: retryAfter > 0 ? retryAfter * 1000 : undefined,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
return res.json()
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
8. That includes the first call. A page whose first `getConfig` fails gets its
|
|
246
|
+
client when a retry succeeds, without a reload.
|
|
247
|
+
9. A token that arrives already stale by the browser's clock, because that
|
|
248
|
+
clock runs ahead of the server's, is not asked for again at once: the
|
|
249
|
+
server would hand back the same one. The provider waits, longer each time,
|
|
250
|
+
and sends with the token it has. A 401 still replaces it straight away.
|
|
251
|
+
|
|
252
|
+
A refresh that fails is reported as `error` from `useLocationClient()` until a
|
|
253
|
+
retry succeeds. Meanwhile every request goes out with the token in hand for as
|
|
254
|
+
long as that token is before its own expiry: `send()`, `verifyAddress()` and a
|
|
255
|
+
map's tiles alike, because the API still accepts it. Once it has expired,
|
|
256
|
+
`send()` and `verifyAddress()` reject with the refresh error rather than a 401,
|
|
257
|
+
so the cause reads as the token endpoint being unreachable, not as the API
|
|
258
|
+
refusing you. A token the API refuses (a 401) is never sent again: that
|
|
259
|
+
request rejects with the refresh error too.
|
|
184
260
|
|
|
185
261
|
## Complete Example with MapLibre
|
|
186
262
|
|
|
@@ -197,10 +273,14 @@ import {
|
|
|
197
273
|
fetchMapStyle,
|
|
198
274
|
createTransformRequest,
|
|
199
275
|
} from '@chaosity/location-client'
|
|
200
|
-
import maplibregl from 'maplibre-gl'
|
|
276
|
+
import * as maplibregl from 'maplibre-gl'
|
|
277
|
+
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
201
278
|
import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder'
|
|
279
|
+
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css'
|
|
202
280
|
|
|
203
|
-
|
|
281
|
+
// Once, before the first map: the worker file the application serves (see
|
|
282
|
+
// Installation).
|
|
283
|
+
maplibregl.setWorkerUrl('/maplibre/maplibre-gl-worker.mjs')
|
|
204
284
|
|
|
205
285
|
export default function MapComponent() {
|
|
206
286
|
const mapContainer = useRef<HTMLDivElement>(null)
|
|
@@ -208,16 +288,18 @@ export default function MapComponent() {
|
|
|
208
288
|
const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
|
|
209
289
|
const [mapError, setMapError] = useState<string | null>(null)
|
|
210
290
|
const [language, setLanguage] = useState('en')
|
|
211
|
-
const { client, getToken, loading, error } = useLocationClient()
|
|
291
|
+
const { client, getToken, apiUrl, loading, error } = useLocationClient()
|
|
212
292
|
|
|
213
293
|
// Keeps map labels in sync with language — reapplies after every setStyle() call
|
|
214
294
|
useMapLanguage(mapInstance, language)
|
|
215
295
|
|
|
216
296
|
useEffect(() => {
|
|
217
|
-
if (!mapContainer.current || map.current || loading || !client)
|
|
297
|
+
if (!mapContainer.current || map.current || loading || !client || !apiUrl)
|
|
298
|
+
return
|
|
218
299
|
;(async () => {
|
|
219
|
-
// Fetch the style with the language baked into the descriptor
|
|
220
|
-
|
|
300
|
+
// Fetch the style with the language baked into the descriptor. The URL
|
|
301
|
+
// comes from the context with the token, so the two always belong together.
|
|
302
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
221
303
|
colorScheme: 'Light',
|
|
222
304
|
language,
|
|
223
305
|
})
|
|
@@ -227,7 +309,7 @@ export default function MapComponent() {
|
|
|
227
309
|
style,
|
|
228
310
|
center: [-123.12, 49.28],
|
|
229
311
|
zoom: 10,
|
|
230
|
-
transformRequest: createTransformRequest(
|
|
312
|
+
transformRequest: createTransformRequest(apiUrl, getToken),
|
|
231
313
|
})
|
|
232
314
|
// Held at once, so the catch and the cleanup below can remove it
|
|
233
315
|
map.current = instance
|
|
@@ -262,7 +344,7 @@ export default function MapComponent() {
|
|
|
262
344
|
setMapInstance(null)
|
|
263
345
|
}
|
|
264
346
|
}
|
|
265
|
-
}, [client, getToken, loading])
|
|
347
|
+
}, [client, getToken, apiUrl, loading])
|
|
266
348
|
|
|
267
349
|
if (error) return <div>Error: {error}</div>
|
|
268
350
|
if (mapError) return <div>Map unavailable: {mapError}</div>
|
|
@@ -277,7 +359,7 @@ one refuses the style request with 403 `FeatureNotEntitledException`, which the
|
|
|
277
359
|
`catch` above puts on screen. 3D terrain and buildings need the `terrain` and `buildings` plan features:
|
|
278
360
|
|
|
279
361
|
```tsx
|
|
280
|
-
const style = await fetchMapStyle(
|
|
362
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
281
363
|
colorScheme: 'Light',
|
|
282
364
|
terrain: 'Terrain3D',
|
|
283
365
|
buildings: 'Buildings3D',
|
|
@@ -73,6 +73,14 @@ export interface LocationClient {
|
|
|
73
73
|
interface LocationClientContextValue {
|
|
74
74
|
client: LocationClient | null;
|
|
75
75
|
getToken: () => string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* The API `client` talks to, from the same `getConfig` answer as the token
|
|
78
|
+
* `getToken` returns (#14). Build a map's style and tile URLs from this
|
|
79
|
+
* rather than restating the URL, so a map and its token cannot come from two
|
|
80
|
+
* different configurations. `null` until a configuration has loaded, and
|
|
81
|
+
* again while a new `configKey` loads.
|
|
82
|
+
*/
|
|
83
|
+
apiUrl: string | null;
|
|
76
84
|
loading: boolean;
|
|
77
85
|
error: string | null;
|
|
78
86
|
}
|
|
@@ -81,7 +89,20 @@ export interface LocationClientProviderProps {
|
|
|
81
89
|
getConfig: () => Promise<ClientConfig & {
|
|
82
90
|
expiresAt?: number;
|
|
83
91
|
}>;
|
|
92
|
+
/**
|
|
93
|
+
* What `getConfig` answers for: an organisation or application id.
|
|
94
|
+
*
|
|
95
|
+
* Changing it drops the old configuration's token, client and `apiUrl` in
|
|
96
|
+
* the same render, and asks `getConfig` again, without remounting the
|
|
97
|
+
* children (#14). A client or a `getToken` kept from before the change
|
|
98
|
+
* refuses from then on, rather than pairing one configuration's token with
|
|
99
|
+
* the other's URL.
|
|
100
|
+
*
|
|
101
|
+
* `getConfig`'s own identity is deliberately not such a signal: it is
|
|
102
|
+
* usually an inline function, new on every render.
|
|
103
|
+
*/
|
|
104
|
+
configKey?: string | number;
|
|
84
105
|
}
|
|
85
|
-
export declare function LocationClientProvider({ children, getConfig, }: LocationClientProviderProps): import("react").JSX.Element;
|
|
106
|
+
export declare function LocationClientProvider({ children, getConfig, configKey, }: LocationClientProviderProps): import("react").JSX.Element;
|
|
86
107
|
export declare function useLocationClient(): LocationClientContextValue;
|
|
87
108
|
export {};
|