@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({ QueryText: query, MaxResults: 5 }),
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 or terrain).
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} refreshBuffer={60}>
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` (`GeoPlacesClient | null`) — The location client instance. Automatically refreshes the token before each `send()` call if needed.
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 automatically handles token lifecycle:
150
-
151
- 1. Fetches an initial token via `getConfig` on mount
152
- 2. Before each `client.send()` call, checks if the token is expired or within the `refreshBuffer` window
153
- 3. If expired, calls `getConfig` again to get a fresh token
154
- 4. Concurrent refresh requests are deduplicated — multiple `send()` calls wait for the same refresh
155
-
156
- No manual token management needed. The `client` always uses a valid token.
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 terrain, 3D buildings, and language baked into the descriptor
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({ QueryText: 'Vancouver', MaxResults: 5 }),
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({ QueryText: 'Vancouver' }),
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 so this package builds against the
8
- * currently published client; it matches `SendOptions` there exactly.
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
- * This application's own configuration, read from the access token —
32
- * bias precision, and the countries it is scoped to (api#65).
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 the `countries` value must not be
39
- * used to shape requests. The token is a snapshot; the API reads the scope
40
- * fresh from the application row on every call. Injecting a stale scope
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 so this package builds against the
8
- * currently published client; it matches `SendOptions` there exactly.
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
- * This application's own configuration, read from the access token —
32
- * bias precision, and the countries it is scoped to (api#65).
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 the `countries` value must not be
39
- * used to shape requests. The token is a snapshot; the API reads the scope
40
- * fresh from the application row on every call. Injecting a stale scope
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.5.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.3.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.6.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",