@chaosity/location-client-react 0.2.0 → 0.3.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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientConfig } from '@chaosity/location-client';
|
|
2
|
+
import { type AppConfigClaims } from '@chaosity/location-client';
|
|
2
3
|
import type { ReactNode } from 'react';
|
|
3
4
|
/**
|
|
4
5
|
* Per-request transport options.
|
|
@@ -26,6 +27,24 @@ export interface LocationClient {
|
|
|
26
27
|
serviceId: string;
|
|
27
28
|
};
|
|
28
29
|
send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
|
|
30
|
+
/**
|
|
31
|
+
* This application's own configuration, read from the access token —
|
|
32
|
+
* bias precision, and the countries it is scoped to (api#65).
|
|
33
|
+
*
|
|
34
|
+
* Here so a React app can SHOW its own settings: populate a country
|
|
35
|
+
* selector with the markets it serves, label a settings screen. Being a
|
|
36
|
+
* few minutes stale is cosmetic for that.
|
|
37
|
+
*
|
|
38
|
+
* It is not an entitlement check, and the `countries` value must not be
|
|
39
|
+
* used to shape requests. The token is a snapshot; the API reads the scope
|
|
40
|
+
* fresh from the application row on every call. Injecting a stale scope
|
|
41
|
+
* turns a request that would have succeeded into a 400. See
|
|
42
|
+
* `AppConfigClaims` in @chaosity/location-client for the measurement.
|
|
43
|
+
*
|
|
44
|
+
* Returns `{}` when the token carries no application config, which is the
|
|
45
|
+
* case until one is set in the portal.
|
|
46
|
+
*/
|
|
47
|
+
getAppConfig(): AppConfigClaims;
|
|
29
48
|
}
|
|
30
49
|
interface LocationClientContextValue {
|
|
31
50
|
client: LocationClient | null;
|
|
@@ -38,9 +57,7 @@ export interface LocationClientProviderProps {
|
|
|
38
57
|
getConfig: () => Promise<ClientConfig & {
|
|
39
58
|
expiresAt?: number;
|
|
40
59
|
}>;
|
|
41
|
-
/** Seconds before expiry to proactively refresh (default: 60) */
|
|
42
|
-
refreshBuffer?: number;
|
|
43
60
|
}
|
|
44
|
-
export declare function LocationClientProvider({ children, getConfig,
|
|
61
|
+
export declare function LocationClientProvider({ children, getConfig, }: LocationClientProviderProps): import("react").JSX.Element;
|
|
45
62
|
export declare function useLocationClient(): LocationClientContextValue;
|
|
46
63
|
export {};
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { GeoPlacesClient } from '@chaosity/location-client';
|
|
3
|
+
import { GeoPlacesClient, TOKEN_REFRESH_BUFFER_SECONDS, readTokenExpiry, } from '@chaosity/location-client';
|
|
4
4
|
import debug from 'debug';
|
|
5
5
|
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
6
6
|
const log = debug('location-client-react:provider');
|
|
7
7
|
const LocationClientContext = createContext(undefined);
|
|
8
8
|
const DEFAULT_LIFETIME_MS = 900000;
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* When this token needs replacing.
|
|
11
|
+
*
|
|
12
|
+
* The `exp` claim first — it is the only value that cannot disagree with what
|
|
13
|
+
* the API will accept, and the server-side TokenProvider reads the same one.
|
|
14
|
+
* `expiresAt` is whatever `getConfig` chose to report, and the final fallback
|
|
15
|
+
* is a guess used only when the token cannot be parsed at all.
|
|
16
|
+
*/
|
|
17
|
+
function expiryOf(cfg) {
|
|
18
|
+
return (readTokenExpiry(cfg.token) ??
|
|
19
|
+
cfg.expiresAt ??
|
|
20
|
+
Date.now() + DEFAULT_LIFETIME_MS);
|
|
21
|
+
}
|
|
22
|
+
export function LocationClientProvider({ children, getConfig, }) {
|
|
10
23
|
const [client, setClient] = useState(null);
|
|
11
24
|
const [loading, setLoading] = useState(true);
|
|
12
25
|
const [error, setError] = useState(null);
|
|
@@ -22,8 +35,8 @@ export function LocationClientProvider({ children, getConfig, refreshBuffer = 60
|
|
|
22
35
|
const isTokenExpired = useCallback(() => {
|
|
23
36
|
if (!expiresAtRef.current)
|
|
24
37
|
return true;
|
|
25
|
-
return Date.now() >= expiresAtRef.current -
|
|
26
|
-
}, [
|
|
38
|
+
return (Date.now() >= expiresAtRef.current - TOKEN_REFRESH_BUFFER_SECONDS * 1000);
|
|
39
|
+
}, []);
|
|
27
40
|
/**
|
|
28
41
|
* Refresh once, however many callers ask at the same moment.
|
|
29
42
|
*
|
|
@@ -38,7 +51,7 @@ export function LocationClientProvider({ children, getConfig, refreshBuffer = 60
|
|
|
38
51
|
refreshPromiseRef.current = (async () => {
|
|
39
52
|
const cfg = await getConfigRef.current();
|
|
40
53
|
tokenRef.current = cfg.token;
|
|
41
|
-
expiresAtRef.current = cfg
|
|
54
|
+
expiresAtRef.current = expiryOf(cfg);
|
|
42
55
|
log('Token refreshed (expires in %ds)', Math.floor((expiresAtRef.current - Date.now()) / 1000));
|
|
43
56
|
if (mountedRef.current)
|
|
44
57
|
setError(null);
|
|
@@ -73,14 +86,14 @@ export function LocationClientProvider({ children, getConfig, refreshBuffer = 60
|
|
|
73
86
|
clearTimeout(timerRef.current);
|
|
74
87
|
if (!expiresAtRef.current)
|
|
75
88
|
return;
|
|
76
|
-
const delay = Math.max(0, expiresAtRef.current -
|
|
89
|
+
const delay = Math.max(0, expiresAtRef.current - TOKEN_REFRESH_BUFFER_SECONDS * 1000 - Date.now());
|
|
77
90
|
log('Next refresh in %ds', Math.floor(delay / 1000));
|
|
78
91
|
timerRef.current = setTimeout(() => {
|
|
79
92
|
// Errors are already surfaced onto state by refreshToken; swallow here so
|
|
80
93
|
// a failed background refresh cannot become an unhandled rejection.
|
|
81
94
|
void refreshToken().catch(() => { });
|
|
82
95
|
}, delay);
|
|
83
|
-
}, [
|
|
96
|
+
}, [refreshToken]);
|
|
84
97
|
// refreshToken and scheduleRefresh reference each other; a ref breaks the cycle
|
|
85
98
|
// without recreating either callback on every render.
|
|
86
99
|
const scheduleRefreshRef = useRef(() => { });
|
|
@@ -141,7 +154,7 @@ export function LocationClientProvider({ children, getConfig, refreshBuffer = 60
|
|
|
141
154
|
if (!mountedRef.current)
|
|
142
155
|
return;
|
|
143
156
|
tokenRef.current = cfg.token;
|
|
144
|
-
expiresAtRef.current = cfg
|
|
157
|
+
expiresAtRef.current = expiryOf(cfg);
|
|
145
158
|
const baseClient = new GeoPlacesClient({
|
|
146
159
|
apiUrl: cfg.apiUrl,
|
|
147
160
|
token: cfg.token,
|
|
@@ -157,6 +170,13 @@ export function LocationClientProvider({ children, getConfig, refreshBuffer = 60
|
|
|
157
170
|
await ensureValidTokenRef.current();
|
|
158
171
|
return baseClient.send(command, options);
|
|
159
172
|
},
|
|
173
|
+
// Reads whatever token the client currently holds. Deliberately not
|
|
174
|
+
// awaiting a refresh: this is display data, callers expect it to be
|
|
175
|
+
// synchronous, and a token that is minutes from expiry carries the
|
|
176
|
+
// same application config as its replacement will.
|
|
177
|
+
getAppConfig() {
|
|
178
|
+
return baseClient.getAppConfig();
|
|
179
|
+
},
|
|
160
180
|
};
|
|
161
181
|
setClient(refreshing);
|
|
162
182
|
scheduleRefreshRef.current();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaosity/location-client-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "React bindings for Chaosity Location Service client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "https://github.com/chaosity-io/location-service-client-react/issues"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@chaosity/location-client": "^0.
|
|
37
|
+
"@chaosity/location-client": "^0.3.0",
|
|
38
38
|
"debug": "^4.4.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@types/react": "^19.0.0",
|
|
55
55
|
"@vitest/coverage-v8": "^3.0.0",
|
|
56
56
|
"eslint": "^10.1.0",
|
|
57
|
-
"happy-dom": "^
|
|
57
|
+
"happy-dom": "^20.11.6",
|
|
58
58
|
"husky": "^9.0.0",
|
|
59
59
|
"maplibre-gl": "^5.0.0",
|
|
60
60
|
"prettier": "^3.8.1",
|