@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
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.staticMapAccept = staticMapAccept;
|
|
4
4
|
exports.buildStaticMapUrl = buildStaticMapUrl;
|
|
5
5
|
exports.fetchStaticMap = fetchStaticMap;
|
|
6
|
-
const errors_js_1 = require("../transport/errors.js");
|
|
7
6
|
const http_js_1 = require("../transport/http.js");
|
|
7
|
+
const mapToken_js_1 = require("./mapToken.js");
|
|
8
8
|
/**
|
|
9
9
|
* The Accept header this request must send.
|
|
10
10
|
*
|
|
@@ -55,11 +55,14 @@ function buildStaticMapUrl(apiUrl, options) {
|
|
|
55
55
|
*
|
|
56
56
|
* A refused plan feature — a Satellite `style`, the default when none is
|
|
57
57
|
* given, or a `politicalView` — rejects with a `LocationServiceException`
|
|
58
|
-
* whose `isFeatureNotEntitled` is true, as `fetchMapStyle` does.
|
|
58
|
+
* whose `isFeatureNotEntitled` is true, as `fetchMapStyle` does. Given
|
|
59
|
+
* `{ getToken, refreshToken }`, a refused token is replaced once, as
|
|
60
|
+
* `fetchMapStyle` does it (#72).
|
|
59
61
|
*
|
|
60
62
|
* @param apiUrl Base URL of the Location Service API
|
|
61
63
|
* @param options Render options; exactly one of center / boundingBox / boundedPositions
|
|
62
|
-
* @param
|
|
64
|
+
* @param tokens Callback returning the current auth token, or
|
|
65
|
+
* `{ getToken, refreshToken }` to recover from a refused one
|
|
63
66
|
* @param request Transport options: `signal` to cancel, `timeoutMs`, `overallTimeoutMs`, `retry`
|
|
64
67
|
*
|
|
65
68
|
* @example
|
|
@@ -69,23 +72,21 @@ function buildStaticMapUrl(apiUrl, options) {
|
|
|
69
72
|
* }, getToken)
|
|
70
73
|
* const url = URL.createObjectURL(blob) // remember to revokeObjectURL
|
|
71
74
|
*/
|
|
72
|
-
async function fetchStaticMap(apiUrl, options,
|
|
73
|
-
const
|
|
74
|
-
// The same guard as fetchMapStyle and the server connector: a render is not
|
|
75
|
-
// worth requesting without a token to send (#37).
|
|
76
|
-
if (!token) {
|
|
77
|
-
throw (0, errors_js_1.noTokenAvailable)('getToken() returned nothing, so no static map was requested. Check the token provider has finished initialising.');
|
|
78
|
-
}
|
|
75
|
+
async function fetchStaticMap(apiUrl, options, tokens, request = {}) {
|
|
76
|
+
const call = (0, http_js_1.startCall)(request);
|
|
79
77
|
// Through the shared transport, so a static map gets the timeout, budget,
|
|
80
78
|
// cancellation and retry every other call has -- and its failures arrive as
|
|
81
79
|
// LocationServiceException rather than as a raw TypeError. The API's own
|
|
82
80
|
// {message, code, requestId} survives, which matters here: the messages are
|
|
83
81
|
// specific and actionable -- "'width' and 'height' are required", "Only one
|
|
84
82
|
// of center, bounding-box or bounded-positions may be set".
|
|
85
|
-
|
|
83
|
+
//
|
|
84
|
+
// The same guard as fetchMapStyle and the server connector: a render is not
|
|
85
|
+
// worth requesting without a token to send (#37).
|
|
86
|
+
return (0, mapToken_js_1.sendWithTokenRefresh)(tokens, 'getToken() returned nothing, so no static map was requested. Check the token provider has finished initialising.', call, (token) => (0, http_js_1.requestBlob)(buildStaticMapUrl(apiUrl, options), {
|
|
86
87
|
headers: {
|
|
87
88
|
Authorization: `Bearer ${token}`,
|
|
88
89
|
Accept: staticMapAccept(options.style),
|
|
89
90
|
},
|
|
90
|
-
},
|
|
91
|
+
}, call));
|
|
91
92
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { GetTokenOptions } from '../auth/TokenProvider.js';
|
|
1
2
|
import type { VerifyAddressResponse } from '../client/commands.js';
|
|
2
3
|
import type { RequestOptions } from '../transport/http.js';
|
|
4
|
+
import type { CommandOutput, CommandWithOutput } from '../types/index.js';
|
|
3
5
|
import type { AppConfigClaims } from '../utils/tokenClaims.js';
|
|
4
6
|
export interface ConnectorConfig {
|
|
5
7
|
/** Falls back to `LOCATION_API_URL` / `LOCATION_SERVICE_API_URL`. */
|
|
@@ -14,10 +16,13 @@ export interface ConnectorConfig {
|
|
|
14
16
|
* a long-lived connector survive expiry.
|
|
15
17
|
*
|
|
16
18
|
* `forceRefresh` is passed as `true` when the API has just rejected the token
|
|
17
|
-
* this returned
|
|
18
|
-
*
|
|
19
|
+
* this returned, and `options` always asks for `cachedUntilExpiry` (#63): a
|
|
20
|
+
* source that can keep sending its cached token through a throttled refresh
|
|
21
|
+
* should, since a connector's token never reaches a browser. The signature
|
|
22
|
+
* is `TokenProvider.getToken`'s, so
|
|
23
|
+
* `getToken: (f, o) => provider.getToken(f, o)` is a complete implementation.
|
|
19
24
|
*/
|
|
20
|
-
getToken?: (forceRefresh?: boolean) => Promise<string | {
|
|
25
|
+
getToken?: (forceRefresh?: boolean, options?: GetTokenOptions) => Promise<string | {
|
|
21
26
|
token?: string;
|
|
22
27
|
} | undefined>;
|
|
23
28
|
/** Falls back to `LOCATION_CLIENT_ID` / `LOCATION_SERVICE_CLIENT_ID`. */
|
|
@@ -109,6 +114,16 @@ export declare class LocationServiceConnector {
|
|
|
109
114
|
* header beats the connector default, whatever the caller capitalised.
|
|
110
115
|
*/
|
|
111
116
|
private effectiveOrigin;
|
|
117
|
+
/**
|
|
118
|
+
* Send a command, and resolve with its output:
|
|
119
|
+
* `await connector.send(new SearchTextCommand(…))` is a
|
|
120
|
+
* `SearchTextCommandOutput`, with nothing to annotate (#68).
|
|
121
|
+
*/
|
|
122
|
+
send<C extends CommandWithOutput>(command: C, options?: SendOptions): Promise<CommandOutput<C>>;
|
|
123
|
+
/**
|
|
124
|
+
* The signature `send` had before it inferred its output (#68), kept for
|
|
125
|
+
* calls naming both type arguments, as `GeoPlacesClient.send` keeps it.
|
|
126
|
+
*/
|
|
112
127
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
113
128
|
/**
|
|
114
129
|
* Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
|
|
@@ -140,7 +140,10 @@ class LocationServiceConnector {
|
|
|
140
140
|
return {
|
|
141
141
|
apiUrl: () => requireApiUrl(apiUrl),
|
|
142
142
|
get: async (forceRefresh) => {
|
|
143
|
-
|
|
143
|
+
// As the environment source below asks its provider (#63).
|
|
144
|
+
const result = await getToken(forceRefresh, {
|
|
145
|
+
cachedUntilExpiry: true,
|
|
146
|
+
});
|
|
144
147
|
if (!result)
|
|
145
148
|
return undefined;
|
|
146
149
|
return typeof result === 'string' ? result : result.token;
|
|
@@ -162,7 +165,9 @@ class LocationServiceConnector {
|
|
|
162
165
|
// Already validated by serverTokenSource, which cannot resolve
|
|
163
166
|
// credentials without it.
|
|
164
167
|
apiUrl: () => env.apiUrl,
|
|
165
|
-
|
|
168
|
+
// A throttled refresh keeps the cached token in use until its own exp
|
|
169
|
+
// (#63): this is a server dispatch, never a token handed to a browser.
|
|
170
|
+
get: async (forceRefresh) => (await env.getToken(forceRefresh, { cachedUntilExpiry: true })).token,
|
|
166
171
|
};
|
|
167
172
|
}
|
|
168
173
|
/**
|
|
@@ -200,8 +205,11 @@ class LocationServiceConnector {
|
|
|
200
205
|
const source = this.source();
|
|
201
206
|
const cmd = command;
|
|
202
207
|
const url = `${source.apiUrl()}${(0, endpoints_js_1.resolveEndpoint)(cmd)}`;
|
|
208
|
+
// The call's deadline starts here, before the token is waited for, so the
|
|
209
|
+
// caller's `overallTimeoutMs` and `signal` bound the whole call (#62).
|
|
210
|
+
const call = (0, http_js_1.startCall)(options);
|
|
203
211
|
try {
|
|
204
|
-
return await this.dispatchWithRetry(source, url, cmd,
|
|
212
|
+
return await this.dispatchWithRetry(source, url, cmd, call);
|
|
205
213
|
}
|
|
206
214
|
catch (err) {
|
|
207
215
|
throw explainMissingOrigin(err, this.effectiveOrigin(options));
|
|
@@ -219,63 +227,14 @@ class LocationServiceConnector {
|
|
|
219
227
|
return this.send(new commands_js_1.VerifyAddressCommand({ PlaceId: placeId }), options);
|
|
220
228
|
}
|
|
221
229
|
async dispatchWithRetry(source, url, cmd, options) {
|
|
222
|
-
|
|
230
|
+
// Raced against the caller's signal and deadline, not given them: the
|
|
231
|
+
// token fetch may be shared with other calls (#62).
|
|
232
|
+
const token = await (0, http_js_1.withinCall)(source.get(), options);
|
|
223
233
|
if (!token)
|
|
224
234
|
throw (0, errors_js_1.noTokenAvailable)(NO_TOKEN_ADVICE);
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
// followed said nothing about when to ask again, it is asked now — but the
|
|
229
|
-
// refused token is still not sent.
|
|
230
|
-
const held = this.refused.check(token);
|
|
231
|
-
if (held && !held.askAgain)
|
|
232
|
-
throw held.error;
|
|
233
|
-
let rejected = held?.error;
|
|
234
|
-
if (!held) {
|
|
235
|
-
try {
|
|
236
|
-
return await this.dispatch(url, token, cmd, options);
|
|
237
|
-
}
|
|
238
|
-
catch (err) {
|
|
239
|
-
if (!(0, errors_js_1.isTokenRejected)(err))
|
|
240
|
-
throw err;
|
|
241
|
-
rejected = err;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// One retry, and only when the replacement is genuinely a different token.
|
|
245
|
-
// That single comparison covers every source: a fixed `token` string, a
|
|
246
|
-
// caller `getToken` that ignores `forceRefresh`, and a cached token the API
|
|
247
|
-
// has revoked before its `exp` all hand back what we already sent — and
|
|
248
|
-
// re-sending it would be a second doomed request for the same answer.
|
|
249
|
-
let fresh;
|
|
250
|
-
try {
|
|
251
|
-
fresh = await source.get(true);
|
|
252
|
-
}
|
|
253
|
-
catch (refusal) {
|
|
254
|
-
// A suspended application's /auth/token refuses it as its data routes
|
|
255
|
-
// refuse its token. Without this, every send asked for another. A
|
|
256
|
-
// refusal that says nothing — a network fault — leaves the source to be
|
|
257
|
-
// asked again, but not the token sent.
|
|
258
|
-
if ((0, tokenHold_js_1.holdFor)(refusal) > 0)
|
|
259
|
-
this.refused.remember(refusal, token);
|
|
260
|
-
// Only when no hold stands: one already standing keeps its own end, so
|
|
261
|
-
// the refused token is tried again once per hold rather than never.
|
|
262
|
-
else if (!held)
|
|
263
|
-
this.refused.remember(rejected, token, { askAgain: true });
|
|
264
|
-
throw refusal;
|
|
265
|
-
}
|
|
266
|
-
if (!fresh || fresh === token) {
|
|
267
|
-
this.refused.remember(rejected, token);
|
|
268
|
-
throw rejected;
|
|
269
|
-
}
|
|
270
|
-
log('401 on a token the API no longer accepts — retrying once, refreshed');
|
|
271
|
-
try {
|
|
272
|
-
return await this.dispatch(url, fresh, cmd, options);
|
|
273
|
-
}
|
|
274
|
-
catch (again) {
|
|
275
|
-
if ((0, errors_js_1.isTokenRejected)(again))
|
|
276
|
-
this.refused.remember(again, fresh);
|
|
277
|
-
throw again;
|
|
278
|
-
}
|
|
235
|
+
// Through `sendRetryingOnce`, like every 401 retry here (#38). The forced
|
|
236
|
+
// refresh is raced against the caller as the first ask was (#62).
|
|
237
|
+
return (0, tokenHold_js_1.sendRetryingOnce)(this.refused, token, (t) => this.dispatch(url, t, cmd, options), () => (0, http_js_1.withinCall)(source.get(true), options), () => log('401 on a token the API no longer accepts — retrying once, refreshed'));
|
|
279
238
|
}
|
|
280
239
|
dispatch(url, token, cmd, options) {
|
|
281
240
|
// The caller's input goes out as the caller wrote it — nothing in the body
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TokenResponse } from '../auth/TokenProvider.js';
|
|
1
|
+
import type { GetTokenOptions, TokenResponse } from '../auth/TokenProvider.js';
|
|
2
2
|
import type { ClientConfig } from '../types/index.js';
|
|
3
3
|
export interface ServerAuthConfig {
|
|
4
4
|
apiUrl?: string;
|
|
@@ -60,8 +60,12 @@ export declare function resolveApiUrl(explicit?: string): string | undefined;
|
|
|
60
60
|
*/
|
|
61
61
|
export interface ServerTokenSource {
|
|
62
62
|
apiUrl: string;
|
|
63
|
-
/**
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Resolves with a token or rejects; it never resolves tokenless. The options
|
|
65
|
+
* are `TokenProvider.getToken`'s: the connector asks for `cachedUntilExpiry`
|
|
66
|
+
* (#63), and `getClientConfig` never does.
|
|
67
|
+
*/
|
|
68
|
+
getToken(forceRefresh?: boolean, options?: GetTokenOptions): Promise<TokenResponse & {
|
|
65
69
|
token: string;
|
|
66
70
|
}>;
|
|
67
71
|
}
|
|
@@ -84,10 +84,10 @@ function resolveApiUrl(explicit) {
|
|
|
84
84
|
*
|
|
85
85
|
* Two answers qualify. `/auth/token`'s own `Invalid credentials` is a secret
|
|
86
86
|
* that matched nothing. The gateway's 401 (`UnauthorizedException`) is the
|
|
87
|
-
* authorizer refusing the Basic pair before `/auth/token` runs
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* every 403
|
|
87
|
+
* authorizer refusing the Basic pair before `/auth/token` runs. An application
|
|
88
|
+
* that is not active is a 403 (`ApplicationNotActiveException`) on either
|
|
89
|
+
* path, whose sentence names its cause, and it is left as the API wrote it,
|
|
90
|
+
* like every 403.
|
|
91
91
|
*/
|
|
92
92
|
function isCredentialsRefusal(error) {
|
|
93
93
|
if (!(error instanceof LocationServiceException_js_1.LocationServiceException))
|
|
@@ -99,11 +99,8 @@ function isCredentialsRefusal(error) {
|
|
|
99
99
|
}
|
|
100
100
|
/** The API's words as a sentence, so the advice can follow them. */
|
|
101
101
|
const sentence = (text) => (/[.!?]$/.test(text) ? text : `${text}.`);
|
|
102
|
-
function credentialsAdvice(clientId
|
|
103
|
-
|
|
104
|
-
return error.code === 'UnauthorizedException'
|
|
105
|
-
? `${check}, and that the application is active there.`
|
|
106
|
-
: `${check}.`;
|
|
102
|
+
function credentialsAdvice(clientId) {
|
|
103
|
+
return `Check that LOCATION_CLIENT_ID ("${clientId}") and LOCATION_CLIENT_SECRET match your application in the developer portal.`;
|
|
107
104
|
}
|
|
108
105
|
function serverTokenSource(config = {}) {
|
|
109
106
|
log('[serverTokenSource] Starting with config:', {
|
|
@@ -139,11 +136,11 @@ function serverTokenSource(config = {}) {
|
|
|
139
136
|
const provider = getTokenProvider(apiUrl, clientId, clientSecret);
|
|
140
137
|
return {
|
|
141
138
|
apiUrl,
|
|
142
|
-
async getToken(forceRefresh = false) {
|
|
139
|
+
async getToken(forceRefresh = false, options) {
|
|
143
140
|
log('[serverTokenSource] Fetching token (forceRefresh=%s)', forceRefresh);
|
|
144
141
|
let result;
|
|
145
142
|
try {
|
|
146
|
-
result = await provider.getToken(forceRefresh);
|
|
143
|
+
result = await provider.getToken(forceRefresh, options);
|
|
147
144
|
}
|
|
148
145
|
catch (error) {
|
|
149
146
|
// The API's code and sentence, passed through (#38). Every 401 and 403
|
|
@@ -153,7 +150,7 @@ function serverTokenSource(config = {}) {
|
|
|
153
150
|
if (isCredentialsRefusal(error)) {
|
|
154
151
|
throw new LocationServiceException_js_1.LocationServiceException({
|
|
155
152
|
code: error.code,
|
|
156
|
-
message: `${sentence(error.message)} ${credentialsAdvice(clientId
|
|
153
|
+
message: `${sentence(error.message)} ${credentialsAdvice(clientId)}`,
|
|
157
154
|
statusCode: error.statusCode,
|
|
158
155
|
requestId: error.requestId,
|
|
159
156
|
details: error.details,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { TokenProvider } from '../auth/TokenProvider.js';
|
|
2
|
-
export type { TokenProviderConfig, TokenResponse, } from '../auth/TokenProvider.js';
|
|
2
|
+
export type { GetTokenOptions, TokenProviderConfig, TokenResponse, } from '../auth/TokenProvider.js';
|
|
3
3
|
export { getClientConfig } from './getClientConfig.js';
|
|
4
4
|
export type { ServerAuthConfig, ServerClientConfig } from './getClientConfig.js';
|
|
5
5
|
export { LocationServiceConnector } from './LocationServiceConnector.js';
|
|
@@ -38,6 +38,24 @@ export interface RequestOptions {
|
|
|
38
38
|
maxAttempts?: number;
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A call's options once it has begun: `deadline` is when `overallTimeoutMs`
|
|
43
|
+
* runs out, fixed at the call's entry, so that a wait before the request — a
|
|
44
|
+
* token — spends the same budget the request then gets the rest of (#62).
|
|
45
|
+
*/
|
|
46
|
+
export interface CallOptions extends RequestOptions {
|
|
47
|
+
deadline: number;
|
|
48
|
+
}
|
|
49
|
+
/** Fix a call's deadline at its entry (#62). */
|
|
50
|
+
export declare function startCall<T extends RequestOptions>(options?: T): T & CallOptions;
|
|
51
|
+
/**
|
|
52
|
+
* Wait for `work` within the call: reject with `AbortedException` when the
|
|
53
|
+
* caller's signal aborts, and with `TimeoutException` when its deadline
|
|
54
|
+
* passes, whichever comes first (#62). `work` itself is left running, because
|
|
55
|
+
* it may be shared: one caller's abort must not fail another waiting on the
|
|
56
|
+
* same token fetch.
|
|
57
|
+
*/
|
|
58
|
+
export declare function withinCall<T>(work: Promise<T>, call: CallOptions): Promise<T>;
|
|
41
59
|
/** Exponential backoff with FULL jitter, so retries never march in lockstep. */
|
|
42
60
|
export declare function backoffMs(attempt: number, random?: () => number): number;
|
|
43
61
|
/**
|
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_OVERALL_TIMEOUT_MS = exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
7
|
+
exports.startCall = startCall;
|
|
8
|
+
exports.withinCall = withinCall;
|
|
7
9
|
exports.backoffMs = backoffMs;
|
|
8
10
|
exports.requestJson = requestJson;
|
|
9
11
|
exports.requestBlob = requestBlob;
|
|
@@ -34,6 +36,40 @@ exports.DEFAULT_OVERALL_TIMEOUT_MS = 30000;
|
|
|
34
36
|
exports.DEFAULT_MAX_ATTEMPTS = 3;
|
|
35
37
|
const BACKOFF_BASE_MS = 250;
|
|
36
38
|
const BACKOFF_CAP_MS = 4000;
|
|
39
|
+
/** Fix a call's deadline at its entry (#62). */
|
|
40
|
+
function startCall(options = {}) {
|
|
41
|
+
return {
|
|
42
|
+
...options,
|
|
43
|
+
deadline: Date.now() + (options.overallTimeoutMs ?? exports.DEFAULT_OVERALL_TIMEOUT_MS),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wait for `work` within the call: reject with `AbortedException` when the
|
|
48
|
+
* caller's signal aborts, and with `TimeoutException` when its deadline
|
|
49
|
+
* passes, whichever comes first (#62). `work` itself is left running, because
|
|
50
|
+
* it may be shared: one caller's abort must not fail another waiting on the
|
|
51
|
+
* same token fetch.
|
|
52
|
+
*/
|
|
53
|
+
function withinCall(work, call) {
|
|
54
|
+
const { signal, deadline } = call;
|
|
55
|
+
const overallTimeoutMs = call.overallTimeoutMs ?? exports.DEFAULT_OVERALL_TIMEOUT_MS;
|
|
56
|
+
if (signal?.aborted)
|
|
57
|
+
return Promise.reject(abortedException(signal));
|
|
58
|
+
const left = deadline - Date.now();
|
|
59
|
+
if (left <= 0)
|
|
60
|
+
return Promise.reject(overallTimeoutException(overallTimeoutMs));
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const timer = setTimeout(() => finish(() => reject(overallTimeoutException(overallTimeoutMs))), left);
|
|
63
|
+
const onAbort = () => finish(() => reject(abortedException(signal)));
|
|
64
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
65
|
+
function finish(settle) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
signal?.removeEventListener('abort', onAbort);
|
|
68
|
+
settle();
|
|
69
|
+
}
|
|
70
|
+
work.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
37
73
|
/**
|
|
38
74
|
* Combine the caller's signal with a per-attempt timeout.
|
|
39
75
|
*
|
|
@@ -115,7 +151,9 @@ async function request(url, init, options, read) {
|
|
|
115
151
|
details: { source: 'client' },
|
|
116
152
|
});
|
|
117
153
|
}
|
|
118
|
-
|
|
154
|
+
// A call that began before this request (#62) brings its own deadline, so
|
|
155
|
+
// the request has only what the wait before it left.
|
|
156
|
+
const deadline = options.deadline ?? Date.now() + overallTimeoutMs;
|
|
119
157
|
let lastError;
|
|
120
158
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
121
159
|
// Checked before every attempt: a signal aborted during backoff must not fire one more.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VerifyAddressCommand, VerifyAddressResponse } from '../client/commands.js';
|
|
1
2
|
/**
|
|
2
3
|
* At least one of `token`, `getToken` or `refreshToken` must supply a token, or
|
|
3
4
|
* `send` refuses locally with `InvalidCredentialsException` rather than putting
|
|
@@ -52,6 +53,30 @@ export interface ClientConfig {
|
|
|
52
53
|
export interface GeoPlacesCommand {
|
|
53
54
|
readonly input: object;
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The part of an AWS SDK command that carries its output type: the request
|
|
58
|
+
* handler its `resolveMiddleware` builds resolves with `{ output }`. The SDK's
|
|
59
|
+
* own `send` reads the output from the command the same way.
|
|
60
|
+
*/
|
|
61
|
+
interface SdkCommandOutputs<O extends object = object> {
|
|
62
|
+
resolveMiddleware(...args: never[]): (...args: never[]) => Promise<{
|
|
63
|
+
output: O;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A command whose output `send` can name (#68): every SDK command, this
|
|
68
|
+
* package's narrowed Places commands among them, and `VerifyAddressCommand`.
|
|
69
|
+
*/
|
|
70
|
+
export type CommandWithOutput = SdkCommandOutputs | VerifyAddressCommand;
|
|
71
|
+
/**
|
|
72
|
+
* What `send` resolves with for a command (#68): an SDK command's own
|
|
73
|
+
* `…CommandOutput`, and `VerifyAddressResponse` for `VerifyAddressCommand`,
|
|
74
|
+
* which is this package's and has no handler.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* type Out = CommandOutput<AutocompleteCommand> // AutocompleteCommandOutput
|
|
78
|
+
*/
|
|
79
|
+
export type CommandOutput<C extends CommandWithOutput> = C extends SdkCommandOutputs<infer O> ? O : VerifyAddressResponse;
|
|
55
80
|
/**
|
|
56
81
|
* Minimal interface for a MapLibre Map instance.
|
|
57
82
|
* Using a structural type avoids hard coupling to a specific maplibre-gl version.
|
|
@@ -69,3 +94,4 @@ export interface MapLike {
|
|
|
69
94
|
getLayoutProperty(layerId: string, name: string): unknown;
|
|
70
95
|
setLayoutProperty(layerId: string, name: string, value: unknown): void;
|
|
71
96
|
}
|
|
97
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RequestOptions } from '../transport/http.js';
|
|
2
|
-
import type { ClientConfig } from '../types/index.js';
|
|
2
|
+
import type { ClientConfig, CommandOutput, CommandWithOutput } from '../types/index.js';
|
|
3
3
|
import type { AppConfigClaims } from '../utils/tokenClaims.js';
|
|
4
4
|
import type { VerifyAddressResponse } from './commands.js';
|
|
5
5
|
export type SendOptions = RequestOptions;
|
|
@@ -40,6 +40,11 @@ export declare class GeoPlacesClient {
|
|
|
40
40
|
getAppConfig(): AppConfigClaims;
|
|
41
41
|
/** Prefer the getToken callback (live ref) over a static token string. */
|
|
42
42
|
private currentToken;
|
|
43
|
+
/**
|
|
44
|
+
* `refreshToken`, held to the caller's signal and deadline (#62). A slow or
|
|
45
|
+
* stuck callback used to hold `send` past both.
|
|
46
|
+
*/
|
|
47
|
+
private askRefreshToken;
|
|
43
48
|
/**
|
|
44
49
|
* A token to send, or a refusal — never `undefined`.
|
|
45
50
|
*
|
|
@@ -48,10 +53,21 @@ export declare class GeoPlacesClient {
|
|
|
48
53
|
*/
|
|
49
54
|
private ensureToken;
|
|
50
55
|
/**
|
|
56
|
+
* Send a command, and resolve with its output:
|
|
57
|
+
* `await client.send(new AutocompleteCommand(…))` is an
|
|
58
|
+
* `AutocompleteCommandOutput`, with nothing to annotate (#68).
|
|
59
|
+
*
|
|
51
60
|
* @param options `signal` to cancel, `timeoutMs` per attempt,
|
|
52
61
|
* `overallTimeoutMs` for the whole call, `retry: false` to disable the
|
|
53
62
|
* retry loop. Every failure throws LocationServiceException.
|
|
54
63
|
*/
|
|
64
|
+
send<C extends CommandWithOutput>(command: C, options?: SendOptions): Promise<CommandOutput<C>>;
|
|
65
|
+
/**
|
|
66
|
+
* The signature `send` had before it inferred its output (#68). It stays
|
|
67
|
+
* so that a call naming both type arguments, and a client typed by a
|
|
68
|
+
* structural `send<TInput, TOutput>` — as `@chaosity/address-form` types
|
|
69
|
+
* its client — still compile.
|
|
70
|
+
*/
|
|
55
71
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
56
72
|
/**
|
|
57
73
|
* Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
|
-
import { TokenHold,
|
|
2
|
+
import { TokenHold, sendRetryingOnce } from '../auth/tokenHold.js';
|
|
3
3
|
import { resolveEndpoint } from '../transport/endpoints.js';
|
|
4
|
-
import {
|
|
5
|
-
import { requestJson } from '../transport/http.js';
|
|
4
|
+
import { noTokenAvailable } from '../transport/errors.js';
|
|
5
|
+
import { requestJson, startCall, withinCall } from '../transport/http.js';
|
|
6
6
|
import { readAppConfigClaims } from '../utils/tokenClaims.js';
|
|
7
7
|
import { VerifyAddressCommand } from './commands.js';
|
|
8
8
|
const log = debug('location-client:api');
|
|
@@ -51,13 +51,21 @@ export class GeoPlacesClient {
|
|
|
51
51
|
currentToken() {
|
|
52
52
|
return this.clientConfig.getToken?.() ?? this.clientConfig.token;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* `refreshToken`, held to the caller's signal and deadline (#62). A slow or
|
|
56
|
+
* stuck callback used to hold `send` past both.
|
|
57
|
+
*/
|
|
58
|
+
askRefreshToken(call) {
|
|
59
|
+
const asked = this.clientConfig.refreshToken?.();
|
|
60
|
+
return asked ? withinCall(asked, call) : Promise.resolve(undefined);
|
|
61
|
+
}
|
|
54
62
|
/**
|
|
55
63
|
* A token to send, or a refusal — never `undefined`.
|
|
56
64
|
*
|
|
57
65
|
* `refreshToken` is asked only when there is nothing at all in hand, so a
|
|
58
66
|
* client configured the ordinary way pays nothing for this.
|
|
59
67
|
*/
|
|
60
|
-
async ensureToken() {
|
|
68
|
+
async ensureToken(call) {
|
|
61
69
|
// Truthiness, not `??`: an empty string is a token source with nothing to
|
|
62
70
|
// give, not a decision to send an empty one. With `??` it survived the
|
|
63
71
|
// coalesce, skipped `refreshToken`, and then failed the check below — so
|
|
@@ -74,7 +82,7 @@ export class GeoPlacesClient {
|
|
|
74
82
|
throw held;
|
|
75
83
|
let token;
|
|
76
84
|
try {
|
|
77
|
-
token = await this.
|
|
85
|
+
token = await this.askRefreshToken(call);
|
|
78
86
|
}
|
|
79
87
|
catch (refusal) {
|
|
80
88
|
this.refused.remember(refusal, NO_TOKEN);
|
|
@@ -85,11 +93,6 @@ export class GeoPlacesClient {
|
|
|
85
93
|
}
|
|
86
94
|
return token;
|
|
87
95
|
}
|
|
88
|
-
/**
|
|
89
|
-
* @param options `signal` to cancel, `timeoutMs` per attempt,
|
|
90
|
-
* `overallTimeoutMs` for the whole call, `retry: false` to disable the
|
|
91
|
-
* retry loop. Every failure throws LocationServiceException.
|
|
92
|
-
*/
|
|
93
96
|
async send(command, options) {
|
|
94
97
|
const cmd = command;
|
|
95
98
|
const url = `${this.clientConfig.apiUrl}${resolveEndpoint(cmd)}`;
|
|
@@ -100,62 +103,15 @@ export class GeoPlacesClient {
|
|
|
100
103
|
// source has not produced one yet spent a whole round trip to learn
|
|
101
104
|
// something it already knew. Ask the refresh source instead, and refuse if
|
|
102
105
|
// there is still nothing.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
if (held && !held.askAgain)
|
|
111
|
-
throw held.error;
|
|
112
|
-
let rejected = held?.error;
|
|
113
|
-
if (!held) {
|
|
114
|
-
try {
|
|
115
|
-
return await this.dispatch(url, token, cmd, options);
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
if (!isTokenRejected(err))
|
|
119
|
-
throw err;
|
|
120
|
-
rejected = err;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
// One shot. `refreshToken` is the only way to actually obtain a new token
|
|
124
|
-
// here — `getToken` is synchronous and returns the one already in hand —
|
|
125
|
-
// but it is re-read as a fallback because a provider that refreshes in the
|
|
106
|
+
// The call's deadline starts here, before any wait for a token (#62).
|
|
107
|
+
const call = startCall(options);
|
|
108
|
+
const token = await this.ensureToken(call);
|
|
109
|
+
// One shot, through `sendRetryingOnce` like every 401 retry here (#38).
|
|
110
|
+
// `refreshToken` is the only way to actually obtain a new token here —
|
|
111
|
+
// `getToken` is synchronous and returns the one already in hand — but it
|
|
112
|
+
// is re-read as a fallback because a provider that refreshes in the
|
|
126
113
|
// background may have landed a new one while this request was in flight.
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
fresh = (await this.clientConfig.refreshToken?.()) ?? this.currentToken();
|
|
130
|
-
}
|
|
131
|
-
catch (refusal) {
|
|
132
|
-
// A suspended application's token route refuses it as its data routes
|
|
133
|
-
// refuse its token. Without this, every send asked again. A refusal that
|
|
134
|
-
// says nothing — a network fault, or a Server Action's error without its
|
|
135
|
-
// fields — leaves the source to be asked again, but not the token sent.
|
|
136
|
-
if (holdFor(refusal) > 0)
|
|
137
|
-
this.refused.remember(refusal, token);
|
|
138
|
-
// Only when no hold stands: one already standing keeps its own end, so
|
|
139
|
-
// the refused token is tried again once per hold rather than never.
|
|
140
|
-
else if (!held)
|
|
141
|
-
this.refused.remember(rejected, token, { askAgain: true });
|
|
142
|
-
throw refusal;
|
|
143
|
-
}
|
|
144
|
-
// Nothing new to send. Repeating the request would fail identically — a
|
|
145
|
-
// second round trip for the same 401.
|
|
146
|
-
if (!fresh || fresh === token) {
|
|
147
|
-
this.refused.remember(rejected, token);
|
|
148
|
-
throw rejected;
|
|
149
|
-
}
|
|
150
|
-
log('401 — retrying %s once with a refreshed token', cmd.constructor?.name);
|
|
151
|
-
try {
|
|
152
|
-
return await this.dispatch(url, fresh, cmd, options);
|
|
153
|
-
}
|
|
154
|
-
catch (again) {
|
|
155
|
-
if (isTokenRejected(again))
|
|
156
|
-
this.refused.remember(again, fresh);
|
|
157
|
-
throw again;
|
|
158
|
-
}
|
|
114
|
+
return sendRetryingOnce(this.refused, token, (t) => this.dispatch(url, t, cmd, call), async () => (await this.askRefreshToken(call)) ?? this.currentToken(), () => log('401 — retrying %s once with a refreshed token', cmd.constructor?.name));
|
|
159
115
|
}
|
|
160
116
|
/**
|
|
161
117
|
* Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,9 @@ export type { RequestOptions } from './transport/http.js';
|
|
|
5
5
|
export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
|
|
6
6
|
export { FEATURE_NOT_ENTITLED, LocationServiceException, } from './errors/LocationServiceException.js';
|
|
7
7
|
export type { LocationServiceErrorCode, LocationServiceExceptionOptions, } from './errors/LocationServiceException.js';
|
|
8
|
-
export * from '
|
|
8
|
+
export * from './aws.js';
|
|
9
9
|
export { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, VerifyAddressCommand, } from './client/commands.js';
|
|
10
10
|
export type { AutocompleteCommandInput, AutocompleteRequest, GeocodeCommandInput, GeocodeRequest, GetPlaceCommandInput, GetPlaceRequest, NeverForwarded, ReverseGeocodeCommandInput, ReverseGeocodeRequest, SearchNearbyCommandInput, SearchNearbyRequest, SearchTextCommandInput, SearchTextRequest, SuggestCommandInput, SuggestRequest, VerifyAddressCommandInput, VerifyAddressResponse, } from './client/commands.js';
|
|
11
|
-
export * from '@aws/amazon-location-utilities-datatypes';
|
|
12
11
|
export { GeoPlaces } from './adapters/GeoPlaces.js';
|
|
13
12
|
export type { GeoPlacesDetailOptions, GeoPlacesOptions, } from './adapters/GeoPlaces.js';
|
|
14
13
|
export { createTransformRequest } from './maps/createTransformRequest.js';
|
|
@@ -17,10 +16,12 @@ export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/m
|
|
|
17
16
|
export type { PoiCategory } from './maps/mapPoi.js';
|
|
18
17
|
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle.js';
|
|
19
18
|
export type { MapStyleOptions } from './maps/mapStyle.js';
|
|
19
|
+
export { refreshTokenOnUnauthorized } from './maps/mapToken.js';
|
|
20
|
+
export type { MapTokenSource, MapTokens, TokenRefreshMap, } from './maps/mapToken.js';
|
|
20
21
|
export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap.js';
|
|
21
22
|
export type { StaticMapFileName, StaticMapOptions } from './maps/staticMap.js';
|
|
22
23
|
export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, POI_DENSITIES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, STYLE_POI_CATEGORIES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums.js';
|
|
23
24
|
export type { Buildings, ColorScheme, ContourDensity, LabelSize, MapFeatureMode, MapStyle, PoiDensity, ScaleBarUnit, SpriteVariant, StaticMapStyle, StylePoiCategory, Terrain, TrafficMode, TravelMode, } from './maps/mapEnums.js';
|
|
24
25
|
export { transformRequest } from './maps/Utils.js';
|
|
25
|
-
export type { ClientConfig, GeoPlacesCommand, MapLike } from './types/index.js';
|
|
26
|
+
export type { ClientConfig, CommandOutput, CommandWithOutput, GeoPlacesCommand, MapLike, } from './types/index.js';
|
|
26
27
|
export type { AppConfigClaims } from './utils/tokenClaims.js';
|