cogsbox-state 0.5.381 → 0.5.382
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/dist/CogsState.jsx +632 -630
- package/dist/CogsState.jsx.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +64 -48
package/package.json
CHANGED
package/src/CogsState.tsx
CHANGED
|
@@ -1817,7 +1817,7 @@ function createProxyHandler<T>(
|
|
|
1817
1817
|
| "LOCKED_AT_BOTTOM"
|
|
1818
1818
|
| "IDLE_NOT_AT_BOTTOM";
|
|
1819
1819
|
|
|
1820
|
-
|
|
1820
|
+
// Refs and State
|
|
1821
1821
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
1822
1822
|
const [range, setRange] = useState({
|
|
1823
1823
|
startIndex: 0,
|
|
@@ -1827,9 +1827,9 @@ function createProxyHandler<T>(
|
|
|
1827
1827
|
const isProgrammaticScroll = useRef(false);
|
|
1828
1828
|
const prevTotalCountRef = useRef(0);
|
|
1829
1829
|
const prevDepsRef = useRef(dependencies);
|
|
1830
|
-
const lastUpdateAtScrollTop = useRef(0);
|
|
1831
1830
|
const [shadowUpdateTrigger, setShadowUpdateTrigger] = useState(0);
|
|
1832
1831
|
|
|
1832
|
+
// Subscribe to external state updates for item heights
|
|
1833
1833
|
useEffect(() => {
|
|
1834
1834
|
const unsubscribe = getGlobalStore
|
|
1835
1835
|
.getState()
|
|
@@ -1845,6 +1845,7 @@ function createProxyHandler<T>(
|
|
|
1845
1845
|
) as any[];
|
|
1846
1846
|
const totalCount = sourceArray.length;
|
|
1847
1847
|
|
|
1848
|
+
// Memoize positions and total height based on measured items
|
|
1848
1849
|
const { totalHeight, positions } = useMemo(() => {
|
|
1849
1850
|
const shadowArray =
|
|
1850
1851
|
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
@@ -1866,7 +1867,7 @@ function createProxyHandler<T>(
|
|
|
1866
1867
|
shadowUpdateTrigger,
|
|
1867
1868
|
]);
|
|
1868
1869
|
|
|
1869
|
-
//
|
|
1870
|
+
// Memoize the virtualized data slice
|
|
1870
1871
|
const virtualState = useMemo(() => {
|
|
1871
1872
|
const start = Math.max(0, range.startIndex);
|
|
1872
1873
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1882,7 +1883,7 @@ function createProxyHandler<T>(
|
|
|
1882
1883
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1883
1884
|
|
|
1884
1885
|
// --- 1. STATE CONTROLLER ---
|
|
1885
|
-
//
|
|
1886
|
+
// Decides which state to transition TO based on data changes.
|
|
1886
1887
|
useLayoutEffect(() => {
|
|
1887
1888
|
const depsChanged = !isDeepEqual(
|
|
1888
1889
|
dependencies,
|
|
@@ -1893,9 +1894,10 @@ function createProxyHandler<T>(
|
|
|
1893
1894
|
if (depsChanged) {
|
|
1894
1895
|
console.log("TRANSITION: Deps changed -> IDLE_AT_TOP");
|
|
1895
1896
|
setStatus("IDLE_AT_TOP");
|
|
1896
|
-
return;
|
|
1897
|
+
return;
|
|
1897
1898
|
}
|
|
1898
1899
|
|
|
1900
|
+
// THIS IS THE CRITICAL CHECK: It only scrolls if new items arrive AND we are already locked.
|
|
1899
1901
|
if (
|
|
1900
1902
|
hasNewItems &&
|
|
1901
1903
|
status === "LOCKED_AT_BOTTOM" &&
|
|
@@ -1912,7 +1914,7 @@ function createProxyHandler<T>(
|
|
|
1912
1914
|
}, [totalCount, ...dependencies]);
|
|
1913
1915
|
|
|
1914
1916
|
// --- 2. STATE ACTION HANDLER ---
|
|
1915
|
-
//
|
|
1917
|
+
// Performs the ACTION for the current state (e.g., scrolling).
|
|
1916
1918
|
useLayoutEffect(() => {
|
|
1917
1919
|
const container = containerRef.current;
|
|
1918
1920
|
if (!container) return;
|
|
@@ -1924,15 +1926,12 @@ function createProxyHandler<T>(
|
|
|
1924
1926
|
stickToBottom &&
|
|
1925
1927
|
totalCount > 0
|
|
1926
1928
|
) {
|
|
1927
|
-
// If we just loaded a new chat, start the process.
|
|
1928
1929
|
console.log(
|
|
1929
|
-
"ACTION (IDLE_AT_TOP): Data
|
|
1930
|
+
"ACTION (IDLE_AT_TOP): Data arrived -> GETTING_HEIGHTS"
|
|
1930
1931
|
);
|
|
1931
1932
|
setStatus("GETTING_HEIGHTS");
|
|
1932
1933
|
} else if (status === "GETTING_HEIGHTS") {
|
|
1933
|
-
console.log(
|
|
1934
|
-
"ACTION (GETTING_HEIGHTS): Setting range to end and starting loop."
|
|
1935
|
-
);
|
|
1934
|
+
console.log("ACTION (GETTING_HEIGHTS): Setting range to end");
|
|
1936
1935
|
setRange({
|
|
1937
1936
|
startIndex: Math.max(0, totalCount - 10 - overscan),
|
|
1938
1937
|
endIndex: totalCount,
|
|
@@ -1949,13 +1948,10 @@ function createProxyHandler<T>(
|
|
|
1949
1948
|
|
|
1950
1949
|
if (lastItemHeight > 0) {
|
|
1951
1950
|
clearInterval(intervalId);
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
setStatus("SCROLLING_TO_BOTTOM");
|
|
1958
|
-
}
|
|
1951
|
+
console.log(
|
|
1952
|
+
"ACTION (GETTING_HEIGHTS): Measurement success -> SCROLLING_TO_BOTTOM"
|
|
1953
|
+
);
|
|
1954
|
+
setStatus("SCROLLING_TO_BOTTOM");
|
|
1959
1955
|
}
|
|
1960
1956
|
}, 100);
|
|
1961
1957
|
} else if (status === "SCROLLING_TO_BOTTOM") {
|
|
@@ -1963,7 +1959,6 @@ function createProxyHandler<T>(
|
|
|
1963
1959
|
"ACTION (SCROLLING_TO_BOTTOM): Executing scroll."
|
|
1964
1960
|
);
|
|
1965
1961
|
isProgrammaticScroll.current = true;
|
|
1966
|
-
// Use 'auto' for initial load, 'smooth' for new messages.
|
|
1967
1962
|
const scrollBehavior =
|
|
1968
1963
|
prevTotalCountRef.current === 0 ? "auto" : "smooth";
|
|
1969
1964
|
|
|
@@ -1974,16 +1969,11 @@ function createProxyHandler<T>(
|
|
|
1974
1969
|
|
|
1975
1970
|
const timeoutId = setTimeout(
|
|
1976
1971
|
() => {
|
|
1977
|
-
console.log(
|
|
1978
|
-
"ACTION (SCROLLING_TO_BOTTOM): Scroll finished -> LOCKED_AT_BOTTOM"
|
|
1979
|
-
);
|
|
1980
1972
|
isProgrammaticScroll.current = false;
|
|
1981
|
-
shouldNotScroll.current = false;
|
|
1982
1973
|
setStatus("LOCKED_AT_BOTTOM");
|
|
1983
1974
|
},
|
|
1984
1975
|
scrollBehavior === "smooth" ? 500 : 50
|
|
1985
1976
|
);
|
|
1986
|
-
|
|
1987
1977
|
return () => clearTimeout(timeoutId);
|
|
1988
1978
|
}
|
|
1989
1979
|
|
|
@@ -1992,46 +1982,67 @@ function createProxyHandler<T>(
|
|
|
1992
1982
|
};
|
|
1993
1983
|
}, [status, totalCount, positions]);
|
|
1994
1984
|
|
|
1985
|
+
// --- 3. USER INTERACTION & RANGE UPDATER (THE CORRECTED VERSION) ---
|
|
1995
1986
|
useEffect(() => {
|
|
1996
1987
|
const container = containerRef.current;
|
|
1997
1988
|
if (!container) return;
|
|
1998
1989
|
|
|
1999
|
-
const scrollThreshold = itemHeight;
|
|
2000
|
-
|
|
2001
1990
|
const handleUserScroll = () => {
|
|
2002
1991
|
if (isProgrammaticScroll.current) {
|
|
2003
1992
|
return;
|
|
2004
1993
|
}
|
|
2005
1994
|
|
|
2006
|
-
const scrollTop = container
|
|
2007
|
-
if (
|
|
2008
|
-
Math.abs(scrollTop - lastUpdateAtScrollTop.current) <
|
|
2009
|
-
scrollThreshold
|
|
2010
|
-
) {
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
1995
|
+
const { scrollTop, clientHeight, scrollHeight } = container;
|
|
2013
1996
|
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
1997
|
+
// Part 1: UPDATE THE STATE MACHINE STATUS (The critical fix)
|
|
1998
|
+
// This tells the rest of the component whether we should auto-scroll on new messages.
|
|
1999
|
+
const isAtBottom =
|
|
2000
|
+
scrollHeight - scrollTop - clientHeight < 1;
|
|
2001
|
+
|
|
2002
|
+
if (isAtBottom) {
|
|
2003
|
+
if (status !== "LOCKED_AT_BOTTOM") {
|
|
2004
|
+
console.log(
|
|
2005
|
+
"SCROLL EVENT: Reached bottom -> LOCKED_AT_BOTTOM"
|
|
2006
|
+
);
|
|
2007
|
+
setStatus("LOCKED_AT_BOTTOM");
|
|
2008
|
+
}
|
|
2009
|
+
} else {
|
|
2010
|
+
if (status !== "IDLE_NOT_AT_BOTTOM") {
|
|
2011
|
+
console.log(
|
|
2012
|
+
"SCROLL EVENT: Scrolled up -> IDLE_NOT_AT_BOTTOM"
|
|
2013
|
+
);
|
|
2014
|
+
setStatus("IDLE_NOT_AT_BOTTOM");
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
2017
|
|
|
2018
|
-
//
|
|
2019
|
-
|
|
2018
|
+
// Part 2: EFFICIENTLY UPDATE THE RENDERED RANGE
|
|
2019
|
+
// This is the superior optimization from your first version. It only
|
|
2020
|
+
// re-renders if the actual items in view have changed.
|
|
2020
2021
|
let high = totalCount - 1;
|
|
2021
2022
|
let low = 0;
|
|
2022
|
-
let
|
|
2023
|
+
let potentialTopIndex = 0;
|
|
2023
2024
|
while (low <= high) {
|
|
2024
2025
|
const mid = Math.floor((low + high) / 2);
|
|
2025
2026
|
if (positions[mid]! < scrollTop) {
|
|
2026
|
-
|
|
2027
|
+
potentialTopIndex = mid;
|
|
2027
2028
|
low = mid + 1;
|
|
2028
2029
|
} else {
|
|
2029
2030
|
high = mid - 1;
|
|
2030
2031
|
}
|
|
2031
2032
|
}
|
|
2032
2033
|
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2034
|
+
const potentialStartIndex = Math.max(
|
|
2035
|
+
0,
|
|
2036
|
+
potentialTopIndex - overscan
|
|
2037
|
+
);
|
|
2038
|
+
|
|
2039
|
+
// Only update state if the visible start index has actually changed.
|
|
2040
|
+
if (potentialStartIndex === range.startIndex) {
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// If we must update, calculate the new end index.
|
|
2045
|
+
let endIndex = potentialStartIndex;
|
|
2035
2046
|
const visibleEnd = scrollTop + clientHeight;
|
|
2036
2047
|
while (
|
|
2037
2048
|
endIndex < totalCount &&
|
|
@@ -2040,13 +2051,13 @@ function createProxyHandler<T>(
|
|
|
2040
2051
|
endIndex++;
|
|
2041
2052
|
}
|
|
2042
2053
|
|
|
2054
|
+
console.log(
|
|
2055
|
+
`Index changed from ${range.startIndex} to ${potentialStartIndex}. Updating range.`
|
|
2056
|
+
);
|
|
2043
2057
|
setRange({
|
|
2044
|
-
startIndex,
|
|
2058
|
+
startIndex: potentialStartIndex,
|
|
2045
2059
|
endIndex: Math.min(totalCount, endIndex + overscan),
|
|
2046
2060
|
});
|
|
2047
|
-
|
|
2048
|
-
// Finally, we record that we did the work at THIS scroll position.
|
|
2049
|
-
lastUpdateAtScrollTop.current = scrollTop;
|
|
2050
2061
|
};
|
|
2051
2062
|
|
|
2052
2063
|
container.addEventListener("scroll", handleUserScroll, {
|
|
@@ -2054,18 +2065,22 @@ function createProxyHandler<T>(
|
|
|
2054
2065
|
});
|
|
2055
2066
|
return () =>
|
|
2056
2067
|
container.removeEventListener("scroll", handleUserScroll);
|
|
2057
|
-
}, [totalCount, positions,
|
|
2068
|
+
}, [totalCount, positions, status, range.startIndex]); // Dependencies are correct
|
|
2058
2069
|
|
|
2070
|
+
// --- 4. EXPOSED ACTIONS ---
|
|
2059
2071
|
const scrollToBottom = useCallback(() => {
|
|
2060
2072
|
console.log(
|
|
2061
2073
|
"USER ACTION: Clicked scroll button -> SCROLLING_TO_BOTTOM"
|
|
2062
2074
|
);
|
|
2075
|
+
// Don't scroll if there's nothing to scroll to.
|
|
2076
|
+
if (totalCount === 0) return;
|
|
2063
2077
|
setStatus("SCROLLING_TO_BOTTOM");
|
|
2064
|
-
}, []);
|
|
2078
|
+
}, [totalCount]);
|
|
2065
2079
|
|
|
2066
2080
|
const scrollToIndex = useCallback(
|
|
2067
2081
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
2068
2082
|
if (containerRef.current && positions[index] !== undefined) {
|
|
2083
|
+
// Manually scrolling to an index means we are no longer at the bottom.
|
|
2069
2084
|
setStatus("IDLE_NOT_AT_BOTTOM");
|
|
2070
2085
|
containerRef.current.scrollTo({
|
|
2071
2086
|
top: positions[index],
|
|
@@ -2076,6 +2091,7 @@ function createProxyHandler<T>(
|
|
|
2076
2091
|
[positions]
|
|
2077
2092
|
);
|
|
2078
2093
|
|
|
2094
|
+
// --- 5. RENDER PROPS ---
|
|
2079
2095
|
const virtualizerProps = {
|
|
2080
2096
|
outer: {
|
|
2081
2097
|
ref: containerRef,
|