cogsbox-state 0.5.381 → 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.381",
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" &&
@@ -1912,7 +1914,7 @@ function createProxyHandler<T>(
1912
1914
  }, [totalCount, ...dependencies]);
1913
1915
 
1914
1916
  // --- 2. STATE ACTION HANDLER ---
1915
- // This effect performs the ACTION for the current state.
1917
+ // Performs the ACTION for the current state (e.g., scrolling).
1916
1918
  useLayoutEffect(() => {
1917
1919
  const container = containerRef.current;
1918
1920
  if (!container) return;
@@ -1924,15 +1926,12 @@ function createProxyHandler<T>(
1924
1926
  stickToBottom &&
1925
1927
  totalCount > 0
1926
1928
  ) {
1927
- // If we just loaded a new chat, start the process.
1928
1929
  console.log(
1929
- "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1930
+ "ACTION (IDLE_AT_TOP): Data arrived -> GETTING_HEIGHTS"
1930
1931
  );
1931
1932
  setStatus("GETTING_HEIGHTS");
1932
1933
  } else if (status === "GETTING_HEIGHTS") {
1933
- console.log(
1934
- "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
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,
@@ -1949,13 +1948,10 @@ function createProxyHandler<T>(
1949
1948
 
1950
1949
  if (lastItemHeight > 0) {
1951
1950
  clearInterval(intervalId);
1952
- if (!shouldNotScroll.current) {
1953
- console.log(
1954
- "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1955
- );
1956
-
1957
- setStatus("SCROLLING_TO_BOTTOM");
1958
- }
1951
+ console.log(
1952
+ "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1953
+ );
1954
+ setStatus("SCROLLING_TO_BOTTOM");
1959
1955
  }
1960
1956
  }, 100);
1961
1957
  } else if (status === "SCROLLING_TO_BOTTOM") {
@@ -1963,7 +1959,6 @@ function createProxyHandler<T>(
1963
1959
  "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1964
1960
  );
1965
1961
  isProgrammaticScroll.current = true;
1966
- // Use 'auto' for initial load, 'smooth' for new messages.
1967
1962
  const scrollBehavior =
1968
1963
  prevTotalCountRef.current === 0 ? "auto" : "smooth";
1969
1964
 
@@ -1974,16 +1969,11 @@ function createProxyHandler<T>(
1974
1969
 
1975
1970
  const timeoutId = setTimeout(
1976
1971
  () => {
1977
- console.log(
1978
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1979
- );
1980
1972
  isProgrammaticScroll.current = false;
1981
- shouldNotScroll.current = false;
1982
1973
  setStatus("LOCKED_AT_BOTTOM");
1983
1974
  },
1984
1975
  scrollBehavior === "smooth" ? 500 : 50
1985
1976
  );
1986
-
1987
1977
  return () => clearTimeout(timeoutId);
1988
1978
  }
1989
1979
 
@@ -1992,46 +1982,67 @@ function createProxyHandler<T>(
1992
1982
  };
1993
1983
  }, [status, totalCount, positions]);
1994
1984
 
1985
+ // --- 3. USER INTERACTION & RANGE UPDATER (THE CORRECTED VERSION) ---
1995
1986
  useEffect(() => {
1996
1987
  const container = containerRef.current;
1997
1988
  if (!container) return;
1998
1989
 
1999
- const scrollThreshold = itemHeight;
2000
-
2001
1990
  const handleUserScroll = () => {
2002
1991
  if (isProgrammaticScroll.current) {
2003
1992
  return;
2004
1993
  }
2005
1994
 
2006
- const scrollTop = container.scrollTop;
2007
- if (
2008
- Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2009
- scrollThreshold
2010
- ) {
2011
- return;
2012
- }
1995
+ const { scrollTop, clientHeight, scrollHeight } = container;
2013
1996
 
2014
- console.log(
2015
- `Threshold passed at ${scrollTop}px. Recalculating range...`
2016
- );
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;
2001
+
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
2017
 
2018
- // NOW we do the expensive work.
2019
- const { clientHeight } = container;
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.
2020
2021
  let high = totalCount - 1;
2021
2022
  let low = 0;
2022
- let topItemIndex = 0;
2023
+ let potentialTopIndex = 0;
2023
2024
  while (low <= high) {
2024
2025
  const mid = Math.floor((low + high) / 2);
2025
2026
  if (positions[mid]! < scrollTop) {
2026
- topItemIndex = mid;
2027
+ potentialTopIndex = mid;
2027
2028
  low = mid + 1;
2028
2029
  } else {
2029
2030
  high = mid - 1;
2030
2031
  }
2031
2032
  }
2032
2033
 
2033
- const startIndex = Math.max(0, topItemIndex - overscan);
2034
- 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;
2035
2046
  const visibleEnd = scrollTop + clientHeight;
2036
2047
  while (
2037
2048
  endIndex < totalCount &&
@@ -2040,13 +2051,13 @@ function createProxyHandler<T>(
2040
2051
  endIndex++;
2041
2052
  }
2042
2053
 
2054
+ console.log(
2055
+ `Index changed from ${range.startIndex} to ${potentialStartIndex}. Updating range.`
2056
+ );
2043
2057
  setRange({
2044
- startIndex,
2058
+ startIndex: potentialStartIndex,
2045
2059
  endIndex: Math.min(totalCount, endIndex + overscan),
2046
2060
  });
2047
-
2048
- // Finally, we record that we did the work at THIS scroll position.
2049
- lastUpdateAtScrollTop.current = scrollTop;
2050
2061
  };
2051
2062
 
2052
2063
  container.addEventListener("scroll", handleUserScroll, {
@@ -2054,18 +2065,22 @@ function createProxyHandler<T>(
2054
2065
  });
2055
2066
  return () =>
2056
2067
  container.removeEventListener("scroll", handleUserScroll);
2057
- }, [totalCount, positions, itemHeight, overscan, status]);
2068
+ }, [totalCount, positions, status, range.startIndex]); // Dependencies are correct
2058
2069
 
2070
+ // --- 4. EXPOSED ACTIONS ---
2059
2071
  const scrollToBottom = useCallback(() => {
2060
2072
  console.log(
2061
2073
  "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2062
2074
  );
2075
+ // Don't scroll if there's nothing to scroll to.
2076
+ if (totalCount === 0) return;
2063
2077
  setStatus("SCROLLING_TO_BOTTOM");
2064
- }, []);
2078
+ }, [totalCount]);
2065
2079
 
2066
2080
  const scrollToIndex = useCallback(
2067
2081
  (index: number, behavior: ScrollBehavior = "smooth") => {
2068
2082
  if (containerRef.current && positions[index] !== undefined) {
2083
+ // Manually scrolling to an index means we are no longer at the bottom.
2069
2084
  setStatus("IDLE_NOT_AT_BOTTOM");
2070
2085
  containerRef.current.scrollTo({
2071
2086
  top: positions[index],
@@ -2076,6 +2091,7 @@ function createProxyHandler<T>(
2076
2091
  [positions]
2077
2092
  );
2078
2093
 
2094
+ // --- 5. RENDER PROPS ---
2079
2095
  const virtualizerProps = {
2080
2096
  outer: {
2081
2097
  ref: containerRef,