cogsbox-state 0.5.296 → 0.5.298

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
@@ -1814,16 +1814,30 @@ function createProxyHandler<T>(
1814
1814
  endIndex: 10,
1815
1815
  });
1816
1816
 
1817
- const getItemHeight = useCallback(
1818
- (index: number): number => {
1819
- const metadata = getGlobalStore
1820
- .getState()
1821
- .getShadowMetadata(stateKey, [...path, index.toString()]);
1822
- return metadata?.virtualizer?.itemHeight || itemHeight;
1823
- },
1824
- [itemHeight, stateKey, path]
1817
+ const [heightsVersion, setHeightsVersion] = useState(0);
1818
+ const forceRecalculate = useCallback(
1819
+ () => setHeightsVersion((v) => v + 1),
1820
+ []
1825
1821
  );
1826
1822
 
1823
+ // --- This useEffect now cleanly subscribes to height changes ---
1824
+ useEffect(() => {
1825
+ // Subscribe to shadow state changes for this specific key.
1826
+ const unsubscribe = getGlobalStore
1827
+ .getState()
1828
+ .subscribeToShadowState(stateKey, forceRecalculate);
1829
+
1830
+ // On initial mount, we still need to trigger one recalculation
1831
+ // to capture heights from the very first render.
1832
+ const timer = setTimeout(forceRecalculate, 50);
1833
+
1834
+ // Cleanup function to unsubscribe when the component unmounts.
1835
+ return () => {
1836
+ unsubscribe();
1837
+ clearTimeout(timer);
1838
+ };
1839
+ }, [stateKey, forceRecalculate]); // Runs only once on mount.
1840
+
1827
1841
  const isAtBottomRef = useRef(stickToBottom);
1828
1842
  const previousTotalCountRef = useRef(0);
1829
1843
  const isInitialMountRef = useRef(true);
@@ -1835,17 +1849,20 @@ function createProxyHandler<T>(
1835
1849
  const totalCount = sourceArray.length;
1836
1850
 
1837
1851
  const { totalHeight, positions } = useMemo(() => {
1852
+ const shadowArray =
1853
+ getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
1854
+ [];
1838
1855
  let height = 0;
1839
1856
  const pos: number[] = [];
1840
1857
  for (let i = 0; i < totalCount; i++) {
1841
1858
  pos[i] = height;
1842
- height += getItemHeight(i);
1843
- console.log("height", getItemHeight(i), height);
1859
+ const measuredHeight =
1860
+ shadowArray[i]?.virtualizer?.itemHeight;
1861
+ height += measuredHeight || itemHeight;
1844
1862
  }
1845
1863
  return { totalHeight: height, positions: pos };
1846
- }, [totalCount, getItemHeight]);
1864
+ }, [totalCount, stateKey, path, itemHeight, heightsVersion]);
1847
1865
 
1848
- // This logic is IDENTICAL to your original code.
1849
1866
  const virtualState = useMemo(() => {
1850
1867
  const start = Math.max(0, range.startIndex);
1851
1868
  const end = Math.min(totalCount, range.endIndex);
@@ -1858,9 +1875,8 @@ function createProxyHandler<T>(
1858
1875
  ...meta,
1859
1876
  validIndices,
1860
1877
  });
1861
- }, [range.startIndex, range.endIndex, sourceArray, totalCount]);
1878
+ }, [range.startIndex, range.endIndex, sourceArray]);
1862
1879
 
1863
- // This useLayoutEffect is from your original code.
1864
1880
  useLayoutEffect(() => {
1865
1881
  const container = containerRef.current;
1866
1882
  if (!container) return;
@@ -1874,15 +1890,12 @@ function createProxyHandler<T>(
1874
1890
  isAtBottomRef.current =
1875
1891
  scrollHeight - scrollTop - clientHeight < 10;
1876
1892
 
1877
- // --- THE ROBUST FIX: Binary search to find the start index ---
1878
- // This is extremely fast and correctly handles all scroll positions.
1879
1893
  let search = (list: number[], value: number) => {
1880
1894
  let low = 0;
1881
1895
  let high = list.length - 1;
1882
1896
  while (low <= high) {
1883
1897
  const mid = Math.floor((low + high) / 2);
1884
- const midValue = list[mid]!;
1885
- if (midValue < value) {
1898
+ if (list[mid]! < value) {
1886
1899
  low = mid + 1;
1887
1900
  } else {
1888
1901
  high = mid - 1;
@@ -1892,7 +1905,6 @@ function createProxyHandler<T>(
1892
1905
  };
1893
1906
 
1894
1907
  let startIndex = search(positions, scrollTop);
1895
-
1896
1908
  let endIndex = startIndex;
1897
1909
  while (
1898
1910
  endIndex < totalCount &&
@@ -1903,7 +1915,7 @@ function createProxyHandler<T>(
1903
1915
 
1904
1916
  startIndex = Math.max(0, startIndex - overscan);
1905
1917
  endIndex = Math.min(totalCount, endIndex + overscan);
1906
- console.log("startIndex", startIndex, "endIndex", endIndex);
1918
+
1907
1919
  setRange((prevRange) => {
1908
1920
  if (
1909
1921
  prevRange.startIndex !== startIndex ||
@@ -1919,7 +1931,6 @@ function createProxyHandler<T>(
1919
1931
  passive: true,
1920
1932
  });
1921
1933
 
1922
- // This stickToBottom logic is IDENTICAL to your original.
1923
1934
  if (stickToBottom) {
1924
1935
  if (isInitialMountRef.current) {
1925
1936
  container.scrollTo({
@@ -1927,15 +1938,13 @@ function createProxyHandler<T>(
1927
1938
  behavior: "auto",
1928
1939
  });
1929
1940
  } else if (wasAtBottom && listGrew) {
1930
- requestAnimationFrame(() => {
1931
- container.scrollTo({
1932
- top: container.scrollHeight,
1933
- behavior: "smooth",
1934
- });
1941
+ // Use 'auto' for an instant jump to the bottom to prevent visual glitches.
1942
+ container.scrollTo({
1943
+ top: container.scrollHeight,
1944
+ behavior: "auto",
1935
1945
  });
1936
1946
  }
1937
1947
  }
1938
-
1939
1948
  isInitialMountRef.current = false;
1940
1949
  handleScroll();
1941
1950
 
@@ -2933,7 +2942,6 @@ export function $cogsSignalStore(proxy: {
2933
2942
  );
2934
2943
  return createElement("text", {}, String(value));
2935
2944
  }
2936
-
2937
2945
  function CogsItemWrapper({
2938
2946
  stateKey,
2939
2947
  itemComponentId,
@@ -2945,19 +2953,31 @@ function CogsItemWrapper({
2945
2953
  itemPath: string[];
2946
2954
  children: React.ReactNode;
2947
2955
  }) {
2956
+ // This hook handles the re-rendering when the item's own data changes.
2948
2957
  const [, forceUpdate] = useState({});
2958
+ // This hook measures the element.
2949
2959
  const [ref, bounds] = useMeasure();
2960
+ // This ref prevents sending the same height update repeatedly.
2961
+ const lastReportedHeight = useRef<number | null>(null);
2950
2962
 
2963
+ // This is the primary effect for this component.
2951
2964
  useEffect(() => {
2952
- if (bounds.height > 0) {
2965
+ // We only report a height if it's a valid number AND it's different
2966
+ // from the last height we reported. This prevents infinite loops.
2967
+ if (bounds.height > 0 && bounds.height !== lastReportedHeight.current) {
2968
+ // Store the new height so we don't report it again.
2969
+ lastReportedHeight.current = bounds.height;
2970
+
2971
+ // Call the store function to save the height and notify listeners.
2953
2972
  getGlobalStore.getState().setShadowMetadata(stateKey, itemPath, {
2954
2973
  virtualizer: {
2955
2974
  itemHeight: bounds.height,
2956
2975
  },
2957
2976
  });
2958
2977
  }
2959
- }, [bounds.height]);
2978
+ }, [bounds.height, stateKey, itemPath]); // Reruns whenever the measured height changes.
2960
2979
 
2980
+ // This effect handles subscribing the item to its own data path for updates.
2961
2981
  useLayoutEffect(() => {
2962
2982
  const fullComponentId = `${stateKey}////${itemComponentId}`;
2963
2983
  const stateEntry = getGlobalStore
@@ -2983,5 +3003,6 @@ function CogsItemWrapper({
2983
3003
  };
2984
3004
  }, [stateKey, itemComponentId, itemPath.join(".")]);
2985
3005
 
3006
+ // The rendered output is a simple div that gets measured.
2986
3007
  return <div ref={ref}>{children}</div>;
2987
3008
  }
package/src/store.ts CHANGED
@@ -98,8 +98,12 @@ type ShadowState<T> =
98
98
  : T extends object
99
99
  ? { [K in keyof T]: ShadowState<T[K]> } & ShadowMetadata
100
100
  : ShadowMetadata;
101
+
101
102
  export type CogsGlobalState = {
103
+ // --- Shadow State and Subscription System ---
102
104
  shadowStateStore: { [key: string]: any };
105
+ shadowStateSubscribers: Map<string, Set<() => void>>; // Stores subscribers for shadow state updates
106
+ subscribeToShadowState: (key: string, callback: () => void) => () => void; // Subscribes a listener, returns an unsubscribe function
103
107
  initializeShadowState: (key: string, initialState: any) => void;
104
108
  updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
105
109
  insertShadowArrayElement: (
@@ -115,9 +119,8 @@ export type CogsGlobalState = {
115
119
  getShadowMetadata: (key: string, path: string[]) => any;
116
120
  setShadowMetadata: (key: string, path: string[], metadata: any) => void;
117
121
 
122
+ // --- Selected Item State ---
118
123
  selectedIndicesMap: Map<string, Map<string, number>>; // stateKey -> (parentPath -> selectedIndex)
119
-
120
- // Add these new methods
121
124
  getSelectedIndex: (
122
125
  stateKey: string,
123
126
  parentPath: string
@@ -135,36 +138,16 @@ export type CogsGlobalState = {
135
138
  path: string[];
136
139
  }) => void;
137
140
  clearSelectedIndexesForState: (stateKey: string) => void;
141
+
142
+ // --- Core State and Updaters ---
138
143
  updaterState: { [key: string]: any };
139
144
  initialStateOptions: { [key: string]: OptionsType };
140
145
  cogsStateStore: { [key: string]: StateValue };
141
146
  isLoadingGlobal: { [key: string]: boolean };
142
-
143
147
  initialStateGlobal: { [key: string]: StateValue };
144
148
  iniitialCreatedState: { [key: string]: StateValue };
145
- validationErrors: Map<string, string[]>;
146
-
147
149
  serverState: { [key: string]: StateValue };
148
- serverSyncActions: { [key: string]: SyncActionsType<any> };
149
-
150
- serverSyncLog: { [key: string]: SyncLogType[] };
151
- serverSideOrNot: { [key: string]: boolean };
152
- setServerSyncLog: (key: string, newValue: SyncLogType) => void;
153
-
154
- setServerSideOrNot: (key: string, value: boolean) => void;
155
- getServerSideOrNot: (key: string) => boolean | undefined;
156
- setServerState: <StateKey extends StateKeys>(
157
- key: StateKey,
158
- value: StateValue
159
- ) => void;
160
150
 
161
- getThisLocalUpdate: (key: string) => UpdateTypeDetail[] | undefined;
162
- setServerSyncActions: (key: string, value: SyncActionsType<any>) => void;
163
- addValidationError: (path: string, message: string) => void;
164
- getValidationErrors: (path: string) => string[];
165
- updateInitialStateGlobal: (key: string, newState: StateValue) => void;
166
- updateInitialCreatedState: (key: string, newState: StateValue) => void;
167
- getInitialOptions: (key: string) => OptionsType | undefined;
168
151
  getUpdaterState: (key: string) => StateUpdater<StateValue>;
169
152
  setUpdaterState: (key: string, newUpdater: any) => void;
170
153
  getKeyState: <StateKey extends StateKeys>(key: StateKey) => StateValue;
@@ -178,15 +161,41 @@ export type CogsGlobalState = {
178
161
  ) => void;
179
162
  setInitialStates: (initialState: StateValue) => void;
180
163
  setCreatedState: (initialState: StateValue) => void;
164
+ updateInitialStateGlobal: (key: string, newState: StateValue) => void;
165
+ updateInitialCreatedState: (key: string, newState: StateValue) => void;
166
+ setIsLoadingGlobal: (key: string, value: boolean) => void;
167
+ setServerState: <StateKey extends StateKeys>(
168
+ key: StateKey,
169
+ value: StateValue
170
+ ) => void;
171
+ getInitialOptions: (key: string) => OptionsType | undefined;
172
+ setInitialStateOptions: (key: string, value: OptionsType) => void;
173
+
174
+ // --- Validation ---
175
+ validationErrors: Map<string, string[]>;
176
+ addValidationError: (path: string, message: string) => void;
177
+ getValidationErrors: (path: string) => string[];
178
+ removeValidationError: (path: string) => void;
179
+
180
+ // --- Server Sync and Logging ---
181
+ serverSyncActions: { [key: string]: SyncActionsType<any> };
182
+ serverSyncLog: { [key: string]: SyncLogType[] };
181
183
  stateLog: { [key: string]: UpdateTypeDetail[] };
184
+ syncInfoStore: Map<string, SyncInfo>;
185
+ serverSideOrNot: { [key: string]: boolean };
186
+ setServerSyncLog: (key: string, newValue: SyncLogType) => void;
187
+ setServerSideOrNot: (key: string, value: boolean) => void;
188
+ getServerSideOrNot: (key: string) => boolean | undefined;
189
+ getThisLocalUpdate: (key: string) => UpdateTypeDetail[] | undefined;
190
+ setServerSyncActions: (key: string, value: SyncActionsType<any>) => void;
182
191
  setStateLog: (
183
192
  key: string,
184
193
  updater: (prevUpdates: UpdateTypeDetail[]) => UpdateTypeDetail[]
185
194
  ) => void;
186
- setIsLoadingGlobal: (key: string, value: boolean) => void;
195
+ setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
196
+ getSyncInfo: (key: string) => SyncInfo | null;
187
197
 
188
- setInitialStateOptions: (key: string, value: OptionsType) => void;
189
- removeValidationError: (path: string) => void;
198
+ // --- Component and DOM Integration ---
190
199
  signalDomElements: Map<
191
200
  string,
192
201
  Set<{
@@ -208,8 +217,10 @@ export type CogsGlobalState = {
208
217
  }
209
218
  ) => void;
210
219
  removeSignalElement: (signalId: string, instanceId: string) => void;
211
- reRenderTriggerPrevValue: Record<string, any>;
220
+ stateComponents: Map<string, ComponentsType>;
212
221
 
222
+ // --- Deprecated/Legacy (Review for removal) ---
223
+ reRenderTriggerPrevValue: Record<string, any>;
213
224
  reactiveDeps: Record<
214
225
  string,
215
226
  {
@@ -228,11 +239,6 @@ export type CogsGlobalState = {
228
239
  ) => void;
229
240
  deleteReactiveDeps: (key: string) => void;
230
241
  subscribe: (listener: () => void) => () => void;
231
-
232
- stateComponents: Map<string, ComponentsType>;
233
- syncInfoStore: Map<string, SyncInfo>;
234
- setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
235
- getSyncInfo: (key: string) => SyncInfo | null;
236
242
  };
237
243
 
238
244
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
@@ -250,30 +256,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
250
256
  return current;
251
257
  },
252
258
 
253
- setShadowMetadata: (key: string, path: string[], metadata: any) => {
254
- set((state) => {
255
- const newShadow = { ...state.shadowStateStore };
256
- if (!newShadow[key]) return state;
257
-
258
- newShadow[key] = JSON.parse(JSON.stringify(newShadow[key]));
259
-
260
- let current: any = newShadow[key];
261
- for (const segment of path) {
262
- if (!current[segment]) current[segment] = {};
263
- current = current[segment];
264
- }
265
-
266
- // Merge the metadata into the existing structure
267
- Object.keys(metadata).forEach((category) => {
268
- if (!current[category]) {
269
- current[category] = {};
270
- }
271
- Object.assign(current[category], metadata[category]);
272
- });
273
-
274
- return { shadowStateStore: newShadow };
275
- });
276
- },
277
259
  initializeShadowState: (key: string, initialState: any) => {
278
260
  const createShadowStructure = (obj: any): any => {
279
261
  if (Array.isArray(obj)) {
@@ -389,6 +371,63 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
389
371
  return { shadowStateStore: newShadow };
390
372
  });
391
373
  },
374
+ shadowStateSubscribers: new Map<string, Set<() => void>>(), // key -> Set of callbacks
375
+
376
+ subscribeToShadowState: (key: string, callback: () => void) => {
377
+ set((state) => {
378
+ const newSubs = new Map(state.shadowStateSubscribers);
379
+ const subsForKey = newSubs.get(key) || new Set();
380
+ subsForKey.add(callback);
381
+ newSubs.set(key, subsForKey);
382
+ return { shadowStateSubscribers: newSubs };
383
+ });
384
+ // Return an unsubscribe function
385
+ return () => {
386
+ set((state) => {
387
+ const newSubs = new Map(state.shadowStateSubscribers);
388
+ const subsForKey = newSubs.get(key);
389
+ if (subsForKey) {
390
+ subsForKey.delete(callback);
391
+ }
392
+ return { shadowStateSubscribers: newSubs };
393
+ });
394
+ };
395
+ },
396
+
397
+ setShadowMetadata: (key: string, path: string[], metadata: any) => {
398
+ let hasChanged = false;
399
+ set((state) => {
400
+ const newShadow = { ...state.shadowStateStore };
401
+ if (!newShadow[key]) return state;
402
+
403
+ newShadow[key] = JSON.parse(JSON.stringify(newShadow[key]));
404
+
405
+ let current: any = newShadow[key];
406
+ for (const segment of path) {
407
+ if (!current[segment]) current[segment] = {};
408
+ current = current[segment];
409
+ }
410
+
411
+ const oldHeight = current.virtualizer?.itemHeight;
412
+ const newHeight = metadata.virtualizer?.itemHeight;
413
+
414
+ if (newHeight && oldHeight !== newHeight) {
415
+ hasChanged = true;
416
+ if (!current.virtualizer) current.virtualizer = {};
417
+ current.virtualizer.itemHeight = newHeight;
418
+ }
419
+
420
+ return { shadowStateStore: newShadow };
421
+ });
422
+
423
+ // If a height value was actually changed, notify the specific subscribers.
424
+ if (hasChanged) {
425
+ const subscribers = get().shadowStateSubscribers.get(key);
426
+ if (subscribers) {
427
+ subscribers.forEach((callback) => callback());
428
+ }
429
+ }
430
+ },
392
431
  selectedIndicesMap: new Map<string, Map<string, number>>(),
393
432
 
394
433
  // Add the new methods