@adventurelabs/scout-core 2.0.14 → 2.0.15
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/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -1
- package/dist/helpers/document_templates.d.ts +1 -0
- package/dist/helpers/document_templates.js +4 -1
- package/dist/helpers/document_templates.queries.d.ts +1 -0
- package/dist/helpers/document_templates.queries.js +11 -0
- package/dist/helpers/maintenance_requests_server.d.ts +7 -0
- package/dist/helpers/user_last_activity.d.ts +5 -0
- package/dist/helpers/user_last_activity.js +12 -0
- package/dist/helpers/user_last_activity_server.d.ts +5 -0
- package/dist/helpers/user_last_activity_server.js +6 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/useInfiniteQuery.d.ts +9 -4
- package/dist/hooks/useInfiniteQuery.js +319 -905
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/types/db.d.ts +1 -0
- package/dist/types/supabase.d.ts +59 -10
- package/package.json +1 -1
|
@@ -1,7 +1,203 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
|
3
3
|
import { useGetSessionsInfiniteByHerdQuery, useGetSessionsInfiniteByDeviceQuery, useGetEventsInfiniteByHerdQuery, useGetEventsInfiniteByDeviceQuery, useGetArtifactsInfiniteByHerdQuery, useGetArtifactsInfiniteByDeviceQuery, useGetFeedInfiniteByHerdQuery, useGetFeedInfiniteByDeviceQuery, useGetAnalysisJobsInfiniteQuery, useGetAnalysisTasksInfiniteQuery, } from "../store/api";
|
|
4
|
-
|
|
4
|
+
/** Signed media URLs are re-minted on every fetch, so they never indicate a changed row. */
|
|
5
|
+
const REMINTED_URL_KEYS = new Set(["media_url", "thumbnail_url", "proxy_url"]);
|
|
6
|
+
function rowsEqual(left, right) {
|
|
7
|
+
if (left === right)
|
|
8
|
+
return true;
|
|
9
|
+
if (typeof left !== "object" ||
|
|
10
|
+
typeof right !== "object" ||
|
|
11
|
+
left === null ||
|
|
12
|
+
right === null) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
16
|
+
return (Array.isArray(left) &&
|
|
17
|
+
Array.isArray(right) &&
|
|
18
|
+
left.length === right.length &&
|
|
19
|
+
left.every((item, index) => rowsEqual(item, right[index])));
|
|
20
|
+
}
|
|
21
|
+
const keys = Object.keys(left).filter((key) => !REMINTED_URL_KEYS.has(key));
|
|
22
|
+
const rightKeys = Object.keys(right).filter((key) => !REMINTED_URL_KEYS.has(key));
|
|
23
|
+
return (keys.length === rightKeys.length &&
|
|
24
|
+
keys.every((key) => Object.prototype.hasOwnProperty.call(right, key) &&
|
|
25
|
+
rowsEqual(Reflect.get(left, key), Reflect.get(right, key))));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reuse the previous row object when a refetch returned the same content, so consumers
|
|
29
|
+
* can memoize on row identity. Rows that only differ by a re-minted signed URL keep the
|
|
30
|
+
* earlier object, and therefore the earlier URL, until the row itself changes.
|
|
31
|
+
*/
|
|
32
|
+
function preserveRowReferences(previous, next, rowKey) {
|
|
33
|
+
const previousByKey = new Map(previous
|
|
34
|
+
.map((row) => [rowKey(row), row])
|
|
35
|
+
.filter(([key]) => key != null));
|
|
36
|
+
return next.map((row) => {
|
|
37
|
+
const key = rowKey(row);
|
|
38
|
+
const previousRow = key == null ? undefined : previousByKey.get(key);
|
|
39
|
+
return previousRow && rowsEqual(previousRow, row) ? previousRow : row;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Cursors are flat records of scalars, so a shallow comparison identifies a page. */
|
|
43
|
+
function sameCursor(left, right) {
|
|
44
|
+
if (left === right)
|
|
45
|
+
return true;
|
|
46
|
+
if (left === null || right === null)
|
|
47
|
+
return false;
|
|
48
|
+
const keys = Object.keys(left);
|
|
49
|
+
return (keys.length === Object.keys(right).length &&
|
|
50
|
+
keys.every((key) => Reflect.get(left, key) === Reflect.get(right, key)));
|
|
51
|
+
}
|
|
52
|
+
const rowId = (row) => row.id;
|
|
53
|
+
const feedRowKey = (row) => `${row.sort_ts ?? ""}_${row.sort_id ?? ""}_${row.feed_type ?? ""}`;
|
|
54
|
+
/**
|
|
55
|
+
* Holds the cursor of the page the live query should request.
|
|
56
|
+
*
|
|
57
|
+
* State is tagged with `key`, and state carrying a stale key reads as null, so a new entity
|
|
58
|
+
* is never requested with the previous entity's cursor.
|
|
59
|
+
*/
|
|
60
|
+
function useCursor(key) {
|
|
61
|
+
const [stored, setStored] = useState(() => ({ key, cursor: null }));
|
|
62
|
+
const setCursor = useCallback((cursor) => setStored({ key, cursor }), [key]);
|
|
63
|
+
return {
|
|
64
|
+
key,
|
|
65
|
+
cursor: stored.key === key ? stored.cursor : null,
|
|
66
|
+
setCursor,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const noPages = (key) => ({
|
|
70
|
+
key,
|
|
71
|
+
pages: [],
|
|
72
|
+
refreshQueue: null,
|
|
73
|
+
});
|
|
74
|
+
function upsertPage(state, page, rowKey) {
|
|
75
|
+
const index = state.pages.findIndex((loaded) => sameCursor(loaded.cursor, page.cursor));
|
|
76
|
+
if (index === -1) {
|
|
77
|
+
return { ...state, pages: [...state.pages, page] };
|
|
78
|
+
}
|
|
79
|
+
const loaded = state.pages[index];
|
|
80
|
+
const rows = preserveRowReferences(loaded.rows, page.rows, rowKey);
|
|
81
|
+
const unchanged = loaded.hasMore === page.hasMore &&
|
|
82
|
+
sameCursor(loaded.nextCursor, page.nextCursor) &&
|
|
83
|
+
loaded.rows.length === rows.length &&
|
|
84
|
+
rows.every((row, position) => row === loaded.rows[position]);
|
|
85
|
+
if (unchanged)
|
|
86
|
+
return state;
|
|
87
|
+
const pages = [...state.pages];
|
|
88
|
+
pages[index] = { ...page, rows };
|
|
89
|
+
return { ...state, pages };
|
|
90
|
+
}
|
|
91
|
+
const dropRefreshedCursor = (state) => {
|
|
92
|
+
if (!state.refreshQueue)
|
|
93
|
+
return state;
|
|
94
|
+
const remaining = state.refreshQueue.slice(1);
|
|
95
|
+
return { ...state, refreshQueue: remaining.length > 0 ? remaining : null };
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Accumulates cursor-paginated responses into a list.
|
|
99
|
+
*
|
|
100
|
+
* A single query subscription serves every page: the cursor handle decides which page is
|
|
101
|
+
* being requested, and each response is upserted into `pages` under that cursor. A
|
|
102
|
+
* preserve-pages refresh walks the cursor back through the loaded pages one at a time,
|
|
103
|
+
* replacing each in place. Pages are tagged with the handle's key, so changing entity or
|
|
104
|
+
* filters empties the list and abandons any refresh in the same render.
|
|
105
|
+
*/
|
|
106
|
+
function useInfinitePages({ key, cursor, setCursor }, query, mapping) {
|
|
107
|
+
const [stored, setStored] = useState(() => noPages(key));
|
|
108
|
+
const latest = useRef({ query, mapping });
|
|
109
|
+
latest.current = { query, mapping };
|
|
110
|
+
const state = stored.key === key ? stored : noPages(key);
|
|
111
|
+
const update = useCallback((change) => {
|
|
112
|
+
setStored((previous) => change(previous.key === key ? previous : noPages(key)));
|
|
113
|
+
}, [key]);
|
|
114
|
+
const { data, isLoading } = query;
|
|
115
|
+
const page = useMemo(() => (data === undefined ? undefined : latest.current.mapping.toPage(data)), [data]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!page || isLoading)
|
|
118
|
+
return;
|
|
119
|
+
update((previous) => upsertPage(previous, { ...page, cursor }, latest.current.mapping.rowKey));
|
|
120
|
+
}, [cursor, isLoading, page, update]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const queue = state.refreshQueue;
|
|
123
|
+
if (!queue)
|
|
124
|
+
return;
|
|
125
|
+
const [refreshing] = queue;
|
|
126
|
+
if (!sameCursor(cursor, refreshing)) {
|
|
127
|
+
setCursor(refreshing);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let cancelled = false;
|
|
131
|
+
void latest.current.query
|
|
132
|
+
.refetch()
|
|
133
|
+
.unwrap()
|
|
134
|
+
.then((response) => {
|
|
135
|
+
if (cancelled)
|
|
136
|
+
return;
|
|
137
|
+
const { mapping } = latest.current;
|
|
138
|
+
const refreshed = mapping.toPage(response);
|
|
139
|
+
update((previous) =>
|
|
140
|
+
// Only apply while this cursor is still the one being refreshed.
|
|
141
|
+
previous.refreshQueue &&
|
|
142
|
+
sameCursor(previous.refreshQueue[0], refreshing)
|
|
143
|
+
? dropRefreshedCursor(upsertPage(previous, { ...refreshed, cursor: refreshing }, mapping.rowKey))
|
|
144
|
+
: previous);
|
|
145
|
+
})
|
|
146
|
+
.catch(() => {
|
|
147
|
+
if (!cancelled)
|
|
148
|
+
update(dropRefreshedCursor);
|
|
149
|
+
});
|
|
150
|
+
return () => {
|
|
151
|
+
cancelled = true;
|
|
152
|
+
};
|
|
153
|
+
}, [cursor, setCursor, state.refreshQueue, update]);
|
|
154
|
+
/** Both modes queue the cursors to re-request, so a refetch always reaches the network. */
|
|
155
|
+
const refetch = useCallback((options) => {
|
|
156
|
+
update((previous) => options?.preservePages && previous.pages.length > 0
|
|
157
|
+
? {
|
|
158
|
+
...previous,
|
|
159
|
+
refreshQueue: previous.pages.map((loaded) => loaded.cursor),
|
|
160
|
+
}
|
|
161
|
+
: { ...noPages(key), refreshQueue: [null] });
|
|
162
|
+
}, [key, update]);
|
|
163
|
+
const lastPage = state.pages[state.pages.length - 1];
|
|
164
|
+
const nextCursor = lastPage?.nextCursor ?? null;
|
|
165
|
+
const hasMore = Boolean(lastPage?.hasMore) && nextCursor !== null;
|
|
166
|
+
const isRefreshing = state.refreshQueue !== null;
|
|
167
|
+
const loadMore = useCallback(() => {
|
|
168
|
+
if (!hasMore || nextCursor === null || isLoading || isRefreshing)
|
|
169
|
+
return;
|
|
170
|
+
setCursor(nextCursor);
|
|
171
|
+
}, [hasMore, isLoading, isRefreshing, nextCursor, setCursor]);
|
|
172
|
+
const items = useMemo(() => {
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
return state.pages
|
|
175
|
+
.flatMap((loaded) => loaded.rows)
|
|
176
|
+
.filter((row) => {
|
|
177
|
+
const rowIdentity = latest.current.mapping.rowKey(row);
|
|
178
|
+
if (rowIdentity == null)
|
|
179
|
+
return true;
|
|
180
|
+
if (seen.has(rowIdentity))
|
|
181
|
+
return false;
|
|
182
|
+
seen.add(rowIdentity);
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
}, [state.pages]);
|
|
186
|
+
return {
|
|
187
|
+
items,
|
|
188
|
+
isLoading: isLoading && state.pages.length === 0,
|
|
189
|
+
isRefreshing,
|
|
190
|
+
isLoadingMore: isLoading && state.pages.length > 0,
|
|
191
|
+
hasMore,
|
|
192
|
+
loadMore,
|
|
193
|
+
refetch,
|
|
194
|
+
error: query.error,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// =====================================================
|
|
198
|
+
// SHARED QUERY ARGS AND RESPONSE MAPPING
|
|
199
|
+
// =====================================================
|
|
200
|
+
function useFiltersKey(options) {
|
|
5
201
|
return useMemo(() => JSON.stringify({
|
|
6
202
|
rangeStart: options.rangeStart ?? null,
|
|
7
203
|
rangeEnd: options.rangeEnd ?? null,
|
|
@@ -14,947 +210,165 @@ function useInfiniteFiltersKey(options) {
|
|
|
14
210
|
options.minFlightDistanceMeters,
|
|
15
211
|
]);
|
|
16
212
|
}
|
|
213
|
+
const rangeArgs = (options) => ({
|
|
214
|
+
limit: options.limit || 20,
|
|
215
|
+
supabase: options.supabase,
|
|
216
|
+
rangeStart: options.rangeStart ?? null,
|
|
217
|
+
rangeEnd: options.rangeEnd ?? null,
|
|
218
|
+
});
|
|
219
|
+
const flightArgs = (options) => ({
|
|
220
|
+
...rangeArgs(options),
|
|
221
|
+
minFlightTimeMinutes: options.minFlightTimeMinutes ?? null,
|
|
222
|
+
minFlightDistanceMeters: options.minFlightDistanceMeters ?? null,
|
|
223
|
+
});
|
|
224
|
+
const sessionsPage = (response) => ({
|
|
225
|
+
rows: response.sessions,
|
|
226
|
+
nextCursor: response.nextCursor,
|
|
227
|
+
hasMore: response.hasMore,
|
|
228
|
+
});
|
|
229
|
+
const eventsPage = (response) => ({
|
|
230
|
+
rows: response.events,
|
|
231
|
+
nextCursor: response.nextCursor,
|
|
232
|
+
hasMore: response.hasMore,
|
|
233
|
+
});
|
|
234
|
+
const artifactsPage = (response) => ({
|
|
235
|
+
rows: response.artifacts,
|
|
236
|
+
nextCursor: response.nextCursor,
|
|
237
|
+
hasMore: response.hasMore,
|
|
238
|
+
});
|
|
239
|
+
const feedPage = (response) => {
|
|
240
|
+
const rows = Array.isArray(response.items) ? response.items : [];
|
|
241
|
+
return {
|
|
242
|
+
rows,
|
|
243
|
+
nextCursor: response.nextCursor,
|
|
244
|
+
// The feed RPC can return fewer rows than the limit while more still exist, so any
|
|
245
|
+
// non-empty page with a next cursor means there is more to load.
|
|
246
|
+
hasMore: rows.length > 0 && response.nextCursor !== null,
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
const analysisJobsPage = (response) => ({
|
|
250
|
+
rows: response.jobs,
|
|
251
|
+
nextCursor: response.nextCursor,
|
|
252
|
+
hasMore: response.hasMore,
|
|
253
|
+
});
|
|
254
|
+
const analysisTasksPage = (response) => ({
|
|
255
|
+
rows: response.tasks,
|
|
256
|
+
nextCursor: response.nextCursor,
|
|
257
|
+
hasMore: response.hasMore,
|
|
258
|
+
});
|
|
17
259
|
// =====================================================
|
|
18
|
-
// SESSIONS
|
|
260
|
+
// SESSIONS
|
|
19
261
|
// =====================================================
|
|
20
262
|
export const useInfiniteSessionsByHerd = (herdId, options) => {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
herdId,
|
|
28
|
-
limit: options.limit || 20,
|
|
29
|
-
cursor: currentCursor,
|
|
30
|
-
supabase: options.supabase,
|
|
31
|
-
rangeStart: options.rangeStart ?? null,
|
|
32
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
33
|
-
minFlightTimeMinutes: options.minFlightTimeMinutes ?? null,
|
|
34
|
-
minFlightDistanceMeters: options.minFlightDistanceMeters ?? null,
|
|
35
|
-
}, {
|
|
36
|
-
skip: !options.enabled || !herdId,
|
|
263
|
+
const filtersKey = useFiltersKey(options);
|
|
264
|
+
const paging = useCursor(`herd:${herdId}|${filtersKey}`);
|
|
265
|
+
const query = useGetSessionsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...flightArgs(options) }, { skip: !options.enabled || !herdId });
|
|
266
|
+
return useInfinitePages(paging, query, {
|
|
267
|
+
toPage: sessionsPage,
|
|
268
|
+
rowKey: rowId,
|
|
37
269
|
});
|
|
38
|
-
// Reset state when herdId changes
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (prevHerdIdRef.current !== undefined &&
|
|
41
|
-
prevHerdIdRef.current !== herdId &&
|
|
42
|
-
options.enabled &&
|
|
43
|
-
herdId) {
|
|
44
|
-
setPages([]);
|
|
45
|
-
setCurrentCursor(null);
|
|
46
|
-
}
|
|
47
|
-
prevHerdIdRef.current = herdId;
|
|
48
|
-
}, [herdId, options.enabled]);
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
51
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
55
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
56
|
-
setPages([]);
|
|
57
|
-
setCurrentCursor(null);
|
|
58
|
-
}
|
|
59
|
-
}, [infiniteFiltersKey]);
|
|
60
|
-
// Update pages when new data arrives
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
63
|
-
setPages((prev) => {
|
|
64
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
65
|
-
(p.cursor &&
|
|
66
|
-
currentCursor &&
|
|
67
|
-
p.cursor.id === currentCursor.id &&
|
|
68
|
-
p.cursor.timestamp === currentCursor.timestamp));
|
|
69
|
-
if (!existingPage) {
|
|
70
|
-
return [
|
|
71
|
-
...prev,
|
|
72
|
-
{ cursor: currentCursor, data: currentQuery.data.sessions },
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
return prev;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
79
|
-
const loadMore = useCallback(() => {
|
|
80
|
-
if (currentQuery.data?.hasMore &&
|
|
81
|
-
currentQuery.data.nextCursor &&
|
|
82
|
-
!currentQuery.isLoading) {
|
|
83
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
84
|
-
}
|
|
85
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
86
|
-
const refetch = useCallback(() => {
|
|
87
|
-
setPages([]);
|
|
88
|
-
setCurrentCursor(null);
|
|
89
|
-
currentQuery.refetch();
|
|
90
|
-
}, [currentQuery]);
|
|
91
|
-
// Flatten all pages into single array
|
|
92
|
-
const allItems = useMemo(() => {
|
|
93
|
-
return pages.flatMap((page) => page.data);
|
|
94
|
-
}, [pages]);
|
|
95
|
-
return {
|
|
96
|
-
items: allItems,
|
|
97
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
98
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
99
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
100
|
-
loadMore,
|
|
101
|
-
refetch,
|
|
102
|
-
error: currentQuery.error,
|
|
103
|
-
};
|
|
104
270
|
};
|
|
105
271
|
export const useInfiniteSessionsByDevice = (deviceId, options) => {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
deviceId,
|
|
113
|
-
limit: options.limit || 20,
|
|
114
|
-
cursor: currentCursor,
|
|
115
|
-
supabase: options.supabase,
|
|
116
|
-
rangeStart: options.rangeStart ?? null,
|
|
117
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
118
|
-
minFlightTimeMinutes: options.minFlightTimeMinutes ?? null,
|
|
119
|
-
minFlightDistanceMeters: options.minFlightDistanceMeters ?? null,
|
|
120
|
-
}, {
|
|
121
|
-
skip: !options.enabled || !deviceId,
|
|
272
|
+
const filtersKey = useFiltersKey(options);
|
|
273
|
+
const paging = useCursor(`device:${deviceId}|${filtersKey}`);
|
|
274
|
+
const query = useGetSessionsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...flightArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
275
|
+
return useInfinitePages(paging, query, {
|
|
276
|
+
toPage: sessionsPage,
|
|
277
|
+
rowKey: rowId,
|
|
122
278
|
});
|
|
123
|
-
// Reset state when deviceId changes
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (prevDeviceIdRef.current !== undefined &&
|
|
126
|
-
prevDeviceIdRef.current !== deviceId &&
|
|
127
|
-
options.enabled &&
|
|
128
|
-
deviceId) {
|
|
129
|
-
setPages([]);
|
|
130
|
-
setCurrentCursor(null);
|
|
131
|
-
}
|
|
132
|
-
prevDeviceIdRef.current = deviceId;
|
|
133
|
-
}, [deviceId, options.enabled]);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
136
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
140
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
141
|
-
setPages([]);
|
|
142
|
-
setCurrentCursor(null);
|
|
143
|
-
}
|
|
144
|
-
}, [infiniteFiltersKey]);
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
147
|
-
setPages((prev) => {
|
|
148
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
149
|
-
(p.cursor &&
|
|
150
|
-
currentCursor &&
|
|
151
|
-
p.cursor.id === currentCursor.id &&
|
|
152
|
-
p.cursor.timestamp === currentCursor.timestamp));
|
|
153
|
-
if (!existingPage) {
|
|
154
|
-
return [
|
|
155
|
-
...prev,
|
|
156
|
-
{ cursor: currentCursor, data: currentQuery.data.sessions },
|
|
157
|
-
];
|
|
158
|
-
}
|
|
159
|
-
return prev;
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
163
|
-
const loadMore = useCallback(() => {
|
|
164
|
-
if (currentQuery.data?.hasMore &&
|
|
165
|
-
currentQuery.data.nextCursor &&
|
|
166
|
-
!currentQuery.isLoading) {
|
|
167
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
168
|
-
}
|
|
169
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
170
|
-
const refetch = useCallback(() => {
|
|
171
|
-
setPages([]);
|
|
172
|
-
setCurrentCursor(null);
|
|
173
|
-
currentQuery.refetch();
|
|
174
|
-
}, [currentQuery]);
|
|
175
|
-
const allItems = useMemo(() => {
|
|
176
|
-
return pages.flatMap((page) => page.data);
|
|
177
|
-
}, [pages]);
|
|
178
|
-
return {
|
|
179
|
-
items: allItems,
|
|
180
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
181
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
182
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
183
|
-
loadMore,
|
|
184
|
-
refetch,
|
|
185
|
-
error: currentQuery.error,
|
|
186
|
-
};
|
|
187
279
|
};
|
|
188
280
|
// =====================================================
|
|
189
|
-
// EVENTS
|
|
281
|
+
// EVENTS
|
|
190
282
|
// =====================================================
|
|
191
283
|
export const useInfiniteEventsByHerd = (herdId, options) => {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
herdId,
|
|
199
|
-
limit: options.limit || 20,
|
|
200
|
-
cursor: currentCursor,
|
|
201
|
-
supabase: options.supabase,
|
|
202
|
-
rangeStart: options.rangeStart ?? null,
|
|
203
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
204
|
-
}, {
|
|
205
|
-
skip: !options.enabled || !herdId,
|
|
284
|
+
const filtersKey = useFiltersKey(options);
|
|
285
|
+
const paging = useCursor(`herd:${herdId}|${filtersKey}`);
|
|
286
|
+
const query = useGetEventsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
287
|
+
return useInfinitePages(paging, query, {
|
|
288
|
+
toPage: eventsPage,
|
|
289
|
+
rowKey: rowId,
|
|
206
290
|
});
|
|
207
|
-
// Reset state when herdId changes
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (prevHerdIdRef.current !== undefined &&
|
|
210
|
-
prevHerdIdRef.current !== herdId &&
|
|
211
|
-
options.enabled &&
|
|
212
|
-
herdId) {
|
|
213
|
-
setPages([]);
|
|
214
|
-
setCurrentCursor(null);
|
|
215
|
-
}
|
|
216
|
-
prevHerdIdRef.current = herdId;
|
|
217
|
-
}, [herdId, options.enabled]);
|
|
218
|
-
useEffect(() => {
|
|
219
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
220
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
224
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
225
|
-
setPages([]);
|
|
226
|
-
setCurrentCursor(null);
|
|
227
|
-
}
|
|
228
|
-
}, [infiniteFiltersKey]);
|
|
229
|
-
useEffect(() => {
|
|
230
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
231
|
-
setPages((prev) => {
|
|
232
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
233
|
-
(p.cursor &&
|
|
234
|
-
currentCursor &&
|
|
235
|
-
p.cursor.id === currentCursor.id &&
|
|
236
|
-
p.cursor.timestamp === currentCursor.timestamp));
|
|
237
|
-
if (!existingPage) {
|
|
238
|
-
return [
|
|
239
|
-
...prev,
|
|
240
|
-
{ cursor: currentCursor, data: currentQuery.data.events },
|
|
241
|
-
];
|
|
242
|
-
}
|
|
243
|
-
return prev;
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
247
|
-
const loadMore = useCallback(() => {
|
|
248
|
-
if (currentQuery.data?.hasMore &&
|
|
249
|
-
currentQuery.data.nextCursor &&
|
|
250
|
-
!currentQuery.isLoading) {
|
|
251
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
252
|
-
}
|
|
253
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
254
|
-
const refetch = useCallback(() => {
|
|
255
|
-
setPages([]);
|
|
256
|
-
setCurrentCursor(null);
|
|
257
|
-
currentQuery.refetch();
|
|
258
|
-
}, [currentQuery]);
|
|
259
|
-
const allItems = useMemo(() => {
|
|
260
|
-
return pages.flatMap((page) => page.data);
|
|
261
|
-
}, [pages]);
|
|
262
|
-
return {
|
|
263
|
-
items: allItems,
|
|
264
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
265
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
266
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
267
|
-
loadMore,
|
|
268
|
-
refetch,
|
|
269
|
-
error: currentQuery.error,
|
|
270
|
-
};
|
|
271
291
|
};
|
|
272
292
|
export const useInfiniteEventsByDevice = (deviceId, options) => {
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
deviceId,
|
|
280
|
-
limit: options.limit || 20,
|
|
281
|
-
cursor: currentCursor,
|
|
282
|
-
supabase: options.supabase,
|
|
283
|
-
rangeStart: options.rangeStart ?? null,
|
|
284
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
285
|
-
}, {
|
|
286
|
-
skip: !options.enabled || !deviceId,
|
|
293
|
+
const filtersKey = useFiltersKey(options);
|
|
294
|
+
const paging = useCursor(`device:${deviceId}|${filtersKey}`);
|
|
295
|
+
const query = useGetEventsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
296
|
+
return useInfinitePages(paging, query, {
|
|
297
|
+
toPage: eventsPage,
|
|
298
|
+
rowKey: rowId,
|
|
287
299
|
});
|
|
288
|
-
// Reset state when deviceId changes
|
|
289
|
-
useEffect(() => {
|
|
290
|
-
if (prevDeviceIdRef.current !== undefined &&
|
|
291
|
-
prevDeviceIdRef.current !== deviceId &&
|
|
292
|
-
options.enabled &&
|
|
293
|
-
deviceId) {
|
|
294
|
-
setPages([]);
|
|
295
|
-
setCurrentCursor(null);
|
|
296
|
-
}
|
|
297
|
-
prevDeviceIdRef.current = deviceId;
|
|
298
|
-
}, [deviceId, options.enabled]);
|
|
299
|
-
useEffect(() => {
|
|
300
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
301
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
305
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
306
|
-
setPages([]);
|
|
307
|
-
setCurrentCursor(null);
|
|
308
|
-
}
|
|
309
|
-
}, [infiniteFiltersKey]);
|
|
310
|
-
useEffect(() => {
|
|
311
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
312
|
-
setPages((prev) => {
|
|
313
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
314
|
-
(p.cursor &&
|
|
315
|
-
currentCursor &&
|
|
316
|
-
p.cursor.id === currentCursor.id &&
|
|
317
|
-
p.cursor.timestamp === currentCursor.timestamp));
|
|
318
|
-
if (!existingPage) {
|
|
319
|
-
return [
|
|
320
|
-
...prev,
|
|
321
|
-
{ cursor: currentCursor, data: currentQuery.data.events },
|
|
322
|
-
];
|
|
323
|
-
}
|
|
324
|
-
return prev;
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
328
|
-
const loadMore = useCallback(() => {
|
|
329
|
-
if (currentQuery.data?.hasMore &&
|
|
330
|
-
currentQuery.data.nextCursor &&
|
|
331
|
-
!currentQuery.isLoading) {
|
|
332
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
333
|
-
}
|
|
334
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
335
|
-
const refetch = useCallback(() => {
|
|
336
|
-
setPages([]);
|
|
337
|
-
setCurrentCursor(null);
|
|
338
|
-
currentQuery.refetch();
|
|
339
|
-
}, [currentQuery]);
|
|
340
|
-
const allItems = useMemo(() => {
|
|
341
|
-
return pages.flatMap((page) => page.data);
|
|
342
|
-
}, [pages]);
|
|
343
|
-
return {
|
|
344
|
-
items: allItems,
|
|
345
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
346
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
347
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
348
|
-
loadMore,
|
|
349
|
-
refetch,
|
|
350
|
-
error: currentQuery.error,
|
|
351
|
-
};
|
|
352
300
|
};
|
|
353
301
|
// =====================================================
|
|
354
|
-
// ARTIFACTS
|
|
302
|
+
// ARTIFACTS
|
|
355
303
|
// =====================================================
|
|
356
304
|
export const useInfiniteArtifactsByHerd = (herdId, options) => {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
herdId,
|
|
364
|
-
limit: options.limit || 20,
|
|
365
|
-
cursor: currentCursor,
|
|
366
|
-
supabase: options.supabase,
|
|
367
|
-
rangeStart: options.rangeStart ?? null,
|
|
368
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
369
|
-
}, {
|
|
370
|
-
skip: !options.enabled || !herdId,
|
|
305
|
+
const filtersKey = useFiltersKey(options);
|
|
306
|
+
const paging = useCursor(`herd:${herdId}|${filtersKey}`);
|
|
307
|
+
const query = useGetArtifactsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
308
|
+
return useInfinitePages(paging, query, {
|
|
309
|
+
toPage: artifactsPage,
|
|
310
|
+
rowKey: rowId,
|
|
371
311
|
});
|
|
372
|
-
// Reset state when herdId changes
|
|
373
|
-
useEffect(() => {
|
|
374
|
-
if (prevHerdIdRef.current !== undefined &&
|
|
375
|
-
prevHerdIdRef.current !== herdId &&
|
|
376
|
-
options.enabled &&
|
|
377
|
-
herdId) {
|
|
378
|
-
setPages([]);
|
|
379
|
-
setCurrentCursor(null);
|
|
380
|
-
}
|
|
381
|
-
prevHerdIdRef.current = herdId;
|
|
382
|
-
}, [herdId, options.enabled]);
|
|
383
|
-
useEffect(() => {
|
|
384
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
385
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
389
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
390
|
-
setPages([]);
|
|
391
|
-
setCurrentCursor(null);
|
|
392
|
-
}
|
|
393
|
-
}, [infiniteFiltersKey]);
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
396
|
-
setPages((prev) => {
|
|
397
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
398
|
-
(p.cursor &&
|
|
399
|
-
currentCursor &&
|
|
400
|
-
p.cursor.id === currentCursor.id &&
|
|
401
|
-
p.cursor.timestamp === currentCursor.timestamp));
|
|
402
|
-
if (!existingPage) {
|
|
403
|
-
return [
|
|
404
|
-
...prev,
|
|
405
|
-
{ cursor: currentCursor, data: currentQuery.data.artifacts },
|
|
406
|
-
];
|
|
407
|
-
}
|
|
408
|
-
return prev;
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
412
|
-
const loadMore = useCallback(() => {
|
|
413
|
-
if (currentQuery.data?.hasMore &&
|
|
414
|
-
currentQuery.data.nextCursor &&
|
|
415
|
-
!currentQuery.isLoading) {
|
|
416
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
417
|
-
}
|
|
418
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
419
|
-
const refetch = useCallback(() => {
|
|
420
|
-
setPages([]);
|
|
421
|
-
setCurrentCursor(null);
|
|
422
|
-
currentQuery.refetch();
|
|
423
|
-
}, [currentQuery]);
|
|
424
|
-
const allItems = useMemo(() => {
|
|
425
|
-
return pages.flatMap((page) => page.data);
|
|
426
|
-
}, [pages]);
|
|
427
|
-
return {
|
|
428
|
-
items: allItems,
|
|
429
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
430
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
431
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
432
|
-
loadMore,
|
|
433
|
-
refetch,
|
|
434
|
-
error: currentQuery.error,
|
|
435
|
-
};
|
|
436
312
|
};
|
|
437
313
|
export const useInfiniteArtifactsByDevice = (deviceId, options) => {
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
deviceId,
|
|
445
|
-
limit: options.limit || 20,
|
|
446
|
-
cursor: currentCursor,
|
|
447
|
-
supabase: options.supabase,
|
|
448
|
-
rangeStart: options.rangeStart ?? null,
|
|
449
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
450
|
-
}, {
|
|
451
|
-
skip: !options.enabled || !deviceId,
|
|
314
|
+
const filtersKey = useFiltersKey(options);
|
|
315
|
+
const paging = useCursor(`device:${deviceId}|${filtersKey}`);
|
|
316
|
+
const query = useGetArtifactsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
317
|
+
return useInfinitePages(paging, query, {
|
|
318
|
+
toPage: artifactsPage,
|
|
319
|
+
rowKey: rowId,
|
|
452
320
|
});
|
|
453
|
-
// Reset state when deviceId changes
|
|
454
|
-
useEffect(() => {
|
|
455
|
-
if (prevDeviceIdRef.current !== undefined &&
|
|
456
|
-
prevDeviceIdRef.current !== deviceId &&
|
|
457
|
-
options.enabled &&
|
|
458
|
-
deviceId) {
|
|
459
|
-
setPages([]);
|
|
460
|
-
setCurrentCursor(null);
|
|
461
|
-
}
|
|
462
|
-
prevDeviceIdRef.current = deviceId;
|
|
463
|
-
}, [deviceId, options.enabled]);
|
|
464
|
-
useEffect(() => {
|
|
465
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
466
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
470
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
471
|
-
setPages([]);
|
|
472
|
-
setCurrentCursor(null);
|
|
473
|
-
}
|
|
474
|
-
}, [infiniteFiltersKey]);
|
|
475
|
-
useEffect(() => {
|
|
476
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
477
|
-
setPages((prev) => {
|
|
478
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
479
|
-
(p.cursor &&
|
|
480
|
-
currentCursor &&
|
|
481
|
-
p.cursor.id === currentCursor.id &&
|
|
482
|
-
p.cursor.timestamp === currentCursor.timestamp));
|
|
483
|
-
if (!existingPage) {
|
|
484
|
-
return [
|
|
485
|
-
...prev,
|
|
486
|
-
{ cursor: currentCursor, data: currentQuery.data.artifacts },
|
|
487
|
-
];
|
|
488
|
-
}
|
|
489
|
-
return prev;
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
493
|
-
const loadMore = useCallback(() => {
|
|
494
|
-
if (currentQuery.data?.hasMore &&
|
|
495
|
-
currentQuery.data.nextCursor &&
|
|
496
|
-
!currentQuery.isLoading) {
|
|
497
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
498
|
-
}
|
|
499
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
500
|
-
const refetch = useCallback(() => {
|
|
501
|
-
setPages([]);
|
|
502
|
-
setCurrentCursor(null);
|
|
503
|
-
currentQuery.refetch();
|
|
504
|
-
}, [currentQuery]);
|
|
505
|
-
const allItems = useMemo(() => {
|
|
506
|
-
return pages.flatMap((page) => page.data);
|
|
507
|
-
}, [pages]);
|
|
508
|
-
return {
|
|
509
|
-
items: allItems,
|
|
510
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
511
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
512
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
513
|
-
loadMore,
|
|
514
|
-
refetch,
|
|
515
|
-
error: currentQuery.error,
|
|
516
|
-
};
|
|
517
|
-
};
|
|
518
|
-
const feedCursorEq = (a, b) => {
|
|
519
|
-
if (a === b)
|
|
520
|
-
return true;
|
|
521
|
-
if (a == null || b == null)
|
|
522
|
-
return false;
|
|
523
|
-
return (a.timestamp === b.timestamp &&
|
|
524
|
-
a.id === b.id &&
|
|
525
|
-
a.feed_type === b.feed_type);
|
|
526
321
|
};
|
|
527
|
-
|
|
322
|
+
// =====================================================
|
|
323
|
+
// FEED (merged events + artifacts)
|
|
324
|
+
// =====================================================
|
|
528
325
|
export const useInfiniteFeedByHerd = (herdId, options) => {
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const pagesLengthRef = useRef(0);
|
|
537
|
-
/** When true, pass null to the query so we don't request (newHerdId, oldCursor) before state commits. */
|
|
538
|
-
const forceNullCursorRef = useRef(false);
|
|
539
|
-
const infiniteFiltersKey = useInfiniteFiltersKey(options);
|
|
540
|
-
const prevInfiniteFiltersKeyRef = useRef(null);
|
|
541
|
-
const cursorForQuery = forceNullCursorRef.current ? null : currentCursor;
|
|
542
|
-
const currentQuery = useGetFeedInfiniteByHerdQuery({
|
|
543
|
-
herdId,
|
|
544
|
-
limit,
|
|
545
|
-
cursor: cursorForQuery,
|
|
546
|
-
supabase: options.supabase,
|
|
547
|
-
rangeStart: options.rangeStart ?? null,
|
|
548
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
549
|
-
}, { skip: !enabled });
|
|
550
|
-
const isLoading = currentQuery.isLoading;
|
|
551
|
-
useEffect(() => {
|
|
552
|
-
pagesLengthRef.current = pages.length;
|
|
553
|
-
}, [pages.length]);
|
|
554
|
-
// Reset when herd changes (match dummy: prev !== herdId && enabled && herdId)
|
|
555
|
-
useEffect(() => {
|
|
556
|
-
if (prevHerdIdRef.current !== undefined &&
|
|
557
|
-
prevHerdIdRef.current !== herdId &&
|
|
558
|
-
enabled &&
|
|
559
|
-
herdId) {
|
|
560
|
-
forceNullCursorRef.current = true;
|
|
561
|
-
setPages([]);
|
|
562
|
-
setCurrentCursor(null);
|
|
563
|
-
setCurrentResult(null);
|
|
564
|
-
lastAddedCursorRef.current = undefined;
|
|
565
|
-
}
|
|
566
|
-
prevHerdIdRef.current = herdId;
|
|
567
|
-
}, [herdId, enabled]);
|
|
568
|
-
useEffect(() => {
|
|
569
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
570
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
574
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
575
|
-
forceNullCursorRef.current = true;
|
|
576
|
-
setPages([]);
|
|
577
|
-
setCurrentCursor(null);
|
|
578
|
-
setCurrentResult(null);
|
|
579
|
-
lastAddedCursorRef.current = undefined;
|
|
580
|
-
}
|
|
581
|
-
}, [infiniteFiltersKey]);
|
|
582
|
-
// When cursor changes, clear ref so we merge the new response
|
|
583
|
-
useEffect(() => {
|
|
584
|
-
lastAddedCursorRef.current = undefined;
|
|
585
|
-
}, [currentCursor]);
|
|
586
|
-
// Merge when we have data (mirror dummy's .then() logic; fetch is done by RTK Query)
|
|
587
|
-
useEffect(() => {
|
|
588
|
-
if (!currentQuery.data || currentQuery.isLoading)
|
|
589
|
-
return;
|
|
590
|
-
const cursor = cursorForQuery;
|
|
591
|
-
const items = Array.isArray(currentQuery.data.items)
|
|
592
|
-
? currentQuery.data.items
|
|
593
|
-
: [];
|
|
594
|
-
const nextCursor = currentQuery.data.nextCursor ?? null;
|
|
595
|
-
// Derive hasMore from items we received (like dummy), so we don't get stuck when API
|
|
596
|
-
// returns hasMore: false e.g. due to RPC/PostgREST returning fewer rows than limit.
|
|
597
|
-
const hasMore = (items.length >= limit || (items.length > 0 && items.length < limit)) &&
|
|
598
|
-
nextCursor != null;
|
|
599
|
-
// After herd switch we force null cursor; once we've merged that first page, allow normal cursor again
|
|
600
|
-
if (cursor === null) {
|
|
601
|
-
forceNullCursorRef.current = false;
|
|
602
|
-
}
|
|
603
|
-
// Only update currentResult for successful response (match dummy)
|
|
604
|
-
if (items.length === 0 && cursor === null) {
|
|
605
|
-
// Leave currentResult unchanged on spurious empty first page
|
|
606
|
-
}
|
|
607
|
-
else {
|
|
608
|
-
setCurrentResult({
|
|
609
|
-
hasMore,
|
|
610
|
-
nextCursor,
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
if (items.length === 0)
|
|
614
|
-
return;
|
|
615
|
-
// Skip merge exactly like dummy: full page already added for this cursor and we have pages
|
|
616
|
-
if (items.length >= limit &&
|
|
617
|
-
feedCursorEq(lastAddedCursorRef.current ?? null, cursor) &&
|
|
618
|
-
pagesLengthRef.current > 0) {
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
setPages((prev) => {
|
|
622
|
-
const existingPage = prev.find((p) => feedCursorEq(p.cursor, cursor));
|
|
623
|
-
const next = !existingPage
|
|
624
|
-
? [...prev, { cursor, data: items }]
|
|
625
|
-
: items.length > existingPage.data.length
|
|
626
|
-
? prev.map((p) => feedCursorEq(p.cursor, cursor) ? { cursor, data: items } : p)
|
|
627
|
-
: prev;
|
|
628
|
-
if (!existingPage && items.length >= limit) {
|
|
629
|
-
lastAddedCursorRef.current = cursor;
|
|
630
|
-
}
|
|
631
|
-
if (existingPage &&
|
|
632
|
-
items.length > existingPage.data.length &&
|
|
633
|
-
items.length >= limit) {
|
|
634
|
-
lastAddedCursorRef.current = cursor;
|
|
635
|
-
}
|
|
636
|
-
return next;
|
|
637
|
-
});
|
|
638
|
-
}, [
|
|
639
|
-
currentQuery.data,
|
|
640
|
-
currentQuery.isLoading,
|
|
641
|
-
cursorForQuery,
|
|
642
|
-
pages.length,
|
|
643
|
-
limit,
|
|
644
|
-
]);
|
|
645
|
-
const loadMore = useCallback(() => {
|
|
646
|
-
if (currentResult?.hasMore &&
|
|
647
|
-
currentResult.nextCursor != null &&
|
|
648
|
-
!isLoading) {
|
|
649
|
-
setCurrentCursor(currentResult.nextCursor);
|
|
650
|
-
}
|
|
651
|
-
}, [currentResult, isLoading]);
|
|
652
|
-
const refetch = useCallback(() => {
|
|
653
|
-
forceNullCursorRef.current = true;
|
|
654
|
-
setPages([]);
|
|
655
|
-
setCurrentCursor(null);
|
|
656
|
-
setCurrentResult(null);
|
|
657
|
-
lastAddedCursorRef.current = undefined;
|
|
658
|
-
currentQuery.refetch();
|
|
659
|
-
}, [currentQuery]);
|
|
660
|
-
const allItems = useMemo(() => {
|
|
661
|
-
const sorted = [...pages].sort((a, b) => {
|
|
662
|
-
if (feedCursorEq(a.cursor, b.cursor))
|
|
663
|
-
return 0;
|
|
664
|
-
if (a.cursor === null)
|
|
665
|
-
return -1;
|
|
666
|
-
if (b.cursor === null)
|
|
667
|
-
return 1;
|
|
668
|
-
const ta = a.cursor.timestamp ?? '';
|
|
669
|
-
const tb = b.cursor.timestamp ?? '';
|
|
670
|
-
return tb.localeCompare(ta);
|
|
671
|
-
});
|
|
672
|
-
const seen = new Set();
|
|
673
|
-
return sorted.flatMap((p) => p.data).filter((item) => {
|
|
674
|
-
const key = `${item.sort_ts ?? ''}_${item.sort_id ?? ''}_${item.feed_type ?? ''}`;
|
|
675
|
-
if (seen.has(key))
|
|
676
|
-
return false;
|
|
677
|
-
seen.add(key);
|
|
678
|
-
return true;
|
|
679
|
-
});
|
|
680
|
-
}, [pages]);
|
|
681
|
-
return {
|
|
682
|
-
items: allItems,
|
|
683
|
-
isLoading: isLoading && pages.length === 0,
|
|
684
|
-
isLoadingMore: isLoading && pages.length > 0,
|
|
685
|
-
hasMore: currentResult?.hasMore ??
|
|
686
|
-
(currentCursor !== null && pages.length > 0) ??
|
|
687
|
-
false,
|
|
688
|
-
loadMore,
|
|
689
|
-
refetch,
|
|
690
|
-
error: currentQuery.error,
|
|
691
|
-
};
|
|
326
|
+
const filtersKey = useFiltersKey(options);
|
|
327
|
+
const paging = useCursor(`herd:${herdId}|${filtersKey}`);
|
|
328
|
+
const query = useGetFeedInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
329
|
+
return useInfinitePages(paging, query, {
|
|
330
|
+
toPage: feedPage,
|
|
331
|
+
rowKey: feedRowKey,
|
|
332
|
+
});
|
|
692
333
|
};
|
|
693
334
|
export const useInfiniteFeedByDevice = (deviceId, options) => {
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const infiniteFiltersKey = useInfiniteFiltersKey(options);
|
|
702
|
-
const prevInfiniteFiltersKeyRef = useRef(null);
|
|
703
|
-
const cursorForQuery = forceNullCursorRef.current ? null : currentCursor;
|
|
704
|
-
const currentQuery = useGetFeedInfiniteByDeviceQuery({
|
|
705
|
-
deviceId,
|
|
706
|
-
limit: options.limit || 20,
|
|
707
|
-
cursor: cursorForQuery,
|
|
708
|
-
supabase: options.supabase,
|
|
709
|
-
rangeStart: options.rangeStart ?? null,
|
|
710
|
-
rangeEnd: options.rangeEnd ?? null,
|
|
711
|
-
}, { skip: !options.enabled || !deviceId });
|
|
712
|
-
useEffect(() => {
|
|
713
|
-
pagesLengthRef.current = pages.length;
|
|
714
|
-
}, [pages.length]);
|
|
715
|
-
// Clear all state whenever device id changes (including to/from undefined)
|
|
716
|
-
useEffect(() => {
|
|
717
|
-
if (prevDeviceIdRef.current !== undefined &&
|
|
718
|
-
prevDeviceIdRef.current !== deviceId) {
|
|
719
|
-
forceNullCursorRef.current = true;
|
|
720
|
-
setPages([]);
|
|
721
|
-
setCurrentCursor(null);
|
|
722
|
-
lastAddedCursorRef.current = undefined;
|
|
723
|
-
setLastResult(null);
|
|
724
|
-
}
|
|
725
|
-
prevDeviceIdRef.current = deviceId;
|
|
726
|
-
}, [deviceId]);
|
|
727
|
-
useEffect(() => {
|
|
728
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
729
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
733
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
734
|
-
forceNullCursorRef.current = true;
|
|
735
|
-
setPages([]);
|
|
736
|
-
setCurrentCursor(null);
|
|
737
|
-
lastAddedCursorRef.current = undefined;
|
|
738
|
-
setLastResult(null);
|
|
739
|
-
}
|
|
740
|
-
}, [infiniteFiltersKey]);
|
|
741
|
-
// When we request a new page (cursor changed), clear ref so we merge the new response
|
|
742
|
-
useEffect(() => {
|
|
743
|
-
lastAddedCursorRef.current = undefined;
|
|
744
|
-
}, [currentCursor]);
|
|
745
|
-
useEffect(() => {
|
|
746
|
-
if (!currentQuery.data || currentQuery.isLoading)
|
|
747
|
-
return;
|
|
748
|
-
const cursor = cursorForQuery;
|
|
749
|
-
if (cursor === null) {
|
|
750
|
-
forceNullCursorRef.current = false;
|
|
751
|
-
}
|
|
752
|
-
if (pagesLengthRef.current > 0 &&
|
|
753
|
-
feedCursorEq(lastAddedCursorRef.current ?? null, cursor))
|
|
754
|
-
return;
|
|
755
|
-
const items = Array.isArray(currentQuery.data?.items)
|
|
756
|
-
? currentQuery.data.items
|
|
757
|
-
: [];
|
|
758
|
-
const limitForPage = options.limit || 20;
|
|
759
|
-
const nextCursor = currentQuery.data?.nextCursor ?? null;
|
|
760
|
-
const hasMore = (items.length >= limitForPage ||
|
|
761
|
-
(items.length > 0 && items.length < limitForPage)) &&
|
|
762
|
-
nextCursor != null;
|
|
763
|
-
setLastResult({ hasMore, nextCursor });
|
|
764
|
-
setPages((prev) => {
|
|
765
|
-
const existingPage = prev.find((p) => feedCursorEq(p.cursor, cursor));
|
|
766
|
-
if (!existingPage) {
|
|
767
|
-
if (items.length >= limitForPage) {
|
|
768
|
-
lastAddedCursorRef.current = cursor;
|
|
769
|
-
}
|
|
770
|
-
return [
|
|
771
|
-
...prev,
|
|
772
|
-
{ cursor, data: items },
|
|
773
|
-
];
|
|
774
|
-
}
|
|
775
|
-
return prev;
|
|
776
|
-
});
|
|
777
|
-
}, [
|
|
778
|
-
currentQuery.data,
|
|
779
|
-
currentQuery.isLoading,
|
|
780
|
-
cursorForQuery,
|
|
781
|
-
pages.length,
|
|
782
|
-
options.limit,
|
|
783
|
-
]);
|
|
784
|
-
const loadMore = useCallback(() => {
|
|
785
|
-
if (lastResult?.hasMore &&
|
|
786
|
-
lastResult.nextCursor != null &&
|
|
787
|
-
!currentQuery.isLoading) {
|
|
788
|
-
setCurrentCursor(lastResult.nextCursor);
|
|
789
|
-
}
|
|
790
|
-
}, [lastResult, currentQuery.isLoading]);
|
|
791
|
-
const refetch = useCallback(() => {
|
|
792
|
-
forceNullCursorRef.current = true;
|
|
793
|
-
setPages([]);
|
|
794
|
-
setCurrentCursor(null);
|
|
795
|
-
lastAddedCursorRef.current = undefined;
|
|
796
|
-
setLastResult(null);
|
|
797
|
-
currentQuery.refetch();
|
|
798
|
-
}, [currentQuery]);
|
|
799
|
-
const allItems = useMemo(() => {
|
|
800
|
-
const seen = new Set();
|
|
801
|
-
return pages.flatMap((p) => p.data).filter((item) => {
|
|
802
|
-
const key = `${item.sort_ts ?? ''}_${item.sort_id ?? ''}_${item.feed_type ?? ''}`;
|
|
803
|
-
if (seen.has(key))
|
|
804
|
-
return false;
|
|
805
|
-
seen.add(key);
|
|
806
|
-
return true;
|
|
807
|
-
});
|
|
808
|
-
}, [pages]);
|
|
809
|
-
return {
|
|
810
|
-
items: allItems,
|
|
811
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
812
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
813
|
-
hasMore: (lastResult?.hasMore ??
|
|
814
|
-
(currentCursor !== null && pages.length > 0)) ??
|
|
815
|
-
false,
|
|
816
|
-
loadMore,
|
|
817
|
-
refetch,
|
|
818
|
-
error: currentQuery.error,
|
|
819
|
-
};
|
|
335
|
+
const filtersKey = useFiltersKey(options);
|
|
336
|
+
const paging = useCursor(`device:${deviceId}|${filtersKey}`);
|
|
337
|
+
const query = useGetFeedInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
338
|
+
return useInfinitePages(paging, query, {
|
|
339
|
+
toPage: feedPage,
|
|
340
|
+
rowKey: feedRowKey,
|
|
341
|
+
});
|
|
820
342
|
};
|
|
821
|
-
function useAnalysisJobsInfiniteFiltersKey(options) {
|
|
822
|
-
return useMemo(() => JSON.stringify({ status: options.status ?? null }), [options.status]);
|
|
823
|
-
}
|
|
824
343
|
export const useInfiniteAnalysisJobs = (options) => {
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
344
|
+
const status = options.status ?? null;
|
|
345
|
+
const paging = useCursor(`status:${status}`);
|
|
346
|
+
const query = useGetAnalysisJobsInfiniteQuery({
|
|
347
|
+
cursor: paging.cursor,
|
|
348
|
+
status,
|
|
830
349
|
limit: options.limit || 20,
|
|
831
|
-
cursor: currentCursor,
|
|
832
350
|
supabase: options.supabase,
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
351
|
+
}, { skip: !options.enabled });
|
|
352
|
+
return useInfinitePages(paging, query, {
|
|
353
|
+
toPage: analysisJobsPage,
|
|
354
|
+
rowKey: rowId,
|
|
836
355
|
});
|
|
837
|
-
useEffect(() => {
|
|
838
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
839
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
843
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
844
|
-
setPages([]);
|
|
845
|
-
setCurrentCursor(null);
|
|
846
|
-
}
|
|
847
|
-
}, [infiniteFiltersKey]);
|
|
848
|
-
useEffect(() => {
|
|
849
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
850
|
-
setPages((prev) => {
|
|
851
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
852
|
-
(p.cursor != null &&
|
|
853
|
-
currentCursor != null &&
|
|
854
|
-
p.cursor.id === currentCursor.id));
|
|
855
|
-
if (!existingPage) {
|
|
856
|
-
return [
|
|
857
|
-
...prev,
|
|
858
|
-
{ cursor: currentCursor, data: currentQuery.data.jobs },
|
|
859
|
-
];
|
|
860
|
-
}
|
|
861
|
-
return prev;
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
865
|
-
const loadMore = useCallback(() => {
|
|
866
|
-
if (currentQuery.data?.hasMore &&
|
|
867
|
-
currentQuery.data.nextCursor &&
|
|
868
|
-
!currentQuery.isLoading) {
|
|
869
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
870
|
-
}
|
|
871
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
872
|
-
const refetch = useCallback(() => {
|
|
873
|
-
setPages([]);
|
|
874
|
-
setCurrentCursor(null);
|
|
875
|
-
currentQuery.refetch();
|
|
876
|
-
}, [currentQuery]);
|
|
877
|
-
const allItems = useMemo(() => pages.flatMap((page) => page.data), [pages]);
|
|
878
|
-
return {
|
|
879
|
-
items: allItems,
|
|
880
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
881
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
882
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
883
|
-
loadMore,
|
|
884
|
-
refetch,
|
|
885
|
-
error: currentQuery.error,
|
|
886
|
-
};
|
|
887
356
|
};
|
|
888
|
-
function useAnalysisTasksInfiniteFiltersKey(options) {
|
|
889
|
-
return useMemo(() => JSON.stringify({
|
|
890
|
-
job_id: options.job_id ?? null,
|
|
891
|
-
status: options.status ?? null,
|
|
892
|
-
}), [options.job_id, options.status]);
|
|
893
|
-
}
|
|
894
357
|
export const useInfiniteAnalysisTasks = (options) => {
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
const
|
|
899
|
-
|
|
358
|
+
const status = options.status ?? null;
|
|
359
|
+
const jobId = options.job_id ?? null;
|
|
360
|
+
const paging = useCursor(`job:${jobId}|status:${status}`);
|
|
361
|
+
const query = useGetAnalysisTasksInfiniteQuery({
|
|
362
|
+
cursor: paging.cursor,
|
|
363
|
+
status,
|
|
364
|
+
job_id: jobId,
|
|
900
365
|
limit: options.limit || 20,
|
|
901
|
-
cursor: currentCursor,
|
|
902
366
|
supabase: options.supabase,
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
367
|
+
}, { skip: !options.enabled });
|
|
368
|
+
return useInfinitePages(paging, query, {
|
|
369
|
+
toPage: analysisTasksPage,
|
|
370
|
+
rowKey: rowId,
|
|
907
371
|
});
|
|
908
|
-
useEffect(() => {
|
|
909
|
-
if (prevInfiniteFiltersKeyRef.current === null) {
|
|
910
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
if (prevInfiniteFiltersKeyRef.current !== infiniteFiltersKey) {
|
|
914
|
-
prevInfiniteFiltersKeyRef.current = infiniteFiltersKey;
|
|
915
|
-
setPages([]);
|
|
916
|
-
setCurrentCursor(null);
|
|
917
|
-
}
|
|
918
|
-
}, [infiniteFiltersKey]);
|
|
919
|
-
useEffect(() => {
|
|
920
|
-
if (currentQuery.data && !currentQuery.isLoading) {
|
|
921
|
-
setPages((prev) => {
|
|
922
|
-
const existingPage = prev.find((p) => (p.cursor === null && currentCursor === null) ||
|
|
923
|
-
(p.cursor != null &&
|
|
924
|
-
currentCursor != null &&
|
|
925
|
-
p.cursor.id === currentCursor.id));
|
|
926
|
-
if (!existingPage) {
|
|
927
|
-
return [
|
|
928
|
-
...prev,
|
|
929
|
-
{ cursor: currentCursor, data: currentQuery.data.tasks },
|
|
930
|
-
];
|
|
931
|
-
}
|
|
932
|
-
return prev;
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
}, [currentQuery.data, currentQuery.isLoading, currentCursor]);
|
|
936
|
-
const loadMore = useCallback(() => {
|
|
937
|
-
if (currentQuery.data?.hasMore &&
|
|
938
|
-
currentQuery.data.nextCursor &&
|
|
939
|
-
!currentQuery.isLoading) {
|
|
940
|
-
setCurrentCursor(currentQuery.data.nextCursor);
|
|
941
|
-
}
|
|
942
|
-
}, [currentQuery.data, currentQuery.isLoading]);
|
|
943
|
-
const refetch = useCallback(() => {
|
|
944
|
-
setPages([]);
|
|
945
|
-
setCurrentCursor(null);
|
|
946
|
-
currentQuery.refetch();
|
|
947
|
-
}, [currentQuery]);
|
|
948
|
-
const allItems = useMemo(() => pages.flatMap((page) => page.data), [pages]);
|
|
949
|
-
return {
|
|
950
|
-
items: allItems,
|
|
951
|
-
isLoading: currentQuery.isLoading && pages.length === 0,
|
|
952
|
-
isLoadingMore: currentQuery.isLoading && pages.length > 0,
|
|
953
|
-
hasMore: currentQuery.data?.hasMore || false,
|
|
954
|
-
loadMore,
|
|
955
|
-
refetch,
|
|
956
|
-
error: currentQuery.error,
|
|
957
|
-
};
|
|
958
372
|
};
|
|
959
373
|
export const useIntersectionObserver = (callback, options = {}) => {
|
|
960
374
|
const [element, setElement] = useState(null);
|