cogsbox-state 0.5.364 → 0.5.366

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.364",
3
+ "version": "0.5.366",
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,13 +1809,19 @@ function createProxyHandler<T>(
1809
1809
  dependencies = [],
1810
1810
  } = options;
1811
1811
 
1812
+ type Status =
1813
+ | "WAITING_FOR_ARRAY"
1814
+ | "GETTING_ARRAY_HEIGHTS"
1815
+ | "MOVING_TO_BOTTOM"
1816
+ | "LOCKED_AT_BOTTOM"
1817
+ | "IDLE_NOT_AT_BOTTOM";
1818
+
1812
1819
  const containerRef = useRef<HTMLDivElement | null>(null);
1813
1820
  const [range, setRange] = useState({
1814
1821
  startIndex: 0,
1815
1822
  endIndex: 10,
1816
1823
  });
1817
- const isLockedToBottomRef = useRef(stickToBottom);
1818
- const isAutoScrolling = useRef(false);
1824
+ const [status, setStatus] = useState<Status>("WAITING_FOR_ARRAY");
1819
1825
  const prevTotalCountRef = useRef(0);
1820
1826
  const prevDepsRef = useRef(dependencies);
1821
1827
 
@@ -1871,103 +1877,141 @@ function createProxyHandler<T>(
1871
1877
  });
1872
1878
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1873
1879
 
1874
- // --- PHASE 1: Detect change & SET THE RANGE ---
1875
- // This effect's ONLY job is to decide if we need to auto-scroll and then set the range to the end.
1880
+ // --- STATE MACHINE CONTROLLER ---
1881
+ // This effect decides which state to transition to based on external changes.
1876
1882
  useLayoutEffect(() => {
1877
- const hasNewItems = totalCount > prevTotalCountRef.current;
1878
1883
  const depsChanged = !isDeepEqual(
1879
1884
  dependencies,
1880
1885
  prevDepsRef.current
1881
1886
  );
1887
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1882
1888
 
1883
1889
  if (depsChanged) {
1884
- isLockedToBottomRef.current = stickToBottom;
1890
+ console.log(
1891
+ "STATE_TRANSITION: Deps changed. -> WAITING_FOR_ARRAY"
1892
+ );
1893
+ setStatus("WAITING_FOR_ARRAY");
1894
+ return;
1885
1895
  }
1886
1896
 
1887
1897
  if (
1888
- isLockedToBottomRef.current &&
1889
- (hasNewItems || depsChanged)
1898
+ hasNewItems &&
1899
+ status === "LOCKED_AT_BOTTOM" &&
1900
+ stickToBottom
1890
1901
  ) {
1891
1902
  console.log(
1892
- "PHASE 1: Auto-scroll needed. Setting range to render the last item."
1903
+ "STATE_TRANSITION: New items arrived while locked. -> GETTING_ARRAY_HEIGHTS"
1893
1904
  );
1894
- setRange({
1895
- startIndex: Math.max(0, totalCount - 10 - overscan),
1896
- endIndex: totalCount,
1897
- });
1905
+ setStatus("GETTING_ARRAY_HEIGHTS");
1898
1906
  }
1899
1907
 
1900
1908
  prevTotalCountRef.current = totalCount;
1901
1909
  prevDepsRef.current = dependencies;
1902
1910
  }, [totalCount, ...dependencies]);
1903
1911
 
1904
- // --- PHASE 2: Wait for measurement & SCROLL ---
1905
- // This effect's ONLY job is to run YOUR loop after Phase 1 is complete.
1912
+ // --- STATE MACHINE ACTIONS ---
1913
+ // This effect handles the actions for each state.
1906
1914
  useLayoutEffect(() => {
1907
1915
  const container = containerRef.current;
1908
- const isRangeSetToEnd =
1909
- range.endIndex === totalCount && totalCount > 0;
1916
+ if (!container) return;
1917
+
1918
+ let intervalId: NodeJS.Timeout | undefined;
1919
+ let scrollTimeoutId: NodeJS.Timeout | undefined;
1910
1920
 
1911
- // We only start the loop if the range is correctly set to the end and we are locked.
1912
1921
  if (
1913
- !container ||
1914
- !isLockedToBottomRef.current ||
1915
- !isRangeSetToEnd
1922
+ status === "WAITING_FOR_ARRAY" &&
1923
+ totalCount > 0 &&
1924
+ stickToBottom
1916
1925
  ) {
1917
- return;
1918
- }
1919
-
1920
- console.log(
1921
- "PHASE 2: Range is set to the end. Starting the measurement loop."
1922
- );
1923
- let loopCount = 0;
1924
- const intervalId = setInterval(() => {
1925
- loopCount++;
1926
- console.log(`LOOP ${loopCount}: Checking last item...`);
1926
+ console.log(
1927
+ "ACTION: WAITING_FOR_ARRAY -> GETTING_ARRAY_HEIGHTS"
1928
+ );
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
+ });
1927
1938
 
1928
- const lastItemIndex = totalCount - 1;
1929
- const shadowArray =
1930
- getGlobalStore
1931
- .getState()
1932
- .getShadowMetadata(stateKey, path) || [];
1933
- const lastItemHeight =
1934
- shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
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
+
1961
+ container.scrollTo({
1962
+ top: container.scrollHeight,
1963
+ behavior: scrollBehavior,
1964
+ });
1935
1965
 
1936
- if (lastItemHeight > 0) {
1937
- console.log(
1938
- `%cSUCCESS: Last item height is ${lastItemHeight}. Scrolling now.`,
1939
- "color: green; font-weight: bold;"
1940
- );
1941
- clearInterval(intervalId);
1966
+ scrollTimeoutId = setTimeout(
1967
+ () => {
1968
+ console.log(
1969
+ "ACTION: Scroll finished. -> LOCKED_AT_BOTTOM"
1970
+ );
1971
+ setStatus("LOCKED_AT_BOTTOM");
1972
+ },
1973
+ scrollBehavior === "smooth" ? 500 : 50
1974
+ );
1975
+ }
1942
1976
 
1943
- isAutoScrolling.current = true;
1944
- container.scrollTo({
1945
- top: container.scrollHeight,
1946
- behavior: "smooth",
1947
- });
1948
- // Give the animation time to finish before unsetting the flag
1949
- setTimeout(() => {
1950
- isAutoScrolling.current = false;
1951
- }, 1000);
1952
- } else if (loopCount > 30) {
1953
- console.error(
1954
- "LOOP TIMEOUT: Last item was never measured. Stopping loop."
1955
- );
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.");
1956
1981
  clearInterval(intervalId);
1957
- } else {
1958
- console.log("...WAITING. Height is not ready.");
1959
1982
  }
1960
- }, 100);
1961
-
1962
- return () => clearInterval(intervalId);
1963
- }, [range]); // This effect is triggered by the `setRange` call in Phase 1.
1983
+ if (scrollTimeoutId) {
1984
+ console.log("CLEANUP: Clearing scroll-end timer.");
1985
+ clearTimeout(scrollTimeoutId);
1986
+ }
1987
+ };
1988
+ }, [status, totalCount, positions]);
1964
1989
 
1965
- // --- PHASE 3: Handle User Scrolling ---
1990
+ // --- USER INTERACTION ---
1991
+ // This effect only handles user scrolls.
1966
1992
  useEffect(() => {
1967
1993
  const container = containerRef.current;
1968
1994
  if (!container) return;
1969
1995
 
1970
- const updateVirtualRange = () => {
1996
+ 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");
2012
+ }
2013
+
2014
+ // Update the rendered range based on scroll position.
1971
2015
  const { scrollTop, clientHeight } = container;
1972
2016
  let low = 0,
1973
2017
  high = totalCount - 1;
@@ -1991,50 +2035,32 @@ function createProxyHandler<T>(
1991
2035
  });
1992
2036
  };
1993
2037
 
1994
- const handleUserScroll = () => {
1995
- if (isAutoScrolling.current) return;
1996
- const isAtBottom =
1997
- container.scrollHeight -
1998
- container.scrollTop -
1999
- container.clientHeight <
2000
- 1;
2001
- if (!isAtBottom) {
2002
- isLockedToBottomRef.current = false;
2003
- }
2004
- updateVirtualRange();
2005
- };
2006
-
2007
2038
  container.addEventListener("scroll", handleUserScroll, {
2008
2039
  passive: true,
2009
2040
  });
2010
- updateVirtualRange(); // Always run to set the initial view
2011
-
2012
2041
  return () =>
2013
2042
  container.removeEventListener("scroll", handleUserScroll);
2014
- }, [totalCount, positions, ...dependencies]);
2043
+ }, [totalCount, positions, status]);
2015
2044
 
2016
2045
  const scrollToBottom = useCallback(
2017
2046
  (behavior: ScrollBehavior = "smooth") => {
2018
- if (containerRef.current) {
2019
- isLockedToBottomRef.current = true;
2020
- containerRef.current.scrollTo({
2021
- top: containerRef.current.scrollHeight,
2022
- behavior,
2023
- });
2024
- }
2047
+ console.log(
2048
+ "USER_ACTION: Clicked scroll to bottom button. -> MOVING_TO_BOTTOM"
2049
+ );
2050
+ setStatus("MOVING_TO_BOTTOM");
2025
2051
  },
2026
2052
  []
2027
2053
  );
2028
2054
 
2029
2055
  const scrollToIndex = useCallback(
2030
2056
  (index: number, behavior: ScrollBehavior = "smooth") => {
2031
- if (containerRef.current && positions[index] !== undefined) {
2032
- isLockedToBottomRef.current = false;
2033
- containerRef.current.scrollTo({
2034
- top: positions[index],
2035
- behavior,
2036
- });
2037
- }
2057
+ // if (containerRef.current && positions[index] !== undefined) {
2058
+ // isLockedToBottomRef.current = false;
2059
+ // containerRef.current.scrollTo({
2060
+ // top: positions[index],
2061
+ // behavior,
2062
+ // });
2063
+ // }
2038
2064
  },
2039
2065
  [positions]
2040
2066
  );