@chaosity/location-client 0.1.5 → 0.1.6

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.
@@ -1,5 +1,7 @@
1
- import { GeocodeAdditionalFeature, GeocodeCommand, GetPlaceAdditionalFeature, GetPlaceCommand, ReverseGeocodeCommand } from '@aws-sdk/client-geo-places';
2
- import { geocodeResponseToFeatureCollection, getPlaceResponseToFeatureCollection, reverseGeocodeResponseToFeatureCollection } from '@aws/amazon-location-utilities-datatypes';
1
+ import { GeocodeCommand, GetPlaceAdditionalFeature, GetPlaceCommand, ReverseGeocodeCommand, SuggestAdditionalFeature, SuggestCommand, } from '@aws-sdk/client-geo-places';
2
+ import { geocodeResponseToFeatureCollection, getPlaceResponseToFeatureCollection, reverseGeocodeResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
3
+ import debug from 'debug';
4
+ const log = debug('location-client:geocoder');
3
5
  /**
4
6
  * GeoPlaces - MapLibre adapter for AWS Location Service
5
7
  *
@@ -18,7 +20,7 @@ export class GeoPlaces {
18
20
  return 'en';
19
21
  }
20
22
  async forwardGeocode(config) {
21
- console.log('[GeoPlaces] forwardGeocode called with:', config);
23
+ log('forwardGeocode query=%s', config.query);
22
24
  const center = this.map.getCenter();
23
25
  const biasPosition = config.proximity && config.proximity.length >= 2
24
26
  ? [config.proximity[0], config.proximity[1]]
@@ -29,61 +31,33 @@ export class GeoPlaces {
29
31
  MaxResults: config.limit || 5,
30
32
  Language: this.normalizeLanguage(config.language),
31
33
  };
32
- if (config.countries || config.bbox) {
33
- commandInput.Filter = {};
34
- if (config.countries)
35
- commandInput.Filter.IncludeCountries = config.countries;
36
- if (config.bbox)
37
- commandInput.Filter.BoundingBox = config.bbox;
34
+ if (config.countries) {
35
+ commandInput.Filter = {
36
+ IncludeCountries: Array.isArray(config.countries) ? config.countries : config.countries.split(','),
37
+ };
38
38
  }
39
- console.log('[GeoPlaces] Sending SearchTextCommand with:', commandInput);
40
- const command = new GeocodeCommand(commandInput);
41
- const response = await this.client.send(command);
42
- console.log('[GeoPlaces] SearchTextCommand response:', response);
39
+ const response = await this.client.send(new GeocodeCommand(commandInput));
43
40
  const result = geocodeResponseToFeatureCollection(response, { flattenProperties: true });
44
- console.log('[GeoPlaces] Converted to FeatureCollection:', result);
41
+ log('forwardGeocode returned %d results', result.features?.length ?? 0);
45
42
  return result;
46
43
  }
47
44
  async reverseGeocode(config) {
48
- console.log('[GeoPlaces] reverseGeocode called with:', config);
45
+ log('reverseGeocode query=%o', config.query);
49
46
  const queryPosition = Array.isArray(config.query) && config.query.length >= 2
50
47
  ? [config.query[0], config.query[1]]
51
48
  : [0, 0];
52
49
  const commandInput = {
53
50
  QueryPosition: queryPosition,
54
51
  MaxResults: config.limit || 1,
55
- Language: this.normalizeLanguage(config.language)
52
+ Language: this.normalizeLanguage(config.language),
56
53
  };
57
- if (config.countries) {
58
- commandInput.Filter = { IncludeCountries: config.countries };
59
- }
60
- const command = new ReverseGeocodeCommand(commandInput);
61
- const response = await this.client.send(command);
62
- const maplibreGeocoderFeatureResults = reverseGeocodeResponseToFeatureCollection(response, { flattenProperties: true });
63
- maplibreGeocoderFeatureResults.features.forEach(feature => {
64
- });
65
- console.log('[GeoPlaces] Converted to FeatureCollection:', maplibreGeocoderFeatureResults);
66
- return maplibreGeocoderFeatureResults;
54
+ const response = await this.client.send(new ReverseGeocodeCommand(commandInput));
55
+ const result = reverseGeocodeResponseToFeatureCollection(response, { flattenProperties: true });
56
+ log('reverseGeocode returned %d results', result.features?.length ?? 0);
57
+ return result;
67
58
  }
68
59
  async getSuggestions(config) {
69
- // const center = this.map.getCenter()
70
- // const biasPosition = config.proximity && config.proximity.length >= 2
71
- // ? [config.proximity[0], config.proximity[1]]
72
- // : [center.lng, center.lat]
73
- // const commandInput: any = {
74
- // QueryText: config.query as string,
75
- // BiasPosition: biasPosition,
76
- // MaxResults: config.limit || 5,
77
- // Language: this.normalizeLanguage(config.language),
78
- // AdditionalFeatures: [SuggestAdditionalFeature.CORE],
79
- // }
80
- // if (config.countries || config.bbox) {
81
- // commandInput.Filter = {}
82
- // if (config.countries) commandInput.Filter.IncludeCountries = config.countries
83
- // if (config.bbox) commandInput.Filter.BoundingBox = config.bbox
84
- // }
85
- // const command = new SuggestCommand(commandInput)
86
- // const response = await this.client.send(command) as SuggestResponse
60
+ log('getSuggestions query=%s', config.query);
87
61
  const center = this.map.getCenter();
88
62
  const biasPosition = config.proximity && config.proximity.length >= 2
89
63
  ? [config.proximity[0], config.proximity[1]]
@@ -93,55 +67,51 @@ export class GeoPlaces {
93
67
  BiasPosition: biasPosition,
94
68
  MaxResults: config.limit || 5,
95
69
  Language: this.normalizeLanguage(config.language),
96
- AdditionalFeatures: [GeocodeAdditionalFeature.SECONDARY_ADDRESSES],
70
+ AdditionalFeatures: [SuggestAdditionalFeature.CORE],
71
+ ...(config.countries || config.bbox ? {
72
+ Filter: {
73
+ ...(config.countries ? { IncludeCountries: Array.isArray(config.countries) ? config.countries : config.countries.split(',') } : {}),
74
+ ...(config.bbox ? { BoundingBox: config.bbox } : {}),
75
+ },
76
+ } : {}),
97
77
  };
98
- if (config.countries || config.bbox) {
99
- commandInput.Filter = {};
100
- if (config.countries)
101
- commandInput.Filter.IncludeCountries = Array.isArray(config.countries) ? config.countries : config.countries.split(',');
102
- // if (config.types) commandInput.Filter.IncludePlaceTypes = config.types.split(',').map(type => new GeocodeFilterPlaceType)
78
+ const response = await this.client.send(new SuggestCommand(commandInput));
79
+ const suggestions = { suggestions: [] };
80
+ for (const item of response.ResultItems ?? []) {
81
+ const text = item.Title;
82
+ if (!text)
83
+ continue;
84
+ const placeId = item.Place?.PlaceId;
85
+ suggestions.suggestions.push({ text, placeId });
103
86
  }
104
- console.log('[GeoPlaces] Sending SearchTextCommand with:', commandInput);
105
- const command = new GeocodeCommand(commandInput);
106
- const response = await this.client.send(command);
107
- console.log('[GeoPlaces] SearchTextCommand response:', response);
108
- const maplibreGeocoderSuggestionResults = { suggestions: [] };
109
- response?.ResultItems?.forEach(item => {
110
- const text = item?.Title || item?.Address?.Label;
111
- const placeId = item?.PlaceId;
112
- if (text) {
113
- maplibreGeocoderSuggestionResults.suggestions.push({
114
- text,
115
- placeId,
116
- });
117
- }
118
- });
119
- console.log('[GeoPlaces] Converted to MaplibreGeocoderSuggestionResults:', maplibreGeocoderSuggestionResults);
120
- return maplibreGeocoderSuggestionResults;
87
+ log('getSuggestions returned %d suggestions', suggestions.suggestions.length);
88
+ return suggestions;
121
89
  }
122
90
  async searchByPlaceId(config) {
123
- console.log('[GeoPlaces] searchByPlaceId called with:', config);
124
- const placeId = config.query;
91
+ log('searchByPlaceId placeId=%s', config.query);
125
92
  const command = new GetPlaceCommand({
126
- PlaceId: placeId,
93
+ PlaceId: config.query,
127
94
  Language: this.normalizeLanguage(config.language),
128
- AdditionalFeatures: [GetPlaceAdditionalFeature.ACCESS, GetPlaceAdditionalFeature.SECONDARY_ADDRESSES, GetPlaceAdditionalFeature.CONTACT, GetPlaceAdditionalFeature.TIME_ZONE],
95
+ AdditionalFeatures: [
96
+ GetPlaceAdditionalFeature.ACCESS,
97
+ GetPlaceAdditionalFeature.SECONDARY_ADDRESSES,
98
+ GetPlaceAdditionalFeature.CONTACT,
99
+ GetPlaceAdditionalFeature.TIME_ZONE,
100
+ ],
129
101
  });
130
102
  const response = await this.client.send(command);
131
103
  const result = getPlaceResponseToFeatureCollection(response, { flattenProperties: true });
132
- console.log('[GeoPlaces] GetPlaceCommand response converted to FeatureCollection:', result);
133
104
  const carmenGeojsonFeatures = result.features.map(feature => ({
134
105
  ...feature,
135
106
  text: response.Title || '',
136
107
  place_name: response.Address?.Label || '',
137
108
  place_type: response.PlaceType ? [response.PlaceType] : [],
138
- bbox: response.MapView
109
+ bbox: response.MapView,
139
110
  }));
140
- console.log('[GeoPlaces] Converted to CarmenGeojsonFeatures:', carmenGeojsonFeatures);
111
+ log('searchByPlaceId returned %d features', carmenGeojsonFeatures.length);
141
112
  return { place: carmenGeojsonFeatures };
142
113
  }
143
114
  async localGeocode(config) {
144
- console.log('[GeoPlaces] localGeocode called with:', config);
145
115
  return this.forwardGeocode(config);
146
116
  }
147
117
  }
@@ -1,6 +1,7 @@
1
1
  export interface TokenResponse {
2
2
  success: boolean;
3
3
  token?: string;
4
+ expiresAt?: number;
4
5
  error?: string;
5
6
  }
6
7
  export interface TokenProviderConfig {
@@ -24,7 +25,7 @@ export interface TokenProviderConfig {
24
25
  *
25
26
  * @example
26
27
  * // ✓ Correct: Server-side usage
27
- * import { TokenProvider } from '@chaosity/location-client'
28
+ * import { TokenProvider } from '@chaosity/location-client/server'
28
29
  *
29
30
  * const provider = new TokenProvider({
30
31
  * apiUrl: process.env.API_URL!,
@@ -39,12 +40,11 @@ export interface TokenProviderConfig {
39
40
  export declare class TokenProvider {
40
41
  private config;
41
42
  private cachedToken?;
42
- private oauth2Client;
43
+ private cachedExpiresAt?;
43
44
  private tokenPromise?;
44
45
  constructor(config: TokenProviderConfig);
45
46
  getToken(forceRefresh?: boolean): Promise<TokenResponse>;
46
47
  private fetchToken;
47
- private getTokenExpiry;
48
48
  private isExpired;
49
49
  clearCache(): void;
50
50
  }
@@ -1,5 +1,3 @@
1
- import ClientOAuth2 from 'client-oauth2';
2
- import { decodeJwt } from 'jose';
3
1
  import debug from 'debug';
4
2
  const log = debug('location-client:auth');
5
3
  /**
@@ -18,7 +16,7 @@ const log = debug('location-client:auth');
18
16
  *
19
17
  * @example
20
18
  * // ✓ Correct: Server-side usage
21
- * import { TokenProvider } from '@chaosity/location-client'
19
+ * import { TokenProvider } from '@chaosity/location-client/server'
22
20
  *
23
21
  * const provider = new TokenProvider({
24
22
  * apiUrl: process.env.API_URL!,
@@ -40,19 +38,15 @@ export class TokenProvider {
40
38
  }
41
39
  log('Initializing TokenProvider for %s', config.apiUrl);
42
40
  this.config = config;
43
- this.oauth2Client = new ClientOAuth2({
44
- clientId: config.clientId,
45
- clientSecret: config.clientSecret,
46
- accessTokenUri: `${config.apiUrl}/auth/token`,
47
- });
48
41
  }
49
42
  async getToken(forceRefresh = false) {
50
43
  if (!forceRefresh && this.cachedToken && !this.isExpired()) {
51
- const exp = this.getTokenExpiry();
52
- log('Using cached token (expires in %ds)', exp ? Math.floor(exp - Date.now() / 1000) : 'unknown');
44
+ const expiresIn = this.cachedExpiresAt ? Math.floor((this.cachedExpiresAt - Date.now()) / 1000) : 'unknown';
45
+ log('Using cached token (expires in %ds)', expiresIn);
53
46
  return {
54
47
  success: true,
55
- token: this.cachedToken
48
+ token: this.cachedToken,
49
+ expiresAt: this.cachedExpiresAt
56
50
  };
57
51
  }
58
52
  // If token fetch is already in progress, wait for it
@@ -75,13 +69,39 @@ export class TokenProvider {
75
69
  }
76
70
  async fetchToken() {
77
71
  try {
78
- const token = await this.oauth2Client.credentials.getToken();
79
- this.cachedToken = token.accessToken;
80
- const exp = this.getTokenExpiry();
81
- log('Token acquired successfully (expires in %ds)', exp ? Math.floor(exp - Date.now() / 1000) : 'unknown');
72
+ const { clientId, clientSecret, apiUrl } = this.config;
73
+ const credentials = btoa(`${clientId}:${clientSecret}`);
74
+ const response = await fetch(`${apiUrl}/auth/token`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Authorization': `Basic ${credentials}`,
78
+ 'Content-Type': 'application/x-www-form-urlencoded',
79
+ },
80
+ body: new URLSearchParams({ grant_type: 'client_credentials' }).toString(),
81
+ });
82
+ if (!response.ok) {
83
+ const errorText = await response.text();
84
+ let errorMessage = `Token request failed: ${response.statusText}`;
85
+ try {
86
+ const errorData = JSON.parse(errorText);
87
+ if (errorData.error_description)
88
+ errorMessage = errorData.error_description;
89
+ else if (errorData.error)
90
+ errorMessage = errorData.error;
91
+ }
92
+ catch { /* use statusText */ }
93
+ throw new Error(errorMessage);
94
+ }
95
+ const data = await response.json();
96
+ this.cachedToken = data.access_token;
97
+ // Use expires_in from OAuth2 response; null-coalesce so 0 is not replaced
98
+ const expiresIn = data.expires_in ?? 900;
99
+ this.cachedExpiresAt = Date.now() + (expiresIn * 1000);
100
+ log('Token acquired successfully (expires in %ds)', expiresIn);
82
101
  return {
83
102
  success: true,
84
- token: this.cachedToken
103
+ token: this.cachedToken,
104
+ expiresAt: this.cachedExpiresAt,
85
105
  };
86
106
  }
87
107
  catch (error) {
@@ -92,24 +112,13 @@ export class TokenProvider {
92
112
  };
93
113
  }
94
114
  }
95
- getTokenExpiry() {
96
- if (!this.cachedToken)
97
- return null;
98
- try {
99
- const decoded = decodeJwt(this.cachedToken);
100
- return decoded.exp || null;
101
- }
102
- catch {
103
- return null;
104
- }
105
- }
106
115
  isExpired(bufferSeconds = 60) {
107
- const exp = this.getTokenExpiry();
108
- if (!exp)
116
+ if (!this.cachedExpiresAt)
109
117
  return true;
110
- return Date.now() / 1000 >= (exp - bufferSeconds);
118
+ return Date.now() >= (this.cachedExpiresAt - bufferSeconds * 1000);
111
119
  }
112
120
  clearCache() {
113
121
  this.cachedToken = undefined;
122
+ this.cachedExpiresAt = undefined;
114
123
  }
115
124
  }
@@ -2,8 +2,10 @@ import { ClientConfig } from '../types';
2
2
  /**
3
3
  * GeoPlacesClient - AWS Location Service compatible client with custom auth
4
4
  *
5
- * Uses AWS Location Client command classes but replaces SigV4 auth with Bearer token.
5
+ * Uses AWS SDK command classes but replaces SigV4 auth with Bearer token.
6
6
  * All request/response types are identical to AWS Location Service.
7
+ *
8
+ * Pass `getToken` in config to support live token refresh without recreating the client.
7
9
  */
8
10
  export declare class GeoPlacesClient {
9
11
  private clientConfig;
@@ -1,11 +1,24 @@
1
1
  import debug from 'debug';
2
+ import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
2
3
  const log = debug('location-client:api');
3
4
  const logError = debug('location-client:api:error');
5
+ // Minification-safe endpoint map — uses constructor identity, not class name strings
6
+ const ENDPOINT_MAP = new Map([
7
+ [AutocompleteCommand, '/address/autocomplete'],
8
+ [GeocodeCommand, '/address/geocode'],
9
+ [GetPlaceCommand, '/address/place'],
10
+ [ReverseGeocodeCommand, '/address/search/reverse-geocode'],
11
+ [SearchNearbyCommand, '/address/search/nearby'],
12
+ [SearchTextCommand, '/address/search/text'],
13
+ [SuggestCommand, '/address/suggestion'],
14
+ ]);
4
15
  /**
5
16
  * GeoPlacesClient - AWS Location Service compatible client with custom auth
6
17
  *
7
- * Uses AWS Location Client command classes but replaces SigV4 auth with Bearer token.
18
+ * Uses AWS SDK command classes but replaces SigV4 auth with Bearer token.
8
19
  * All request/response types are identical to AWS Location Service.
20
+ *
21
+ * Pass `getToken` in config to support live token refresh without recreating the client.
9
22
  */
10
23
  export class GeoPlacesClient {
11
24
  constructor(config) {
@@ -17,62 +30,43 @@ export class GeoPlacesClient {
17
30
  const url = `${this.clientConfig.apiUrl}${endpoint}`;
18
31
  const commandName = command.constructor.name;
19
32
  const input = command.input;
20
- log(`[GeoPlacesClient] Sending ${commandName} to ${endpoint}`);
21
- log('[GeoPlacesClient] Request body:', JSON.stringify(input, null, 2));
22
- log('Sending %s request to %s', commandName, endpoint);
33
+ // Prefer getToken callback (live ref) over static token string
34
+ const token = this.clientConfig.getToken?.() ?? this.clientConfig.token;
35
+ log('Sending %s to %s', commandName, endpoint);
23
36
  const startTime = Date.now();
24
37
  const response = await fetch(url, {
25
38
  method: 'POST',
26
39
  headers: {
27
40
  'Content-Type': 'application/json',
28
- 'Authorization': `Bearer ${this.clientConfig.token}`
41
+ 'Authorization': `Bearer ${token}`,
29
42
  },
30
- body: JSON.stringify(input)
43
+ body: JSON.stringify(input),
31
44
  });
32
45
  const duration = Date.now() - startTime;
33
46
  if (!response.ok) {
34
47
  const errorText = await response.text();
35
- log(`[GeoPlacesClient] Request failed: ${response.status} ${response.statusText}`, errorText);
36
48
  logError('Request failed: %s %s (%dms)', response.status, response.statusText, duration);
37
- // Try to parse error message from response
38
49
  let errorMessage = `API request failed: ${response.statusText}`;
39
50
  try {
40
51
  const errorData = JSON.parse(errorText);
41
- if (errorData.message) {
52
+ if (errorData.message)
42
53
  errorMessage = errorData.message;
43
- }
44
54
  }
45
55
  catch {
46
- // If not JSON, use raw text if available
47
56
  if (errorText)
48
57
  errorMessage = errorText;
49
58
  }
50
59
  throw new Error(errorMessage);
51
60
  }
52
61
  const result = await response.json();
53
- log(`[GeoPlacesClient] Response (${duration}ms):`, JSON.stringify(result, null, 2));
54
62
  log('Request successful: %s (%dms)', response.status, duration);
55
63
  return result;
56
64
  }
57
65
  getEndpoint(command) {
58
- const commandName = command.constructor.name;
59
- switch (commandName) {
60
- case 'AutocompleteCommand':
61
- return '/address/autocomplete';
62
- case 'GeocodeCommand':
63
- return '/address/geocode';
64
- case 'GetPlaceCommand':
65
- return '/address/place';
66
- case 'ReverseGeocodeCommand':
67
- return '/address/search/reverse-geocode';
68
- case 'SearchNearbyCommand':
69
- return '/address/search/nearby';
70
- case 'SearchTextCommand':
71
- return '/address/search/text';
72
- case 'SuggestCommand':
73
- return '/address/suggestion';
74
- default:
75
- throw new Error(`Unknown command type: ${commandName}`);
66
+ for (const [CommandClass, endpoint] of ENDPOINT_MAP) {
67
+ if (command instanceof CommandClass)
68
+ return endpoint;
76
69
  }
70
+ throw new Error(`Unknown command type: ${command.constructor?.name ?? typeof command}`);
77
71
  }
78
72
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export { TokenProvider } from './auth/TokenProvider';
2
- export type { TokenResponse, TokenProviderConfig } from './auth/TokenProvider';
3
1
  export { GeoPlacesClient } from './client/GeoPlacesClient';
4
2
  export * from '@aws-sdk/client-geo-places';
5
3
  export * from '@aws/amazon-location-utilities-datatypes';
@@ -7,7 +5,3 @@ export { GeoPlaces } from './adapters/GeoPlaces';
7
5
  export { createTransformRequest } from './maps/createTransformRequest';
8
6
  export { transformRequest } from './maps/Utils';
9
7
  export type { ClientConfig } from './types';
10
- export { getClientConfig } from './server/getClientConfig';
11
- export type { ServerAuthConfig } from './server/getClientConfig';
12
- export { LocationServiceConnector } from './server/LocationServiceConnector';
13
- export type { ConnectorConfig } from './server/LocationServiceConnector';
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- // Auth (Custom - only difference from AWS SDK)
2
- export { TokenProvider } from './auth/TokenProvider';
3
1
  // Client (Custom - uses our auth instead of AWS SigV4)
4
2
  export { GeoPlacesClient } from './client/GeoPlacesClient';
5
3
  // Re-export AWS SDK commands and types
@@ -11,6 +9,4 @@ export { GeoPlaces } from './adapters/GeoPlaces';
11
9
  // Maps utilities
12
10
  export { createTransformRequest } from './maps/createTransformRequest';
13
11
  export { transformRequest } from './maps/Utils';
14
- // Server-only utilities
15
- export { getClientConfig } from './server/getClientConfig';
16
- export { LocationServiceConnector } from './server/LocationServiceConnector';
12
+ // Server-only utilities are available via '@chaosity/location-client/server'
@@ -1,8 +1,2 @@
1
1
  import { ClientConfig } from "../types";
2
- export declare function transformRequest(url: string, config: ClientConfig): {
3
- url: string;
4
- headers: Record<string, string>;
5
- } | {
6
- url: string;
7
- headers?: undefined;
8
- };
2
+ export declare function transformRequest(url: string, config: ClientConfig): import("maplibre-gl").RequestParameters | undefined;
@@ -1,24 +1,5 @@
1
+ import { createTransformRequest } from "./createTransformRequest";
1
2
  export function transformRequest(url, config) {
2
- if (url.startsWith(config.apiUrl)) {
3
- const headers = {
4
- 'Authorization': `Bearer ${config.token}`,
5
- };
6
- if (url.includes('/tiles/')) {
7
- headers['Accept'] = 'application/x-protobuf';
8
- }
9
- else if (url.includes('/glyphs/')) {
10
- headers['Accept'] = 'application/x-protobuf';
11
- }
12
- else if (url.includes('/sprites/') && url.endsWith('.png')) {
13
- headers['Accept'] = 'image/png';
14
- }
15
- else if (url.includes('/sprites/') && url.endsWith('.json')) {
16
- headers['Accept'] = 'application/json';
17
- }
18
- else if (url.includes('/descriptor')) {
19
- headers['Accept'] = 'application/json';
20
- }
21
- return { url, headers };
22
- }
23
- return { url };
3
+ const token = config.getToken?.() ?? config.token;
4
+ return createTransformRequest(config.apiUrl, () => token)(url);
24
5
  }
@@ -1,3 +1,4 @@
1
+ import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
1
2
  import debug from 'debug';
2
3
  import { getClientConfig } from './getClientConfig';
3
4
  const log = debug('location-client:connector');
@@ -40,9 +41,14 @@ export class LocationServiceConnector {
40
41
  async send(command, options) {
41
42
  const config = await this.configPromise;
42
43
  // Get token - ConnectorConfig may have getToken, ServerClientConfig has static token
43
- const token = 'getToken' in config && config.getToken
44
- ? (await config.getToken()).token
45
- : config.token;
44
+ let token;
45
+ if ('getToken' in config && typeof config.getToken === 'function') {
46
+ const result = await config.getToken();
47
+ token = result ? (typeof result === 'string' ? result : result.token) : undefined;
48
+ }
49
+ else {
50
+ token = config.token;
51
+ }
46
52
  if (!token) {
47
53
  throw new Error('No token available');
48
54
  }
@@ -87,24 +93,20 @@ export class LocationServiceConnector {
87
93
  return result;
88
94
  }
89
95
  getEndpoint(command) {
90
- const commandName = command.constructor.name;
91
- switch (commandName) {
92
- case 'AutocompleteCommand':
93
- return '/address/autocomplete';
94
- case 'GeocodeCommand':
95
- return '/address/geocode';
96
- case 'GetPlaceCommand':
97
- return '/address/place';
98
- case 'ReverseGeocodeCommand':
99
- return '/address/search/reverse-geocode';
100
- case 'SearchNearbyCommand':
101
- return '/address/search/nearby';
102
- case 'SearchTextCommand':
103
- return '/address/search/text';
104
- case 'SuggestCommand':
105
- return '/address/suggestion';
106
- default:
107
- throw new Error(`Unknown command type: ${commandName}`);
108
- }
96
+ if (command instanceof AutocompleteCommand)
97
+ return '/address/autocomplete';
98
+ if (command instanceof GeocodeCommand)
99
+ return '/address/geocode';
100
+ if (command instanceof GetPlaceCommand)
101
+ return '/address/place';
102
+ if (command instanceof ReverseGeocodeCommand)
103
+ return '/address/search/reverse-geocode';
104
+ if (command instanceof SearchNearbyCommand)
105
+ return '/address/search/nearby';
106
+ if (command instanceof SearchTextCommand)
107
+ return '/address/search/text';
108
+ if (command instanceof SuggestCommand)
109
+ return '/address/suggestion';
110
+ throw new Error(`Unknown command type: ${command.constructor?.name ?? typeof command}`);
109
111
  }
110
112
  }
@@ -5,6 +5,7 @@ export interface ServerAuthConfig {
5
5
  clientSecret?: string;
6
6
  }
7
7
  export interface ServerClientConfig extends ClientConfig {
8
+ expiresAt?: number;
8
9
  }
9
10
  /**
10
11
  * Get client configuration with OAuth2 authentication.
@@ -1,6 +1,22 @@
1
1
  import debug from 'debug';
2
2
  import { TokenProvider } from '../auth/TokenProvider';
3
3
  const log = debug('location-client:clientConfig');
4
+ // Singleton instance to prevent race conditions
5
+ let tokenProviderInstance = null;
6
+ let currentConfig = null;
7
+ function getTokenProvider(apiUrl, clientId, clientSecret) {
8
+ const configKey = `${apiUrl}:${clientId}`;
9
+ // Reuse existing instance if config matches
10
+ if (tokenProviderInstance && currentConfig === configKey) {
11
+ log('[getTokenProvider] Reusing existing TokenProvider instance');
12
+ return tokenProviderInstance;
13
+ }
14
+ // Create new instance if config changed
15
+ log('[getTokenProvider] Creating new TokenProvider instance');
16
+ tokenProviderInstance = new TokenProvider({ apiUrl, clientId, clientSecret });
17
+ currentConfig = configKey;
18
+ return tokenProviderInstance;
19
+ }
4
20
  /**
5
21
  * Get client configuration with OAuth2 authentication.
6
22
  *
@@ -50,8 +66,8 @@ export async function getClientConfig(config = {}) {
50
66
  throw new Error('Missing required configuration. Set environment variables: ' +
51
67
  'LOCATION_API_URL, LOCATION_CLIENT_ID, LOCATION_CLIENT_SECRET');
52
68
  }
53
- log('[getClientConfig] Creating TokenProvider');
54
- const provider = new TokenProvider({ apiUrl, clientId, clientSecret });
69
+ log('[getClientConfig] Getting TokenProvider instance');
70
+ const provider = getTokenProvider(apiUrl, clientId, clientSecret);
55
71
  log('[getClientConfig] Fetching token');
56
72
  const result = await provider.getToken();
57
73
  if (!result.success || !result.token) {
@@ -61,6 +77,7 @@ export async function getClientConfig(config = {}) {
61
77
  log('[getClientConfig] Token fetched successfully, length:', result.token.length);
62
78
  return {
63
79
  apiUrl,
64
- token: result.token
80
+ token: result.token,
81
+ expiresAt: result.expiresAt
65
82
  };
66
83
  }
@@ -2,3 +2,5 @@ export { getClientConfig } from './getClientConfig';
2
2
  export type { ServerAuthConfig, ServerClientConfig } from './getClientConfig';
3
3
  export { LocationServiceConnector } from './LocationServiceConnector';
4
4
  export type { ConnectorConfig, SendOptions } from './LocationServiceConnector';
5
+ export { TokenProvider } from '../auth/TokenProvider';
6
+ export type { TokenProviderConfig, TokenResponse } from '../auth/TokenProvider';
@@ -1,2 +1,3 @@
1
1
  export { getClientConfig } from './getClientConfig';
2
2
  export { LocationServiceConnector } from './LocationServiceConnector';
3
+ export { TokenProvider } from '../auth/TokenProvider';
@@ -1,4 +1,7 @@
1
1
  export interface ClientConfig {
2
2
  apiUrl: string;
3
3
  token: string;
4
+ /** Optional callback to get the current token dynamically. When provided,
5
+ * called on every request so token updates are reflected without recreating the client. */
6
+ getToken?: () => string | undefined;
4
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaosity/location-client",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,8 @@
18
18
  "scripts": {
19
19
  "build": "tsc",
20
20
  "dev": "tsc --watch",
21
- "test": "jest",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
22
23
  "prepublishOnly": "npm run build"
23
24
  },
24
25
  "keywords": [
@@ -43,9 +44,7 @@
43
44
  "@aws-sdk/client-geo-places": "^3.0.0",
44
45
  "@aws/amazon-location-utilities-datatypes": "^1.0.0",
45
46
  "@maplibre/maplibre-gl-geocoder": "^1.9.4",
46
- "client-oauth2": "^4.3.3",
47
- "debug": "^4.4.3",
48
- "jose": "^5.0.0"
47
+ "debug": "^4.4.3"
49
48
  },
50
49
  "peerDependencies": {
51
50
  "maplibre-gl": "^5.0.0"
@@ -59,7 +58,11 @@
59
58
  "@types/debug": "^4.1.12",
60
59
  "@types/node": "^25.0.3",
61
60
  "maplibre-gl": "^5.0.0",
62
- "typescript": "^5.0.0"
61
+ "typescript": "^5.0.0",
62
+ "vitest": "^3.0.0"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
63
66
  },
64
67
  "files": [
65
68
  "dist",