cogsbox-state 0.5.382 → 0.5.384

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