cogsbox-state 0.5.367 → 0.5.369

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.369",
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,22 +1809,18 @@ 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);
1823
+
1828
1824
  const prevDepsRef = useRef(dependencies);
1829
1825
 
1830
1826
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
@@ -1879,136 +1875,82 @@ function createProxyHandler<T>(
1879
1875
  });
1880
1876
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1881
1877
 
1882
- // --- 1. STATE CONTROLLER ---
1883
- // This effect decides which state to transition TO.
1878
+ // --- Main Controller Effect ---
1884
1879
  useLayoutEffect(() => {
1880
+ const container = containerRef.current;
1881
+ if (!container) return;
1882
+
1885
1883
  const depsChanged = !isDeepEqual(
1886
1884
  dependencies,
1887
1885
  prevDepsRef.current
1888
1886
  );
1889
1887
  const hasNewItems = totalCount > prevTotalCountRef.current;
1890
1888
 
1889
+ // 1. On Chat Change: Reset to a clean state and trigger an instant scroll.
1891
1890
  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
1891
  console.log(
1903
- "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1892
+ "EVENT: Chat changed. Resetting state and scrolling."
1904
1893
  );
1905
- setStatus("GETTING_HEIGHTS");
1894
+ setStatus("SCROLLING_TO_BOTTOM");
1895
+ // This return is important. It lets the effect re-run with the new status.
1896
+ return;
1906
1897
  }
1907
1898
 
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;
1917
-
1918
- let intervalId: NodeJS.Timeout | undefined;
1899
+ // 2. On New Message: Check if we SHOULD scroll.
1900
+ // This will only happen if we are IDLE (meaning we are already at the bottom).
1901
+ if (hasNewItems && status === "IDLE") {
1902
+ const isAtBottom =
1903
+ container.scrollHeight -
1904
+ container.scrollTop -
1905
+ container.clientHeight <
1906
+ itemHeight;
1907
+ if (isAtBottom && stickToBottom) {
1908
+ console.log(
1909
+ "EVENT: New message arrived while at bottom. -> SCROLLING_TO_BOTTOM"
1910
+ );
1911
+ setStatus("SCROLLING_TO_BOTTOM");
1912
+ return; // Let the effect re-run with the new status.
1913
+ }
1914
+ }
1919
1915
 
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") {
1931
- console.log(
1932
- "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1933
- );
1934
- setRange({
1935
- startIndex: Math.max(0, totalCount - 10 - overscan),
1936
- endIndex: totalCount,
1937
- });
1916
+ let scrollTimeoutId: NodeJS.Timeout | undefined;
1938
1917
 
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") {
1918
+ // 3. If we are in the SCROLLING state, perform the scroll action.
1919
+ if (status === "SCROLLING_TO_BOTTOM") {
1920
+ // A chat change or initial load should be instant. New messages should be smooth.
1921
+ const isInitial =
1922
+ prevTotalCountRef.current === 0 || depsChanged;
1923
+ const scrollBehavior = isInitial ? "auto" : "smooth";
1957
1924
  console.log(
1958
- "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1925
+ `ACTION: Scrolling to bottom. Behavior: ${scrollBehavior}`
1959
1926
  );
1960
- // Use 'auto' for initial load, 'smooth' for new messages.
1961
- const scrollBehavior =
1962
- prevTotalCountRef.current === 0 ? "auto" : "smooth";
1963
1927
 
1964
1928
  container.scrollTo({
1965
1929
  top: container.scrollHeight,
1966
1930
  behavior: scrollBehavior,
1967
1931
  });
1968
1932
 
1969
- const timeoutId = setTimeout(
1933
+ // After the scroll, transition back to IDLE.
1934
+ // The timeout lets the animation finish so this doesn't happen too early.
1935
+ scrollTimeoutId = setTimeout(
1970
1936
  () => {
1971
- console.log(
1972
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1973
- );
1974
- setStatus("LOCKED_AT_BOTTOM");
1937
+ console.log("ACTION: Scroll finished. -> IDLE");
1938
+ setStatus("IDLE");
1975
1939
  },
1976
- scrollBehavior === "smooth" ? 500 : 50
1940
+ isInitial ? 50 : 500
1977
1941
  );
1978
-
1979
- return () => clearTimeout(timeoutId);
1980
1942
  }
1981
1943
 
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
-
1944
+ // --- User Scroll Handling & Range Update ---
1995
1945
  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
- }
1946
+ // If the user scrolls, they are in control. Immediately go to IDLE.
1947
+ // This will also cancel any pending scroll-end timeouts.
1948
+ if (status === "SCROLLING_TO_BOTTOM") {
1949
+ console.log("USER ACTION: Interrupted scroll. -> IDLE");
1950
+ setStatus("IDLE");
2009
1951
  }
2010
- // We always update the range, regardless of state.
2011
- // This is the full, non-placeholder function.
1952
+
1953
+ // This is the full range update function.
2012
1954
  const { scrollTop, clientHeight } = container;
2013
1955
  let low = 0,
2014
1956
  high = totalCount - 1;
@@ -2035,9 +1977,21 @@ function createProxyHandler<T>(
2035
1977
  container.addEventListener("scroll", handleUserScroll, {
2036
1978
  passive: true,
2037
1979
  });
2038
- return () =>
1980
+
1981
+ // Always update range on render if we are IDLE.
1982
+ if (status === "IDLE") {
1983
+ handleUserScroll();
1984
+ }
1985
+
1986
+ // Update refs for the next render cycle.
1987
+ prevTotalCountRef.current = totalCount;
1988
+ prevDepsRef.current = dependencies;
1989
+
1990
+ return () => {
2039
1991
  container.removeEventListener("scroll", handleUserScroll);
2040
- }, [totalCount, positions, status]); // Depends on status to know if it should break the lock
1992
+ if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
1993
+ };
1994
+ }, [totalCount, positions, status, ...dependencies]);
2041
1995
 
2042
1996
  const scrollToBottom = useCallback(() => {
2043
1997
  console.log(
@@ -2049,7 +2003,7 @@ function createProxyHandler<T>(
2049
2003
  const scrollToIndex = useCallback(
2050
2004
  (index: number, behavior: ScrollBehavior = "smooth") => {
2051
2005
  if (containerRef.current && positions[index] !== undefined) {
2052
- setStatus("IDLE_NOT_AT_BOTTOM");
2006
+ setStatus("IDLE");
2053
2007
  containerRef.current.scrollTo({
2054
2008
  top: positions[index],
2055
2009
  behavior,