cogsbox-state 0.5.366 → 0.5.368

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.366",
3
+ "version": "0.5.368",
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
@@ -1810,18 +1810,15 @@ function createProxyHandler<T>(
1810
1810
  } = options;
1811
1811
 
1812
1812
  type Status =
1813
- | "WAITING_FOR_ARRAY"
1814
- | "GETTING_ARRAY_HEIGHTS"
1815
- | "MOVING_TO_BOTTOM"
1816
- | "LOCKED_AT_BOTTOM"
1817
- | "IDLE_NOT_AT_BOTTOM";
1813
+ | "IDLE" // User is in control, or we are resting at the bottom.
1814
+ | "SCROLLING_TO_BOTTOM"; // Performing a scroll animation.
1818
1815
 
1819
1816
  const containerRef = useRef<HTMLDivElement | null>(null);
1820
1817
  const [range, setRange] = useState({
1821
1818
  startIndex: 0,
1822
1819
  endIndex: 10,
1823
1820
  });
1824
- const [status, setStatus] = useState<Status>("WAITING_FOR_ARRAY");
1821
+ const [status, setStatus] = useState<Status>("IDLE");
1825
1822
  const prevTotalCountRef = useRef(0);
1826
1823
  const prevDepsRef = useRef(dependencies);
1827
1824
 
@@ -1877,141 +1874,82 @@ function createProxyHandler<T>(
1877
1874
  });
1878
1875
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1879
1876
 
1880
- // --- STATE MACHINE CONTROLLER ---
1881
- // This effect decides which state to transition to based on external changes.
1877
+ // --- Main Controller Effect ---
1882
1878
  useLayoutEffect(() => {
1879
+ const container = containerRef.current;
1880
+ if (!container) return;
1881
+
1883
1882
  const depsChanged = !isDeepEqual(
1884
1883
  dependencies,
1885
1884
  prevDepsRef.current
1886
1885
  );
1887
1886
  const hasNewItems = totalCount > prevTotalCountRef.current;
1888
1887
 
1888
+ // 1. On Chat Change: Reset to a clean state and trigger an instant scroll.
1889
1889
  if (depsChanged) {
1890
1890
  console.log(
1891
- "STATE_TRANSITION: Deps changed. -> WAITING_FOR_ARRAY"
1891
+ "EVENT: Chat changed. Resetting state and scrolling."
1892
1892
  );
1893
- setStatus("WAITING_FOR_ARRAY");
1893
+ setStatus("SCROLLING_TO_BOTTOM");
1894
+ // This return is important. It lets the effect re-run with the new status.
1894
1895
  return;
1895
1896
  }
1896
1897
 
1897
- if (
1898
- hasNewItems &&
1899
- status === "LOCKED_AT_BOTTOM" &&
1900
- stickToBottom
1901
- ) {
1902
- console.log(
1903
- "STATE_TRANSITION: New items arrived while locked. -> GETTING_ARRAY_HEIGHTS"
1904
- );
1905
- setStatus("GETTING_ARRAY_HEIGHTS");
1898
+ // 2. On New Message: Check if we SHOULD scroll.
1899
+ // This will only happen if we are IDLE (meaning we are already at the bottom).
1900
+ if (hasNewItems && status === "IDLE") {
1901
+ const isAtBottom =
1902
+ container.scrollHeight -
1903
+ container.scrollTop -
1904
+ container.clientHeight <
1905
+ itemHeight;
1906
+ if (isAtBottom && stickToBottom) {
1907
+ console.log(
1908
+ "EVENT: New message arrived while at bottom. -> SCROLLING_TO_BOTTOM"
1909
+ );
1910
+ setStatus("SCROLLING_TO_BOTTOM");
1911
+ return; // Let the effect re-run with the new status.
1912
+ }
1906
1913
  }
1907
1914
 
1908
- prevTotalCountRef.current = totalCount;
1909
- prevDepsRef.current = dependencies;
1910
- }, [totalCount, ...dependencies]);
1911
-
1912
- // --- STATE MACHINE ACTIONS ---
1913
- // This effect handles the actions for each state.
1914
- useLayoutEffect(() => {
1915
- const container = containerRef.current;
1916
- if (!container) return;
1917
-
1918
- let intervalId: NodeJS.Timeout | undefined;
1919
1915
  let scrollTimeoutId: NodeJS.Timeout | undefined;
1920
1916
 
1921
- if (
1922
- status === "WAITING_FOR_ARRAY" &&
1923
- totalCount > 0 &&
1924
- stickToBottom
1925
- ) {
1917
+ // 3. If we are in the SCROLLING state, perform the scroll action.
1918
+ if (status === "SCROLLING_TO_BOTTOM") {
1919
+ // A chat change or initial load should be instant. New messages should be smooth.
1920
+ const isInitial =
1921
+ prevTotalCountRef.current === 0 || depsChanged;
1922
+ const scrollBehavior = isInitial ? "auto" : "smooth";
1926
1923
  console.log(
1927
- "ACTION: WAITING_FOR_ARRAY -> GETTING_ARRAY_HEIGHTS"
1924
+ `ACTION: Scrolling to bottom. Behavior: ${scrollBehavior}`
1928
1925
  );
1929
- setStatus("GETTING_ARRAY_HEIGHTS");
1930
- } else if (status === "GETTING_ARRAY_HEIGHTS") {
1931
- console.log(
1932
- "ACTION: GETTING_ARRAY_HEIGHTS. Setting range and starting loop."
1933
- );
1934
- setRange({
1935
- startIndex: Math.max(0, totalCount - 10 - overscan),
1936
- endIndex: totalCount,
1937
- });
1938
-
1939
- intervalId = setInterval(() => {
1940
- const lastItemIndex = totalCount - 1;
1941
- const shadowArray =
1942
- getGlobalStore
1943
- .getState()
1944
- .getShadowMetadata(stateKey, path) || [];
1945
- const lastItemHeight =
1946
- shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1947
-
1948
- if (lastItemHeight > 0) {
1949
- clearInterval(intervalId);
1950
- console.log(
1951
- "ACTION: Measurement success. -> MOVING_TO_BOTTOM"
1952
- );
1953
- setStatus("MOVING_TO_BOTTOM");
1954
- }
1955
- }, 100);
1956
- } else if (status === "MOVING_TO_BOTTOM") {
1957
- console.log("ACTION: MOVING_TO_BOTTOM. Executing scroll.");
1958
- const scrollBehavior =
1959
- prevTotalCountRef.current === 0 ? "auto" : "smooth";
1960
1926
 
1961
1927
  container.scrollTo({
1962
1928
  top: container.scrollHeight,
1963
1929
  behavior: scrollBehavior,
1964
1930
  });
1965
1931
 
1932
+ // After the scroll, transition back to IDLE.
1933
+ // The timeout lets the animation finish so this doesn't happen too early.
1966
1934
  scrollTimeoutId = setTimeout(
1967
1935
  () => {
1968
- console.log(
1969
- "ACTION: Scroll finished. -> LOCKED_AT_BOTTOM"
1970
- );
1971
- setStatus("LOCKED_AT_BOTTOM");
1936
+ console.log("ACTION: Scroll finished. -> IDLE");
1937
+ setStatus("IDLE");
1972
1938
  },
1973
- scrollBehavior === "smooth" ? 500 : 50
1939
+ isInitial ? 50 : 500
1974
1940
  );
1975
1941
  }
1976
1942
 
1977
- // THE FIX: This cleanup runs whenever the state changes, killing any active timers.
1978
- return () => {
1979
- if (intervalId) {
1980
- console.log("CLEANUP: Clearing measurement loop timer.");
1981
- clearInterval(intervalId);
1982
- }
1983
- if (scrollTimeoutId) {
1984
- console.log("CLEANUP: Clearing scroll-end timer.");
1985
- clearTimeout(scrollTimeoutId);
1986
- }
1987
- };
1988
- }, [status, totalCount, positions]);
1989
-
1990
- // --- USER INTERACTION ---
1991
- // This effect only handles user scrolls.
1992
- useEffect(() => {
1993
- const container = containerRef.current;
1994
- if (!container) return;
1995
-
1943
+ // --- User Scroll Handling & Range Update ---
1996
1944
  const handleUserScroll = () => {
1997
- const isAtBottom =
1998
- container.scrollHeight -
1999
- container.scrollTop -
2000
- container.clientHeight <
2001
- 1;
2002
- // If the user scrolls up from a locked or scrolling state, move to idle.
2003
- if (
2004
- !isAtBottom &&
2005
- (status === "LOCKED_AT_BOTTOM" ||
2006
- status === "MOVING_TO_BOTTOM")
2007
- ) {
2008
- console.log(
2009
- "USER_ACTION: Scrolled up. -> IDLE_NOT_AT_BOTTOM"
2010
- );
2011
- setStatus("IDLE_NOT_AT_BOTTOM");
1945
+ // If the user scrolls, they are in control. Immediately go to IDLE.
1946
+ // This will also cancel any pending scroll-end timeouts.
1947
+ if (status === "SCROLLING_TO_BOTTOM") {
1948
+ console.log("USER ACTION: Interrupted scroll. -> IDLE");
1949
+ setStatus("IDLE");
2012
1950
  }
2013
1951
 
2014
- // Update the rendered range based on scroll position.
1952
+ // This is the full range update function.
2015
1953
  const { scrollTop, clientHeight } = container;
2016
1954
  let low = 0,
2017
1955
  high = totalCount - 1;
@@ -2038,29 +1976,38 @@ function createProxyHandler<T>(
2038
1976
  container.addEventListener("scroll", handleUserScroll, {
2039
1977
  passive: true,
2040
1978
  });
2041
- return () =>
1979
+
1980
+ // Always update range on render if we are IDLE.
1981
+ if (status === "IDLE") {
1982
+ handleUserScroll();
1983
+ }
1984
+
1985
+ // Update refs for the next render cycle.
1986
+ prevTotalCountRef.current = totalCount;
1987
+ prevDepsRef.current = dependencies;
1988
+
1989
+ return () => {
2042
1990
  container.removeEventListener("scroll", handleUserScroll);
2043
- }, [totalCount, positions, status]);
1991
+ if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
1992
+ };
1993
+ }, [totalCount, positions, status, ...dependencies]);
2044
1994
 
2045
- const scrollToBottom = useCallback(
2046
- (behavior: ScrollBehavior = "smooth") => {
2047
- console.log(
2048
- "USER_ACTION: Clicked scroll to bottom button. -> MOVING_TO_BOTTOM"
2049
- );
2050
- setStatus("MOVING_TO_BOTTOM");
2051
- },
2052
- []
2053
- );
1995
+ const scrollToBottom = useCallback(() => {
1996
+ console.log(
1997
+ "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
1998
+ );
1999
+ setStatus("SCROLLING_TO_BOTTOM");
2000
+ }, []);
2054
2001
 
2055
2002
  const scrollToIndex = useCallback(
2056
2003
  (index: number, behavior: ScrollBehavior = "smooth") => {
2057
- // if (containerRef.current && positions[index] !== undefined) {
2058
- // isLockedToBottomRef.current = false;
2059
- // containerRef.current.scrollTo({
2060
- // top: positions[index],
2061
- // behavior,
2062
- // });
2063
- // }
2004
+ if (containerRef.current && positions[index] !== undefined) {
2005
+ setStatus("IDLE");
2006
+ containerRef.current.scrollTo({
2007
+ top: positions[index],
2008
+ behavior,
2009
+ });
2010
+ }
2064
2011
  },
2065
2012
  [positions]
2066
2013
  );