cogsbox-state 0.5.366 → 0.5.367

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.367",
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,19 +1809,21 @@ function createProxyHandler<T>(
1809
1809
  dependencies = [],
1810
1810
  } = options;
1811
1811
 
1812
+ // YOUR STATE MACHINE STATES
1812
1813
  type Status =
1813
- | "WAITING_FOR_ARRAY"
1814
- | "GETTING_ARRAY_HEIGHTS"
1815
- | "MOVING_TO_BOTTOM"
1816
- | "LOCKED_AT_BOTTOM"
1817
- | "IDLE_NOT_AT_BOTTOM";
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
1818
1819
 
1819
1820
  const containerRef = useRef<HTMLDivElement | null>(null);
1820
1821
  const [range, setRange] = useState({
1821
1822
  startIndex: 0,
1822
1823
  endIndex: 10,
1823
1824
  });
1824
- const [status, setStatus] = useState<Status>("WAITING_FOR_ARRAY");
1825
+ const [status, setStatus] = useState<Status>("IDLE_AT_TOP");
1826
+
1825
1827
  const prevTotalCountRef = useRef(0);
1826
1828
  const prevDepsRef = useRef(dependencies);
1827
1829
 
@@ -1877,8 +1879,8 @@ function createProxyHandler<T>(
1877
1879
  });
1878
1880
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1879
1881
 
1880
- // --- STATE MACHINE CONTROLLER ---
1881
- // This effect decides which state to transition to based on external changes.
1882
+ // --- 1. STATE CONTROLLER ---
1883
+ // This effect decides which state to transition TO.
1882
1884
  useLayoutEffect(() => {
1883
1885
  const depsChanged = !isDeepEqual(
1884
1886
  dependencies,
@@ -1887,11 +1889,9 @@ function createProxyHandler<T>(
1887
1889
  const hasNewItems = totalCount > prevTotalCountRef.current;
1888
1890
 
1889
1891
  if (depsChanged) {
1890
- console.log(
1891
- "STATE_TRANSITION: Deps changed. -> WAITING_FOR_ARRAY"
1892
- );
1893
- setStatus("WAITING_FOR_ARRAY");
1894
- return;
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
1895
  }
1896
1896
 
1897
1897
  if (
@@ -1900,36 +1900,36 @@ function createProxyHandler<T>(
1900
1900
  stickToBottom
1901
1901
  ) {
1902
1902
  console.log(
1903
- "STATE_TRANSITION: New items arrived while locked. -> GETTING_ARRAY_HEIGHTS"
1903
+ "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1904
1904
  );
1905
- setStatus("GETTING_ARRAY_HEIGHTS");
1905
+ setStatus("GETTING_HEIGHTS");
1906
1906
  }
1907
1907
 
1908
1908
  prevTotalCountRef.current = totalCount;
1909
1909
  prevDepsRef.current = dependencies;
1910
1910
  }, [totalCount, ...dependencies]);
1911
1911
 
1912
- // --- STATE MACHINE ACTIONS ---
1913
- // This effect handles the actions for each state.
1912
+ // --- 2. STATE ACTION HANDLER ---
1913
+ // This effect performs the ACTION for the current state.
1914
1914
  useLayoutEffect(() => {
1915
1915
  const container = containerRef.current;
1916
1916
  if (!container) return;
1917
1917
 
1918
1918
  let intervalId: NodeJS.Timeout | undefined;
1919
- let scrollTimeoutId: NodeJS.Timeout | undefined;
1920
1919
 
1921
1920
  if (
1922
- status === "WAITING_FOR_ARRAY" &&
1923
- totalCount > 0 &&
1924
- stickToBottom
1921
+ status === "IDLE_AT_TOP" &&
1922
+ stickToBottom &&
1923
+ totalCount > 0
1925
1924
  ) {
1925
+ // If we just loaded a new chat, start the process.
1926
1926
  console.log(
1927
- "ACTION: WAITING_FOR_ARRAY -> GETTING_ARRAY_HEIGHTS"
1927
+ "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1928
1928
  );
1929
- setStatus("GETTING_ARRAY_HEIGHTS");
1930
- } else if (status === "GETTING_ARRAY_HEIGHTS") {
1929
+ setStatus("GETTING_HEIGHTS");
1930
+ } else if (status === "GETTING_HEIGHTS") {
1931
1931
  console.log(
1932
- "ACTION: GETTING_ARRAY_HEIGHTS. Setting range and starting loop."
1932
+ "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1933
1933
  );
1934
1934
  setRange({
1935
1935
  startIndex: Math.max(0, totalCount - 10 - overscan),
@@ -1948,13 +1948,16 @@ function createProxyHandler<T>(
1948
1948
  if (lastItemHeight > 0) {
1949
1949
  clearInterval(intervalId);
1950
1950
  console.log(
1951
- "ACTION: Measurement success. -> MOVING_TO_BOTTOM"
1951
+ "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1952
1952
  );
1953
- setStatus("MOVING_TO_BOTTOM");
1953
+ setStatus("SCROLLING_TO_BOTTOM");
1954
1954
  }
1955
1955
  }, 100);
1956
- } else if (status === "MOVING_TO_BOTTOM") {
1957
- console.log("ACTION: MOVING_TO_BOTTOM. Executing scroll.");
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.
1958
1961
  const scrollBehavior =
1959
1962
  prevTotalCountRef.current === 0 ? "auto" : "smooth";
1960
1963
 
@@ -1963,55 +1966,49 @@ function createProxyHandler<T>(
1963
1966
  behavior: scrollBehavior,
1964
1967
  });
1965
1968
 
1966
- scrollTimeoutId = setTimeout(
1969
+ const timeoutId = setTimeout(
1967
1970
  () => {
1968
1971
  console.log(
1969
- "ACTION: Scroll finished. -> LOCKED_AT_BOTTOM"
1972
+ "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1970
1973
  );
1971
1974
  setStatus("LOCKED_AT_BOTTOM");
1972
1975
  },
1973
1976
  scrollBehavior === "smooth" ? 500 : 50
1974
1977
  );
1978
+
1979
+ return () => clearTimeout(timeoutId);
1975
1980
  }
1976
1981
 
1977
- // THE FIX: This cleanup runs whenever the state changes, killing any active timers.
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
+
1978
1985
  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
- }
1986
+ if (intervalId) clearInterval(intervalId);
1987
1987
  };
1988
1988
  }, [status, totalCount, positions]);
1989
1989
 
1990
- // --- USER INTERACTION ---
1991
- // This effect only handles user scrolls.
1990
+ // --- 3. USER INTERACTION & RANGE UPDATER ---
1992
1991
  useEffect(() => {
1993
1992
  const container = containerRef.current;
1994
1993
  if (!container) return;
1995
1994
 
1996
1995
  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");
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
+ }
2012
2009
  }
2013
-
2014
- // Update the rendered range based on scroll position.
2010
+ // We always update the range, regardless of state.
2011
+ // This is the full, non-placeholder function.
2015
2012
  const { scrollTop, clientHeight } = container;
2016
2013
  let low = 0,
2017
2014
  high = totalCount - 1;
@@ -2040,27 +2037,24 @@ function createProxyHandler<T>(
2040
2037
  });
2041
2038
  return () =>
2042
2039
  container.removeEventListener("scroll", handleUserScroll);
2043
- }, [totalCount, positions, status]);
2040
+ }, [totalCount, positions, status]); // Depends on status to know if it should break the lock
2044
2041
 
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
- );
2042
+ const scrollToBottom = useCallback(() => {
2043
+ console.log(
2044
+ "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2045
+ );
2046
+ setStatus("SCROLLING_TO_BOTTOM");
2047
+ }, []);
2054
2048
 
2055
2049
  const scrollToIndex = useCallback(
2056
2050
  (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
- // }
2051
+ if (containerRef.current && positions[index] !== undefined) {
2052
+ setStatus("IDLE_NOT_AT_BOTTOM");
2053
+ containerRef.current.scrollTo({
2054
+ top: positions[index],
2055
+ behavior,
2056
+ });
2057
+ }
2064
2058
  },
2065
2059
  [positions]
2066
2060
  );