@chaosity/location-client-react 0.5.0 → 0.7.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
|
@@ -61,7 +61,12 @@ function SearchComponent() {
|
|
|
61
61
|
const searchPlaces = async (query: string) => {
|
|
62
62
|
if (!client) return
|
|
63
63
|
const response: SuggestCommandOutput = await client.send(
|
|
64
|
-
new SuggestCommand({
|
|
64
|
+
new SuggestCommand({
|
|
65
|
+
QueryText: query,
|
|
66
|
+
MaxResults: 5,
|
|
67
|
+
// Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
|
|
68
|
+
BiasPosition: [-123.1207, 49.2827],
|
|
69
|
+
}),
|
|
65
70
|
)
|
|
66
71
|
return response.ResultItems
|
|
67
72
|
}
|
|
@@ -77,7 +82,7 @@ function SearchComponent() {
|
|
|
77
82
|
|
|
78
83
|
### useMapLanguage
|
|
79
84
|
|
|
80
|
-
React hook that keeps map label language in sync. Automatically reapplies after `map.setStyle()` calls (e.g. when switching color schemes
|
|
85
|
+
React hook that keeps map label language in sync. Automatically reapplies after `map.setStyle()` calls (e.g. when switching color schemes).
|
|
81
86
|
|
|
82
87
|
```tsx
|
|
83
88
|
import { useMapLanguage } from '@chaosity/location-client-react'
|
|
@@ -116,7 +121,7 @@ function MapComponent() {
|
|
|
116
121
|
Provides the location client and automatic token refresh to all child components.
|
|
117
122
|
|
|
118
123
|
```tsx
|
|
119
|
-
<LocationClientProvider getConfig={getLocationConfig}
|
|
124
|
+
<LocationClientProvider getConfig={getLocationConfig}>
|
|
120
125
|
{children}
|
|
121
126
|
</LocationClientProvider>
|
|
122
127
|
```
|
|
@@ -124,9 +129,14 @@ Provides the location client and automatic token refresh to all child components
|
|
|
124
129
|
**Props:**
|
|
125
130
|
|
|
126
131
|
- `getConfig` — Async function that returns `{ apiUrl: string, token: string, expiresAt?: number }`. Called on init and whenever the token needs refreshing.
|
|
127
|
-
- `refreshBuffer` (optional, default: `60`) — Seconds before token expiry to proactively refresh. Prevents mid-request expiration.
|
|
128
132
|
- `children` — Child components.
|
|
129
133
|
|
|
134
|
+
There is no `refreshBuffer` prop. It was removed in `0.3.0` — a value shorter
|
|
135
|
+
than the server's own re-mint window made the client judge a token stale that
|
|
136
|
+
the server would not yet replace, and the two spun against each other. Both
|
|
137
|
+
sides now apply the same buffer to the token's own `exp`. Passing it does
|
|
138
|
+
nothing.
|
|
139
|
+
|
|
130
140
|
### useLocationClient
|
|
131
141
|
|
|
132
142
|
Hook to access the location client in any component.
|
|
@@ -137,7 +147,7 @@ const { client, getToken, loading, error } = useLocationClient()
|
|
|
137
147
|
|
|
138
148
|
**Returns:**
|
|
139
149
|
|
|
140
|
-
- `client` (`
|
|
150
|
+
- `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.
|
|
141
151
|
- `getToken` (`() => string | undefined`) — Returns the current token. Useful for direct API calls (e.g., map style fetch).
|
|
142
152
|
- `loading` (`boolean`) — Whether the client is initializing.
|
|
143
153
|
- `error` (`string | null`) — Error message if initialization or token refresh failed.
|
|
@@ -146,14 +156,31 @@ const { client, getToken, loading, error } = useLocationClient()
|
|
|
146
156
|
|
|
147
157
|
## Token Refresh
|
|
148
158
|
|
|
149
|
-
The provider
|
|
150
|
-
|
|
151
|
-
1.
|
|
152
|
-
2.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
159
|
+
The provider owns the token lifecycle. There is nothing to manage manually.
|
|
160
|
+
|
|
161
|
+
1. `getConfig` is called on mount for the initial token.
|
|
162
|
+
2. A timer refreshes **ahead of expiry**, 60 seconds before the token's own
|
|
163
|
+
`exp`. This is what keeps a map alive: MapLibre requests tiles, glyphs and
|
|
164
|
+
sprites directly, never through `send()`, so a refresh that happened only
|
|
165
|
+
inside `send()` would never fire for them.
|
|
166
|
+
3. `send()` and `verifyAddress()` check too, and refresh first if the token is
|
|
167
|
+
inside that window.
|
|
168
|
+
4. Returning to a backgrounded tab refreshes immediately — a throttled tab's
|
|
169
|
+
timer can be arbitrarily late.
|
|
170
|
+
5. If the API rejects a token **before** its `exp` — revoked from the portal, or
|
|
171
|
+
minted against a client secret since rotated — the 401 triggers a refresh and
|
|
172
|
+
the request is retried once with the new token. Nothing on this side has any
|
|
173
|
+
other reason to replace that token, so without this the failures continue
|
|
174
|
+
until the timer next comes around: for a token with 14 minutes left, 14
|
|
175
|
+
minutes of a broken page. The core added this in 0.7.0, so every core this
|
|
176
|
+
package's peer range admits has it.
|
|
177
|
+
6. Concurrent refreshes are deduplicated — everything waiting shares one call to
|
|
178
|
+
`getConfig`.
|
|
179
|
+
|
|
180
|
+
A refresh that fails is reported as `error` from `useLocationClient()`, and
|
|
181
|
+
rejects the `send()` or `verifyAddress()` that triggered it — with the refresh error rather than a
|
|
182
|
+
401, so the cause reads as the token endpoint being unreachable and not as the
|
|
183
|
+
API refusing you.
|
|
157
184
|
|
|
158
185
|
## Complete Example with MapLibre
|
|
159
186
|
|
|
@@ -179,6 +206,7 @@ export default function MapComponent() {
|
|
|
179
206
|
const mapContainer = useRef<HTMLDivElement>(null)
|
|
180
207
|
const map = useRef<maplibregl.Map | null>(null)
|
|
181
208
|
const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
|
|
209
|
+
const [mapError, setMapError] = useState<string | null>(null)
|
|
182
210
|
const [language, setLanguage] = useState('en')
|
|
183
211
|
const { client, getToken, loading, error } = useLocationClient()
|
|
184
212
|
|
|
@@ -188,11 +216,9 @@ export default function MapComponent() {
|
|
|
188
216
|
useEffect(() => {
|
|
189
217
|
if (!mapContainer.current || map.current || loading || !client) return
|
|
190
218
|
;(async () => {
|
|
191
|
-
// Fetch style with
|
|
219
|
+
// Fetch the style with the language baked into the descriptor
|
|
192
220
|
const style = await fetchMapStyle(API_URL, 'Standard', getToken, {
|
|
193
221
|
colorScheme: 'Light',
|
|
194
|
-
terrain: 'Terrain3D',
|
|
195
|
-
buildings: 'Buildings3D',
|
|
196
222
|
language,
|
|
197
223
|
})
|
|
198
224
|
|
|
@@ -201,18 +227,15 @@ export default function MapComponent() {
|
|
|
201
227
|
style,
|
|
202
228
|
center: [-123.12, 49.28],
|
|
203
229
|
zoom: 10,
|
|
204
|
-
maxPitch: 85,
|
|
205
230
|
transformRequest: createTransformRequest(API_URL, getToken),
|
|
206
231
|
})
|
|
232
|
+
// Held at once, so the catch and the cleanup below can remove it
|
|
233
|
+
map.current = instance
|
|
207
234
|
|
|
208
235
|
instance.addControl(
|
|
209
236
|
new maplibregl.NavigationControl({ visualizePitch: true }),
|
|
210
237
|
'top-right',
|
|
211
238
|
)
|
|
212
|
-
instance.addControl(
|
|
213
|
-
new maplibregl.TerrainControl({ source: 'amazon' }),
|
|
214
|
-
'top-right',
|
|
215
|
-
)
|
|
216
239
|
|
|
217
240
|
const geoPlaces = new GeoPlaces(client, instance)
|
|
218
241
|
const geocoder = new MaplibreGeocoder(geoPlaces, {
|
|
@@ -222,9 +245,15 @@ export default function MapComponent() {
|
|
|
222
245
|
})
|
|
223
246
|
instance.addControl(geocoder, 'top-left')
|
|
224
247
|
|
|
225
|
-
map.current = instance
|
|
226
248
|
setMapInstance(instance)
|
|
227
|
-
})()
|
|
249
|
+
})().catch((err: unknown) => {
|
|
250
|
+
// A refused style request lands here, and its message says why — for
|
|
251
|
+
// an option outside the application's plan, it names the feature.
|
|
252
|
+
// Anything that failed after the map was built removes it too.
|
|
253
|
+
map.current?.remove()
|
|
254
|
+
map.current = null
|
|
255
|
+
setMapError(err instanceof Error ? err.message : String(err))
|
|
256
|
+
})
|
|
228
257
|
|
|
229
258
|
return () => {
|
|
230
259
|
if (map.current) {
|
|
@@ -236,12 +265,36 @@ export default function MapComponent() {
|
|
|
236
265
|
}, [client, getToken, loading])
|
|
237
266
|
|
|
238
267
|
if (error) return <div>Error: {error}</div>
|
|
268
|
+
if (mapError) return <div>Map unavailable: {mapError}</div>
|
|
239
269
|
if (loading) return <div>Loading map...</div>
|
|
240
270
|
|
|
241
271
|
return <div ref={mapContainer} style={{ width: '100%', height: '600px' }} />
|
|
242
272
|
}
|
|
243
273
|
```
|
|
244
274
|
|
|
275
|
+
Some map options are features of the application's plan, and a plan without
|
|
276
|
+
one refuses the style request with 403 `FeatureNotEntitledException`, which the
|
|
277
|
+
`catch` above puts on screen. 3D terrain and buildings need the `terrain` and `buildings` plan features:
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
const style = await fetchMapStyle(API_URL, 'Standard', getToken, {
|
|
281
|
+
colorScheme: 'Light',
|
|
282
|
+
terrain: 'Terrain3D',
|
|
283
|
+
buildings: 'Buildings3D',
|
|
284
|
+
language,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// …then `maxPitch: 85` on the map, and a control to toggle the terrain. The
|
|
288
|
+
// descriptor names its own elevation source, so read it rather than typing it:
|
|
289
|
+
instance.addControl(
|
|
290
|
+
new maplibregl.TerrainControl({ source: style.terrain!.source }),
|
|
291
|
+
'top-right',
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`@chaosity/location-client`'s README lists every plan feature and what asks
|
|
296
|
+
for it.
|
|
297
|
+
|
|
245
298
|
### useMapLanguage
|
|
246
299
|
|
|
247
300
|
Hook that keeps map labels in the specified language. Registers a persistent `style.load` listener so language is automatically reapplied after `map.setStyle()` calls.
|
|
@@ -275,13 +328,62 @@ function MyComponent() {
|
|
|
275
328
|
|
|
276
329
|
const search = async () => {
|
|
277
330
|
const response: SuggestCommandOutput = await client!.send(
|
|
278
|
-
new SuggestCommand({
|
|
331
|
+
new SuggestCommand({
|
|
332
|
+
QueryText: 'Vancouver',
|
|
333
|
+
MaxResults: 5,
|
|
334
|
+
// Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
|
|
335
|
+
BiasPosition: [-123.1207, 49.2827],
|
|
336
|
+
}),
|
|
279
337
|
)
|
|
280
338
|
return response.ResultItems
|
|
281
339
|
}
|
|
282
340
|
}
|
|
283
341
|
```
|
|
284
342
|
|
|
343
|
+
## Verifying an address
|
|
344
|
+
|
|
345
|
+
`POST /address/verify` resolves the PlaceId a person chose — a building, or a
|
|
346
|
+
unit from its `SecondaryAddresses` — to the full place record plus `verified`.
|
|
347
|
+
It is the one Places result you may store; the core package's README has the
|
|
348
|
+
whole flow. **Needs `@chaosity/location-client` 0.10.0 or later**, which is this
|
|
349
|
+
package's peer range from the release that adds `verifyAddress`.
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { useLocationClient } from '@chaosity/location-client-react'
|
|
353
|
+
|
|
354
|
+
function useVerifyOnSubmit() {
|
|
355
|
+
const { client } = useLocationClient()
|
|
356
|
+
|
|
357
|
+
// Call it from the submit handler, once per chosen PlaceId.
|
|
358
|
+
return async (placeId: string) => {
|
|
359
|
+
const answer = await client!.verifyAddress(placeId)
|
|
360
|
+
return answer.verified ? answer : undefined // `answer` is what you may keep
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The same request as a command, through `send`:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
import {
|
|
369
|
+
VerifyAddressCommand,
|
|
370
|
+
type VerifyAddressResponse,
|
|
371
|
+
} from '@chaosity/location-client'
|
|
372
|
+
|
|
373
|
+
const answer: VerifyAddressResponse = await client!.send(
|
|
374
|
+
new VerifyAddressCommand({ PlaceId: placeId }),
|
|
375
|
+
)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Either form goes through the provider's pre-send refresh and its 401 retry,
|
|
379
|
+
exactly as `send` does. A `verified: false` resolves; it is not an error.
|
|
380
|
+
|
|
381
|
+
**Every verify is billed, whether or not the address verifies.** That is why
|
|
382
|
+
there is no `useVerifyAddress` hook. A hook keyed on a PlaceId would call on
|
|
383
|
+
every pick, including picks nobody submits, so the call belongs in your submit
|
|
384
|
+
handler instead. Keeping one answer per PlaceId is up to you: a verify result
|
|
385
|
+
may be stored, so keep it for as long as your form lives.
|
|
386
|
+
|
|
285
387
|
## Logging
|
|
286
388
|
|
|
287
389
|
Enable debug logging with the `DEBUG` environment variable:
|
|
@@ -302,7 +404,11 @@ import {
|
|
|
302
404
|
|
|
303
405
|
const { client } = useLocationClient()
|
|
304
406
|
const response: SuggestCommandOutput = await client!.send(
|
|
305
|
-
new SuggestCommand({
|
|
407
|
+
new SuggestCommand({
|
|
408
|
+
QueryText: 'Vancouver',
|
|
409
|
+
// Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
|
|
410
|
+
BiasPosition: [-123.1207, 49.2827],
|
|
411
|
+
}),
|
|
306
412
|
)
|
|
307
413
|
```
|
|
308
414
|
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import type { ClientConfig } from '@chaosity/location-client';
|
|
1
|
+
import type { ClientConfig, VerifyAddressResponse } from '@chaosity/location-client';
|
|
2
2
|
import { type AppConfigClaims } from '@chaosity/location-client';
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
/**
|
|
5
5
|
* Per-request transport options.
|
|
6
6
|
*
|
|
7
|
-
* Declared structurally rather than imported
|
|
8
|
-
*
|
|
7
|
+
* Declared structurally rather than imported, like `LocationClient` below. A
|
|
8
|
+
* hand copy does not move when the core does — `overallTimeoutMs` reached the
|
|
9
|
+
* core in 0.8.0 and not this copy (#26) — so `test/core-surface.test.ts` fails
|
|
10
|
+
* while the core's `SendOptions` has a key this one lacks.
|
|
9
11
|
*/
|
|
10
12
|
export interface SendOptions {
|
|
13
|
+
/** Caller cancellation. Aborting rejects with code `AbortedException`. */
|
|
11
14
|
signal?: AbortSignal;
|
|
15
|
+
/** Per ATTEMPT, not for the whole call. */
|
|
12
16
|
timeoutMs?: number;
|
|
17
|
+
/** The whole call: attempts and the waits between them. Core 0.8.0+. */
|
|
18
|
+
overallTimeoutMs?: number;
|
|
19
|
+
/** `false` disables retries entirely. */
|
|
13
20
|
retry?: false | {
|
|
14
21
|
maxAttempts?: number;
|
|
15
22
|
};
|
|
@@ -21,6 +28,9 @@ export interface SendOptions {
|
|
|
21
28
|
* real client to refresh tokens first, and a class with private fields is not
|
|
22
29
|
* structurally assignable — which is why this used to be an `Object.create`
|
|
23
30
|
* prototype hack.
|
|
31
|
+
*
|
|
32
|
+
* It must carry every public member of the core client this package builds
|
|
33
|
+
* with: `test/core-surface.test.ts` fails when the core gains one (#26).
|
|
24
34
|
*/
|
|
25
35
|
export interface LocationClient {
|
|
26
36
|
readonly config: {
|
|
@@ -28,21 +38,35 @@ export interface LocationClient {
|
|
|
28
38
|
};
|
|
29
39
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
30
40
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
41
|
+
* Verify a PlaceId through `POST /address/verify`: the full place record plus
|
|
42
|
+
* `verified`, the one Places result an integrator may store (#26). A
|
|
43
|
+
* `verified: false` resolves; it is not an error. Refreshes a stale token
|
|
44
|
+
* first, as `send` does.
|
|
45
|
+
*
|
|
46
|
+
* Needs @chaosity/location-client 0.10.0 or later, the peer range's floor.
|
|
47
|
+
* `send(new VerifyAddressCommand({ PlaceId }))` is the same request.
|
|
48
|
+
*
|
|
49
|
+
* Billed per call, whether or not the address verifies: call it once per
|
|
50
|
+
* chosen PlaceId, at submit.
|
|
51
|
+
*/
|
|
52
|
+
verifyAddress(placeId: string, options?: SendOptions): Promise<VerifyAddressResponse>;
|
|
53
|
+
/**
|
|
54
|
+
* This application's own configuration, read from the access token
|
|
55
|
+
* (api#65). The fields are whatever the installed @chaosity/location-client
|
|
56
|
+
* reads — its `AppConfigClaims` is the list, and this passes that client's
|
|
57
|
+
* answer through unchanged. So a field the core adds appears here with no
|
|
58
|
+
* change to this package, and is absent under a core too old to read it;
|
|
59
|
+
* the peer range cannot say which.
|
|
33
60
|
*
|
|
34
61
|
* Here so a React app can SHOW its own settings: populate a country
|
|
35
62
|
* selector with the markets it serves, label a settings screen. Being a
|
|
36
63
|
* few minutes stale is cosmetic for that.
|
|
37
64
|
*
|
|
38
|
-
* It is not an entitlement check, and
|
|
39
|
-
*
|
|
40
|
-
* fresh from the application
|
|
41
|
-
* turns a request that would have succeeded into a 400. See
|
|
65
|
+
* It is not an entitlement check, and none of it may be used to shape or
|
|
66
|
+
* refuse requests. The token is a snapshot; the API reads every setting
|
|
67
|
+
* fresh from the application on every call. Injecting a stale country
|
|
68
|
+
* scope turns a request that would have succeeded into a 400. See
|
|
42
69
|
* `AppConfigClaims` in @chaosity/location-client for the measurement.
|
|
43
|
-
*
|
|
44
|
-
* Returns `{}` when the token carries no application config, which is the
|
|
45
|
-
* case until one is set in the portal.
|
|
46
70
|
*/
|
|
47
71
|
getAppConfig(): AppConfigClaims;
|
|
48
72
|
}
|
|
@@ -166,6 +166,28 @@ function LocationClientProvider({ children, getConfig, }) {
|
|
|
166
166
|
apiUrl: cfg.apiUrl,
|
|
167
167
|
token: cfg.token,
|
|
168
168
|
getToken,
|
|
169
|
+
/**
|
|
170
|
+
* The 401 escape hatch (#19).
|
|
171
|
+
*
|
|
172
|
+
* Covers what the timer cannot: a token revoked from the portal, or
|
|
173
|
+
* minted against a client secret since rotated, is refused by the API
|
|
174
|
+
* while still minutes from its own `exp` — so nothing on this side has
|
|
175
|
+
* any reason to replace it, and every request fails until the buffer
|
|
176
|
+
* finally comes around. `getToken` cannot help, being synchronous.
|
|
177
|
+
*
|
|
178
|
+
* The client awaits this after a 401 and retries the request once with
|
|
179
|
+
* what it returns; the same token, or nothing, means no retry, so a
|
|
180
|
+
* doomed request is never sent — or billed — twice.
|
|
181
|
+
*
|
|
182
|
+
* It REJECTS when the refresh itself fails, and that is left to
|
|
183
|
+
* propagate out of `send` deliberately: the consumer learns the token
|
|
184
|
+
* endpoint is down rather than being told the API rejected them. Same
|
|
185
|
+
* answer the pre-flight `ensureValidToken` path already gives.
|
|
186
|
+
*/
|
|
187
|
+
refreshToken: async () => {
|
|
188
|
+
await refreshToken();
|
|
189
|
+
return tokenRef.current;
|
|
190
|
+
},
|
|
169
191
|
});
|
|
170
192
|
// A plain object, not Object.create(baseClient): the prototype hack was
|
|
171
193
|
// opaque, and its `send` dropped the second argument entirely — so once
|
|
@@ -177,6 +199,12 @@ function LocationClientProvider({ children, getConfig, }) {
|
|
|
177
199
|
await ensureValidTokenRef.current();
|
|
178
200
|
return baseClient.send(command, options);
|
|
179
201
|
},
|
|
202
|
+
// Behind the same pre-send refresh as `send`: forwarding it bare
|
|
203
|
+
// would send a stale token that `send` would have replaced (#26).
|
|
204
|
+
async verifyAddress(placeId, options) {
|
|
205
|
+
await ensureValidTokenRef.current();
|
|
206
|
+
return baseClient.verifyAddress(placeId, options);
|
|
207
|
+
},
|
|
180
208
|
// Reads whatever token the client currently holds. Deliberately not
|
|
181
209
|
// awaiting a refresh: this is display data, callers expect it to be
|
|
182
210
|
// synchronous, and a token that is minutes from expiry carries the
|
|
@@ -203,7 +231,7 @@ function LocationClientProvider({ children, getConfig, }) {
|
|
|
203
231
|
if (timerRef.current)
|
|
204
232
|
clearTimeout(timerRef.current);
|
|
205
233
|
};
|
|
206
|
-
}, [getToken]);
|
|
234
|
+
}, [getToken, refreshToken]);
|
|
207
235
|
return ((0, jsx_runtime_1.jsx)(LocationClientContext.Provider, { value: { client, getToken, loading, error }, children: children }));
|
|
208
236
|
}
|
|
209
237
|
function useLocationClient() {
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import type { ClientConfig } from '@chaosity/location-client';
|
|
1
|
+
import type { ClientConfig, VerifyAddressResponse } from '@chaosity/location-client';
|
|
2
2
|
import { type AppConfigClaims } from '@chaosity/location-client';
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
4
|
/**
|
|
5
5
|
* Per-request transport options.
|
|
6
6
|
*
|
|
7
|
-
* Declared structurally rather than imported
|
|
8
|
-
*
|
|
7
|
+
* Declared structurally rather than imported, like `LocationClient` below. A
|
|
8
|
+
* hand copy does not move when the core does — `overallTimeoutMs` reached the
|
|
9
|
+
* core in 0.8.0 and not this copy (#26) — so `test/core-surface.test.ts` fails
|
|
10
|
+
* while the core's `SendOptions` has a key this one lacks.
|
|
9
11
|
*/
|
|
10
12
|
export interface SendOptions {
|
|
13
|
+
/** Caller cancellation. Aborting rejects with code `AbortedException`. */
|
|
11
14
|
signal?: AbortSignal;
|
|
15
|
+
/** Per ATTEMPT, not for the whole call. */
|
|
12
16
|
timeoutMs?: number;
|
|
17
|
+
/** The whole call: attempts and the waits between them. Core 0.8.0+. */
|
|
18
|
+
overallTimeoutMs?: number;
|
|
19
|
+
/** `false` disables retries entirely. */
|
|
13
20
|
retry?: false | {
|
|
14
21
|
maxAttempts?: number;
|
|
15
22
|
};
|
|
@@ -21,6 +28,9 @@ export interface SendOptions {
|
|
|
21
28
|
* real client to refresh tokens first, and a class with private fields is not
|
|
22
29
|
* structurally assignable — which is why this used to be an `Object.create`
|
|
23
30
|
* prototype hack.
|
|
31
|
+
*
|
|
32
|
+
* It must carry every public member of the core client this package builds
|
|
33
|
+
* with: `test/core-surface.test.ts` fails when the core gains one (#26).
|
|
24
34
|
*/
|
|
25
35
|
export interface LocationClient {
|
|
26
36
|
readonly config: {
|
|
@@ -28,21 +38,35 @@ export interface LocationClient {
|
|
|
28
38
|
};
|
|
29
39
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
30
40
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
41
|
+
* Verify a PlaceId through `POST /address/verify`: the full place record plus
|
|
42
|
+
* `verified`, the one Places result an integrator may store (#26). A
|
|
43
|
+
* `verified: false` resolves; it is not an error. Refreshes a stale token
|
|
44
|
+
* first, as `send` does.
|
|
45
|
+
*
|
|
46
|
+
* Needs @chaosity/location-client 0.10.0 or later, the peer range's floor.
|
|
47
|
+
* `send(new VerifyAddressCommand({ PlaceId }))` is the same request.
|
|
48
|
+
*
|
|
49
|
+
* Billed per call, whether or not the address verifies: call it once per
|
|
50
|
+
* chosen PlaceId, at submit.
|
|
51
|
+
*/
|
|
52
|
+
verifyAddress(placeId: string, options?: SendOptions): Promise<VerifyAddressResponse>;
|
|
53
|
+
/**
|
|
54
|
+
* This application's own configuration, read from the access token
|
|
55
|
+
* (api#65). The fields are whatever the installed @chaosity/location-client
|
|
56
|
+
* reads — its `AppConfigClaims` is the list, and this passes that client's
|
|
57
|
+
* answer through unchanged. So a field the core adds appears here with no
|
|
58
|
+
* change to this package, and is absent under a core too old to read it;
|
|
59
|
+
* the peer range cannot say which.
|
|
33
60
|
*
|
|
34
61
|
* Here so a React app can SHOW its own settings: populate a country
|
|
35
62
|
* selector with the markets it serves, label a settings screen. Being a
|
|
36
63
|
* few minutes stale is cosmetic for that.
|
|
37
64
|
*
|
|
38
|
-
* It is not an entitlement check, and
|
|
39
|
-
*
|
|
40
|
-
* fresh from the application
|
|
41
|
-
* turns a request that would have succeeded into a 400. See
|
|
65
|
+
* It is not an entitlement check, and none of it may be used to shape or
|
|
66
|
+
* refuse requests. The token is a snapshot; the API reads every setting
|
|
67
|
+
* fresh from the application on every call. Injecting a stale country
|
|
68
|
+
* scope turns a request that would have succeeded into a 400. See
|
|
42
69
|
* `AppConfigClaims` in @chaosity/location-client for the measurement.
|
|
43
|
-
*
|
|
44
|
-
* Returns `{}` when the token carries no application config, which is the
|
|
45
|
-
* case until one is set in the portal.
|
|
46
70
|
*/
|
|
47
71
|
getAppConfig(): AppConfigClaims;
|
|
48
72
|
}
|
|
@@ -159,6 +159,28 @@ export function LocationClientProvider({ children, getConfig, }) {
|
|
|
159
159
|
apiUrl: cfg.apiUrl,
|
|
160
160
|
token: cfg.token,
|
|
161
161
|
getToken,
|
|
162
|
+
/**
|
|
163
|
+
* The 401 escape hatch (#19).
|
|
164
|
+
*
|
|
165
|
+
* Covers what the timer cannot: a token revoked from the portal, or
|
|
166
|
+
* minted against a client secret since rotated, is refused by the API
|
|
167
|
+
* while still minutes from its own `exp` — so nothing on this side has
|
|
168
|
+
* any reason to replace it, and every request fails until the buffer
|
|
169
|
+
* finally comes around. `getToken` cannot help, being synchronous.
|
|
170
|
+
*
|
|
171
|
+
* The client awaits this after a 401 and retries the request once with
|
|
172
|
+
* what it returns; the same token, or nothing, means no retry, so a
|
|
173
|
+
* doomed request is never sent — or billed — twice.
|
|
174
|
+
*
|
|
175
|
+
* It REJECTS when the refresh itself fails, and that is left to
|
|
176
|
+
* propagate out of `send` deliberately: the consumer learns the token
|
|
177
|
+
* endpoint is down rather than being told the API rejected them. Same
|
|
178
|
+
* answer the pre-flight `ensureValidToken` path already gives.
|
|
179
|
+
*/
|
|
180
|
+
refreshToken: async () => {
|
|
181
|
+
await refreshToken();
|
|
182
|
+
return tokenRef.current;
|
|
183
|
+
},
|
|
162
184
|
});
|
|
163
185
|
// A plain object, not Object.create(baseClient): the prototype hack was
|
|
164
186
|
// opaque, and its `send` dropped the second argument entirely — so once
|
|
@@ -170,6 +192,12 @@ export function LocationClientProvider({ children, getConfig, }) {
|
|
|
170
192
|
await ensureValidTokenRef.current();
|
|
171
193
|
return baseClient.send(command, options);
|
|
172
194
|
},
|
|
195
|
+
// Behind the same pre-send refresh as `send`: forwarding it bare
|
|
196
|
+
// would send a stale token that `send` would have replaced (#26).
|
|
197
|
+
async verifyAddress(placeId, options) {
|
|
198
|
+
await ensureValidTokenRef.current();
|
|
199
|
+
return baseClient.verifyAddress(placeId, options);
|
|
200
|
+
},
|
|
173
201
|
// Reads whatever token the client currently holds. Deliberately not
|
|
174
202
|
// awaiting a refresh: this is display data, callers expect it to be
|
|
175
203
|
// synchronous, and a token that is minutes from expiry carries the
|
|
@@ -196,7 +224,7 @@ export function LocationClientProvider({ children, getConfig, }) {
|
|
|
196
224
|
if (timerRef.current)
|
|
197
225
|
clearTimeout(timerRef.current);
|
|
198
226
|
};
|
|
199
|
-
}, [getToken]);
|
|
227
|
+
}, [getToken, refreshToken]);
|
|
200
228
|
return (_jsx(LocationClientContext.Provider, { value: { client, getToken, loading, error }, children: children }));
|
|
201
229
|
}
|
|
202
230
|
export function useLocationClient() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaosity/location-client-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "React bindings for Chaosity Location Service client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"debug": "^4.4.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@chaosity/location-client": ">=0.
|
|
41
|
+
"@chaosity/location-client": ">=0.10.0",
|
|
42
42
|
"maplibre-gl": "^5.0.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@chaosity/location-client": "^0.
|
|
51
|
+
"@chaosity/location-client": "^0.10.0",
|
|
52
52
|
"@eslint/js": "^10.0.1",
|
|
53
53
|
"@testing-library/react": "^16.0.0",
|
|
54
54
|
"@testing-library/user-event": "^14.0.0",
|