cogsbox-state 0.5.367 → 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.367",
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
@@ -1809,21 +1809,16 @@ function createProxyHandler<T>(
1809
1809
  dependencies = [],
1810
1810
  } = options;
1811
1811
 
1812
- // YOUR STATE MACHINE STATES
1813
1812
  type Status =
1814
- | "IDLE_AT_TOP" // Initial state for a new chat
1815
- | "GETTING_HEIGHTS" // Waiting for the last item to be measured
1816
- | "SCROLLING_TO_BOTTOM" // Performing the scroll animation
1817
- | "LOCKED_AT_BOTTOM" // Idle at the bottom, waiting for new messages
1818
- | "IDLE_NOT_AT_BOTTOM"; // User has scrolled up and broken the lock
1813
+ | "IDLE" // User is in control, or we are resting at the bottom.
1814
+ | "SCROLLING_TO_BOTTOM"; // Performing a scroll animation.
1819
1815
 
1820
1816
  const containerRef = useRef<HTMLDivElement | null>(null);
1821
1817
  const [range, setRange] = useState({
1822
1818
  startIndex: 0,
1823
1819
  endIndex: 10,
1824
1820
  });
1825
- const [status, setStatus] = useState<Status>("IDLE_AT_TOP");
1826
-
1821
+ const [status, setStatus] = useState<Status>("IDLE");
1827
1822
  const prevTotalCountRef = useRef(0);
1828
1823
  const prevDepsRef = useRef(dependencies);
1829
1824
 
@@ -1879,136 +1874,82 @@ function createProxyHandler<T>(
1879
1874
  });
1880
1875
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1881
1876
 
1882
- // --- 1. STATE CONTROLLER ---
1883
- // This effect decides which state to transition TO.
1877
+ // --- Main Controller Effect ---
1884
1878
  useLayoutEffect(() => {
1879
+ const container = containerRef.current;
1880
+ if (!container) return;
1881
+
1885
1882
  const depsChanged = !isDeepEqual(
1886
1883
  dependencies,
1887
1884
  prevDepsRef.current
1888
1885
  );
1889
1886
  const hasNewItems = totalCount > prevTotalCountRef.current;
1890
1887
 
1888
+ // 1. On Chat Change: Reset to a clean state and trigger an instant scroll.
1891
1889
  if (depsChanged) {
1892
- console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1893
- setStatus("IDLE_AT_TOP");
1894
- return; // Stop here, let the next effect handle the action for the new state.
1895
- }
1896
-
1897
- if (
1898
- hasNewItems &&
1899
- status === "LOCKED_AT_BOTTOM" &&
1900
- stickToBottom
1901
- ) {
1902
1890
  console.log(
1903
- "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1891
+ "EVENT: Chat changed. Resetting state and scrolling."
1904
1892
  );
1905
- setStatus("GETTING_HEIGHTS");
1893
+ setStatus("SCROLLING_TO_BOTTOM");
1894
+ // This return is important. It lets the effect re-run with the new status.
1895
+ return;
1906
1896
  }
1907
1897
 
1908
- prevTotalCountRef.current = totalCount;
1909
- prevDepsRef.current = dependencies;
1910
- }, [totalCount, ...dependencies]);
1911
-
1912
- // --- 2. STATE ACTION HANDLER ---
1913
- // This effect performs the ACTION for the current state.
1914
- useLayoutEffect(() => {
1915
- const container = containerRef.current;
1916
- if (!container) return;
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
+ }
1913
+ }
1917
1914
 
1918
- let intervalId: NodeJS.Timeout | undefined;
1915
+ let scrollTimeoutId: NodeJS.Timeout | undefined;
1919
1916
 
1920
- if (
1921
- status === "IDLE_AT_TOP" &&
1922
- stickToBottom &&
1923
- totalCount > 0
1924
- ) {
1925
- // If we just loaded a new chat, start the process.
1926
- console.log(
1927
- "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1928
- );
1929
- setStatus("GETTING_HEIGHTS");
1930
- } else if (status === "GETTING_HEIGHTS") {
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";
1931
1923
  console.log(
1932
- "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1924
+ `ACTION: Scrolling to bottom. Behavior: ${scrollBehavior}`
1933
1925
  );
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 (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1952
- );
1953
- setStatus("SCROLLING_TO_BOTTOM");
1954
- }
1955
- }, 100);
1956
- } else if (status === "SCROLLING_TO_BOTTOM") {
1957
- console.log(
1958
- "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1959
- );
1960
- // Use 'auto' for initial load, 'smooth' for new messages.
1961
- const scrollBehavior =
1962
- prevTotalCountRef.current === 0 ? "auto" : "smooth";
1963
1926
 
1964
1927
  container.scrollTo({
1965
1928
  top: container.scrollHeight,
1966
1929
  behavior: scrollBehavior,
1967
1930
  });
1968
1931
 
1969
- const timeoutId = setTimeout(
1932
+ // After the scroll, transition back to IDLE.
1933
+ // The timeout lets the animation finish so this doesn't happen too early.
1934
+ scrollTimeoutId = setTimeout(
1970
1935
  () => {
1971
- console.log(
1972
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1973
- );
1974
- setStatus("LOCKED_AT_BOTTOM");
1936
+ console.log("ACTION: Scroll finished. -> IDLE");
1937
+ setStatus("IDLE");
1975
1938
  },
1976
- scrollBehavior === "smooth" ? 500 : 50
1939
+ isInitial ? 50 : 500
1977
1940
  );
1978
-
1979
- return () => clearTimeout(timeoutId);
1980
1941
  }
1981
1942
 
1982
- // If status is IDLE_NOT_AT_BOTTOM or LOCKED_AT_BOTTOM, we do nothing here.
1983
- // The scroll has either finished or been disabled by the user.
1984
-
1985
- return () => {
1986
- if (intervalId) clearInterval(intervalId);
1987
- };
1988
- }, [status, totalCount, positions]);
1989
-
1990
- // --- 3. USER INTERACTION & RANGE UPDATER ---
1991
- useEffect(() => {
1992
- const container = containerRef.current;
1993
- if (!container) return;
1994
-
1943
+ // --- User Scroll Handling & Range Update ---
1995
1944
  const handleUserScroll = () => {
1996
- // This is the core logic you wanted.
1997
- if (status !== "IDLE_NOT_AT_BOTTOM") {
1998
- const isAtBottom =
1999
- container.scrollHeight -
2000
- container.scrollTop -
2001
- container.clientHeight <
2002
- 1;
2003
- if (!isAtBottom) {
2004
- console.log(
2005
- "USER ACTION: Scrolled up -> IDLE_NOT_AT_BOTTOM"
2006
- );
2007
- setStatus("IDLE_NOT_AT_BOTTOM");
2008
- }
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");
2009
1950
  }
2010
- // We always update the range, regardless of state.
2011
- // This is the full, non-placeholder function.
1951
+
1952
+ // This is the full range update function.
2012
1953
  const { scrollTop, clientHeight } = container;
2013
1954
  let low = 0,
2014
1955
  high = totalCount - 1;
@@ -2035,9 +1976,21 @@ function createProxyHandler<T>(
2035
1976
  container.addEventListener("scroll", handleUserScroll, {
2036
1977
  passive: true,
2037
1978
  });
2038
- 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 () => {
2039
1990
  container.removeEventListener("scroll", handleUserScroll);
2040
- }, [totalCount, positions, status]); // Depends on status to know if it should break the lock
1991
+ if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
1992
+ };
1993
+ }, [totalCount, positions, status, ...dependencies]);
2041
1994
 
2042
1995
  const scrollToBottom = useCallback(() => {
2043
1996
  console.log(
@@ -2049,7 +2002,7 @@ function createProxyHandler<T>(
2049
2002
  const scrollToIndex = useCallback(
2050
2003
  (index: number, behavior: ScrollBehavior = "smooth") => {
2051
2004
  if (containerRef.current && positions[index] !== undefined) {
2052
- setStatus("IDLE_NOT_AT_BOTTOM");
2005
+ setStatus("IDLE");
2053
2006
  containerRef.current.scrollTo({
2054
2007
  top: positions[index],
2055
2008
  behavior,