{"version":3,"file":"useInfiniteScroll.mjs","names":[],"sources":["../../../../package/common/infiniteBlock/useInfiniteScroll.ts"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport type React from 'react';\n\nconst RETRY_DELAY = 3000;\n// How far ahead of the end a load starts, so that scrolling at a normal pace rarely reaches the\n// spinner. The same as VirtualList's default `loadMoreThreshold`.\nconst LOAD_AHEAD = '100px 0px';\n\n// Loads more items once a sentinel element at the end of the list comes near.\n// IntersectionObserver only fires on a change of intersection state, so if a page of\n// freshly-loaded items still doesn't fill the container, the sentinel stays in range and\n// we have to re-arm the observer ourselves to pick up the next page.\n// Without `onLoadMore` it watches nothing. Passing it only once the container and sentinel are\n// rendered is what starts the watching: an observer set up before then never would.\nexport function useInfiniteScroll(\n  containerRef: React.RefObject<HTMLDivElement | null>,\n  onLoadMore: (() => Promise<void>) | undefined,\n) {\n  const [isLoading, setIsLoading] = useState(false);\n  const isLoadingRef = useRef(false);\n  const sentinelRef = useRef<HTMLDivElement>(null);\n  // The observer of the current effect, so that a load which outlives its own still re-arms this one.\n  const rearmRef = useRef(() => {});\n  const retryRef = useRef(0);\n  // Elements beside the sentinel when the last load started, until the list holds a different number.\n  const loadedFromRef = useRef<number | null>(null);\n\n  // Kept in a ref so a caller passing a fresh onLoadMore function each render doesn't\n  // tear down and recreate the observer every time.\n  const onLoadMoreRef = useRef(onLoadMore);\n  useEffect(() => {\n    onLoadMoreRef.current = onLoadMore;\n  });\n  const isEnabled = onLoadMore !== undefined;\n\n  // A load re-arms only once the list has changed, so one that added nothing ends the asking. Not\n  // when it settles: its items often render later, a React Query page a whole task later. Counted\n  // in the DOM rather than from the caller's items, where a Fragment of messages counts as one.\n  useLayoutEffect(() => {\n    const from = loadedFromRef.current;\n    if (isLoading || from === null) return;\n    if (sentinelRef.current?.parentElement?.childElementCount === from) return;\n    loadedFromRef.current = null;\n    rearmRef.current();\n  });\n\n  useEffect(() => {\n    const root = containerRef.current;\n    const sentinel = sentinelRef.current;\n    if (!isEnabled || !root || !sentinel) return undefined;\n\n    // Observing again reports the sentinel at once, for a page that still left it in range.\n    const rearm = () => {\n      observer.unobserve(sentinel);\n      observer.observe(sentinel);\n    };\n\n    const loadMore = () => {\n      const load = onLoadMoreRef.current;\n      if (!load || isLoadingRef.current) return;\n      isLoadingRef.current = true;\n      loadedFromRef.current = sentinel.parentElement?.childElementCount ?? null;\n      setIsLoading(true);\n\n      load().then(\n        () => {\n          isLoadingRef.current = false;\n          setIsLoading(false);\n        },\n        () => {\n          isLoadingRef.current = false;\n          loadedFromRef.current = null;\n          setIsLoading(false);\n          // A failure waits before trying again, or it would retry without end while the sentinel\n          // stays in range. It does try again: a list too short to scroll offers no other way.\n          retryRef.current = window.setTimeout(() => rearmRef.current(), RETRY_DELAY);\n        },\n      );\n    };\n\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) loadMore();\n      },\n      { root, rootMargin: LOAD_AHEAD },\n    );\n\n    observer.observe(sentinel);\n    rearmRef.current = rearm;\n\n    return () => {\n      window.clearTimeout(retryRef.current);\n      observer.disconnect();\n      rearmRef.current = () => {};\n    };\n  }, [containerRef, isEnabled]);\n\n  return { isLoading, sentinelRef };\n}\n"],"mappings":"8FAGA,IAAM,EAAc,IAGd,EAAa,YAQnB,SAAgB,EACd,EACA,EACA,CACA,GAAM,CAAC,EAAW,GAAgB,EAAS,EAAK,EAC1C,EAAe,EAAO,EAAK,EAC3B,EAAc,EAAuB,IAAI,EAEzC,EAAW,MAAa,CAAC,CAAC,EAC1B,EAAW,EAAO,CAAC,EAEnB,EAAgB,EAAsB,IAAI,EAI1C,EAAgB,EAAO,CAAU,EACvC,MAAgB,CACd,EAAc,QAAU,CAC1B,CAAC,EACD,IAAM,EAAY,IAAe,IAAA,GAgEjC,OA3DA,MAAsB,CACpB,IAAM,EAAO,EAAc,QACvB,GAAa,IAAS,MACtB,EAAY,SAAS,eAAe,oBAAsB,IAC9D,EAAc,QAAU,KACxB,EAAS,QAAQ,EACnB,CAAC,EAED,MAAgB,CACd,IAAM,EAAO,EAAa,QACpB,EAAW,EAAY,QAC7B,GAAI,CAAC,GAAa,CAAC,GAAQ,CAAC,EAAU,OAGtC,IAAM,MAAc,CAClB,EAAS,UAAU,CAAQ,EAC3B,EAAS,QAAQ,CAAQ,CAC3B,EAEM,MAAiB,CACrB,IAAM,EAAO,EAAc,QACtB,GAAQ,GAAa,UAC1B,EAAa,QAAU,GACvB,EAAc,QAAU,EAAS,eAAe,mBAAqB,KACrE,EAAa,EAAI,EAEjB,EAAK,CAAC,CAAC,SACC,CACJ,EAAa,QAAU,GACvB,EAAa,EAAK,CACpB,MACM,CACJ,EAAa,QAAU,GACvB,EAAc,QAAU,KACxB,EAAa,EAAK,EAGlB,EAAS,QAAU,OAAO,eAAiB,EAAS,QAAQ,EAAG,CAAW,CAC5E,CACF,EACF,EAEM,EAAW,IAAI,sBAClB,CAAC,KAAW,CACP,EAAM,gBAAgB,EAAS,CACrC,EACA,CAAE,OAAM,WAAY,CAAW,CACjC,EAKA,OAHA,EAAS,QAAQ,CAAQ,EACzB,EAAS,QAAU,MAEN,CACX,OAAO,aAAa,EAAS,OAAO,EACpC,EAAS,WAAW,EACpB,EAAS,YAAgB,CAAC,CAC5B,CACF,EAAG,CAAC,EAAc,CAAS,CAAC,EAErB,CAAE,YAAW,aAAY,CAClC"}