cogsbox-state 0.5.369 → 0.5.370

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