@adventurelabs/scout-core 2.0.14 → 2.0.16
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 +10 -4
- package/dist/hooks/useInfiniteQuery.js +342 -905
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/store/api.d.ts +20 -20
- package/dist/types/db.d.ts +1 -0
- package/dist/types/supabase.d.ts +59 -10
- package/package.json +1 -1
|
@@ -1,7 +1,226 @@
|
|
|
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
|
+
const emptyPaging = (key) => ({
|
|
55
|
+
key,
|
|
56
|
+
cursor: null,
|
|
57
|
+
pages: [],
|
|
58
|
+
refreshQueue: null,
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Holds the paging state for `key`, read by the query args and by the engine.
|
|
62
|
+
*
|
|
63
|
+
* State carrying a stale key reads as empty, so a new entity starts from its first page in
|
|
64
|
+
* the same render rather than waiting for an effect to clear the previous one.
|
|
65
|
+
*/
|
|
66
|
+
function usePaging(key) {
|
|
67
|
+
const [stored, setStored] = useState(() => emptyPaging(key));
|
|
68
|
+
// Memoized so the empty fallback keeps a stable identity: `items` is derived from
|
|
69
|
+
// `state.pages`, and a fresh array each render would churn consumer dependencies.
|
|
70
|
+
const state = useMemo(() => (stored.key === key ? stored : emptyPaging(key)), [key, stored]);
|
|
71
|
+
const update = useCallback((change) => setStored((previous) => change(previous.key === key ? previous : emptyPaging(key))), [key]);
|
|
72
|
+
return { cursor: state.cursor, state, update };
|
|
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 paging state's cursor decides which
|
|
101
|
+
* page is 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. Changing entity or filters changes the paging key, which empties
|
|
104
|
+
* the list and abandons any refresh in the same render.
|
|
105
|
+
*/
|
|
106
|
+
function useInfinitePages({ cursor, state, update }, query, mapping) {
|
|
107
|
+
const latest = useRef({ query, mapping });
|
|
108
|
+
latest.current = { query, mapping };
|
|
109
|
+
const setCursor = useCallback((next) => update((previous) => sameCursor(previous.cursor, next)
|
|
110
|
+
? previous
|
|
111
|
+
: { ...previous, cursor: next }), [update]);
|
|
112
|
+
/**
|
|
113
|
+
* Re-requests the page the query currently points at. RTK's `refetch` throws
|
|
114
|
+
* synchronously when the query is skipped, so callers see a rejection instead of a fault
|
|
115
|
+
* at the call site.
|
|
116
|
+
*/
|
|
117
|
+
const requestPage = useCallback(async () => latest.current.query.refetch().unwrap(), []);
|
|
118
|
+
const { currentData, isFetching } = query;
|
|
119
|
+
const page = useMemo(() => currentData === undefined
|
|
120
|
+
? undefined
|
|
121
|
+
: latest.current.mapping.toPage(currentData), [currentData]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!page)
|
|
124
|
+
return;
|
|
125
|
+
update((previous) => upsertPage(previous, { ...page, cursor }, latest.current.mapping.rowKey));
|
|
126
|
+
}, [cursor, page, update]);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const queue = state.refreshQueue;
|
|
129
|
+
if (!queue)
|
|
130
|
+
return;
|
|
131
|
+
const [refreshing] = queue;
|
|
132
|
+
if (!sameCursor(cursor, refreshing)) {
|
|
133
|
+
setCursor(refreshing);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let cancelled = false;
|
|
137
|
+
// A refresh that cannot run drops its cursor and moves on, so a rejection never leaves
|
|
138
|
+
// the queue stuck.
|
|
139
|
+
void (async () => {
|
|
140
|
+
try {
|
|
141
|
+
const response = await requestPage();
|
|
142
|
+
if (cancelled)
|
|
143
|
+
return;
|
|
144
|
+
const { mapping } = latest.current;
|
|
145
|
+
const refreshed = mapping.toPage(response);
|
|
146
|
+
update((previous) =>
|
|
147
|
+
// Only apply while this cursor is still the one being refreshed.
|
|
148
|
+
previous.refreshQueue &&
|
|
149
|
+
sameCursor(previous.refreshQueue[0], refreshing)
|
|
150
|
+
? dropRefreshedCursor(upsertPage(previous, { ...refreshed, cursor: refreshing }, mapping.rowKey))
|
|
151
|
+
: previous);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
if (!cancelled)
|
|
155
|
+
update(dropRefreshedCursor);
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
158
|
+
return () => {
|
|
159
|
+
cancelled = true;
|
|
160
|
+
};
|
|
161
|
+
}, [cursor, requestPage, setCursor, state.refreshQueue, update]);
|
|
162
|
+
/** Both modes queue the cursors to re-request, so a refetch always reaches the network. */
|
|
163
|
+
const refetch = useCallback((options) => {
|
|
164
|
+
update((previous) => options?.preservePages && previous.pages.length > 0
|
|
165
|
+
? {
|
|
166
|
+
...previous,
|
|
167
|
+
refreshQueue: previous.pages.map((loaded) => loaded.cursor),
|
|
168
|
+
}
|
|
169
|
+
: { ...emptyPaging(previous.key), refreshQueue: [null] });
|
|
170
|
+
}, [update]);
|
|
171
|
+
const lastPage = state.pages[state.pages.length - 1];
|
|
172
|
+
const nextCursor = lastPage?.nextCursor ?? null;
|
|
173
|
+
const hasMore = Boolean(lastPage?.hasMore) && nextCursor !== null;
|
|
174
|
+
const isRefreshing = state.refreshQueue !== null;
|
|
175
|
+
const loadMore = useCallback(() => {
|
|
176
|
+
if (!hasMore || nextCursor === null || isFetching || isRefreshing)
|
|
177
|
+
return;
|
|
178
|
+
// The cursor already resting on `nextCursor` means that page's request failed, since a
|
|
179
|
+
// loaded page would have moved `nextCursor` on. Re-setting it would be a no-op, so the
|
|
180
|
+
// failed request is retried instead of leaving the list unable to advance.
|
|
181
|
+
if (sameCursor(cursor, nextCursor)) {
|
|
182
|
+
void requestPage().catch(() => { });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
setCursor(nextCursor);
|
|
186
|
+
}, [
|
|
187
|
+
cursor,
|
|
188
|
+
hasMore,
|
|
189
|
+
isFetching,
|
|
190
|
+
isRefreshing,
|
|
191
|
+
nextCursor,
|
|
192
|
+
requestPage,
|
|
193
|
+
setCursor,
|
|
194
|
+
]);
|
|
195
|
+
const items = useMemo(() => {
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
return state.pages
|
|
198
|
+
.flatMap((loaded) => loaded.rows)
|
|
199
|
+
.filter((row) => {
|
|
200
|
+
const rowIdentity = latest.current.mapping.rowKey(row);
|
|
201
|
+
if (rowIdentity == null)
|
|
202
|
+
return true;
|
|
203
|
+
if (seen.has(rowIdentity))
|
|
204
|
+
return false;
|
|
205
|
+
seen.add(rowIdentity);
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
}, [state.pages]);
|
|
209
|
+
return {
|
|
210
|
+
items,
|
|
211
|
+
isLoading: state.pages.length === 0 && (isFetching || isRefreshing),
|
|
212
|
+
isRefreshing,
|
|
213
|
+
isLoadingMore: isFetching && state.pages.length > 0 && !isRefreshing,
|
|
214
|
+
hasMore,
|
|
215
|
+
loadMore,
|
|
216
|
+
refetch,
|
|
217
|
+
error: query.error,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
// =====================================================
|
|
221
|
+
// SHARED QUERY ARGS AND RESPONSE MAPPING
|
|
222
|
+
// =====================================================
|
|
223
|
+
function useFiltersKey(options) {
|
|
5
224
|
return useMemo(() => JSON.stringify({
|
|
6
225
|
rangeStart: options.rangeStart ?? null,
|
|
7
226
|
rangeEnd: options.rangeEnd ?? null,
|
|
@@ -14,947 +233,165 @@ function useInfiniteFiltersKey(options) {
|
|
|
14
233
|
options.minFlightDistanceMeters,
|
|
15
234
|
]);
|
|
16
235
|
}
|
|
236
|
+
const rangeArgs = (options) => ({
|
|
237
|
+
limit: options.limit || 20,
|
|
238
|
+
supabase: options.supabase,
|
|
239
|
+
rangeStart: options.rangeStart ?? null,
|
|
240
|
+
rangeEnd: options.rangeEnd ?? null,
|
|
241
|
+
});
|
|
242
|
+
const flightArgs = (options) => ({
|
|
243
|
+
...rangeArgs(options),
|
|
244
|
+
minFlightTimeMinutes: options.minFlightTimeMinutes ?? null,
|
|
245
|
+
minFlightDistanceMeters: options.minFlightDistanceMeters ?? null,
|
|
246
|
+
});
|
|
247
|
+
const sessionsPage = (response) => ({
|
|
248
|
+
rows: response.sessions,
|
|
249
|
+
nextCursor: response.nextCursor,
|
|
250
|
+
hasMore: response.hasMore,
|
|
251
|
+
});
|
|
252
|
+
const eventsPage = (response) => ({
|
|
253
|
+
rows: response.events,
|
|
254
|
+
nextCursor: response.nextCursor,
|
|
255
|
+
hasMore: response.hasMore,
|
|
256
|
+
});
|
|
257
|
+
const artifactsPage = (response) => ({
|
|
258
|
+
rows: response.artifacts,
|
|
259
|
+
nextCursor: response.nextCursor,
|
|
260
|
+
hasMore: response.hasMore,
|
|
261
|
+
});
|
|
262
|
+
const feedPage = (response) => {
|
|
263
|
+
const rows = Array.isArray(response.items) ? response.items : [];
|
|
264
|
+
return {
|
|
265
|
+
rows,
|
|
266
|
+
nextCursor: response.nextCursor,
|
|
267
|
+
// The feed RPC can return fewer rows than the limit while more still exist, so any
|
|
268
|
+
// non-empty page with a next cursor means there is more to load.
|
|
269
|
+
hasMore: rows.length > 0 && response.nextCursor !== null,
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
const analysisJobsPage = (response) => ({
|
|
273
|
+
rows: response.jobs,
|
|
274
|
+
nextCursor: response.nextCursor,
|
|
275
|
+
hasMore: response.hasMore,
|
|
276
|
+
});
|
|
277
|
+
const analysisTasksPage = (response) => ({
|
|
278
|
+
rows: response.tasks,
|
|
279
|
+
nextCursor: response.nextCursor,
|
|
280
|
+
hasMore: response.hasMore,
|
|
281
|
+
});
|
|
17
282
|
// =====================================================
|
|
18
|
-
// SESSIONS
|
|
283
|
+
// SESSIONS
|
|
19
284
|
// =====================================================
|
|
20
285
|
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,
|
|
286
|
+
const filtersKey = useFiltersKey(options);
|
|
287
|
+
const paging = usePaging(`herd:${herdId}|${filtersKey}`);
|
|
288
|
+
const query = useGetSessionsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...flightArgs(options) }, { skip: !options.enabled || !herdId });
|
|
289
|
+
return useInfinitePages(paging, query, {
|
|
290
|
+
toPage: sessionsPage,
|
|
291
|
+
rowKey: rowId,
|
|
37
292
|
});
|
|
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
293
|
};
|
|
105
294
|
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,
|
|
295
|
+
const filtersKey = useFiltersKey(options);
|
|
296
|
+
const paging = usePaging(`device:${deviceId}|${filtersKey}`);
|
|
297
|
+
const query = useGetSessionsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...flightArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
298
|
+
return useInfinitePages(paging, query, {
|
|
299
|
+
toPage: sessionsPage,
|
|
300
|
+
rowKey: rowId,
|
|
122
301
|
});
|
|
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
302
|
};
|
|
188
303
|
// =====================================================
|
|
189
|
-
// EVENTS
|
|
304
|
+
// EVENTS
|
|
190
305
|
// =====================================================
|
|
191
306
|
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,
|
|
307
|
+
const filtersKey = useFiltersKey(options);
|
|
308
|
+
const paging = usePaging(`herd:${herdId}|${filtersKey}`);
|
|
309
|
+
const query = useGetEventsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
310
|
+
return useInfinitePages(paging, query, {
|
|
311
|
+
toPage: eventsPage,
|
|
312
|
+
rowKey: rowId,
|
|
206
313
|
});
|
|
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
314
|
};
|
|
272
315
|
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,
|
|
316
|
+
const filtersKey = useFiltersKey(options);
|
|
317
|
+
const paging = usePaging(`device:${deviceId}|${filtersKey}`);
|
|
318
|
+
const query = useGetEventsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
319
|
+
return useInfinitePages(paging, query, {
|
|
320
|
+
toPage: eventsPage,
|
|
321
|
+
rowKey: rowId,
|
|
287
322
|
});
|
|
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
323
|
};
|
|
353
324
|
// =====================================================
|
|
354
|
-
// ARTIFACTS
|
|
325
|
+
// ARTIFACTS
|
|
355
326
|
// =====================================================
|
|
356
327
|
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,
|
|
328
|
+
const filtersKey = useFiltersKey(options);
|
|
329
|
+
const paging = usePaging(`herd:${herdId}|${filtersKey}`);
|
|
330
|
+
const query = useGetArtifactsInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
331
|
+
return useInfinitePages(paging, query, {
|
|
332
|
+
toPage: artifactsPage,
|
|
333
|
+
rowKey: rowId,
|
|
371
334
|
});
|
|
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
335
|
};
|
|
437
336
|
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,
|
|
337
|
+
const filtersKey = useFiltersKey(options);
|
|
338
|
+
const paging = usePaging(`device:${deviceId}|${filtersKey}`);
|
|
339
|
+
const query = useGetArtifactsInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
340
|
+
return useInfinitePages(paging, query, {
|
|
341
|
+
toPage: artifactsPage,
|
|
342
|
+
rowKey: rowId,
|
|
452
343
|
});
|
|
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
344
|
};
|
|
527
|
-
|
|
345
|
+
// =====================================================
|
|
346
|
+
// FEED (merged events + artifacts)
|
|
347
|
+
// =====================================================
|
|
528
348
|
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
|
-
};
|
|
349
|
+
const filtersKey = useFiltersKey(options);
|
|
350
|
+
const paging = usePaging(`herd:${herdId}|${filtersKey}`);
|
|
351
|
+
const query = useGetFeedInfiniteByHerdQuery({ herdId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !herdId });
|
|
352
|
+
return useInfinitePages(paging, query, {
|
|
353
|
+
toPage: feedPage,
|
|
354
|
+
rowKey: feedRowKey,
|
|
355
|
+
});
|
|
692
356
|
};
|
|
693
357
|
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
|
-
};
|
|
358
|
+
const filtersKey = useFiltersKey(options);
|
|
359
|
+
const paging = usePaging(`device:${deviceId}|${filtersKey}`);
|
|
360
|
+
const query = useGetFeedInfiniteByDeviceQuery({ deviceId, cursor: paging.cursor, ...rangeArgs(options) }, { skip: !options.enabled || !deviceId });
|
|
361
|
+
return useInfinitePages(paging, query, {
|
|
362
|
+
toPage: feedPage,
|
|
363
|
+
rowKey: feedRowKey,
|
|
364
|
+
});
|
|
820
365
|
};
|
|
821
|
-
function useAnalysisJobsInfiniteFiltersKey(options) {
|
|
822
|
-
return useMemo(() => JSON.stringify({ status: options.status ?? null }), [options.status]);
|
|
823
|
-
}
|
|
824
366
|
export const useInfiniteAnalysisJobs = (options) => {
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
367
|
+
const status = options.status ?? null;
|
|
368
|
+
const paging = usePaging(`status:${status}`);
|
|
369
|
+
const query = useGetAnalysisJobsInfiniteQuery({
|
|
370
|
+
cursor: paging.cursor,
|
|
371
|
+
status,
|
|
830
372
|
limit: options.limit || 20,
|
|
831
|
-
cursor: currentCursor,
|
|
832
373
|
supabase: options.supabase,
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
374
|
+
}, { skip: !options.enabled });
|
|
375
|
+
return useInfinitePages(paging, query, {
|
|
376
|
+
toPage: analysisJobsPage,
|
|
377
|
+
rowKey: rowId,
|
|
836
378
|
});
|
|
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
379
|
};
|
|
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
380
|
export const useInfiniteAnalysisTasks = (options) => {
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
const
|
|
899
|
-
|
|
381
|
+
const status = options.status ?? null;
|
|
382
|
+
const jobId = options.job_id ?? null;
|
|
383
|
+
const paging = usePaging(`job:${jobId}|status:${status}`);
|
|
384
|
+
const query = useGetAnalysisTasksInfiniteQuery({
|
|
385
|
+
cursor: paging.cursor,
|
|
386
|
+
status,
|
|
387
|
+
job_id: jobId,
|
|
900
388
|
limit: options.limit || 20,
|
|
901
|
-
cursor: currentCursor,
|
|
902
389
|
supabase: options.supabase,
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
390
|
+
}, { skip: !options.enabled });
|
|
391
|
+
return useInfinitePages(paging, query, {
|
|
392
|
+
toPage: analysisTasksPage,
|
|
393
|
+
rowKey: rowId,
|
|
907
394
|
});
|
|
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
395
|
};
|
|
959
396
|
export const useIntersectionObserver = (callback, options = {}) => {
|
|
960
397
|
const [element, setElement] = useState(null);
|