@chaosity/location-client 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -12
- package/dist/cjs/client/GeoPlacesClient.d.ts +11 -3
- package/dist/cjs/client/GeoPlacesClient.js +38 -12
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/maps/mapStyle.d.ts +3 -1
- package/dist/cjs/maps/mapStyle.js +24 -19
- package/dist/cjs/maps/staticMap.d.ts +3 -1
- package/dist/cjs/maps/staticMap.js +18 -12
- package/dist/cjs/server/LocationServiceConnector.d.ts +1 -1
- package/dist/cjs/server/LocationServiceConnector.js +17 -20
- package/dist/cjs/server/getClientConfig.d.ts +19 -0
- package/dist/cjs/server/getClientConfig.js +44 -10
- package/dist/cjs/transport/errors.d.ts +18 -1
- package/dist/cjs/transport/errors.js +25 -1
- package/dist/cjs/transport/http.d.ts +36 -0
- package/dist/cjs/transport/http.js +118 -15
- package/dist/cjs/types/index.d.ts +19 -2
- package/dist/cjs/utils/tokenClaims.d.ts +11 -13
- package/dist/cjs/utils/tokenClaims.js +11 -14
- package/dist/client/GeoPlacesClient.d.ts +11 -3
- package/dist/client/GeoPlacesClient.js +39 -13
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/maps/mapStyle.d.ts +3 -1
- package/dist/maps/mapStyle.js +25 -20
- package/dist/maps/staticMap.d.ts +3 -1
- package/dist/maps/staticMap.js +19 -13
- package/dist/server/LocationServiceConnector.d.ts +1 -1
- package/dist/server/LocationServiceConnector.js +18 -21
- package/dist/server/getClientConfig.d.ts +19 -0
- package/dist/server/getClientConfig.js +43 -10
- package/dist/transport/errors.d.ts +18 -1
- package/dist/transport/errors.js +24 -1
- package/dist/transport/http.d.ts +36 -0
- package/dist/transport/http.js +116 -14
- package/dist/types/index.d.ts +19 -2
- package/dist/utils/tokenClaims.d.ts +11 -13
- package/dist/utils/tokenClaims.js +11 -14
- package/package.json +1 -1
- package/dist/cjs/utils/roundPosition.d.ts +0 -66
- package/dist/cjs/utils/roundPosition.js +0 -109
- package/dist/utils/roundPosition.d.ts +0 -66
- package/dist/utils/roundPosition.js +0 -104
package/README.md
CHANGED
|
@@ -226,9 +226,57 @@ token already in hand, and a token the API stops accepting **before** its `exp`
|
|
|
226
226
|
the refresh buffer elapses. `refreshToken` is awaited after a 401, and the
|
|
227
227
|
request is retried **once** with what it returns. Return the same token, or
|
|
228
228
|
nothing, and no retry is sent — a request that is going to fail again is not
|
|
229
|
-
worth
|
|
229
|
+
worth a second round trip. A 403 is never retried: a new token cannot fix an
|
|
230
230
|
`Origin` the application does not allow.
|
|
231
231
|
|
|
232
|
+
#### Request options
|
|
233
|
+
|
|
234
|
+
Every call in this package takes the same options object — `client.send`,
|
|
235
|
+
`connector.send`, `fetchMapStyle` and `fetchStaticMap` — and every failure
|
|
236
|
+
arrives as a `LocationServiceException`.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
await client.send(command, {
|
|
240
|
+
signal, // AbortSignal — cancels mid-flight AND mid-backoff
|
|
241
|
+
timeoutMs: 10_000, // per ATTEMPT
|
|
242
|
+
overallTimeoutMs: 30_000, // the whole call, waits between attempts included
|
|
243
|
+
retry: { maxAttempts: 3 }, // or `false` for none
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`overallTimeoutMs` is the one worth setting deliberately, and it defaults to
|
|
248
|
+
30 s. `timeoutMs` bounds an attempt, not a call: the API answers a spent quota
|
|
249
|
+
with `Retry-After: 60`, and honouring that literally across two retries blocked
|
|
250
|
+
the caller for about two minutes — past any Lambda budget. Now no attempt gets
|
|
251
|
+
more time than the call has left, and a retry that would have to wait longer
|
|
252
|
+
than the remaining budget is not made at all. You get the API's own error back
|
|
253
|
+
instead, `retryAfterMs` intact, so you can queue the work rather than guess.
|
|
254
|
+
|
|
255
|
+
The map helpers take them as a trailing argument:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
const style = await fetchMapStyle(
|
|
259
|
+
apiUrl,
|
|
260
|
+
'Standard',
|
|
261
|
+
getToken,
|
|
262
|
+
{ language: 'fr' },
|
|
263
|
+
{ signal },
|
|
264
|
+
)
|
|
265
|
+
const blob = await fetchStaticMap(
|
|
266
|
+
apiUrl,
|
|
267
|
+
{ width: 640, height: 400, center },
|
|
268
|
+
getToken,
|
|
269
|
+
{ signal },
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**A call with no token is refused locally rather than sent.** Every path that
|
|
274
|
+
builds an `Authorization` header checks first, so a token source that has not
|
|
275
|
+
produced one yet raises `InvalidCredentialsException` instead of putting
|
|
276
|
+
`Bearer undefined` on the wire — which could only ever come back a 401, a round
|
|
277
|
+
trip spent to be told what you already know. `GeoPlacesClient` asks `refreshToken` first, so a
|
|
278
|
+
client whose token simply has not arrived yet still works.
|
|
279
|
+
|
|
232
280
|
#### GeoPlaces Adapter
|
|
233
281
|
|
|
234
282
|
Implements the `MaplibreGeocoderApi` interface for use with `@maplibre/maplibre-gl-geocoder`. Methods are called automatically by the geocoder control.
|
|
@@ -437,17 +485,30 @@ const connector = new LocationServiceConnector({
|
|
|
437
485
|
})
|
|
438
486
|
```
|
|
439
487
|
|
|
440
|
-
##
|
|
441
|
-
|
|
442
|
-
`
|
|
443
|
-
|
|
444
|
-
the
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
488
|
+
## Coordinates Are Sent As You Supply Them
|
|
489
|
+
|
|
490
|
+
Both `GeoPlacesClient` and `LocationServiceConnector` put your command input on
|
|
491
|
+
the wire unchanged. `BiasPosition` and `QueryPosition` arrive at the service at
|
|
492
|
+
the precision you passed.
|
|
493
|
+
|
|
494
|
+
Earlier versions rounded `BiasPosition` to a grid — 3 decimal places by
|
|
495
|
+
default — so that nearby callers could share a cached upstream answer. Requests
|
|
496
|
+
are no longer cached, so the rounding had nothing left to share and only
|
|
497
|
+
lowered the precision the geocoder worked from.
|
|
498
|
+
|
|
499
|
+
That is not a coarser result, it is a different one. A 3 dp grid moves a
|
|
500
|
+
coordinate by up to ~70 m, depending where in its cell the coordinate falls,
|
|
501
|
+
and the places a search returns change well inside that distance: measured
|
|
502
|
+
against this service, a bias moved ~70 m returned a different set of nearby
|
|
503
|
+
places, not the same set in a different order. If you were relying on the
|
|
504
|
+
rounding to group nearby requests, round before you call.
|
|
505
|
+
|
|
506
|
+
One coordinate is normalised: a static map's `center`, `bounding-box` and
|
|
507
|
+
`bounded-positions` are rounded to six decimals — ~10 cm, below one pixel of a
|
|
508
|
+
raster render. That is this library's choice, well inside what the service
|
|
509
|
+
accepts: at most fourteen decimals per number, and at most 36 characters for
|
|
510
|
+
the pair. A value straight from `map.getCenter()` carries fifteen or sixteen
|
|
511
|
+
decimals and fails the first of those. A format rule, not a precision policy.
|
|
451
512
|
|
|
452
513
|
## Logging
|
|
453
514
|
|
|
@@ -18,7 +18,7 @@ export declare class GeoPlacesClient {
|
|
|
18
18
|
constructor(config: ClientConfig);
|
|
19
19
|
/**
|
|
20
20
|
* This application's own configuration, as carried on the access token
|
|
21
|
-
* (api#65) —
|
|
21
|
+
* (api#65) — today, the countries it is scoped to.
|
|
22
22
|
*
|
|
23
23
|
* Provided so an application can SHOW its own settings: populate a country
|
|
24
24
|
* selector with the markets it actually serves, label a settings screen,
|
|
@@ -34,8 +34,16 @@ export declare class GeoPlacesClient {
|
|
|
34
34
|
/** Prefer the getToken callback (live ref) over a static token string. */
|
|
35
35
|
private currentToken;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* A token to send, or a refusal — never `undefined`.
|
|
38
|
+
*
|
|
39
|
+
* `refreshToken` is asked only when there is nothing at all in hand, so a
|
|
40
|
+
* client configured the ordinary way pays nothing for this.
|
|
41
|
+
*/
|
|
42
|
+
private ensureToken;
|
|
43
|
+
/**
|
|
44
|
+
* @param options `signal` to cancel, `timeoutMs` per attempt,
|
|
45
|
+
* `overallTimeoutMs` for the whole call, `retry: false` to disable the
|
|
46
|
+
* retry loop. Every failure throws LocationServiceException.
|
|
39
47
|
*/
|
|
40
48
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
41
49
|
private dispatch;
|
|
@@ -8,7 +8,6 @@ const debug_1 = __importDefault(require("debug"));
|
|
|
8
8
|
const endpoints_js_1 = require("../transport/endpoints.js");
|
|
9
9
|
const errors_js_1 = require("../transport/errors.js");
|
|
10
10
|
const http_js_1 = require("../transport/http.js");
|
|
11
|
-
const roundPosition_js_1 = require("../utils/roundPosition.js");
|
|
12
11
|
const tokenClaims_js_1 = require("../utils/tokenClaims.js");
|
|
13
12
|
const log = (0, debug_1.default)('location-client:api');
|
|
14
13
|
/**
|
|
@@ -26,7 +25,7 @@ class GeoPlacesClient {
|
|
|
26
25
|
}
|
|
27
26
|
/**
|
|
28
27
|
* This application's own configuration, as carried on the access token
|
|
29
|
-
* (api#65) —
|
|
28
|
+
* (api#65) — today, the countries it is scoped to.
|
|
30
29
|
*
|
|
31
30
|
* Provided so an application can SHOW its own settings: populate a country
|
|
32
31
|
* selector with the markets it actually serves, label a settings screen,
|
|
@@ -46,13 +45,39 @@ class GeoPlacesClient {
|
|
|
46
45
|
return this.clientConfig.getToken?.() ?? this.clientConfig.token;
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* A token to send, or a refusal — never `undefined`.
|
|
49
|
+
*
|
|
50
|
+
* `refreshToken` is asked only when there is nothing at all in hand, so a
|
|
51
|
+
* client configured the ordinary way pays nothing for this.
|
|
52
|
+
*/
|
|
53
|
+
async ensureToken() {
|
|
54
|
+
// `||`, not `??`: an empty string is a token source with nothing to give,
|
|
55
|
+
// not a decision to send an empty one. With `??` it survived the coalesce,
|
|
56
|
+
// skipped `refreshToken`, and then failed the check two lines below — so
|
|
57
|
+
// `getToken: () => undefined` got the refresh ask and `getToken: () => ''`
|
|
58
|
+
// did not, which is a distinction no caller means to draw.
|
|
59
|
+
const token = this.currentToken() || (await this.clientConfig.refreshToken?.());
|
|
60
|
+
if (!token) {
|
|
61
|
+
throw (0, errors_js_1.noTokenAvailable)('the client has no token yet. Pass `token`, or a `getToken`/`refreshToken` that has one.');
|
|
62
|
+
}
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @param options `signal` to cancel, `timeoutMs` per attempt,
|
|
67
|
+
* `overallTimeoutMs` for the whole call, `retry: false` to disable the
|
|
68
|
+
* retry loop. Every failure throws LocationServiceException.
|
|
51
69
|
*/
|
|
52
70
|
async send(command, options) {
|
|
53
71
|
const cmd = command;
|
|
54
72
|
const url = `${this.clientConfig.apiUrl}${(0, endpoints_js_1.resolveEndpoint)(cmd)}`;
|
|
55
|
-
|
|
73
|
+
// The fifth and last place in this package that turns a token into an
|
|
74
|
+
// `Authorization` header, and the last one that would send `Bearer
|
|
75
|
+
// undefined` (#37). The 401 self-heal below cannot cover this case — it
|
|
76
|
+
// needs a request to have been rejected first — so a client whose token
|
|
77
|
+
// source has not produced one yet spent a whole round trip to learn
|
|
78
|
+
// something it already knew. Ask the refresh source instead, and refuse if
|
|
79
|
+
// there is still nothing.
|
|
80
|
+
const token = await this.ensureToken();
|
|
56
81
|
try {
|
|
57
82
|
return await this.dispatch(url, token, cmd, options);
|
|
58
83
|
}
|
|
@@ -65,8 +90,8 @@ class GeoPlacesClient {
|
|
|
65
90
|
// refreshes in the background may have landed a new one while this
|
|
66
91
|
// request was in flight.
|
|
67
92
|
const fresh = (await this.clientConfig.refreshToken?.()) ?? this.currentToken();
|
|
68
|
-
// Nothing new to send. Repeating the request would fail identically
|
|
69
|
-
//
|
|
93
|
+
// Nothing new to send. Repeating the request would fail identically — a
|
|
94
|
+
// second round trip for the same 401.
|
|
70
95
|
if (!fresh || fresh === token)
|
|
71
96
|
throw err;
|
|
72
97
|
log('401 — retrying %s once with a refreshed token', cmd.constructor?.name);
|
|
@@ -74,10 +99,11 @@ class GeoPlacesClient {
|
|
|
74
99
|
}
|
|
75
100
|
}
|
|
76
101
|
dispatch(url, token, cmd, options) {
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
// The caller's input goes out as the caller wrote it. `BiasPosition` used
|
|
103
|
+
// to be rounded here to a grid sized by a token claim, so nearby callers
|
|
104
|
+
// shared a server cache entry; with no cache the rounding only lowered the
|
|
105
|
+
// precision the upstream geocoder had to work with, which moves the
|
|
106
|
+
// results rather than coarsening them (#51).
|
|
81
107
|
log('Sending %s to %s', cmd.constructor?.name, url);
|
|
82
108
|
return (0, http_js_1.requestJson)(url, {
|
|
83
109
|
method: 'POST',
|
|
@@ -85,7 +111,7 @@ class GeoPlacesClient {
|
|
|
85
111
|
'Content-Type': 'application/json',
|
|
86
112
|
Authorization: `Bearer ${token}`,
|
|
87
113
|
},
|
|
88
|
-
body: JSON.stringify(input),
|
|
114
|
+
body: JSON.stringify(cmd.input),
|
|
89
115
|
}, options);
|
|
90
116
|
}
|
|
91
117
|
}
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { GeoPlacesClient } from './client/GeoPlacesClient.js';
|
|
2
2
|
export type { SendOptions } from './client/GeoPlacesClient.js';
|
|
3
|
-
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_TIMEOUT_MS } from './transport/http.js';
|
|
3
|
+
export { DEFAULT_MAX_ATTEMPTS, DEFAULT_OVERALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, } from './transport/http.js';
|
|
4
4
|
export type { RequestOptions } from './transport/http.js';
|
|
5
5
|
export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
|
|
6
6
|
export { LocationServiceException } from './errors/LocationServiceException.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -14,13 +14,14 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.transformRequest = exports.TRAVEL_MODES = exports.TRAFFIC_MODES = exports.TERRAINS = exports.STATIC_MAP_STYLES = exports.SPRITE_VARIANTS = exports.SCALE_BAR_UNITS = exports.MAP_STYLES = exports.MAP_FEATURE_MODES = exports.LABEL_SIZES = exports.CONTOUR_DENSITIES = exports.COLOR_SCHEMES = exports.BUILDINGS = exports.staticMapAccept = exports.fetchStaticMap = exports.buildStaticMapUrl = exports.fetchMapStyle = exports.buildMapStyleUrl = exports.setPoiVisibility = exports.setAllPoiVisibility = exports.POI_CATEGORIES = exports.applyMapLanguage = exports.createTransformRequest = exports.GeoPlaces = exports.LocationServiceException = exports.readTokenExpiry = exports.TOKEN_REFRESH_BUFFER_SECONDS = exports.DEFAULT_TIMEOUT_MS = exports.DEFAULT_MAX_ATTEMPTS = exports.GeoPlacesClient = void 0;
|
|
17
|
+
exports.transformRequest = exports.TRAVEL_MODES = exports.TRAFFIC_MODES = exports.TERRAINS = exports.STATIC_MAP_STYLES = exports.SPRITE_VARIANTS = exports.SCALE_BAR_UNITS = exports.MAP_STYLES = exports.MAP_FEATURE_MODES = exports.LABEL_SIZES = exports.CONTOUR_DENSITIES = exports.COLOR_SCHEMES = exports.BUILDINGS = exports.staticMapAccept = exports.fetchStaticMap = exports.buildStaticMapUrl = exports.fetchMapStyle = exports.buildMapStyleUrl = exports.setPoiVisibility = exports.setAllPoiVisibility = exports.POI_CATEGORIES = exports.applyMapLanguage = exports.createTransformRequest = exports.GeoPlaces = exports.LocationServiceException = exports.readTokenExpiry = exports.TOKEN_REFRESH_BUFFER_SECONDS = exports.DEFAULT_TIMEOUT_MS = exports.DEFAULT_OVERALL_TIMEOUT_MS = exports.DEFAULT_MAX_ATTEMPTS = exports.GeoPlacesClient = void 0;
|
|
18
18
|
// Client (Custom - uses our auth instead of AWS SigV4)
|
|
19
19
|
var GeoPlacesClient_js_1 = require("./client/GeoPlacesClient.js");
|
|
20
20
|
Object.defineProperty(exports, "GeoPlacesClient", { enumerable: true, get: function () { return GeoPlacesClient_js_1.GeoPlacesClient; } });
|
|
21
|
-
// Transport options — cancellation, per-attempt timeout, retry
|
|
21
|
+
// Transport options — cancellation, per-attempt timeout, overall budget, retry
|
|
22
22
|
var http_js_1 = require("./transport/http.js");
|
|
23
23
|
Object.defineProperty(exports, "DEFAULT_MAX_ATTEMPTS", { enumerable: true, get: function () { return http_js_1.DEFAULT_MAX_ATTEMPTS; } });
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_OVERALL_TIMEOUT_MS", { enumerable: true, get: function () { return http_js_1.DEFAULT_OVERALL_TIMEOUT_MS; } });
|
|
24
25
|
Object.defineProperty(exports, "DEFAULT_TIMEOUT_MS", { enumerable: true, get: function () { return http_js_1.DEFAULT_TIMEOUT_MS; } });
|
|
25
26
|
// Token refresh policy — shared by the server provider and the React provider
|
|
26
27
|
var tokenRefresh_js_1 = require("./auth/tokenRefresh.js");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StyleSpecification } from 'maplibre-gl';
|
|
2
|
+
import type { RequestOptions } from '../transport/http.js';
|
|
2
3
|
import type { Buildings, ColorScheme, ContourDensity, MapStyle, Terrain, TrafficMode, TravelMode } from './mapEnums.js';
|
|
3
4
|
/**
|
|
4
5
|
* Options for building an AWS Location Service map style URL.
|
|
@@ -66,6 +67,7 @@ export declare function buildMapStyleUrl(apiUrl: string, mapStyle: MapStyle, opt
|
|
|
66
67
|
* @param mapStyle - Map style name (e.g. 'Standard', 'Monochrome', 'Satellite', 'Hybrid')
|
|
67
68
|
* @param getToken - Callback returning the current auth token
|
|
68
69
|
* @param options - Style options; `language` is applied to the descriptor, all others become URL params
|
|
70
|
+
* @param request - Transport options: `signal` to cancel, `timeoutMs`, `overallTimeoutMs`, `retry`
|
|
69
71
|
* @returns Modified MapLibre StyleSpecification object
|
|
70
72
|
*
|
|
71
73
|
* @example
|
|
@@ -74,4 +76,4 @@ export declare function buildMapStyleUrl(apiUrl: string, mapStyle: MapStyle, opt
|
|
|
74
76
|
*/
|
|
75
77
|
export declare function fetchMapStyle(apiUrl: string, mapStyle: MapStyle, getToken: () => string | undefined, options?: MapStyleOptions & {
|
|
76
78
|
language?: string;
|
|
77
|
-
}): Promise<StyleSpecification>;
|
|
79
|
+
}, request?: RequestOptions): Promise<StyleSpecification>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildMapStyleUrl = buildMapStyleUrl;
|
|
4
4
|
exports.fetchMapStyle = fetchMapStyle;
|
|
5
5
|
const errors_js_1 = require("../transport/errors.js");
|
|
6
|
+
const http_js_1 = require("../transport/http.js");
|
|
6
7
|
const mapLanguage_js_1 = require("./mapLanguage.js");
|
|
7
8
|
/**
|
|
8
9
|
* Build a map style descriptor URL for the Location Service API.
|
|
@@ -50,38 +51,42 @@ function buildMapStyleUrl(apiUrl, mapStyle, options = {}) {
|
|
|
50
51
|
* @param mapStyle - Map style name (e.g. 'Standard', 'Monochrome', 'Satellite', 'Hybrid')
|
|
51
52
|
* @param getToken - Callback returning the current auth token
|
|
52
53
|
* @param options - Style options; `language` is applied to the descriptor, all others become URL params
|
|
54
|
+
* @param request - Transport options: `signal` to cancel, `timeoutMs`, `overallTimeoutMs`, `retry`
|
|
53
55
|
* @returns Modified MapLibre StyleSpecification object
|
|
54
56
|
*
|
|
55
57
|
* @example
|
|
56
58
|
* const style = await fetchMapStyle(API_URL, 'Standard', getToken, { colorScheme: 'Dark', language: 'fr' })
|
|
57
59
|
* const map = new maplibregl.Map({ style, transformRequest: createTransformRequest(API_URL, getToken) })
|
|
58
60
|
*/
|
|
59
|
-
async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}) {
|
|
61
|
+
async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}, request = {}) {
|
|
60
62
|
const { language, ...styleOptions } = options;
|
|
61
63
|
const url = buildMapStyleUrl(apiUrl, mapStyle, styleOptions);
|
|
62
64
|
const token = getToken();
|
|
63
|
-
|
|
65
|
+
// `Bearer undefined` used to go out here, and came back as a 401 the caller
|
|
66
|
+
// had to work backwards from — a whole round trip for a request that was
|
|
67
|
+
// never going to succeed (#37).
|
|
68
|
+
if (!token) {
|
|
69
|
+
throw (0, errors_js_1.noTokenAvailable)('getToken() returned nothing, so no style request was sent. Check the token provider has finished initialising.');
|
|
70
|
+
}
|
|
71
|
+
// Through the shared transport, not a bare fetch: this gets the same
|
|
72
|
+
// per-attempt timeout, overall budget, cancellation and retry as every other
|
|
73
|
+
// call in the package, and the same error type on the way out. It also keeps
|
|
74
|
+
// the API's own message, which is the whole point of reading the body — for
|
|
75
|
+
// a style request that sentence is Amazon's, forwarded verbatim by
|
|
76
|
+
// location-service-api#89:
|
|
77
|
+
//
|
|
78
|
+
// 400 "Traffic is not supported for style."
|
|
79
|
+
// 400 "light is not a supported color scheme for style Standard."
|
|
80
|
+
//
|
|
81
|
+
// This used to throw `Failed to fetch map style: 400`, discarding all of it
|
|
82
|
+
// two lines before anyone could read it — the same defect #89 fixed in the
|
|
83
|
+
// API, one layer up.
|
|
84
|
+
const style = await (0, http_js_1.requestJson)(url, {
|
|
64
85
|
headers: {
|
|
65
86
|
Authorization: `Bearer ${token}`,
|
|
66
87
|
Accept: 'application/json',
|
|
67
88
|
},
|
|
68
|
-
});
|
|
69
|
-
if (!response.ok) {
|
|
70
|
-
// Read the body. The API sends `{message, code, requestId}` and the message
|
|
71
|
-
// is the whole point of it — for a style request it is Amazon's own
|
|
72
|
-
// sentence, forwarded verbatim by location-service-api#89:
|
|
73
|
-
//
|
|
74
|
-
// 400 "Traffic is not supported for style."
|
|
75
|
-
// 400 "light is not a supported color scheme for style Standard."
|
|
76
|
-
//
|
|
77
|
-
// This used to throw `Failed to fetch map style: 400`, discarding all of it
|
|
78
|
-
// two lines before anyone could read it — the same defect #89 fixed in the
|
|
79
|
-
// API, one layer up. Reuses parseErrorResponse so a style failure arrives as
|
|
80
|
-
// the same LocationServiceException as every other call in this package,
|
|
81
|
-
// with `code`, `statusCode` and `requestId` intact.
|
|
82
|
-
throw (0, errors_js_1.parseErrorResponse)(response.status, response.statusText, await response.text(), response.headers);
|
|
83
|
-
}
|
|
84
|
-
const style = (await response.json());
|
|
89
|
+
}, request);
|
|
85
90
|
if (language) {
|
|
86
91
|
applyLanguageToDescriptor(style, language);
|
|
87
92
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestOptions } from '../transport/http.js';
|
|
1
2
|
import type { ColorScheme, LabelSize, MapFeatureMode, ScaleBarUnit, StaticMapStyle } from './mapEnums.js';
|
|
2
3
|
/**
|
|
3
4
|
* Static maps: build the URL, send the right headers, get a Blob.
|
|
@@ -74,6 +75,7 @@ export declare function buildStaticMapUrl(apiUrl: string, options: StaticMapOpti
|
|
|
74
75
|
* @param apiUrl Base URL of the Location Service API
|
|
75
76
|
* @param options Render options; exactly one of center / boundingBox / boundedPositions
|
|
76
77
|
* @param getToken Callback returning the current auth token
|
|
78
|
+
* @param request Transport options: `signal` to cancel, `timeoutMs`, `overallTimeoutMs`, `retry`
|
|
77
79
|
*
|
|
78
80
|
* @example
|
|
79
81
|
* const blob = await fetchStaticMap(API_URL, {
|
|
@@ -82,4 +84,4 @@ export declare function buildStaticMapUrl(apiUrl: string, options: StaticMapOpti
|
|
|
82
84
|
* }, getToken)
|
|
83
85
|
* const url = URL.createObjectURL(blob) // remember to revokeObjectURL
|
|
84
86
|
*/
|
|
85
|
-
export declare function fetchStaticMap(apiUrl: string, options: StaticMapOptions, getToken: () => string | undefined): Promise<Blob>;
|
|
87
|
+
export declare function fetchStaticMap(apiUrl: string, options: StaticMapOptions, getToken: () => string | undefined, request?: RequestOptions): Promise<Blob>;
|
|
@@ -4,6 +4,7 @@ exports.staticMapAccept = staticMapAccept;
|
|
|
4
4
|
exports.buildStaticMapUrl = buildStaticMapUrl;
|
|
5
5
|
exports.fetchStaticMap = fetchStaticMap;
|
|
6
6
|
const errors_js_1 = require("../transport/errors.js");
|
|
7
|
+
const http_js_1 = require("../transport/http.js");
|
|
7
8
|
/**
|
|
8
9
|
* The Accept header this request must send.
|
|
9
10
|
*
|
|
@@ -55,6 +56,7 @@ function buildStaticMapUrl(apiUrl, options) {
|
|
|
55
56
|
* @param apiUrl Base URL of the Location Service API
|
|
56
57
|
* @param options Render options; exactly one of center / boundingBox / boundedPositions
|
|
57
58
|
* @param getToken Callback returning the current auth token
|
|
59
|
+
* @param request Transport options: `signal` to cancel, `timeoutMs`, `overallTimeoutMs`, `retry`
|
|
58
60
|
*
|
|
59
61
|
* @example
|
|
60
62
|
* const blob = await fetchStaticMap(API_URL, {
|
|
@@ -63,19 +65,23 @@ function buildStaticMapUrl(apiUrl, options) {
|
|
|
63
65
|
* }, getToken)
|
|
64
66
|
* const url = URL.createObjectURL(blob) // remember to revokeObjectURL
|
|
65
67
|
*/
|
|
66
|
-
async function fetchStaticMap(apiUrl, options, getToken) {
|
|
67
|
-
const
|
|
68
|
+
async function fetchStaticMap(apiUrl, options, getToken, request = {}) {
|
|
69
|
+
const token = getToken();
|
|
70
|
+
// The same guard as fetchMapStyle and the server connector: a render is not
|
|
71
|
+
// worth requesting without a token to send (#37).
|
|
72
|
+
if (!token) {
|
|
73
|
+
throw (0, errors_js_1.noTokenAvailable)('getToken() returned nothing, so no static map was requested. Check the token provider has finished initialising.');
|
|
74
|
+
}
|
|
75
|
+
// Through the shared transport, so a static map gets the timeout, budget,
|
|
76
|
+
// cancellation and retry every other call has -- and its failures arrive as
|
|
77
|
+
// LocationServiceException rather than as a raw TypeError. The API's own
|
|
78
|
+
// {message, code, requestId} survives, which matters here: the messages are
|
|
79
|
+
// specific and actionable -- "'width' and 'height' are required", "Only one
|
|
80
|
+
// of center, bounding-box or bounded-positions may be set".
|
|
81
|
+
return (0, http_js_1.requestBlob)(buildStaticMapUrl(apiUrl, options), {
|
|
68
82
|
headers: {
|
|
69
|
-
Authorization: `Bearer ${
|
|
83
|
+
Authorization: `Bearer ${token}`,
|
|
70
84
|
Accept: staticMapAccept(options.style),
|
|
71
85
|
},
|
|
72
|
-
});
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
// Same treatment as fetchMapStyle: the API's {message, code, requestId} is
|
|
75
|
-
// the useful part, and a bare "failed: 400" throws it away. The messages
|
|
76
|
-
// here are specific and actionable -- "'width' and 'height' are required",
|
|
77
|
-
// "Only one of center, bounding-box or bounded-positions may be set".
|
|
78
|
-
throw (0, errors_js_1.parseErrorResponse)(response.status, response.statusText, await response.text());
|
|
79
|
-
}
|
|
80
|
-
return response.blob();
|
|
86
|
+
}, request);
|
|
81
87
|
}
|
|
@@ -77,7 +77,7 @@ export declare class LocationServiceConnector {
|
|
|
77
77
|
private buildSource;
|
|
78
78
|
/**
|
|
79
79
|
* This application's own configuration, as carried on the access token
|
|
80
|
-
* (api#65) —
|
|
80
|
+
* (api#65) — today, the countries it is scoped to.
|
|
81
81
|
*
|
|
82
82
|
* Provided so an application can SHOW its own settings: populate a country
|
|
83
83
|
* selector with the markets it actually serves, label a settings screen, and
|
|
@@ -9,7 +9,6 @@ const LocationServiceException_js_1 = require("../errors/LocationServiceExceptio
|
|
|
9
9
|
const endpoints_js_1 = require("../transport/endpoints.js");
|
|
10
10
|
const errors_js_1 = require("../transport/errors.js");
|
|
11
11
|
const http_js_1 = require("../transport/http.js");
|
|
12
|
-
const roundPosition_js_1 = require("../utils/roundPosition.js");
|
|
13
12
|
const tokenClaims_js_1 = require("../utils/tokenClaims.js");
|
|
14
13
|
const getClientConfig_js_1 = require("./getClientConfig.js");
|
|
15
14
|
const log = (0, debug_1.default)('location-client:connector');
|
|
@@ -33,6 +32,12 @@ function headerValue(headers, name) {
|
|
|
33
32
|
* spelling of it survives the merge.
|
|
34
33
|
*/
|
|
35
34
|
const SYSTEM_HEADERS = ['origin', 'content-type', 'authorization'];
|
|
35
|
+
/**
|
|
36
|
+
* What to check when the token source comes up empty. The guard itself is
|
|
37
|
+
* shared with the map fetches (`noTokenAvailable`); only the advice differs,
|
|
38
|
+
* and on this path the answer is always the credentials.
|
|
39
|
+
*/
|
|
40
|
+
const NO_TOKEN_ADVICE = 'check clientId/clientSecret configuration';
|
|
36
41
|
/** The caller's headers minus `names`, however they capitalised them. */
|
|
37
42
|
function withoutHeaders(headers, names) {
|
|
38
43
|
if (!headers)
|
|
@@ -152,7 +157,7 @@ class LocationServiceConnector {
|
|
|
152
157
|
}
|
|
153
158
|
/**
|
|
154
159
|
* This application's own configuration, as carried on the access token
|
|
155
|
-
* (api#65) —
|
|
160
|
+
* (api#65) — today, the countries it is scoped to.
|
|
156
161
|
*
|
|
157
162
|
* Provided so an application can SHOW its own settings: populate a country
|
|
158
163
|
* selector with the markets it actually serves, label a settings screen, and
|
|
@@ -193,7 +198,7 @@ class LocationServiceConnector {
|
|
|
193
198
|
async dispatchWithRetry(source, url, cmd, options) {
|
|
194
199
|
const token = await source.get();
|
|
195
200
|
if (!token)
|
|
196
|
-
throw noTokenAvailable();
|
|
201
|
+
throw (0, errors_js_1.noTokenAvailable)(NO_TOKEN_ADVICE);
|
|
197
202
|
try {
|
|
198
203
|
return await this.dispatch(url, token, cmd, options);
|
|
199
204
|
}
|
|
@@ -204,8 +209,8 @@ class LocationServiceConnector {
|
|
|
204
209
|
// token. That single comparison covers every source: a fixed `token`
|
|
205
210
|
// string, a caller `getToken` that ignores `forceRefresh`, and a cached
|
|
206
211
|
// token the API has revoked before its `exp` all hand back what we
|
|
207
|
-
// already sent — and re-sending it would be a second doomed request
|
|
208
|
-
//
|
|
212
|
+
// already sent — and re-sending it would be a second doomed request for
|
|
213
|
+
// the same answer.
|
|
209
214
|
const fresh = await source.get(true);
|
|
210
215
|
if (!fresh || fresh === token)
|
|
211
216
|
throw err;
|
|
@@ -214,13 +219,12 @@ class LocationServiceConnector {
|
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
dispatch(url, token, cmd, options) {
|
|
217
|
-
// The
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
const input = (0, roundPosition_js_1.roundPositionFields)(cmd.input, biasDecimals);
|
|
222
|
+
// The caller's input goes out as the caller wrote it — nothing in the body
|
|
223
|
+
// is derived from the token any more. `BiasPosition` used to be rounded
|
|
224
|
+
// here to a grid sized by a token claim, so nearby callers shared a server
|
|
225
|
+
// cache entry; with no cache the rounding only lowered the precision the
|
|
226
|
+
// upstream geocoder had to work with, which moves the results rather than
|
|
227
|
+
// coarsening them (#51).
|
|
224
228
|
// Every system header is set exactly ONCE, and the caller's own spelling of
|
|
225
229
|
// each is dropped first.
|
|
226
230
|
//
|
|
@@ -245,7 +249,7 @@ class LocationServiceConnector {
|
|
|
245
249
|
Authorization: `Bearer ${token}`,
|
|
246
250
|
};
|
|
247
251
|
log('Sending %s request to %s', cmd.constructor?.name, url);
|
|
248
|
-
return (0, http_js_1.requestJson)(url, { method: 'POST', headers, body: JSON.stringify(input) }, options);
|
|
252
|
+
return (0, http_js_1.requestJson)(url, { method: 'POST', headers, body: JSON.stringify(cmd.input) }, options);
|
|
249
253
|
}
|
|
250
254
|
}
|
|
251
255
|
exports.LocationServiceConnector = LocationServiceConnector;
|
|
@@ -261,10 +265,3 @@ function requireApiUrl(explicit) {
|
|
|
261
265
|
}
|
|
262
266
|
return apiUrl;
|
|
263
267
|
}
|
|
264
|
-
function noTokenAvailable() {
|
|
265
|
-
return new LocationServiceException_js_1.LocationServiceException({
|
|
266
|
-
code: 'InvalidCredentialsException',
|
|
267
|
-
message: 'No token available — check clientId/clientSecret configuration',
|
|
268
|
-
details: { source: 'client' },
|
|
269
|
-
});
|
|
270
|
-
}
|
|
@@ -14,7 +14,26 @@ export interface ServerAuthConfig {
|
|
|
14
14
|
*/
|
|
15
15
|
forceRefresh?: boolean;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* How many applications one process keeps token providers for.
|
|
19
|
+
*
|
|
20
|
+
* A provider holds a URL, a client id, a secret and one cached JWT, so the
|
|
21
|
+
* ceiling is about memory containment rather than a tuned working set — 64 is
|
|
22
|
+
* far above what a single-tenant service needs and far below anything worth
|
|
23
|
+
* worrying about. An agency past it pays a re-mint for the least recently used
|
|
24
|
+
* application, which is exactly the behaviour this replaced, but only for the
|
|
25
|
+
* coldest one instead of for every alternation.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MAX_CACHED_PROVIDERS = 64;
|
|
17
28
|
export interface ServerClientConfig extends ClientConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Narrowed back to required. `ClientConfig.token` is optional because a
|
|
31
|
+
* client can be driven by `getToken`/`refreshToken` alone, but this type is
|
|
32
|
+
* what `getClientConfig` RESOLVES — it either has a token or it threw — and
|
|
33
|
+
* widening it would push a needless `string | undefined` onto every consumer
|
|
34
|
+
* that reads `config.token`.
|
|
35
|
+
*/
|
|
36
|
+
token: string;
|
|
18
37
|
expiresAt?: number;
|
|
19
38
|
}
|
|
20
39
|
/**
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_CACHED_PROVIDERS = void 0;
|
|
6
7
|
exports.resolveApiUrl = resolveApiUrl;
|
|
7
8
|
exports.serverTokenSource = serverTokenSource;
|
|
8
9
|
exports.getClientConfig = getClientConfig;
|
|
@@ -11,9 +12,32 @@ const node_crypto_1 = require("node:crypto");
|
|
|
11
12
|
const TokenProvider_js_1 = require("../auth/TokenProvider.js");
|
|
12
13
|
const LocationServiceException_js_1 = require("../errors/LocationServiceException.js");
|
|
13
14
|
const log = (0, debug_1.default)('location-client:clientConfig');
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* How many applications one process keeps token providers for.
|
|
17
|
+
*
|
|
18
|
+
* A provider holds a URL, a client id, a secret and one cached JWT, so the
|
|
19
|
+
* ceiling is about memory containment rather than a tuned working set — 64 is
|
|
20
|
+
* far above what a single-tenant service needs and far below anything worth
|
|
21
|
+
* worrying about. An agency past it pays a re-mint for the least recently used
|
|
22
|
+
* application, which is exactly the behaviour this replaced, but only for the
|
|
23
|
+
* coldest one instead of for every alternation.
|
|
24
|
+
*/
|
|
25
|
+
exports.MAX_CACHED_PROVIDERS = 64;
|
|
26
|
+
/**
|
|
27
|
+
* One provider per configuration, most-recently-used last.
|
|
28
|
+
*
|
|
29
|
+
* This used to be TWO module-level variables holding a single provider, and a
|
|
30
|
+
* process serving more than one application therefore evicted the cache on
|
|
31
|
+
* every alternation: A→B→A→B took a full `/auth/token` round trip per call,
|
|
32
|
+
* each writing a jti row, all against the one shared token-endpoint throttle.
|
|
33
|
+
* The hit rate under alternating load was 0% (#39). Nothing leaked between
|
|
34
|
+
* tenants — each caller closes over the provider it asked for — so what this
|
|
35
|
+
* fixes is availability and cost, not confidentiality.
|
|
36
|
+
*
|
|
37
|
+
* A `Map` iterates in insertion order, so re-inserting on a hit makes the first
|
|
38
|
+
* key the least recently used, and an LRU needs no other bookkeeping.
|
|
39
|
+
*/
|
|
40
|
+
const tokenProviders = new Map();
|
|
17
41
|
function getTokenProvider(apiUrl, clientId, clientSecret) {
|
|
18
42
|
// The SECRET is part of the key. Without it, rotating a client secret while
|
|
19
43
|
// keeping the same clientId left this process reusing a provider built on the
|
|
@@ -21,16 +45,26 @@ function getTokenProvider(apiUrl, clientId, clientSecret) {
|
|
|
21
45
|
// rather than concatenated so the key is never a secret in its own right, and
|
|
22
46
|
// never ends up in a log line (#5).
|
|
23
47
|
const configKey = `${apiUrl}:${clientId}:${(0, node_crypto_1.createHash)('sha256').update(clientSecret).digest('hex').slice(0, 16)}`;
|
|
24
|
-
|
|
25
|
-
if (
|
|
48
|
+
const cached = tokenProviders.get(configKey);
|
|
49
|
+
if (cached) {
|
|
26
50
|
log('[getTokenProvider] Reusing existing TokenProvider instance');
|
|
27
|
-
|
|
51
|
+
// Re-insert to mark it most recently used.
|
|
52
|
+
tokenProviders.delete(configKey);
|
|
53
|
+
tokenProviders.set(configKey, cached);
|
|
54
|
+
return cached;
|
|
28
55
|
}
|
|
29
|
-
// Create new instance if config changed
|
|
30
56
|
log('[getTokenProvider] Creating new TokenProvider instance');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
const provider = new TokenProvider_js_1.TokenProvider({ apiUrl, clientId, clientSecret });
|
|
58
|
+
tokenProviders.set(configKey, provider);
|
|
59
|
+
if (tokenProviders.size > exports.MAX_CACHED_PROVIDERS) {
|
|
60
|
+
const leastRecentlyUsed = tokenProviders.keys().next().value;
|
|
61
|
+
/* c8 ignore next — size > 0 here, so the iterator always yields */
|
|
62
|
+
if (leastRecentlyUsed !== undefined) {
|
|
63
|
+
log('[getTokenProvider] Evicting the least recently used TokenProvider');
|
|
64
|
+
tokenProviders.delete(leastRecentlyUsed);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return provider;
|
|
34
68
|
}
|
|
35
69
|
/**
|
|
36
70
|
* Where the API lives, from the argument or the environment.
|