@chaosity/location-client 0.8.0 → 0.10.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 +264 -42
- package/dist/adapters/GeoPlaces.d.ts +30 -3
- package/dist/adapters/GeoPlaces.js +5 -1
- package/dist/cjs/adapters/GeoPlaces.d.ts +30 -3
- package/dist/cjs/adapters/GeoPlaces.js +5 -4
- package/dist/cjs/client/GeoPlacesClient.d.ts +16 -4
- package/dist/cjs/client/GeoPlacesClient.js +24 -10
- package/dist/cjs/client/commands.d.ts +159 -0
- package/dist/cjs/client/commands.js +108 -0
- package/dist/cjs/errors/LocationServiceException.d.ts +28 -1
- package/dist/cjs/errors/LocationServiceException.js +31 -2
- package/dist/cjs/index.d.ts +5 -3
- package/dist/cjs/index.js +17 -1
- package/dist/cjs/maps/mapEnums.d.ts +82 -4
- package/dist/cjs/maps/mapEnums.js +98 -5
- package/dist/cjs/maps/mapStyle.d.ts +87 -10
- package/dist/cjs/maps/mapStyle.js +28 -5
- package/dist/cjs/maps/staticMap.d.ts +40 -0
- package/dist/cjs/maps/staticMap.js +4 -0
- package/dist/cjs/server/LocationServiceConnector.d.ts +23 -5
- package/dist/cjs/server/LocationServiceConnector.js +32 -14
- package/dist/cjs/transport/endpoints.js +3 -0
- package/dist/cjs/transport/http.d.ts +2 -2
- package/dist/cjs/transport/http.js +2 -2
- package/dist/cjs/types/index.d.ts +6 -5
- package/dist/cjs/utils/tokenClaims.d.ts +37 -13
- package/dist/cjs/utils/tokenClaims.js +36 -14
- package/dist/client/GeoPlacesClient.d.ts +16 -4
- package/dist/client/GeoPlacesClient.js +24 -10
- package/dist/client/commands.d.ts +159 -0
- package/dist/client/commands.js +97 -0
- package/dist/errors/LocationServiceException.d.ts +28 -1
- package/dist/errors/LocationServiceException.js +30 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +7 -2
- package/dist/maps/mapEnums.d.ts +82 -4
- package/dist/maps/mapEnums.js +97 -4
- package/dist/maps/mapStyle.d.ts +87 -10
- package/dist/maps/mapStyle.js +28 -5
- package/dist/maps/staticMap.d.ts +40 -0
- package/dist/maps/staticMap.js +4 -0
- package/dist/server/LocationServiceConnector.d.ts +23 -5
- package/dist/server/LocationServiceConnector.js +32 -14
- package/dist/transport/endpoints.js +3 -0
- package/dist/transport/http.d.ts +2 -2
- package/dist/transport/http.js +2 -2
- package/dist/types/index.d.ts +6 -5
- package/dist/utils/tokenClaims.d.ts +37 -13
- package/dist/utils/tokenClaims.js +36 -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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
|
+
import { VerifyAddressCommand } from '../client/commands.js';
|
|
2
3
|
import { LocationServiceException } from '../errors/LocationServiceException.js';
|
|
3
4
|
import { resolveEndpoint } from '../transport/endpoints.js';
|
|
4
5
|
import { isTokenRejected, noTokenAvailable } from '../transport/errors.js';
|
|
5
6
|
import { requestJson } from '../transport/http.js';
|
|
6
|
-
import { roundPositionFields } from '../utils/roundPosition.js';
|
|
7
7
|
import { readAppConfigClaims } from '../utils/tokenClaims.js';
|
|
8
8
|
import { resolveApiUrl, serverTokenSource } from './getClientConfig.js';
|
|
9
9
|
const log = debug('location-client:connector');
|
|
@@ -89,7 +89,13 @@ function explainMissingOrigin(err, sentOrigin) {
|
|
|
89
89
|
* ```typescript
|
|
90
90
|
* // Credentials and apiUrl from the environment, Origin supplied here
|
|
91
91
|
* const connector = new LocationServiceConnector({ origin: 'https://app.example.com' })
|
|
92
|
-
* const result = await connector.send(
|
|
92
|
+
* const result = await connector.send(
|
|
93
|
+
* new SearchTextCommand({
|
|
94
|
+
* QueryText: 'Space Needle',
|
|
95
|
+
* // SearchText takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
|
|
96
|
+
* BiasPosition: [-122.3493, 47.6205],
|
|
97
|
+
* }),
|
|
98
|
+
* )
|
|
93
99
|
* ```
|
|
94
100
|
*/
|
|
95
101
|
export class LocationServiceConnector {
|
|
@@ -152,17 +158,19 @@ export class LocationServiceConnector {
|
|
|
152
158
|
}
|
|
153
159
|
/**
|
|
154
160
|
* This application's own configuration, as carried on the access token
|
|
155
|
-
* (api#65)
|
|
161
|
+
* (api#65): the routes it may call, the domain its requests must come from,
|
|
162
|
+
* and the countries it is scoped to (#40).
|
|
156
163
|
*
|
|
157
164
|
* Provided so an application can SHOW its own settings: populate a country
|
|
158
165
|
* selector with the markets it actually serves, label a settings screen, and
|
|
159
166
|
* so on. Being a few minutes stale is cosmetic for that.
|
|
160
167
|
*
|
|
161
168
|
* It is not an entitlement check. See AppConfigClaims for why acting on
|
|
162
|
-
*
|
|
169
|
+
* any of it client-side makes requests fail that would otherwise succeed.
|
|
163
170
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
171
|
+
* Every token the API issues carries `allowedResources` and `allowedDomain`;
|
|
172
|
+
* `countries` only once a scope is configured in the portal. Returns `{}`
|
|
173
|
+
* for a token carrying none of them.
|
|
166
174
|
*/
|
|
167
175
|
async getAppConfig() {
|
|
168
176
|
return readAppConfigClaims(await this.source().get());
|
|
@@ -190,6 +198,17 @@ export class LocationServiceConnector {
|
|
|
190
198
|
throw explainMissingOrigin(err, this.effectiveOrigin(options));
|
|
191
199
|
}
|
|
192
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
|
|
203
|
+
* (#54). Resolves the full place record plus `verified`, and resolves a
|
|
204
|
+
* `verified: false` too — see VerifyAddressResponse for what may be stored.
|
|
205
|
+
*
|
|
206
|
+
* Billed per call, whether or not the address verifies: call it once per
|
|
207
|
+
* chosen PlaceId, never per keystroke.
|
|
208
|
+
*/
|
|
209
|
+
verifyAddress(placeId, options) {
|
|
210
|
+
return this.send(new VerifyAddressCommand({ PlaceId: placeId }), options);
|
|
211
|
+
}
|
|
193
212
|
async dispatchWithRetry(source, url, cmd, options) {
|
|
194
213
|
const token = await source.get();
|
|
195
214
|
if (!token)
|
|
@@ -214,13 +233,12 @@ export class LocationServiceConnector {
|
|
|
214
233
|
}
|
|
215
234
|
}
|
|
216
235
|
dispatch(url, token, cmd, options) {
|
|
217
|
-
// The
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
const input = roundPositionFields(cmd.input, biasDecimals);
|
|
236
|
+
// The caller's input goes out as the caller wrote it — nothing in the body
|
|
237
|
+
// is derived from the token any more. `BiasPosition` used to be rounded
|
|
238
|
+
// here to a grid sized by a token claim, so nearby callers shared a server
|
|
239
|
+
// cache entry; with no cache the rounding only lowered the precision the
|
|
240
|
+
// upstream geocoder had to work with, which moves the results rather than
|
|
241
|
+
// coarsening them (#51).
|
|
224
242
|
// Every system header is set exactly ONCE, and the caller's own spelling of
|
|
225
243
|
// each is dropped first.
|
|
226
244
|
//
|
|
@@ -245,7 +263,7 @@ export class LocationServiceConnector {
|
|
|
245
263
|
Authorization: `Bearer ${token}`,
|
|
246
264
|
};
|
|
247
265
|
log('Sending %s request to %s', cmd.constructor?.name, url);
|
|
248
|
-
return requestJson(url, { method: 'POST', headers, body: JSON.stringify(input) }, options);
|
|
266
|
+
return requestJson(url, { method: 'POST', headers, body: JSON.stringify(cmd.input) }, options);
|
|
249
267
|
}
|
|
250
268
|
}
|
|
251
269
|
function requireApiUrl(explicit) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
|
|
2
|
+
import { VerifyAddressCommand } from '../client/commands.js';
|
|
2
3
|
import { LocationServiceException } from '../errors/LocationServiceException.js';
|
|
3
4
|
/**
|
|
4
5
|
* Command class -> API path.
|
|
@@ -17,6 +18,8 @@ const ENDPOINTS = new Map([
|
|
|
17
18
|
[SearchNearbyCommand, '/address/search/nearby'],
|
|
18
19
|
[SearchTextCommand, '/address/search/text'],
|
|
19
20
|
[SuggestCommand, '/address/suggestion'],
|
|
21
|
+
// This package's own: the route has no AWS command (#54).
|
|
22
|
+
[VerifyAddressCommand, '/address/verify'],
|
|
20
23
|
]);
|
|
21
24
|
export function resolveEndpoint(command) {
|
|
22
25
|
for (const [CommandClass, endpoint] of ENDPOINTS) {
|
package/dist/transport/http.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
|
4
4
|
* Ceiling for the WHOLE call — every attempt plus every wait between them.
|
|
5
5
|
*
|
|
6
6
|
* `timeoutMs` bounds an attempt, not a call, and the gap between those two is
|
|
7
|
-
* where the caller's own deadline disappears.
|
|
8
|
-
*
|
|
7
|
+
* where the caller's own deadline disappears. Nothing bounds the `Retry-After`
|
|
8
|
+
* a 429 carries, and the retry loop honoured one of 60 s literally: two waits of
|
|
9
9
|
* a minute each, so one call could sit for ~120 s — past any Lambda budget,
|
|
10
10
|
* past any HTTP gateway, and until now uncancellable (#37).
|
|
11
11
|
*
|
package/dist/transport/http.js
CHANGED
|
@@ -8,8 +8,8 @@ export const DEFAULT_TIMEOUT_MS = 10000;
|
|
|
8
8
|
* Ceiling for the WHOLE call — every attempt plus every wait between them.
|
|
9
9
|
*
|
|
10
10
|
* `timeoutMs` bounds an attempt, not a call, and the gap between those two is
|
|
11
|
-
* where the caller's own deadline disappears.
|
|
12
|
-
*
|
|
11
|
+
* where the caller's own deadline disappears. Nothing bounds the `Retry-After`
|
|
12
|
+
* a 429 carries, and the retry loop honoured one of 60 s literally: two waits of
|
|
13
13
|
* a minute each, so one call could sit for ~120 s — past any Lambda budget,
|
|
14
14
|
* past any HTTP gateway, and until now uncancellable (#37).
|
|
15
15
|
*
|
package/dist/types/index.d.ts
CHANGED
|
@@ -42,11 +42,12 @@ export interface ClientConfig {
|
|
|
42
42
|
refreshToken?: () => Promise<string | undefined>;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* Minimal interface for AWS SDK
|
|
46
|
-
*
|
|
47
|
-
* Smithy's Command base class which has an
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* Minimal interface for a command object: the AWS SDK's, and this package's
|
|
46
|
+
* own `VerifyAddressCommand` (#54). The SDK's commands (AutocompleteCommand,
|
|
47
|
+
* SearchTextCommand, etc.) extend Smithy's Command base class, which has an
|
|
48
|
+
* `input` property containing the request parameters; `VerifyAddressCommand`
|
|
49
|
+
* has the same `input` and nothing else. This interface captures what we
|
|
50
|
+
* actually need from commands without coupling to Smithy internals.
|
|
50
51
|
*/
|
|
51
52
|
export interface GeoPlacesCommand {
|
|
52
53
|
readonly input: object;
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read advisory application config out of the access token (api#65).
|
|
3
3
|
*
|
|
4
|
-
* The API puts an application's own settings — `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The API puts an application's own settings into the JWT — `countries`,
|
|
5
|
+
* `allowedResources` and `allowedDomain` — so this library can stop
|
|
6
|
+
* hard-coding values it has no other way of knowing. The last two were in the
|
|
7
|
+
* token all along and were not surfaced until #40, so an application could
|
|
8
|
+
* only learn it lacked a route from the 403.
|
|
9
|
+
*
|
|
10
|
+
* `biasDecimals` used to be here too. It sized the grid this library rounded
|
|
11
|
+
* `BiasPosition` onto, so that nearby callers shared a server cache entry; the
|
|
12
|
+
* cache is gone, the API issues no such claim, and rounding a coordinate with
|
|
13
|
+
* nothing to share it with only lowered the precision the upstream geocoder
|
|
14
|
+
* received (#51).
|
|
7
15
|
*
|
|
8
16
|
* DELIBERATELY UNVERIFIED, and that is safe. This library has no signing key
|
|
9
17
|
* and does not need one: every claim here is re-read from the application row
|
|
10
18
|
* by the API on each request, and the API's answer is the one that counts. A
|
|
11
19
|
* forged token would fail at the authorizer long before any of this mattered.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* Nothing read here reaches a request at all now — it is displayed, never
|
|
21
|
+
* acted on, so a forged value misinforms only the caller who forged it.
|
|
14
22
|
*
|
|
15
23
|
* A JWT is signed, not encrypted, so the payload is plain base64url. Nothing
|
|
16
24
|
* secret is in it; these are the caller's own settings.
|
|
17
25
|
*/
|
|
18
26
|
export interface AppConfigClaims {
|
|
19
|
-
/**
|
|
20
|
-
* Bias precision this application is entitled to.
|
|
21
|
-
*
|
|
22
|
-
* Safe to act on: it only changes how a coordinate is rounded before
|
|
23
|
-
* sending, and the server re-rounds to its own configured value anyway. A
|
|
24
|
-
* stale value here costs precision, never correctness.
|
|
25
|
-
*/
|
|
26
|
-
biasDecimals?: number;
|
|
27
27
|
/**
|
|
28
28
|
* Countries this application may search, ISO 3166-1 alpha-2.
|
|
29
29
|
*
|
|
@@ -44,6 +44,30 @@ export interface AppConfigClaims {
|
|
|
44
44
|
* Sending nothing and letting the API scope the request is always correct.
|
|
45
45
|
*/
|
|
46
46
|
countries?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* The routes this application may call, as the API names them — method and
|
|
49
|
+
* route template, such as `POST /address/autocomplete` or
|
|
50
|
+
* `GET /maps/static/{fileName}` (#40).
|
|
51
|
+
*
|
|
52
|
+
* So an application can ask "may I call the static-map route?" before it
|
|
53
|
+
* offers one, rather than only learning from the 403. It answers for the
|
|
54
|
+
* route, not for the options sent on it: a plan feature (`@planFeature`) is
|
|
55
|
+
* answered only by its own 403, and a static map's default Satellite render
|
|
56
|
+
* is one (#55). The same rule as `countries`
|
|
57
|
+
* applies, for the same reason: show it, never refuse with it. A route
|
|
58
|
+
* granted since the token was minted is answered by the API, which reads the
|
|
59
|
+
* entitlement fresh on every request.
|
|
60
|
+
*
|
|
61
|
+
* The API issues this claim JSON-encoded — a string holding the list —
|
|
62
|
+
* because it copies the application's stored setting. Both forms are read.
|
|
63
|
+
*/
|
|
64
|
+
allowedResources?: string[];
|
|
65
|
+
/**
|
|
66
|
+
* The domain this application's requests must come from (#40). A request
|
|
67
|
+
* whose `Origin` is neither this host nor one of its subdomains is refused
|
|
68
|
+
* 403, so this is what to show next to "Origin not allowed".
|
|
69
|
+
*/
|
|
70
|
+
allowedDomain?: string;
|
|
47
71
|
}
|
|
48
72
|
/**
|
|
49
73
|
* Decode a JWT payload without verifying it.
|
|
@@ -1,20 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read advisory application config out of the access token (api#65).
|
|
3
3
|
*
|
|
4
|
-
* The API puts an application's own settings — `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The API puts an application's own settings into the JWT — `countries`,
|
|
5
|
+
* `allowedResources` and `allowedDomain` — so this library can stop
|
|
6
|
+
* hard-coding values it has no other way of knowing. The last two were in the
|
|
7
|
+
* token all along and were not surfaced until #40, so an application could
|
|
8
|
+
* only learn it lacked a route from the 403.
|
|
9
|
+
*
|
|
10
|
+
* `biasDecimals` used to be here too. It sized the grid this library rounded
|
|
11
|
+
* `BiasPosition` onto, so that nearby callers shared a server cache entry; the
|
|
12
|
+
* cache is gone, the API issues no such claim, and rounding a coordinate with
|
|
13
|
+
* nothing to share it with only lowered the precision the upstream geocoder
|
|
14
|
+
* received (#51).
|
|
7
15
|
*
|
|
8
16
|
* DELIBERATELY UNVERIFIED, and that is safe. This library has no signing key
|
|
9
17
|
* and does not need one: every claim here is re-read from the application row
|
|
10
18
|
* by the API on each request, and the API's answer is the one that counts. A
|
|
11
19
|
* forged token would fail at the authorizer long before any of this mattered.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
20
|
+
* Nothing read here reaches a request at all now — it is displayed, never
|
|
21
|
+
* acted on, so a forged value misinforms only the caller who forged it.
|
|
14
22
|
*
|
|
15
23
|
* A JWT is signed, not encrypted, so the payload is plain base64url. Nothing
|
|
16
24
|
* secret is in it; these are the caller's own settings.
|
|
17
25
|
*/
|
|
26
|
+
/** A list of strings, whether the claim carries it as a list or JSON-encoded. */
|
|
27
|
+
function readStringList(value) {
|
|
28
|
+
let list = value;
|
|
29
|
+
if (typeof list === 'string') {
|
|
30
|
+
try {
|
|
31
|
+
list = JSON.parse(list);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(list))
|
|
38
|
+
return undefined;
|
|
39
|
+
return list.filter((v) => typeof v === 'string');
|
|
40
|
+
}
|
|
18
41
|
/**
|
|
19
42
|
* Decode a JWT payload without verifying it.
|
|
20
43
|
*
|
|
@@ -36,20 +59,19 @@ export function readAppConfigClaims(token) {
|
|
|
36
59
|
const json = decodeURIComponent(Array.from(atob(padded), (c) => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`).join(''));
|
|
37
60
|
const payload = JSON.parse(json);
|
|
38
61
|
const claims = {};
|
|
39
|
-
if (typeof payload.biasDecimals === 'number') {
|
|
40
|
-
claims.biasDecimals = payload.biasDecimals;
|
|
41
|
-
}
|
|
42
|
-
else if (typeof payload.biasDecimals === 'string' &&
|
|
43
|
-
payload.biasDecimals.trim() !== '') {
|
|
44
|
-
const n = Number(payload.biasDecimals);
|
|
45
|
-
if (Number.isFinite(n))
|
|
46
|
-
claims.biasDecimals = n;
|
|
47
|
-
}
|
|
48
62
|
if (Array.isArray(payload.countries)) {
|
|
49
63
|
const list = payload.countries.filter((c) => typeof c === 'string');
|
|
50
64
|
if (list.length)
|
|
51
65
|
claims.countries = list;
|
|
52
66
|
}
|
|
67
|
+
// Kept when empty, unlike countries: no countries means "search the
|
|
68
|
+
// world", but no resources means an application entitled to nothing.
|
|
69
|
+
const resources = readStringList(payload.allowedResources);
|
|
70
|
+
if (resources)
|
|
71
|
+
claims.allowedResources = resources;
|
|
72
|
+
if (typeof payload.allowedDomain === 'string' && payload.allowedDomain) {
|
|
73
|
+
claims.allowedDomain = payload.allowedDomain;
|
|
74
|
+
}
|
|
53
75
|
return claims;
|
|
54
76
|
}
|
|
55
77
|
catch {
|
package/package.json
CHANGED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Round coordinate arrays in command inputs so nearby callers share a server
|
|
3
|
-
* cache entry.
|
|
4
|
-
*
|
|
5
|
-
* `BiasPosition` is a hint about where to look, so collapsing it onto a grid
|
|
6
|
-
* lets two users in the same area reuse one upstream answer. `QueryPosition`
|
|
7
|
-
* is not a hint — reverse-geocode resolves an exact point a user clicked or a
|
|
8
|
-
* device reported, and rounding it returns a neighbour's address. It is left
|
|
9
|
-
* alone (RFC-0001 §2).
|
|
10
|
-
*
|
|
11
|
-
* WHY THE DEFAULT MOVED FROM 2 dp TO 3 dp
|
|
12
|
-
*
|
|
13
|
-
* 2 dp is a ~1.1 km grid, and that is not a coarser answer — it is a wrong
|
|
14
|
-
* one. Measured against Amazon Location directly, `QueryText: "cafe"` from
|
|
15
|
-
* Sydney CBD returns a completely different set of places when the bias moves
|
|
16
|
-
* 111 m:
|
|
17
|
-
*
|
|
18
|
-
* base -> Crepe de Paris | Deli Ziosa | CBD Patisserie
|
|
19
|
-
* +111 m -> Cafe Chocolat | Paradiso Cafe | Incanto Coffee
|
|
20
|
-
*
|
|
21
|
-
* So two users a kilometre apart both received whichever places were nearest
|
|
22
|
-
* the FIRST of them, with nothing in the response saying so. The server moved
|
|
23
|
-
* to 3 dp in api#21 — but this library rounds BEFORE sending, on both the
|
|
24
|
-
* browser and server paths, so the precision was already gone by the time the
|
|
25
|
-
* request arrived and that fix never reached anyone using the SDK. This is
|
|
26
|
-
* what makes it reach them.
|
|
27
|
-
*
|
|
28
|
-
* The cost is hit rate: cells shrink 100x in area. RFC-0001 §5 expected only
|
|
29
|
-
* "single-digit to low-double-digit percent" in mixed traffic, so there was
|
|
30
|
-
* little to protect, and a miss costs one upstream call while a wrong answer
|
|
31
|
-
* costs trust.
|
|
32
|
-
*/
|
|
33
|
-
/** The server's floor (api#65). A request for less is clamped up to it. */
|
|
34
|
-
export declare const DEFAULT_BIAS_DECIMALS = 3;
|
|
35
|
-
/**
|
|
36
|
-
* The band the server enforces (api#65). Mirrored here so the value this
|
|
37
|
-
* library rounds to is the value the server will actually key its cache by —
|
|
38
|
-
* rounding to something outside the band just means being wrong about what
|
|
39
|
-
* was sent.
|
|
40
|
-
*
|
|
41
|
-
* The floor is correctness: below 3 dp the nearest places are not the ones
|
|
42
|
-
* returned. The ceiling is cost: cells shrink 100x in area per decimal, so
|
|
43
|
-
* finer precision collapses the cache hit rate and every miss is a billable
|
|
44
|
-
* upstream call.
|
|
45
|
-
*/
|
|
46
|
-
export declare const MIN_BIAS_DECIMALS = 3;
|
|
47
|
-
export declare const MAX_BIAS_DECIMALS = 5;
|
|
48
|
-
/**
|
|
49
|
-
* Clamp a requested precision into the allowed band.
|
|
50
|
-
*
|
|
51
|
-
* Anything absent or malformed becomes the default rather than throwing: this
|
|
52
|
-
* runs on every request, and a surprising token claim must not break geocoding
|
|
53
|
-
* for an application that is otherwise working.
|
|
54
|
-
*/
|
|
55
|
-
export declare function clampBiasDecimals(requested?: unknown): number;
|
|
56
|
-
/**
|
|
57
|
-
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
58
|
-
* Returns the original object if no position fields are present.
|
|
59
|
-
*
|
|
60
|
-
* @param input the command input
|
|
61
|
-
* @param decimals precision for this application; defaults to the floor.
|
|
62
|
-
* Comes from the `biasDecimals` JWT claim where the token
|
|
63
|
-
* carries one (api#65) — an application entitled to finer
|
|
64
|
-
* bias gets it without the caller configuring anything.
|
|
65
|
-
*/
|
|
66
|
-
export declare function roundPositionFields<T extends object>(input: T, decimals?: number): T;
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Round coordinate arrays in command inputs so nearby callers share a server
|
|
4
|
-
* cache entry.
|
|
5
|
-
*
|
|
6
|
-
* `BiasPosition` is a hint about where to look, so collapsing it onto a grid
|
|
7
|
-
* lets two users in the same area reuse one upstream answer. `QueryPosition`
|
|
8
|
-
* is not a hint — reverse-geocode resolves an exact point a user clicked or a
|
|
9
|
-
* device reported, and rounding it returns a neighbour's address. It is left
|
|
10
|
-
* alone (RFC-0001 §2).
|
|
11
|
-
*
|
|
12
|
-
* WHY THE DEFAULT MOVED FROM 2 dp TO 3 dp
|
|
13
|
-
*
|
|
14
|
-
* 2 dp is a ~1.1 km grid, and that is not a coarser answer — it is a wrong
|
|
15
|
-
* one. Measured against Amazon Location directly, `QueryText: "cafe"` from
|
|
16
|
-
* Sydney CBD returns a completely different set of places when the bias moves
|
|
17
|
-
* 111 m:
|
|
18
|
-
*
|
|
19
|
-
* base -> Crepe de Paris | Deli Ziosa | CBD Patisserie
|
|
20
|
-
* +111 m -> Cafe Chocolat | Paradiso Cafe | Incanto Coffee
|
|
21
|
-
*
|
|
22
|
-
* So two users a kilometre apart both received whichever places were nearest
|
|
23
|
-
* the FIRST of them, with nothing in the response saying so. The server moved
|
|
24
|
-
* to 3 dp in api#21 — but this library rounds BEFORE sending, on both the
|
|
25
|
-
* browser and server paths, so the precision was already gone by the time the
|
|
26
|
-
* request arrived and that fix never reached anyone using the SDK. This is
|
|
27
|
-
* what makes it reach them.
|
|
28
|
-
*
|
|
29
|
-
* The cost is hit rate: cells shrink 100x in area. RFC-0001 §5 expected only
|
|
30
|
-
* "single-digit to low-double-digit percent" in mixed traffic, so there was
|
|
31
|
-
* little to protect, and a miss costs one upstream call while a wrong answer
|
|
32
|
-
* costs trust.
|
|
33
|
-
*/
|
|
34
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
exports.MAX_BIAS_DECIMALS = exports.MIN_BIAS_DECIMALS = exports.DEFAULT_BIAS_DECIMALS = void 0;
|
|
36
|
-
exports.clampBiasDecimals = clampBiasDecimals;
|
|
37
|
-
exports.roundPositionFields = roundPositionFields;
|
|
38
|
-
/** The server's floor (api#65). A request for less is clamped up to it. */
|
|
39
|
-
exports.DEFAULT_BIAS_DECIMALS = 3;
|
|
40
|
-
/**
|
|
41
|
-
* The band the server enforces (api#65). Mirrored here so the value this
|
|
42
|
-
* library rounds to is the value the server will actually key its cache by —
|
|
43
|
-
* rounding to something outside the band just means being wrong about what
|
|
44
|
-
* was sent.
|
|
45
|
-
*
|
|
46
|
-
* The floor is correctness: below 3 dp the nearest places are not the ones
|
|
47
|
-
* returned. The ceiling is cost: cells shrink 100x in area per decimal, so
|
|
48
|
-
* finer precision collapses the cache hit rate and every miss is a billable
|
|
49
|
-
* upstream call.
|
|
50
|
-
*/
|
|
51
|
-
exports.MIN_BIAS_DECIMALS = 3;
|
|
52
|
-
exports.MAX_BIAS_DECIMALS = 5;
|
|
53
|
-
/**
|
|
54
|
-
* Clamp a requested precision into the allowed band.
|
|
55
|
-
*
|
|
56
|
-
* Anything absent or malformed becomes the default rather than throwing: this
|
|
57
|
-
* runs on every request, and a surprising token claim must not break geocoding
|
|
58
|
-
* for an application that is otherwise working.
|
|
59
|
-
*/
|
|
60
|
-
function clampBiasDecimals(requested) {
|
|
61
|
-
const n = Number(requested);
|
|
62
|
-
if ((typeof requested !== 'number' &&
|
|
63
|
-
!(typeof requested === 'string' && requested.trim() !== '')) ||
|
|
64
|
-
!Number.isFinite(n)) {
|
|
65
|
-
return exports.DEFAULT_BIAS_DECIMALS;
|
|
66
|
-
}
|
|
67
|
-
const floored = Math.floor(n);
|
|
68
|
-
if (floored < exports.MIN_BIAS_DECIMALS)
|
|
69
|
-
return exports.MIN_BIAS_DECIMALS;
|
|
70
|
-
if (floored > exports.MAX_BIAS_DECIMALS)
|
|
71
|
-
return exports.MAX_BIAS_DECIMALS;
|
|
72
|
-
return floored;
|
|
73
|
-
}
|
|
74
|
-
/** Known position field names and whether they should be rounded */
|
|
75
|
-
const POSITION_FIELDS = {
|
|
76
|
-
BiasPosition: true, // geocode, autocomplete, search — round for cache
|
|
77
|
-
QueryPosition: false, // reverse geocode — keep full precision
|
|
78
|
-
};
|
|
79
|
-
function roundCoord(value, decimals) {
|
|
80
|
-
const factor = 10 ** decimals;
|
|
81
|
-
return Math.round(value * factor) / factor;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
85
|
-
* Returns the original object if no position fields are present.
|
|
86
|
-
*
|
|
87
|
-
* @param input the command input
|
|
88
|
-
* @param decimals precision for this application; defaults to the floor.
|
|
89
|
-
* Comes from the `biasDecimals` JWT claim where the token
|
|
90
|
-
* carries one (api#65) — an application entitled to finer
|
|
91
|
-
* bias gets it without the caller configuring anything.
|
|
92
|
-
*/
|
|
93
|
-
function roundPositionFields(input, decimals = exports.DEFAULT_BIAS_DECIMALS) {
|
|
94
|
-
if (!input || typeof input !== 'object')
|
|
95
|
-
return input;
|
|
96
|
-
const dp = clampBiasDecimals(decimals);
|
|
97
|
-
let cloned = null;
|
|
98
|
-
for (const [field, shouldRound] of Object.entries(POSITION_FIELDS)) {
|
|
99
|
-
if (!shouldRound)
|
|
100
|
-
continue;
|
|
101
|
-
const value = input[field];
|
|
102
|
-
if (!Array.isArray(value))
|
|
103
|
-
continue;
|
|
104
|
-
if (!cloned)
|
|
105
|
-
cloned = { ...input };
|
|
106
|
-
cloned[field] = value.map((v) => typeof v === 'number' && Number.isFinite(v) ? roundCoord(v, dp) : v);
|
|
107
|
-
}
|
|
108
|
-
return cloned ?? input;
|
|
109
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Round coordinate arrays in command inputs so nearby callers share a server
|
|
3
|
-
* cache entry.
|
|
4
|
-
*
|
|
5
|
-
* `BiasPosition` is a hint about where to look, so collapsing it onto a grid
|
|
6
|
-
* lets two users in the same area reuse one upstream answer. `QueryPosition`
|
|
7
|
-
* is not a hint — reverse-geocode resolves an exact point a user clicked or a
|
|
8
|
-
* device reported, and rounding it returns a neighbour's address. It is left
|
|
9
|
-
* alone (RFC-0001 §2).
|
|
10
|
-
*
|
|
11
|
-
* WHY THE DEFAULT MOVED FROM 2 dp TO 3 dp
|
|
12
|
-
*
|
|
13
|
-
* 2 dp is a ~1.1 km grid, and that is not a coarser answer — it is a wrong
|
|
14
|
-
* one. Measured against Amazon Location directly, `QueryText: "cafe"` from
|
|
15
|
-
* Sydney CBD returns a completely different set of places when the bias moves
|
|
16
|
-
* 111 m:
|
|
17
|
-
*
|
|
18
|
-
* base -> Crepe de Paris | Deli Ziosa | CBD Patisserie
|
|
19
|
-
* +111 m -> Cafe Chocolat | Paradiso Cafe | Incanto Coffee
|
|
20
|
-
*
|
|
21
|
-
* So two users a kilometre apart both received whichever places were nearest
|
|
22
|
-
* the FIRST of them, with nothing in the response saying so. The server moved
|
|
23
|
-
* to 3 dp in api#21 — but this library rounds BEFORE sending, on both the
|
|
24
|
-
* browser and server paths, so the precision was already gone by the time the
|
|
25
|
-
* request arrived and that fix never reached anyone using the SDK. This is
|
|
26
|
-
* what makes it reach them.
|
|
27
|
-
*
|
|
28
|
-
* The cost is hit rate: cells shrink 100x in area. RFC-0001 §5 expected only
|
|
29
|
-
* "single-digit to low-double-digit percent" in mixed traffic, so there was
|
|
30
|
-
* little to protect, and a miss costs one upstream call while a wrong answer
|
|
31
|
-
* costs trust.
|
|
32
|
-
*/
|
|
33
|
-
/** The server's floor (api#65). A request for less is clamped up to it. */
|
|
34
|
-
export declare const DEFAULT_BIAS_DECIMALS = 3;
|
|
35
|
-
/**
|
|
36
|
-
* The band the server enforces (api#65). Mirrored here so the value this
|
|
37
|
-
* library rounds to is the value the server will actually key its cache by —
|
|
38
|
-
* rounding to something outside the band just means being wrong about what
|
|
39
|
-
* was sent.
|
|
40
|
-
*
|
|
41
|
-
* The floor is correctness: below 3 dp the nearest places are not the ones
|
|
42
|
-
* returned. The ceiling is cost: cells shrink 100x in area per decimal, so
|
|
43
|
-
* finer precision collapses the cache hit rate and every miss is a billable
|
|
44
|
-
* upstream call.
|
|
45
|
-
*/
|
|
46
|
-
export declare const MIN_BIAS_DECIMALS = 3;
|
|
47
|
-
export declare const MAX_BIAS_DECIMALS = 5;
|
|
48
|
-
/**
|
|
49
|
-
* Clamp a requested precision into the allowed band.
|
|
50
|
-
*
|
|
51
|
-
* Anything absent or malformed becomes the default rather than throwing: this
|
|
52
|
-
* runs on every request, and a surprising token claim must not break geocoding
|
|
53
|
-
* for an application that is otherwise working.
|
|
54
|
-
*/
|
|
55
|
-
export declare function clampBiasDecimals(requested?: unknown): number;
|
|
56
|
-
/**
|
|
57
|
-
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
58
|
-
* Returns the original object if no position fields are present.
|
|
59
|
-
*
|
|
60
|
-
* @param input the command input
|
|
61
|
-
* @param decimals precision for this application; defaults to the floor.
|
|
62
|
-
* Comes from the `biasDecimals` JWT claim where the token
|
|
63
|
-
* carries one (api#65) — an application entitled to finer
|
|
64
|
-
* bias gets it without the caller configuring anything.
|
|
65
|
-
*/
|
|
66
|
-
export declare function roundPositionFields<T extends object>(input: T, decimals?: number): T;
|