@adventurelabs/scout-core 1.0.108 → 1.0.110
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/dist/hooks/useScoutRefresh.js +92 -41
- package/dist/store/hooks.d.ts +0 -2
- package/dist/store/hooks.js +0 -8
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { useAppDispatch
|
|
1
|
+
import { useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
|
+
import { useAppDispatch } from "../store/hooks";
|
|
3
|
+
import { useStore } from "react-redux";
|
|
3
4
|
import { EnumScoutStateStatus, setHerdModules, setStatus, setHerdModulesLoadingState, setHerdModulesLoadedInMs, setHerdModulesApiDuration, setUserApiDuration, setDataProcessingDuration, setUser, setDataSource, setDataSourceInfo, } from "../store/scout";
|
|
4
5
|
import { EnumHerdModulesLoadingState } from "../types/herd_module";
|
|
5
6
|
import { server_load_herd_modules } from "../helpers/herds";
|
|
6
|
-
import { server_get_user } from "../helpers/users";
|
|
7
7
|
import { scoutCache } from "../helpers/cache";
|
|
8
8
|
import { EnumDataSource } from "../types/data_source";
|
|
9
|
+
import { createBrowserClient } from "@supabase/ssr";
|
|
9
10
|
/**
|
|
10
11
|
* Hook for refreshing scout data with detailed timing measurements and cache-first loading
|
|
11
12
|
*
|
|
@@ -33,9 +34,12 @@ export function useScoutRefresh(options = {}) {
|
|
|
33
34
|
const { autoRefresh = true, onRefreshComplete, cacheFirst = true, cacheTtlMs = 24 * 60 * 60 * 1000, // 24 hours default (1 day)
|
|
34
35
|
} = options;
|
|
35
36
|
const dispatch = useAppDispatch();
|
|
36
|
-
const
|
|
37
|
-
const currentUser = useUser();
|
|
37
|
+
const store = useStore();
|
|
38
38
|
const refreshInProgressRef = useRef(false);
|
|
39
|
+
// Create Supabase client directly to avoid circular dependency
|
|
40
|
+
const supabase = useMemo(() => {
|
|
41
|
+
return createBrowserClient(process.env.NEXT_PUBLIC_SUPABASE_URL || "", process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "");
|
|
42
|
+
}, []);
|
|
39
43
|
// Refs to store timing measurements
|
|
40
44
|
const timingRefs = useRef({
|
|
41
45
|
startTime: 0,
|
|
@@ -46,7 +50,7 @@ export function useScoutRefresh(options = {}) {
|
|
|
46
50
|
cacheSaveDuration: 0,
|
|
47
51
|
});
|
|
48
52
|
// Helper function for deep comparison of objects
|
|
49
|
-
const deepEqual = useCallback((obj1, obj2) => {
|
|
53
|
+
const deepEqual = useCallback((obj1, obj2, visited = new WeakMap()) => {
|
|
50
54
|
if (obj1 === obj2)
|
|
51
55
|
return true;
|
|
52
56
|
if (obj1 == null || obj2 == null)
|
|
@@ -55,13 +59,22 @@ export function useScoutRefresh(options = {}) {
|
|
|
55
59
|
return false;
|
|
56
60
|
if (typeof obj1 !== "object")
|
|
57
61
|
return obj1 === obj2;
|
|
62
|
+
// Handle circular references
|
|
63
|
+
if (visited.has(obj1)) {
|
|
64
|
+
return visited.get(obj1) === obj2;
|
|
65
|
+
}
|
|
66
|
+
visited.set(obj1, obj2);
|
|
67
|
+
// Handle Date objects
|
|
68
|
+
if (obj1 instanceof Date && obj2 instanceof Date) {
|
|
69
|
+
return obj1.getTime() === obj2.getTime();
|
|
70
|
+
}
|
|
58
71
|
if (Array.isArray(obj1) !== Array.isArray(obj2))
|
|
59
72
|
return false;
|
|
60
73
|
if (Array.isArray(obj1)) {
|
|
61
74
|
if (obj1.length !== obj2.length)
|
|
62
75
|
return false;
|
|
63
76
|
for (let i = 0; i < obj1.length; i++) {
|
|
64
|
-
if (!deepEqual(obj1[i], obj2[i]))
|
|
77
|
+
if (!deepEqual(obj1[i], obj2[i], visited))
|
|
65
78
|
return false;
|
|
66
79
|
}
|
|
67
80
|
return true;
|
|
@@ -73,25 +86,44 @@ export function useScoutRefresh(options = {}) {
|
|
|
73
86
|
for (const key of keys1) {
|
|
74
87
|
if (!keys2.includes(key))
|
|
75
88
|
return false;
|
|
76
|
-
if (!deepEqual(obj1[key], obj2[key]))
|
|
89
|
+
if (!deepEqual(obj1[key], obj2[key], visited))
|
|
77
90
|
return false;
|
|
78
91
|
}
|
|
79
92
|
return true;
|
|
80
93
|
}, []);
|
|
81
|
-
// Helper function to
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
// Helper function to normalize herd modules for comparison (excludes metadata)
|
|
95
|
+
const normalizeHerdModulesForComparison = useCallback((herdModules) => {
|
|
96
|
+
if (!Array.isArray(herdModules))
|
|
97
|
+
return herdModules;
|
|
98
|
+
return herdModules.map((hm) => {
|
|
99
|
+
if (!hm || typeof hm !== "object")
|
|
100
|
+
return hm;
|
|
101
|
+
// Create a copy without metadata fields that don't represent business data changes
|
|
102
|
+
const { timestamp_last_refreshed, ...businessData } = hm;
|
|
103
|
+
return businessData;
|
|
104
|
+
});
|
|
105
|
+
}, []);
|
|
106
|
+
// Helper function to conditionally dispatch only if business data has changed
|
|
107
|
+
const conditionalDispatch = useCallback((newData, currentData, actionCreator, dataType, useNormalizedComparison = false) => {
|
|
108
|
+
let dataToCompare = newData;
|
|
109
|
+
let currentToCompare = currentData;
|
|
110
|
+
// For herd modules, use normalized comparison that ignores metadata
|
|
111
|
+
if (useNormalizedComparison && dataType.includes("Herd modules")) {
|
|
112
|
+
dataToCompare = normalizeHerdModulesForComparison(newData);
|
|
113
|
+
currentToCompare = normalizeHerdModulesForComparison(currentData);
|
|
114
|
+
}
|
|
115
|
+
if (!deepEqual(dataToCompare, currentToCompare)) {
|
|
116
|
+
console.log(`[useScoutRefresh] ${dataType} business data changed, updating store`);
|
|
117
|
+
dispatch(actionCreator(newData)); // Always dispatch the full data including timestamps
|
|
86
118
|
return true;
|
|
87
119
|
}
|
|
88
120
|
else {
|
|
89
|
-
console.log(`[useScoutRefresh] ${dataType} data unchanged, skipping store update`);
|
|
121
|
+
console.log(`[useScoutRefresh] ${dataType} business data unchanged, skipping store update`);
|
|
90
122
|
return false;
|
|
91
123
|
}
|
|
92
|
-
}, [dispatch, deepEqual]);
|
|
93
|
-
// Helper function to handle IndexedDB errors
|
|
94
|
-
const handleIndexedDbError = async (error, operation, retryFn) => {
|
|
124
|
+
}, [dispatch, deepEqual, normalizeHerdModulesForComparison]);
|
|
125
|
+
// Helper function to handle IndexedDB errors - memoized for stability
|
|
126
|
+
const handleIndexedDbError = useCallback(async (error, operation, retryFn) => {
|
|
95
127
|
if (error instanceof Error &&
|
|
96
128
|
(error.message.includes("object store") ||
|
|
97
129
|
error.message.includes("NotFoundError"))) {
|
|
@@ -108,7 +140,7 @@ export function useScoutRefresh(options = {}) {
|
|
|
108
140
|
console.error(`[useScoutRefresh] Database reset and retry failed:`, resetError);
|
|
109
141
|
}
|
|
110
142
|
}
|
|
111
|
-
};
|
|
143
|
+
}, []);
|
|
112
144
|
const handleRefresh = useCallback(async () => {
|
|
113
145
|
// Prevent concurrent refresh calls
|
|
114
146
|
if (refreshInProgressRef.current) {
|
|
@@ -142,20 +174,13 @@ export function useScoutRefresh(options = {}) {
|
|
|
142
174
|
cacheAge: cacheResult.age,
|
|
143
175
|
isStale: cacheResult.isStale,
|
|
144
176
|
}));
|
|
145
|
-
// Conditionally update the store with cached data if different
|
|
146
|
-
|
|
177
|
+
// Conditionally update the store with cached data if business data is different
|
|
178
|
+
// Get current state at execution time to avoid dependency issues
|
|
179
|
+
const currentHerdModules = store.getState().scout.herd_modules;
|
|
180
|
+
const herdModulesChanged = conditionalDispatch(cachedHerdModules, currentHerdModules, setHerdModules, "Herd modules (cache)", true);
|
|
147
181
|
if (herdModulesChanged) {
|
|
148
182
|
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
|
|
149
183
|
}
|
|
150
|
-
// Always load user data from API
|
|
151
|
-
const userStartTime = Date.now();
|
|
152
|
-
const res_new_user = await server_get_user();
|
|
153
|
-
const userApiDuration = Date.now() - userStartTime;
|
|
154
|
-
timingRefs.current.userApiDuration = userApiDuration;
|
|
155
|
-
dispatch(setUserApiDuration(userApiDuration));
|
|
156
|
-
if (res_new_user && res_new_user.data) {
|
|
157
|
-
conditionalDispatch(res_new_user.data, currentUser, setUser, "User (initial)");
|
|
158
|
-
}
|
|
159
184
|
// If cache is fresh, we still background fetch but don't wait
|
|
160
185
|
if (!cacheResult.isStale) {
|
|
161
186
|
console.log("[useScoutRefresh] Cache is fresh, background fetching fresh data...");
|
|
@@ -165,8 +190,22 @@ export function useScoutRefresh(options = {}) {
|
|
|
165
190
|
console.log("[useScoutRefresh] Starting background fetch...");
|
|
166
191
|
const backgroundStartTime = Date.now();
|
|
167
192
|
const [backgroundHerdModulesResult, backgroundUserResult] = await Promise.all([
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
(async () => {
|
|
194
|
+
const start = Date.now();
|
|
195
|
+
const result = await server_load_herd_modules();
|
|
196
|
+
const duration = Date.now() - start;
|
|
197
|
+
timingRefs.current.herdModulesDuration = duration;
|
|
198
|
+
dispatch(setHerdModulesApiDuration(duration));
|
|
199
|
+
return result;
|
|
200
|
+
})(),
|
|
201
|
+
(async () => {
|
|
202
|
+
const start = Date.now();
|
|
203
|
+
const { data } = await supabase.auth.getUser();
|
|
204
|
+
const duration = Date.now() - start;
|
|
205
|
+
timingRefs.current.userApiDuration = duration;
|
|
206
|
+
dispatch(setUserApiDuration(duration));
|
|
207
|
+
return { data: data.user, status: "success" };
|
|
208
|
+
})(),
|
|
170
209
|
]);
|
|
171
210
|
const backgroundDuration = Date.now() - backgroundStartTime;
|
|
172
211
|
console.log(`[useScoutRefresh] Background fetch completed in ${backgroundDuration}ms`);
|
|
@@ -188,9 +227,13 @@ export function useScoutRefresh(options = {}) {
|
|
|
188
227
|
}
|
|
189
228
|
});
|
|
190
229
|
}
|
|
191
|
-
// Conditionally update store with fresh background data
|
|
192
|
-
|
|
193
|
-
|
|
230
|
+
// Conditionally update store with fresh background data using normalized comparison
|
|
231
|
+
const currentHerdModules = store.getState().scout.herd_modules;
|
|
232
|
+
const currentUser = store.getState().scout.user;
|
|
233
|
+
conditionalDispatch(backgroundHerdModulesResult.data, currentHerdModules, setHerdModules, "Herd modules (background)", true);
|
|
234
|
+
if (backgroundUserResult && backgroundUserResult.data) {
|
|
235
|
+
conditionalDispatch(backgroundUserResult.data, currentUser, setUser, "User (background)");
|
|
236
|
+
}
|
|
194
237
|
// Update data source to DATABASE
|
|
195
238
|
dispatch(setDataSource(EnumDataSource.DATABASE));
|
|
196
239
|
dispatch(setDataSourceInfo({
|
|
@@ -240,10 +283,14 @@ export function useScoutRefresh(options = {}) {
|
|
|
240
283
|
(async () => {
|
|
241
284
|
const start = Date.now();
|
|
242
285
|
console.log(`[useScoutRefresh] Starting user request at ${new Date(start).toISOString()}`);
|
|
243
|
-
const
|
|
286
|
+
const { data } = await supabase.auth.getUser();
|
|
244
287
|
const duration = Date.now() - start;
|
|
245
288
|
console.log(`[useScoutRefresh] User request completed in ${duration}ms`);
|
|
246
|
-
return {
|
|
289
|
+
return {
|
|
290
|
+
result: { data: data.user, status: "success" },
|
|
291
|
+
duration,
|
|
292
|
+
start,
|
|
293
|
+
};
|
|
247
294
|
})(),
|
|
248
295
|
]);
|
|
249
296
|
const parallelDuration = Date.now() - parallelStartTime;
|
|
@@ -292,9 +339,11 @@ export function useScoutRefresh(options = {}) {
|
|
|
292
339
|
await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs);
|
|
293
340
|
});
|
|
294
341
|
}
|
|
295
|
-
// Step 4: Conditionally update store with fresh data
|
|
342
|
+
// Step 4: Conditionally update store with fresh data using normalized comparison
|
|
296
343
|
const dataProcessingStartTime = Date.now();
|
|
297
|
-
const
|
|
344
|
+
const currentHerdModules = store.getState().scout.herd_modules;
|
|
345
|
+
const currentUser = store.getState().scout.user;
|
|
346
|
+
const herdModulesChanged = conditionalDispatch(compatible_new_herd_modules, currentHerdModules, setHerdModules, "Herd modules (fresh API)", true);
|
|
298
347
|
const userChanged = conditionalDispatch(res_new_user.data, currentUser, setUser, "User (fresh API)");
|
|
299
348
|
if (herdModulesChanged) {
|
|
300
349
|
dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
|
|
@@ -334,19 +383,21 @@ export function useScoutRefresh(options = {}) {
|
|
|
334
383
|
console.log(` - Total duration: ${loadingDuration}ms`);
|
|
335
384
|
console.log(` - Herd modules: ${timingRefs.current.herdModulesDuration}ms`);
|
|
336
385
|
console.log(` - User API: ${timingRefs.current.userApiDuration}ms`);
|
|
386
|
+
// Call completion callback even on error for consistency
|
|
387
|
+
onRefreshComplete?.();
|
|
337
388
|
}
|
|
338
389
|
finally {
|
|
339
390
|
refreshInProgressRef.current = false;
|
|
340
391
|
}
|
|
341
392
|
}, [
|
|
342
393
|
dispatch,
|
|
394
|
+
store,
|
|
395
|
+
supabase,
|
|
343
396
|
onRefreshComplete,
|
|
344
397
|
cacheFirst,
|
|
345
398
|
cacheTtlMs,
|
|
346
|
-
handleIndexedDbError,
|
|
347
|
-
currentHerdModules,
|
|
348
|
-
currentUser,
|
|
349
399
|
conditionalDispatch,
|
|
400
|
+
handleIndexedDbError,
|
|
350
401
|
]);
|
|
351
402
|
useEffect(() => {
|
|
352
403
|
if (autoRefresh) {
|
package/dist/store/hooks.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export declare const useAppDispatch: import("react-redux").UseDispatch<import("redux").Dispatch<import("redux").UnknownAction>>;
|
|
2
|
-
export declare const useHerdModules: () => import("../types/herd_module").IHerdModule[];
|
|
3
|
-
export declare const useUser: () => import("@supabase/auth-js").User | null;
|
|
4
2
|
export declare const useHerdModulesLoadingState: () => any;
|
|
5
3
|
export declare const useIsHerdModulesLoading: () => boolean;
|
|
6
4
|
export declare const useIsHerdModulesLoaded: () => boolean;
|
package/dist/store/hooks.js
CHANGED
|
@@ -2,14 +2,6 @@ import { useDispatch, useSelector } from "react-redux";
|
|
|
2
2
|
import { EnumHerdModulesLoadingState } from "../types/herd_module";
|
|
3
3
|
// Simple wrapper for useDispatch to maintain compatibility
|
|
4
4
|
export const useAppDispatch = useDispatch;
|
|
5
|
-
// Selector hook for current herd modules
|
|
6
|
-
export const useHerdModules = () => {
|
|
7
|
-
return useSelector((state) => state.scout.herd_modules);
|
|
8
|
-
};
|
|
9
|
-
// Selector hook for current user
|
|
10
|
-
export const useUser = () => {
|
|
11
|
-
return useSelector((state) => state.scout.user);
|
|
12
|
-
};
|
|
13
5
|
// Selector hook for herd modules loading state
|
|
14
6
|
export const useHerdModulesLoadingState = () => {
|
|
15
7
|
return useSelector((state) => state.scout.herd_modules_loading_state);
|