@chaosity/location-client-react 0.7.0 → 0.8.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
|
@@ -128,7 +128,8 @@ Provides the location client and automatic token refresh to all child components
|
|
|
128
128
|
|
|
129
129
|
**Props:**
|
|
130
130
|
|
|
131
|
-
- `getConfig` — Async function that returns `{ apiUrl: string, token: string, expiresAt?: number }`. Called on
|
|
131
|
+
- `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)).
|
|
132
|
+
- `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
133
|
- `children` — Child components.
|
|
133
134
|
|
|
134
135
|
There is no `refreshBuffer` prop. It was removed in `0.3.0` — a value shorter
|
|
@@ -137,20 +138,47 @@ the server would not yet replace, and the two spun against each other. Both
|
|
|
137
138
|
sides now apply the same buffer to the token's own `exp`. Passing it does
|
|
138
139
|
nothing.
|
|
139
140
|
|
|
141
|
+
#### Switching organisation or application
|
|
142
|
+
|
|
143
|
+
Either of these is correct:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// Keeps the children mounted: only the client is replaced.
|
|
147
|
+
<LocationClientProvider configKey={orgId} getConfig={getLocationConfig}>
|
|
148
|
+
|
|
149
|
+
// Remounts everything below the provider, and its state with it.
|
|
150
|
+
<LocationClientProvider key={orgId} getConfig={getLocationConfig}>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Without either, the provider goes on using the old configuration until its
|
|
154
|
+
next token refresh, up to 14 minutes later. A new `getConfig` function is not
|
|
155
|
+
a signal on its own, because it is usually a new function on every render.
|
|
156
|
+
|
|
157
|
+
After a switch, a client or `getToken` kept from before it refuses: `send()`
|
|
158
|
+
and `verifyAddress()` reject, `getToken()` returns `undefined`, and
|
|
159
|
+
`getAppConfig()` answers `{}`. So one configuration's token never reaches
|
|
160
|
+
another's URL. Build anything that holds
|
|
161
|
+
on to them, such as a MapLibre map or a geocoder, from the context's current
|
|
162
|
+
values, and rebuild it when `client`, `getToken` or `apiUrl` changes, as the
|
|
163
|
+
[complete example](#complete-example-with-maplibre) does. The same happens
|
|
164
|
+
without a `configKey` when `getConfig` starts answering with a different
|
|
165
|
+
`apiUrl`.
|
|
166
|
+
|
|
140
167
|
### useLocationClient
|
|
141
168
|
|
|
142
169
|
Hook to access the location client in any component.
|
|
143
170
|
|
|
144
171
|
```tsx
|
|
145
|
-
const { client, getToken, loading, error } = useLocationClient()
|
|
172
|
+
const { client, getToken, apiUrl, loading, error } = useLocationClient()
|
|
146
173
|
```
|
|
147
174
|
|
|
148
175
|
**Returns:**
|
|
149
176
|
|
|
150
177
|
- `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
|
-
- `
|
|
178
|
+
- `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.
|
|
179
|
+
- `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.
|
|
180
|
+
- `loading` (`boolean`) — Whether the client is initializing. `true` again while a new `configKey` loads.
|
|
181
|
+
- `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
182
|
|
|
155
183
|
**Throws:** Error if used outside `LocationClientProvider`.
|
|
156
184
|
|
|
@@ -176,11 +204,46 @@ The provider owns the token lifecycle. There is nothing to manage manually.
|
|
|
176
204
|
package's peer range admits has it.
|
|
177
205
|
6. Concurrent refreshes are deduplicated — everything waiting shares one call to
|
|
178
206
|
`getConfig`.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
207
|
+
7. A failed `getConfig` is retried on the provider's own timer, backing off
|
|
208
|
+
exponentially from 1 to 30 seconds, with jitter. When `getConfig` rejects
|
|
209
|
+
with an error that has `retryAfterMs` (milliseconds), it waits that long
|
|
210
|
+
instead. Until then nothing else asks, neither a map reading its token for
|
|
211
|
+
every tile nor a `send()`. The tab coming back into view, or the browser
|
|
212
|
+
coming back online, retries at once, unless the wait was a `retryAfterMs`.
|
|
213
|
+
|
|
214
|
+
A Server Action, as in the Quick Start, cannot pass `retryAfterMs` on:
|
|
215
|
+
React sends a thrown error to the browser without its own fields. To honour
|
|
216
|
+
a token route's `Retry-After`, call the route from the browser and throw it
|
|
217
|
+
there:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
async function getConfig() {
|
|
221
|
+
const res = await fetch('/api/location-token')
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
const retryAfter = Number(res.headers.get('retry-after'))
|
|
224
|
+
throw Object.assign(new Error(`token route answered ${res.status}`), {
|
|
225
|
+
retryAfterMs: retryAfter > 0 ? retryAfter * 1000 : undefined,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
return res.json()
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
8. That includes the first call. A page whose first `getConfig` fails gets its
|
|
233
|
+
client when a retry succeeds, without a reload.
|
|
234
|
+
9. A token that arrives already stale by the browser's clock, because that
|
|
235
|
+
clock runs ahead of the server's, is not asked for again at once: the
|
|
236
|
+
server would hand back the same one. The provider waits, longer each time,
|
|
237
|
+
and sends with the token it has. A 401 still replaces it straight away.
|
|
238
|
+
|
|
239
|
+
A refresh that fails is reported as `error` from `useLocationClient()` until a
|
|
240
|
+
retry succeeds. Meanwhile every request goes out with the token in hand for as
|
|
241
|
+
long as that token is before its own expiry: `send()`, `verifyAddress()` and a
|
|
242
|
+
map's tiles alike, because the API still accepts it. Once it has expired,
|
|
243
|
+
`send()` and `verifyAddress()` reject with the refresh error rather than a 401,
|
|
244
|
+
so the cause reads as the token endpoint being unreachable, not as the API
|
|
245
|
+
refusing you. A token the API refuses (a 401) is never sent again: that
|
|
246
|
+
request rejects with the refresh error too.
|
|
184
247
|
|
|
185
248
|
## Complete Example with MapLibre
|
|
186
249
|
|
|
@@ -200,24 +263,24 @@ import {
|
|
|
200
263
|
import maplibregl from 'maplibre-gl'
|
|
201
264
|
import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder'
|
|
202
265
|
|
|
203
|
-
const API_URL = process.env.NEXT_PUBLIC_LOCATION_API_URL!
|
|
204
|
-
|
|
205
266
|
export default function MapComponent() {
|
|
206
267
|
const mapContainer = useRef<HTMLDivElement>(null)
|
|
207
268
|
const map = useRef<maplibregl.Map | null>(null)
|
|
208
269
|
const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
|
|
209
270
|
const [mapError, setMapError] = useState<string | null>(null)
|
|
210
271
|
const [language, setLanguage] = useState('en')
|
|
211
|
-
const { client, getToken, loading, error } = useLocationClient()
|
|
272
|
+
const { client, getToken, apiUrl, loading, error } = useLocationClient()
|
|
212
273
|
|
|
213
274
|
// Keeps map labels in sync with language — reapplies after every setStyle() call
|
|
214
275
|
useMapLanguage(mapInstance, language)
|
|
215
276
|
|
|
216
277
|
useEffect(() => {
|
|
217
|
-
if (!mapContainer.current || map.current || loading || !client)
|
|
278
|
+
if (!mapContainer.current || map.current || loading || !client || !apiUrl)
|
|
279
|
+
return
|
|
218
280
|
;(async () => {
|
|
219
|
-
// Fetch the style with the language baked into the descriptor
|
|
220
|
-
|
|
281
|
+
// Fetch the style with the language baked into the descriptor. The URL
|
|
282
|
+
// comes from the context with the token, so the two always belong together.
|
|
283
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
221
284
|
colorScheme: 'Light',
|
|
222
285
|
language,
|
|
223
286
|
})
|
|
@@ -227,7 +290,7 @@ export default function MapComponent() {
|
|
|
227
290
|
style,
|
|
228
291
|
center: [-123.12, 49.28],
|
|
229
292
|
zoom: 10,
|
|
230
|
-
transformRequest: createTransformRequest(
|
|
293
|
+
transformRequest: createTransformRequest(apiUrl, getToken),
|
|
231
294
|
})
|
|
232
295
|
// Held at once, so the catch and the cleanup below can remove it
|
|
233
296
|
map.current = instance
|
|
@@ -262,7 +325,7 @@ export default function MapComponent() {
|
|
|
262
325
|
setMapInstance(null)
|
|
263
326
|
}
|
|
264
327
|
}
|
|
265
|
-
}, [client, getToken, loading])
|
|
328
|
+
}, [client, getToken, apiUrl, loading])
|
|
266
329
|
|
|
267
330
|
if (error) return <div>Error: {error}</div>
|
|
268
331
|
if (mapError) return <div>Map unavailable: {mapError}</div>
|
|
@@ -277,7 +340,7 @@ one refuses the style request with 403 `FeatureNotEntitledException`, which the
|
|
|
277
340
|
`catch` above puts on screen. 3D terrain and buildings need the `terrain` and `buildings` plan features:
|
|
278
341
|
|
|
279
342
|
```tsx
|
|
280
|
-
const style = await fetchMapStyle(
|
|
343
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
281
344
|
colorScheme: 'Light',
|
|
282
345
|
terrain: 'Terrain3D',
|
|
283
346
|
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 {};
|