cogsbox-state 0.5.359 → 0.5.361

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-state",
3
- "version": "0.5.359",
3
+ "version": "0.5.361",
4
4
  "description": "React state management library with form controls and server sync",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/CogsState.tsx CHANGED
@@ -1808,16 +1808,15 @@ function createProxyHandler<T>(
1808
1808
  stickToBottom = false,
1809
1809
  dependencies = [],
1810
1810
  } = options;
1811
- const [instanceKey, setInstanceKey] = useState(0);
1811
+
1812
1812
  const containerRef = useRef<HTMLDivElement | null>(null);
1813
1813
  const [range, setRange] = useState({
1814
1814
  startIndex: 0,
1815
1815
  endIndex: 10,
1816
1816
  });
1817
1817
  const isLockedToBottomRef = useRef(stickToBottom);
1818
-
1819
- // This ref will hold the ID of our loop so we can stop it.
1820
- const scrollLoopId = useRef<NodeJS.Timeout | null>(null);
1818
+ const isAutoScrolling = useRef(false);
1819
+ const prevTotalCountRef = useRef(0);
1821
1820
 
1822
1821
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1823
1822
 
@@ -1858,7 +1857,6 @@ function createProxyHandler<T>(
1858
1857
  ]);
1859
1858
 
1860
1859
  const virtualState = useMemo(() => {
1861
- // ... same as before ...
1862
1860
  const start = Math.max(0, range.startIndex);
1863
1861
  const end = Math.min(totalCount, range.endIndex);
1864
1862
  const validIndices = Array.from(
@@ -1872,46 +1870,44 @@ function createProxyHandler<T>(
1872
1870
  });
1873
1871
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1874
1872
 
1875
- // --- EFFECT #1: THE SCROLL ALGORITHM ---
1876
- // This runs when the data or dependencies change.
1873
+ // --- PHASE 1: Detect auto-scroll need and SET THE RANGE ---
1874
+ useLayoutEffect(() => {
1875
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1876
+ if (isLockedToBottomRef.current && hasNewItems) {
1877
+ console.log(
1878
+ "PHASE 1: Auto-scroll needed. Setting range to render the last item."
1879
+ );
1880
+ setRange({
1881
+ startIndex: Math.max(0, totalCount - 10 - overscan),
1882
+ endIndex: totalCount,
1883
+ });
1884
+ }
1885
+ prevTotalCountRef.current = totalCount;
1886
+ }, [totalCount]);
1887
+
1888
+ // --- PHASE 2: Wait for measurement and SCROLL ---
1877
1889
  useLayoutEffect(() => {
1878
1890
  const container = containerRef.current;
1891
+ const isRangeAtEnd =
1892
+ range.endIndex === totalCount && totalCount > 0;
1893
+
1879
1894
  if (
1880
1895
  !container ||
1881
- !stickToBottom ||
1882
- !isLockedToBottomRef.current
1896
+ !isLockedToBottomRef.current ||
1897
+ !isRangeAtEnd
1883
1898
  ) {
1884
1899
  return;
1885
1900
  }
1886
1901
 
1887
- // If a loop is already running, stop it before starting a new one.
1888
- if (scrollLoopId.current) {
1889
- clearInterval(scrollLoopId.current);
1890
- }
1891
-
1892
- console.log("ALGORITHM: Starting...");
1893
-
1894
- // STEP 1: Set range to render the last item.
1895
- setRange({
1896
- startIndex: Math.max(0, totalCount - 10 - overscan),
1897
- endIndex: totalCount,
1898
- });
1899
-
1900
- // STEP 2: Start the LOOP.
1901
1902
  console.log(
1902
- "ALGORITHM: Starting LOOP to wait for measurement."
1903
+ "PHASE 2: Range is at the end. Starting the measurement loop."
1903
1904
  );
1904
1905
  let loopCount = 0;
1905
- scrollLoopId.current = setInterval(() => {
1906
+ const intervalId = setInterval(() => {
1906
1907
  loopCount++;
1907
1908
  console.log(`LOOP ${loopCount}: Checking last item...`);
1908
1909
 
1909
1910
  const lastItemIndex = totalCount - 1;
1910
- if (lastItemIndex < 0) {
1911
- clearInterval(scrollLoopId.current!);
1912
- return;
1913
- }
1914
-
1915
1911
  const shadowArray =
1916
1912
  getGlobalStore
1917
1913
  .getState()
@@ -1921,67 +1917,75 @@ function createProxyHandler<T>(
1921
1917
 
1922
1918
  if (lastItemHeight > 0) {
1923
1919
  console.log(
1924
- `%cSUCCESS: Last item is measured. Scrolling.`,
1920
+ `%cSUCCESS: Last item height is ${lastItemHeight}. Scrolling now.`,
1925
1921
  "color: green; font-weight: bold;"
1926
1922
  );
1927
- clearInterval(scrollLoopId.current!);
1928
- scrollLoopId.current = null;
1923
+ clearInterval(intervalId);
1929
1924
 
1925
+ isAutoScrolling.current = true;
1930
1926
  container.scrollTo({
1931
1927
  top: container.scrollHeight,
1932
1928
  behavior: "smooth",
1933
1929
  });
1930
+ setTimeout(() => {
1931
+ isAutoScrolling.current = false;
1932
+ }, 1000);
1933
+ } else if (loopCount > 20) {
1934
+ console.error(
1935
+ "LOOP TIMEOUT: Last item was never measured. Stopping loop."
1936
+ );
1937
+ clearInterval(intervalId);
1934
1938
  } else {
1935
1939
  console.log("...WAITING. Height is not ready.");
1936
- if (loopCount > 30) {
1937
- console.error("LOOP TIMEOUT. Stopping.");
1938
- clearInterval(scrollLoopId.current!);
1939
- scrollLoopId.current = null;
1940
- }
1941
1940
  }
1942
1941
  }, 100);
1943
1942
 
1944
- return () => {
1945
- if (scrollLoopId.current) {
1946
- clearInterval(scrollLoopId.current);
1947
- }
1948
- };
1949
- }, [totalCount, ...dependencies]);
1943
+ return () => clearInterval(intervalId);
1944
+ }, [range.endIndex, totalCount, positions]);
1950
1945
 
1951
- // --- EFFECT #2: USER INTERACTION & RESET ---
1952
- // This handles manual scrolling and resetting when the chat changes.
1946
+ // --- PHASE 3: Handle User Interaction and Resets ---
1953
1947
  useEffect(() => {
1954
1948
  const container = containerRef.current;
1955
1949
  if (!container) return;
1956
1950
 
1957
- // When the chat changes (dependencies change), reset the lock.
1958
1951
  console.log(
1959
- "DEPENDENCY CHANGE: Resetting scroll lock to:",
1960
- stickToBottom
1952
+ "DEPENDENCY CHANGE: Resetting scroll lock and initial view."
1961
1953
  );
1962
1954
  isLockedToBottomRef.current = stickToBottom;
1963
1955
 
1964
1956
  const updateVirtualRange = () => {
1965
- /* ... same as before ... */
1957
+ const { scrollTop, clientHeight } = container;
1958
+ let low = 0,
1959
+ high = totalCount - 1;
1960
+ while (low <= high) {
1961
+ const mid = Math.floor((low + high) / 2);
1962
+ if (positions[mid]! < scrollTop) low = mid + 1;
1963
+ else high = mid - 1;
1964
+ }
1965
+ const startIndex = Math.max(0, high - overscan);
1966
+ let endIndex = startIndex;
1967
+ const visibleEnd = scrollTop + clientHeight;
1968
+ while (
1969
+ endIndex < totalCount &&
1970
+ positions[endIndex]! < visibleEnd
1971
+ ) {
1972
+ endIndex++;
1973
+ }
1974
+ setRange({
1975
+ startIndex,
1976
+ endIndex: Math.min(totalCount, endIndex + overscan),
1977
+ });
1966
1978
  };
1967
1979
 
1968
1980
  const handleUserScroll = () => {
1981
+ if (isAutoScrolling.current) return;
1969
1982
  const isAtBottom =
1970
1983
  container.scrollHeight -
1971
1984
  container.scrollTop -
1972
1985
  container.clientHeight <
1973
1986
  1;
1974
1987
  if (!isAtBottom) {
1975
- if (isLockedToBottomRef.current) {
1976
- console.log("USER SCROLL: Lock broken.");
1977
- isLockedToBottomRef.current = false;
1978
- // If a scroll loop was running, kill it.
1979
- if (scrollLoopId.current) {
1980
- clearInterval(scrollLoopId.current);
1981
- scrollLoopId.current = null;
1982
- console.log("...Auto-scroll loop terminated by user.");
1983
- }
1984
- }
1988
+ isLockedToBottomRef.current = false;
1985
1989
  }
1986
1990
  updateVirtualRange();
1987
1991
  };
@@ -1993,26 +1997,12 @@ function createProxyHandler<T>(
1993
1997
 
1994
1998
  return () =>
1995
1999
  container.removeEventListener("scroll", handleUserScroll);
1996
- }, [totalCount, positions]);
1997
- useEffect(() => {
1998
- console.log(
1999
- "DEPENDENCY CHANGE: Resetting scroll lock and scrolling to bottom."
2000
- );
2001
- if (containerRef.current) {
2002
- // Reset the lock so the main effect can take over on the next data load.
2003
- isLockedToBottomRef.current = stickToBottom;
2004
- // Scroll to the top to show the loading state for the new chat.
2005
- containerRef.current.scrollTop = 0;
2006
- // Reset the range
2007
- setRange({ startIndex: 0, endIndex: 10 });
2008
- }
2009
- }, dependencies);
2000
+ }, [...dependencies]);
2001
+
2010
2002
  const scrollToBottom = useCallback(
2011
2003
  (behavior: ScrollBehavior = "smooth") => {
2012
2004
  if (containerRef.current) {
2013
2005
  isLockedToBottomRef.current = true;
2014
- console.log("USER ACTION: Scroll lock ENABLED.");
2015
- // This is a manual trigger, so we don't need the loop. Just scroll.
2016
2006
  containerRef.current.scrollTo({
2017
2007
  top: containerRef.current.scrollHeight,
2018
2008
  behavior,
@@ -2026,7 +2016,6 @@ function createProxyHandler<T>(
2026
2016
  (index: number, behavior: ScrollBehavior = "smooth") => {
2027
2017
  if (containerRef.current && positions[index] !== undefined) {
2028
2018
  isLockedToBottomRef.current = false;
2029
- console.log("USER ACTION: Scroll lock DISABLED.");
2030
2019
  containerRef.current.scrollTo({
2031
2020
  top: positions[index],
2032
2021
  behavior,