cogsbox-state 0.5.460 → 0.5.462

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
@@ -284,7 +284,7 @@ export type EndType<T, IsArrayElement = false> = {
284
284
  hideMessage?: boolean;
285
285
  }) => JSX.Element;
286
286
  lastSynced?: SyncInfo;
287
- } & (IsArrayElement extends true ? { cut: () => void } : {});
287
+ } & (IsArrayElement extends true ? { cutThis: () => void } : {});
288
288
 
289
289
  export type StateObject<T> = (T extends any[]
290
290
  ? ArrayEndType<T>
@@ -419,13 +419,7 @@ export type OptionsType<T extends unknown = unknown, TApiParams = never> = {
419
419
  onSuccess?: (data: any) => void;
420
420
  onError?: (error: any) => void;
421
421
  };
422
- middleware?: ({
423
- updateLog,
424
- update,
425
- }: {
426
- updateLog: UpdateTypeDetail[] | undefined;
427
- update: UpdateTypeDetail;
428
- }) => void;
422
+ middleware?: ({ update }: { update: UpdateTypeDetail }) => void;
429
423
 
430
424
  modifyState?: (state: T) => T;
431
425
  localStorage?: {
@@ -574,6 +568,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
574
568
  __syncNotifications?: Record<string, Function>;
575
569
  __apiParamsMap?: Record<string, any>;
576
570
  __useSync?: UseSyncType<State>;
571
+ __syncSchemas?: Record<string, any>;
577
572
  }
578
573
  ) => {
579
574
  let newInitialState = initialState;
@@ -621,7 +616,12 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
621
616
  mergedOptions.validation.key = `${opt.validation.key}.${key}`;
622
617
  }
623
618
  }
624
-
619
+ if (opt?.__syncSchemas?.[key]?.schemas?.validation) {
620
+ mergedOptions.validation = {
621
+ zodSchemaV4: opt.__syncSchemas[key].schemas.validation,
622
+ ...existingOptions.validation,
623
+ };
624
+ }
625
625
  if (Object.keys(mergedOptions).length > 0) {
626
626
  const existingGlobalOptions = getInitialOptions(key);
627
627
 
@@ -692,38 +692,33 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
692
692
  notifyComponents(stateKey as string);
693
693
  }
694
694
 
695
- return { useCogsState, setCogsOptions } as CogsApi<State>;
695
+ return { useCogsState, setCogsOptions } as CogsApi<State, never>;
696
696
  };
697
-
698
- // Fix for UseCogsStateHook to support per-key apiParams
699
697
  type UseCogsStateHook<
700
698
  T extends Record<string, any>,
701
- TApiParamsMap extends Record<string, any> = Record<string, never>,
699
+ TApiParamsMap extends Record<string, any> = never,
702
700
  > = <StateKey extends keyof TransformedStateType<T> & string>(
703
701
  stateKey: StateKey,
704
- options?: Prettify<
705
- OptionsType<TransformedStateType<T>[StateKey], TApiParamsMap[StateKey]> &
706
- // This is the conditional part that solves the problem
707
- ([keyof TApiParamsMap] extends [never]
708
- ? // If TApiParamsMap has NO keys, intersect with an empty object.
709
- // This means `syncOptions` is NOT a valid property.
710
- {}
711
- : // If TApiParamsMap HAS keys, intersect with a type that REQUIRES `syncOptions`.
712
- {
713
- syncOptions: Prettify<
714
- SyncOptionsType<
715
- StateKey extends keyof TApiParamsMap
716
- ? TApiParamsMap[StateKey]
717
- : never
718
- >
719
- >;
720
- })
721
- >
702
+ options?: [TApiParamsMap] extends [never]
703
+ ? // When TApiParamsMap is never (no sync)
704
+ Prettify<OptionsType<TransformedStateType<T>[StateKey]>>
705
+ : // When TApiParamsMap exists (sync enabled)
706
+ StateKey extends keyof TApiParamsMap
707
+ ? Prettify<
708
+ OptionsType<
709
+ TransformedStateType<T>[StateKey],
710
+ TApiParamsMap[StateKey]
711
+ > & {
712
+ syncOptions: Prettify<SyncOptionsType<TApiParamsMap[StateKey]>>;
713
+ }
714
+ >
715
+ : Prettify<OptionsType<TransformedStateType<T>[StateKey]>>
722
716
  ) => StateObject<TransformedStateType<T>[StateKey]>;
723
- // Updated CogsApi type
717
+
718
+ // Update CogsApi to default to never instead of Record<string, never>
724
719
  type CogsApi<
725
720
  T extends Record<string, any>,
726
- TApiParamsMap extends Record<string, any> = Record<string, never>,
721
+ TApiParamsMap extends Record<string, any> = never,
727
722
  > = {
728
723
  useCogsState: UseCogsStateHook<T, TApiParamsMap>;
729
724
  setCogsOptions: SetCogsOptionsFunc<T>;
@@ -781,12 +776,13 @@ export function createCogsStateFromSync<
781
776
  __syncNotifications: syncSchema.notifications,
782
777
  __apiParamsMap: apiParamsMap,
783
778
  __useSync: useSync,
779
+ __syncSchemas: schemas,
784
780
  }) as any;
785
781
  }
786
782
  const {
787
783
  getInitialOptions,
788
784
 
789
- setStateLog,
785
+ addStateLog,
790
786
  updateInitialStateGlobal,
791
787
  } = getGlobalStore.getState();
792
788
  const saveToLocalStorage = <T,>(
@@ -1006,8 +1002,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1006
1002
 
1007
1003
  let noStateKey = stateKey ? false : true;
1008
1004
  const [thisKey] = useState(stateKey ?? uuidv4());
1009
- const stateLog = getGlobalStore.getState().stateLog[thisKey];
1010
- const componentUpdatesRef = useRef(new Set<string>());
1005
+
1011
1006
  const componentIdRef = useRef(componentId ?? uuidv4());
1012
1007
  const latestInitialOptionsRef = useRef<OptionsType<TStateObject> | null>(
1013
1008
  null
@@ -1252,6 +1247,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1252
1247
  notifyComponents(thisKey);
1253
1248
  }
1254
1249
  }, [thisKey, ...(dependencies || [])]);
1250
+
1255
1251
  useLayoutEffect(() => {
1256
1252
  if (noStateKey) {
1257
1253
  setAndMergeOptions(thisKey as string, {
@@ -1321,19 +1317,16 @@ export function useCogsStateFn<TStateObject extends unknown>(
1321
1317
  }, []);
1322
1318
 
1323
1319
  const syncApiRef = useRef<SyncApi | null>(null);
1320
+
1324
1321
  const effectiveSetState = (
1325
1322
  newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
1326
1323
  path: string[],
1327
1324
  updateObj: UpdateOptions
1328
1325
  ) => {
1329
1326
  const fullPath = [thisKey, ...path].join('.');
1330
- if (Array.isArray(path)) {
1331
- const pathKey = `${thisKey}-${path.join('.')}`;
1332
- componentUpdatesRef.current.add(pathKey);
1333
- }
1327
+
1334
1328
  const store = getGlobalStore.getState();
1335
1329
 
1336
- // FETCH ONCE at the beginning
1337
1330
  const shadowMeta = store.getShadowMetadata(thisKey, path);
1338
1331
  const nestedShadowValue = store.getShadowValue(fullPath) as TStateObject;
1339
1332
 
@@ -1561,15 +1554,11 @@ export function useCogsStateFn<TStateObject extends unknown>(
1561
1554
  }
1562
1555
  }
1563
1556
 
1564
- // Assumes `isDeepEqual` is available in this scope.
1565
- // Assumes `isDeepEqual` is available in this scope.
1566
-
1567
- const newState = getGlobalStore.getState().getShadowValue(thisKey);
1568
- const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1557
+ const rootMeta = store.getShadowMetadata(thisKey, []);
1569
1558
  const notifiedComponents = new Set<string>();
1570
1559
 
1571
1560
  if (!rootMeta?.components) {
1572
- return newState;
1561
+ return;
1573
1562
  }
1574
1563
 
1575
1564
  // --- PASS 1: Notify specific subscribers based on update type ---
@@ -1751,26 +1740,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1751
1740
  }
1752
1741
  });
1753
1742
  notifiedComponents.clear();
1754
- setStateLog(thisKey, (prevLogs) => {
1755
- const logs = [...(prevLogs ?? []), newUpdate];
1756
- const aggregatedLogs = new Map<string, typeof newUpdate>();
1757
-
1758
- logs.forEach((log) => {
1759
- const uniqueKey = `${log.stateKey}:${JSON.stringify(log.path)}`;
1760
- const existing = aggregatedLogs.get(uniqueKey);
1761
-
1762
- if (existing) {
1763
- existing.timeStamp = Math.max(existing.timeStamp, log.timeStamp);
1764
- existing.newValue = log.newValue;
1765
- existing.oldValue = existing.oldValue ?? log.oldValue;
1766
- existing.updateType = log.updateType;
1767
- } else {
1768
- aggregatedLogs.set(uniqueKey, { ...(log as any) });
1769
- }
1770
- });
1771
-
1772
- return Array.from(aggregatedLogs.values());
1773
- });
1743
+ addStateLog(thisKey, newUpdate);
1774
1744
 
1775
1745
  saveToLocalStorage(
1776
1746
  payload,
@@ -1781,12 +1751,9 @@ export function useCogsStateFn<TStateObject extends unknown>(
1781
1751
 
1782
1752
  if (latestInitialOptionsRef.current?.middleware) {
1783
1753
  latestInitialOptionsRef.current!.middleware({
1784
- updateLog: stateLog,
1785
1754
  update: newUpdate,
1786
1755
  });
1787
1756
  }
1788
-
1789
- return newState;
1790
1757
  };
1791
1758
 
1792
1759
  if (!getGlobalStore.getState().initialStateGlobal[thisKey]) {
@@ -1959,48 +1926,33 @@ const notifySelectionComponents = (
1959
1926
  }
1960
1927
  }
1961
1928
  };
1929
+
1962
1930
  function createProxyHandler<T>(
1963
1931
  stateKey: string,
1964
1932
  effectiveSetState: EffectiveSetState<T>,
1965
1933
  componentId: string,
1966
1934
  sessionId?: string
1967
1935
  ): StateObject<T> {
1968
- type CacheEntry = {
1969
- proxy: any;
1970
- stateVersion: number;
1971
- };
1972
-
1973
- const shapeCache = new Map<string, CacheEntry>();
1936
+ const proxyCache = new Map<string, any>();
1974
1937
  let stateVersion = 0;
1975
1938
 
1976
- const invalidateCachePath = (path: string[]) => {
1977
- const pathKey = path.join('.');
1978
- for (const [key] of shapeCache) {
1979
- if (key === pathKey || key.startsWith(pathKey + '.')) {
1980
- shapeCache.delete(key);
1981
- }
1982
- }
1983
- stateVersion++;
1984
- };
1985
-
1986
1939
  function rebuildStateShape({
1987
- currentState,
1988
1940
  path = [],
1989
1941
  meta,
1990
1942
  componentId,
1991
1943
  }: {
1992
- currentState: T;
1993
1944
  path: string[];
1994
1945
  componentId: string;
1995
1946
  meta?: MetaData;
1996
1947
  }): any {
1997
- const cacheKey = path.map(String).join('.');
1948
+ const derivationSignature = meta
1949
+ ? JSON.stringify(meta.validIds || meta.transforms)
1950
+ : '';
1951
+ const cacheKey = path.join('.') + ':' + derivationSignature;
1998
1952
  const stateKeyPathKey = [stateKey, ...path].join('.');
1999
-
2000
- currentState = getGlobalStore
2001
- .getState()
2002
- .getShadowValue(stateKeyPathKey, meta?.validIds);
2003
-
1953
+ if (proxyCache.has(cacheKey)) {
1954
+ return proxyCache.get(cacheKey);
1955
+ }
2004
1956
  type CallableStateObject<T> = {
2005
1957
  (): T;
2006
1958
  } & {
@@ -2198,1167 +2150,1154 @@ function createProxyHandler<T>(
2198
2150
  return [];
2199
2151
  };
2200
2152
  }
2201
- if (Array.isArray(currentState)) {
2202
- if (prop === 'getSelected') {
2203
- return () => {
2204
- const fullKey = stateKey + '.' + path.join('.');
2205
- registerComponentDependency(stateKey, componentId, [
2206
- ...path,
2207
- 'getSelected',
2208
- ]);
2209
-
2210
- const selectedIndicesMap =
2211
- getGlobalStore.getState().selectedIndicesMap;
2212
- if (!selectedIndicesMap || !selectedIndicesMap.has(fullKey)) {
2213
- return undefined;
2214
- }
2215
-
2216
- const selectedItemKey = selectedIndicesMap.get(fullKey)!;
2217
- if (meta?.validIds) {
2218
- if (!meta.validIds.includes(selectedItemKey)) {
2219
- return undefined;
2220
- }
2221
- }
2222
2153
 
2223
- const value = getGlobalStore
2224
- .getState()
2225
- .getShadowValue(selectedItemKey);
2154
+ if (prop === 'getSelected') {
2155
+ return () => {
2156
+ const fullKey = stateKey + '.' + path.join('.');
2157
+ registerComponentDependency(stateKey, componentId, [
2158
+ ...path,
2159
+ 'getSelected',
2160
+ ]);
2161
+
2162
+ const selectedIndicesMap =
2163
+ getGlobalStore.getState().selectedIndicesMap;
2164
+ if (!selectedIndicesMap || !selectedIndicesMap.has(fullKey)) {
2165
+ return undefined;
2166
+ }
2226
2167
 
2227
- if (!value) {
2168
+ const selectedItemKey = selectedIndicesMap.get(fullKey)!;
2169
+ if (meta?.validIds) {
2170
+ if (!meta.validIds.includes(selectedItemKey)) {
2228
2171
  return undefined;
2229
2172
  }
2173
+ }
2230
2174
 
2231
- return rebuildStateShape({
2232
- currentState: value,
2233
- path: selectedItemKey.split('.').slice(1) as string[],
2234
- componentId: componentId!,
2235
- });
2236
- };
2237
- }
2238
- if (prop === 'getSelectedIndex') {
2239
- return () => {
2240
- const selectedIndex = getGlobalStore
2241
- .getState()
2242
- .getSelectedIndex(
2243
- stateKey + '.' + path.join('.'),
2244
- meta?.validIds
2245
- );
2246
-
2247
- return selectedIndex;
2248
- };
2249
- }
2250
- if (prop === 'clearSelected') {
2251
- notifySelectionComponents(stateKey, path);
2252
- return () => {
2253
- getGlobalStore.getState().clearSelectedIndex({
2254
- arrayKey: stateKey + '.' + path.join('.'),
2255
- });
2256
- };
2257
- }
2175
+ const value = getGlobalStore
2176
+ .getState()
2177
+ .getShadowValue(selectedItemKey);
2258
2178
 
2259
- if (prop === 'useVirtualView') {
2260
- return (
2261
- options: VirtualViewOptions
2262
- ): VirtualStateObjectResult<any[]> => {
2263
- const {
2264
- itemHeight = 50,
2265
- overscan = 6,
2266
- stickToBottom = false,
2267
- scrollStickTolerance = 75,
2268
- } = options;
2269
-
2270
- const containerRef = useRef<HTMLDivElement | null>(null);
2271
- const [range, setRange] = useState({
2272
- startIndex: 0,
2273
- endIndex: 10,
2274
- });
2275
- const [rerender, forceUpdate] = useState({});
2276
- const initialScrollRef = useRef(true);
2277
-
2278
- // Scroll state management
2279
- const scrollStateRef = useRef({
2280
- isUserScrolling: false,
2281
- lastScrollTop: 0,
2282
- scrollUpCount: 0,
2283
- isNearBottom: true,
2284
- });
2179
+ if (!value) {
2180
+ return undefined;
2181
+ }
2285
2182
 
2286
- // Measurement cache
2287
- const measurementCache = useRef(
2288
- new Map<string, { height: number; offset: number }>()
2183
+ return rebuildStateShape({
2184
+ path: selectedItemKey.split('.').slice(1) as string[],
2185
+ componentId: componentId!,
2186
+ });
2187
+ };
2188
+ }
2189
+ if (prop === 'getSelectedIndex') {
2190
+ return () => {
2191
+ const selectedIndex = getGlobalStore
2192
+ .getState()
2193
+ .getSelectedIndex(
2194
+ stateKey + '.' + path.join('.'),
2195
+ meta?.validIds
2289
2196
  );
2290
2197
 
2291
- // Separate effect for handling rerender updates
2292
- useLayoutEffect(() => {
2293
- if (
2294
- !stickToBottom ||
2295
- !containerRef.current ||
2296
- scrollStateRef.current.isUserScrolling
2297
- )
2298
- return;
2198
+ return selectedIndex;
2199
+ };
2200
+ }
2201
+ if (prop === 'clearSelected') {
2202
+ notifySelectionComponents(stateKey, path);
2203
+ return () => {
2204
+ getGlobalStore.getState().clearSelectedIndex({
2205
+ arrayKey: stateKey + '.' + path.join('.'),
2206
+ });
2207
+ };
2208
+ }
2299
2209
 
2300
- const container = containerRef.current;
2301
- container.scrollTo({
2302
- top: container.scrollHeight,
2303
- behavior: initialScrollRef.current ? 'instant' : 'smooth',
2304
- });
2305
- }, [rerender, stickToBottom]);
2210
+ if (prop === 'useVirtualView') {
2211
+ return (
2212
+ options: VirtualViewOptions
2213
+ ): VirtualStateObjectResult<any[]> => {
2214
+ const {
2215
+ itemHeight = 50,
2216
+ overscan = 6,
2217
+ stickToBottom = false,
2218
+ scrollStickTolerance = 75,
2219
+ } = options;
2220
+
2221
+ const containerRef = useRef<HTMLDivElement | null>(null);
2222
+ const [range, setRange] = useState({
2223
+ startIndex: 0,
2224
+ endIndex: 10,
2225
+ });
2226
+ const [rerender, forceUpdate] = useState({});
2227
+ const initialScrollRef = useRef(true);
2228
+
2229
+ // Scroll state management
2230
+ const scrollStateRef = useRef({
2231
+ isUserScrolling: false,
2232
+ lastScrollTop: 0,
2233
+ scrollUpCount: 0,
2234
+ isNearBottom: true,
2235
+ });
2236
+
2237
+ // Measurement cache
2238
+ const measurementCache = useRef(
2239
+ new Map<string, { height: number; offset: number }>()
2240
+ );
2241
+
2242
+ // Separate effect for handling rerender updates
2243
+ useLayoutEffect(() => {
2244
+ if (
2245
+ !stickToBottom ||
2246
+ !containerRef.current ||
2247
+ scrollStateRef.current.isUserScrolling
2248
+ )
2249
+ return;
2306
2250
 
2307
- const arrayKeys =
2251
+ const container = containerRef.current;
2252
+ container.scrollTo({
2253
+ top: container.scrollHeight,
2254
+ behavior: initialScrollRef.current ? 'instant' : 'smooth',
2255
+ });
2256
+ }, [rerender, stickToBottom]);
2257
+
2258
+ const arrayKeys =
2259
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2260
+ ?.arrayKeys || [];
2261
+
2262
+ // Calculate total height and offsets
2263
+ const { totalHeight, itemOffsets } = useMemo(() => {
2264
+ let runningOffset = 0;
2265
+ const offsets = new Map<
2266
+ string,
2267
+ { height: number; offset: number }
2268
+ >();
2269
+ const allItemKeys =
2308
2270
  getGlobalStore.getState().getShadowMetadata(stateKey, path)
2309
2271
  ?.arrayKeys || [];
2310
2272
 
2311
- // Calculate total height and offsets
2312
- const { totalHeight, itemOffsets } = useMemo(() => {
2313
- let runningOffset = 0;
2314
- const offsets = new Map<
2315
- string,
2316
- { height: number; offset: number }
2317
- >();
2318
- const allItemKeys =
2319
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2320
- ?.arrayKeys || [];
2321
-
2322
- allItemKeys.forEach((itemKey) => {
2323
- const itemPath = itemKey.split('.').slice(1);
2324
- const measuredHeight =
2325
- getGlobalStore
2326
- .getState()
2327
- .getShadowMetadata(stateKey, itemPath)?.virtualizer
2328
- ?.itemHeight || itemHeight;
2329
-
2330
- offsets.set(itemKey, {
2331
- height: measuredHeight,
2332
- offset: runningOffset,
2333
- });
2273
+ allItemKeys.forEach((itemKey) => {
2274
+ const itemPath = itemKey.split('.').slice(1);
2275
+ const measuredHeight =
2276
+ getGlobalStore
2277
+ .getState()
2278
+ .getShadowMetadata(stateKey, itemPath)?.virtualizer
2279
+ ?.itemHeight || itemHeight;
2334
2280
 
2335
- runningOffset += measuredHeight;
2281
+ offsets.set(itemKey, {
2282
+ height: measuredHeight,
2283
+ offset: runningOffset,
2336
2284
  });
2337
2285
 
2338
- measurementCache.current = offsets;
2339
- return { totalHeight: runningOffset, itemOffsets: offsets };
2340
- }, [arrayKeys.length, itemHeight]);
2341
-
2342
- // Improved initial positioning effect
2343
- useLayoutEffect(() => {
2344
- if (
2345
- stickToBottom &&
2346
- arrayKeys.length > 0 &&
2347
- containerRef.current &&
2348
- !scrollStateRef.current.isUserScrolling &&
2349
- initialScrollRef.current
2350
- ) {
2351
- const container = containerRef.current;
2352
-
2353
- // Wait for container to have dimensions
2354
- const waitForContainer = () => {
2355
- if (container.clientHeight > 0) {
2356
- const visibleCount = Math.ceil(
2357
- container.clientHeight / itemHeight
2358
- );
2359
- const endIndex = arrayKeys.length - 1;
2360
- const startIndex = Math.max(
2361
- 0,
2362
- endIndex - visibleCount - overscan
2363
- );
2286
+ runningOffset += measuredHeight;
2287
+ });
2364
2288
 
2365
- setRange({ startIndex, endIndex });
2289
+ measurementCache.current = offsets;
2290
+ return { totalHeight: runningOffset, itemOffsets: offsets };
2291
+ }, [arrayKeys.length, itemHeight]);
2366
2292
 
2367
- // Ensure scroll after range is set
2368
- requestAnimationFrame(() => {
2369
- scrollToBottom('instant');
2370
- initialScrollRef.current = false; // Mark initial scroll as done
2371
- });
2372
- } else {
2373
- // Container not ready, try again
2374
- requestAnimationFrame(waitForContainer);
2375
- }
2376
- };
2293
+ // Improved initial positioning effect
2294
+ useLayoutEffect(() => {
2295
+ if (
2296
+ stickToBottom &&
2297
+ arrayKeys.length > 0 &&
2298
+ containerRef.current &&
2299
+ !scrollStateRef.current.isUserScrolling &&
2300
+ initialScrollRef.current
2301
+ ) {
2302
+ const container = containerRef.current;
2377
2303
 
2378
- waitForContainer();
2379
- }
2380
- }, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
2304
+ // Wait for container to have dimensions
2305
+ const waitForContainer = () => {
2306
+ if (container.clientHeight > 0) {
2307
+ const visibleCount = Math.ceil(
2308
+ container.clientHeight / itemHeight
2309
+ );
2310
+ const endIndex = arrayKeys.length - 1;
2311
+ const startIndex = Math.max(
2312
+ 0,
2313
+ endIndex - visibleCount - overscan
2314
+ );
2381
2315
 
2382
- // Combined scroll handler
2383
- const handleScroll = useCallback(() => {
2384
- const container = containerRef.current;
2385
- if (!container) return;
2316
+ setRange({ startIndex, endIndex });
2386
2317
 
2387
- const currentScrollTop = container.scrollTop;
2388
- const { scrollHeight, clientHeight } = container;
2389
- const scrollState = scrollStateRef.current;
2390
-
2391
- // Check if user is near bottom
2392
- const distanceFromBottom =
2393
- scrollHeight - (currentScrollTop + clientHeight);
2394
- const wasNearBottom = scrollState.isNearBottom;
2395
- scrollState.isNearBottom =
2396
- distanceFromBottom <= scrollStickTolerance;
2397
-
2398
- // Detect scroll direction
2399
- if (currentScrollTop < scrollState.lastScrollTop) {
2400
- // User scrolled up
2401
- scrollState.scrollUpCount++;
2402
-
2403
- if (scrollState.scrollUpCount > 3 && wasNearBottom) {
2404
- // User has deliberately scrolled away from bottom
2405
- scrollState.isUserScrolling = true;
2406
- console.log('User scrolled away from bottom');
2318
+ // Ensure scroll after range is set
2319
+ requestAnimationFrame(() => {
2320
+ scrollToBottom('instant');
2321
+ initialScrollRef.current = false; // Mark initial scroll as done
2322
+ });
2323
+ } else {
2324
+ // Container not ready, try again
2325
+ requestAnimationFrame(waitForContainer);
2407
2326
  }
2408
- } else if (scrollState.isNearBottom) {
2409
- // Reset if we're back near the bottom
2410
- scrollState.isUserScrolling = false;
2411
- scrollState.scrollUpCount = 0;
2327
+ };
2328
+
2329
+ waitForContainer();
2330
+ }
2331
+ }, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
2332
+
2333
+ // Combined scroll handler
2334
+ const handleScroll = useCallback(() => {
2335
+ const container = containerRef.current;
2336
+ if (!container) return;
2337
+
2338
+ const currentScrollTop = container.scrollTop;
2339
+ const { scrollHeight, clientHeight } = container;
2340
+ const scrollState = scrollStateRef.current;
2341
+
2342
+ // Check if user is near bottom
2343
+ const distanceFromBottom =
2344
+ scrollHeight - (currentScrollTop + clientHeight);
2345
+ const wasNearBottom = scrollState.isNearBottom;
2346
+ scrollState.isNearBottom =
2347
+ distanceFromBottom <= scrollStickTolerance;
2348
+
2349
+ // Detect scroll direction
2350
+ if (currentScrollTop < scrollState.lastScrollTop) {
2351
+ // User scrolled up
2352
+ scrollState.scrollUpCount++;
2353
+
2354
+ if (scrollState.scrollUpCount > 3 && wasNearBottom) {
2355
+ // User has deliberately scrolled away from bottom
2356
+ scrollState.isUserScrolling = true;
2357
+ console.log('User scrolled away from bottom');
2412
2358
  }
2359
+ } else if (scrollState.isNearBottom) {
2360
+ // Reset if we're back near the bottom
2361
+ scrollState.isUserScrolling = false;
2362
+ scrollState.scrollUpCount = 0;
2363
+ }
2413
2364
 
2414
- scrollState.lastScrollTop = currentScrollTop;
2365
+ scrollState.lastScrollTop = currentScrollTop;
2415
2366
 
2416
- // Update visible range
2417
- let newStartIndex = 0;
2418
- for (let i = 0; i < arrayKeys.length; i++) {
2419
- const itemKey = arrayKeys[i];
2420
- const item = measurementCache.current.get(itemKey!);
2421
- if (item && item.offset + item.height > currentScrollTop) {
2422
- newStartIndex = i;
2423
- break;
2424
- }
2367
+ // Update visible range
2368
+ let newStartIndex = 0;
2369
+ for (let i = 0; i < arrayKeys.length; i++) {
2370
+ const itemKey = arrayKeys[i];
2371
+ const item = measurementCache.current.get(itemKey!);
2372
+ if (item && item.offset + item.height > currentScrollTop) {
2373
+ newStartIndex = i;
2374
+ break;
2425
2375
  }
2376
+ }
2426
2377
 
2427
- // Only update if range actually changed
2428
- if (newStartIndex !== range.startIndex) {
2429
- const visibleCount = Math.ceil(clientHeight / itemHeight);
2430
- setRange({
2431
- startIndex: Math.max(0, newStartIndex - overscan),
2432
- endIndex: Math.min(
2433
- arrayKeys.length - 1,
2434
- newStartIndex + visibleCount + overscan
2435
- ),
2436
- });
2437
- }
2438
- }, [
2439
- arrayKeys.length,
2440
- range.startIndex,
2441
- itemHeight,
2442
- overscan,
2443
- scrollStickTolerance,
2444
- ]);
2378
+ // Only update if range actually changed
2379
+ if (newStartIndex !== range.startIndex) {
2380
+ const visibleCount = Math.ceil(clientHeight / itemHeight);
2381
+ setRange({
2382
+ startIndex: Math.max(0, newStartIndex - overscan),
2383
+ endIndex: Math.min(
2384
+ arrayKeys.length - 1,
2385
+ newStartIndex + visibleCount + overscan
2386
+ ),
2387
+ });
2388
+ }
2389
+ }, [
2390
+ arrayKeys.length,
2391
+ range.startIndex,
2392
+ itemHeight,
2393
+ overscan,
2394
+ scrollStickTolerance,
2395
+ ]);
2396
+
2397
+ // Set up scroll listener
2398
+ useEffect(() => {
2399
+ const container = containerRef.current;
2400
+ if (!container || !stickToBottom) return;
2401
+
2402
+ container.addEventListener('scroll', handleScroll, {
2403
+ passive: true,
2404
+ });
2445
2405
 
2446
- // Set up scroll listener
2447
- useEffect(() => {
2406
+ return () => {
2407
+ container.removeEventListener('scroll', handleScroll);
2408
+ };
2409
+ }, [handleScroll, stickToBottom]);
2410
+ const scrollToBottom = useCallback(
2411
+ (behavior: ScrollBehavior = 'smooth') => {
2448
2412
  const container = containerRef.current;
2449
- if (!container || !stickToBottom) return;
2413
+ if (!container) return;
2450
2414
 
2451
- container.addEventListener('scroll', handleScroll, {
2452
- passive: true,
2453
- });
2415
+ // Reset scroll state
2416
+ scrollStateRef.current.isUserScrolling = false;
2417
+ scrollStateRef.current.isNearBottom = true;
2418
+ scrollStateRef.current.scrollUpCount = 0;
2454
2419
 
2455
- return () => {
2456
- container.removeEventListener('scroll', handleScroll);
2457
- };
2458
- }, [handleScroll, stickToBottom]);
2459
- const scrollToBottom = useCallback(
2460
- (behavior: ScrollBehavior = 'smooth') => {
2461
- const container = containerRef.current;
2462
- if (!container) return;
2463
-
2464
- // Reset scroll state
2465
- scrollStateRef.current.isUserScrolling = false;
2466
- scrollStateRef.current.isNearBottom = true;
2467
- scrollStateRef.current.scrollUpCount = 0;
2468
-
2469
- const performScroll = () => {
2470
- // Multiple attempts to ensure we hit the bottom
2471
- const attemptScroll = (attempts = 0) => {
2472
- if (attempts > 5) return; // Prevent infinite loops
2473
-
2474
- const currentHeight = container.scrollHeight;
2475
- const currentScroll = container.scrollTop;
2476
- const clientHeight = container.clientHeight;
2477
-
2478
- // Check if we're already at the bottom
2479
- if (currentScroll + clientHeight >= currentHeight - 1) {
2480
- return;
2481
- }
2420
+ const performScroll = () => {
2421
+ // Multiple attempts to ensure we hit the bottom
2422
+ const attemptScroll = (attempts = 0) => {
2423
+ if (attempts > 5) return; // Prevent infinite loops
2482
2424
 
2483
- container.scrollTo({
2484
- top: currentHeight,
2485
- behavior: behavior,
2486
- });
2487
-
2488
- // In slow environments, check again after a short delay
2489
- setTimeout(() => {
2490
- const newHeight = container.scrollHeight;
2491
- const newScroll = container.scrollTop;
2492
-
2493
- // If height changed or we're not at bottom, try again
2494
- if (
2495
- newHeight !== currentHeight ||
2496
- newScroll + clientHeight < newHeight - 1
2497
- ) {
2498
- attemptScroll(attempts + 1);
2499
- }
2500
- }, 50);
2501
- };
2425
+ const currentHeight = container.scrollHeight;
2426
+ const currentScroll = container.scrollTop;
2427
+ const clientHeight = container.clientHeight;
2502
2428
 
2503
- attemptScroll();
2504
- };
2429
+ // Check if we're already at the bottom
2430
+ if (currentScroll + clientHeight >= currentHeight - 1) {
2431
+ return;
2432
+ }
2505
2433
 
2506
- // Use requestIdleCallback for better performance in slow environments
2507
- if ('requestIdleCallback' in window) {
2508
- requestIdleCallback(performScroll, { timeout: 100 });
2509
- } else {
2510
- // Fallback to rAF chain
2511
- requestAnimationFrame(() => {
2512
- requestAnimationFrame(performScroll);
2434
+ container.scrollTo({
2435
+ top: currentHeight,
2436
+ behavior: behavior,
2513
2437
  });
2514
- }
2515
- },
2516
- []
2517
- );
2518
- // Auto-scroll to bottom when new content arrives
2519
- // Consolidated auto-scroll effect with debouncing
2520
- useEffect(() => {
2521
- if (!stickToBottom || !containerRef.current) return;
2522
-
2523
- const container = containerRef.current;
2524
- const scrollState = scrollStateRef.current;
2525
2438
 
2526
- // Debounced scroll function
2527
- let scrollTimeout: NodeJS.Timeout;
2528
- const debouncedScrollToBottom = () => {
2529
- clearTimeout(scrollTimeout);
2530
- scrollTimeout = setTimeout(() => {
2531
- if (
2532
- !scrollState.isUserScrolling &&
2533
- scrollState.isNearBottom
2534
- ) {
2535
- scrollToBottom(
2536
- initialScrollRef.current ? 'instant' : 'smooth'
2537
- );
2538
- }
2539
- }, 100);
2540
- };
2439
+ // In slow environments, check again after a short delay
2440
+ setTimeout(() => {
2441
+ const newHeight = container.scrollHeight;
2442
+ const newScroll = container.scrollTop;
2541
2443
 
2542
- // Single MutationObserver for all DOM changes
2543
- const observer = new MutationObserver(() => {
2544
- if (!scrollState.isUserScrolling) {
2545
- debouncedScrollToBottom();
2546
- }
2547
- });
2444
+ // If height changed or we're not at bottom, try again
2445
+ if (
2446
+ newHeight !== currentHeight ||
2447
+ newScroll + clientHeight < newHeight - 1
2448
+ ) {
2449
+ attemptScroll(attempts + 1);
2450
+ }
2451
+ }, 50);
2452
+ };
2548
2453
 
2549
- observer.observe(container, {
2550
- childList: true,
2551
- subtree: true,
2552
- attributes: true,
2553
- attributeFilter: ['style', 'class'], // More specific than just 'height'
2554
- });
2454
+ attemptScroll();
2455
+ };
2555
2456
 
2556
- // Handle image loads with event delegation
2557
- const handleImageLoad = (e: Event) => {
2457
+ // Use requestIdleCallback for better performance in slow environments
2458
+ if ('requestIdleCallback' in window) {
2459
+ requestIdleCallback(performScroll, { timeout: 100 });
2460
+ } else {
2461
+ // Fallback to rAF chain
2462
+ requestAnimationFrame(() => {
2463
+ requestAnimationFrame(performScroll);
2464
+ });
2465
+ }
2466
+ },
2467
+ []
2468
+ );
2469
+ // Auto-scroll to bottom when new content arrives
2470
+ // Consolidated auto-scroll effect with debouncing
2471
+ useEffect(() => {
2472
+ if (!stickToBottom || !containerRef.current) return;
2473
+
2474
+ const container = containerRef.current;
2475
+ const scrollState = scrollStateRef.current;
2476
+
2477
+ // Debounced scroll function
2478
+ let scrollTimeout: NodeJS.Timeout;
2479
+ const debouncedScrollToBottom = () => {
2480
+ clearTimeout(scrollTimeout);
2481
+ scrollTimeout = setTimeout(() => {
2558
2482
  if (
2559
- e.target instanceof HTMLImageElement &&
2560
- !scrollState.isUserScrolling
2483
+ !scrollState.isUserScrolling &&
2484
+ scrollState.isNearBottom
2561
2485
  ) {
2562
- debouncedScrollToBottom();
2486
+ scrollToBottom(
2487
+ initialScrollRef.current ? 'instant' : 'smooth'
2488
+ );
2563
2489
  }
2564
- };
2490
+ }, 100);
2491
+ };
2492
+
2493
+ // Single MutationObserver for all DOM changes
2494
+ const observer = new MutationObserver(() => {
2495
+ if (!scrollState.isUserScrolling) {
2496
+ debouncedScrollToBottom();
2497
+ }
2498
+ });
2565
2499
 
2566
- container.addEventListener('load', handleImageLoad, true);
2500
+ observer.observe(container, {
2501
+ childList: true,
2502
+ subtree: true,
2503
+ attributes: true,
2504
+ attributeFilter: ['style', 'class'], // More specific than just 'height'
2505
+ });
2567
2506
 
2568
- // Initial scroll with proper timing
2569
- if (initialScrollRef.current) {
2570
- // For initial load, wait for next tick to ensure DOM is ready
2571
- setTimeout(() => {
2572
- scrollToBottom('instant');
2573
- }, 0);
2574
- } else {
2507
+ // Handle image loads with event delegation
2508
+ const handleImageLoad = (e: Event) => {
2509
+ if (
2510
+ e.target instanceof HTMLImageElement &&
2511
+ !scrollState.isUserScrolling
2512
+ ) {
2575
2513
  debouncedScrollToBottom();
2576
2514
  }
2515
+ };
2577
2516
 
2578
- return () => {
2579
- clearTimeout(scrollTimeout);
2580
- observer.disconnect();
2581
- container.removeEventListener('load', handleImageLoad, true);
2582
- };
2583
- }, [stickToBottom, arrayKeys.length, scrollToBottom]);
2584
- // Create virtual state
2585
- const virtualState = useMemo(() => {
2586
- const store = getGlobalStore.getState();
2587
- const sourceArray = store.getShadowValue(
2588
- [stateKey, ...path].join('.')
2589
- ) as any[];
2590
- const currentKeys =
2591
- store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
2592
-
2593
- const slicedArray = sourceArray.slice(
2594
- range.startIndex,
2595
- range.endIndex + 1
2596
- );
2597
- const slicedIds = currentKeys.slice(
2598
- range.startIndex,
2599
- range.endIndex + 1
2600
- );
2517
+ container.addEventListener('load', handleImageLoad, true);
2601
2518
 
2602
- return rebuildStateShape({
2603
- currentState: slicedArray as any,
2604
- path,
2605
- componentId: componentId!,
2606
- meta: { ...meta, validIds: slicedIds },
2607
- });
2608
- }, [range.startIndex, range.endIndex, arrayKeys.length]);
2609
-
2610
- return {
2611
- virtualState,
2612
- virtualizerProps: {
2613
- outer: {
2614
- ref: containerRef,
2615
- style: {
2616
- overflowY: 'auto',
2617
- height: '100%',
2618
- position: 'relative',
2619
- },
2620
- },
2621
- inner: {
2622
- style: {
2623
- height: `${totalHeight}px`,
2624
- position: 'relative',
2625
- },
2519
+ // Initial scroll with proper timing
2520
+ if (initialScrollRef.current) {
2521
+ // For initial load, wait for next tick to ensure DOM is ready
2522
+ setTimeout(() => {
2523
+ scrollToBottom('instant');
2524
+ }, 0);
2525
+ } else {
2526
+ debouncedScrollToBottom();
2527
+ }
2528
+
2529
+ return () => {
2530
+ clearTimeout(scrollTimeout);
2531
+ observer.disconnect();
2532
+ container.removeEventListener('load', handleImageLoad, true);
2533
+ };
2534
+ }, [stickToBottom, arrayKeys.length, scrollToBottom]);
2535
+ // Create virtual state
2536
+ const virtualState = useMemo(() => {
2537
+ const store = getGlobalStore.getState();
2538
+ const sourceArray = store.getShadowValue(
2539
+ [stateKey, ...path].join('.')
2540
+ ) as any[];
2541
+ const currentKeys =
2542
+ store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
2543
+
2544
+ const slicedArray = sourceArray.slice(
2545
+ range.startIndex,
2546
+ range.endIndex + 1
2547
+ );
2548
+ const slicedIds = currentKeys.slice(
2549
+ range.startIndex,
2550
+ range.endIndex + 1
2551
+ );
2552
+
2553
+ return rebuildStateShape({
2554
+ path,
2555
+ componentId: componentId!,
2556
+ meta: { ...meta, validIds: slicedIds },
2557
+ });
2558
+ }, [range.startIndex, range.endIndex, arrayKeys.length]);
2559
+
2560
+ return {
2561
+ virtualState,
2562
+ virtualizerProps: {
2563
+ outer: {
2564
+ ref: containerRef,
2565
+ style: {
2566
+ overflowY: 'auto',
2567
+ height: '100%',
2568
+ position: 'relative',
2626
2569
  },
2627
- list: {
2628
- style: {
2629
- transform: `translateY(${
2630
- measurementCache.current.get(
2631
- arrayKeys[range.startIndex]!
2632
- )?.offset || 0
2633
- }px)`,
2634
- },
2570
+ },
2571
+ inner: {
2572
+ style: {
2573
+ height: `${totalHeight}px`,
2574
+ position: 'relative',
2635
2575
  },
2636
2576
  },
2637
- scrollToBottom,
2638
- scrollToIndex: (
2639
- index: number,
2640
- behavior: ScrollBehavior = 'smooth'
2641
- ) => {
2642
- if (containerRef.current && arrayKeys[index]) {
2643
- const offset =
2644
- measurementCache.current.get(arrayKeys[index]!)?.offset ||
2645
- 0;
2646
- containerRef.current.scrollTo({ top: offset, behavior });
2647
- }
2577
+ list: {
2578
+ style: {
2579
+ transform: `translateY(${
2580
+ measurementCache.current.get(arrayKeys[range.startIndex]!)
2581
+ ?.offset || 0
2582
+ }px)`,
2583
+ },
2648
2584
  },
2649
- };
2650
- };
2651
- }
2652
- if (prop === 'stateMap') {
2653
- return (
2654
- callbackfn: (
2655
- setter: any,
2585
+ },
2586
+ scrollToBottom,
2587
+ scrollToIndex: (
2656
2588
  index: number,
2589
+ behavior: ScrollBehavior = 'smooth'
2590
+ ) => {
2591
+ if (containerRef.current && arrayKeys[index]) {
2592
+ const offset =
2593
+ measurementCache.current.get(arrayKeys[index]!)?.offset ||
2594
+ 0;
2595
+ containerRef.current.scrollTo({ top: offset, behavior });
2596
+ }
2597
+ },
2598
+ };
2599
+ };
2600
+ }
2601
+ if (prop === 'stateMap') {
2602
+ return (
2603
+ callbackfn: (
2604
+ setter: any,
2605
+ index: number,
2606
+
2607
+ arraySetter: any
2608
+ ) => void
2609
+ ) => {
2610
+ const [arrayKeys, setArrayKeys] = useState<any>(
2611
+ meta?.validIds ??
2612
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2613
+ ?.arrayKeys
2614
+ );
2615
+ // getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
2616
+ // console.log(
2617
+ // "stateKeyPathKeyccccccccccccccccc",
2618
+ // stateKeyPathKey
2619
+ // );
2620
+ // setArrayKeys(
2621
+ // getGlobalStore.getState().getShadowMetadata(stateKey, path)
2622
+ // );
2623
+ // });
2624
+
2625
+ const shadowValue = getGlobalStore
2626
+ .getState()
2627
+ .getShadowValue(stateKeyPathKey, meta?.validIds) as any[];
2628
+ if (!arrayKeys) {
2629
+ throw new Error('No array keys found for mapping');
2630
+ }
2631
+ const arraySetter = rebuildStateShape({
2632
+ path,
2633
+ componentId: componentId!,
2634
+ meta,
2635
+ });
2657
2636
 
2658
- arraySetter: any
2659
- ) => void
2660
- ) => {
2661
- const [arrayKeys, setArrayKeys] = useState<any>(
2662
- meta?.validIds ??
2663
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2664
- ?.arrayKeys
2665
- );
2666
- // getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
2667
- // console.log(
2668
- // "stateKeyPathKeyccccccccccccccccc",
2669
- // stateKeyPathKey
2670
- // );
2671
- // setArrayKeys(
2672
- // getGlobalStore.getState().getShadowMetadata(stateKey, path)
2673
- // );
2674
- // });
2675
-
2676
- const shadowValue = getGlobalStore
2677
- .getState()
2678
- .getShadowValue(stateKeyPathKey, meta?.validIds) as any[];
2679
- if (!arrayKeys) {
2680
- throw new Error('No array keys found for mapping');
2681
- }
2682
- const arraySetter = rebuildStateShape({
2683
- currentState: shadowValue as any,
2684
- path,
2637
+ return shadowValue.map((item, index) => {
2638
+ const itemPath = arrayKeys[index]?.split('.').slice(1);
2639
+ const itemSetter = rebuildStateShape({
2640
+ path: itemPath as any,
2685
2641
  componentId: componentId!,
2686
2642
  meta,
2687
2643
  });
2688
2644
 
2689
- return shadowValue.map((item, index) => {
2690
- const itemPath = arrayKeys[index]?.split('.').slice(1);
2691
- const itemSetter = rebuildStateShape({
2692
- currentState: item,
2693
- path: itemPath as any,
2694
- componentId: componentId!,
2695
- meta,
2696
- });
2697
-
2698
- return callbackfn(
2699
- itemSetter,
2700
- index,
2645
+ return callbackfn(
2646
+ itemSetter,
2647
+ index,
2701
2648
 
2702
- arraySetter
2703
- );
2704
- });
2705
- };
2706
- }
2649
+ arraySetter
2650
+ );
2651
+ });
2652
+ };
2653
+ }
2707
2654
 
2708
- if (prop === '$stateMap') {
2709
- return (callbackfn: any) =>
2710
- createElement(SignalMapRenderer, {
2711
- proxy: {
2712
- _stateKey: stateKey,
2713
- _path: path,
2714
- _mapFn: callbackfn,
2715
- _meta: meta,
2716
- },
2717
- rebuildStateShape,
2718
- });
2719
- } // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
2655
+ if (prop === '$stateMap') {
2656
+ return (callbackfn: any) =>
2657
+ createElement(SignalMapRenderer, {
2658
+ proxy: {
2659
+ _stateKey: stateKey,
2660
+ _path: path,
2661
+ _mapFn: callbackfn,
2662
+ _meta: meta,
2663
+ },
2664
+ rebuildStateShape,
2665
+ });
2666
+ } // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
2667
+
2668
+ if (prop === 'stateFind') {
2669
+ return (
2670
+ callbackfn: (value: any, index: number) => boolean
2671
+ ): StateObject<any> | undefined => {
2672
+ // 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
2673
+ const arrayKeys =
2674
+ meta?.validIds ??
2675
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2676
+ ?.arrayKeys;
2677
+
2678
+ if (!arrayKeys) {
2679
+ return undefined;
2680
+ }
2720
2681
 
2721
- if (prop === 'stateFind') {
2722
- return (
2723
- callbackfn: (value: any, index: number) => boolean
2724
- ): StateObject<any> | undefined => {
2725
- // 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
2726
- const arrayKeys =
2727
- meta?.validIds ??
2728
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2729
- ?.arrayKeys;
2682
+ // 2. Iterate through the keys, get the value for each, and run the callback.
2683
+ for (let i = 0; i < arrayKeys.length; i++) {
2684
+ const itemKey = arrayKeys[i];
2685
+ if (!itemKey) continue; // Safety check
2730
2686
 
2731
- if (!arrayKeys) {
2732
- return undefined;
2733
- }
2687
+ const itemValue = getGlobalStore
2688
+ .getState()
2689
+ .getShadowValue(itemKey);
2734
2690
 
2735
- // 2. Iterate through the keys, get the value for each, and run the callback.
2736
- for (let i = 0; i < arrayKeys.length; i++) {
2737
- const itemKey = arrayKeys[i];
2738
- if (!itemKey) continue; // Safety check
2691
+ // 3. If the callback returns true, we've found our item.
2692
+ if (callbackfn(itemValue, i)) {
2693
+ // Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
2694
+ const itemPath = itemKey.split('.').slice(1);
2739
2695
 
2740
- const itemValue = getGlobalStore
2741
- .getState()
2742
- .getShadowValue(itemKey);
2743
-
2744
- // 3. If the callback returns true, we've found our item.
2745
- if (callbackfn(itemValue, i)) {
2746
- // Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
2747
- const itemPath = itemKey.split('.').slice(1);
2748
-
2749
- // 4. Rebuild a new, fully functional StateObject for just that item and return it.
2750
- return rebuildStateShape({
2751
- currentState: itemValue,
2752
- path: itemPath,
2753
- componentId: componentId,
2754
- meta, // Pass along meta for potential further chaining
2755
- });
2756
- }
2696
+ // 4. Rebuild a new, fully functional StateObject for just that item and return it.
2697
+ return rebuildStateShape({
2698
+ path: itemPath,
2699
+ componentId: componentId,
2700
+ meta, // Pass along meta for potential further chaining
2701
+ });
2757
2702
  }
2703
+ }
2758
2704
 
2759
- // 5. If the loop finishes without finding anything, return undefined.
2760
- return undefined;
2761
- };
2762
- }
2763
- if (prop === 'stateFilter') {
2764
- return (callbackfn: (value: any, index: number) => boolean) => {
2765
- const arrayKeys =
2766
- meta?.validIds ??
2767
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2768
- ?.arrayKeys;
2769
-
2770
- if (!arrayKeys) {
2771
- throw new Error('No array keys found for filtering.');
2772
- }
2705
+ // 5. If the loop finishes without finding anything, return undefined.
2706
+ return undefined;
2707
+ };
2708
+ }
2709
+ if (prop === 'stateFilter') {
2710
+ return (callbackfn: (value: any, index: number) => boolean) => {
2711
+ const currentState = getGlobalStore
2712
+ .getState()
2713
+ .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
2714
+ if (!Array.isArray(currentState)) return [];
2715
+ const arrayKeys =
2716
+ meta?.validIds ??
2717
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2718
+ ?.arrayKeys;
2719
+
2720
+ if (!arrayKeys) {
2721
+ throw new Error('No array keys found for filtering.');
2722
+ }
2773
2723
 
2774
- const newValidIds: string[] = [];
2775
- const filteredArray = currentState.filter(
2776
- (val: any, index: number) => {
2777
- const didPass = callbackfn(val, index);
2778
- if (didPass) {
2779
- newValidIds.push(arrayKeys[index]!);
2780
- return true;
2781
- }
2782
- return false;
2724
+ const newValidIds: string[] = [];
2725
+ const filteredArray = currentState.filter(
2726
+ (val: any, index: number) => {
2727
+ const didPass = callbackfn(val, index);
2728
+ if (didPass) {
2729
+ newValidIds.push(arrayKeys[index]!);
2730
+ return true;
2783
2731
  }
2784
- );
2732
+ return false;
2733
+ }
2734
+ );
2785
2735
 
2786
- return rebuildStateShape({
2787
- currentState: filteredArray as any,
2788
- path,
2789
- componentId: componentId!,
2790
- meta: {
2791
- validIds: newValidIds,
2792
- transforms: [
2793
- ...(meta?.transforms || []),
2794
- {
2795
- type: 'filter',
2796
- fn: callbackfn,
2797
- },
2798
- ],
2799
- },
2800
- });
2801
- };
2802
- }
2803
- if (prop === 'stateSort') {
2804
- return (compareFn: (a: any, b: any) => number) => {
2805
- const arrayKeys =
2806
- meta?.validIds ??
2807
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2808
- ?.arrayKeys;
2809
- if (!arrayKeys) {
2810
- throw new Error('No array keys found for sorting');
2736
+ return rebuildStateShape({
2737
+ path,
2738
+ componentId: componentId!,
2739
+ meta: {
2740
+ validIds: newValidIds,
2741
+ transforms: [
2742
+ ...(meta?.transforms || []),
2743
+ {
2744
+ type: 'filter',
2745
+ fn: callbackfn,
2746
+ },
2747
+ ],
2748
+ },
2749
+ });
2750
+ };
2751
+ }
2752
+ if (prop === 'stateSort') {
2753
+ return (compareFn: (a: any, b: any) => number) => {
2754
+ const currentState = getGlobalStore
2755
+ .getState()
2756
+ .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
2757
+ if (!Array.isArray(currentState)) return []; // Guard clause
2758
+ const arrayKeys =
2759
+ meta?.validIds ??
2760
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2761
+ ?.arrayKeys;
2762
+ if (!arrayKeys) {
2763
+ throw new Error('No array keys found for sorting');
2764
+ }
2765
+ const itemsWithIds = currentState.map((item, index) => ({
2766
+ item,
2767
+ key: arrayKeys[index],
2768
+ }));
2769
+
2770
+ itemsWithIds
2771
+ .sort((a, b) => compareFn(a.item, b.item))
2772
+ .filter(Boolean);
2773
+
2774
+ return rebuildStateShape({
2775
+ path,
2776
+ componentId: componentId!,
2777
+ meta: {
2778
+ validIds: itemsWithIds.map((i) => i.key) as string[],
2779
+ transforms: [
2780
+ ...(meta?.transforms || []),
2781
+ { type: 'sort', fn: compareFn },
2782
+ ],
2783
+ },
2784
+ });
2785
+ };
2786
+ }
2787
+ // In createProxyHandler, inside the get trap where you have other array methods:
2788
+ if (prop === 'stream') {
2789
+ return function <U = InferArrayElement<T>, R = U>(
2790
+ options: StreamOptions<U, R> = {}
2791
+ ): StreamHandle<U> {
2792
+ const {
2793
+ bufferSize = 100,
2794
+ flushInterval = 100,
2795
+ bufferStrategy = 'accumulate',
2796
+ store,
2797
+ onFlush,
2798
+ } = options;
2799
+
2800
+ let buffer: U[] = [];
2801
+ let isPaused = false;
2802
+ let flushTimer: NodeJS.Timeout | null = null;
2803
+
2804
+ const addToBuffer = (item: U) => {
2805
+ if (isPaused) return;
2806
+
2807
+ if (bufferStrategy === 'sliding' && buffer.length >= bufferSize) {
2808
+ buffer.shift();
2809
+ } else if (
2810
+ bufferStrategy === 'dropping' &&
2811
+ buffer.length >= bufferSize
2812
+ ) {
2813
+ return;
2811
2814
  }
2812
- const itemsWithIds = currentState.map((item, index) => ({
2813
- item,
2814
- key: arrayKeys[index],
2815
- }));
2816
2815
 
2817
- itemsWithIds
2818
- .sort((a, b) => compareFn(a.item, b.item))
2819
- .filter(Boolean);
2816
+ buffer.push(item);
2820
2817
 
2821
- return rebuildStateShape({
2822
- currentState: itemsWithIds.map((i) => i.item) as any,
2823
- path,
2824
- componentId: componentId!,
2825
- meta: {
2826
- validIds: itemsWithIds.map((i) => i.key) as string[],
2827
- transforms: [
2828
- ...(meta?.transforms || []),
2829
- { type: 'sort', fn: compareFn },
2830
- ],
2831
- },
2832
- });
2818
+ if (buffer.length >= bufferSize) {
2819
+ flushBuffer();
2820
+ }
2833
2821
  };
2834
- }
2835
- // In createProxyHandler, inside the get trap where you have other array methods:
2836
- if (prop === 'stream') {
2837
- return function <U = InferArrayElement<T>, R = U>(
2838
- options: StreamOptions<U, R> = {}
2839
- ): StreamHandle<U> {
2840
- const {
2841
- bufferSize = 100,
2842
- flushInterval = 100,
2843
- bufferStrategy = 'accumulate',
2844
- store,
2845
- onFlush,
2846
- } = options;
2847
-
2848
- let buffer: U[] = [];
2849
- let isPaused = false;
2850
- let flushTimer: NodeJS.Timeout | null = null;
2851
-
2852
- const addToBuffer = (item: U) => {
2853
- if (isPaused) return;
2854
2822
 
2855
- if (
2856
- bufferStrategy === 'sliding' &&
2857
- buffer.length >= bufferSize
2858
- ) {
2859
- buffer.shift();
2860
- } else if (
2861
- bufferStrategy === 'dropping' &&
2862
- buffer.length >= bufferSize
2863
- ) {
2864
- return;
2865
- }
2866
-
2867
- buffer.push(item);
2823
+ const flushBuffer = () => {
2824
+ if (buffer.length === 0) return;
2868
2825
 
2869
- if (buffer.length >= bufferSize) {
2870
- flushBuffer();
2871
- }
2872
- };
2826
+ const toFlush = [...buffer];
2827
+ buffer = [];
2873
2828
 
2874
- const flushBuffer = () => {
2875
- if (buffer.length === 0) return;
2876
-
2877
- const toFlush = [...buffer];
2878
- buffer = [];
2879
-
2880
- if (store) {
2881
- const result = store(toFlush);
2882
- if (result !== undefined) {
2883
- const items = Array.isArray(result) ? result : [result];
2884
- items.forEach((item) => {
2885
- effectiveSetState(item as any, path, {
2886
- updateType: 'insert',
2887
- });
2888
- });
2889
- }
2890
- } else {
2891
- toFlush.forEach((item) => {
2829
+ if (store) {
2830
+ const result = store(toFlush);
2831
+ if (result !== undefined) {
2832
+ const items = Array.isArray(result) ? result : [result];
2833
+ items.forEach((item) => {
2892
2834
  effectiveSetState(item as any, path, {
2893
2835
  updateType: 'insert',
2894
2836
  });
2895
2837
  });
2896
2838
  }
2839
+ } else {
2840
+ toFlush.forEach((item) => {
2841
+ effectiveSetState(item as any, path, {
2842
+ updateType: 'insert',
2843
+ });
2844
+ });
2845
+ }
2897
2846
 
2898
- onFlush?.(toFlush);
2899
- };
2847
+ onFlush?.(toFlush);
2848
+ };
2900
2849
 
2901
- if (flushInterval > 0) {
2902
- flushTimer = setInterval(flushBuffer, flushInterval);
2903
- }
2850
+ if (flushInterval > 0) {
2851
+ flushTimer = setInterval(flushBuffer, flushInterval);
2852
+ }
2904
2853
 
2905
- const streamId = uuidv4();
2906
- const currentMeta =
2907
- getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
2908
- {};
2909
- const streams = currentMeta.streams || new Map();
2910
- streams.set(streamId, { buffer, flushTimer });
2854
+ const streamId = uuidv4();
2855
+ const currentMeta =
2856
+ getGlobalStore.getState().getShadowMetadata(stateKey, path) || {};
2857
+ const streams = currentMeta.streams || new Map();
2858
+ streams.set(streamId, { buffer, flushTimer });
2911
2859
 
2912
- getGlobalStore.getState().setShadowMetadata(stateKey, path, {
2913
- ...currentMeta,
2914
- streams,
2915
- });
2860
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
2861
+ ...currentMeta,
2862
+ streams,
2863
+ });
2916
2864
 
2917
- return {
2918
- write: (data: U) => addToBuffer(data),
2919
- writeMany: (data: U[]) => data.forEach(addToBuffer),
2920
- flush: () => flushBuffer(),
2921
- pause: () => {
2922
- isPaused = true;
2923
- },
2924
- resume: () => {
2925
- isPaused = false;
2926
- if (buffer.length > 0) flushBuffer();
2927
- },
2928
- close: () => {
2929
- flushBuffer();
2930
- if (flushTimer) clearInterval(flushTimer);
2865
+ return {
2866
+ write: (data: U) => addToBuffer(data),
2867
+ writeMany: (data: U[]) => data.forEach(addToBuffer),
2868
+ flush: () => flushBuffer(),
2869
+ pause: () => {
2870
+ isPaused = true;
2871
+ },
2872
+ resume: () => {
2873
+ isPaused = false;
2874
+ if (buffer.length > 0) flushBuffer();
2875
+ },
2876
+ close: () => {
2877
+ flushBuffer();
2878
+ if (flushTimer) clearInterval(flushTimer);
2931
2879
 
2932
- const meta = getGlobalStore
2933
- .getState()
2934
- .getShadowMetadata(stateKey, path);
2935
- if (meta?.streams) {
2936
- meta.streams.delete(streamId);
2937
- }
2938
- },
2939
- };
2880
+ const meta = getGlobalStore
2881
+ .getState()
2882
+ .getShadowMetadata(stateKey, path);
2883
+ if (meta?.streams) {
2884
+ meta.streams.delete(streamId);
2885
+ }
2886
+ },
2940
2887
  };
2941
- }
2888
+ };
2889
+ }
2942
2890
 
2943
- if (prop === 'stateList') {
2944
- return (
2945
- callbackfn: (
2946
- setter: any,
2947
- index: number,
2948
- arraySetter: any
2949
- ) => ReactNode
2950
- ) => {
2951
- const StateListWrapper = () => {
2952
- const componentIdsRef = useRef<Map<string, string>>(new Map());
2891
+ if (prop === 'stateList') {
2892
+ return (
2893
+ callbackfn: (
2894
+ setter: any,
2895
+ index: number,
2896
+ arraySetter: any
2897
+ ) => ReactNode
2898
+ ) => {
2899
+ const StateListWrapper = () => {
2900
+ const componentIdsRef = useRef<Map<string, string>>(new Map());
2901
+
2902
+ const cacheKey =
2903
+ meta?.transforms && meta.transforms.length > 0
2904
+ ? `${componentId}-${hashTransforms(meta.transforms)}`
2905
+ : `${componentId}-base`;
2906
+
2907
+ const [updateTrigger, forceUpdate] = useState({});
2908
+
2909
+ const { validIds, arrayValues } = useMemo(() => {
2910
+ const cached = getGlobalStore
2911
+ .getState()
2912
+ .getShadowMetadata(stateKey, path)
2913
+ ?.transformCaches?.get(cacheKey);
2953
2914
 
2954
- const cacheKey =
2955
- meta?.transforms && meta.transforms.length > 0
2956
- ? `${componentId}-${hashTransforms(meta.transforms)}`
2957
- : `${componentId}-base`;
2915
+ let freshValidIds: string[];
2958
2916
 
2959
- const [updateTrigger, forceUpdate] = useState({});
2917
+ if (cached && cached.validIds) {
2918
+ freshValidIds = cached.validIds;
2919
+ } else {
2920
+ freshValidIds = applyTransforms(
2921
+ stateKey,
2922
+ path,
2923
+ meta?.transforms
2924
+ );
2960
2925
 
2961
- const { validIds, arrayValues } = useMemo(() => {
2962
- const cached = getGlobalStore
2926
+ getGlobalStore
2963
2927
  .getState()
2964
- .getShadowMetadata(stateKey, path)
2965
- ?.transformCaches?.get(cacheKey);
2966
-
2967
- let freshValidIds: string[];
2968
-
2969
- if (cached && cached.validIds) {
2970
- freshValidIds = cached.validIds;
2971
- } else {
2972
- freshValidIds = applyTransforms(
2973
- stateKey,
2974
- path,
2975
- meta?.transforms
2976
- );
2977
-
2978
- getGlobalStore
2979
- .getState()
2980
- .setTransformCache(stateKey, path, cacheKey, {
2981
- validIds: freshValidIds,
2982
- computedAt: Date.now(),
2983
- transforms: meta?.transforms || [],
2984
- });
2985
- }
2928
+ .setTransformCache(stateKey, path, cacheKey, {
2929
+ validIds: freshValidIds,
2930
+ computedAt: Date.now(),
2931
+ transforms: meta?.transforms || [],
2932
+ });
2933
+ }
2986
2934
 
2987
- const freshValues = getGlobalStore
2988
- .getState()
2989
- .getShadowValue(stateKeyPathKey, freshValidIds);
2935
+ const freshValues = getGlobalStore
2936
+ .getState()
2937
+ .getShadowValue(stateKeyPathKey, freshValidIds);
2990
2938
 
2991
- return {
2992
- validIds: freshValidIds,
2993
- arrayValues: freshValues || [],
2994
- };
2995
- }, [cacheKey, updateTrigger]);
2939
+ return {
2940
+ validIds: freshValidIds,
2941
+ arrayValues: freshValues || [],
2942
+ };
2943
+ }, [cacheKey, updateTrigger]);
2996
2944
 
2997
- useEffect(() => {
2998
- const unsubscribe = getGlobalStore
2999
- .getState()
3000
- .subscribeToPath(stateKeyPathKey, (e) => {
3001
- // A data change has occurred for the source array.
2945
+ useEffect(() => {
2946
+ const unsubscribe = getGlobalStore
2947
+ .getState()
2948
+ .subscribeToPath(stateKeyPathKey, (e) => {
2949
+ // A data change has occurred for the source array.
3002
2950
 
3003
- if (e.type === 'GET_SELECTED') {
3004
- return;
3005
- }
3006
- const shadowMeta = getGlobalStore
3007
- .getState()
3008
- .getShadowMetadata(stateKey, path);
3009
-
3010
- const caches = shadowMeta?.transformCaches;
3011
- if (caches) {
3012
- // Iterate over ALL keys in the cache map.
3013
- for (const key of caches.keys()) {
3014
- // If the key belongs to this component instance, delete it.
3015
- // This purges caches for 'sort by name', 'sort by score', etc.
3016
- if (key.startsWith(componentId)) {
3017
- caches.delete(key);
3018
- }
2951
+ if (e.type === 'GET_SELECTED') {
2952
+ return;
2953
+ }
2954
+ const shadowMeta = getGlobalStore
2955
+ .getState()
2956
+ .getShadowMetadata(stateKey, path);
2957
+
2958
+ const caches = shadowMeta?.transformCaches;
2959
+ if (caches) {
2960
+ // Iterate over ALL keys in the cache map.
2961
+ for (const key of caches.keys()) {
2962
+ // If the key belongs to this component instance, delete it.
2963
+ // This purges caches for 'sort by name', 'sort by score', etc.
2964
+ if (key.startsWith(componentId)) {
2965
+ caches.delete(key);
3019
2966
  }
3020
2967
  }
2968
+ }
3021
2969
 
3022
- if (
3023
- e.type === 'INSERT' ||
3024
- e.type === 'REMOVE' ||
3025
- e.type === 'CLEAR_SELECTION'
3026
- ) {
3027
- forceUpdate({});
3028
- }
3029
- });
3030
-
3031
- return () => {
3032
- unsubscribe();
3033
- };
2970
+ if (
2971
+ e.type === 'INSERT' ||
2972
+ e.type === 'REMOVE' ||
2973
+ e.type === 'CLEAR_SELECTION'
2974
+ ) {
2975
+ forceUpdate({});
2976
+ }
2977
+ });
3034
2978
 
3035
- // This effect's logic now depends on the componentId to perform the purge.
3036
- }, [componentId, stateKeyPathKey]);
2979
+ return () => {
2980
+ unsubscribe();
2981
+ };
3037
2982
 
3038
- if (!Array.isArray(arrayValues)) {
3039
- return null;
3040
- }
2983
+ // This effect's logic now depends on the componentId to perform the purge.
2984
+ }, [componentId, stateKeyPathKey]);
3041
2985
 
3042
- const arraySetter = rebuildStateShape({
3043
- currentState: arrayValues as any,
3044
- path,
3045
- componentId: componentId!,
3046
- meta: {
3047
- ...meta,
3048
- validIds: validIds,
3049
- },
3050
- });
2986
+ if (!Array.isArray(arrayValues)) {
2987
+ return null;
2988
+ }
3051
2989
 
3052
- return (
3053
- <>
3054
- {arrayValues.map((item, localIndex) => {
3055
- const itemKey = validIds[localIndex];
2990
+ const arraySetter = rebuildStateShape({
2991
+ path,
2992
+ componentId: componentId!,
2993
+ meta: {
2994
+ ...meta,
2995
+ validIds: validIds,
2996
+ },
2997
+ });
3056
2998
 
3057
- if (!itemKey) {
3058
- return null;
3059
- }
2999
+ return (
3000
+ <>
3001
+ {arrayValues.map((item, localIndex) => {
3002
+ const itemKey = validIds[localIndex];
3060
3003
 
3061
- let itemComponentId =
3062
- componentIdsRef.current.get(itemKey);
3063
- if (!itemComponentId) {
3064
- itemComponentId = uuidv4();
3065
- componentIdsRef.current.set(itemKey, itemComponentId);
3066
- }
3004
+ if (!itemKey) {
3005
+ return null;
3006
+ }
3067
3007
 
3068
- const itemPath = itemKey.split('.').slice(1);
3008
+ let itemComponentId = componentIdsRef.current.get(itemKey);
3009
+ if (!itemComponentId) {
3010
+ itemComponentId = uuidv4();
3011
+ componentIdsRef.current.set(itemKey, itemComponentId);
3012
+ }
3069
3013
 
3070
- return createElement(MemoizedCogsItemWrapper, {
3071
- key: itemKey,
3072
- stateKey,
3073
- itemComponentId,
3074
- itemPath,
3075
- localIndex,
3076
- arraySetter,
3077
- rebuildStateShape,
3078
- renderFn: callbackfn,
3079
- });
3080
- })}
3081
- </>
3082
- );
3083
- };
3014
+ const itemPath = itemKey.split('.').slice(1);
3084
3015
 
3085
- return <StateListWrapper />;
3086
- };
3087
- }
3088
- if (prop === 'stateFlattenOn') {
3089
- return (fieldName: string) => {
3090
- const arrayToMap = currentState as any[];
3091
- shapeCache.clear();
3092
- stateVersion++;
3093
- const flattenedResults = arrayToMap.flatMap(
3094
- (val: any) => val[fieldName] ?? []
3016
+ return createElement(MemoizedCogsItemWrapper, {
3017
+ key: itemKey,
3018
+ stateKey,
3019
+ itemComponentId,
3020
+ itemPath,
3021
+ localIndex,
3022
+ arraySetter,
3023
+ rebuildStateShape,
3024
+ renderFn: callbackfn,
3025
+ });
3026
+ })}
3027
+ </>
3095
3028
  );
3096
- return rebuildStateShape({
3097
- currentState: flattenedResults as any,
3098
- path: [...path, '[*]', fieldName],
3099
- componentId: componentId!,
3100
- meta,
3101
- });
3102
- };
3103
- }
3104
- if (prop === 'index') {
3105
- return (index: number) => {
3106
- const arrayKeys = getGlobalStore
3107
- .getState()
3108
- .getShadowMetadata(stateKey, path)
3109
- ?.arrayKeys?.filter(
3110
- (key) =>
3111
- !meta?.validIds ||
3112
- (meta?.validIds && meta?.validIds?.includes(key))
3113
- );
3114
- const itemId = arrayKeys?.[index];
3115
- if (!itemId) return undefined;
3116
- const value = getGlobalStore
3117
- .getState()
3118
- .getShadowValue(itemId, meta?.validIds);
3119
- const state = rebuildStateShape({
3120
- currentState: value,
3121
- path: itemId.split('.').slice(1) as string[],
3122
- componentId: componentId!,
3123
- meta,
3124
- });
3125
- return state;
3126
- };
3127
- }
3128
- if (prop === 'last') {
3129
- return () => {
3130
- const currentArray = getGlobalStore
3131
- .getState()
3132
- .getShadowValue(stateKey, path) as any[];
3133
- if (currentArray.length === 0) return undefined;
3134
- const lastIndex = currentArray.length - 1;
3135
- const lastValue = currentArray[lastIndex];
3136
- const newPath = [...path, lastIndex.toString()];
3137
- return rebuildStateShape({
3138
- currentState: lastValue,
3139
- path: newPath,
3140
- componentId: componentId!,
3141
- meta,
3142
- });
3143
- };
3144
- }
3145
- if (prop === 'insert') {
3146
- return (
3147
- payload: InsertParams<InferArrayElement<T>>,
3148
- index?: number
3149
- ) => {
3150
- effectiveSetState(payload as any, path, { updateType: 'insert' });
3151
- return rebuildStateShape({
3152
- currentState: getGlobalStore
3153
- .getState()
3154
- .getShadowValue(stateKey, path),
3155
- path,
3156
- componentId: componentId!,
3157
- meta,
3158
- });
3159
- };
3160
- }
3161
- if (prop === 'uniqueInsert') {
3162
- return (
3163
- payload: UpdateArg<T>,
3164
- fields?: (keyof InferArrayElement<T>)[],
3165
- onMatch?: (existingItem: any) => any
3166
- ) => {
3167
- const currentArray = getGlobalStore
3168
- .getState()
3169
- .getShadowValue(stateKey, path) as any[];
3170
- const newValue = isFunction<T>(payload)
3171
- ? payload(currentArray as any)
3172
- : (payload as any);
3173
-
3174
- let matchedItem: any = null;
3175
- const isUnique = !currentArray.some((item) => {
3176
- const isMatch = fields
3177
- ? fields.every((field) =>
3178
- isDeepEqual(item[field], newValue[field])
3179
- )
3180
- : isDeepEqual(item, newValue);
3181
- if (isMatch) matchedItem = item;
3182
- return isMatch;
3183
- });
3184
-
3185
- if (isUnique) {
3186
- invalidateCachePath(path);
3187
- effectiveSetState(newValue, path, { updateType: 'insert' });
3188
- } else if (onMatch && matchedItem) {
3189
- const updatedItem = onMatch(matchedItem);
3190
- const updatedArray = currentArray.map((item) =>
3191
- isDeepEqual(item, matchedItem) ? updatedItem : item
3192
- );
3193
- invalidateCachePath(path);
3194
- effectiveSetState(updatedArray as any, path, {
3195
- updateType: 'update',
3196
- });
3197
- }
3198
3029
  };
3199
- }
3200
3030
 
3201
- if (prop === 'cut') {
3202
- return (index?: number, options?: { waitForSync?: boolean }) => {
3203
- const validKeys =
3204
- meta?.validIds ??
3205
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
3206
- ?.arrayKeys;
3207
-
3208
- if (!validKeys || validKeys.length === 0) return;
3031
+ return <StateListWrapper />;
3032
+ };
3033
+ }
3034
+ if (prop === 'stateFlattenOn') {
3035
+ return (fieldName: string) => {
3036
+ const currentState = getGlobalStore
3037
+ .getState()
3038
+ .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3039
+ if (!Array.isArray(currentState)) return []; // Guard clause
3040
+ const arrayToMap = currentState as any[];
3209
3041
 
3210
- const indexToCut =
3211
- index == -1
3212
- ? validKeys.length - 1
3213
- : index !== undefined
3214
- ? index
3215
- : validKeys.length - 1;
3042
+ stateVersion++;
3043
+ const flattenedResults = arrayToMap.flatMap(
3044
+ (val: any) => val[fieldName] ?? []
3045
+ );
3046
+ return rebuildStateShape({
3047
+ path: [...path, '[*]', fieldName],
3048
+ componentId: componentId!,
3049
+ meta,
3050
+ });
3051
+ };
3052
+ }
3053
+ if (prop === 'index') {
3054
+ return (index: number) => {
3055
+ const arrayKeys = getGlobalStore
3056
+ .getState()
3057
+ .getShadowMetadata(stateKey, path)
3058
+ ?.arrayKeys?.filter(
3059
+ (key) =>
3060
+ !meta?.validIds ||
3061
+ (meta?.validIds && meta?.validIds?.includes(key))
3062
+ );
3063
+ const itemId = arrayKeys?.[index];
3064
+ if (!itemId) return undefined;
3065
+ const value = getGlobalStore
3066
+ .getState()
3067
+ .getShadowValue(itemId, meta?.validIds);
3068
+ const state = rebuildStateShape({
3069
+ path: itemId.split('.').slice(1) as string[],
3070
+ componentId: componentId!,
3071
+ meta,
3072
+ });
3073
+ return state;
3074
+ };
3075
+ }
3076
+ if (prop === 'last') {
3077
+ return () => {
3078
+ const currentArray = getGlobalStore
3079
+ .getState()
3080
+ .getShadowValue(stateKey, path) as any[];
3081
+ if (currentArray.length === 0) return undefined;
3082
+ const lastIndex = currentArray.length - 1;
3083
+ const lastValue = currentArray[lastIndex];
3084
+ const newPath = [...path, lastIndex.toString()];
3085
+ return rebuildStateShape({
3086
+ path: newPath,
3087
+ componentId: componentId!,
3088
+ meta,
3089
+ });
3090
+ };
3091
+ }
3092
+ if (prop === 'insert') {
3093
+ return (
3094
+ payload: InsertParams<InferArrayElement<T>>,
3095
+ index?: number
3096
+ ) => {
3097
+ effectiveSetState(payload as any, path, { updateType: 'insert' });
3098
+ return rebuildStateShape({
3099
+ path,
3100
+ componentId: componentId!,
3101
+ meta,
3102
+ });
3103
+ };
3104
+ }
3105
+ if (prop === 'uniqueInsert') {
3106
+ return (
3107
+ payload: UpdateArg<T>,
3108
+ fields?: (keyof InferArrayElement<T>)[],
3109
+ onMatch?: (existingItem: any) => any
3110
+ ) => {
3111
+ const currentArray = getGlobalStore
3112
+ .getState()
3113
+ .getShadowValue(stateKey, path) as any[];
3114
+ const newValue = isFunction<T>(payload)
3115
+ ? payload(currentArray as any)
3116
+ : (payload as any);
3117
+
3118
+ let matchedItem: any = null;
3119
+ const isUnique = !currentArray.some((item) => {
3120
+ const isMatch = fields
3121
+ ? fields.every((field) =>
3122
+ isDeepEqual(item[field], newValue[field])
3123
+ )
3124
+ : isDeepEqual(item, newValue);
3125
+ if (isMatch) matchedItem = item;
3126
+ return isMatch;
3127
+ });
3216
3128
 
3217
- const fullIdToCut = validKeys[indexToCut];
3218
- if (!fullIdToCut) return; // Index out of bounds
3129
+ if (isUnique) {
3130
+ effectiveSetState(newValue, path, { updateType: 'insert' });
3131
+ } else if (onMatch && matchedItem) {
3132
+ const updatedItem = onMatch(matchedItem);
3133
+ const updatedArray = currentArray.map((item) =>
3134
+ isDeepEqual(item, matchedItem) ? updatedItem : item
3135
+ );
3219
3136
 
3220
- const pathForCut = fullIdToCut.split('.').slice(1);
3221
- effectiveSetState(currentState, pathForCut, {
3222
- updateType: 'cut',
3137
+ effectiveSetState(updatedArray as any, path, {
3138
+ updateType: 'update',
3223
3139
  });
3224
- };
3225
- }
3226
- if (prop === 'cutSelected') {
3227
- return () => {
3228
- const validKeys = applyTransforms(
3229
- stateKey,
3230
- path,
3231
- meta?.transforms
3232
- );
3140
+ }
3141
+ };
3142
+ }
3233
3143
 
3234
- if (!validKeys || validKeys.length === 0) return;
3144
+ if (prop === 'cut') {
3145
+ return (index?: number, options?: { waitForSync?: boolean }) => {
3146
+ const currentState = getGlobalStore
3147
+ .getState()
3148
+ .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3149
+ const validKeys =
3150
+ meta?.validIds ??
3151
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
3152
+ ?.arrayKeys;
3153
+
3154
+ if (!validKeys || validKeys.length === 0) return;
3155
+
3156
+ const indexToCut =
3157
+ index == -1
3158
+ ? validKeys.length - 1
3159
+ : index !== undefined
3160
+ ? index
3161
+ : validKeys.length - 1;
3162
+
3163
+ const fullIdToCut = validKeys[indexToCut];
3164
+ if (!fullIdToCut) return; // Index out of bounds
3165
+
3166
+ const pathForCut = fullIdToCut.split('.').slice(1);
3167
+ effectiveSetState(currentState, pathForCut, {
3168
+ updateType: 'cut',
3169
+ });
3170
+ };
3171
+ }
3172
+ if (prop === 'cutSelected') {
3173
+ return () => {
3174
+ const validKeys = applyTransforms(stateKey, path, meta?.transforms);
3175
+ const currentState = getGlobalStore
3176
+ .getState()
3177
+ .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3178
+ if (!validKeys || validKeys.length === 0) return;
3235
3179
 
3236
- const indexKeyToCut = getGlobalStore
3237
- .getState()
3238
- .selectedIndicesMap.get(stateKeyPathKey);
3180
+ const indexKeyToCut = getGlobalStore
3181
+ .getState()
3182
+ .selectedIndicesMap.get(stateKeyPathKey);
3239
3183
 
3240
- let indexToCut = validKeys.findIndex(
3241
- (key) => key === indexKeyToCut
3242
- );
3184
+ let indexToCut = validKeys.findIndex(
3185
+ (key) => key === indexKeyToCut
3186
+ );
3243
3187
 
3244
- const pathForCut = validKeys[
3245
- indexToCut == -1 ? validKeys.length - 1 : indexToCut
3246
- ]
3247
- ?.split('.')
3248
- .slice(1);
3249
- getGlobalStore
3250
- .getState()
3251
- .clearSelectedIndex({ arrayKey: stateKeyPathKey });
3252
- const parentPath = pathForCut?.slice(0, -1)!;
3253
- notifySelectionComponents(stateKey, parentPath);
3254
- effectiveSetState(currentState, pathForCut!, {
3255
- updateType: 'cut',
3256
- });
3257
- };
3258
- }
3259
- if (prop === 'cutByValue') {
3260
- return (value: string | number | boolean) => {
3261
- // Step 1: Get the list of all unique keys for the current view.
3262
- const arrayMeta = getGlobalStore
3263
- .getState()
3264
- .getShadowMetadata(stateKey, path);
3265
- const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
3188
+ const pathForCut = validKeys[
3189
+ indexToCut == -1 ? validKeys.length - 1 : indexToCut
3190
+ ]
3191
+ ?.split('.')
3192
+ .slice(1);
3193
+ getGlobalStore
3194
+ .getState()
3195
+ .clearSelectedIndex({ arrayKey: stateKeyPathKey });
3196
+ const parentPath = pathForCut?.slice(0, -1)!;
3197
+ notifySelectionComponents(stateKey, parentPath);
3198
+ effectiveSetState(currentState, pathForCut!, {
3199
+ updateType: 'cut',
3200
+ });
3201
+ };
3202
+ }
3203
+ if (prop === 'cutByValue') {
3204
+ return (value: string | number | boolean) => {
3205
+ // Step 1: Get the list of all unique keys for the current view.
3206
+ const arrayMeta = getGlobalStore
3207
+ .getState()
3208
+ .getShadowMetadata(stateKey, path);
3209
+ const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
3266
3210
 
3267
- if (!relevantKeys) return;
3211
+ if (!relevantKeys) return;
3268
3212
 
3269
- let keyToCut: string | null = null;
3213
+ let keyToCut: string | null = null;
3270
3214
 
3271
- // Step 2: Iterate through the KEYS, get the value for each, and find the match.
3272
- for (const key of relevantKeys) {
3273
- const itemValue = getGlobalStore.getState().getShadowValue(key);
3274
- if (itemValue === value) {
3275
- keyToCut = key;
3276
- break; // We found the key, no need to search further.
3277
- }
3215
+ // Step 2: Iterate through the KEYS, get the value for each, and find the match.
3216
+ for (const key of relevantKeys) {
3217
+ const itemValue = getGlobalStore.getState().getShadowValue(key);
3218
+ if (itemValue === value) {
3219
+ keyToCut = key;
3220
+ break; // We found the key, no need to search further.
3278
3221
  }
3222
+ }
3279
3223
 
3280
- // Step 3: If we found a matching key, use it to perform the cut.
3281
- if (keyToCut) {
3282
- const itemPath = keyToCut.split('.').slice(1);
3283
- effectiveSetState(null as any, itemPath, { updateType: 'cut' });
3284
- }
3285
- };
3286
- }
3224
+ // Step 3: If we found a matching key, use it to perform the cut.
3225
+ if (keyToCut) {
3226
+ const itemPath = keyToCut.split('.').slice(1);
3227
+ effectiveSetState(null as any, itemPath, { updateType: 'cut' });
3228
+ }
3229
+ };
3230
+ }
3287
3231
 
3288
- if (prop === 'toggleByValue') {
3289
- return (value: string | number | boolean) => {
3290
- // Step 1: Get the list of all unique keys for the current view.
3291
- const arrayMeta = getGlobalStore
3292
- .getState()
3293
- .getShadowMetadata(stateKey, path);
3294
- const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
3232
+ if (prop === 'toggleByValue') {
3233
+ return (value: string | number | boolean) => {
3234
+ // Step 1: Get the list of all unique keys for the current view.
3235
+ const arrayMeta = getGlobalStore
3236
+ .getState()
3237
+ .getShadowMetadata(stateKey, path);
3238
+ const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
3295
3239
 
3296
- if (!relevantKeys) return;
3240
+ if (!relevantKeys) return;
3297
3241
 
3298
- let keyToCut: string | null = null;
3242
+ let keyToCut: string | null = null;
3299
3243
 
3300
- // Step 2: Iterate through the KEYS to find the one matching the value. This is the robust way.
3301
- for (const key of relevantKeys) {
3302
- const itemValue = getGlobalStore.getState().getShadowValue(key);
3303
- console.log('itemValue sdasdasdasd', itemValue);
3304
- if (itemValue === value) {
3305
- keyToCut = key;
3306
- break; // Found it!
3307
- }
3244
+ // Step 2: Iterate through the KEYS to find the one matching the value. This is the robust way.
3245
+ for (const key of relevantKeys) {
3246
+ const itemValue = getGlobalStore.getState().getShadowValue(key);
3247
+ console.log('itemValue sdasdasdasd', itemValue);
3248
+ if (itemValue === value) {
3249
+ keyToCut = key;
3250
+ break; // Found it!
3308
3251
  }
3252
+ }
3253
+ console.log('itemValue keyToCut', keyToCut);
3254
+ // Step 3: Act based on whether the key was found.
3255
+ if (keyToCut) {
3256
+ // Item exists, so we CUT it using its *actual* key.
3257
+ const itemPath = keyToCut.split('.').slice(1);
3309
3258
  console.log('itemValue keyToCut', keyToCut);
3310
- // Step 3: Act based on whether the key was found.
3311
- if (keyToCut) {
3312
- // Item exists, so we CUT it using its *actual* key.
3313
- const itemPath = keyToCut.split('.').slice(1);
3314
- console.log('itemValue keyToCut', keyToCut);
3315
- effectiveSetState(value as any, itemPath, {
3316
- updateType: 'cut',
3317
- });
3318
- } else {
3319
- // Item does not exist, so we INSERT it.
3320
- effectiveSetState(value as any, path, { updateType: 'insert' });
3321
- }
3322
- };
3323
- }
3324
- if (prop === 'findWith') {
3325
- return (
3326
- searchKey: keyof InferArrayElement<T>,
3327
- searchValue: any
3328
- ) => {
3329
- const arrayKeys = getGlobalStore
3330
- .getState()
3331
- .getShadowMetadata(stateKey, path)?.arrayKeys;
3259
+ effectiveSetState(value as any, itemPath, {
3260
+ updateType: 'cut',
3261
+ });
3262
+ } else {
3263
+ // Item does not exist, so we INSERT it.
3264
+ effectiveSetState(value as any, path, { updateType: 'insert' });
3265
+ }
3266
+ };
3267
+ }
3268
+ if (prop === 'findWith') {
3269
+ return (searchKey: keyof InferArrayElement<T>, searchValue: any) => {
3270
+ const arrayKeys = getGlobalStore
3271
+ .getState()
3272
+ .getShadowMetadata(stateKey, path)?.arrayKeys;
3332
3273
 
3333
- if (!arrayKeys) {
3334
- throw new Error('No array keys found for sorting');
3335
- }
3274
+ if (!arrayKeys) {
3275
+ throw new Error('No array keys found for sorting');
3276
+ }
3336
3277
 
3337
- let value = null;
3338
- let foundPath: string[] = [];
3278
+ let value = null;
3279
+ let foundPath: string[] = [];
3339
3280
 
3340
- for (const fullPath of arrayKeys) {
3341
- let shadowValue = getGlobalStore
3342
- .getState()
3343
- .getShadowValue(fullPath, meta?.validIds);
3344
- if (shadowValue && shadowValue[searchKey] === searchValue) {
3345
- value = shadowValue;
3346
- foundPath = fullPath.split('.').slice(1);
3347
- break;
3348
- }
3281
+ for (const fullPath of arrayKeys) {
3282
+ let shadowValue = getGlobalStore
3283
+ .getState()
3284
+ .getShadowValue(fullPath, meta?.validIds);
3285
+ if (shadowValue && shadowValue[searchKey] === searchValue) {
3286
+ value = shadowValue;
3287
+ foundPath = fullPath.split('.').slice(1);
3288
+ break;
3349
3289
  }
3290
+ }
3350
3291
 
3351
- return rebuildStateShape({
3352
- currentState: value as any,
3353
- path: foundPath,
3354
- componentId: componentId!,
3355
- meta,
3356
- });
3357
- };
3358
- }
3292
+ return rebuildStateShape({
3293
+ path: foundPath,
3294
+ componentId: componentId!,
3295
+ meta,
3296
+ });
3297
+ };
3359
3298
  }
3360
3299
 
3361
- if (prop === 'cut') {
3300
+ if (prop === 'cutThis') {
3362
3301
  let shadowValue = getGlobalStore
3363
3302
  .getState()
3364
3303
  .getShadowValue(path.join('.'));
@@ -3483,7 +3422,6 @@ function createProxyHandler<T>(
3483
3422
  const init = getGlobalStore
3484
3423
  .getState()
3485
3424
  .getInitialOptions(stateKey)?.validation;
3486
- if (!init?.key) throw new Error('Validation key not found');
3487
3425
 
3488
3426
  // For each error, set shadow metadata
3489
3427
  zodErrors.forEach((error) => {
@@ -3697,7 +3635,9 @@ function createProxyHandler<T>(
3697
3635
  const currentValueAtPath = getGlobalStore
3698
3636
  .getState()
3699
3637
  .getShadowValue([stateKey, ...path].join('.'));
3700
-
3638
+ const currentState = getGlobalStore
3639
+ .getState()
3640
+ .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3701
3641
  console.log('currentValueAtPath', currentValueAtPath);
3702
3642
  if (typeof currentState != 'boolean') {
3703
3643
  throw new Error('toggle() can only be used on boolean values');
@@ -3727,7 +3667,6 @@ function createProxyHandler<T>(
3727
3667
  .getState()
3728
3668
  .getShadowValue(stateKey, nextPath);
3729
3669
  return rebuildStateShape({
3730
- currentState: nextValue,
3731
3670
  path: nextPath,
3732
3671
  componentId: componentId!,
3733
3672
  meta,
@@ -3736,10 +3675,7 @@ function createProxyHandler<T>(
3736
3675
  };
3737
3676
 
3738
3677
  const proxyInstance = new Proxy(baseFunction, handler);
3739
- shapeCache.set(cacheKey, {
3740
- proxy: proxyInstance,
3741
- stateVersion: stateVersion,
3742
- });
3678
+ proxyCache.set(cacheKey, proxyInstance);
3743
3679
  return proxyInstance;
3744
3680
  }
3745
3681
 
@@ -3765,11 +3701,10 @@ function createProxyHandler<T>(
3765
3701
  getGlobalStore.getState().initialStateGlobal[stateKey];
3766
3702
 
3767
3703
  getGlobalStore.getState().clearSelectedIndexesForState(stateKey);
3768
- shapeCache.clear();
3704
+
3769
3705
  stateVersion++;
3770
3706
  getGlobalStore.getState().initializeShadowState(stateKey, initialState);
3771
- const newProxy = rebuildStateShape({
3772
- currentState: initialState,
3707
+ rebuildStateShape({
3773
3708
  path: [],
3774
3709
  componentId: componentId!,
3775
3710
  });
@@ -3796,7 +3731,6 @@ function createProxyHandler<T>(
3796
3731
  return initialState;
3797
3732
  },
3798
3733
  updateInitialState: (newState: T) => {
3799
- shapeCache.clear();
3800
3734
  stateVersion++;
3801
3735
 
3802
3736
  const newUpdaterState = createProxyHandler(
@@ -3838,7 +3772,6 @@ function createProxyHandler<T>(
3838
3772
  },
3839
3773
  };
3840
3774
  const returnShape = rebuildStateShape({
3841
- currentState: getGlobalStore.getState().getShadowValue(stateKey, []),
3842
3775
  componentId,
3843
3776
  path: [],
3844
3777
  });