cogsbox-state 0.5.368 → 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.368",
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,16 +1809,21 @@ 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");
1825
+ const [status, setStatus] = useState<Status>("IDLE_AT_TOP");
1826
+
1822
1827
  const prevTotalCountRef = useRef(0);
1823
1828
  const prevDepsRef = useRef(dependencies);
1824
1829
 
@@ -1860,6 +1865,7 @@ function createProxyHandler<T>(
1860
1865
  shadowUpdateTrigger,
1861
1866
  ]);
1862
1867
 
1868
+ // THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
1863
1869
  const virtualState = useMemo(() => {
1864
1870
  const start = Math.max(0, range.startIndex);
1865
1871
  const end = Math.min(totalCount, range.endIndex);
@@ -1874,82 +1880,136 @@ function createProxyHandler<T>(
1874
1880
  });
1875
1881
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1876
1882
 
1877
- // --- Main Controller Effect ---
1883
+ // --- 1. STATE CONTROLLER ---
1884
+ // This effect decides which state to transition TO.
1878
1885
  useLayoutEffect(() => {
1879
- const container = containerRef.current;
1880
- if (!container) return;
1881
-
1882
1886
  const depsChanged = !isDeepEqual(
1883
1887
  dependencies,
1884
1888
  prevDepsRef.current
1885
1889
  );
1886
1890
  const hasNewItems = totalCount > prevTotalCountRef.current;
1887
1891
 
1888
- // 1. On Chat Change: Reset to a clean state and trigger an instant scroll.
1889
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
+ ) {
1890
1903
  console.log(
1891
- "EVENT: Chat changed. Resetting state and scrolling."
1904
+ "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1892
1905
  );
1893
- setStatus("SCROLLING_TO_BOTTOM");
1894
- // This return is important. It lets the effect re-run with the new status.
1895
- return;
1906
+ setStatus("GETTING_HEIGHTS");
1896
1907
  }
1897
1908
 
1898
- // 2. On New Message: Check if we SHOULD scroll.
1899
- // This will only happen if we are IDLE (meaning we are already at the bottom).
1900
- if (hasNewItems && status === "IDLE") {
1901
- const isAtBottom =
1902
- container.scrollHeight -
1903
- container.scrollTop -
1904
- container.clientHeight <
1905
- itemHeight;
1906
- if (isAtBottom && stickToBottom) {
1907
- console.log(
1908
- "EVENT: New message arrived while at bottom. -> SCROLLING_TO_BOTTOM"
1909
- );
1910
- setStatus("SCROLLING_TO_BOTTOM");
1911
- return; // Let the effect re-run with the new status.
1912
- }
1913
- }
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;
1914
1918
 
1915
- let scrollTimeoutId: NodeJS.Timeout | undefined;
1919
+ let intervalId: NodeJS.Timeout | undefined;
1916
1920
 
1917
- // 3. If we are in the SCROLLING state, perform the scroll action.
1918
- if (status === "SCROLLING_TO_BOTTOM") {
1919
- // A chat change or initial load should be instant. New messages should be smooth.
1920
- const isInitial =
1921
- prevTotalCountRef.current === 0 || depsChanged;
1922
- const scrollBehavior = isInitial ? "auto" : "smooth";
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") {
1923
1932
  console.log(
1924
- `ACTION: Scrolling to bottom. Behavior: ${scrollBehavior}`
1933
+ "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1925
1934
  );
1935
+ setRange({
1936
+ startIndex: Math.max(0, totalCount - 10 - overscan),
1937
+ endIndex: totalCount,
1938
+ });
1939
+
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") {
1958
+ console.log(
1959
+ "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
1960
+ );
1961
+ // Use 'auto' for initial load, 'smooth' for new messages.
1962
+ const scrollBehavior =
1963
+ prevTotalCountRef.current === 0 ? "auto" : "smooth";
1926
1964
 
1927
1965
  container.scrollTo({
1928
1966
  top: container.scrollHeight,
1929
1967
  behavior: scrollBehavior,
1930
1968
  });
1931
1969
 
1932
- // After the scroll, transition back to IDLE.
1933
- // The timeout lets the animation finish so this doesn't happen too early.
1934
- scrollTimeoutId = setTimeout(
1970
+ const timeoutId = setTimeout(
1935
1971
  () => {
1936
- console.log("ACTION: Scroll finished. -> IDLE");
1937
- setStatus("IDLE");
1972
+ console.log(
1973
+ "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
1974
+ );
1975
+ setStatus("LOCKED_AT_BOTTOM");
1938
1976
  },
1939
- isInitial ? 50 : 500
1977
+ scrollBehavior === "smooth" ? 500 : 50
1940
1978
  );
1979
+
1980
+ return () => clearTimeout(timeoutId);
1941
1981
  }
1942
1982
 
1943
- // --- 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
+
1944
1996
  const handleUserScroll = () => {
1945
- // If the user scrolls, they are in control. Immediately go to IDLE.
1946
- // This will also cancel any pending scroll-end timeouts.
1947
- if (status === "SCROLLING_TO_BOTTOM") {
1948
- console.log("USER ACTION: Interrupted scroll. -> IDLE");
1949
- 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
+ }
1950
2010
  }
1951
-
1952
- // This is the full range update function.
2011
+ // We always update the range, regardless of state.
2012
+ // This is the full, non-placeholder function.
1953
2013
  const { scrollTop, clientHeight } = container;
1954
2014
  let low = 0,
1955
2015
  high = totalCount - 1;
@@ -1976,21 +2036,9 @@ function createProxyHandler<T>(
1976
2036
  container.addEventListener("scroll", handleUserScroll, {
1977
2037
  passive: true,
1978
2038
  });
1979
-
1980
- // Always update range on render if we are IDLE.
1981
- if (status === "IDLE") {
1982
- handleUserScroll();
1983
- }
1984
-
1985
- // Update refs for the next render cycle.
1986
- prevTotalCountRef.current = totalCount;
1987
- prevDepsRef.current = dependencies;
1988
-
1989
- return () => {
2039
+ return () =>
1990
2040
  container.removeEventListener("scroll", handleUserScroll);
1991
- if (scrollTimeoutId) clearTimeout(scrollTimeoutId);
1992
- };
1993
- }, [totalCount, positions, status, ...dependencies]);
2041
+ }, [totalCount, positions, status]); // Depends on status to know if it should break the lock
1994
2042
 
1995
2043
  const scrollToBottom = useCallback(() => {
1996
2044
  console.log(
@@ -2002,7 +2050,7 @@ function createProxyHandler<T>(
2002
2050
  const scrollToIndex = useCallback(
2003
2051
  (index: number, behavior: ScrollBehavior = "smooth") => {
2004
2052
  if (containerRef.current && positions[index] !== undefined) {
2005
- setStatus("IDLE");
2053
+ setStatus("IDLE_NOT_AT_BOTTOM");
2006
2054
  containerRef.current.scrollTo({
2007
2055
  top: positions[index],
2008
2056
  behavior,