@adventurelabs/scout-core 2.0.3 → 2.0.4
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 +5 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +2 -2
- package/dist/helpers/compliance.d.ts +17 -1
- package/dist/helpers/compliance.js +48 -0
- package/dist/helpers/compliance.queries.d.ts +17 -1
- package/dist/helpers/compliance.queries.js +76 -0
- package/dist/helpers/operating_contexts.d.ts +4 -1
- package/dist/helpers/operating_contexts.js +9 -0
- package/dist/helpers/operating_contexts.queries.d.ts +4 -1
- package/dist/helpers/operating_contexts.queries.js +9 -0
- package/dist/hooks/useScoutRealtimeOperatingContexts.d.ts +5 -5
- package/dist/hooks/useScoutRealtimeOperatingContexts.js +9 -0
- package/dist/hooks/useScoutRefresh.d.ts +7 -1
- package/dist/hooks/useScoutRefresh.js +149 -52
- package/dist/providers/ScoutRefreshProvider.js +221 -51
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +2 -2
- package/dist/types/db.d.ts +36 -1
- package/dist/types/jwt_mint.js +3 -3
- package/dist/types/supabase.d.ts +361 -0
- package/package.json +1 -1
|
@@ -15,19 +15,15 @@ function dispatchInTransition(dispatch, action) {
|
|
|
15
15
|
dispatch(action);
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
async function loadAuthenticatedUser(supabase
|
|
18
|
+
async function loadAuthenticatedUser(supabase) {
|
|
19
19
|
const startTime = Date.now();
|
|
20
|
-
if (offline) {
|
|
21
|
-
const { data: { session }, } = await supabase.auth.getSession();
|
|
22
|
-
if (!session?.user) {
|
|
23
|
-
throw new Error("No authenticated user");
|
|
24
|
-
}
|
|
25
|
-
return { user: session.user, duration: Date.now() - startTime };
|
|
26
|
-
}
|
|
27
20
|
let lastError = new Error("Unable to load authenticated user");
|
|
28
21
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
29
22
|
try {
|
|
30
|
-
const { data } = await supabase.auth.getUser();
|
|
23
|
+
const { data, error } = await supabase.auth.getUser();
|
|
24
|
+
if (error) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
31
27
|
if (!data.user) {
|
|
32
28
|
throw new Error("Invalid user response");
|
|
33
29
|
}
|
|
@@ -59,16 +55,30 @@ async function recoverScoutCache(error, operation, userId, retry) {
|
|
|
59
55
|
*
|
|
60
56
|
* Automatic refreshes hydrate from cache before revalidating online.
|
|
61
57
|
*/
|
|
62
|
-
export function useScoutRefresh(options = {}) {
|
|
63
|
-
const { supabase: supabaseOption, autoRefresh = true, onRefreshComplete, cacheFirst = true, cacheTtlMs = 24 * 60 * 60 * 1000, onlineRefetchMinIntervalMs = 15 * 1000, } = options;
|
|
58
|
+
export function useScoutRefresh(options = {}, identityOptions = {}) {
|
|
59
|
+
const { supabase: supabaseOption, autoRefresh = true, onRefreshComplete, cacheFirst = true, cacheTtlMs = 24 * 60 * 60 * 1000, onlineRefetchMinIntervalMs = 15 * 1000, offlineUserId, } = options;
|
|
60
|
+
const { validatedUserId, onUserValidated } = identityOptions;
|
|
64
61
|
const dispatch = useAppDispatch();
|
|
65
62
|
const refreshIdRef = useRef(0);
|
|
66
63
|
const activeUserIdRef = useRef(null);
|
|
64
|
+
const validatedUserIdRef = useRef(null);
|
|
65
|
+
const providedValidatedUserIdRef = useRef(validatedUserId);
|
|
66
|
+
providedValidatedUserIdRef.current = validatedUserId;
|
|
67
67
|
const lastQueryAtRef = useRef(0);
|
|
68
68
|
const onRefreshCompleteRef = useRef(onRefreshComplete);
|
|
69
|
+
const onUserValidatedRef = useRef(onUserValidated);
|
|
69
70
|
useEffect(() => {
|
|
70
71
|
onRefreshCompleteRef.current = onRefreshComplete;
|
|
71
|
-
|
|
72
|
+
onUserValidatedRef.current = onUserValidated;
|
|
73
|
+
}, [onRefreshComplete, onUserValidated]);
|
|
74
|
+
const notifyUserValidated = useCallback((userId) => {
|
|
75
|
+
try {
|
|
76
|
+
onUserValidatedRef.current?.(userId);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error("[useScoutRefresh] User validation callback failed:", error);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
72
82
|
const supabase = useMemo(() => {
|
|
73
83
|
if (supabaseOption) {
|
|
74
84
|
return supabaseOption;
|
|
@@ -79,12 +89,14 @@ export function useScoutRefresh(options = {}) {
|
|
|
79
89
|
dispatch(resetScoutState());
|
|
80
90
|
dispatch(scoutApi.util.resetApiState());
|
|
81
91
|
}, [dispatch]);
|
|
92
|
+
const cacheIdentityRef = useRef({ supabase, offlineUserId });
|
|
82
93
|
const handleRefresh = useCallback(async ({ force = true } = {}) => {
|
|
83
94
|
const refreshId = ++refreshIdRef.current;
|
|
84
95
|
const isCurrent = () => refreshIdRef.current === refreshId;
|
|
85
96
|
const startTime = Date.now();
|
|
86
97
|
const isOffline = typeof navigator === "undefined" || !navigator.onLine;
|
|
87
98
|
let cachedHerdModules = null;
|
|
99
|
+
let cacheReady = false;
|
|
88
100
|
const complete = (loadingState) => {
|
|
89
101
|
if (!isCurrent())
|
|
90
102
|
return;
|
|
@@ -93,57 +105,84 @@ export function useScoutRefresh(options = {}) {
|
|
|
93
105
|
dispatch(setStatus(EnumScoutStateStatus.DONE_LOADING));
|
|
94
106
|
onRefreshCompleteRef.current?.();
|
|
95
107
|
};
|
|
96
|
-
|
|
97
|
-
dispatch(setStatus(EnumScoutStateStatus.LOADING));
|
|
98
|
-
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.LOADING));
|
|
99
|
-
const { user, duration } = await loadAuthenticatedUser(supabase, isOffline);
|
|
100
|
-
if (!isCurrent())
|
|
101
|
-
return;
|
|
102
|
-
const userId = user.id;
|
|
108
|
+
const activateUser = (userId) => {
|
|
103
109
|
if (activeUserIdRef.current &&
|
|
104
110
|
activeUserIdRef.current !== userId) {
|
|
105
111
|
resetClientState();
|
|
112
|
+
cachedHerdModules = null;
|
|
106
113
|
}
|
|
107
114
|
activeUserIdRef.current = userId;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
};
|
|
116
|
+
const hydrateCache = async (userId) => {
|
|
117
|
+
activateUser(userId);
|
|
118
|
+
cacheReady = false;
|
|
111
119
|
try {
|
|
112
120
|
await scoutCache.setScope(userId);
|
|
113
121
|
cacheReady = true;
|
|
114
122
|
}
|
|
115
123
|
catch (cacheError) {
|
|
116
124
|
console.warn("[useScoutRefresh] Cache unavailable:", cacheError);
|
|
125
|
+
return;
|
|
117
126
|
}
|
|
118
|
-
if (cacheFirst
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
dispatch(setDataSourceInfo({
|
|
130
|
-
source: EnumDataSource.CACHE,
|
|
131
|
-
timestamp: Date.now(),
|
|
132
|
-
cacheAge: cacheResult.age,
|
|
133
|
-
isStale: cacheResult.isStale,
|
|
134
|
-
}));
|
|
135
|
-
dispatchInTransition(dispatch, setHerdModules({
|
|
136
|
-
modules: cachedHerdModules,
|
|
137
|
-
}));
|
|
138
|
-
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (cacheError) {
|
|
142
|
-
console.warn("[useScoutRefresh] Cache load failed:", cacheError);
|
|
143
|
-
await recoverScoutCache(cacheError, "cache load", userId);
|
|
127
|
+
if (!cacheFirst || (force && !isOffline)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const cacheStartTime = Date.now();
|
|
131
|
+
try {
|
|
132
|
+
const cacheResult = await scoutCache.getHerdModules(userId);
|
|
133
|
+
if (!isCurrent() || activeUserIdRef.current !== userId)
|
|
134
|
+
return;
|
|
135
|
+
dispatch(setCacheLoadDuration(Date.now() - cacheStartTime));
|
|
136
|
+
if (cacheResult.data === null) {
|
|
137
|
+
return;
|
|
144
138
|
}
|
|
139
|
+
cachedHerdModules = cacheResult.data;
|
|
140
|
+
dispatch(setDataSource(EnumDataSource.CACHE));
|
|
141
|
+
dispatch(setDataSourceInfo({
|
|
142
|
+
source: EnumDataSource.CACHE,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
cacheAge: cacheResult.age,
|
|
145
|
+
isStale: cacheResult.isStale,
|
|
146
|
+
}));
|
|
147
|
+
dispatchInTransition(dispatch, setHerdModules({ modules: cachedHerdModules }));
|
|
148
|
+
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
|
|
149
|
+
}
|
|
150
|
+
catch (cacheError) {
|
|
151
|
+
console.warn("[useScoutRefresh] Cache load failed:", cacheError);
|
|
152
|
+
await recoverScoutCache(cacheError, "cache load", userId);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
dispatch(setStatus(EnumScoutStateStatus.LOADING));
|
|
157
|
+
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.LOADING));
|
|
158
|
+
const providedValidatedUserId = providedValidatedUserIdRef.current;
|
|
159
|
+
const hasProvidedValidation = providedValidatedUserId !== undefined;
|
|
160
|
+
let cacheUserId = hasProvidedValidation
|
|
161
|
+
? providedValidatedUserId
|
|
162
|
+
: validatedUserIdRef.current ??
|
|
163
|
+
offlineUserId ??
|
|
164
|
+
activeUserIdRef.current;
|
|
165
|
+
let sessionUser = null;
|
|
166
|
+
let sessionDuration = 0;
|
|
167
|
+
if (!cacheUserId && !hasProvidedValidation) {
|
|
168
|
+
const sessionStart = Date.now();
|
|
169
|
+
const { data: { session }, } = await supabase.auth.getSession();
|
|
170
|
+
if (!isCurrent())
|
|
171
|
+
return;
|
|
172
|
+
sessionDuration = Date.now() - sessionStart;
|
|
173
|
+
sessionUser = session?.user ?? null;
|
|
174
|
+
cacheUserId = sessionUser?.id ?? null;
|
|
175
|
+
}
|
|
176
|
+
if (cacheUserId) {
|
|
177
|
+
await hydrateCache(cacheUserId);
|
|
178
|
+
if (!isCurrent())
|
|
179
|
+
return;
|
|
145
180
|
}
|
|
146
181
|
if (isOffline) {
|
|
182
|
+
if (sessionUser) {
|
|
183
|
+
dispatch(setUserApiDuration(sessionDuration));
|
|
184
|
+
dispatchInTransition(dispatch, setUser(sessionUser));
|
|
185
|
+
}
|
|
147
186
|
if (cachedHerdModules === null) {
|
|
148
187
|
dispatch(setDataSource(EnumDataSource.UNKNOWN));
|
|
149
188
|
dispatch(setDataSourceInfo({
|
|
@@ -156,6 +195,19 @@ export function useScoutRefresh(options = {}) {
|
|
|
156
195
|
: EnumHerdModulesLoadingState.UNSUCCESSFULLY_LOADED);
|
|
157
196
|
return;
|
|
158
197
|
}
|
|
198
|
+
const { user, duration } = await loadAuthenticatedUser(supabase);
|
|
199
|
+
if (!isCurrent())
|
|
200
|
+
return;
|
|
201
|
+
const userId = user.id;
|
|
202
|
+
validatedUserIdRef.current = userId;
|
|
203
|
+
notifyUserValidated(userId);
|
|
204
|
+
if (cacheUserId !== userId || !cacheReady) {
|
|
205
|
+
await hydrateCache(userId);
|
|
206
|
+
if (!isCurrent())
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
dispatch(setUserApiDuration(duration));
|
|
210
|
+
dispatchInTransition(dispatch, setUser(user));
|
|
159
211
|
const queryStart = Date.now();
|
|
160
212
|
const herdModulesResponse = await load_herd_modules_query(supabase);
|
|
161
213
|
if (!isCurrent() || activeUserIdRef.current !== userId)
|
|
@@ -225,8 +277,48 @@ export function useScoutRefresh(options = {}) {
|
|
|
225
277
|
supabase,
|
|
226
278
|
cacheFirst,
|
|
227
279
|
cacheTtlMs,
|
|
280
|
+
offlineUserId,
|
|
281
|
+
notifyUserValidated,
|
|
228
282
|
resetClientState,
|
|
229
283
|
]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const previousIdentity = cacheIdentityRef.current;
|
|
286
|
+
if (previousIdentity.supabase === supabase &&
|
|
287
|
+
previousIdentity.offlineUserId === offlineUserId) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
cacheIdentityRef.current = { supabase, offlineUserId };
|
|
291
|
+
refreshIdRef.current++;
|
|
292
|
+
activeUserIdRef.current = null;
|
|
293
|
+
validatedUserIdRef.current = null;
|
|
294
|
+
resetClientState();
|
|
295
|
+
}, [supabase, offlineUserId, resetClientState]);
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (validatedUserId === undefined) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
validatedUserIdRef.current = validatedUserId;
|
|
301
|
+
if (validatedUserId === null) {
|
|
302
|
+
if (activeUserIdRef.current) {
|
|
303
|
+
refreshIdRef.current++;
|
|
304
|
+
activeUserIdRef.current = null;
|
|
305
|
+
resetClientState();
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (validatedUserId === activeUserIdRef.current) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!autoRefresh) {
|
|
313
|
+
refreshIdRef.current++;
|
|
314
|
+
if (activeUserIdRef.current) {
|
|
315
|
+
resetClientState();
|
|
316
|
+
}
|
|
317
|
+
activeUserIdRef.current = validatedUserId;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
void handleRefresh({ force: false });
|
|
321
|
+
}, [autoRefresh, validatedUserId, handleRefresh, resetClientState]);
|
|
230
322
|
useEffect(() => {
|
|
231
323
|
if (autoRefresh) {
|
|
232
324
|
void handleRefresh({ force: false });
|
|
@@ -238,6 +330,8 @@ export function useScoutRefresh(options = {}) {
|
|
|
238
330
|
const previousUserId = activeUserIdRef.current ?? scoutCache.getScopeUserId();
|
|
239
331
|
refreshIdRef.current++;
|
|
240
332
|
activeUserIdRef.current = null;
|
|
333
|
+
validatedUserIdRef.current = null;
|
|
334
|
+
notifyUserValidated(null);
|
|
241
335
|
resetClientState();
|
|
242
336
|
void clearScoutClientState(previousUserId ?? undefined).catch((error) => {
|
|
243
337
|
console.warn("[useScoutRefresh] Sign-out cache clear failed:", error);
|
|
@@ -245,16 +339,19 @@ export function useScoutRefresh(options = {}) {
|
|
|
245
339
|
return;
|
|
246
340
|
}
|
|
247
341
|
const nextUserId = session?.user?.id;
|
|
248
|
-
if (event === "SIGNED_IN" &&
|
|
249
|
-
nextUserId
|
|
250
|
-
nextUserId
|
|
342
|
+
if (event === "SIGNED_IN" && nextUserId) {
|
|
343
|
+
validatedUserIdRef.current = nextUserId;
|
|
344
|
+
notifyUserValidated(nextUserId);
|
|
345
|
+
if (nextUserId === activeUserIdRef.current) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
251
348
|
refreshIdRef.current++;
|
|
252
349
|
activeUserIdRef.current = nextUserId;
|
|
253
350
|
resetClientState();
|
|
254
351
|
void handleRefresh({ force: true });
|
|
255
352
|
}
|
|
256
353
|
});
|
|
257
|
-
}, [supabase, handleRefresh, resetClientState]);
|
|
354
|
+
}, [supabase, handleRefresh, notifyUserValidated, resetClientState]);
|
|
258
355
|
useEffect(() => {
|
|
259
356
|
if (!autoRefresh || typeof window === "undefined")
|
|
260
357
|
return;
|