cogsbox-state 0.5.275 → 0.5.277

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.275",
3
+ "version": "0.5.277",
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
@@ -37,7 +37,6 @@ import { z } from "zod";
37
37
  import { formRefStore, getGlobalStore, type ComponentsType } from "./store.js";
38
38
  import { useCogsConfig } from "./CogsStateClient.js";
39
39
  import { applyPatch } from "fast-json-patch";
40
- import useMeasure from "react-use-measure";
41
40
 
42
41
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
43
42
 
@@ -1471,13 +1470,7 @@ function createProxyHandler<T>(
1471
1470
  function rebuildStateShape(
1472
1471
  currentState: T,
1473
1472
  path: string[] = [],
1474
- meta?: {
1475
- filtered?: string[][];
1476
- validIndices?: number[];
1477
- // --- NEW, OPTIONAL PROPERTIES ---
1478
- isVirtual?: boolean;
1479
- setVirtualItemHeight?: (index: number, height: number) => void;
1480
- }
1473
+ meta?: { filtered?: string[][]; validIndices?: number[] }
1481
1474
  ): any {
1482
1475
  const cacheKey = path.map(String).join(".");
1483
1476
 
@@ -1774,16 +1767,12 @@ function createProxyHandler<T>(
1774
1767
  };
1775
1768
  }
1776
1769
 
1777
- // This replaces the old `useVirtualView` implementation completely.
1778
-
1779
1770
  if (prop === "useVirtualView") {
1780
1771
  return (
1781
1772
  options: VirtualViewOptions
1782
1773
  ): VirtualStateObjectResult<any[]> => {
1783
- // We rename `itemHeight` to `estimatedItemHeight` for clarity.
1784
- // It's now just a fallback for items that haven't been measured yet.
1785
1774
  const {
1786
- itemHeight: estimatedItemHeight = 50, // A sensible default
1775
+ itemHeight,
1787
1776
  overscan = 5,
1788
1777
  stickToBottom = false,
1789
1778
  } = options;
@@ -1794,48 +1783,31 @@ function createProxyHandler<T>(
1794
1783
  endIndex: 10,
1795
1784
  });
1796
1785
 
1797
- // --- 1. LOCAL HEIGHT MANAGEMENT ---
1798
- // The heights are stored in state LOCAL to this hook.
1799
- const [measuredHeights, setMeasuredHeights] = useState(
1800
- () => new Map<number, number>()
1801
- );
1802
-
1803
- // The "height setter" function that will be passed down.
1804
- const setVirtualItemHeight = useCallback(
1805
- (index: number, height: number) => {
1806
- setMeasuredHeights((prev) => {
1807
- if (prev.get(index) === height) return prev; // Avoids infinite loops
1808
- const newMap = new Map(prev);
1809
- newMap.set(index, height);
1810
- return newMap;
1811
- });
1812
- },
1813
- []
1814
- );
1786
+ // --- State Tracking Refs ---
1787
+ const isAtBottomRef = useRef(stickToBottom);
1788
+ const previousTotalCountRef = useRef(0);
1789
+ // NEW: Ref to explicitly track if this is the component's first render cycle.
1790
+ const isInitialMountRef = useRef(true);
1815
1791
 
1816
- // --- 2. DATA AND META SETUP ---
1817
1792
  const sourceArray = getGlobalStore().getNestedState(
1818
1793
  stateKey,
1819
1794
  path
1820
1795
  ) as any[];
1821
1796
  const totalCount = sourceArray.length;
1822
1797
 
1823
- // --- 3. DYNAMIC LAYOUT CALCULATION ---
1824
- const { totalHeight, itemPositions } = useMemo(() => {
1825
- let total = 0;
1826
- const positions = Array(totalCount);
1827
- for (let i = 0; i < totalCount; i++) {
1828
- positions[i] = total;
1829
- total += measuredHeights.get(i) ?? estimatedItemHeight;
1830
- }
1831
- return { totalHeight: total, itemPositions: positions };
1832
- }, [totalCount, measuredHeights, estimatedItemHeight]);
1833
-
1834
- // --- 4. SCROLL HANDLING & RANGE CALCULATION ---
1835
- // Refs for the "stick to bottom" logic
1836
- const isAtBottomRef = useRef(stickToBottom);
1837
- const previousTotalCountRef = useRef(totalCount);
1838
- const isInitialMountRef = useRef(true);
1798
+ const virtualState = useMemo(() => {
1799
+ const start = Math.max(0, range.startIndex);
1800
+ const end = Math.min(totalCount, range.endIndex);
1801
+ const validIndices = Array.from(
1802
+ { length: end - start },
1803
+ (_, i) => start + i
1804
+ );
1805
+ const slicedArray = validIndices.map((idx) => sourceArray[idx]);
1806
+ return rebuildStateShape(slicedArray as any, path, {
1807
+ ...meta,
1808
+ validIndices,
1809
+ });
1810
+ }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1839
1811
 
1840
1812
  useLayoutEffect(() => {
1841
1813
  const container = containerRef.current;
@@ -1849,41 +1821,43 @@ function createProxyHandler<T>(
1849
1821
  const { scrollTop, clientHeight, scrollHeight } = container;
1850
1822
  isAtBottomRef.current =
1851
1823
  scrollHeight - scrollTop - clientHeight < 10;
1852
-
1853
- // Find the start index by searching through our dynamic positions array.
1854
- let startIndex = 0;
1855
- while (
1856
- startIndex < totalCount - 1 &&
1857
- itemPositions[startIndex + 1] <= scrollTop
1858
- ) {
1859
- startIndex++;
1860
- }
1861
-
1862
- // Find the end index.
1863
- let endIndex = startIndex;
1864
- while (
1865
- endIndex < totalCount - 1 &&
1866
- itemPositions[endIndex] < scrollTop + clientHeight
1867
- ) {
1868
- endIndex++;
1869
- }
1870
-
1871
- setRange({
1872
- startIndex: Math.max(0, startIndex - overscan),
1873
- endIndex: Math.min(totalCount - 1, endIndex + overscan),
1824
+ const start = Math.max(
1825
+ 0,
1826
+ Math.floor(scrollTop / itemHeight) - overscan
1827
+ );
1828
+ const end = Math.min(
1829
+ totalCount,
1830
+ Math.ceil((scrollTop + clientHeight) / itemHeight) +
1831
+ overscan
1832
+ );
1833
+ setRange((prevRange) => {
1834
+ if (
1835
+ prevRange.startIndex !== start ||
1836
+ prevRange.endIndex !== end
1837
+ ) {
1838
+ return { startIndex: start, endIndex: end };
1839
+ }
1840
+ return prevRange;
1874
1841
  });
1875
1842
  };
1876
1843
 
1877
- handleScroll(); // Initial calculation
1844
+ container.addEventListener("scroll", handleScroll, {
1845
+ passive: true,
1846
+ });
1878
1847
 
1879
- // Stick to bottom logic
1848
+ // --- THE CORRECTED DECISION LOGIC ---
1880
1849
  if (stickToBottom) {
1881
1850
  if (isInitialMountRef.current) {
1851
+ // SCENARIO 1: First render of the component.
1852
+ // Go to the bottom unconditionally. Use `auto` scroll for an instant jump.
1882
1853
  container.scrollTo({
1883
1854
  top: container.scrollHeight,
1884
1855
  behavior: "auto",
1885
1856
  });
1886
1857
  } else if (wasAtBottom && listGrew) {
1858
+ // SCENARIO 2: Subsequent renders (new messages arrive).
1859
+ // Only scroll if the user was already at the bottom.
1860
+ // Use `smooth` for a nice animated scroll for new messages.
1887
1861
  requestAnimationFrame(() => {
1888
1862
  container.scrollTo({
1889
1863
  top: container.scrollHeight,
@@ -1892,16 +1866,17 @@ function createProxyHandler<T>(
1892
1866
  });
1893
1867
  }
1894
1868
  }
1869
+
1870
+ // After the logic runs, it's no longer the initial mount.
1895
1871
  isInitialMountRef.current = false;
1896
1872
 
1897
- container.addEventListener("scroll", handleScroll, {
1898
- passive: true,
1899
- });
1873
+ // Always run handleScroll once to set the initial visible window.
1874
+ handleScroll();
1875
+
1900
1876
  return () =>
1901
1877
  container.removeEventListener("scroll", handleScroll);
1902
- }, [totalCount, overscan, stickToBottom, itemPositions]); // Reruns when positions change
1878
+ }, [totalCount, itemHeight, overscan, stickToBottom]);
1903
1879
 
1904
- // --- 5. API AND PROPS ---
1905
1880
  const scrollToBottom = useCallback(
1906
1881
  (behavior: ScrollBehavior = "smooth") => {
1907
1882
  if (containerRef.current) {
@@ -1916,57 +1891,38 @@ function createProxyHandler<T>(
1916
1891
 
1917
1892
  const scrollToIndex = useCallback(
1918
1893
  (index: number, behavior: ScrollBehavior = "smooth") => {
1919
- if (
1920
- containerRef.current &&
1921
- itemPositions[index] !== undefined
1922
- ) {
1894
+ if (containerRef.current) {
1923
1895
  containerRef.current.scrollTo({
1924
- top: itemPositions[index],
1896
+ top: index * itemHeight,
1925
1897
  behavior,
1926
1898
  });
1927
1899
  }
1928
1900
  },
1929
- [itemPositions]
1901
+ [itemHeight]
1930
1902
  );
1931
1903
 
1904
+ // Same virtualizer props as before
1932
1905
  const virtualizerProps = {
1933
1906
  outer: {
1934
1907
  ref: containerRef,
1935
- style: { overflowY: "auto" as const, height: "100%" },
1908
+ style: { overflowY: "auto", height: "100%" },
1936
1909
  },
1937
1910
  inner: {
1938
1911
  style: {
1939
- height: `${totalHeight}px`, // Use the dynamic total height
1940
- position: "relative" as const,
1912
+ height: `${totalCount * itemHeight}px`,
1913
+ position: "relative",
1941
1914
  },
1942
1915
  },
1943
- // This is now just a slice of the full array to map over.
1944
1916
  list: {
1945
- // We no longer need to apply a transform here because CogsItemWrapper
1946
- // will eventually be positioned absolutely by its index.
1947
- // For now, let's keep it simple. The user's code will map over virtualState.
1948
- style: {},
1917
+ style: {
1918
+ transform: `translateY(${range.startIndex * itemHeight}px)`,
1919
+ },
1949
1920
  },
1950
1921
  };
1951
1922
 
1952
- // We only want to render the items in the current virtual range.
1953
- // We achieve this by setting the `validIndices` in the meta object for the final render pass.
1954
- const finalVirtualState = useMemo(() => {
1955
- const visibleIndices = [];
1956
- for (let i = range.startIndex; i <= range.endIndex; i++) {
1957
- if (i < totalCount) visibleIndices.push(i);
1958
- }
1959
- return rebuildStateShape(sourceArray as any, path, {
1960
- ...meta,
1961
- isVirtual: true,
1962
- setVirtualItemHeight,
1963
- validIndices: visibleIndices,
1964
- });
1965
- }, [range, sourceArray, setVirtualItemHeight]);
1966
-
1967
1923
  return {
1968
- virtualState: finalVirtualState,
1969
- virtualizerProps,
1924
+ virtualState,
1925
+ virtualizerProps: virtualizerProps as any,
1970
1926
  scrollToBottom,
1971
1927
  scrollToIndex,
1972
1928
  };
@@ -2018,16 +1974,12 @@ function createProxyHandler<T>(
2018
1974
  };
2019
1975
  }
2020
1976
 
2021
- // This is the complete code for the `stateMap` property inside your proxy's `get` trap.
2022
-
2023
1977
  if (prop === "stateMap") {
2024
1978
  return (
2025
1979
  callbackfn: (
2026
1980
  value: InferArrayElement<T>,
2027
1981
  setter: StateObject<InferArrayElement<T>>,
2028
1982
  info: {
2029
- // The `register` function is no longer needed for reactivity because
2030
- // CogsItemWrapper handles it, but we keep it for API compatibility.
2031
1983
  register: () => void;
2032
1984
  index: number;
2033
1985
  originalIndex: number;
@@ -2038,6 +1990,7 @@ function createProxyHandler<T>(
2038
1990
  .getState()
2039
1991
  .getNestedState(stateKey, path) as any[];
2040
1992
 
1993
+ // Defensive check to make sure we are mapping over an array
2041
1994
  if (!Array.isArray(arrayToMap)) {
2042
1995
  console.warn(
2043
1996
  `stateMap called on a non-array value at path: ${path.join(".")}. The current value is:`,
@@ -2046,55 +1999,54 @@ function createProxyHandler<T>(
2046
1999
  return null;
2047
2000
  }
2048
2001
 
2002
+ // If we have validIndices, only map those items
2049
2003
  const indicesToMap =
2050
2004
  meta?.validIndices ||
2051
2005
  Array.from({ length: arrayToMap.length }, (_, i) => i);
2052
2006
 
2053
2007
  return indicesToMap.map((originalIndex, localIndex) => {
2054
2008
  const item = arrayToMap[originalIndex];
2055
- const itemPath = [...path, originalIndex.toString()];
2056
- const itemPathKey = itemPath.join(".");
2009
+ const finalPath = [...path, originalIndex.toString()];
2010
+ const setter = rebuildStateShape(item, finalPath, meta);
2011
+
2012
+ // Create the register function right here. It closes over the necessary variables.
2013
+ const register = () => {
2014
+ const [, forceUpdate] = useState({});
2015
+ const itemComponentId = `${componentId}-${path.join(".")}-${originalIndex}`;
2016
+
2017
+ useLayoutEffect(() => {
2018
+ const fullComponentId = `${stateKey}////${itemComponentId}`;
2019
+ const stateEntry = getGlobalStore
2020
+ .getState()
2021
+ .stateComponents.get(stateKey) || {
2022
+ components: new Map(),
2023
+ };
2024
+
2025
+ stateEntry.components.set(fullComponentId, {
2026
+ forceUpdate: () => forceUpdate({}),
2027
+ paths: new Set([finalPath.join(".")]),
2028
+ });
2057
2029
 
2058
- // This is the unique ID for your existing reactivity system.
2059
- const itemComponentId = `${componentId}-${itemPathKey}`;
2030
+ getGlobalStore
2031
+ .getState()
2032
+ .stateComponents.set(stateKey, stateEntry);
2060
2033
 
2061
- const setter = rebuildStateShape(item, itemPath, meta);
2034
+ return () => {
2035
+ const currentEntry = getGlobalStore
2036
+ .getState()
2037
+ .stateComponents.get(stateKey);
2038
+ if (currentEntry) {
2039
+ currentEntry.components.delete(fullComponentId);
2040
+ }
2041
+ };
2042
+ }, [stateKey, itemComponentId]);
2043
+ };
2062
2044
 
2063
- // Get the user's rendered component (e.g., <Message />)
2064
- const userRenderedItem = callbackfn(item, setter, {
2065
- // This function is now a no-op because CogsItemWrapper handles registration.
2066
- register: () => {
2067
- console.warn(
2068
- "The `register` function in stateMap is deprecated. Item reactivity is now automatic."
2069
- );
2070
- },
2045
+ return callbackfn(item, setter, {
2046
+ register,
2071
2047
  index: localIndex,
2072
2048
  originalIndex,
2073
2049
  });
2074
-
2075
- // --- This is the bridge between the virtualizer and the wrapper ---
2076
-
2077
- // Check if we are in a virtual context by looking for the setter in the meta object.
2078
- // `meta.setVirtualItemHeight` is passed down from `useVirtualView`.
2079
- const onMeasure =
2080
- meta?.isVirtual && meta!.setVirtualItemHeight
2081
- ? (height: number) =>
2082
- meta!.setVirtualItemHeight!(originalIndex, height)
2083
- : undefined; // If not in a virtual context, this will be undefined.
2084
-
2085
- // --- Wrap the user's output in your enhanced CogsItemWrapper ---
2086
- // This wrapper handles BOTH reactivity AND optional measurement.
2087
- return (
2088
- <CogsItemWrapper
2089
- key={itemPathKey} // Use the unique path for React's key.
2090
- stateKey={stateKey}
2091
- itemComponentId={itemComponentId}
2092
- itemPath={itemPath}
2093
- onMeasure={onMeasure} // Pass the onMeasure function (or undefined)
2094
- >
2095
- {userRenderedItem}
2096
- </CogsItemWrapper>
2097
- );
2098
2050
  });
2099
2051
  };
2100
2052
  }
@@ -2762,13 +2714,7 @@ function SignalMapRenderer({
2762
2714
  rebuildStateShape: (
2763
2715
  currentState: any,
2764
2716
  path: string[],
2765
- meta?: {
2766
- filtered?: string[][];
2767
- validIndices?: number[];
2768
- // --- NEW, OPTIONAL PROPERTIES ---
2769
- isVirtual?: boolean;
2770
- setVirtualItemHeight?: (index: number, height: number) => void;
2771
- }
2717
+ meta?: { filtered?: string[][]; validIndices?: number[] }
2772
2718
  ) => any;
2773
2719
  }) {
2774
2720
  const value = getGlobalStore().getNestedState(proxy._stateKey, proxy._path);
@@ -2885,13 +2831,11 @@ function CogsItemWrapper({
2885
2831
  itemComponentId,
2886
2832
  itemPath,
2887
2833
  children,
2888
- onMeasure, // OPTIONAL PROP
2889
2834
  }: {
2890
2835
  stateKey: string;
2891
2836
  itemComponentId: string;
2892
2837
  itemPath: string[];
2893
2838
  children: React.ReactNode;
2894
- onMeasure?: (height: number) => void; // The type definition
2895
2839
  }) {
2896
2840
  // This is a real component, so we can safely call hooks.
2897
2841
  const [, forceUpdate] = useState({});
@@ -2923,14 +2867,7 @@ function CogsItemWrapper({
2923
2867
  }
2924
2868
  };
2925
2869
  }, [stateKey, itemComponentId, itemPath.join(".")]); // Effect dependency array is stable.
2926
- const [measureRef, bounds] = useMeasure();
2927
-
2928
- useLayoutEffect(() => {
2929
- if (!onMeasure || bounds.height === 0) return;
2930
-
2931
- onMeasure(bounds.height);
2932
- }, [bounds.height, onMeasure]);
2933
2870
 
2934
- return <div ref={measureRef}>{children}</div>;
2935
2871
  // Render the actual component the user provided.
2872
+ return <>{children}</>;
2936
2873
  }