@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.
@@ -1,11 +1,12 @@
1
- import { useEffect, useCallback, useRef } from "react";
2
- import { useAppDispatch, useHerdModules, useUser } from "../store/hooks";
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 currentHerdModules = useHerdModules();
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 conditionally dispatch only if data has changed
82
- const conditionalDispatch = useCallback((newData, currentData, actionCreator, dataType) => {
83
- if (!deepEqual(newData, currentData)) {
84
- console.log(`[useScoutRefresh] ${dataType} data changed, updating store`);
85
- dispatch(actionCreator(newData));
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
- const herdModulesChanged = conditionalDispatch(cachedHerdModules, currentHerdModules, setHerdModules, "Herd modules (cache)");
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
- server_load_herd_modules(),
169
- server_get_user(),
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
- conditionalDispatch(backgroundHerdModulesResult.data, currentHerdModules, setHerdModules, "Herd modules (background)");
193
- conditionalDispatch(backgroundUserResult.data, currentUser, setUser, "User (background)");
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 result = await server_get_user();
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 { result, duration, start };
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 if different
342
+ // Step 4: Conditionally update store with fresh data using normalized comparison
296
343
  const dataProcessingStartTime = Date.now();
297
- const herdModulesChanged = conditionalDispatch(compatible_new_herd_modules, currentHerdModules, setHerdModules, "Herd modules (fresh API)");
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) {
@@ -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;
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.108",
3
+ "version": "1.0.110",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",