@chaosity/location-client 0.12.0 → 0.13.1

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.
Files changed (62) hide show
  1. package/README.md +125 -36
  2. package/dist/adapters/GeoPlaces.d.ts +7 -1
  3. package/dist/adapters/GeoPlaces.js +11 -5
  4. package/dist/auth/TokenProvider.d.ts +22 -1
  5. package/dist/auth/TokenProvider.js +42 -2
  6. package/dist/auth/tokenHold.d.ts +14 -5
  7. package/dist/auth/tokenHold.js +70 -5
  8. package/dist/aws.d.ts +4 -0
  9. package/dist/aws.js +2 -0
  10. package/dist/cjs/adapters/GeoPlaces.d.ts +7 -1
  11. package/dist/cjs/adapters/GeoPlaces.js +10 -4
  12. package/dist/cjs/auth/TokenProvider.d.ts +22 -1
  13. package/dist/cjs/auth/TokenProvider.js +42 -2
  14. package/dist/cjs/auth/tokenHold.d.ts +14 -5
  15. package/dist/cjs/auth/tokenHold.js +71 -5
  16. package/dist/cjs/aws.d.ts +4 -0
  17. package/dist/cjs/aws.js +155 -0
  18. package/dist/cjs/client/GeoPlacesClient.d.ts +17 -1
  19. package/dist/cjs/client/GeoPlacesClient.js +18 -62
  20. package/dist/cjs/index.d.ts +4 -3
  21. package/dist/cjs/index.js +13 -8
  22. package/dist/cjs/maps/createTransformRequest.d.ts +25 -0
  23. package/dist/cjs/maps/createTransformRequest.js +1 -0
  24. package/dist/cjs/maps/mapPoi.d.ts +10 -5
  25. package/dist/cjs/maps/mapPoi.js +14 -5
  26. package/dist/cjs/maps/mapStyle.d.ts +9 -2
  27. package/dist/cjs/maps/mapStyle.js +16 -12
  28. package/dist/cjs/maps/mapToken.d.ts +87 -0
  29. package/dist/cjs/maps/mapToken.js +181 -0
  30. package/dist/cjs/maps/staticMap.d.ts +7 -3
  31. package/dist/cjs/maps/staticMap.js +13 -12
  32. package/dist/cjs/server/LocationServiceConnector.d.ts +18 -3
  33. package/dist/cjs/server/LocationServiceConnector.js +17 -58
  34. package/dist/cjs/server/getClientConfig.d.ts +7 -3
  35. package/dist/cjs/server/getClientConfig.js +9 -12
  36. package/dist/cjs/server/index.d.ts +1 -1
  37. package/dist/cjs/transport/http.d.ts +18 -0
  38. package/dist/cjs/transport/http.js +39 -1
  39. package/dist/cjs/types/index.d.ts +26 -0
  40. package/dist/client/GeoPlacesClient.d.ts +17 -1
  41. package/dist/client/GeoPlacesClient.js +21 -65
  42. package/dist/index.d.ts +4 -3
  43. package/dist/index.js +11 -7
  44. package/dist/maps/createTransformRequest.d.ts +25 -0
  45. package/dist/maps/createTransformRequest.js +1 -1
  46. package/dist/maps/mapPoi.d.ts +10 -5
  47. package/dist/maps/mapPoi.js +14 -5
  48. package/dist/maps/mapStyle.d.ts +9 -2
  49. package/dist/maps/mapStyle.js +17 -13
  50. package/dist/maps/mapToken.d.ts +87 -0
  51. package/dist/maps/mapToken.js +177 -0
  52. package/dist/maps/staticMap.d.ts +7 -3
  53. package/dist/maps/staticMap.js +14 -13
  54. package/dist/server/LocationServiceConnector.d.ts +18 -3
  55. package/dist/server/LocationServiceConnector.js +20 -61
  56. package/dist/server/getClientConfig.d.ts +7 -3
  57. package/dist/server/getClientConfig.js +9 -12
  58. package/dist/server/index.d.ts +1 -1
  59. package/dist/transport/http.d.ts +18 -0
  60. package/dist/transport/http.js +37 -1
  61. package/dist/types/index.d.ts +26 -0
  62. package/package.json +3 -3
@@ -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, holdFor } from '../auth/tokenHold.js';
2
+ import { TokenHold, sendRetryingOnce } from '../auth/tokenHold.js';
3
3
  import { resolveEndpoint } from '../transport/endpoints.js';
4
- import { isTokenRejected, noTokenAvailable } from '../transport/errors.js';
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.clientConfig.refreshToken?.();
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
- const token = await this.ensureToken();
104
- // This token was refused a moment ago and nothing has replaced it: answer
105
- // with that refusal rather than send it, and ask `refreshToken`, again
106
- // (#38). A different token from `getToken` ends the hold. When the refresh
107
- // that followed said nothing about when to ask again, it is asked now —
108
- // but the refused token is still not sent.
109
- const held = this.refused.check(token);
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
- let fresh;
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 '@aws-sdk/client-geo-places';
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';
package/dist/index.js CHANGED
@@ -6,15 +6,18 @@ export { DEFAULT_MAX_ATTEMPTS, DEFAULT_OVERALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, }
6
6
  export { TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from './auth/tokenRefresh.js';
7
7
  // Errors
8
8
  export { FEATURE_NOT_ENTITLED, LocationServiceException, } from './errors/LocationServiceException.js';
9
- // Re-export AWS SDK commands and types
10
- export * from '@aws-sdk/client-geo-places';
11
- // …except the seven Places commands and their inputs, which take neither
12
- // IntendedUse nor Key: the service strips both from every request (#40). A
13
- // named export wins over the `export *` above, as GeoPlacesClient's does.
9
+ // The AWS SDK's commands and types, and AWS Location Utilities' converters
10
+ // (#42): generated into ./aws.ts, values by name and types by `export type *`.
11
+ // Not `export *` of the packages themselves, which kept ~93 KB of SDK in the
12
+ // bundle of a consumer importing only a map helper — scripts/sdk-exports.mjs
13
+ // says why. A name this file exports itself is left out of ./aws.ts.
14
+ export * from './aws.js';
15
+ // The seven Places commands and their inputs are this package's own: they take
16
+ // neither IntendedUse nor Key, which the service strips from every request
17
+ // (#40). A named export wins over `export *`, as GeoPlacesClient's does, and
18
+ // ./aws.ts leaves them out.
14
19
  // VerifyAddressCommand is this package's own: its route has no SDK command (#54).
15
20
  export { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, VerifyAddressCommand, } from './client/commands.js';
16
- // Re-export AWS Location Utilities (data type conversions)
17
- export * from '@aws/amazon-location-utilities-datatypes';
18
21
  // Adapters (Custom - for MapLibre integration)
19
22
  export { GeoPlaces } from './adapters/GeoPlaces.js';
20
23
  // Maps utilities
@@ -22,6 +25,7 @@ export { createTransformRequest } from './maps/createTransformRequest.js';
22
25
  export { applyMapLanguage } from './maps/mapLanguage.js';
23
26
  export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi.js';
24
27
  export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle.js';
28
+ export { refreshTokenOnUnauthorized } from './maps/mapToken.js';
25
29
  export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap.js';
26
30
  // Accepted values for every map parameter, as VALUES so a picker can be built
27
31
  // from them, plus the matching types. Case sensitive — see mapEnums.ts.
@@ -1,4 +1,29 @@
1
1
  import type { RequestTransformFunction } from 'maplibre-gl';
2
+ /**
3
+ * Does this URL belong to our API?
4
+ *
5
+ * This decides who receives the customer's bearer token, and it used to be
6
+ * `url.startsWith(apiUrl)` — a string test standing in for a URL test. For
7
+ * `apiUrl = "https://api.example.com"`, the host `api.example.com.evil.test`
8
+ * is a prefix match, so a style referencing
9
+ * `https://api.example.com.evil.test/tiles/1/2/3` was handed
10
+ * `Authorization: Bearer <token>` and the token left the building (#34).
11
+ *
12
+ * That is reachable because a style descriptor is DATA: its `sprite`, `glyphs`
13
+ * and `sources` entries are URLs the style author chose, and MapLibre asks
14
+ * transformRequest about every one of them. Any style not wholly ours — a
15
+ * customer's own, or one edited through a tool — can name a host it likes.
16
+ *
17
+ * Compared as URLs instead, which also gets host case-folding, default ports
18
+ * (`https://api.test:443` === `https://api.test`) and userinfo right for free,
19
+ * and adds a path check so a shared host serving another tenant under a
20
+ * different base path is not "ours" either.
21
+ *
22
+ * Fails CLOSED: anything that will not parse gets no token. The only way to
23
+ * reach that is a relative `apiUrl` in a runtime with no `location` to resolve
24
+ * it against — i.e. not a browser, which is the only place MapLibre runs.
25
+ */
26
+ export declare function isOurApi(url: string, apiUrl: string): boolean;
2
27
  /**
3
28
  * Creates a transformRequest function for MapLibre that adds authentication
4
29
  * and proper Accept headers for AWS Location Service API requests.
@@ -22,7 +22,7 @@
22
22
  * reach that is a relative `apiUrl` in a runtime with no `location` to resolve
23
23
  * it against — i.e. not a browser, which is the only place MapLibre runs.
24
24
  */
25
- function isOurApi(url, apiUrl) {
25
+ export function isOurApi(url, apiUrl) {
26
26
  const base = typeof location === 'undefined' ? undefined : location.href;
27
27
  let ours;
28
28
  let theirs;
@@ -1,20 +1,25 @@
1
1
  import type { Map } from 'maplibre-gl';
2
2
  /**
3
3
  * Mapping of POI category names to their MapLibre layer IDs in AWS GeoMaps tiles.
4
- * Layer IDs are stable across Standard, Monochrome, and Hybrid map styles.
4
+ *
5
+ * Standard and Hybrid carry the same sixteen `poi*` layers; Monochrome carries
6
+ * only the three park layers, and Satellite none. A category whose layers a
7
+ * style does not carry is skipped. AWS publishes no list of these ids, so
8
+ * `test/map-poi-layers.test.ts` holds this map to each style's layers as the
9
+ * service served them: every `poi*` layer in exactly one category (#33).
5
10
  */
6
11
  export declare const POI_CATEGORIES: {
7
12
  readonly food_drink: readonly ["poi_100_food_drink"];
8
13
  readonly entertainment: readonly ["poi_200_going_out_entertainment"];
9
14
  readonly sights: readonly ["poi_300_sights_museums"];
10
- readonly transit: readonly ["poi_400_transit"];
15
+ readonly transit: readonly ["poi_400_transit", "poi_400_transit_small"];
11
16
  readonly accommodations: readonly ["poi_500_accommodations"];
12
17
  readonly leisure: readonly ["poi_550_leisure_outdoor"];
13
18
  readonly shopping: readonly ["poi_600_shopping"];
14
- readonly business: readonly ["poi_700_business_services"];
15
- readonly facilities: readonly ["poi_800_facilities"];
19
+ readonly business: readonly ["poi_700_business_services", "poi_700_business_services_generic"];
20
+ readonly facilities: readonly ["poi_800_facilities", "poi_800_facilities_generic"];
16
21
  readonly areas: readonly ["poi_900_areas_buildings"];
17
- readonly parks: readonly ["poi_landuse_park", "poi_landuse_public_complex"];
22
+ readonly parks: readonly ["poi_landuse_park", "poi_landuse_park_lowzoom", "poi_landuse_public_complex"];
18
23
  };
19
24
  export type PoiCategory = keyof typeof POI_CATEGORIES;
20
25
  /**
@@ -1,19 +1,28 @@
1
1
  /**
2
2
  * Mapping of POI category names to their MapLibre layer IDs in AWS GeoMaps tiles.
3
- * Layer IDs are stable across Standard, Monochrome, and Hybrid map styles.
3
+ *
4
+ * Standard and Hybrid carry the same sixteen `poi*` layers; Monochrome carries
5
+ * only the three park layers, and Satellite none. A category whose layers a
6
+ * style does not carry is skipped. AWS publishes no list of these ids, so
7
+ * `test/map-poi-layers.test.ts` holds this map to each style's layers as the
8
+ * service served them: every `poi*` layer in exactly one category (#33).
4
9
  */
5
10
  export const POI_CATEGORIES = {
6
11
  food_drink: ['poi_100_food_drink'],
7
12
  entertainment: ['poi_200_going_out_entertainment'],
8
13
  sights: ['poi_300_sights_museums'],
9
- transit: ['poi_400_transit'],
14
+ transit: ['poi_400_transit', 'poi_400_transit_small'],
10
15
  accommodations: ['poi_500_accommodations'],
11
16
  leisure: ['poi_550_leisure_outdoor'],
12
17
  shopping: ['poi_600_shopping'],
13
- business: ['poi_700_business_services'],
14
- facilities: ['poi_800_facilities'],
18
+ business: ['poi_700_business_services', 'poi_700_business_services_generic'],
19
+ facilities: ['poi_800_facilities', 'poi_800_facilities_generic'],
15
20
  areas: ['poi_900_areas_buildings'],
16
- parks: ['poi_landuse_park', 'poi_landuse_public_complex'],
21
+ parks: [
22
+ 'poi_landuse_park',
23
+ 'poi_landuse_park_lowzoom',
24
+ 'poi_landuse_public_complex',
25
+ ],
17
26
  };
18
27
  /**
19
28
  * Set the visibility of one or more POI categories on the map.
@@ -1,6 +1,7 @@
1
1
  import type { StyleSpecification } from 'maplibre-gl';
2
2
  import type { RequestOptions } from '../transport/http.js';
3
3
  import type { Buildings, ColorScheme, ContourDensity, MapStyle, PoiDensity, StylePoiCategory, Terrain, TrafficMode, TravelMode } from './mapEnums.js';
4
+ import type { MapTokenSource } from './mapToken.js';
4
5
  /**
5
6
  * Options for building an AWS Location Service map style URL.
6
7
  * All parameters map directly to query parameters supported by the style descriptor endpoint.
@@ -137,10 +138,16 @@ export declare function buildMapStyleUrl(apiUrl: string, mapStyle: MapStyle, opt
137
138
  * `event.error.status` is 403, and `event.error.body` is a `Blob` holding the
138
139
  * same `{ message, code }` JSON.
139
140
  *
141
+ * A REFUSED TOKEN. Given `{ getToken, refreshToken }` in place of a bare
142
+ * `getToken`, a 401 asks `refreshToken` once and sends again when it brings a
143
+ * different token, within the same `signal` and `overallTimeoutMs` (#72). A
144
+ * 403 is never retried: a new token cannot change it.
145
+ *
140
146
  * @param apiUrl - Base URL of the Location Service API
141
147
  * @param mapStyle - Map style name: 'Standard' or 'Monochrome', or 'Satellite'
142
148
  * or 'Hybrid', which need the `satellite` plan feature (see `MAP_STYLES`)
143
- * @param getToken - Callback returning the current auth token
149
+ * @param tokens - Callback returning the current auth token, or
150
+ * `{ getToken, refreshToken }` to recover from a refused one
144
151
  * @param options - Style options; `language` is applied to the descriptor, all
145
152
  * others become URL params. Those tagged `@planFeature` need that feature of
146
153
  * the application's plan
@@ -151,6 +158,6 @@ export declare function buildMapStyleUrl(apiUrl: string, mapStyle: MapStyle, opt
151
158
  * const style = await fetchMapStyle(API_URL, 'Standard', getToken, { colorScheme: 'Dark', language: 'fr' })
152
159
  * const map = new maplibregl.Map({ style, transformRequest: createTransformRequest(API_URL, getToken) })
153
160
  */
154
- export declare function fetchMapStyle(apiUrl: string, mapStyle: MapStyle, getToken: () => string | undefined, options?: MapStyleOptions & {
161
+ export declare function fetchMapStyle(apiUrl: string, mapStyle: MapStyle, tokens: MapTokenSource, options?: MapStyleOptions & {
155
162
  language?: string;
156
163
  }, request?: RequestOptions): Promise<StyleSpecification>;
@@ -1,6 +1,6 @@
1
- import { noTokenAvailable } from '../transport/errors.js';
2
- import { requestJson } from '../transport/http.js';
1
+ import { requestJson, startCall } from '../transport/http.js';
3
2
  import { labelsByName, languageExpression } from './mapLanguage.js';
3
+ import { sendWithTokenRefresh } from './mapToken.js';
4
4
  /**
5
5
  * Build a map style descriptor URL for the Location Service API.
6
6
  *
@@ -63,10 +63,16 @@ export function buildMapStyleUrl(apiUrl, mapStyle, options = {}) {
63
63
  * `event.error.status` is 403, and `event.error.body` is a `Blob` holding the
64
64
  * same `{ message, code }` JSON.
65
65
  *
66
+ * A REFUSED TOKEN. Given `{ getToken, refreshToken }` in place of a bare
67
+ * `getToken`, a 401 asks `refreshToken` once and sends again when it brings a
68
+ * different token, within the same `signal` and `overallTimeoutMs` (#72). A
69
+ * 403 is never retried: a new token cannot change it.
70
+ *
66
71
  * @param apiUrl - Base URL of the Location Service API
67
72
  * @param mapStyle - Map style name: 'Standard' or 'Monochrome', or 'Satellite'
68
73
  * or 'Hybrid', which need the `satellite` plan feature (see `MAP_STYLES`)
69
- * @param getToken - Callback returning the current auth token
74
+ * @param tokens - Callback returning the current auth token, or
75
+ * `{ getToken, refreshToken }` to recover from a refused one
70
76
  * @param options - Style options; `language` is applied to the descriptor, all
71
77
  * others become URL params. Those tagged `@planFeature` need that feature of
72
78
  * the application's plan
@@ -77,16 +83,10 @@ export function buildMapStyleUrl(apiUrl, mapStyle, options = {}) {
77
83
  * const style = await fetchMapStyle(API_URL, 'Standard', getToken, { colorScheme: 'Dark', language: 'fr' })
78
84
  * const map = new maplibregl.Map({ style, transformRequest: createTransformRequest(API_URL, getToken) })
79
85
  */
80
- export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}, request = {}) {
86
+ export async function fetchMapStyle(apiUrl, mapStyle, tokens, options = {}, request = {}) {
81
87
  const { language, ...styleOptions } = options;
82
88
  const url = buildMapStyleUrl(apiUrl, mapStyle, styleOptions);
83
- const token = getToken();
84
- // `Bearer undefined` used to go out here, and came back as a 401 the caller
85
- // had to work backwards from — a whole round trip for a request that was
86
- // never going to succeed (#37).
87
- if (!token) {
88
- throw noTokenAvailable('getToken() returned nothing, so no style request was sent. Check the token provider has finished initialising.');
89
- }
89
+ const call = startCall(request);
90
90
  // Through the shared transport, not a bare fetch: this gets the same
91
91
  // per-attempt timeout, overall budget, cancellation and retry as every other
92
92
  // call in the package, and the same error type on the way out. It also keeps
@@ -100,12 +100,16 @@ export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}, re
100
100
  // This used to throw `Failed to fetch map style: 400`, discarding all of it
101
101
  // two lines before anyone could read it — the same defect #89 fixed in the
102
102
  // API, one layer up.
103
- const style = await requestJson(url, {
103
+ //
104
+ // `Bearer undefined` used to go out here, and came back as a 401 the caller
105
+ // had to work backwards from — a whole round trip for a request that was
106
+ // never going to succeed (#37). `sendWithTokenRefresh` refuses first.
107
+ const style = await sendWithTokenRefresh(tokens, 'getToken() returned nothing, so no style request was sent. Check the token provider has finished initialising.', call, (token) => requestJson(url, {
104
108
  headers: {
105
109
  Authorization: `Bearer ${token}`,
106
110
  Accept: 'application/json',
107
111
  },
108
- }, request);
112
+ }, call));
109
113
  if (language) {
110
114
  applyLanguageToDescriptor(style, language);
111
115
  }
@@ -0,0 +1,87 @@
1
+ import type { CallOptions } from '../transport/http.js';
2
+ /**
3
+ * Where a map helper gets its token, and how it gets a new one (#72).
4
+ *
5
+ * `getToken` is synchronous by contract: MapLibre's `transformRequest` calls
6
+ * it for every tile and cannot wait. `refreshToken` is how a map recovers when
7
+ * the API refuses the token in hand before its `exp` — a rotated secret, for
8
+ * one — which nothing else in the map path can do. It is the escape hatch
9
+ * `GeoPlacesClient` takes, under the same name.
10
+ */
11
+ export interface MapTokens {
12
+ /** The token in hand, now. */
13
+ getToken: () => string | undefined;
14
+ /** Obtain a new token after the API refused the one in hand. */
15
+ refreshToken?: () => Promise<string | undefined>;
16
+ }
17
+ /** A bare `getToken`, which behaves as it always has, or `MapTokens`. */
18
+ export type MapTokenSource = (() => string | undefined) | MapTokens;
19
+ /**
20
+ * Send with the token in hand, and after a 401 once more with a different one
21
+ * from `refreshToken`, all within the call's signal and deadline (#62). A 403
22
+ * is never retried, and neither is the token the API refused.
23
+ */
24
+ export declare function sendWithTokenRefresh<T>(source: MapTokenSource, noTokenAdvice: string, call: CallOptions, send: (token: string) => Promise<T>): Promise<T>;
25
+ /** A tile's coordinates, as `map.refreshTiles` takes them. */
26
+ interface TileCoordinates {
27
+ x: number;
28
+ y: number;
29
+ z: number;
30
+ }
31
+ /** What MapLibre's `error` event carries for a refused request. */
32
+ interface MapErrorEvent {
33
+ error?: {
34
+ status?: number;
35
+ url?: string;
36
+ };
37
+ sourceId?: string;
38
+ tile?: {
39
+ tileID: {
40
+ canonical: TileCoordinates;
41
+ };
42
+ };
43
+ }
44
+ /** The part of a MapLibre `Map` that `refreshTokenOnUnauthorized` uses. */
45
+ export interface TokenRefreshMap {
46
+ on(type: 'error', listener: (event: MapErrorEvent) => void): unknown;
47
+ off(type: 'error', listener: (event: MapErrorEvent) => void): unknown;
48
+ refreshTiles(sourceId: string, tileIds?: TileCoordinates[]): void;
49
+ }
50
+ /**
51
+ * Recover the tiles MapLibre fetches itself when the API refuses the token
52
+ * (#72).
53
+ *
54
+ * `createTransformRequest` builds each request synchronously and never sees
55
+ * the answer, so a refused tile used to leave a hole until the page reloaded.
56
+ * This listens for MapLibre's `error` events. On a 401 from a URL of our API it
57
+ * asks `refreshToken` once for the whole burst. When `getToken` then returns a
58
+ * different token, it reloads each refused tile with `map.refreshTiles`, whose
59
+ * requests carry that token. So `refreshToken` must make `getToken` return
60
+ * what it obtained: the tiles have no other way to receive it. A refused
61
+ * request that is not a tile (a glyph, a sprite) has the token replaced for
62
+ * the next one, and nothing reloaded.
63
+ *
64
+ * Tiles are reloaded by id, never a whole source: MapLibre 6 reloads a source's
65
+ * errored tiles as still loading, and they wait for a load that never comes.
66
+ *
67
+ * What it learns is held as the fetch helpers hold it, and shared with them
68
+ * when they are handed the same `tokens` object (#38): a token `refreshToken`
69
+ * could not replace, a token it brought that the API refused on a tile
70
+ * reloaded with it, and a failure that says when to ask again, are not asked
71
+ * about again until they lapse or the token in hand changes. While the token
72
+ * a refresh brought is in hand, any other refused tile is reloaded once with
73
+ * it, without asking.
74
+ *
75
+ * Returns a function that stops listening.
76
+ *
77
+ * @example
78
+ * const tokens = { getToken, refreshToken }
79
+ * const map = new Map({
80
+ * container: 'map',
81
+ * style: await fetchMapStyle(API_URL, 'Standard', tokens),
82
+ * transformRequest: createTransformRequest(API_URL, getToken),
83
+ * })
84
+ * refreshTokenOnUnauthorized(map, API_URL, tokens)
85
+ */
86
+ export declare function refreshTokenOnUnauthorized(map: TokenRefreshMap, apiUrl: string, tokens: Required<MapTokens>): () => void;
87
+ export {};