cogsbox-state 0.5.369 → 0.5.371

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.369",
3
+ "version": "0.5.371",
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,18 +1809,23 @@ function createProxyHandler<T>(
1809
1809
  dependencies = [],
1810
1810
  } = options;
1811
1811
 
1812
+ // YOUR STATE MACHINE STATES
1812
1813
  type Status =
1813
- | "IDLE" // User is in control, or we are resting at the bottom.
1814
- | "SCROLLING_TO_BOTTOM"; // Performing a scroll animation.
1814
+ | "IDLE_AT_TOP"
1815
+ | "GETTING_HEIGHTS"
1816
+ | "SCROLLING_TO_BOTTOM"
1817
+ | "LOCKED_AT_BOTTOM"
1818
+ | "IDLE_NOT_AT_BOTTOM";
1815
1819
 
1820
+ const shouldNotScroll = useRef(false);
1816
1821
  const containerRef = useRef<HTMLDivElement | null>(null);
1817
1822
  const [range, setRange] = useState({
1818
1823
  startIndex: 0,
1819
1824
  endIndex: 10,
1820
1825
  });
1821
- const [status, setStatus] = useState<Status>("IDLE");
1822
- const prevTotalCountRef = useRef(0);
1826
+ const [status, setStatus] = useState<Status>("IDLE_AT_TOP");
1823
1827
 
1828
+ const prevTotalCountRef = useRef(0);
1824
1829
  const prevDepsRef = useRef(dependencies);
1825
1830
 
1826
1831
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
@@ -1861,6 +1866,7 @@ function createProxyHandler<T>(
1861
1866
  shadowUpdateTrigger,
1862
1867
  ]);
1863
1868
 
1869
+ // THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
1864
1870
  const virtualState = useMemo(() => {
1865
1871
  const start = Math.max(0, range.startIndex);
1866
1872
  const end = Math.min(totalCount, range.endIndex);
@@ -1875,82 +1881,141 @@ function createProxyHandler<T>(
1875
1881
  });
1876
1882
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1877
1883
 
1878
- // --- Main Controller Effect ---
1884
+ // --- 1. STATE CONTROLLER ---
1885
+ // This effect decides which state to transition TO.
1879
1886
  useLayoutEffect(() => {
1880
- const container = containerRef.current;
1881
- if (!container) return;
1882
-
1883
1887
  const depsChanged = !isDeepEqual(
1884
1888
  dependencies,
1885
1889
  prevDepsRef.current
1886
1890
  );
1887
1891
  const hasNewItems = totalCount > prevTotalCountRef.current;
1888
1892
 
1889
- // 1. On Chat Change: Reset to a clean state and trigger an instant scroll.
1890
1893
  if (depsChanged) {
1894
+ console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1895
+ setStatus("IDLE_AT_TOP");
1896
+ return; // Stop here, let the next effect handle the action for the new state.
1897
+ }
1898
+
1899
+ if (
1900
+ hasNewItems &&
1901
+ status === "LOCKED_AT_BOTTOM" &&
1902
+ stickToBottom
1903
+ ) {
1891
1904
  console.log(
1892
- "EVENT: Chat changed. Resetting state and scrolling."
1905
+ "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1893
1906
  );
1894
- setStatus("SCROLLING_TO_BOTTOM");
1895
- // This return is important. It lets the effect re-run with the new status.
1896
- return;
1907
+ setStatus("GETTING_HEIGHTS");
1897
1908
  }
1898
1909
 
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
- }
1910
+ prevTotalCountRef.current = totalCount;
1911
+ prevDepsRef.current = dependencies;
1912
+ }, [totalCount, ...dependencies]);
1913
+
1914
+ // --- 2. STATE ACTION HANDLER ---
1915
+ // This effect performs the ACTION for the current state.
1916
+ useLayoutEffect(() => {
1917
+ const container = containerRef.current;
1918
+ if (!container) return;
1915
1919
 
1916
- let scrollTimeoutId: NodeJS.Timeout | undefined;
1920
+ let intervalId: NodeJS.Timeout | undefined;
1917
1921
 
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";
1922
+ if (
1923
+ status === "IDLE_AT_TOP" &&
1924
+ stickToBottom &&
1925
+ totalCount > 0
1926
+ ) {
1927
+ // If we just loaded a new chat, start the process.
1928
+ console.log(
1929
+ "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1930
+ );
1931
+ setStatus("GETTING_HEIGHTS");
1932
+ } else if (status === "GETTING_HEIGHTS") {
1924
1933
  console.log(
1925
- `ACTION: Scrolling to bottom. Behavior: ${scrollBehavior}`
1934
+ "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1926
1935
  );
1936
+ setRange({
1937
+ startIndex: Math.max(0, totalCount - 10 - overscan),
1938
+ endIndex: totalCount,
1939
+ });
1940
+
1941
+ intervalId = setInterval(() => {
1942
+ const lastItemIndex = totalCount - 1;
1943
+ const shadowArray =
1944
+ getGlobalStore
1945
+ .getState()
1946
+ .getShadowMetadata(stateKey, path) || [];
1947
+ const lastItemHeight =
1948
+ shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1949
+
1950
+ if (lastItemHeight > 0) {
1951
+ clearInterval(intervalId);
1952
+ console.log(
1953
+ "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
1954
+ );
1955
+ setStatus("SCROLLING_TO_BOTTOM");
1956
+ }
1957
+ }, 100);
1958
+ } else if (
1959
+ status === "SCROLLING_TO_BOTTOM" &&
1960
+ !shouldNotScroll.current
1961
+ ) {
1962
+ console.log(
1963
+ "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1964
+ );
1965
+ // Use 'auto' for initial load, 'smooth' for new messages.
1966
+ const scrollBehavior =
1967
+ prevTotalCountRef.current === 0 ? "auto" : "smooth";
1927
1968
 
1928
1969
  container.scrollTo({
1929
1970
  top: container.scrollHeight,
1930
1971
  behavior: scrollBehavior,
1931
1972
  });
1932
1973
 
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(
1974
+ const timeoutId = setTimeout(
1936
1975
  () => {
1937
- console.log("ACTION: Scroll finished. -> IDLE");
1938
- setStatus("IDLE");
1976
+ console.log(
1977
+ "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1978
+ );
1979
+ shouldNotScroll.current = false;
1980
+ setStatus("LOCKED_AT_BOTTOM");
1939
1981
  },
1940
- isInitial ? 50 : 500
1982
+ scrollBehavior === "smooth" ? 500 : 50
1941
1983
  );
1984
+
1985
+ return () => clearTimeout(timeoutId);
1942
1986
  }
1943
1987
 
1944
- // --- User Scroll Handling & Range Update ---
1988
+ // If status is IDLE_NOT_AT_BOTTOM or LOCKED_AT_BOTTOM, we do nothing here.
1989
+ // The scroll has either finished or been disabled by the user.
1990
+
1991
+ return () => {
1992
+ if (intervalId) clearInterval(intervalId);
1993
+ };
1994
+ }, [status, totalCount, positions]);
1995
+
1996
+ // --- 3. USER INTERACTION & RANGE UPDATER ---
1997
+ useEffect(() => {
1998
+ const container = containerRef.current;
1999
+ if (!container) return;
2000
+
1945
2001
  const handleUserScroll = () => {
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");
2002
+ // This is the core logic you wanted.
2003
+ if (status !== "IDLE_NOT_AT_BOTTOM") {
2004
+ const isAtBottom =
2005
+ container.scrollHeight -
2006
+ container.scrollTop -
2007
+ container.clientHeight <
2008
+ 1;
2009
+ if (!isAtBottom) {
2010
+ console.log(
2011
+ "USER ACTION: Scrolled up -> IDLE_NOT_AT_BOTTOM"
2012
+ );
2013
+ shouldNotScroll.current = true;
2014
+ setStatus("IDLE_NOT_AT_BOTTOM");
2015
+ }
1951
2016
  }
1952
-
1953
- // This is the full range update function.
2017
+ // We always update the range, regardless of state.
2018
+ // This is the full, non-placeholder function.
1954
2019
  const { scrollTop, clientHeight } = container;
1955
2020
  let low = 0,
1956
2021
  high = totalCount - 1;
@@ -1977,21 +2042,9 @@ function createProxyHandler<T>(
1977
2042
  container.addEventListener("scroll", handleUserScroll, {
1978
2043
  passive: true,
1979
2044
  });
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 () => {
2045
+ return () =>
1991
2046
  container.removeEventListener("scroll", handleUserScroll);
1992
- if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
1993
- };
1994
- }, [totalCount, positions, status, ...dependencies]);
2047
+ }, [totalCount, positions, status]); // Depends on status to know if it should break the lock
1995
2048
 
1996
2049
  const scrollToBottom = useCallback(() => {
1997
2050
  console.log(
@@ -2003,7 +2056,7 @@ function createProxyHandler<T>(
2003
2056
  const scrollToIndex = useCallback(
2004
2057
  (index: number, behavior: ScrollBehavior = "smooth") => {
2005
2058
  if (containerRef.current && positions[index] !== undefined) {
2006
- setStatus("IDLE");
2059
+ setStatus("IDLE_NOT_AT_BOTTOM");
2007
2060
  containerRef.current.scrollTo({
2008
2061
  top: positions[index],
2009
2062
  behavior,