@adventurelabs/scout-core 1.4.31 → 1.4.32

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.
@@ -20,6 +20,7 @@ export declare const useInfiniteEventsByHerd: (herdId: number, options: UseInfin
20
20
  export declare const useInfiniteEventsByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IEventAndTagsPrettyLocation>;
21
21
  export declare const useInfiniteArtifactsByHerd: (herdId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IArtifactWithMediaUrl>;
22
22
  export declare const useInfiniteArtifactsByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IArtifactWithMediaUrl>;
23
+ /** useInfiniteFeedByHerd: logic matches useInfiniteFeedByHerdDummy verbatim; only the fetch is via API (RTK Query) instead of supabase.rpc. */
23
24
  export declare const useInfiniteFeedByHerd: (herdId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IFeedItem>;
24
25
  export declare const useInfiniteFeedByDevice: (deviceId: number, options: UseInfiniteScrollOptions) => InfiniteScrollData<IFeedItem>;
25
26
  export declare const useIntersectionObserver: (callback: () => void, options?: IntersectionObserverInit) => import("react").Dispatch<import("react").SetStateAction<Element | null>>;
@@ -408,96 +408,131 @@ export const useInfiniteArtifactsByDevice = (deviceId, options) => {
408
408
  };
409
409
  };
410
410
  const feedCursorEq = (a, b) => {
411
- if (a === null && b === null)
411
+ if (a === b)
412
412
  return true;
413
- if (!a || !b)
413
+ if (a == null || b == null)
414
414
  return false;
415
- return a.timestamp === b.timestamp && a.id === b.id && a.feed_type === b.feed_type;
415
+ return (a.timestamp === b.timestamp &&
416
+ a.id === b.id &&
417
+ a.feed_type === b.feed_type);
416
418
  };
419
+ /** useInfiniteFeedByHerd: logic matches useInfiniteFeedByHerdDummy verbatim; only the fetch is via API (RTK Query) instead of supabase.rpc. */
417
420
  export const useInfiniteFeedByHerd = (herdId, options) => {
421
+ const limit = options.limit ?? 20;
422
+ const enabled = !!(options.enabled && herdId);
418
423
  const [pages, setPages] = useState([]);
419
424
  const [currentCursor, setCurrentCursor] = useState(null);
420
- // Match dummy: use state so hasMore/nextCursor trigger re-renders and loadMore sees latest (ref was not updating UI)
421
- const [lastResult, setLastResult] = useState(null);
425
+ const [currentResult, setCurrentResult] = useState(null);
422
426
  const prevHerdIdRef = useRef(undefined);
423
427
  const lastAddedCursorRef = useRef(undefined);
424
428
  const pagesLengthRef = useRef(0);
425
429
  const currentQuery = useGetFeedInfiniteByHerdQuery({
426
430
  herdId,
427
- limit: options.limit || 20,
431
+ limit,
428
432
  cursor: currentCursor,
429
433
  supabase: options.supabase,
430
- }, { skip: !options.enabled || !herdId });
434
+ }, { skip: !enabled });
435
+ const isLoading = currentQuery.isLoading;
431
436
  useEffect(() => {
432
437
  pagesLengthRef.current = pages.length;
433
438
  }, [pages.length]);
434
- // Clear all state whenever herd id changes (including to/from undefined)
439
+ // Reset when herd changes (match dummy: prev !== herdId && enabled && herdId)
435
440
  useEffect(() => {
436
441
  if (prevHerdIdRef.current !== undefined &&
437
- prevHerdIdRef.current !== herdId) {
442
+ prevHerdIdRef.current !== herdId &&
443
+ enabled &&
444
+ herdId) {
438
445
  setPages([]);
439
446
  setCurrentCursor(null);
447
+ setCurrentResult(null);
440
448
  lastAddedCursorRef.current = undefined;
441
- setLastResult(null);
442
449
  }
443
450
  prevHerdIdRef.current = herdId;
444
- }, [herdId]);
445
- // When we request a new page (cursor changed), clear ref so we merge the new response
451
+ }, [herdId, enabled]);
452
+ // When cursor changes, clear ref so we merge the new response
446
453
  useEffect(() => {
447
454
  lastAddedCursorRef.current = undefined;
448
455
  }, [currentCursor]);
456
+ // Merge when we have data (mirror dummy's .then() logic; fetch is done by RTK Query)
449
457
  useEffect(() => {
450
458
  if (!currentQuery.data || currentQuery.isLoading)
451
459
  return;
452
- // Match dummy: only skip merge when we already have pages and this cursor was already added as a full page
453
- if (pagesLengthRef.current > 0 &&
454
- feedCursorEq(lastAddedCursorRef.current ?? null, currentCursor))
455
- return;
456
- const items = Array.isArray(currentQuery.data?.items)
460
+ const cursor = currentCursor;
461
+ const items = Array.isArray(currentQuery.data.items)
457
462
  ? currentQuery.data.items
458
463
  : [];
459
- const limit = options.limit || 20;
460
- const nextResult = {
461
- hasMore: currentQuery.data.hasMore ?? false,
462
- nextCursor: currentQuery.data.nextCursor ?? null,
463
- };
464
- setLastResult(nextResult);
464
+ const hasMore = currentQuery.data.hasMore ?? false;
465
+ const nextCursor = currentQuery.data.nextCursor ?? null;
466
+ // Only update currentResult for successful response (match dummy)
467
+ if (items.length === 0 && cursor === null) {
468
+ // Leave currentResult unchanged on spurious empty first page
469
+ }
470
+ else {
471
+ setCurrentResult({
472
+ hasMore: hasMore && nextCursor != null,
473
+ nextCursor,
474
+ });
475
+ }
476
+ if (items.length === 0)
477
+ return;
478
+ // Skip merge exactly like dummy: full page already added for this cursor and we have pages
479
+ if (items.length >= limit &&
480
+ feedCursorEq(lastAddedCursorRef.current ?? null, cursor) &&
481
+ pagesLengthRef.current > 0) {
482
+ return;
483
+ }
465
484
  setPages((prev) => {
466
- const existingPage = prev.find((p) => feedCursorEq(p.cursor, currentCursor));
467
- if (!existingPage) {
468
- if (items.length >= limit) {
469
- lastAddedCursorRef.current = currentCursor;
470
- }
471
- return [
472
- ...prev,
473
- { cursor: currentCursor, data: items },
474
- ];
485
+ const existingPage = prev.find((p) => feedCursorEq(p.cursor, cursor));
486
+ const next = !existingPage
487
+ ? [...prev, { cursor, data: items }]
488
+ : items.length > existingPage.data.length
489
+ ? prev.map((p) => feedCursorEq(p.cursor, cursor) ? { cursor, data: items } : p)
490
+ : prev;
491
+ if (!existingPage && items.length >= limit) {
492
+ lastAddedCursorRef.current = cursor;
475
493
  }
476
- return prev;
494
+ if (existingPage &&
495
+ items.length > existingPage.data.length &&
496
+ items.length >= limit) {
497
+ lastAddedCursorRef.current = cursor;
498
+ }
499
+ return next;
477
500
  });
478
- // Request next page when this response has more (avoids relying on consumer's
479
- // IntersectionObserver when sentinel is already in view and callback doesn't re-fire).
480
- if (nextResult.hasMore && nextResult.nextCursor != null) {
481
- setCurrentCursor(nextResult.nextCursor);
482
- }
483
- }, [currentQuery.data, currentQuery.isLoading, currentCursor, pages.length, options.limit]);
501
+ }, [
502
+ currentQuery.data,
503
+ currentQuery.isLoading,
504
+ currentCursor,
505
+ pages.length,
506
+ limit,
507
+ ]);
484
508
  const loadMore = useCallback(() => {
485
- if (lastResult?.hasMore &&
486
- lastResult.nextCursor != null &&
487
- !currentQuery.isLoading) {
488
- setCurrentCursor(lastResult.nextCursor);
509
+ if (currentResult?.hasMore &&
510
+ currentResult.nextCursor != null &&
511
+ !isLoading) {
512
+ setCurrentCursor(currentResult.nextCursor);
489
513
  }
490
- }, [lastResult, currentQuery.isLoading]);
514
+ }, [currentResult, isLoading]);
491
515
  const refetch = useCallback(() => {
492
516
  setPages([]);
493
517
  setCurrentCursor(null);
518
+ setCurrentResult(null);
494
519
  lastAddedCursorRef.current = undefined;
495
- setLastResult(null);
496
520
  currentQuery.refetch();
497
521
  }, [currentQuery]);
498
522
  const allItems = useMemo(() => {
523
+ const sorted = [...pages].sort((a, b) => {
524
+ if (feedCursorEq(a.cursor, b.cursor))
525
+ return 0;
526
+ if (a.cursor === null)
527
+ return -1;
528
+ if (b.cursor === null)
529
+ return 1;
530
+ const ta = a.cursor.timestamp ?? '';
531
+ const tb = b.cursor.timestamp ?? '';
532
+ return tb.localeCompare(ta);
533
+ });
499
534
  const seen = new Set();
500
- return pages.flatMap((p) => p.data).filter((item) => {
535
+ return sorted.flatMap((p) => p.data).filter((item) => {
501
536
  const key = `${item.sort_ts ?? ''}_${item.sort_id ?? ''}_${item.feed_type ?? ''}`;
502
537
  if (seen.has(key))
503
538
  return false;
@@ -507,10 +542,10 @@ export const useInfiniteFeedByHerd = (herdId, options) => {
507
542
  }, [pages]);
508
543
  return {
509
544
  items: allItems,
510
- isLoading: currentQuery.isLoading && pages.length === 0,
511
- isLoadingMore: currentQuery.isLoading && pages.length > 0,
512
- hasMore: (lastResult?.hasMore ??
513
- (currentCursor !== null && pages.length > 0)) ??
545
+ isLoading: isLoading && pages.length === 0,
546
+ isLoadingMore: isLoading && pages.length > 0,
547
+ hasMore: currentResult?.hasMore ??
548
+ (currentCursor !== null && pages.length > 0) ??
514
549
  false,
515
550
  loadMore,
516
551
  refetch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.4.31",
3
+ "version": "1.4.32",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",