@adventurelabs/scout-core 2.0.8 → 2.0.10
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
CHANGED
|
@@ -35,6 +35,8 @@ barrel.
|
|
|
35
35
|
`ScoutRefreshProvider` accepts an existing browser Supabase client and otherwise
|
|
36
36
|
creates one stable client. Its automatic refresh uses a fresh IndexedDB cache
|
|
37
37
|
without another request; manual and reconnect refreshes still query the API.
|
|
38
|
+
Returning to a visible tab revalidates herd data and refreshes JWT mints that
|
|
39
|
+
are near expiry, covering browsers that suspend background timers.
|
|
38
40
|
Offline PWAs can pass a persisted `offlineUserId` to restore that user's herd
|
|
39
41
|
modules and JWT mints before online authentication is revalidated. Cached mints
|
|
40
42
|
remain available when expired; pass `false` as the fourth argument to
|
|
@@ -60,6 +60,7 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
60
60
|
const { validatedUserId, onUserValidated } = identityOptions;
|
|
61
61
|
const dispatch = useAppDispatch();
|
|
62
62
|
const refreshIdRef = useRef(0);
|
|
63
|
+
const refreshInProgressIdRef = useRef(null);
|
|
63
64
|
const activeUserIdRef = useRef(null);
|
|
64
65
|
const validatedUserIdRef = useRef(null);
|
|
65
66
|
const providedValidatedUserIdRef = useRef(validatedUserId);
|
|
@@ -91,7 +92,10 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
91
92
|
}, [dispatch]);
|
|
92
93
|
const cacheIdentityRef = useRef({ supabase, offlineUserId });
|
|
93
94
|
const handleRefresh = useCallback(async ({ force = true } = {}) => {
|
|
95
|
+
if (refreshInProgressIdRef.current !== null && !force)
|
|
96
|
+
return;
|
|
94
97
|
const refreshId = ++refreshIdRef.current;
|
|
98
|
+
refreshInProgressIdRef.current = refreshId;
|
|
95
99
|
const isCurrent = () => refreshIdRef.current === refreshId;
|
|
96
100
|
const startTime = Date.now();
|
|
97
101
|
const isOffline = typeof navigator === "undefined" || !navigator.onLine;
|
|
@@ -268,6 +272,9 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
268
272
|
}
|
|
269
273
|
}
|
|
270
274
|
finally {
|
|
275
|
+
if (refreshInProgressIdRef.current === refreshId) {
|
|
276
|
+
refreshInProgressIdRef.current = null;
|
|
277
|
+
}
|
|
271
278
|
if (isCurrent()) {
|
|
272
279
|
lastQueryAtRef.current = Date.now();
|
|
273
280
|
}
|
|
@@ -355,14 +362,25 @@ export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
|
355
362
|
useEffect(() => {
|
|
356
363
|
if (!autoRefresh || typeof window === "undefined")
|
|
357
364
|
return;
|
|
358
|
-
const
|
|
365
|
+
const revalidate = () => {
|
|
366
|
+
if (!navigator.onLine)
|
|
367
|
+
return;
|
|
359
368
|
const now = Date.now();
|
|
360
369
|
if (now - lastQueryAtRef.current >= onlineRefetchMinIntervalMs) {
|
|
361
370
|
void handleRefresh({ force: true });
|
|
362
371
|
}
|
|
363
372
|
};
|
|
364
|
-
|
|
365
|
-
|
|
373
|
+
const handleVisibilityChange = () => {
|
|
374
|
+
if (document.visibilityState === "visible") {
|
|
375
|
+
revalidate();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
window.addEventListener("online", revalidate);
|
|
379
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
380
|
+
return () => {
|
|
381
|
+
window.removeEventListener("online", revalidate);
|
|
382
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
383
|
+
};
|
|
366
384
|
}, [autoRefresh, handleRefresh, onlineRefetchMinIntervalMs]);
|
|
367
385
|
const clearCache = useCallback(async () => {
|
|
368
386
|
try {
|
|
@@ -82,7 +82,9 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
82
82
|
const [mint, setMint] = useState(null);
|
|
83
83
|
const [inFlight, setInFlight] = useState(false);
|
|
84
84
|
const [error, setError] = useState(null);
|
|
85
|
+
const [offline, setOffline] = useState(isBrowserOffline);
|
|
85
86
|
const refreshIdRef = useRef(0);
|
|
87
|
+
const refreshInProgressRef = useRef(false);
|
|
86
88
|
const authUserIdRef = useRef(null);
|
|
87
89
|
const cacheHydrationIdRef = useRef(0);
|
|
88
90
|
const signedOutRef = useRef(false);
|
|
@@ -101,15 +103,16 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
101
103
|
console.error("[ScoutRefreshProvider] User validation callback failed:", validationError);
|
|
102
104
|
}
|
|
103
105
|
}, [onUserValidated]);
|
|
104
|
-
const status = useMemo(() => derive_jwt_mint_status(enabled, mint, inFlight, error), [enabled, mint, inFlight, error]);
|
|
106
|
+
const status = useMemo(() => derive_jwt_mint_status(enabled, mint, inFlight, error, offline), [enabled, mint, inFlight, error, offline]);
|
|
105
107
|
const refreshMint = useCallback(async (force = true) => {
|
|
106
108
|
if (!enabled || isBrowserOffline())
|
|
107
109
|
return;
|
|
110
|
+
if (refreshInProgressRef.current && !force)
|
|
111
|
+
return;
|
|
112
|
+
refreshInProgressRef.current = true;
|
|
108
113
|
cacheHydrationIdRef.current++;
|
|
109
114
|
const refreshId = ++refreshIdRef.current;
|
|
110
115
|
const isCurrent = () => refreshIdRef.current === refreshId;
|
|
111
|
-
setInFlight(true);
|
|
112
|
-
setError(null);
|
|
113
116
|
try {
|
|
114
117
|
const { data, error: authError } = await supabase.auth.getUser();
|
|
115
118
|
if (authError) {
|
|
@@ -142,6 +145,8 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
142
145
|
!shouldRefreshMint(currentMint, ttlSec, refreshBeforeExpirySec)) {
|
|
143
146
|
return;
|
|
144
147
|
}
|
|
148
|
+
setInFlight(true);
|
|
149
|
+
setError(null);
|
|
145
150
|
const mintResponse = await mintToken(supabase);
|
|
146
151
|
if (!isCurrent() || authUserIdRef.current !== userId)
|
|
147
152
|
return;
|
|
@@ -176,6 +181,7 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
176
181
|
}
|
|
177
182
|
finally {
|
|
178
183
|
if (isCurrent()) {
|
|
184
|
+
refreshInProgressRef.current = false;
|
|
179
185
|
setInFlight(false);
|
|
180
186
|
}
|
|
181
187
|
}
|
|
@@ -201,10 +207,12 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
201
207
|
authUserIdRef.current = null;
|
|
202
208
|
cacheHydrationIdRef.current++;
|
|
203
209
|
refreshIdRef.current++;
|
|
210
|
+
refreshInProgressRef.current = false;
|
|
204
211
|
updateMint(null);
|
|
205
212
|
setInFlight(false);
|
|
206
213
|
}
|
|
207
214
|
if (!enabled) {
|
|
215
|
+
refreshInProgressRef.current = false;
|
|
208
216
|
updateMint(null);
|
|
209
217
|
setError(null);
|
|
210
218
|
setInFlight(false);
|
|
@@ -219,6 +227,7 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
219
227
|
authUserIdRef.current = null;
|
|
220
228
|
cacheHydrationIdRef.current++;
|
|
221
229
|
refreshIdRef.current++;
|
|
230
|
+
refreshInProgressRef.current = false;
|
|
222
231
|
updateMint(null);
|
|
223
232
|
setError(null);
|
|
224
233
|
setInFlight(false);
|
|
@@ -287,40 +296,78 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
287
296
|
}
|
|
288
297
|
};
|
|
289
298
|
void initializeMint();
|
|
299
|
+
const refreshCurrentMintIfDue = () => {
|
|
300
|
+
const currentMint = mintRef.current;
|
|
301
|
+
if (!currentMint ||
|
|
302
|
+
shouldRefreshMint(currentMint, ttlSec, refreshBeforeExpirySec)) {
|
|
303
|
+
void refreshMint(false);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
290
306
|
const unsubscribeAuth = subscribeToSupabaseAuth(supabase, (event, session) => {
|
|
291
307
|
if (event === "SIGNED_OUT") {
|
|
292
308
|
signedOutRef.current = true;
|
|
293
309
|
authUserIdRef.current = null;
|
|
294
310
|
cacheHydrationIdRef.current++;
|
|
295
311
|
refreshIdRef.current++;
|
|
312
|
+
refreshInProgressRef.current = false;
|
|
296
313
|
updateMint(null);
|
|
297
314
|
setError(null);
|
|
298
315
|
setInFlight(false);
|
|
299
316
|
return;
|
|
300
317
|
}
|
|
301
318
|
if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
|
|
319
|
+
const wasSignedOut = signedOutRef.current;
|
|
302
320
|
signedOutRef.current = false;
|
|
303
|
-
refreshIdRef.current++;
|
|
304
|
-
setInFlight(false);
|
|
305
321
|
const userId = session?.user?.id;
|
|
306
|
-
if (userId) {
|
|
322
|
+
if (!userId) {
|
|
323
|
+
refreshCurrentMintIfDue();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const knownUserId = authUserIdRef.current;
|
|
327
|
+
const userChanged = wasSignedOut ||
|
|
328
|
+
(knownUserId !== null && knownUserId !== userId);
|
|
329
|
+
if (userChanged) {
|
|
330
|
+
cacheHydrationIdRef.current++;
|
|
331
|
+
refreshIdRef.current++;
|
|
332
|
+
refreshInProgressRef.current = false;
|
|
307
333
|
void initializeMint(userId, true);
|
|
308
334
|
}
|
|
335
|
+
else if (knownUserId === null || !mintRef.current) {
|
|
336
|
+
void initializeMint(userId);
|
|
337
|
+
}
|
|
309
338
|
else {
|
|
310
|
-
|
|
339
|
+
refreshCurrentMintIfDue();
|
|
311
340
|
}
|
|
312
341
|
}
|
|
313
342
|
});
|
|
314
343
|
const handleOnline = () => {
|
|
344
|
+
setOffline(false);
|
|
315
345
|
void refreshMint(false);
|
|
316
346
|
};
|
|
347
|
+
const handleOffline = () => {
|
|
348
|
+
setOffline(true);
|
|
349
|
+
};
|
|
350
|
+
const handleVisibilityChange = () => {
|
|
351
|
+
if (document.visibilityState !== "visible")
|
|
352
|
+
return;
|
|
353
|
+
const currentlyOffline = isBrowserOffline();
|
|
354
|
+
setOffline(currentlyOffline);
|
|
355
|
+
if (currentlyOffline)
|
|
356
|
+
return;
|
|
357
|
+
refreshCurrentMintIfDue();
|
|
358
|
+
};
|
|
317
359
|
window.addEventListener("online", handleOnline);
|
|
360
|
+
window.addEventListener("offline", handleOffline);
|
|
361
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
318
362
|
return () => {
|
|
319
363
|
cancelled = true;
|
|
320
364
|
cacheHydrationIdRef.current++;
|
|
321
365
|
refreshIdRef.current++;
|
|
366
|
+
refreshInProgressRef.current = false;
|
|
322
367
|
unsubscribeAuth();
|
|
323
368
|
window.removeEventListener("online", handleOnline);
|
|
369
|
+
window.removeEventListener("offline", handleOffline);
|
|
370
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
324
371
|
};
|
|
325
372
|
}, [
|
|
326
373
|
enabled,
|
|
@@ -328,6 +375,8 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
328
375
|
offlineUserId,
|
|
329
376
|
validatedUserId,
|
|
330
377
|
cacheKey,
|
|
378
|
+
ttlSec,
|
|
379
|
+
refreshBeforeExpirySec,
|
|
331
380
|
refreshMint,
|
|
332
381
|
updateMint,
|
|
333
382
|
]);
|
|
@@ -340,7 +389,7 @@ function useJwtMintLifecycle({ enabled, supabase, identity, cacheKey, ttlSec, re
|
|
|
340
389
|
const refreshAtSec = Math.max(expSec - refreshBeforeExpirySec, mint.iat + 1);
|
|
341
390
|
const delayMs = Math.max((refreshAtSec - nowSec) * 1000, 1000);
|
|
342
391
|
const timer = setTimeout(() => {
|
|
343
|
-
void refreshMint();
|
|
392
|
+
void refreshMint(false);
|
|
344
393
|
}, delayMs);
|
|
345
394
|
return () => clearTimeout(timer);
|
|
346
395
|
}, [enabled, mint, ttlSec, refreshBeforeExpirySec, refreshMint]);
|
package/dist/types/jwt_mint.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ export declare enum EnumJwtMintStatus {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function derive_jwt_mint_status(enabled: boolean, mint: {
|
|
9
9
|
token: string;
|
|
10
|
-
|
|
10
|
+
exp?: number;
|
|
11
|
+
} | null | undefined, inFlight: boolean, error: string | null, allowExpired?: boolean): EnumJwtMintStatus;
|
package/dist/types/jwt_mint.js
CHANGED
|
@@ -6,17 +6,18 @@ export var EnumJwtMintStatus;
|
|
|
6
6
|
EnumJwtMintStatus["READY"] = "ready";
|
|
7
7
|
EnumJwtMintStatus["ERROR"] = "error";
|
|
8
8
|
})(EnumJwtMintStatus || (EnumJwtMintStatus = {}));
|
|
9
|
-
export function derive_jwt_mint_status(enabled, mint, inFlight, error) {
|
|
9
|
+
export function derive_jwt_mint_status(enabled, mint, inFlight, error, allowExpired = false) {
|
|
10
10
|
if (!enabled) {
|
|
11
11
|
return EnumJwtMintStatus.IDLE;
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const expired = mint?.exp != null && mint.exp <= Math.floor(Date.now() / 1000);
|
|
14
|
+
const hasUsableMint = Boolean(mint?.token) && (allowExpired || !expired);
|
|
15
|
+
if (inFlight) {
|
|
16
|
+
return hasUsableMint
|
|
17
|
+
? EnumJwtMintStatus.REFRESHING
|
|
18
|
+
: EnumJwtMintStatus.LOADING;
|
|
15
19
|
}
|
|
16
|
-
if (
|
|
17
|
-
return EnumJwtMintStatus.REFRESHING;
|
|
18
|
-
}
|
|
19
|
-
if (mint?.token) {
|
|
20
|
+
if (hasUsableMint) {
|
|
20
21
|
return EnumJwtMintStatus.READY;
|
|
21
22
|
}
|
|
22
23
|
if (error) {
|