cogsbox-state 0.5.364 → 0.5.365

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.365",
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,136 @@ 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;
1910
1919
 
1911
- // We only start the loop if the range is correctly set to the end and we are locked.
1912
1920
  if (
1913
- !container ||
1914
- !isLockedToBottomRef.current ||
1915
- !isRangeSetToEnd
1921
+ status === "WAITING_FOR_ARRAY" &&
1922
+ totalCount > 0 &&
1923
+ stickToBottom
1916
1924
  ) {
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...`);
1925
+ console.log(
1926
+ "ACTION: WAITING_FOR_ARRAY -> GETTING_ARRAY_HEIGHTS"
1927
+ );
1928
+ setStatus("GETTING_ARRAY_HEIGHTS");
1929
+ } else if (status === "GETTING_ARRAY_HEIGHTS") {
1930
+ console.log(
1931
+ "ACTION: GETTING_ARRAY_HEIGHTS. Setting range and starting loop."
1932
+ );
1933
+ setRange({
1934
+ startIndex: Math.max(0, totalCount - 10 - overscan),
1935
+ endIndex: totalCount,
1936
+ });
1927
1937
 
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;
1938
+ intervalId = setInterval(() => {
1939
+ const lastItemIndex = totalCount - 1;
1940
+ const shadowArray =
1941
+ getGlobalStore
1942
+ .getState()
1943
+ .getShadowMetadata(stateKey, path) || [];
1944
+ const lastItemHeight =
1945
+ shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1946
+
1947
+ if (lastItemHeight > 0) {
1948
+ clearInterval(intervalId);
1949
+ console.log(
1950
+ "ACTION: Measurement success. -> MOVING_TO_BOTTOM"
1951
+ );
1952
+ setStatus("MOVING_TO_BOTTOM");
1953
+ }
1954
+ }, 100);
1955
+ } else if (status === "MOVING_TO_BOTTOM") {
1956
+ console.log("ACTION: MOVING_TO_BOTTOM. Executing scroll.");
1957
+ const scrollBehavior =
1958
+ prevTotalCountRef.current === 0 ? "auto" : "smooth";
1959
+
1960
+ container.scrollTo({
1961
+ top: container.scrollHeight,
1962
+ behavior: scrollBehavior,
1963
+ });
1935
1964
 
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);
1965
+ // After scrolling, we are locked at the bottom.
1966
+ // Use a timeout to wait for the animation to finish.
1967
+ const timeoutId = setTimeout(
1968
+ () => {
1969
+ console.log(
1970
+ "ACTION: Scroll finished. -> LOCKED_AT_BOTTOM"
1971
+ );
1972
+ setStatus("LOCKED_AT_BOTTOM");
1973
+ },
1974
+ scrollBehavior === "smooth" ? 500 : 50
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
- );
1956
- clearInterval(intervalId);
1957
- } else {
1958
- console.log("...WAITING. Height is not ready.");
1959
- }
1960
- }, 100);
1977
+ return () => clearTimeout(timeoutId);
1978
+ }
1961
1979
 
1962
- return () => clearInterval(intervalId);
1963
- }, [range]); // This effect is triggered by the `setRange` call in Phase 1.
1980
+ return () => {
1981
+ if (intervalId) clearInterval(intervalId);
1982
+ };
1983
+ }, [status, totalCount, positions]);
1964
1984
 
1965
- // --- PHASE 3: Handle User Scrolling ---
1985
+ // --- USER INTERACTION ---
1986
+ // This effect only handles user scrolls.
1966
1987
  useEffect(() => {
1967
1988
  const container = containerRef.current;
1968
1989
  if (!container) return;
1969
1990
 
1970
- const updateVirtualRange = () => {
1991
+ const handleUserScroll = () => {
1992
+ const isAtBottom =
1993
+ container.scrollHeight -
1994
+ container.scrollTop -
1995
+ container.clientHeight <
1996
+ 1;
1997
+ // If the user scrolls up from a locked or scrolling state, move to idle.
1998
+ if (
1999
+ !isAtBottom &&
2000
+ (status === "LOCKED_AT_BOTTOM" ||
2001
+ status === "MOVING_TO_BOTTOM")
2002
+ ) {
2003
+ console.log(
2004
+ "USER_ACTION: Scrolled up. -> IDLE_NOT_AT_BOTTOM"
2005
+ );
2006
+ setStatus("IDLE_NOT_AT_BOTTOM");
2007
+ }
2008
+
2009
+ // Update the rendered range based on scroll position.
1971
2010
  const { scrollTop, clientHeight } = container;
1972
2011
  let low = 0,
1973
2012
  high = totalCount - 1;
@@ -1991,50 +2030,32 @@ function createProxyHandler<T>(
1991
2030
  });
1992
2031
  };
1993
2032
 
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
2033
  container.addEventListener("scroll", handleUserScroll, {
2008
2034
  passive: true,
2009
2035
  });
2010
- updateVirtualRange(); // Always run to set the initial view
2011
-
2012
2036
  return () =>
2013
2037
  container.removeEventListener("scroll", handleUserScroll);
2014
- }, [totalCount, positions, ...dependencies]);
2038
+ }, [totalCount, positions, status]);
2015
2039
 
2016
2040
  const scrollToBottom = useCallback(
2017
2041
  (behavior: ScrollBehavior = "smooth") => {
2018
- if (containerRef.current) {
2019
- isLockedToBottomRef.current = true;
2020
- containerRef.current.scrollTo({
2021
- top: containerRef.current.scrollHeight,
2022
- behavior,
2023
- });
2024
- }
2042
+ console.log(
2043
+ "USER_ACTION: Clicked scroll to bottom button. -> MOVING_TO_BOTTOM"
2044
+ );
2045
+ setStatus("MOVING_TO_BOTTOM");
2025
2046
  },
2026
2047
  []
2027
2048
  );
2028
2049
 
2029
2050
  const scrollToIndex = useCallback(
2030
2051
  (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
- }
2052
+ // if (containerRef.current && positions[index] !== undefined) {
2053
+ // isLockedToBottomRef.current = false;
2054
+ // containerRef.current.scrollTo({
2055
+ // top: positions[index],
2056
+ // behavior,
2057
+ // });
2058
+ // }
2038
2059
  },
2039
2060
  [positions]
2040
2061
  );