cogsbox-state 0.5.291 → 0.5.292

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/src/CogsState.tsx CHANGED
@@ -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 = getGlobalStore
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 = getGlobalStore
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
- const shadowUpdate = () => {
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 = getGlobalStore.getState().stateComponents.get(thisKey);
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 = getGlobalStore.getState().serverState[thisKey];
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("$") &&
@@ -1807,8 +1802,9 @@ function createProxyHandler<T>(
1807
1802
  return (
1808
1803
  options: VirtualViewOptions
1809
1804
  ): VirtualStateObjectResult<any[]> => {
1805
+ // --- CHANGE 1: itemHeight is now optional with a default ---
1810
1806
  const {
1811
- itemHeight,
1807
+ itemHeight = 50, // Serves as a fallback for unmeasured items
1812
1808
  overscan = 5,
1813
1809
  stickToBottom = false,
1814
1810
  } = options;
@@ -1818,16 +1814,20 @@ function createProxyHandler<T>(
1818
1814
  startIndex: 0,
1819
1815
  endIndex: 10,
1820
1816
  });
1821
- const getItemHeight = useCallback((index: number) => {
1822
- const metadata = getGlobalStore
1823
- .getState()
1824
- .getShadowMetadata(stateKey, [...path, index.toString()]);
1825
- return metadata?.virtualizer?.itemHeight || options.itemHeight;
1826
- }, []);
1827
- // --- State Tracking Refs ---
1817
+
1818
+ // --- CHANGE 2: Add a helper to get heights from shadow store ---
1819
+ const getItemHeight = useCallback(
1820
+ (index: number): number => {
1821
+ const metadata = getGlobalStore
1822
+ .getState()
1823
+ .getShadowMetadata(stateKey, [...path, index.toString()]);
1824
+ return metadata?.virtualizer?.itemHeight || itemHeight;
1825
+ },
1826
+ [itemHeight, stateKey, path]
1827
+ );
1828
+
1828
1829
  const isAtBottomRef = useRef(stickToBottom);
1829
1830
  const previousTotalCountRef = useRef(0);
1830
- // NEW: Ref to explicitly track if this is the component's first render cycle.
1831
1831
  const isInitialMountRef = useRef(true);
1832
1832
 
1833
1833
  const sourceArray = getGlobalStore().getNestedState(
@@ -1836,6 +1836,19 @@ function createProxyHandler<T>(
1836
1836
  ) as any[];
1837
1837
  const totalCount = sourceArray.length;
1838
1838
 
1839
+ // --- CHANGE 3: Pre-calculate total height and item positions ---
1840
+ // This replaces all instances of `totalCount * itemHeight`.
1841
+ const { totalHeight, positions } = useMemo(() => {
1842
+ let currentHeight = 0;
1843
+ const pos: number[] = [];
1844
+ for (let i = 0; i < totalCount; i++) {
1845
+ pos[i] = currentHeight;
1846
+ currentHeight += getItemHeight(i);
1847
+ }
1848
+ return { totalHeight: currentHeight, positions: pos };
1849
+ }, [totalCount, getItemHeight]);
1850
+
1851
+ // This part is IDENTICAL to your original code
1839
1852
  const virtualState = useMemo(() => {
1840
1853
  const start = Math.max(0, range.startIndex);
1841
1854
  const end = Math.min(totalCount, range.endIndex);
@@ -1862,21 +1875,36 @@ function createProxyHandler<T>(
1862
1875
  const { scrollTop, clientHeight, scrollHeight } = container;
1863
1876
  isAtBottomRef.current =
1864
1877
  scrollHeight - scrollTop - clientHeight < 10;
1865
- const start = Math.max(
1866
- 0,
1867
- Math.floor(scrollTop / itemHeight) - overscan
1868
- );
1869
- const end = Math.min(
1870
- totalCount,
1871
- Math.ceil((scrollTop + clientHeight) / itemHeight) +
1872
- overscan
1873
- );
1878
+
1879
+ // --- CHANGE 4: Update scroll logic to use positions array ---
1880
+ // This is the dynamic equivalent of `Math.floor(scrollTop / itemHeight)`.
1881
+ let startIndex = 0;
1882
+ // A simple loop is robust and easy to understand.
1883
+ for (let i = 0; i < positions.length; i++) {
1884
+ if (positions[i]! >= scrollTop) {
1885
+ startIndex = i;
1886
+ break;
1887
+ }
1888
+ }
1889
+
1890
+ let endIndex = startIndex;
1891
+ while (
1892
+ endIndex < totalCount &&
1893
+ positions[endIndex] &&
1894
+ positions[endIndex]! < scrollTop + clientHeight
1895
+ ) {
1896
+ endIndex++;
1897
+ }
1898
+
1899
+ startIndex = Math.max(0, startIndex - overscan);
1900
+ endIndex = Math.min(totalCount, endIndex + overscan);
1901
+
1874
1902
  setRange((prevRange) => {
1875
1903
  if (
1876
- prevRange.startIndex !== start ||
1877
- prevRange.endIndex !== end
1904
+ prevRange.startIndex !== startIndex ||
1905
+ prevRange.endIndex !== endIndex
1878
1906
  ) {
1879
- return { startIndex: start, endIndex: end };
1907
+ return { startIndex: startIndex, endIndex: endIndex };
1880
1908
  }
1881
1909
  return prevRange;
1882
1910
  });
@@ -1886,19 +1914,14 @@ function createProxyHandler<T>(
1886
1914
  passive: true,
1887
1915
  });
1888
1916
 
1889
- // --- THE CORRECTED DECISION LOGIC ---
1917
+ // This logic is IDENTICAL to your original code
1890
1918
  if (stickToBottom) {
1891
1919
  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
1920
  container.scrollTo({
1895
1921
  top: container.scrollHeight,
1896
1922
  behavior: "auto",
1897
1923
  });
1898
1924
  } 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
1925
  requestAnimationFrame(() => {
1903
1926
  container.scrollTo({
1904
1927
  top: container.scrollHeight,
@@ -1907,16 +1930,13 @@ function createProxyHandler<T>(
1907
1930
  });
1908
1931
  }
1909
1932
  }
1910
-
1911
- // After the logic runs, it's no longer the initial mount.
1912
1933
  isInitialMountRef.current = false;
1913
-
1914
- // Always run handleScroll once to set the initial visible window.
1915
1934
  handleScroll();
1916
1935
 
1917
1936
  return () =>
1918
1937
  container.removeEventListener("scroll", handleScroll);
1919
- }, [totalCount, itemHeight, overscan, stickToBottom]);
1938
+ // The dependencies are almost identical, just swapping itemHeight for `positions`
1939
+ }, [totalCount, overscan, stickToBottom, positions]);
1920
1940
 
1921
1941
  const scrollToBottom = useCallback(
1922
1942
  (behavior: ScrollBehavior = "smooth") => {
@@ -1930,19 +1950,20 @@ function createProxyHandler<T>(
1930
1950
  []
1931
1951
  );
1932
1952
 
1953
+ // --- CHANGE 5: Update scrollToIndex to use positions array ---
1933
1954
  const scrollToIndex = useCallback(
1934
1955
  (index: number, behavior: ScrollBehavior = "smooth") => {
1935
- if (containerRef.current) {
1956
+ if (containerRef.current && positions[index] !== undefined) {
1936
1957
  containerRef.current.scrollTo({
1937
- top: index * itemHeight,
1958
+ top: positions[index], // Instead of `index * itemHeight`
1938
1959
  behavior,
1939
1960
  });
1940
1961
  }
1941
1962
  },
1942
- [itemHeight]
1963
+ [positions] // Depends on `positions` now instead of `itemHeight`
1943
1964
  );
1944
1965
 
1945
- // Same virtualizer props as before
1966
+ // --- CHANGE 6: Update props to use dynamic totalHeight and offsets ---
1946
1967
  const virtualizerProps = {
1947
1968
  outer: {
1948
1969
  ref: containerRef,
@@ -1950,13 +1971,14 @@ function createProxyHandler<T>(
1950
1971
  },
1951
1972
  inner: {
1952
1973
  style: {
1953
- height: `${totalCount * itemHeight}px`,
1974
+ height: `${totalHeight}px`, // Use calculated total height
1954
1975
  position: "relative",
1955
1976
  },
1956
1977
  },
1957
1978
  list: {
1958
1979
  style: {
1959
- transform: `translateY(${range.startIndex * itemHeight}px)`,
1980
+ // Use the pre-calculated position of the first visible item
1981
+ transform: `translateY(${positions[range.startIndex] || 0}px)`,
1960
1982
  },
1961
1983
  },
1962
1984
  };
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: (key: string, arrayPath: string[]) => void;
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: (key: string, arrayPath: string[]) => {
332
+ insertShadowArrayElement: (
333
+ key: string,
334
+ arrayPath: string[],
335
+ newItem: any
336
+ ) => {
329
337
  set((state) => {
330
338
  const newShadow = { ...state.shadowStateStore };
331
- let current = newShadow[key];
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?.[segment];
346
+ current = current[segment];
347
+ if (!current) return state;
335
348
  }
336
349
 
337
350
  if (Array.isArray(current)) {
338
- current.push({});
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[],