@chaosity/location-client 0.12.0 → 0.13.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 +118 -36
- package/dist/adapters/GeoPlaces.d.ts +7 -1
- package/dist/adapters/GeoPlaces.js +11 -5
- package/dist/auth/TokenProvider.d.ts +22 -1
- package/dist/auth/TokenProvider.js +42 -2
- package/dist/auth/tokenHold.d.ts +14 -5
- package/dist/auth/tokenHold.js +70 -5
- package/dist/aws.d.ts +4 -0
- package/dist/aws.js +2 -0
- package/dist/cjs/adapters/GeoPlaces.d.ts +7 -1
- package/dist/cjs/adapters/GeoPlaces.js +10 -4
- package/dist/cjs/auth/TokenProvider.d.ts +22 -1
- package/dist/cjs/auth/TokenProvider.js +42 -2
- package/dist/cjs/auth/tokenHold.d.ts +14 -5
- package/dist/cjs/auth/tokenHold.js +71 -5
- package/dist/cjs/aws.d.ts +4 -0
- package/dist/cjs/aws.js +155 -0
- package/dist/cjs/client/GeoPlacesClient.d.ts +17 -1
- package/dist/cjs/client/GeoPlacesClient.js +18 -62
- package/dist/cjs/index.d.ts +4 -3
- package/dist/cjs/index.js +13 -8
- package/dist/cjs/maps/createTransformRequest.d.ts +25 -0
- package/dist/cjs/maps/createTransformRequest.js +1 -0
- package/dist/cjs/maps/mapPoi.d.ts +10 -5
- package/dist/cjs/maps/mapPoi.js +14 -5
- package/dist/cjs/maps/mapStyle.d.ts +9 -2
- package/dist/cjs/maps/mapStyle.js +16 -12
- package/dist/cjs/maps/mapToken.d.ts +84 -0
- package/dist/cjs/maps/mapToken.js +126 -0
- package/dist/cjs/maps/staticMap.d.ts +7 -3
- package/dist/cjs/maps/staticMap.js +13 -12
- package/dist/cjs/server/LocationServiceConnector.d.ts +18 -3
- package/dist/cjs/server/LocationServiceConnector.js +17 -58
- package/dist/cjs/server/getClientConfig.d.ts +7 -3
- package/dist/cjs/server/getClientConfig.js +9 -12
- package/dist/cjs/server/index.d.ts +1 -1
- package/dist/cjs/transport/http.d.ts +18 -0
- package/dist/cjs/transport/http.js +39 -1
- package/dist/cjs/types/index.d.ts +26 -0
- package/dist/client/GeoPlacesClient.d.ts +17 -1
- package/dist/client/GeoPlacesClient.js +21 -65
- package/dist/index.d.ts +4 -3
- package/dist/index.js +11 -7
- package/dist/maps/createTransformRequest.d.ts +25 -0
- package/dist/maps/createTransformRequest.js +1 -1
- package/dist/maps/mapPoi.d.ts +10 -5
- package/dist/maps/mapPoi.js +14 -5
- package/dist/maps/mapStyle.d.ts +9 -2
- package/dist/maps/mapStyle.js +17 -13
- package/dist/maps/mapToken.d.ts +84 -0
- package/dist/maps/mapToken.js +122 -0
- package/dist/maps/staticMap.d.ts +7 -3
- package/dist/maps/staticMap.js +14 -13
- package/dist/server/LocationServiceConnector.d.ts +18 -3
- package/dist/server/LocationServiceConnector.js +20 -61
- package/dist/server/getClientConfig.d.ts +7 -3
- package/dist/server/getClientConfig.js +9 -12
- package/dist/server/index.d.ts +1 -1
- package/dist/transport/http.d.ts +18 -0
- package/dist/transport/http.js +37 -1
- package/dist/types/index.d.ts +26 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@ AWS Location Service compatible client with custom Bearer token authentication.
|
|
|
22
22
|
npm install @chaosity/location-client
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
TypeScript users need TypeScript 5.0 or later: the declarations use
|
|
26
|
+
`export type *`.
|
|
27
|
+
|
|
25
28
|
The Places commands, the transport and the static-map and style-URL helpers need
|
|
26
29
|
nothing else. The **map** features are optional peer dependencies, so install
|
|
27
30
|
them only if you use them:
|
|
@@ -98,17 +101,13 @@ const config = await getClientConfig({
|
|
|
98
101
|
### Client-Side: Using the GeoPlacesClient
|
|
99
102
|
|
|
100
103
|
```typescript
|
|
101
|
-
import {
|
|
102
|
-
GeoPlacesClient,
|
|
103
|
-
SuggestCommand,
|
|
104
|
-
type SuggestCommandOutput,
|
|
105
|
-
} from '@chaosity/location-client'
|
|
104
|
+
import { GeoPlacesClient, SuggestCommand } from '@chaosity/location-client'
|
|
106
105
|
|
|
107
106
|
// apiUrl and token as getLocationConfig() above returns them
|
|
108
107
|
const { apiUrl, token } = await getLocationConfig()
|
|
109
108
|
const client = new GeoPlacesClient({ apiUrl, token })
|
|
110
109
|
|
|
111
|
-
const response
|
|
110
|
+
const response = await client.send(
|
|
112
111
|
new SuggestCommand({
|
|
113
112
|
QueryText: 'Vancouver',
|
|
114
113
|
MaxResults: 5,
|
|
@@ -116,8 +115,14 @@ const response: SuggestCommandOutput = await client.send(
|
|
|
116
115
|
BiasPosition: [-123.1207, 49.2827],
|
|
117
116
|
}),
|
|
118
117
|
)
|
|
118
|
+
response.ResultItems
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
`send` resolves with the command's own output type — `SuggestCommandOutput`
|
|
122
|
+
here — so there is nothing to annotate, on `GeoPlacesClient` and
|
|
123
|
+
`LocationServiceConnector` alike (since 0.13.0). `CommandOutput<C>` names it
|
|
124
|
+
when you need it.
|
|
125
|
+
|
|
121
126
|
### Verifying an address
|
|
122
127
|
|
|
123
128
|
`POST /address/verify` resolves one PlaceId to the full place record plus
|
|
@@ -129,15 +134,10 @@ Japan, which may not be stored at all.
|
|
|
129
134
|
The flow is suggest, then an optional unit pick, then verify at submit:
|
|
130
135
|
|
|
131
136
|
```typescript
|
|
132
|
-
import {
|
|
133
|
-
GetPlaceCommand,
|
|
134
|
-
SuggestCommand,
|
|
135
|
-
type GetPlaceCommandOutput,
|
|
136
|
-
type SuggestCommandOutput,
|
|
137
|
-
} from '@chaosity/location-client'
|
|
137
|
+
import { GetPlaceCommand, SuggestCommand } from '@chaosity/location-client'
|
|
138
138
|
|
|
139
139
|
// 1. While the person types: suggestions, for display.
|
|
140
|
-
const suggestions
|
|
140
|
+
const suggestions = await client.send(
|
|
141
141
|
// Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
|
|
142
142
|
new SuggestCommand({
|
|
143
143
|
QueryText: '100 George St, Sydney',
|
|
@@ -147,7 +147,7 @@ const suggestions: SuggestCommandOutput = await client.send(
|
|
|
147
147
|
const picked = suggestions.ResultItems?.[0]?.Place?.PlaceId
|
|
148
148
|
|
|
149
149
|
// 2. Optionally, offer the building's units. Each has its own PlaceId.
|
|
150
|
-
const building
|
|
150
|
+
const building = await client.send(
|
|
151
151
|
new GetPlaceCommand({
|
|
152
152
|
PlaceId: picked!,
|
|
153
153
|
AdditionalFeatures: ['SecondaryAddresses'],
|
|
@@ -221,6 +221,41 @@ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
|
221
221
|
// then `maxPitch: 85` on the map, so the camera can tilt to see them
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
+
#### When the API refuses the map's token
|
|
225
|
+
|
|
226
|
+
`getToken` is synchronous, so it can only hand over the token already in hand.
|
|
227
|
+
For when the API refuses that token before its `exp` — after the application's
|
|
228
|
+
secret is rotated, for one — pass `{ getToken, refreshToken }` where the map
|
|
229
|
+
helpers take `getToken`, and add `refreshTokenOnUnauthorized` for the tiles
|
|
230
|
+
MapLibre fetches itself:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import {
|
|
234
|
+
fetchMapStyle,
|
|
235
|
+
refreshTokenOnUnauthorized,
|
|
236
|
+
} from '@chaosity/location-client'
|
|
237
|
+
|
|
238
|
+
// refreshToken obtains a new token, and makes getToken return it from then on.
|
|
239
|
+
const tokens = { getToken, refreshToken }
|
|
240
|
+
|
|
241
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', tokens)
|
|
242
|
+
// ...the map as above, with createTransformRequest(apiUrl, getToken)...
|
|
243
|
+
const stop = refreshTokenOnUnauthorized(map, apiUrl, tokens)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`fetchMapStyle` and `fetchStaticMap` then answer a 401 by asking `refreshToken`
|
|
247
|
+
once and sending again with the new token, within the same `signal` and
|
|
248
|
+
`overallTimeoutMs`. `refreshTokenOnUnauthorized` listens for the map's `error`
|
|
249
|
+
events: on a 401 from your API it asks `refreshToken` once for the burst, and
|
|
250
|
+
reloads the refused tiles when `getToken` returns a different token. The
|
|
251
|
+
tiles receive the new token no other way — they read `getToken` through
|
|
252
|
+
`createTransformRequest` — so a `refreshToken` that leaves `getToken` unchanged
|
|
253
|
+
reloads nothing. A 403 is never retried: a new token cannot change it. A token
|
|
254
|
+
`refreshToken` could not replace is not asked about again for 30 seconds, by
|
|
255
|
+
any helper handed the same `tokens` object. `stop()` removes the listener.
|
|
256
|
+
|
|
257
|
+
A bare `getToken` works as it always has: one request, and its 401 to you.
|
|
258
|
+
|
|
224
259
|
### The MapLibre worker
|
|
225
260
|
|
|
226
261
|
MapLibre 6 loads and parses its tiles in a Web Worker, and it finds the
|
|
@@ -458,10 +493,13 @@ worth a second round trip. A 403 is never retried: a new token cannot fix an
|
|
|
458
493
|
**A refused token is not sent again for 30 seconds.** When the API refuses a
|
|
459
494
|
token and `refreshToken` cannot replace it — it rejects with a 401 or 403, or
|
|
460
495
|
returns the same token — the next sends with that token reject with the same
|
|
461
|
-
refusal without a request, and without asking `refreshToken`. A
|
|
462
|
-
|
|
463
|
-
send used to cost a refused request
|
|
464
|
-
|
|
496
|
+
refusal without a request, and without asking `refreshToken`. A rotated secret
|
|
497
|
+
is refused by the data route and, through a `refreshToken` that holds the old
|
|
498
|
+
secret, by the token route too, so each send used to cost a refused request
|
|
499
|
+
and a refused refresh. A suspended application is refused 403
|
|
500
|
+
`ApplicationNotActiveException`, which is never retried, so it is not held
|
|
501
|
+
here: each send has the API's answer. A different token from `getToken` ends
|
|
502
|
+
the wait at once. A `refreshToken` that fails with a
|
|
465
503
|
`Retry-After` is held for that long instead. One that fails with nothing to
|
|
466
504
|
say about when to try again — a network fault, or a Server Action's error,
|
|
467
505
|
which reaches the browser without its fields — is asked again on the next
|
|
@@ -477,7 +515,7 @@ Every call in this package takes the same options object — `client.send`,
|
|
|
477
515
|
await client.send(command, {
|
|
478
516
|
signal, // AbortSignal — cancels mid-flight AND mid-backoff
|
|
479
517
|
timeoutMs: 10_000, // per ATTEMPT
|
|
480
|
-
overallTimeoutMs: 30_000, // the whole call
|
|
518
|
+
overallTimeoutMs: 30_000, // the whole call: the wait for a token and between attempts
|
|
481
519
|
retry: { maxAttempts: 3 }, // or `false` for none
|
|
482
520
|
})
|
|
483
521
|
```
|
|
@@ -545,9 +583,9 @@ try {
|
|
|
545
583
|
}
|
|
546
584
|
```
|
|
547
585
|
|
|
548
|
-
`message` is the API's own sentence
|
|
549
|
-
`error_description
|
|
550
|
-
|
|
586
|
+
`message` is the API's own sentence — on `/auth/token`, its
|
|
587
|
+
`error_description` — so a suspended application's refusal names that cause
|
|
588
|
+
and points to the portal.
|
|
551
589
|
|
|
552
590
|
#### GeoPlaces Adapter
|
|
553
591
|
|
|
@@ -574,6 +612,13 @@ import { createTransformRequest } from '@chaosity/location-client'
|
|
|
574
612
|
const transformRequest = createTransformRequest(apiUrl, () => currentToken)
|
|
575
613
|
```
|
|
576
614
|
|
|
615
|
+
#### refreshTokenOnUnauthorized
|
|
616
|
+
|
|
617
|
+
`refreshTokenOnUnauthorized(map, apiUrl, { getToken, refreshToken })` reloads
|
|
618
|
+
the tiles the API refused once `refreshToken` has given `getToken` a new token,
|
|
619
|
+
and returns a function that stops listening. See
|
|
620
|
+
[When the API refuses the map's token](#when-the-api-refuses-the-maps-token).
|
|
621
|
+
|
|
577
622
|
#### fetchMapStyle
|
|
578
623
|
|
|
579
624
|
Fetches the map style descriptor with Bearer auth and applies optional language to the descriptor JSON before MapLibre processes it (eliminates the visual flash that occurs when modifying layers post-load).
|
|
@@ -589,6 +634,9 @@ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
|
589
634
|
```
|
|
590
635
|
|
|
591
636
|
Hand `style` to `new maplibregl.Map`, with the worker set, as in [MapLibre Map Integration](#maplibre-map-integration).
|
|
637
|
+
Pass `{ getToken, refreshToken }` in place of `getToken` to recover from a
|
|
638
|
+
refused token, as [When the API refuses the map's token](#when-the-api-refuses-the-maps-token)
|
|
639
|
+
shows; `fetchStaticMap` takes the same.
|
|
592
640
|
|
|
593
641
|
The overlays need the `terrain`, `buildings`, `contours`, `traffic` and `travel-modes` plan features — see [Plan features](#plan-features):
|
|
594
642
|
|
|
@@ -719,10 +767,9 @@ feature:
|
|
|
719
767
|
import {
|
|
720
768
|
SearchTextCommand,
|
|
721
769
|
searchTextResponseToFeatureCollection,
|
|
722
|
-
type SearchTextCommandOutput,
|
|
723
770
|
} from '@chaosity/location-client'
|
|
724
771
|
|
|
725
|
-
const response
|
|
772
|
+
const response = await client.send(
|
|
726
773
|
new SearchTextCommand({
|
|
727
774
|
QueryText: 'coffee',
|
|
728
775
|
// SearchText takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
|
|
@@ -754,14 +801,35 @@ const replacement = await getClientConfig({ forceRefresh: true })
|
|
|
754
801
|
```
|
|
755
802
|
|
|
756
803
|
A refusal arrives with the API's code and sentence. Where the API refused the
|
|
757
|
-
credentials themselves, the message goes on to say which variables to
|
|
758
|
-
and names the client ID. An application that is not active
|
|
759
|
-
`
|
|
760
|
-
|
|
761
|
-
says to check both. A refusal is remembered for 30 seconds, and a
|
|
804
|
+
credentials themselves, a 401, the message goes on to say which variables to
|
|
805
|
+
check, and names the client ID. An application that is not active is refused
|
|
806
|
+
403 `ApplicationNotActiveException`, in a sentence that names that cause, and
|
|
807
|
+
gets no advice added. A refusal is remembered for 30 seconds, and a
|
|
762
808
|
`Retry-After` for as long as it asks: calls in that time reject at once,
|
|
763
809
|
without asking `/auth/token` again.
|
|
764
810
|
|
|
811
|
+
When the token goes to a browser, the browser is what sees it refused, and
|
|
812
|
+
only it can say so. `@chaosity/location-client-react`'s provider asks its
|
|
813
|
+
`getConfig` again with `{ refusedToken }` after a 401. Answer it by replacing
|
|
814
|
+
the cached token only when it is the one refused:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
'use server'
|
|
818
|
+
import { getClientConfig } from '@chaosity/location-client/server'
|
|
819
|
+
|
|
820
|
+
export async function getLocationConfig(request?: { refusedToken?: string }) {
|
|
821
|
+
const config = await getClientConfig()
|
|
822
|
+
return request?.refusedToken === config.token
|
|
823
|
+
? getClientConfig({ forceRefresh: true })
|
|
824
|
+
: config
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
Asked again without it, `getClientConfig()` hands back the token it caches:
|
|
829
|
+
the one just refused. The check mints once per refused token, and nothing for a
|
|
830
|
+
report of any other. It does not stop a caller echoing the token it was just
|
|
831
|
+
given to make the server mint again; rate-limit the endpoint if that matters.
|
|
832
|
+
|
|
765
833
|
The return value is **plain data** — no methods, no closures — so it can be
|
|
766
834
|
returned straight out of a Next.js Server Action to a Client Component. It is
|
|
767
835
|
therefore a snapshot: the token in it stops working at its `expiresAt`, and the
|
|
@@ -818,11 +886,23 @@ which wins over both). `/auth/token` is the one endpoint exempt.
|
|
|
818
886
|
|
|
819
887
|
A connector configured this way keeps working indefinitely: it holds a live
|
|
820
888
|
token source, refreshes before expiry, and retries once with a new token if the
|
|
821
|
-
API rejects the one it sent.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
889
|
+
API rejects the one it sent. When that refresh before expiry fails with anything
|
|
890
|
+
but a refusal — a 429 or 503 from the token route, or a network fault — it keeps
|
|
891
|
+
sending the token it has until 5 seconds before that token's own `exp`, and asks
|
|
892
|
+
the token route nothing more while a `Retry-After` stands; past that it rejects
|
|
893
|
+
with the API's error, `retryAfterMs` intact. A `getToken` you supply is asked
|
|
894
|
+
for this with `{ cachedUntilExpiry: true }`, which `TokenProvider.getToken`
|
|
895
|
+
understands. `getClientConfig` never does it: a token that close to its expiry
|
|
896
|
+
is one a browser provider would ask to replace at once.
|
|
897
|
+
|
|
898
|
+
After the API rejects a token, if no new token can be had, or the new one is
|
|
899
|
+
refused too — a rotated secret's refresh is refused, for one — the refusal is
|
|
900
|
+
remembered for 30 seconds: sends in that time reject with it at once, without a
|
|
901
|
+
data request or a token request. A suspended application is refused 403 on
|
|
902
|
+
every route, which no new token can change: its data requests are not retried,
|
|
903
|
+
and a token request's refusal is remembered the same way. A refresh that fails
|
|
904
|
+
with nothing to say about when to try again, a network fault for one, is asked
|
|
905
|
+
again on the next send, without the refused token first. Pass an
|
|
826
906
|
explicit `token` instead and you opt out of all of that — it is a fixed string,
|
|
827
907
|
and it dies at its own `exp`:
|
|
828
908
|
|
|
@@ -832,7 +912,7 @@ and it dies at its own `exp`:
|
|
|
832
912
|
const connector = new LocationServiceConnector({
|
|
833
913
|
apiUrl,
|
|
834
914
|
origin: 'https://your-allowed-domain.example',
|
|
835
|
-
getToken: (forceRefresh) => provider.getToken(forceRefresh),
|
|
915
|
+
getToken: (forceRefresh, options) => provider.getToken(forceRefresh, options),
|
|
836
916
|
})
|
|
837
917
|
```
|
|
838
918
|
|
|
@@ -878,7 +958,9 @@ DEBUG=location-client:api npm run dev
|
|
|
878
958
|
|
|
879
959
|
## TypeScript Support
|
|
880
960
|
|
|
881
|
-
|
|
961
|
+
The request and response types are the AWS SDK's, and `send` resolves with the
|
|
962
|
+
command's own output type. To name it — for a variable declared before the
|
|
963
|
+
call, say — use the SDK's `…CommandOutput` or `CommandOutput<C>`:
|
|
882
964
|
|
|
883
965
|
```typescript
|
|
884
966
|
import type { SuggestCommandOutput } from '@chaosity/location-client'
|
|
@@ -74,7 +74,13 @@ export declare class GeoPlaces implements MaplibreGeocoderApi {
|
|
|
74
74
|
private client;
|
|
75
75
|
private map;
|
|
76
76
|
private details;
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* @param client Anything with the client's `send` — the adapter calls
|
|
79
|
+
* nothing else (#65). `@chaosity/location-client-react`'s
|
|
80
|
+
* `useLocationClient()` hands out an interface, not a `GeoPlacesClient`,
|
|
81
|
+
* and a class with private fields admits no other object.
|
|
82
|
+
*/
|
|
83
|
+
constructor(client: Pick<GeoPlacesClient, 'send'>, map: Map, options?: GeoPlacesOptions);
|
|
78
84
|
/**
|
|
79
85
|
* Build the AdditionalFeatures list from the opt-ins, or omit it entirely.
|
|
80
86
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetPlaceAdditionalFeature
|
|
1
|
+
import { GetPlaceAdditionalFeature } from '@aws-sdk/client-geo-places';
|
|
2
2
|
import { geocodeResponseToFeatureCollection, getPlaceResponseToFeatureCollection, reverseGeocodeResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
|
|
3
3
|
import debug from 'debug';
|
|
4
4
|
import {
|
|
@@ -68,6 +68,12 @@ function insideBox([x, y], bbox) {
|
|
|
68
68
|
return minX <= maxX ? x >= minX && x <= maxX : x >= minX || x <= maxX;
|
|
69
69
|
}
|
|
70
70
|
export class GeoPlaces {
|
|
71
|
+
/**
|
|
72
|
+
* @param client Anything with the client's `send` — the adapter calls
|
|
73
|
+
* nothing else (#65). `@chaosity/location-client-react`'s
|
|
74
|
+
* `useLocationClient()` hands out an interface, not a `GeoPlacesClient`,
|
|
75
|
+
* and a class with private fields admits no other object.
|
|
76
|
+
*/
|
|
71
77
|
constructor(client, map, options = {}) {
|
|
72
78
|
this.client = client;
|
|
73
79
|
this.map = map;
|
|
@@ -117,7 +123,7 @@ export class GeoPlaces {
|
|
|
117
123
|
: config.countries.split(','),
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
|
-
const response =
|
|
126
|
+
const response = await this.client.send(new GeocodeCommand(commandInput));
|
|
121
127
|
const converted = geocodeResponseToFeatureCollection(response, {
|
|
122
128
|
flattenProperties: true,
|
|
123
129
|
});
|
|
@@ -145,7 +151,7 @@ export class GeoPlaces {
|
|
|
145
151
|
MaxResults: config.limit || 1,
|
|
146
152
|
Language: this.normalizeLanguage(config.language),
|
|
147
153
|
};
|
|
148
|
-
const response =
|
|
154
|
+
const response = await this.client.send(new ReverseGeocodeCommand(commandInput));
|
|
149
155
|
const converted = reverseGeocodeResponseToFeatureCollection(response, {
|
|
150
156
|
flattenProperties: true,
|
|
151
157
|
});
|
|
@@ -203,7 +209,7 @@ export class GeoPlaces {
|
|
|
203
209
|
}
|
|
204
210
|
: {}),
|
|
205
211
|
};
|
|
206
|
-
const response =
|
|
212
|
+
const response = await this.client.send(new SuggestCommand(commandInput));
|
|
207
213
|
const suggestions = { suggestions: [] };
|
|
208
214
|
for (const item of response.ResultItems ?? []) {
|
|
209
215
|
const text = item.Title;
|
|
@@ -226,7 +232,7 @@ export class GeoPlaces {
|
|
|
226
232
|
Language: this.normalizeLanguage(config.language),
|
|
227
233
|
...(additionalFeatures ? { AdditionalFeatures: additionalFeatures } : {}),
|
|
228
234
|
});
|
|
229
|
-
const response =
|
|
235
|
+
const response = await this.client.send(command);
|
|
230
236
|
const result = getPlaceResponseToFeatureCollection(response, {
|
|
231
237
|
flattenProperties: true,
|
|
232
238
|
});
|
|
@@ -4,6 +4,21 @@ export interface TokenResponse {
|
|
|
4
4
|
expiresAt?: number;
|
|
5
5
|
error?: string;
|
|
6
6
|
}
|
|
7
|
+
export interface GetTokenOptions {
|
|
8
|
+
/**
|
|
9
|
+
* When a refresh fails with an error that waiting can fix — a 429 or 503
|
|
10
|
+
* from `/auth/token`, a network fault, or the `Retry-After` such a failure
|
|
11
|
+
* left standing — return the cached token instead, while it is before its
|
|
12
|
+
* own `exp` (#63). A refusal (401 or 403) still clears the cache and throws,
|
|
13
|
+
* and a forced refresh never falls back: it is asked for because the API
|
|
14
|
+
* refused the cached token.
|
|
15
|
+
*
|
|
16
|
+
* For server-side dispatch only. Never set it where the token is handed to
|
|
17
|
+
* a browser: a token inside `TOKEN_REFRESH_BUFFER_SECONDS` is one the React
|
|
18
|
+
* provider asks to replace at once, so `getClientConfig` does not.
|
|
19
|
+
*/
|
|
20
|
+
cachedUntilExpiry?: boolean;
|
|
21
|
+
}
|
|
7
22
|
export interface TokenProviderConfig {
|
|
8
23
|
apiUrl: string;
|
|
9
24
|
clientId: string;
|
|
@@ -45,7 +60,7 @@ export declare class TokenProvider {
|
|
|
45
60
|
/** A refusal or a Retry-After from `/auth/token`, until it lapses (#38). */
|
|
46
61
|
private readonly hold;
|
|
47
62
|
constructor(config: TokenProviderConfig);
|
|
48
|
-
getToken(forceRefresh?: boolean): Promise<TokenResponse>;
|
|
63
|
+
getToken(forceRefresh?: boolean, options?: GetTokenOptions): Promise<TokenResponse>;
|
|
49
64
|
/**
|
|
50
65
|
* Fetch a token, distinguishing transient failure from terminal (#9).
|
|
51
66
|
*
|
|
@@ -61,6 +76,12 @@ export declare class TokenProvider {
|
|
|
61
76
|
* with `success: false`, so a stale token can never be used by accident.
|
|
62
77
|
*/
|
|
63
78
|
private fetchToken;
|
|
79
|
+
/**
|
|
80
|
+
* #63: the cached token, when the caller asked for it and the failure is one
|
|
81
|
+
* that waiting fixes. A refusal has already cleared the cache in fetchToken,
|
|
82
|
+
* so it can never be the cached token handed back here.
|
|
83
|
+
*/
|
|
84
|
+
private fallback;
|
|
64
85
|
private isExpired;
|
|
65
86
|
clearCache(): void;
|
|
66
87
|
}
|
|
@@ -4,6 +4,11 @@ import { requestJson } from '../transport/http.js';
|
|
|
4
4
|
import { TokenHold, isTokenRefusal } from './tokenHold.js';
|
|
5
5
|
import { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './tokenRefresh.js';
|
|
6
6
|
const log = debug('location-client:auth');
|
|
7
|
+
/**
|
|
8
|
+
* #63: how close to its own `exp` a cached token may still be sent when a
|
|
9
|
+
* refresh fails. A margin for the clocks of this host and the API to disagree.
|
|
10
|
+
*/
|
|
11
|
+
const EXPIRY_SKEW_MS = 5000;
|
|
7
12
|
/**
|
|
8
13
|
* TokenProvider - SERVER-SIDE ONLY
|
|
9
14
|
*
|
|
@@ -45,7 +50,7 @@ export class TokenProvider {
|
|
|
45
50
|
log('Initializing TokenProvider for %s', config.apiUrl);
|
|
46
51
|
this.config = config;
|
|
47
52
|
}
|
|
48
|
-
async getToken(forceRefresh = false) {
|
|
53
|
+
async getToken(forceRefresh = false, options = {}) {
|
|
49
54
|
if (!forceRefresh && this.cachedToken && !this.isExpired()) {
|
|
50
55
|
const expiresIn = this.cachedExpiresAt
|
|
51
56
|
? Math.floor((this.cachedExpiresAt - Date.now()) / 1000)
|
|
@@ -64,12 +69,20 @@ export class TokenProvider {
|
|
|
64
69
|
const held = this.hold.check()?.error;
|
|
65
70
|
if (held) {
|
|
66
71
|
log('Token request held: %s', held.message);
|
|
72
|
+
const fallback = this.fallback(forceRefresh, options, held);
|
|
73
|
+
if (fallback)
|
|
74
|
+
return fallback;
|
|
67
75
|
throw held;
|
|
68
76
|
}
|
|
69
77
|
// If token fetch is already in progress, wait for it
|
|
70
78
|
if (this.tokenPromise) {
|
|
71
79
|
log('Token fetch in progress, waiting for existing request...');
|
|
72
|
-
return this.tokenPromise
|
|
80
|
+
return this.tokenPromise.catch((error) => {
|
|
81
|
+
const fallback = this.fallback(forceRefresh, options, error);
|
|
82
|
+
if (fallback)
|
|
83
|
+
return fallback;
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
73
86
|
}
|
|
74
87
|
// Start new token fetch
|
|
75
88
|
const reason = forceRefresh
|
|
@@ -83,6 +96,12 @@ export class TokenProvider {
|
|
|
83
96
|
const result = await this.tokenPromise;
|
|
84
97
|
return result;
|
|
85
98
|
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const fallback = this.fallback(forceRefresh, options, error);
|
|
101
|
+
if (fallback)
|
|
102
|
+
return fallback;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
86
105
|
finally {
|
|
87
106
|
// Clear promise after completion (success or failure)
|
|
88
107
|
this.tokenPromise = undefined;
|
|
@@ -154,6 +173,27 @@ export class TokenProvider {
|
|
|
154
173
|
expiresAt: this.cachedExpiresAt,
|
|
155
174
|
};
|
|
156
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* #63: the cached token, when the caller asked for it and the failure is one
|
|
178
|
+
* that waiting fixes. A refusal has already cleared the cache in fetchToken,
|
|
179
|
+
* so it can never be the cached token handed back here.
|
|
180
|
+
*/
|
|
181
|
+
fallback(forceRefresh, options, error) {
|
|
182
|
+
if (forceRefresh || !options.cachedUntilExpiry)
|
|
183
|
+
return undefined;
|
|
184
|
+
if (isTokenRefusal(error))
|
|
185
|
+
return undefined;
|
|
186
|
+
if (!this.cachedToken || !this.cachedExpiresAt)
|
|
187
|
+
return undefined;
|
|
188
|
+
if (Date.now() >= this.cachedExpiresAt - EXPIRY_SKEW_MS)
|
|
189
|
+
return undefined;
|
|
190
|
+
log('Refresh failed (%s); the cached token is valid for %ds more, sending it', error instanceof Error ? error.message : String(error), Math.floor((this.cachedExpiresAt - Date.now()) / 1000));
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
token: this.cachedToken,
|
|
194
|
+
expiresAt: this.cachedExpiresAt,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
157
197
|
isExpired(bufferSeconds = TOKEN_REFRESH_BUFFER_SECONDS) {
|
|
158
198
|
if (!this.cachedExpiresAt)
|
|
159
199
|
return true;
|
package/dist/auth/tokenHold.d.ts
CHANGED
|
@@ -9,11 +9,11 @@ import { LocationServiceException } from '../errors/LocationServiceException.js'
|
|
|
9
9
|
* served paid for a doomed data request and a doomed token request, all
|
|
10
10
|
* against the application's own token-route throttle.
|
|
11
11
|
*
|
|
12
|
-
* Thirty seconds
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Thirty seconds is short beside the API's own waits: it accepts a
|
|
13
|
+
* reactivated application again within five minutes, and refuses a newly
|
|
14
|
+
* created or reactivated one for up to about thirty seconds while its key goes
|
|
15
|
+
* live. So this side adds at most thirty seconds to either, and turns a
|
|
16
|
+
* request rate into a trickle meanwhile.
|
|
17
17
|
*/
|
|
18
18
|
export declare const TOKEN_REFUSAL_HOLD_MS = 30000;
|
|
19
19
|
/**
|
|
@@ -70,3 +70,12 @@ export declare class TokenHold {
|
|
|
70
70
|
} | undefined;
|
|
71
71
|
forget(): void;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Send with `token`, and after a 401 once more with `refresh()`'s token when it
|
|
75
|
+
* is a DIFFERENT one, remembering on `hold` what the API and the source said,
|
|
76
|
+
* against the token it concerns (#38).
|
|
77
|
+
*
|
|
78
|
+
* The one copy of the 401 retry: both send paths and the two map fetches (#72)
|
|
79
|
+
* call it, so when to ask again is decided here and nowhere else.
|
|
80
|
+
*/
|
|
81
|
+
export declare function sendRetryingOnce<T>(hold: TokenHold, token: string, send: (token: string) => Promise<T>, refresh: () => Promise<string | undefined>, onRetry?: () => void): Promise<T>;
|
package/dist/auth/tokenHold.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LocationServiceException } from '../errors/LocationServiceException.js';
|
|
2
|
+
import { isTokenRejected } from '../transport/errors.js';
|
|
2
3
|
/**
|
|
3
4
|
* How long a refusal is remembered: a token the API refused, or a token
|
|
4
5
|
* request `/auth/token` refused (#38).
|
|
@@ -9,11 +10,11 @@ import { LocationServiceException } from '../errors/LocationServiceException.js'
|
|
|
9
10
|
* served paid for a doomed data request and a doomed token request, all
|
|
10
11
|
* against the application's own token-route throttle.
|
|
11
12
|
*
|
|
12
|
-
* Thirty seconds
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Thirty seconds is short beside the API's own waits: it accepts a
|
|
14
|
+
* reactivated application again within five minutes, and refuses a newly
|
|
15
|
+
* created or reactivated one for up to about thirty seconds while its key goes
|
|
16
|
+
* live. So this side adds at most thirty seconds to either, and turns a
|
|
17
|
+
* request rate into a trickle meanwhile.
|
|
17
18
|
*/
|
|
18
19
|
export const TOKEN_REFUSAL_HOLD_MS = 30000;
|
|
19
20
|
/**
|
|
@@ -107,3 +108,67 @@ export class TokenHold {
|
|
|
107
108
|
this.held = undefined;
|
|
108
109
|
}
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Send with `token`, and after a 401 once more with `refresh()`'s token when it
|
|
113
|
+
* is a DIFFERENT one, remembering on `hold` what the API and the source said,
|
|
114
|
+
* against the token it concerns (#38).
|
|
115
|
+
*
|
|
116
|
+
* The one copy of the 401 retry: both send paths and the two map fetches (#72)
|
|
117
|
+
* call it, so when to ask again is decided here and nowhere else.
|
|
118
|
+
*/
|
|
119
|
+
export async function sendRetryingOnce(hold, token, send, refresh, onRetry) {
|
|
120
|
+
// This token was refused a moment ago and nothing has replaced it: answer
|
|
121
|
+
// with that refusal rather than send it, and ask the source again. A
|
|
122
|
+
// different token ends the hold. When the refresh that followed said nothing
|
|
123
|
+
// about when to ask again, it is asked now — but the refused token is still
|
|
124
|
+
// not sent.
|
|
125
|
+
const held = hold.check(token);
|
|
126
|
+
if (held && !held.askAgain)
|
|
127
|
+
throw held.error;
|
|
128
|
+
let rejected = held?.error;
|
|
129
|
+
if (!held) {
|
|
130
|
+
try {
|
|
131
|
+
return await send(token);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
if (!isTokenRejected(err))
|
|
135
|
+
throw err;
|
|
136
|
+
rejected = err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// One retry, and only when the replacement is genuinely a different token.
|
|
140
|
+
// That single comparison covers every source: a fixed `token` string, a
|
|
141
|
+
// `getToken` that ignores `forceRefresh`, a `refreshToken` that hands back
|
|
142
|
+
// what it had, and a cached token the API has revoked before its `exp` —
|
|
143
|
+
// re-sending any of them is a second doomed request for the same answer.
|
|
144
|
+
let fresh;
|
|
145
|
+
try {
|
|
146
|
+
fresh = await refresh();
|
|
147
|
+
}
|
|
148
|
+
catch (refusal) {
|
|
149
|
+
// A rotated secret's token route refuses the refresh as the data route
|
|
150
|
+
// refused its token. Without this, every send asked again. A refusal that
|
|
151
|
+
// says nothing — a network fault, or a Server Action's error without its
|
|
152
|
+
// fields — leaves the source to be asked again, but not the token sent.
|
|
153
|
+
if (holdFor(refusal) > 0)
|
|
154
|
+
hold.remember(refusal, token);
|
|
155
|
+
// Only when no hold stands: one already standing keeps its own end, so
|
|
156
|
+
// the refused token is tried again once per hold rather than never.
|
|
157
|
+
else if (!held)
|
|
158
|
+
hold.remember(rejected, token, { askAgain: true });
|
|
159
|
+
throw refusal;
|
|
160
|
+
}
|
|
161
|
+
if (!fresh || fresh === token) {
|
|
162
|
+
hold.remember(rejected, token);
|
|
163
|
+
throw rejected;
|
|
164
|
+
}
|
|
165
|
+
onRetry?.();
|
|
166
|
+
try {
|
|
167
|
+
return await send(fresh);
|
|
168
|
+
}
|
|
169
|
+
catch (again) {
|
|
170
|
+
if (isTokenRejected(again))
|
|
171
|
+
hold.remember(again, fresh);
|
|
172
|
+
throw again;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/aws.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type * from '@aws-sdk/client-geo-places';
|
|
2
|
+
export { $Command, AccessDeniedException, AccessDeniedException$, AccessPoint$, AccessPointType, AccessRestriction$, Address$, AddressComponentMatchScores$, AddressComponentPhonemes$, AddressTranslationComponent, AdminNames$, AdminNamesPreference, Autocomplete$, AutocompleteAdditionalFeature, AutocompleteAddressHighlights$, AutocompleteFilter$, AutocompleteFilterPlaceType, AutocompleteHighlights$, AutocompleteIntendedUse, AutocompleteRequest$, AutocompleteResponse$, AutocompleteResultItem$, BusinessChain$, Category$, ComponentMatchScores$, ContactDetails$, Contacts$, Country$, CountryHighlights$, CrossReference$, FilterCircle$, FoodType$, GeoPlacesServiceException, GeoPlacesServiceException$, Geocode$, GeocodeAdditionalFeature, GeocodeAddressNamesMode, GeocodeFilter$, GeocodeFilterPlaceType, GeocodeIntendedUse, GeocodeParsedQuery$, GeocodeParsedQueryAddressComponents$, GeocodeQueryComponents$, GeocodeRequest$, GeocodeResponse$, GeocodeResultItem$, GetPlace$, GetPlaceAdditionalFeature, GetPlaceAddressNamesMode, GetPlaceIntendedUse, GetPlaceRequest$, GetPlaceResponse$, Highlight$, InternalServerException, InternalServerException$, Intersection$, MatchScoreDetails$, OpeningHours$, OpeningHoursComponents$, ParsedQueryComponent$, ParsedQuerySecondaryAddressComponent$, PhonemeDetails$, PhonemeTranscription$, PlaceAttribute, PlaceType, PostalAuthority, PostalCodeDetails$, PostalCodeMode, PostalCodeType, QueryRefinement$, QueryType, RecordTypeCode, Region$, RegionHighlights$, RelatedPlace$, ReverseGeocode$, ReverseGeocodeAdditionalFeature, ReverseGeocodeAddressNamesMode, ReverseGeocodeFilter$, ReverseGeocodeFilterPlaceType, ReverseGeocodeIntendedUse, ReverseGeocodeRequest$, ReverseGeocodeResponse$, ReverseGeocodeResultItem$, SearchNearby$, SearchNearbyAdditionalFeature, SearchNearbyFilter$, SearchNearbyIntendedUse, SearchNearbyRequest$, SearchNearbyResponse$, SearchNearbyResultItem$, SearchText$, SearchTextAdditionalFeature, SearchTextFilter$, SearchTextIntendedUse, SearchTextRequest$, SearchTextResponse$, SearchTextResultItem$, SearchTextTravelMode, SecondaryAddressComponent$, SecondaryAddressComponentMatchScore$, StreetComponents$, SubRegion$, SubRegionHighlights$, Suggest$, SuggestAdditionalFeature, SuggestAddressHighlights$, SuggestFilter$, SuggestHighlights$, SuggestIntendedUse, SuggestPlaceResult$, SuggestQueryResult$, SuggestRequest$, SuggestResponse$, SuggestResultItem$, SuggestResultItemType, SuggestTravelMode, ThrottlingException, ThrottlingException$, TimeZone$, TranslationDetails$, TranslationName$, TranslationNameType, TypePlacement, UspsZip$, UspsZipPlus4$, ValidationException, ValidationException$, ValidationExceptionField$, ValidationExceptionReason, ZipClassificationCode, __Client, errorTypeRegistries, } from '@aws-sdk/client-geo-places';
|
|
3
|
+
export type * from '@aws/amazon-location-utilities-datatypes';
|
|
4
|
+
export { calculateIsolinesResponseToFeatureCollection, calculateRoutesResponseToFeatureCollections, devicePositionsToFeatureCollection, featureCollectionToGeofence, geocodeResponseToFeatureCollection, geofencesToFeatureCollection, getPlaceResponseToFeatureCollection, optimizeWaypointsResponseToFeatureCollection, placeToFeatureCollection, reverseGeocodeResponseToFeatureCollection, routeToFeatureCollection, searchNearbyResponseToFeatureCollection, searchTextResponseToFeatureCollection, snapToRoadsResponseToFeatureCollection, suggestResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
|
package/dist/aws.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { $Command, AccessDeniedException, AccessDeniedException$, AccessPoint$, AccessPointType, AccessRestriction$, Address$, AddressComponentMatchScores$, AddressComponentPhonemes$, AddressTranslationComponent, AdminNames$, AdminNamesPreference, Autocomplete$, AutocompleteAdditionalFeature, AutocompleteAddressHighlights$, AutocompleteFilter$, AutocompleteFilterPlaceType, AutocompleteHighlights$, AutocompleteIntendedUse, AutocompleteRequest$, AutocompleteResponse$, AutocompleteResultItem$, BusinessChain$, Category$, ComponentMatchScores$, ContactDetails$, Contacts$, Country$, CountryHighlights$, CrossReference$, FilterCircle$, FoodType$, GeoPlacesServiceException, GeoPlacesServiceException$, Geocode$, GeocodeAdditionalFeature, GeocodeAddressNamesMode, GeocodeFilter$, GeocodeFilterPlaceType, GeocodeIntendedUse, GeocodeParsedQuery$, GeocodeParsedQueryAddressComponents$, GeocodeQueryComponents$, GeocodeRequest$, GeocodeResponse$, GeocodeResultItem$, GetPlace$, GetPlaceAdditionalFeature, GetPlaceAddressNamesMode, GetPlaceIntendedUse, GetPlaceRequest$, GetPlaceResponse$, Highlight$, InternalServerException, InternalServerException$, Intersection$, MatchScoreDetails$, OpeningHours$, OpeningHoursComponents$, ParsedQueryComponent$, ParsedQuerySecondaryAddressComponent$, PhonemeDetails$, PhonemeTranscription$, PlaceAttribute, PlaceType, PostalAuthority, PostalCodeDetails$, PostalCodeMode, PostalCodeType, QueryRefinement$, QueryType, RecordTypeCode, Region$, RegionHighlights$, RelatedPlace$, ReverseGeocode$, ReverseGeocodeAdditionalFeature, ReverseGeocodeAddressNamesMode, ReverseGeocodeFilter$, ReverseGeocodeFilterPlaceType, ReverseGeocodeIntendedUse, ReverseGeocodeRequest$, ReverseGeocodeResponse$, ReverseGeocodeResultItem$, SearchNearby$, SearchNearbyAdditionalFeature, SearchNearbyFilter$, SearchNearbyIntendedUse, SearchNearbyRequest$, SearchNearbyResponse$, SearchNearbyResultItem$, SearchText$, SearchTextAdditionalFeature, SearchTextFilter$, SearchTextIntendedUse, SearchTextRequest$, SearchTextResponse$, SearchTextResultItem$, SearchTextTravelMode, SecondaryAddressComponent$, SecondaryAddressComponentMatchScore$, StreetComponents$, SubRegion$, SubRegionHighlights$, Suggest$, SuggestAdditionalFeature, SuggestAddressHighlights$, SuggestFilter$, SuggestHighlights$, SuggestIntendedUse, SuggestPlaceResult$, SuggestQueryResult$, SuggestRequest$, SuggestResponse$, SuggestResultItem$, SuggestResultItemType, SuggestTravelMode, ThrottlingException, ThrottlingException$, TimeZone$, TranslationDetails$, TranslationName$, TranslationNameType, TypePlacement, UspsZip$, UspsZipPlus4$, ValidationException, ValidationException$, ValidationExceptionField$, ValidationExceptionReason, ZipClassificationCode, __Client, errorTypeRegistries, } from '@aws-sdk/client-geo-places';
|
|
2
|
+
export { calculateIsolinesResponseToFeatureCollection, calculateRoutesResponseToFeatureCollections, devicePositionsToFeatureCollection, featureCollectionToGeofence, geocodeResponseToFeatureCollection, geofencesToFeatureCollection, getPlaceResponseToFeatureCollection, optimizeWaypointsResponseToFeatureCollection, placeToFeatureCollection, reverseGeocodeResponseToFeatureCollection, routeToFeatureCollection, searchNearbyResponseToFeatureCollection, searchTextResponseToFeatureCollection, snapToRoadsResponseToFeatureCollection, suggestResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
|
|
@@ -74,7 +74,13 @@ export declare class GeoPlaces implements MaplibreGeocoderApi {
|
|
|
74
74
|
private client;
|
|
75
75
|
private map;
|
|
76
76
|
private details;
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* @param client Anything with the client's `send` — the adapter calls
|
|
79
|
+
* nothing else (#65). `@chaosity/location-client-react`'s
|
|
80
|
+
* `useLocationClient()` hands out an interface, not a `GeoPlacesClient`,
|
|
81
|
+
* and a class with private fields admits no other object.
|
|
82
|
+
*/
|
|
83
|
+
constructor(client: Pick<GeoPlacesClient, 'send'>, map: Map, options?: GeoPlacesOptions);
|
|
78
84
|
/**
|
|
79
85
|
* Build the AdditionalFeatures list from the opt-ins, or omit it entirely.
|
|
80
86
|
*
|