cogsbox-state 0.5.380 → 0.5.382

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.380",
3
+ "version": "0.5.382",
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
@@ -1817,7 +1817,7 @@ function createProxyHandler<T>(
1817
1817
  | "LOCKED_AT_BOTTOM"
1818
1818
  | "IDLE_NOT_AT_BOTTOM";
1819
1819
 
1820
- const shouldNotScroll = useRef(false);
1820
+ // Refs and State
1821
1821
  const containerRef = useRef<HTMLDivElement | null>(null);
1822
1822
  const [range, setRange] = useState({
1823
1823
  startIndex: 0,
@@ -1827,9 +1827,9 @@ function createProxyHandler<T>(
1827
1827
  const isProgrammaticScroll = useRef(false);
1828
1828
  const prevTotalCountRef = useRef(0);
1829
1829
  const prevDepsRef = useRef(dependencies);
1830
- const lastUpdateAtScrollTop = useRef(0);
1831
1830
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1832
1831
 
1832
+ // Subscribe to external state updates for item heights
1833
1833
  useEffect(() => {
1834
1834
  const unsubscribe = getGlobalStore
1835
1835
  .getState()
@@ -1845,6 +1845,7 @@ function createProxyHandler<T>(
1845
1845
  ) as any[];
1846
1846
  const totalCount = sourceArray.length;
1847
1847
 
1848
+ // Memoize positions and total height based on measured items
1848
1849
  const { totalHeight, positions } = useMemo(() => {
1849
1850
  const shadowArray =
1850
1851
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
@@ -1866,7 +1867,7 @@ function createProxyHandler<T>(
1866
1867
  shadowUpdateTrigger,
1867
1868
  ]);
1868
1869
 
1869
- // THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
1870
+ // Memoize the virtualized data slice
1870
1871
  const virtualState = useMemo(() => {
1871
1872
  const start = Math.max(0, range.startIndex);
1872
1873
  const end = Math.min(totalCount, range.endIndex);
@@ -1882,7 +1883,7 @@ function createProxyHandler<T>(
1882
1883
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1883
1884
 
1884
1885
  // --- 1. STATE CONTROLLER ---
1885
- // This effect decides which state to transition TO.
1886
+ // Decides which state to transition TO based on data changes.
1886
1887
  useLayoutEffect(() => {
1887
1888
  const depsChanged = !isDeepEqual(
1888
1889
  dependencies,
@@ -1893,9 +1894,10 @@ function createProxyHandler<T>(
1893
1894
  if (depsChanged) {
1894
1895
  console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1895
1896
  setStatus("IDLE_AT_TOP");
1896
- return; // Stop here, let the next effect handle the action for the new state.
1897
+ return;
1897
1898
  }
1898
1899
 
1900
+ // THIS IS THE CRITICAL CHECK: It only scrolls if new items arrive AND we are already locked.
1899
1901
  if (
1900
1902
  hasNewItems &&
1901
1903
  status === "LOCKED_AT_BOTTOM" &&
@@ -1907,11 +1909,12 @@ function createProxyHandler<T>(
1907
1909
  setStatus("GETTING_HEIGHTS");
1908
1910
  }
1909
1911
 
1912
+ prevTotalCountRef.current = totalCount;
1910
1913
  prevDepsRef.current = dependencies;
1911
1914
  }, [totalCount, ...dependencies]);
1912
1915
 
1913
1916
  // --- 2. STATE ACTION HANDLER ---
1914
- // This effect performs the ACTION for the current state.
1917
+ // Performs the ACTION for the current state (e.g., scrolling).
1915
1918
  useLayoutEffect(() => {
1916
1919
  const container = containerRef.current;
1917
1920
  if (!container) return;
@@ -1923,22 +1926,17 @@ function createProxyHandler<T>(
1923
1926
  stickToBottom &&
1924
1927
  totalCount > 0
1925
1928
  ) {
1926
- // If we just loaded a new chat, start the process.
1927
1929
  console.log(
1928
- "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1930
+ "ACTION (IDLE_AT_TOP): Data arrived -> GETTING_HEIGHTS"
1929
1931
  );
1930
1932
  setStatus("GETTING_HEIGHTS");
1931
1933
  } else if (status === "GETTING_HEIGHTS") {
1932
- console.log(
1933
- "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1934
- );
1935
-
1934
+ console.log("ACTION (GETTING_HEIGHTS): Setting range to end");
1936
1935
  setRange({
1937
1936
  startIndex: Math.max(0, totalCount - 10 - overscan),
1938
1937
  endIndex: totalCount,
1939
1938
  });
1940
1939
 
1941
- let intervalId: NodeJS.Timeout;
1942
1940
  intervalId = setInterval(() => {
1943
1941
  const lastItemIndex = totalCount - 1;
1944
1942
  const shadowArray =
@@ -1950,50 +1948,17 @@ function createProxyHandler<T>(
1950
1948
 
1951
1949
  if (lastItemHeight > 0) {
1952
1950
  clearInterval(intervalId);
1953
-
1954
- if (!shouldNotScroll.current) {
1955
- const prevCount = prevTotalCountRef.current;
1956
- const addedItems = totalCount - prevCount;
1957
- const smallAddition = addedItems > 0 && addedItems <= 3;
1958
-
1959
- if (smallAddition) {
1960
- // Let DOM render before measuring + scrolling
1961
- requestAnimationFrame(() => {
1962
- const prevBottom =
1963
- positions[prevCount] ?? container.scrollHeight;
1964
- const newBottom = container.scrollHeight;
1965
- const delta = newBottom - prevBottom;
1966
-
1967
- if (delta > 0) {
1968
- container.scrollBy({
1969
- top: delta,
1970
- behavior: "smooth",
1971
- });
1972
- }
1973
- prevTotalCountRef.current = totalCount;
1974
-
1975
- console.log(
1976
- "ACTION (GETTING_HEIGHTS): Small addition -> LOCKED_AT_BOTTOM"
1977
- );
1978
- setStatus("LOCKED_AT_BOTTOM");
1979
- });
1980
- } else {
1981
- console.log(
1982
- "ACTION (GETTING_HEIGHTS): Large change -> SCROLLING_TO_BOTTOM"
1983
- );
1984
- setStatus("SCROLLING_TO_BOTTOM");
1985
- }
1986
- }
1951
+ console.log(
1952
+ "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1953
+ );
1954
+ setStatus("SCROLLING_TO_BOTTOM");
1987
1955
  }
1988
- }, 50);
1989
-
1990
- return () => clearInterval(intervalId);
1956
+ }, 100);
1991
1957
  } else if (status === "SCROLLING_TO_BOTTOM") {
1992
1958
  console.log(
1993
1959
  "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1994
1960
  );
1995
1961
  isProgrammaticScroll.current = true;
1996
- // Use 'auto' for initial load, 'smooth' for new messages.
1997
1962
  const scrollBehavior =
1998
1963
  prevTotalCountRef.current === 0 ? "auto" : "smooth";
1999
1964
 
@@ -2004,17 +1969,11 @@ function createProxyHandler<T>(
2004
1969
 
2005
1970
  const timeoutId = setTimeout(
2006
1971
  () => {
2007
- console.log(
2008
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
2009
- );
2010
1972
  isProgrammaticScroll.current = false;
2011
- shouldNotScroll.current = false;
2012
1973
  setStatus("LOCKED_AT_BOTTOM");
2013
- prevTotalCountRef.current = totalCount;
2014
1974
  },
2015
1975
  scrollBehavior === "smooth" ? 500 : 50
2016
1976
  );
2017
-
2018
1977
  return () => clearTimeout(timeoutId);
2019
1978
  }
2020
1979
 
@@ -2023,46 +1982,67 @@ function createProxyHandler<T>(
2023
1982
  };
2024
1983
  }, [status, totalCount, positions]);
2025
1984
 
1985
+ // --- 3. USER INTERACTION & RANGE UPDATER (THE CORRECTED VERSION) ---
2026
1986
  useEffect(() => {
2027
1987
  const container = containerRef.current;
2028
1988
  if (!container) return;
2029
1989
 
2030
- const scrollThreshold = itemHeight;
2031
-
2032
1990
  const handleUserScroll = () => {
2033
1991
  if (isProgrammaticScroll.current) {
2034
1992
  return;
2035
1993
  }
2036
1994
 
2037
- const scrollTop = container.scrollTop;
2038
- if (
2039
- Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2040
- scrollThreshold
2041
- ) {
2042
- return;
2043
- }
1995
+ const { scrollTop, clientHeight, scrollHeight } = container;
2044
1996
 
2045
- console.log(
2046
- `Threshold passed at ${scrollTop}px. Recalculating range...`
2047
- );
1997
+ // Part 1: UPDATE THE STATE MACHINE STATUS (The critical fix)
1998
+ // This tells the rest of the component whether we should auto-scroll on new messages.
1999
+ const isAtBottom =
2000
+ scrollHeight - scrollTop - clientHeight < 1;
2048
2001
 
2049
- // NOW we do the expensive work.
2050
- const { clientHeight } = container;
2002
+ if (isAtBottom) {
2003
+ if (status !== "LOCKED_AT_BOTTOM") {
2004
+ console.log(
2005
+ "SCROLL EVENT: Reached bottom -> LOCKED_AT_BOTTOM"
2006
+ );
2007
+ setStatus("LOCKED_AT_BOTTOM");
2008
+ }
2009
+ } else {
2010
+ if (status !== "IDLE_NOT_AT_BOTTOM") {
2011
+ console.log(
2012
+ "SCROLL EVENT: Scrolled up -> IDLE_NOT_AT_BOTTOM"
2013
+ );
2014
+ setStatus("IDLE_NOT_AT_BOTTOM");
2015
+ }
2016
+ }
2017
+
2018
+ // Part 2: EFFICIENTLY UPDATE THE RENDERED RANGE
2019
+ // This is the superior optimization from your first version. It only
2020
+ // re-renders if the actual items in view have changed.
2051
2021
  let high = totalCount - 1;
2052
2022
  let low = 0;
2053
- let topItemIndex = 0;
2023
+ let potentialTopIndex = 0;
2054
2024
  while (low <= high) {
2055
2025
  const mid = Math.floor((low + high) / 2);
2056
2026
  if (positions[mid]! < scrollTop) {
2057
- topItemIndex = mid;
2027
+ potentialTopIndex = mid;
2058
2028
  low = mid + 1;
2059
2029
  } else {
2060
2030
  high = mid - 1;
2061
2031
  }
2062
2032
  }
2063
2033
 
2064
- const startIndex = Math.max(0, topItemIndex - overscan);
2065
- let endIndex = startIndex;
2034
+ const potentialStartIndex = Math.max(
2035
+ 0,
2036
+ potentialTopIndex - overscan
2037
+ );
2038
+
2039
+ // Only update state if the visible start index has actually changed.
2040
+ if (potentialStartIndex === range.startIndex) {
2041
+ return;
2042
+ }
2043
+
2044
+ // If we must update, calculate the new end index.
2045
+ let endIndex = potentialStartIndex;
2066
2046
  const visibleEnd = scrollTop + clientHeight;
2067
2047
  while (
2068
2048
  endIndex < totalCount &&
@@ -2071,13 +2051,13 @@ function createProxyHandler<T>(
2071
2051
  endIndex++;
2072
2052
  }
2073
2053
 
2054
+ console.log(
2055
+ `Index changed from ${range.startIndex} to ${potentialStartIndex}. Updating range.`
2056
+ );
2074
2057
  setRange({
2075
- startIndex,
2058
+ startIndex: potentialStartIndex,
2076
2059
  endIndex: Math.min(totalCount, endIndex + overscan),
2077
2060
  });
2078
-
2079
- // Finally, we record that we did the work at THIS scroll position.
2080
- lastUpdateAtScrollTop.current = scrollTop;
2081
2061
  };
2082
2062
 
2083
2063
  container.addEventListener("scroll", handleUserScroll, {
@@ -2085,18 +2065,22 @@ function createProxyHandler<T>(
2085
2065
  });
2086
2066
  return () =>
2087
2067
  container.removeEventListener("scroll", handleUserScroll);
2088
- }, [totalCount, positions, itemHeight, overscan, status]);
2068
+ }, [totalCount, positions, status, range.startIndex]); // Dependencies are correct
2089
2069
 
2070
+ // --- 4. EXPOSED ACTIONS ---
2090
2071
  const scrollToBottom = useCallback(() => {
2091
2072
  console.log(
2092
2073
  "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2093
2074
  );
2075
+ // Don't scroll if there's nothing to scroll to.
2076
+ if (totalCount === 0) return;
2094
2077
  setStatus("SCROLLING_TO_BOTTOM");
2095
- }, []);
2078
+ }, [totalCount]);
2096
2079
 
2097
2080
  const scrollToIndex = useCallback(
2098
2081
  (index: number, behavior: ScrollBehavior = "smooth") => {
2099
2082
  if (containerRef.current && positions[index] !== undefined) {
2083
+ // Manually scrolling to an index means we are no longer at the bottom.
2100
2084
  setStatus("IDLE_NOT_AT_BOTTOM");
2101
2085
  containerRef.current.scrollTo({
2102
2086
  top: positions[index],
@@ -2107,6 +2091,7 @@ function createProxyHandler<T>(
2107
2091
  [positions]
2108
2092
  );
2109
2093
 
2094
+ // --- 5. RENDER PROPS ---
2110
2095
  const virtualizerProps = {
2111
2096
  outer: {
2112
2097
  ref: containerRef,