@adventurelabs/scout-core 1.4.28 → 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,93 +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
- // Defer lastResult to next tick so consumer's IntersectionObserver is recreated
461
- // after hasMore is true and its callback can fire (sentinel may already be in view).
462
- const nextResult = {
463
- hasMore: currentQuery.data.hasMore ?? false,
464
- nextCursor: currentQuery.data.nextCursor ?? null,
465
- };
466
- queueMicrotask(() => 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
+ }
467
484
  setPages((prev) => {
468
- const existingPage = prev.find((p) => feedCursorEq(p.cursor, currentCursor));
469
- if (!existingPage) {
470
- if (items.length >= limit) {
471
- lastAddedCursorRef.current = currentCursor;
472
- }
473
- return [
474
- ...prev,
475
- { cursor: currentCursor, data: items },
476
- ];
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;
477
493
  }
478
- return prev;
494
+ if (existingPage &&
495
+ items.length > existingPage.data.length &&
496
+ items.length >= limit) {
497
+ lastAddedCursorRef.current = cursor;
498
+ }
499
+ return next;
479
500
  });
480
- }, [currentQuery.data, currentQuery.isLoading, currentCursor, pages.length, options.limit]);
501
+ }, [
502
+ currentQuery.data,
503
+ currentQuery.isLoading,
504
+ currentCursor,
505
+ pages.length,
506
+ limit,
507
+ ]);
481
508
  const loadMore = useCallback(() => {
482
- if (lastResult?.hasMore &&
483
- lastResult.nextCursor != null &&
484
- !currentQuery.isLoading) {
485
- setCurrentCursor(lastResult.nextCursor);
509
+ if (currentResult?.hasMore &&
510
+ currentResult.nextCursor != null &&
511
+ !isLoading) {
512
+ setCurrentCursor(currentResult.nextCursor);
486
513
  }
487
- }, [lastResult, currentQuery.isLoading]);
514
+ }, [currentResult, isLoading]);
488
515
  const refetch = useCallback(() => {
489
516
  setPages([]);
490
517
  setCurrentCursor(null);
518
+ setCurrentResult(null);
491
519
  lastAddedCursorRef.current = undefined;
492
- setLastResult(null);
493
520
  currentQuery.refetch();
494
521
  }, [currentQuery]);
495
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
+ });
496
534
  const seen = new Set();
497
- return pages.flatMap((p) => p.data).filter((item) => {
535
+ return sorted.flatMap((p) => p.data).filter((item) => {
498
536
  const key = `${item.sort_ts ?? ''}_${item.sort_id ?? ''}_${item.feed_type ?? ''}`;
499
537
  if (seen.has(key))
500
538
  return false;
@@ -504,10 +542,10 @@ export const useInfiniteFeedByHerd = (herdId, options) => {
504
542
  }, [pages]);
505
543
  return {
506
544
  items: allItems,
507
- isLoading: currentQuery.isLoading && pages.length === 0,
508
- isLoadingMore: currentQuery.isLoading && pages.length > 0,
509
- hasMore: (lastResult?.hasMore ??
510
- (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) ??
511
549
  false,
512
550
  loadMore,
513
551
  refetch,
@@ -555,7 +593,7 @@ export const useInfiniteFeedByDevice = (deviceId, options) => {
555
593
  hasMore: currentQuery.data?.hasMore ?? false,
556
594
  nextCursor: currentQuery.data?.nextCursor ?? null,
557
595
  };
558
- queueMicrotask(() => setLastResult(nextResult));
596
+ setLastResult(nextResult);
559
597
  setPages((prev) => {
560
598
  const existingPage = prev.find((p) => feedCursorEq(p.cursor, currentCursor));
561
599
  if (!existingPage) {
@@ -573,6 +611,9 @@ export const useInfiniteFeedByDevice = (deviceId, options) => {
573
611
  }
574
612
  return prev;
575
613
  });
614
+ if (nextResult.hasMore && nextResult.nextCursor != null) {
615
+ setCurrentCursor(nextResult.nextCursor);
616
+ }
576
617
  }, [currentQuery.data, currentQuery.isLoading, currentCursor, pages.length, options.limit]);
577
618
  const loadMore = useCallback(() => {
578
619
  if (lastResult?.hasMore &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.4.28",
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",