@adventurelabs/scout-core 1.0.96 → 1.0.98

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,7 +1,7 @@
1
1
  "use client";
2
2
  import { useAppDispatch } from "../store/hooks";
3
3
  import { useSelector } from "react-redux";
4
- import { useEffect, useRef, useCallback } from "react";
4
+ import { useEffect, useRef, useCallback, useMemo } from "react";
5
5
  import { setActiveHerdGpsTrackersConnectivity } from "../store/scout";
6
6
  import { server_get_connectivity_by_device_id } from "../helpers/connectivity";
7
7
  import { EnumWebResponse } from "../types/requests";
@@ -12,6 +12,21 @@ export function useScoutRealtimeConnectivity(scoutSupabase) {
12
12
  const activeHerdId = useSelector((state) => state.scout.active_herd_id);
13
13
  const connectivity = useSelector((state) => state.scout.active_herd_gps_trackers_connectivity);
14
14
  const herdModules = useSelector((state) => state.scout.herd_modules);
15
+ // Create stable reference for GPS device IDs to prevent unnecessary refetching
16
+ const gpsDeviceIds = useMemo(() => {
17
+ if (!activeHerdId)
18
+ return "";
19
+ const activeHerdModule = herdModules.find((hm) => hm.herd.id.toString() === activeHerdId);
20
+ if (!activeHerdModule)
21
+ return "";
22
+ const gpsDevices = activeHerdModule.devices.filter((device) => device.device_type &&
23
+ ["gps_tracker", "gps_tracker_vehicle", "gps_tracker_person"].includes(device.device_type));
24
+ return gpsDevices
25
+ .map((d) => d.id)
26
+ .filter(Boolean)
27
+ .sort()
28
+ .join(",");
29
+ }, [activeHerdId, herdModules]);
15
30
  // Handle connectivity broadcasts
16
31
  const handleConnectivityBroadcast = useCallback((payload) => {
17
32
  const { event, payload: data } = payload;
@@ -25,81 +40,108 @@ export function useScoutRealtimeConnectivity(scoutSupabase) {
25
40
  switch (data.operation) {
26
41
  case "INSERT":
27
42
  if (!updatedConnectivity[deviceId]) {
28
- updatedConnectivity[deviceId] = [];
43
+ updatedConnectivity[deviceId] = {
44
+ most_recent: connectivityData,
45
+ history: [connectivityData],
46
+ };
29
47
  }
30
- updatedConnectivity[deviceId].push(connectivityData);
31
- // Keep only recent 100 entries
32
- if (updatedConnectivity[deviceId].length > 100) {
33
- updatedConnectivity[deviceId] = updatedConnectivity[deviceId]
48
+ else {
49
+ // Create a copy of the existing historical data
50
+ const newHistory = [
51
+ ...updatedConnectivity[deviceId].history,
52
+ connectivityData,
53
+ ];
54
+ // Keep only recent 100 entries
55
+ const sortedHistory = newHistory
34
56
  .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
35
57
  new Date(a.timestamp_start || 0).getTime())
36
58
  .slice(0, 100);
59
+ updatedConnectivity[deviceId] = {
60
+ most_recent: sortedHistory[0],
61
+ history: sortedHistory,
62
+ };
37
63
  }
38
64
  break;
39
65
  case "UPDATE":
40
66
  if (updatedConnectivity[deviceId]) {
41
- const index = updatedConnectivity[deviceId].findIndex((c) => c.id === connectivityData.id);
67
+ // Create a copy of the history array before modifying
68
+ const newHistory = [...updatedConnectivity[deviceId].history];
69
+ const index = newHistory.findIndex((c) => c.id === connectivityData.id);
42
70
  if (index >= 0) {
43
- updatedConnectivity[deviceId][index] = connectivityData;
71
+ newHistory[index] = connectivityData;
72
+ // Update most_recent if this was the most recent item
73
+ const sortedHistory = newHistory.sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
74
+ new Date(a.timestamp_start || 0).getTime());
75
+ updatedConnectivity[deviceId] = {
76
+ most_recent: sortedHistory[0],
77
+ history: sortedHistory,
78
+ };
44
79
  }
45
80
  }
46
81
  break;
47
82
  case "DELETE":
48
83
  if (updatedConnectivity[deviceId]) {
49
- updatedConnectivity[deviceId] = updatedConnectivity[deviceId].filter((c) => c.id !== connectivityData.id);
50
- if (updatedConnectivity[deviceId].length === 0) {
84
+ // Filter creates a new array, so this is safe
85
+ const newHistory = updatedConnectivity[deviceId].history.filter((c) => c.id !== connectivityData.id);
86
+ if (newHistory.length === 0) {
51
87
  delete updatedConnectivity[deviceId];
52
88
  }
89
+ else {
90
+ const sortedHistory = newHistory.sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
91
+ new Date(a.timestamp_start || 0).getTime());
92
+ updatedConnectivity[deviceId] = {
93
+ most_recent: sortedHistory[0],
94
+ history: sortedHistory,
95
+ };
96
+ }
53
97
  }
54
98
  break;
55
99
  }
56
100
  console.log("[Connectivity] updating tracker connectivity in response to broadcast");
57
101
  dispatch(setActiveHerdGpsTrackersConnectivity(updatedConnectivity));
58
- }, [connectivity]);
102
+ }, [connectivity, dispatch]);
59
103
  // Fetch initial connectivity data
60
104
  const fetchInitialData = useCallback(async () => {
61
- if (!activeHerdId)
105
+ if (!gpsDeviceIds)
62
106
  return;
63
- const herdId = activeHerdId; // Type narrowing
64
- const activeHerdModule = herdModules.find((hm) => hm.herd.id.toString() === herdId);
65
- if (!activeHerdModule)
66
- return;
67
- const gpsDevices = activeHerdModule.devices.filter((device) => device.device_type &&
68
- ["gps_tracker", "gps_tracker_vehicle", "gps_tracker_person"].includes(device.device_type));
69
- if (gpsDevices.length === 0) {
107
+ const deviceIds = gpsDeviceIds.split(",").filter(Boolean).map(Number);
108
+ if (deviceIds.length === 0) {
70
109
  return;
71
110
  }
72
- console.log(`[Connectivity] Loading data for ${gpsDevices.length} GPS trackers`);
111
+ console.log(`[Connectivity] Loading data for ${deviceIds.length} GPS trackers`);
73
112
  const timestampFilter = getDaysAgoTimestamp(1);
74
113
  const connectivityData = {};
75
- await Promise.all(gpsDevices.map(async (device) => {
76
- if (!device.id)
77
- return;
114
+ await Promise.all(deviceIds.map(async (deviceId) => {
78
115
  try {
79
- const response = await server_get_connectivity_by_device_id(device.id, timestampFilter);
116
+ const response = await server_get_connectivity_by_device_id(deviceId, timestampFilter);
80
117
  if (response.status === EnumWebResponse.SUCCESS && response.data) {
81
118
  const trackerData = response.data.filter((conn) => !conn.session_id);
82
119
  if (trackerData.length > 0) {
83
- connectivityData[device.id] = trackerData
120
+ const sortedData = trackerData
84
121
  .sort((a, b) => new Date(b.timestamp_start || 0).getTime() -
85
122
  new Date(a.timestamp_start || 0).getTime())
86
123
  .slice(0, 100);
124
+ connectivityData[deviceId] = {
125
+ most_recent: sortedData[0],
126
+ history: sortedData,
127
+ };
87
128
  }
88
129
  }
89
130
  else {
90
- console.error(`[Connectivity] API error for device ${device.id}:`, response.msg || "Unknown error");
131
+ console.warn(`[Connectivity] API error for device ${deviceId}:`, response.msg || "Unknown error");
91
132
  }
92
133
  }
93
134
  catch (error) {
94
- console.error(`[Connectivity] Failed to fetch data for device ${device.id}:`, error);
135
+ console.warn(`[Connectivity] Failed to fetch data for device ${deviceId}:`, error);
95
136
  }
96
137
  }));
97
138
  dispatch(setActiveHerdGpsTrackersConnectivity(connectivityData));
98
139
  console.log(`[Connectivity] Loaded data for ${Object.keys(connectivityData).length} devices`);
99
- }, [activeHerdId, herdModules]);
140
+ }, [gpsDeviceIds, dispatch]);
100
141
  useEffect(() => {
101
- if (!scoutSupabase || !activeHerdId || herdModules.length == 0)
142
+ if (!scoutSupabase || gpsDeviceIds === "")
102
143
  return;
144
+ console.log(`[Connectivity Hook] Loading data for ${gpsDeviceIds}`);
103
145
  // Clean up existing channels
104
146
  channels.current.forEach((channel) => scoutSupabase.removeChannel(channel));
105
147
  channels.current = [];
@@ -112,7 +154,7 @@ export function useScoutRealtimeConnectivity(scoutSupabase) {
112
154
  console.log(`[Connectivity] ✅ Connected to herd ${activeHerdId}`);
113
155
  }
114
156
  else if (status === "CHANNEL_ERROR") {
115
- console.error(`[Connectivity] Failed to connect to herd ${activeHerdId}`);
157
+ console.warn(`[Connectivity] 🟡 Failed to connect to herd ${activeHerdId}`);
116
158
  }
117
159
  });
118
160
  channels.current.push(channel);
@@ -122,5 +164,11 @@ export function useScoutRealtimeConnectivity(scoutSupabase) {
122
164
  channels.current.forEach((ch) => scoutSupabase.removeChannel(ch));
123
165
  channels.current = [];
124
166
  };
125
- }, [activeHerdId, herdModules]);
167
+ }, [
168
+ scoutSupabase,
169
+ gpsDeviceIds,
170
+ activeHerdId,
171
+ handleConnectivityBroadcast,
172
+ fetchInitialData,
173
+ ]);
126
174
  }
@@ -39,7 +39,7 @@ export function useScoutRealtimeDevices(scoutSupabase) {
39
39
  console.log(`[Devices] ✅ Connected to herd ${herdId}`);
40
40
  }
41
41
  else if (status === "CHANNEL_ERROR") {
42
- console.error(`[Devices] Failed to connect to herd ${herdId}`);
42
+ console.warn(`[Devices] 🟡 Failed to connect to herd ${herdId}`);
43
43
  }
44
44
  });
45
45
  };
@@ -1,4 +1,5 @@
1
+ import { HistoricalData } from "./data";
1
2
  import { IConnectivityWithCoordinates } from "./db";
2
3
  export type MapDeviceIdToConnectivity = {
3
- [deviceId: number]: IConnectivityWithCoordinates[];
4
+ [deviceId: number]: HistoricalData<IConnectivityWithCoordinates>;
4
5
  };
@@ -0,0 +1,4 @@
1
+ export type HistoricalData<T> = {
2
+ most_recent: T;
3
+ history: T[];
4
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.0.96",
3
+ "version": "1.0.98",
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",