cogsbox-state 0.5.291 → 0.5.293
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.d.ts +1 -1
- package/dist/CogsState.jsx +676 -675
- package/dist/CogsState.jsx.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.js +65 -49
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +130 -99
- package/src/store.ts +34 -6
package/src/CogsState.tsx
CHANGED
|
@@ -42,7 +42,7 @@ import useMeasure from "react-use-measure";
|
|
|
42
42
|
type Prettify<T> = { [K in keyof T]: T[K] } & {};
|
|
43
43
|
|
|
44
44
|
export type VirtualViewOptions = {
|
|
45
|
-
itemHeight
|
|
45
|
+
itemHeight?: number;
|
|
46
46
|
overscan?: number;
|
|
47
47
|
stickToBottom?: boolean;
|
|
48
48
|
};
|
|
@@ -1039,6 +1039,8 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1039
1039
|
const pathKey = `${thisKey}-${path.join(".")}`;
|
|
1040
1040
|
componentUpdatesRef.current.add(pathKey);
|
|
1041
1041
|
}
|
|
1042
|
+
const store = getGlobalStore.getState();
|
|
1043
|
+
|
|
1042
1044
|
setState(thisKey, (prevValue: TStateObject) => {
|
|
1043
1045
|
const payload = isFunction<TStateObject>(newStateOrFunction)
|
|
1044
1046
|
? newStateOrFunction(prevValue as TStateObject)
|
|
@@ -1047,9 +1049,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1047
1049
|
const signalId = `${thisKey}-${path.join(".")}`;
|
|
1048
1050
|
if (signalId) {
|
|
1049
1051
|
let isArrayOperation = false;
|
|
1050
|
-
let elements =
|
|
1051
|
-
.getState()
|
|
1052
|
-
.signalDomElements.get(signalId);
|
|
1052
|
+
let elements = store.signalDomElements.get(signalId);
|
|
1053
1053
|
|
|
1054
1054
|
if (
|
|
1055
1055
|
(!elements || elements.size === 0) &&
|
|
@@ -1062,9 +1062,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1062
1062
|
if (Array.isArray(arrayValue)) {
|
|
1063
1063
|
isArrayOperation = true;
|
|
1064
1064
|
const arraySignalId = `${thisKey}-${arrayPath.join(".")}`;
|
|
1065
|
-
elements =
|
|
1066
|
-
.getState()
|
|
1067
|
-
.signalDomElements.get(arraySignalId);
|
|
1065
|
+
elements = store.signalDomElements.get(arraySignalId);
|
|
1068
1066
|
}
|
|
1069
1067
|
}
|
|
1070
1068
|
|
|
@@ -1089,32 +1087,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1089
1087
|
}
|
|
1090
1088
|
}
|
|
1091
1089
|
|
|
1092
|
-
|
|
1093
|
-
const store = getGlobalStore.getState();
|
|
1094
|
-
|
|
1095
|
-
switch (updateObj.updateType) {
|
|
1096
|
-
case "update":
|
|
1097
|
-
// For updates, just mirror the structure at the path
|
|
1098
|
-
store.updateShadowAtPath(thisKey, path, payload);
|
|
1099
|
-
break;
|
|
1100
|
-
|
|
1101
|
-
case "insert":
|
|
1102
|
-
// For array insert, add empty element to shadow array
|
|
1103
|
-
const parentPath = path.slice(0, -1);
|
|
1104
|
-
store.insertShadowArrayElement(thisKey, parentPath);
|
|
1105
|
-
break;
|
|
1106
|
-
|
|
1107
|
-
case "cut":
|
|
1108
|
-
// For array cut, remove element from shadow array
|
|
1109
|
-
const arrayPath = path.slice(0, -1);
|
|
1110
|
-
const index = parseInt(path[path.length - 1]!);
|
|
1111
|
-
store.removeShadowArrayElement(thisKey, arrayPath, index);
|
|
1112
|
-
break;
|
|
1113
|
-
}
|
|
1114
|
-
};
|
|
1115
|
-
|
|
1116
|
-
shadowUpdate();
|
|
1117
|
-
console.log("shadowState", getGlobalStore.getState().shadowStateStore);
|
|
1090
|
+
console.log("shadowState", store.shadowStateStore);
|
|
1118
1091
|
if (
|
|
1119
1092
|
updateObj.updateType === "update" &&
|
|
1120
1093
|
(validationKey || latestInitialOptionsRef.current?.validation?.key) &&
|
|
@@ -1164,7 +1137,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1164
1137
|
});
|
|
1165
1138
|
}
|
|
1166
1139
|
|
|
1167
|
-
const stateEntry =
|
|
1140
|
+
const stateEntry = store.stateComponents.get(thisKey);
|
|
1168
1141
|
console.log("stateEntry >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", stateEntry);
|
|
1169
1142
|
if (stateEntry) {
|
|
1170
1143
|
const changedPaths = getDifferences(prevValue, payload);
|
|
@@ -1282,6 +1255,27 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1282
1255
|
newValue,
|
|
1283
1256
|
} satisfies UpdateTypeDetail;
|
|
1284
1257
|
|
|
1258
|
+
switch (updateObj.updateType) {
|
|
1259
|
+
case "update":
|
|
1260
|
+
// For updates, just mirror the structure at the path
|
|
1261
|
+
store.updateShadowAtPath(thisKey, path, payload);
|
|
1262
|
+
break;
|
|
1263
|
+
|
|
1264
|
+
case "insert":
|
|
1265
|
+
// For array insert, add empty element to shadow array
|
|
1266
|
+
|
|
1267
|
+
const parentPath = path.slice(0, -1);
|
|
1268
|
+
store.insertShadowArrayElement(thisKey, parentPath, newValue);
|
|
1269
|
+
break;
|
|
1270
|
+
|
|
1271
|
+
case "cut":
|
|
1272
|
+
// For array cut, remove element from shadow array
|
|
1273
|
+
const arrayPath = path.slice(0, -1);
|
|
1274
|
+
const index = parseInt(path[path.length - 1]!);
|
|
1275
|
+
store.removeShadowArrayElement(thisKey, arrayPath, index);
|
|
1276
|
+
break;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1285
1279
|
setStateLog(thisKey, (prevLogs) => {
|
|
1286
1280
|
const logs = [...(prevLogs ?? []), newUpdate];
|
|
1287
1281
|
|
|
@@ -1322,7 +1316,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1322
1316
|
});
|
|
1323
1317
|
}
|
|
1324
1318
|
if (latestInitialOptionsRef.current?.serverSync) {
|
|
1325
|
-
const serverStateStore =
|
|
1319
|
+
const serverStateStore = store.serverState[thisKey];
|
|
1326
1320
|
const serverSync = latestInitialOptionsRef.current?.serverSync;
|
|
1327
1321
|
setServerSyncActions(thisKey, {
|
|
1328
1322
|
syncKey:
|
|
@@ -1564,6 +1558,7 @@ function createProxyHandler<T>(
|
|
|
1564
1558
|
"_stateKey",
|
|
1565
1559
|
"getComponents",
|
|
1566
1560
|
]);
|
|
1561
|
+
|
|
1567
1562
|
if (
|
|
1568
1563
|
prop !== "then" &&
|
|
1569
1564
|
!prop.startsWith("$") &&
|
|
@@ -1808,7 +1803,7 @@ function createProxyHandler<T>(
|
|
|
1808
1803
|
options: VirtualViewOptions
|
|
1809
1804
|
): VirtualStateObjectResult<any[]> => {
|
|
1810
1805
|
const {
|
|
1811
|
-
itemHeight,
|
|
1806
|
+
itemHeight = 50, // Default/estimated height
|
|
1812
1807
|
overscan = 5,
|
|
1813
1808
|
stickToBottom = false,
|
|
1814
1809
|
} = options;
|
|
@@ -1818,17 +1813,13 @@ function createProxyHandler<T>(
|
|
|
1818
1813
|
startIndex: 0,
|
|
1819
1814
|
endIndex: 10,
|
|
1820
1815
|
});
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
.getState()
|
|
1824
|
-
.getShadowMetadata(stateKey, [...path, index.toString()]);
|
|
1825
|
-
return metadata?.virtualizer?.itemHeight || options.itemHeight;
|
|
1826
|
-
}, []);
|
|
1827
|
-
// --- State Tracking Refs ---
|
|
1816
|
+
|
|
1817
|
+
// --- State Tracking Refs for Stability ---
|
|
1828
1818
|
const isAtBottomRef = useRef(stickToBottom);
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1819
|
+
// Store the scroll position before a new item is added
|
|
1820
|
+
const scrollOffsetRef = useRef(0);
|
|
1821
|
+
// Ref to track if the list has grown, to trigger scroll correction
|
|
1822
|
+
const listGrewRef = useRef(false);
|
|
1832
1823
|
|
|
1833
1824
|
const sourceArray = getGlobalStore().getNestedState(
|
|
1834
1825
|
stateKey,
|
|
@@ -1836,6 +1827,31 @@ function createProxyHandler<T>(
|
|
|
1836
1827
|
) as any[];
|
|
1837
1828
|
const totalCount = sourceArray.length;
|
|
1838
1829
|
|
|
1830
|
+
// Helper to get measured heights or the default
|
|
1831
|
+
const getItemHeight = useCallback(
|
|
1832
|
+
(index: number): number => {
|
|
1833
|
+
const metadata = getGlobalStore
|
|
1834
|
+
.getState()
|
|
1835
|
+
.getShadowMetadata(stateKey, [...path, index.toString()]);
|
|
1836
|
+
return metadata?.virtualizer?.itemHeight || itemHeight;
|
|
1837
|
+
},
|
|
1838
|
+
[itemHeight, stateKey, path]
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
// Pre-calculate total height and the top offset of each item
|
|
1842
|
+
const { totalHeight, positions } = useMemo(() => {
|
|
1843
|
+
let currentHeight = 0;
|
|
1844
|
+
const pos: number[] = [];
|
|
1845
|
+
for (let i = 0; i < totalCount; i++) {
|
|
1846
|
+
pos[i] = currentHeight;
|
|
1847
|
+
currentHeight += getItemHeight(i);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
console.log("totalHeight", totalHeight);
|
|
1851
|
+
return { totalHeight: currentHeight, positions: pos };
|
|
1852
|
+
}, [totalCount, getItemHeight]);
|
|
1853
|
+
|
|
1854
|
+
// This is identical to your original code
|
|
1839
1855
|
const virtualState = useMemo(() => {
|
|
1840
1856
|
const start = Math.max(0, range.startIndex);
|
|
1841
1857
|
const end = Math.min(totalCount, range.endIndex);
|
|
@@ -1850,73 +1866,92 @@ function createProxyHandler<T>(
|
|
|
1850
1866
|
});
|
|
1851
1867
|
}, [range.startIndex, range.endIndex, sourceArray, totalCount]);
|
|
1852
1868
|
|
|
1869
|
+
// --- STABLE SCROLL LOGIC ---
|
|
1870
|
+
// useLayoutEffect runs after DOM mutations but before the browser paints.
|
|
1871
|
+
// This is the perfect place to correct scroll positions.
|
|
1853
1872
|
useLayoutEffect(() => {
|
|
1854
1873
|
const container = containerRef.current;
|
|
1855
1874
|
if (!container) return;
|
|
1856
1875
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1876
|
+
// If the list grew, we need to adjust the scroll position
|
|
1877
|
+
// to prevent the content from jumping.
|
|
1878
|
+
if (listGrewRef.current) {
|
|
1879
|
+
listGrewRef.current = false; // Reset the flag
|
|
1880
|
+
|
|
1881
|
+
if (isAtBottomRef.current) {
|
|
1882
|
+
// If we were at the bottom, stay at the bottom.
|
|
1883
|
+
// This is the fix for the auto-scroll issue.
|
|
1884
|
+
container.scrollTop = container.scrollHeight;
|
|
1885
|
+
} else {
|
|
1886
|
+
// If we were in the middle, restore the previous scroll position
|
|
1887
|
+
// plus the height of the content that was added above us.
|
|
1888
|
+
// This is an advanced case, but for now, let's keep it simple
|
|
1889
|
+
// as most use-cases are for chat-like views. For a simple list,
|
|
1890
|
+
// just staying at the bottom is the main goal.
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}, [totalHeight]); // This effect runs whenever the total height changes
|
|
1894
|
+
|
|
1895
|
+
useEffect(() => {
|
|
1896
|
+
const container = containerRef.current;
|
|
1897
|
+
if (!container) return;
|
|
1898
|
+
|
|
1899
|
+
// Track the previous total count to detect when new items are added
|
|
1900
|
+
let previousTotalCount = totalCount;
|
|
1860
1901
|
|
|
1861
1902
|
const handleScroll = () => {
|
|
1903
|
+
if (!container) return;
|
|
1862
1904
|
const { scrollTop, clientHeight, scrollHeight } = container;
|
|
1905
|
+
// Update "is at bottom" status on every scroll
|
|
1863
1906
|
isAtBottomRef.current =
|
|
1864
1907
|
scrollHeight - scrollTop - clientHeight < 10;
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1908
|
+
scrollOffsetRef.current = scrollTop;
|
|
1909
|
+
|
|
1910
|
+
// Find start/end indices based on positions
|
|
1911
|
+
let startIndex = 0;
|
|
1912
|
+
for (let i = 0; i < positions.length; i++) {
|
|
1913
|
+
if (positions[i]! >= scrollTop) {
|
|
1914
|
+
startIndex = i;
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
let endIndex = startIndex;
|
|
1920
|
+
while (
|
|
1921
|
+
endIndex < totalCount &&
|
|
1922
|
+
positions[endIndex]! < scrollTop + clientHeight
|
|
1923
|
+
) {
|
|
1924
|
+
endIndex++;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
startIndex = Math.max(0, startIndex - overscan);
|
|
1928
|
+
endIndex = Math.min(totalCount, endIndex + overscan);
|
|
1929
|
+
|
|
1874
1930
|
setRange((prevRange) => {
|
|
1875
1931
|
if (
|
|
1876
|
-
prevRange.startIndex !==
|
|
1877
|
-
prevRange.endIndex !==
|
|
1932
|
+
prevRange.startIndex !== startIndex ||
|
|
1933
|
+
prevRange.endIndex !== endIndex
|
|
1878
1934
|
) {
|
|
1879
|
-
return { startIndex
|
|
1935
|
+
return { startIndex, endIndex };
|
|
1880
1936
|
}
|
|
1881
1937
|
return prevRange;
|
|
1882
1938
|
});
|
|
1883
1939
|
};
|
|
1884
1940
|
|
|
1941
|
+
// Check if the list has grown *before* the next render cycle
|
|
1942
|
+
if (totalCount > previousTotalCount) {
|
|
1943
|
+
listGrewRef.current = true;
|
|
1944
|
+
}
|
|
1945
|
+
previousTotalCount = totalCount;
|
|
1946
|
+
|
|
1885
1947
|
container.addEventListener("scroll", handleScroll, {
|
|
1886
1948
|
passive: true,
|
|
1887
1949
|
});
|
|
1888
|
-
|
|
1889
|
-
// --- THE CORRECTED DECISION LOGIC ---
|
|
1890
|
-
if (stickToBottom) {
|
|
1891
|
-
if (isInitialMountRef.current) {
|
|
1892
|
-
// SCENARIO 1: First render of the component.
|
|
1893
|
-
// Go to the bottom unconditionally. Use `auto` scroll for an instant jump.
|
|
1894
|
-
container.scrollTo({
|
|
1895
|
-
top: container.scrollHeight,
|
|
1896
|
-
behavior: "auto",
|
|
1897
|
-
});
|
|
1898
|
-
} else if (wasAtBottom && listGrew) {
|
|
1899
|
-
// SCENARIO 2: Subsequent renders (new messages arrive).
|
|
1900
|
-
// Only scroll if the user was already at the bottom.
|
|
1901
|
-
// Use `smooth` for a nice animated scroll for new messages.
|
|
1902
|
-
requestAnimationFrame(() => {
|
|
1903
|
-
container.scrollTo({
|
|
1904
|
-
top: container.scrollHeight,
|
|
1905
|
-
behavior: "smooth",
|
|
1906
|
-
});
|
|
1907
|
-
});
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
// After the logic runs, it's no longer the initial mount.
|
|
1912
|
-
isInitialMountRef.current = false;
|
|
1913
|
-
|
|
1914
|
-
// Always run handleScroll once to set the initial visible window.
|
|
1915
|
-
handleScroll();
|
|
1950
|
+
handleScroll(); // Initial calculation
|
|
1916
1951
|
|
|
1917
1952
|
return () =>
|
|
1918
1953
|
container.removeEventListener("scroll", handleScroll);
|
|
1919
|
-
}, [totalCount,
|
|
1954
|
+
}, [totalCount, overscan, positions]); // Depend on positions to re-run scroll logic
|
|
1920
1955
|
|
|
1921
1956
|
const scrollToBottom = useCallback(
|
|
1922
1957
|
(behavior: ScrollBehavior = "smooth") => {
|
|
@@ -1932,31 +1967,27 @@ function createProxyHandler<T>(
|
|
|
1932
1967
|
|
|
1933
1968
|
const scrollToIndex = useCallback(
|
|
1934
1969
|
(index: number, behavior: ScrollBehavior = "smooth") => {
|
|
1935
|
-
if (containerRef.current) {
|
|
1970
|
+
if (containerRef.current && positions[index] !== undefined) {
|
|
1936
1971
|
containerRef.current.scrollTo({
|
|
1937
|
-
top: index
|
|
1972
|
+
top: positions[index],
|
|
1938
1973
|
behavior,
|
|
1939
1974
|
});
|
|
1940
1975
|
}
|
|
1941
1976
|
},
|
|
1942
|
-
[
|
|
1977
|
+
[positions]
|
|
1943
1978
|
);
|
|
1944
1979
|
|
|
1945
|
-
// Same virtualizer props as before
|
|
1946
1980
|
const virtualizerProps = {
|
|
1947
1981
|
outer: {
|
|
1948
1982
|
ref: containerRef,
|
|
1949
1983
|
style: { overflowY: "auto", height: "100%" },
|
|
1950
1984
|
},
|
|
1951
1985
|
inner: {
|
|
1952
|
-
style: {
|
|
1953
|
-
height: `${totalCount * itemHeight}px`,
|
|
1954
|
-
position: "relative",
|
|
1955
|
-
},
|
|
1986
|
+
style: { height: `${totalHeight}px`, position: "relative" },
|
|
1956
1987
|
},
|
|
1957
1988
|
list: {
|
|
1958
1989
|
style: {
|
|
1959
|
-
transform: `translateY(${range.startIndex
|
|
1990
|
+
transform: `translateY(${positions[range.startIndex] || 0}px)`,
|
|
1960
1991
|
},
|
|
1961
1992
|
},
|
|
1962
1993
|
};
|
package/src/store.ts
CHANGED
|
@@ -102,7 +102,11 @@ export type CogsGlobalState = {
|
|
|
102
102
|
shadowStateStore: { [key: string]: any };
|
|
103
103
|
initializeShadowState: (key: string, initialState: any) => void;
|
|
104
104
|
updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
|
|
105
|
-
insertShadowArrayElement: (
|
|
105
|
+
insertShadowArrayElement: (
|
|
106
|
+
key: string,
|
|
107
|
+
arrayPath: string[],
|
|
108
|
+
newItem: any
|
|
109
|
+
) => void;
|
|
106
110
|
removeShadowArrayElement: (
|
|
107
111
|
key: string,
|
|
108
112
|
arrayPath: string[],
|
|
@@ -325,22 +329,46 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
|
|
|
325
329
|
});
|
|
326
330
|
},
|
|
327
331
|
|
|
328
|
-
insertShadowArrayElement: (
|
|
332
|
+
insertShadowArrayElement: (
|
|
333
|
+
key: string,
|
|
334
|
+
arrayPath: string[],
|
|
335
|
+
newItem: any
|
|
336
|
+
) => {
|
|
329
337
|
set((state) => {
|
|
330
338
|
const newShadow = { ...state.shadowStateStore };
|
|
331
|
-
|
|
339
|
+
if (!newShadow[key]) return state;
|
|
340
|
+
|
|
341
|
+
newShadow[key] = JSON.parse(JSON.stringify(newShadow[key]));
|
|
342
|
+
|
|
343
|
+
let current: any = newShadow[key];
|
|
332
344
|
|
|
333
345
|
for (const segment of arrayPath) {
|
|
334
|
-
current = current
|
|
346
|
+
current = current[segment];
|
|
347
|
+
if (!current) return state;
|
|
335
348
|
}
|
|
336
349
|
|
|
337
350
|
if (Array.isArray(current)) {
|
|
338
|
-
|
|
351
|
+
// Create shadow structure based on the actual new item
|
|
352
|
+
const createShadowStructure = (obj: any): any => {
|
|
353
|
+
if (Array.isArray(obj)) {
|
|
354
|
+
return obj.map((item) => createShadowStructure(item));
|
|
355
|
+
}
|
|
356
|
+
if (typeof obj === "object" && obj !== null) {
|
|
357
|
+
const shadow: any = {};
|
|
358
|
+
for (const k in obj) {
|
|
359
|
+
shadow[k] = createShadowStructure(obj[k]);
|
|
360
|
+
}
|
|
361
|
+
return shadow;
|
|
362
|
+
}
|
|
363
|
+
return {}; // Leaf nodes get empty object for metadata
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
current.push(createShadowStructure(newItem));
|
|
339
367
|
}
|
|
368
|
+
|
|
340
369
|
return { shadowStateStore: newShadow };
|
|
341
370
|
});
|
|
342
371
|
},
|
|
343
|
-
|
|
344
372
|
removeShadowArrayElement: (
|
|
345
373
|
key: string,
|
|
346
374
|
arrayPath: string[],
|