cogsbox-state 0.5.359 → 0.5.360

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.359",
3
+ "version": "0.5.360",
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
@@ -1808,7 +1808,7 @@ function createProxyHandler<T>(
1808
1808
  stickToBottom = false,
1809
1809
  dependencies = [],
1810
1810
  } = options;
1811
- const [instanceKey, setInstanceKey] = useState(0);
1811
+
1812
1812
  const containerRef = useRef<HTMLDivElement | null>(null);
1813
1813
  const [range, setRange] = useState({
1814
1814
  startIndex: 0,
@@ -1816,8 +1816,11 @@ function createProxyHandler<T>(
1816
1816
  });
1817
1817
  const isLockedToBottomRef = useRef(stickToBottom);
1818
1818
 
1819
- // This ref will hold the ID of our loop so we can stop it.
1820
- const scrollLoopId = useRef<NodeJS.Timeout | null>(null);
1819
+ // This flag prevents our own scroll animation from breaking the lock.
1820
+ const isAutoScrolling = useRef(false);
1821
+
1822
+ const prevDepsRef = useRef(dependencies);
1823
+ const prevTotalCountRef = useRef(0);
1821
1824
 
1822
1825
  const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
1823
1826
 
@@ -1837,6 +1840,7 @@ function createProxyHandler<T>(
1837
1840
  const totalCount = sourceArray.length;
1838
1841
 
1839
1842
  const { totalHeight, positions } = useMemo(() => {
1843
+ // ... same as before ...
1840
1844
  const shadowArray =
1841
1845
  getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1842
1846
  [];
@@ -1872,116 +1876,110 @@ function createProxyHandler<T>(
1872
1876
  });
1873
1877
  }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1874
1878
 
1875
- // --- EFFECT #1: THE SCROLL ALGORITHM ---
1876
- // This runs when the data or dependencies change.
1879
+ // The single, authoritative effect for ALL layout logic.
1877
1880
  useLayoutEffect(() => {
1878
1881
  const container = containerRef.current;
1879
- if (
1880
- !container ||
1881
- !stickToBottom ||
1882
- !isLockedToBottomRef.current
1883
- ) {
1884
- return;
1885
- }
1886
-
1887
- // If a loop is already running, stop it before starting a new one.
1888
- if (scrollLoopId.current) {
1889
- clearInterval(scrollLoopId.current);
1890
- }
1891
-
1892
- console.log("ALGORITHM: Starting...");
1893
-
1894
- // STEP 1: Set range to render the last item.
1895
- setRange({
1896
- startIndex: Math.max(0, totalCount - 10 - overscan),
1897
- endIndex: totalCount,
1898
- });
1882
+ if (!container) return;
1899
1883
 
1900
- // STEP 2: Start the LOOP.
1901
- console.log(
1902
- "ALGORITHM: Starting LOOP to wait for measurement."
1884
+ const depsChanged = !isDeepEqual(
1885
+ dependencies,
1886
+ prevDepsRef.current
1903
1887
  );
1904
- let loopCount = 0;
1905
- scrollLoopId.current = setInterval(() => {
1906
- loopCount++;
1907
- console.log(`LOOP ${loopCount}: Checking last item...`);
1908
-
1909
- const lastItemIndex = totalCount - 1;
1910
- if (lastItemIndex < 0) {
1911
- clearInterval(scrollLoopId.current!);
1912
- return;
1913
- }
1888
+ const hasNewItems = totalCount > prevTotalCountRef.current;
1914
1889
 
1915
- const shadowArray =
1916
- getGlobalStore
1917
- .getState()
1918
- .getShadowMetadata(stateKey, path) || [];
1919
- const lastItemHeight =
1920
- shadowArray[lastItemIndex]?.virtualizer?.itemHeight || 0;
1921
-
1922
- if (lastItemHeight > 0) {
1923
- console.log(
1924
- `%cSUCCESS: Last item is measured. Scrolling.`,
1925
- "color: green; font-weight: bold;"
1926
- );
1927
- clearInterval(scrollLoopId.current!);
1928
- scrollLoopId.current = null;
1890
+ if (depsChanged) {
1891
+ console.log("DEPENDENCY CHANGE: Resetting scroll lock.");
1892
+ isLockedToBottomRef.current = stickToBottom;
1893
+ }
1929
1894
 
1930
- container.scrollTo({
1931
- top: container.scrollHeight,
1932
- behavior: "smooth",
1933
- });
1934
- } else {
1935
- console.log("...WAITING. Height is not ready.");
1936
- if (loopCount > 30) {
1937
- console.error("LOOP TIMEOUT. Stopping.");
1938
- clearInterval(scrollLoopId.current!);
1939
- scrollLoopId.current = null;
1940
- }
1941
- }
1942
- }, 100);
1895
+ const shouldStartLoop =
1896
+ isLockedToBottomRef.current && (hasNewItems || depsChanged);
1943
1897
 
1944
- return () => {
1945
- if (scrollLoopId.current) {
1946
- clearInterval(scrollLoopId.current);
1898
+ const updateVirtualRange = () => {
1899
+ // This is the full, non-placeholder function.
1900
+ const { scrollTop, clientHeight } = container;
1901
+ let low = 0,
1902
+ high = totalCount - 1;
1903
+ while (low <= high) {
1904
+ const mid = Math.floor((low + high) / 2);
1905
+ if (positions[mid]! < scrollTop) low = mid + 1;
1906
+ else high = mid - 1;
1947
1907
  }
1908
+ const startIndex = Math.max(0, high - overscan);
1909
+ let endIndex = startIndex;
1910
+ const visibleEnd = scrollTop + clientHeight;
1911
+ while (
1912
+ endIndex < totalCount &&
1913
+ positions[endIndex]! < visibleEnd
1914
+ ) {
1915
+ endIndex++;
1916
+ }
1917
+ setRange({
1918
+ startIndex,
1919
+ endIndex: Math.min(totalCount, endIndex + overscan),
1920
+ });
1948
1921
  };
1949
- }, [totalCount, ...dependencies]);
1950
1922
 
1951
- // --- EFFECT #2: USER INTERACTION & RESET ---
1952
- // This handles manual scrolling and resetting when the chat changes.
1953
- useEffect(() => {
1954
- const container = containerRef.current;
1955
- if (!container) return;
1923
+ let intervalId: NodeJS.Timeout | undefined;
1956
1924
 
1957
- // When the chat changes (dependencies change), reset the lock.
1958
- console.log(
1959
- "DEPENDENCY CHANGE: Resetting scroll lock to:",
1960
- stickToBottom
1961
- );
1962
- isLockedToBottomRef.current = stickToBottom;
1925
+ if (shouldStartLoop) {
1926
+ // --- YOUR ALGORITHM ---
1927
+ console.log("ALGORITHM: Starting...");
1928
+ setRange({
1929
+ startIndex: Math.max(0, totalCount - 10 - overscan),
1930
+ endIndex: totalCount,
1931
+ });
1963
1932
 
1964
- const updateVirtualRange = () => {
1965
- /* ... same as before ... */
1966
- };
1933
+ intervalId = setInterval(() => {
1934
+ const lastItemIndex = totalCount - 1;
1935
+ if (lastItemIndex < 0) {
1936
+ clearInterval(intervalId);
1937
+ return;
1938
+ }
1939
+
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("%cSUCCESS: Scrolling now.", "color: green;");
1950
+
1951
+ // Set the flag to true before we start our animation.
1952
+ isAutoScrolling.current = true;
1953
+
1954
+ container.scrollTo({
1955
+ top: container.scrollHeight,
1956
+ behavior: "smooth",
1957
+ });
1958
+
1959
+ // After 1 second, assume animation is done and unset the flag.
1960
+ setTimeout(() => {
1961
+ isAutoScrolling.current = false;
1962
+ }, 1000);
1963
+ }
1964
+ }, 100);
1965
+ } else {
1966
+ updateVirtualRange();
1967
+ }
1967
1968
 
1968
1969
  const handleUserScroll = () => {
1970
+ // If our code is scrolling, ignore this event.
1971
+ if (isAutoScrolling.current) return;
1972
+
1969
1973
  const isAtBottom =
1970
1974
  container.scrollHeight -
1971
1975
  container.scrollTop -
1972
1976
  container.clientHeight <
1973
1977
  1;
1974
- if (!isAtBottom) {
1975
- if (isLockedToBottomRef.current) {
1976
- console.log("USER SCROLL: Lock broken.");
1977
- isLockedToBottomRef.current = false;
1978
- // If a scroll loop was running, kill it.
1979
- if (scrollLoopId.current) {
1980
- clearInterval(scrollLoopId.current);
1981
- scrollLoopId.current = null;
1982
- console.log("...Auto-scroll loop terminated by user.");
1983
- }
1984
- }
1978
+ if (!isAtBottom && isLockedToBottomRef.current) {
1979
+ console.log("USER SCROLL: Lock broken.");
1980
+ isLockedToBottomRef.current = false;
1981
+ // If a loop was somehow running, kill it.
1982
+ if (intervalId) clearInterval(intervalId);
1985
1983
  }
1986
1984
  updateVirtualRange();
1987
1985
  };
@@ -1989,24 +1987,17 @@ function createProxyHandler<T>(
1989
1987
  container.addEventListener("scroll", handleUserScroll, {
1990
1988
  passive: true,
1991
1989
  });
1992
- updateVirtualRange();
1993
1990
 
1994
- return () =>
1991
+ // Update refs for the next render.
1992
+ prevDepsRef.current = dependencies;
1993
+ prevTotalCountRef.current = totalCount;
1994
+
1995
+ return () => {
1995
1996
  container.removeEventListener("scroll", handleUserScroll);
1996
- }, [totalCount, positions]);
1997
- useEffect(() => {
1998
- console.log(
1999
- "DEPENDENCY CHANGE: Resetting scroll lock and scrolling to bottom."
2000
- );
2001
- if (containerRef.current) {
2002
- // Reset the lock so the main effect can take over on the next data load.
2003
- isLockedToBottomRef.current = stickToBottom;
2004
- // Scroll to the top to show the loading state for the new chat.
2005
- containerRef.current.scrollTop = 0;
2006
- // Reset the range
2007
- setRange({ startIndex: 0, endIndex: 10 });
2008
- }
2009
- }, dependencies);
1997
+ if (intervalId) clearInterval(intervalId);
1998
+ };
1999
+ }, [totalCount, positions, ...dependencies]);
2000
+
2010
2001
  const scrollToBottom = useCallback(
2011
2002
  (behavior: ScrollBehavior = "smooth") => {
2012
2003
  if (containerRef.current) {