cogsbox-state 0.5.402 → 0.5.404

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.402",
3
+ "version": "0.5.404",
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
@@ -1803,6 +1803,8 @@ function createProxyHandler<T>(
1803
1803
  return selectedIndex ?? -1;
1804
1804
  };
1805
1805
  }
1806
+ // Simplified useVirtualView approach
1807
+ // Optimal approach - replace the useVirtualView implementation
1806
1808
  if (prop === "useVirtualView") {
1807
1809
  return (
1808
1810
  options: VirtualViewOptions
@@ -1814,30 +1816,19 @@ function createProxyHandler<T>(
1814
1816
  dependencies = [],
1815
1817
  } = options;
1816
1818
 
1817
- // YOUR STATE MACHINE STATES
1818
- type Status =
1819
- | "IDLE_AT_TOP"
1820
- | "GETTING_HEIGHTS"
1821
- | "SCROLLING_TO_BOTTOM"
1822
- | "LOCKED_AT_BOTTOM"
1823
- | "IDLE_NOT_AT_BOTTOM";
1824
-
1825
- const shouldNotScroll = useRef(false);
1826
1819
  const containerRef = useRef<HTMLDivElement | null>(null);
1827
1820
  const [range, setRange] = useState({
1828
1821
  startIndex: 0,
1829
1822
  endIndex: 10,
1830
1823
  });
1831
- const [status, setStatus] = useState<Status>("IDLE_AT_TOP");
1832
- const isProgrammaticScroll = useRef(false);
1833
- const prevTotalCountRef = useRef(0);
1834
- const prevDepsRef = useRef(dependencies);
1835
- const lastUpdateAtScrollTop = useRef(0);
1836
1824
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1837
- const scrollAnchorRef = useRef<{
1838
- top: number;
1839
- height: number;
1840
- } | null>(null);
1825
+ const isUserScrollingRef = useRef(false);
1826
+ const shouldStickToBottomRef = useRef(true);
1827
+ const scrollToBottomIntervalRef = useRef<NodeJS.Timeout | null>(
1828
+ null
1829
+ );
1830
+
1831
+ // Subscribe to shadow state updates
1841
1832
  useEffect(() => {
1842
1833
  const unsubscribe = getGlobalStore
1843
1834
  .getState()
@@ -1853,6 +1844,7 @@ function createProxyHandler<T>(
1853
1844
  ) as any[];
1854
1845
  const totalCount = sourceArray.length;
1855
1846
 
1847
+ // Calculate heights and positions
1856
1848
  const { totalHeight, positions } = useMemo(() => {
1857
1849
  const shadowArray =
1858
1850
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
@@ -1874,7 +1866,7 @@ function createProxyHandler<T>(
1874
1866
  shadowUpdateTrigger,
1875
1867
  ]);
1876
1868
 
1877
- // THIS IS THE FULL, NON-PLACEHOLDER FUNCTION
1869
+ // Create virtual state
1878
1870
  const virtualState = useMemo(() => {
1879
1871
  const start = Math.max(0, range.startIndex);
1880
1872
  const end = Math.min(totalCount, range.endIndex);
@@ -1889,256 +1881,147 @@ function createProxyHandler<T>(
1889
1881
  });
1890
1882
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1891
1883
 
1892
- // --- 1. STATE CONTROLLER ---
1893
- // --- 1. STATE CONTROLLER (CORRECTED) ---
1894
- useLayoutEffect(() => {
1895
- const container = containerRef.current;
1896
- if (!container) return;
1897
-
1898
- const hasNewItems = totalCount > prevTotalCountRef.current;
1899
-
1900
- // THIS IS THE NEW, IMPORTANT LOGIC FOR ANCHORING
1901
- if (hasNewItems && scrollAnchorRef.current) {
1902
- const { top: prevScrollTop, height: prevScrollHeight } =
1903
- scrollAnchorRef.current;
1904
-
1905
- // This is the key: Tell the app we are about to programmatically scroll.
1906
- isProgrammaticScroll.current = true;
1907
-
1908
- // Restore the scroll position.
1909
- container.scrollTop =
1910
- prevScrollTop + (container.scrollHeight - prevScrollHeight);
1911
- console.log(
1912
- `ANCHOR RESTORED to scrollTop: ${container.scrollTop}`
1913
- );
1914
-
1915
- // IMPORTANT: After the scroll, allow user scroll events again.
1916
- // Use a timeout to ensure this runs after the scroll event has fired and been ignored.
1917
- setTimeout(() => {
1918
- isProgrammaticScroll.current = false;
1919
- }, 100);
1920
-
1921
- scrollAnchorRef.current = null; // Clear the anchor after using it.
1922
- }
1923
- // YOUR ORIGINAL LOGIC CONTINUES UNCHANGED IN THE `ELSE` BLOCK
1924
- else {
1925
- const depsChanged = !isDeepEqual(
1926
- dependencies,
1927
- prevDepsRef.current
1928
- );
1929
-
1930
- if (depsChanged) {
1931
- console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
1932
- setStatus("IDLE_AT_TOP");
1933
- return;
1934
- }
1884
+ // Handle auto-scroll to bottom
1885
+ useEffect(() => {
1886
+ if (!stickToBottom || !containerRef.current || totalCount === 0)
1887
+ return;
1888
+ if (!shouldStickToBottomRef.current) return;
1935
1889
 
1936
- if (
1937
- hasNewItems &&
1938
- status === "LOCKED_AT_BOTTOM" &&
1939
- stickToBottom
1940
- ) {
1941
- console.log(
1942
- "TRANSITION: New items arrived while locked -> GETTING_HEIGHTS"
1943
- );
1944
- setStatus("GETTING_HEIGHTS");
1945
- }
1890
+ // Clear any existing interval
1891
+ if (scrollToBottomIntervalRef.current) {
1892
+ clearInterval(scrollToBottomIntervalRef.current);
1946
1893
  }
1947
1894
 
1948
- prevTotalCountRef.current = totalCount;
1949
- prevDepsRef.current = dependencies;
1950
- }, [totalCount, ...dependencies]);
1951
-
1952
- // --- 2. STATE ACTION HANDLER ---
1953
- // This effect performs the ACTION for the current state.
1954
- useLayoutEffect(() => {
1955
- const container = containerRef.current;
1956
- if (!container) return;
1957
-
1958
- let intervalId: NodeJS.Timeout | undefined;
1895
+ // For initial load or big jumps, show the end immediately
1896
+ const jumpThreshold = 50;
1897
+ const isInitialLoad = range.endIndex < jumpThreshold;
1898
+ const isBigJump = totalCount > range.endIndex + jumpThreshold;
1959
1899
 
1960
- if (
1961
- status === "IDLE_AT_TOP" &&
1962
- stickToBottom &&
1963
- totalCount > 0
1964
- ) {
1965
- // If we just loaded a new chat, start the process.
1966
- console.log(
1967
- "ACTION (IDLE_AT_TOP): Data has arrived -> GETTING_HEIGHTS"
1968
- );
1969
- setStatus("GETTING_HEIGHTS");
1970
- } else if (status === "GETTING_HEIGHTS") {
1971
- console.log(
1972
- "ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
1973
- );
1900
+ if (isInitialLoad || isBigJump) {
1901
+ // Jump to show the last items immediately
1974
1902
  setRange({
1975
- startIndex: Math.max(0, totalCount - 10 - overscan),
1903
+ startIndex: Math.max(0, totalCount - 20),
1976
1904
  endIndex: totalCount,
1977
1905
  });
1906
+ }
1978
1907
 
1979
- let attemptCount = 0;
1980
- const maxAttempts = 50;
1908
+ // Keep scrolling to bottom until we're actually there
1909
+ let attempts = 0;
1910
+ const maxAttempts = 50; // 5 seconds max
1981
1911
 
1982
- intervalId = setInterval(() => {
1983
- attemptCount++;
1984
- const lastItemIndex = totalCount - 1;
1985
- const shadowArray =
1986
- getGlobalStore
1987
- .getState()
1988
- .getShadowMetadata(stateKey, path) || [];
1989
- const lastItemHeight =
1990
- shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1912
+ scrollToBottomIntervalRef.current = setInterval(() => {
1913
+ const container = containerRef.current;
1914
+ if (!container) return;
1991
1915
 
1992
- console.log(
1993
- `ACTION (GETTING_HEIGHTS): attempt ${attemptCount}, lastItemHeight =`,
1994
- lastItemHeight
1995
- );
1916
+ attempts++;
1996
1917
 
1997
- if (lastItemHeight > 0) {
1998
- clearInterval(intervalId);
1999
- console.log(
2000
- "ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
2001
- );
2002
- setStatus("SCROLLING_TO_BOTTOM");
2003
- } else if (attemptCount >= maxAttempts) {
2004
- clearInterval(intervalId);
2005
- console.log(
2006
- "ACTION (GETTING_HEIGHTS): Timeout - proceeding anyway"
2007
- );
2008
- setStatus("SCROLLING_TO_BOTTOM");
2009
- }
2010
- }, 100);
2011
- } else if (status === "SCROLLING_TO_BOTTOM") {
2012
- console.log(
2013
- "ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
2014
- );
2015
- isProgrammaticScroll.current = true;
2016
- // Use 'auto' for initial load, 'smooth' for new messages.
2017
- const scrollBehavior =
2018
- prevTotalCountRef.current === 0 ? "auto" : "smooth";
2019
-
2020
- container.scrollTo({
2021
- top: container.scrollHeight,
2022
- behavior: scrollBehavior,
2023
- });
1918
+ const { scrollTop, scrollHeight, clientHeight } = container;
1919
+ const currentBottom = scrollTop + clientHeight;
1920
+ const actualBottom = scrollHeight;
1921
+ const isAtBottom = actualBottom - currentBottom < 5;
2024
1922
 
2025
- const timeoutId = setTimeout(
2026
- () => {
2027
- console.log(
2028
- "ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
2029
- );
2030
- isProgrammaticScroll.current = false;
2031
- shouldNotScroll.current = false;
2032
- setStatus("LOCKED_AT_BOTTOM");
2033
- },
2034
- scrollBehavior === "smooth" ? 500 : 50
1923
+ console.log(
1924
+ `Scroll attempt ${attempts}: currentBottom=${currentBottom}, actualBottom=${actualBottom}, isAtBottom=${isAtBottom}`
2035
1925
  );
2036
1926
 
2037
- return () => clearTimeout(timeoutId);
2038
- }
1927
+ if (isAtBottom || attempts >= maxAttempts) {
1928
+ clearInterval(scrollToBottomIntervalRef.current!);
1929
+ scrollToBottomIntervalRef.current = null;
1930
+ console.log(
1931
+ isAtBottom ? "Reached bottom!" : "Timeout - giving up"
1932
+ );
1933
+ } else {
1934
+ // Use instant scroll, not smooth
1935
+ container.scrollTop = container.scrollHeight;
1936
+ }
1937
+ }, 100);
2039
1938
 
1939
+ // Cleanup
2040
1940
  return () => {
2041
- if (intervalId) clearInterval(intervalId);
1941
+ if (scrollToBottomIntervalRef.current) {
1942
+ clearInterval(scrollToBottomIntervalRef.current);
1943
+ scrollToBottomIntervalRef.current = null;
1944
+ }
2042
1945
  };
2043
- }, [status, totalCount, positions]);
1946
+ }, [totalCount, stickToBottom]);
2044
1947
 
1948
+ // Handle user scroll
2045
1949
  useEffect(() => {
2046
1950
  const container = containerRef.current;
2047
1951
  if (!container) return;
2048
1952
 
2049
- const scrollThreshold = itemHeight;
1953
+ let scrollTimeout: NodeJS.Timeout;
2050
1954
 
2051
- const handleUserScroll = () => {
2052
- // This guard is now critical. It will ignore our anchor restoration scroll.
2053
- if (isProgrammaticScroll.current) {
2054
- return;
1955
+ const handleScroll = () => {
1956
+ if (scrollToBottomIntervalRef.current) {
1957
+ // Stop auto-scrolling if user scrolls
1958
+ clearInterval(scrollToBottomIntervalRef.current);
1959
+ scrollToBottomIntervalRef.current = null;
2055
1960
  }
2056
1961
 
2057
1962
  const { scrollTop, scrollHeight, clientHeight } = container;
2058
-
2059
- // Part 1: Handle Status and Anchoring
2060
1963
  const isAtBottom =
2061
1964
  scrollHeight - scrollTop - clientHeight < 10;
2062
1965
 
2063
- if (isAtBottom) {
2064
- if (status !== "LOCKED_AT_BOTTOM") {
2065
- setStatus("LOCKED_AT_BOTTOM");
2066
- }
2067
- // If we are at the bottom, there is no anchor needed.
2068
- scrollAnchorRef.current = null;
2069
- } else {
2070
- if (status !== "IDLE_NOT_AT_BOTTOM") {
2071
- setStatus("IDLE_NOT_AT_BOTTOM");
2072
- }
2073
- // User is scrolled up. Continuously update the anchor with their latest position.
2074
- scrollAnchorRef.current = {
2075
- top: scrollTop,
2076
- height: scrollHeight,
2077
- };
2078
- }
2079
-
2080
- // Part 2: YOUR original, working logic for updating the visible range.
2081
- if (
2082
- Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
2083
- scrollThreshold
2084
- ) {
2085
- return;
2086
- }
2087
-
2088
- console.log(
2089
- `Threshold passed at ${scrollTop}px. Recalculating range...`
2090
- );
2091
-
2092
- // ... your logic to find startIndex and endIndex ...
2093
- let high = totalCount - 1;
2094
- let low = 0;
2095
- let topItemIndex = 0;
2096
- while (low <= high) {
2097
- const mid = Math.floor((low + high) / 2);
2098
- if (positions[mid]! < scrollTop) {
2099
- topItemIndex = mid;
2100
- low = mid + 1;
2101
- } else {
2102
- high = mid - 1;
1966
+ // Update whether we should stick to bottom
1967
+ shouldStickToBottomRef.current = isAtBottom;
1968
+
1969
+ // Mark as user scrolling
1970
+ clearTimeout(scrollTimeout);
1971
+ isUserScrollingRef.current = true;
1972
+ scrollTimeout = setTimeout(() => {
1973
+ isUserScrollingRef.current = false;
1974
+ }, 150);
1975
+
1976
+ // Update visible range
1977
+ let startIndex = 0;
1978
+ for (let i = 0; i < positions.length; i++) {
1979
+ if (positions[i]! > scrollTop - itemHeight * overscan) {
1980
+ startIndex = Math.max(0, i - 1);
1981
+ break;
2103
1982
  }
2104
1983
  }
2105
1984
 
2106
- const startIndex = Math.max(0, topItemIndex - overscan);
2107
1985
  let endIndex = startIndex;
2108
- const visibleEnd = scrollTop + clientHeight;
2109
- while (
2110
- endIndex < totalCount &&
2111
- positions[endIndex]! < visibleEnd
2112
- ) {
2113
- endIndex++;
1986
+ const viewportEnd = scrollTop + clientHeight;
1987
+ for (let i = startIndex; i < positions.length; i++) {
1988
+ if (positions[i]! > viewportEnd + itemHeight * overscan) {
1989
+ break;
1990
+ }
1991
+ endIndex = i;
2114
1992
  }
2115
1993
 
2116
1994
  setRange({
2117
- startIndex,
2118
- endIndex: Math.min(totalCount, endIndex + overscan),
1995
+ startIndex: Math.max(0, startIndex),
1996
+ endIndex: Math.min(totalCount, endIndex + 1 + overscan),
2119
1997
  });
2120
-
2121
- lastUpdateAtScrollTop.current = scrollTop;
2122
1998
  };
2123
1999
 
2124
- container.addEventListener("scroll", handleUserScroll, {
2000
+ container.addEventListener("scroll", handleScroll, {
2125
2001
  passive: true,
2126
2002
  });
2127
- return () =>
2128
- container.removeEventListener("scroll", handleUserScroll);
2129
- }, [totalCount, positions, itemHeight, overscan, status]);
2003
+ handleScroll(); // Initial calculation
2130
2004
 
2131
- const scrollToBottom = useCallback(() => {
2132
- console.log(
2133
- "USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
2134
- );
2135
- setStatus("SCROLLING_TO_BOTTOM");
2136
- }, []);
2005
+ return () => {
2006
+ container.removeEventListener("scroll", handleScroll);
2007
+ clearTimeout(scrollTimeout);
2008
+ };
2009
+ }, [positions, totalCount, itemHeight, overscan]);
2010
+
2011
+ const scrollToBottom = useCallback(
2012
+ (behavior: ScrollBehavior = "auto") => {
2013
+ shouldStickToBottomRef.current = true;
2014
+ if (containerRef.current) {
2015
+ containerRef.current.scrollTop =
2016
+ containerRef.current.scrollHeight;
2017
+ }
2018
+ },
2019
+ []
2020
+ );
2137
2021
 
2138
2022
  const scrollToIndex = useCallback(
2139
2023
  (index: number, behavior: ScrollBehavior = "smooth") => {
2140
2024
  if (containerRef.current && positions[index] !== undefined) {
2141
- setStatus("IDLE_NOT_AT_BOTTOM");
2142
2025
  containerRef.current.scrollTo({
2143
2026
  top: positions[index],
2144
2027
  behavior,