cogsbox-state 0.5.465 → 0.5.466

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
@@ -14,30 +14,32 @@ import {
14
14
  type ReactNode,
15
15
  type RefObject,
16
16
  } from 'react';
17
- import { createRoot } from 'react-dom/client';
17
+
18
18
  import {
19
19
  getDifferences,
20
20
  isArray,
21
21
  isFunction,
22
22
  type GenericObject,
23
23
  } from './utility.js';
24
- import { ValidationWrapper } from './Functions.js';
24
+ import {
25
+ FormElementWrapper,
26
+ MemoizedCogsItemWrapper,
27
+ ValidationWrapper,
28
+ } from './Components.js';
25
29
  import { isDeepEqual, transformStateFunc } from './utility.js';
26
30
  import superjson from 'superjson';
27
31
  import { v4 as uuidv4 } from 'uuid';
28
32
 
29
33
  import {
30
- buildShadowNode,
31
34
  formRefStore,
32
35
  getGlobalStore,
33
- METADATA_KEYS,
34
36
  ValidationError,
35
37
  ValidationStatus,
36
38
  type ComponentsType,
37
39
  } from './store.js';
38
40
  import { useCogsConfig } from './CogsStateClient.js';
39
41
  import { Operation } from 'fast-json-patch';
40
- import { useInView } from 'react-intersection-observer';
42
+
41
43
  import * as z3 from 'zod/v3';
42
44
  import * as z4 from 'zod/v4';
43
45
 
@@ -488,6 +490,7 @@ const {
488
490
  initializeShadowState,
489
491
  updateShadowAtPath,
490
492
  insertShadowArrayElement,
493
+ insertManyShadowArrayElements,
491
494
  removeShadowArrayElement,
492
495
  getSelectedIndex,
493
496
  setInitialStateOptions,
@@ -513,8 +516,7 @@ function getArrayData(stateKey: string, path: string[], meta?: MetaData) {
513
516
  const value = getGlobalStore.getState().getShadowValue(stateKey, path);
514
517
  return { isArray: false, value, keys: [] };
515
518
  }
516
-
517
- const arrayPathKey = path.join('.');
519
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
518
520
  const viewIds = meta?.arrayViews?.[arrayPathKey] ?? shadowMeta.arrayKeys;
519
521
 
520
522
  // FIX: If the derived view is empty, return an empty array and keys.
@@ -1467,11 +1469,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
1467
1469
  return; // Ignore if no valid data
1468
1470
  }
1469
1471
 
1470
- console.log(
1471
- '✅ SERVER_STATE_UPDATE received with data:',
1472
- serverStateData
1473
- );
1474
-
1475
1472
  setAndMergeOptions(thisKey, { serverState: serverStateData });
1476
1473
 
1477
1474
  const mergeConfig =
@@ -1481,7 +1478,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
1481
1478
  ? { strategy: 'append' }
1482
1479
  : null;
1483
1480
 
1484
- // ✅ FIX 1: The path for the root value is now `[]`.
1485
1481
  const currentState = getShadowValue(thisKey, []);
1486
1482
  const incomingData = serverStateData.data;
1487
1483
 
@@ -1499,7 +1495,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1499
1495
  );
1500
1496
  return;
1501
1497
  }
1502
- console.log('SERVER_STATE_UPDATE 2');
1498
+
1503
1499
  const existingIds = new Set(
1504
1500
  currentState.map((item: any) => item[keyField])
1505
1501
  );
@@ -1509,9 +1505,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1509
1505
  );
1510
1506
 
1511
1507
  if (newUniqueItems.length > 0) {
1512
- newUniqueItems.forEach((item) => {
1513
- insertShadowArrayElement(thisKey, [], item);
1514
- });
1508
+ insertManyShadowArrayElements(thisKey, [], newUniqueItems);
1515
1509
  }
1516
1510
 
1517
1511
  // Mark the entire final state as synced
@@ -1698,27 +1692,16 @@ type MetaData = {
1698
1692
  fn: Function;
1699
1693
  path: string[]; // Which array this transform applies to
1700
1694
  }>;
1695
+ serverStateIsUpStream?: boolean;
1701
1696
  };
1702
1697
 
1703
- function hashTransforms(transforms: any[]) {
1704
- if (!transforms || transforms.length === 0) {
1705
- return '';
1706
- }
1707
- return transforms
1708
- .map(
1709
- (transform) =>
1710
- `${transform.type}${JSON.stringify(transform.dependencies || [])}`
1711
- )
1712
- .join('');
1713
- }
1714
-
1715
1698
  const applyTransforms = (
1716
1699
  stateKey: string,
1717
1700
  path: string[],
1718
- transforms?: Array<{ type: 'filter' | 'sort'; fn: Function }>
1701
+ meta?: MetaData
1719
1702
  ): string[] => {
1720
1703
  let ids = getShadowMetadata(stateKey, path)?.arrayKeys || [];
1721
- console.log('ids', ids);
1704
+ const transforms = meta?.transforms;
1722
1705
  if (!transforms || transforms.length === 0) {
1723
1706
  return ids;
1724
1707
  }
@@ -1775,8 +1758,7 @@ const notifySelectionComponents = (
1775
1758
  parentPath: string[],
1776
1759
  currentSelected?: string | undefined
1777
1760
  ) => {
1778
- const store = getGlobalStore.getState();
1779
- const rootMeta = store.getShadowMetadata(stateKey, []);
1761
+ const rootMeta = getShadowMetadata(stateKey, []);
1780
1762
  const notifiedComponents = new Set<string>();
1781
1763
 
1782
1764
  // Handle "all" reactive components first
@@ -1793,20 +1775,18 @@ const notifySelectionComponents = (
1793
1775
  });
1794
1776
  }
1795
1777
 
1796
- store
1797
- .getShadowMetadata(stateKey, [...parentPath, 'getSelected'])
1798
- ?.pathComponents?.forEach((componentId) => {
1799
- const thisComp = rootMeta?.components?.get(componentId);
1800
- thisComp?.forceUpdate();
1801
- });
1778
+ getShadowMetadata(stateKey, [
1779
+ ...parentPath,
1780
+ 'getSelected',
1781
+ ])?.pathComponents?.forEach((componentId) => {
1782
+ const thisComp = rootMeta?.components?.get(componentId);
1783
+ thisComp?.forceUpdate();
1784
+ });
1802
1785
 
1803
- const parentMeta = store.getShadowMetadata(stateKey, parentPath);
1786
+ const parentMeta = getShadowMetadata(stateKey, parentPath);
1804
1787
  for (let arrayKey of parentMeta?.arrayKeys || []) {
1805
1788
  const key = arrayKey + '.selected';
1806
- const selectedItem = store.getShadowMetadata(
1807
- stateKey,
1808
- key.split('.').slice(1)
1809
- );
1789
+ const selectedItem = getShadowMetadata(stateKey, key.split('.').slice(1));
1810
1790
  if (arrayKey == currentSelected) {
1811
1791
  selectedItem?.pathComponents?.forEach((componentId) => {
1812
1792
  const thisComp = rootMeta?.components?.get(componentId);
@@ -1817,7 +1797,7 @@ const notifySelectionComponents = (
1817
1797
  };
1818
1798
  function getScopedData(stateKey: string, path: string[], meta?: MetaData) {
1819
1799
  const shadowMeta = getShadowMetadata(stateKey, path);
1820
- const arrayPathKey = path.join('.');
1800
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
1821
1801
  const arrayKeys = meta?.arrayViews?.[arrayPathKey];
1822
1802
 
1823
1803
  // FIX: If the derived view is empty, return an empty array directly.
@@ -2168,6 +2148,13 @@ function createProxyHandler<T>(
2168
2148
  const [rerender, forceUpdate] = useState({});
2169
2149
  const initialScrollRef = useRef(true);
2170
2150
 
2151
+ useEffect(() => {
2152
+ const interval = setInterval(() => {
2153
+ forceUpdate({});
2154
+ }, 1000);
2155
+ return () => clearInterval(interval);
2156
+ }, []);
2157
+
2171
2158
  // Scroll state management
2172
2159
  const scrollStateRef = useRef({
2173
2160
  isUserScrolling: false,
@@ -2180,57 +2167,28 @@ function createProxyHandler<T>(
2180
2167
  const measurementCache = useRef(
2181
2168
  new Map<string, { height: number; offset: number }>()
2182
2169
  );
2170
+ const { keys: arrayKeys } = getArrayData(stateKey, path, meta);
2183
2171
 
2184
- // Separate effect for handling rerender updates
2185
- useLayoutEffect(() => {
2186
- if (
2187
- !stickToBottom ||
2188
- !containerRef.current ||
2189
- scrollStateRef.current.isUserScrolling
2190
- )
2191
- return;
2192
-
2193
- const container = containerRef.current;
2194
- container.scrollTo({
2195
- top: container.scrollHeight,
2196
- behavior: initialScrollRef.current ? 'instant' : 'smooth',
2197
- });
2198
- }, [rerender, stickToBottom]);
2199
-
2200
- const { arrayKeys = [] } = getScopedData(stateKey, path, meta);
2201
-
2202
- // Calculate total height and offsets
2203
- const { totalHeight, itemOffsets } = useMemo(() => {
2204
- let runningOffset = 0;
2205
- const offsets = new Map<
2206
- string,
2207
- { height: number; offset: number }
2208
- >();
2209
- const allItemKeys =
2210
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2211
- ?.arrayKeys || [];
2212
-
2213
- allItemKeys.forEach((itemKey) => {
2214
- const itemPath = itemKey.split('.').slice(1);
2215
- const measuredHeight =
2216
- getGlobalStore
2217
- .getState()
2218
- .getShadowMetadata(stateKey, itemPath)?.virtualizer
2219
- ?.itemHeight || itemHeight;
2220
-
2221
- offsets.set(itemKey, {
2222
- height: measuredHeight,
2223
- offset: runningOffset,
2172
+ // Subscribe to state changes like stateList does
2173
+ useEffect(() => {
2174
+ const stateKeyPathKey = [stateKey, ...path].join('.');
2175
+ const unsubscribe = getGlobalStore
2176
+ .getState()
2177
+ .subscribeToPath(stateKeyPathKey, (e) => {
2178
+ if (e.type === 'GET_SELECTED') {
2179
+ return;
2180
+ }
2181
+ if (e.type === 'SERVER_STATE_UPDATE') {
2182
+ // forceUpdate({});
2183
+ }
2224
2184
  });
2225
2185
 
2226
- runningOffset += measuredHeight;
2227
- });
2228
-
2229
- measurementCache.current = offsets;
2230
- return { totalHeight: runningOffset, itemOffsets: offsets };
2231
- }, [arrayKeys.length, itemHeight]);
2186
+ return () => {
2187
+ unsubscribe();
2188
+ };
2189
+ }, [componentId, stateKey, path.join('.')]);
2232
2190
 
2233
- // Improved initial positioning effect
2191
+ // YOUR ORIGINAL INITIAL POSITIONING - KEEPING EXACTLY AS IS
2234
2192
  useLayoutEffect(() => {
2235
2193
  if (
2236
2194
  stickToBottom &&
@@ -2241,7 +2199,6 @@ function createProxyHandler<T>(
2241
2199
  ) {
2242
2200
  const container = containerRef.current;
2243
2201
 
2244
- // Wait for container to have dimensions
2245
2202
  const waitForContainer = () => {
2246
2203
  if (container.clientHeight > 0) {
2247
2204
  const visibleCount = Math.ceil(
@@ -2255,13 +2212,11 @@ function createProxyHandler<T>(
2255
2212
 
2256
2213
  setRange({ startIndex, endIndex });
2257
2214
 
2258
- // Ensure scroll after range is set
2259
2215
  requestAnimationFrame(() => {
2260
2216
  scrollToBottom('instant');
2261
- initialScrollRef.current = false; // Mark initial scroll as done
2217
+ initialScrollRef.current = false;
2262
2218
  });
2263
2219
  } else {
2264
- // Container not ready, try again
2265
2220
  requestAnimationFrame(waitForContainer);
2266
2221
  }
2267
2222
  };
@@ -2270,7 +2225,16 @@ function createProxyHandler<T>(
2270
2225
  }
2271
2226
  }, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
2272
2227
 
2273
- // Combined scroll handler
2228
+ const rangeRef = useRef(range);
2229
+ useLayoutEffect(() => {
2230
+ rangeRef.current = range;
2231
+ }, [range]);
2232
+
2233
+ const arrayKeysRef = useRef(arrayKeys);
2234
+ useLayoutEffect(() => {
2235
+ arrayKeysRef.current = arrayKeys;
2236
+ }, [arrayKeys]);
2237
+
2274
2238
  const handleScroll = useCallback(() => {
2275
2239
  const container = containerRef.current;
2276
2240
  if (!container) return;
@@ -2314,9 +2278,14 @@ function createProxyHandler<T>(
2314
2278
  break;
2315
2279
  }
2316
2280
  }
2317
-
2281
+ console.log(
2282
+ 'hadnlescroll ',
2283
+ measurementCache.current,
2284
+ newStartIndex,
2285
+ range
2286
+ );
2318
2287
  // Only update if range actually changed
2319
- if (newStartIndex !== range.startIndex) {
2288
+ if (newStartIndex !== range.startIndex && range.startIndex != 0) {
2320
2289
  const visibleCount = Math.ceil(clientHeight / itemHeight);
2321
2290
  setRange({
2322
2291
  startIndex: Math.max(0, newStartIndex - overscan),
@@ -2337,36 +2306,34 @@ function createProxyHandler<T>(
2337
2306
  // Set up scroll listener
2338
2307
  useEffect(() => {
2339
2308
  const container = containerRef.current;
2340
- if (!container || !stickToBottom) return;
2309
+ if (!container) return;
2341
2310
 
2342
2311
  container.addEventListener('scroll', handleScroll, {
2343
2312
  passive: true,
2344
2313
  });
2345
-
2346
2314
  return () => {
2347
2315
  container.removeEventListener('scroll', handleScroll);
2348
2316
  };
2349
2317
  }, [handleScroll, stickToBottom]);
2318
+
2319
+ // YOUR ORIGINAL SCROLL TO BOTTOM FUNCTION - KEEPING EXACTLY AS IS
2350
2320
  const scrollToBottom = useCallback(
2351
2321
  (behavior: ScrollBehavior = 'smooth') => {
2352
2322
  const container = containerRef.current;
2353
2323
  if (!container) return;
2354
2324
 
2355
- // Reset scroll state
2356
2325
  scrollStateRef.current.isUserScrolling = false;
2357
2326
  scrollStateRef.current.isNearBottom = true;
2358
2327
  scrollStateRef.current.scrollUpCount = 0;
2359
2328
 
2360
2329
  const performScroll = () => {
2361
- // Multiple attempts to ensure we hit the bottom
2362
2330
  const attemptScroll = (attempts = 0) => {
2363
- if (attempts > 5) return; // Prevent infinite loops
2331
+ if (attempts > 5) return;
2364
2332
 
2365
2333
  const currentHeight = container.scrollHeight;
2366
2334
  const currentScroll = container.scrollTop;
2367
2335
  const clientHeight = container.clientHeight;
2368
2336
 
2369
- // Check if we're already at the bottom
2370
2337
  if (currentScroll + clientHeight >= currentHeight - 1) {
2371
2338
  return;
2372
2339
  }
@@ -2376,12 +2343,10 @@ function createProxyHandler<T>(
2376
2343
  behavior: behavior,
2377
2344
  });
2378
2345
 
2379
- // In slow environments, check again after a short delay
2380
2346
  setTimeout(() => {
2381
2347
  const newHeight = container.scrollHeight;
2382
2348
  const newScroll = container.scrollTop;
2383
2349
 
2384
- // If height changed or we're not at bottom, try again
2385
2350
  if (
2386
2351
  newHeight !== currentHeight ||
2387
2352
  newScroll + clientHeight < newHeight - 1
@@ -2394,11 +2359,9 @@ function createProxyHandler<T>(
2394
2359
  attemptScroll();
2395
2360
  };
2396
2361
 
2397
- // Use requestIdleCallback for better performance in slow environments
2398
2362
  if ('requestIdleCallback' in window) {
2399
2363
  requestIdleCallback(performScroll, { timeout: 100 });
2400
2364
  } else {
2401
- // Fallback to rAF chain
2402
2365
  requestAnimationFrame(() => {
2403
2366
  requestAnimationFrame(performScroll);
2404
2367
  });
@@ -2406,15 +2369,14 @@ function createProxyHandler<T>(
2406
2369
  },
2407
2370
  []
2408
2371
  );
2409
- // Auto-scroll to bottom when new content arrives
2410
- // Consolidated auto-scroll effect with debouncing
2372
+
2373
+ // YOUR ORIGINAL AUTO-SCROLL EFFECTS - KEEPING ALL OF THEM
2411
2374
  useEffect(() => {
2412
2375
  if (!stickToBottom || !containerRef.current) return;
2413
2376
 
2414
2377
  const container = containerRef.current;
2415
2378
  const scrollState = scrollStateRef.current;
2416
2379
 
2417
- // Debounced scroll function
2418
2380
  let scrollTimeout: NodeJS.Timeout;
2419
2381
  const debouncedScrollToBottom = () => {
2420
2382
  clearTimeout(scrollTimeout);
@@ -2430,7 +2392,6 @@ function createProxyHandler<T>(
2430
2392
  }, 100);
2431
2393
  };
2432
2394
 
2433
- // Single MutationObserver for all DOM changes
2434
2395
  const observer = new MutationObserver(() => {
2435
2396
  if (!scrollState.isUserScrolling) {
2436
2397
  debouncedScrollToBottom();
@@ -2441,24 +2402,10 @@ function createProxyHandler<T>(
2441
2402
  childList: true,
2442
2403
  subtree: true,
2443
2404
  attributes: true,
2444
- attributeFilter: ['style', 'class'], // More specific than just 'height'
2405
+ attributeFilter: ['style', 'class'],
2445
2406
  });
2446
2407
 
2447
- // Handle image loads with event delegation
2448
- const handleImageLoad = (e: Event) => {
2449
- if (
2450
- e.target instanceof HTMLImageElement &&
2451
- !scrollState.isUserScrolling
2452
- ) {
2453
- debouncedScrollToBottom();
2454
- }
2455
- };
2456
-
2457
- container.addEventListener('load', handleImageLoad, true);
2458
-
2459
- // Initial scroll with proper timing
2460
2408
  if (initialScrollRef.current) {
2461
- // For initial load, wait for next tick to ensure DOM is ready
2462
2409
  setTimeout(() => {
2463
2410
  scrollToBottom('instant');
2464
2411
  }, 0);
@@ -2469,31 +2416,28 @@ function createProxyHandler<T>(
2469
2416
  return () => {
2470
2417
  clearTimeout(scrollTimeout);
2471
2418
  observer.disconnect();
2472
- container.removeEventListener('load', handleImageLoad, true);
2473
2419
  };
2474
2420
  }, [stickToBottom, arrayKeys.length, scrollToBottom]);
2475
- // Create virtual state
2421
+
2422
+ // Create virtual state - NO NEED to get values, only IDs!
2476
2423
  const virtualState = useMemo(() => {
2477
- const store = getGlobalStore.getState();
2478
- const sourceArray = store.getShadowValue(stateKey, path) as any[];
2479
- const currentKeys =
2480
- store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
2424
+ // 2. Physically slice the corresponding keys.
2425
+ const slicedKeys = Array.isArray(arrayKeys)
2426
+ ? arrayKeys.slice(range.startIndex, range.endIndex + 1)
2427
+ : [];
2481
2428
 
2482
- const slicedArray = sourceArray.slice(
2483
- range.startIndex,
2484
- range.endIndex + 1
2485
- );
2486
- const slicedIds = currentKeys.slice(
2487
- range.startIndex,
2488
- range.endIndex + 1
2489
- );
2429
+ // Use the same keying as getArrayData (empty string for root)
2490
2430
  const arrayPath = path.length > 0 ? path.join('.') : 'root';
2491
2431
  return rebuildStateShape({
2492
2432
  path,
2493
2433
  componentId: componentId!,
2494
- meta: { ...meta, arrayViews: { [arrayPath]: slicedIds } },
2434
+ meta: {
2435
+ ...meta,
2436
+ arrayViews: { [arrayPath]: slicedKeys },
2437
+ serverStateIsUpStream: true,
2438
+ },
2495
2439
  });
2496
- }, [range.startIndex, range.endIndex, arrayKeys.length]);
2440
+ }, [range.startIndex, range.endIndex, arrayKeys, meta]);
2497
2441
 
2498
2442
  return {
2499
2443
  virtualState,
@@ -2501,15 +2445,14 @@ function createProxyHandler<T>(
2501
2445
  outer: {
2502
2446
  ref: containerRef,
2503
2447
  style: {
2504
- overflowY: 'auto',
2448
+ overflowY: 'auto' as const,
2505
2449
  height: '100%',
2506
- position: 'relative',
2450
+ position: 'relative' as const,
2507
2451
  },
2508
2452
  },
2509
2453
  inner: {
2510
2454
  style: {
2511
- height: `${totalHeight}px`,
2512
- position: 'relative',
2455
+ position: 'relative' as const,
2513
2456
  },
2514
2457
  },
2515
2458
  list: {
@@ -2623,7 +2566,7 @@ function createProxyHandler<T>(
2623
2566
  }
2624
2567
  if (prop === 'stateSort') {
2625
2568
  return (compareFn: (a: any, b: any) => number) => {
2626
- const arrayPathKey = path.join('.');
2569
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2627
2570
 
2628
2571
  // FIX: Use the more robust `getArrayData` which always correctly resolves the keys for a view.
2629
2572
  const { value: currentArray, keys: currentViewIds } = getArrayData(
@@ -2772,28 +2715,33 @@ function createProxyHandler<T>(
2772
2715
  arraySetter: any
2773
2716
  ) => ReactNode
2774
2717
  ) => {
2775
- console.log('meta outside', JSON.stringify(meta));
2776
2718
  const StateListWrapper = () => {
2777
2719
  const componentIdsRef = useRef<Map<string, string>>(new Map());
2778
2720
 
2779
2721
  const [updateTrigger, forceUpdate] = useState({});
2780
2722
 
2781
- console.log('updateTrigger updateTrigger updateTrigger');
2723
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2782
2724
 
2783
- const validIds = applyTransforms(
2725
+ const validIds = useMemo(() => {
2726
+ return applyTransforms(stateKey, path, meta);
2727
+ }, [
2784
2728
  stateKey,
2785
- path,
2786
- meta?.transforms
2787
- );
2788
- //the above get the new coorect valid ids i need ot udpate the meta object with this info
2789
- const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2790
- const updatedMeta = {
2791
- ...meta,
2792
- arrayViews: {
2793
- ...(meta?.arrayViews || {}),
2794
- [arrayPathKey]: validIds, // Update the arrayViews with the new valid IDs
2795
- },
2796
- };
2729
+ path.join('.'),
2730
+ // Only recalculate if the underlying array keys or transforms change
2731
+ getShadowMetadata(stateKey, path)?.arrayKeys,
2732
+ meta?.transforms,
2733
+ ]);
2734
+
2735
+ // Memoize the updated meta to prevent creating new objects on every render
2736
+ const updatedMeta = useMemo(() => {
2737
+ return {
2738
+ ...meta,
2739
+ arrayViews: {
2740
+ ...(meta?.arrayViews || {}),
2741
+ [arrayPathKey]: validIds,
2742
+ },
2743
+ };
2744
+ }, [meta, arrayPathKey, validIds]);
2797
2745
 
2798
2746
  // Now use the updated meta when getting array data
2799
2747
  const { value: arrayValues } = getArrayData(
@@ -2802,15 +2750,10 @@ function createProxyHandler<T>(
2802
2750
  updatedMeta
2803
2751
  );
2804
2752
 
2805
- console.log('validIds', validIds);
2806
- console.log('arrayValues', arrayValues);
2807
-
2808
2753
  useEffect(() => {
2809
2754
  const unsubscribe = getGlobalStore
2810
2755
  .getState()
2811
2756
  .subscribeToPath(stateKeyPathKey, (e) => {
2812
- // A data change has occurred for the source array.
2813
- console.log('changed array statelist ', e);
2814
2757
  if (e.type === 'GET_SELECTED') {
2815
2758
  return;
2816
2759
  }
@@ -2832,8 +2775,11 @@ function createProxyHandler<T>(
2832
2775
 
2833
2776
  if (
2834
2777
  e.type === 'INSERT' ||
2778
+ e.type === 'INSERT_MANY' ||
2835
2779
  e.type === 'REMOVE' ||
2836
- e.type === 'CLEAR_SELECTION'
2780
+ e.type === 'CLEAR_SELECTION' ||
2781
+ (e.type === 'SERVER_STATE_UPDATE' &&
2782
+ !meta?.serverStateIsUpStream)
2837
2783
  ) {
2838
2784
  forceUpdate({});
2839
2785
  }
@@ -2856,38 +2802,35 @@ function createProxyHandler<T>(
2856
2802
  componentId: componentId!,
2857
2803
  meta: updatedMeta, // Use updated meta here
2858
2804
  });
2859
- console.log('arrayValues', arrayValues);
2860
2805
 
2861
- return (
2862
- <>
2863
- {arrayValues.map((item, localIndex) => {
2864
- const itemKey = validIds[localIndex];
2806
+ const returnValue = arrayValues.map((item, localIndex) => {
2807
+ const itemKey = validIds[localIndex];
2865
2808
 
2866
- if (!itemKey) {
2867
- return null;
2868
- }
2809
+ if (!itemKey) {
2810
+ return null;
2811
+ }
2869
2812
 
2870
- let itemComponentId = componentIdsRef.current.get(itemKey);
2871
- if (!itemComponentId) {
2872
- itemComponentId = uuidv4();
2873
- componentIdsRef.current.set(itemKey, itemComponentId);
2874
- }
2813
+ let itemComponentId = componentIdsRef.current.get(itemKey);
2814
+ if (!itemComponentId) {
2815
+ itemComponentId = uuidv4();
2816
+ componentIdsRef.current.set(itemKey, itemComponentId);
2817
+ }
2875
2818
 
2876
- const itemPath = [...path, itemKey];
2877
-
2878
- return createElement(MemoizedCogsItemWrapper, {
2879
- key: itemKey,
2880
- stateKey,
2881
- itemComponentId,
2882
- itemPath,
2883
- localIndex,
2884
- arraySetter,
2885
- rebuildStateShape,
2886
- renderFn: callbackfn,
2887
- });
2888
- })}
2889
- </>
2890
- );
2819
+ const itemPath = [...path, itemKey];
2820
+
2821
+ return createElement(MemoizedCogsItemWrapper, {
2822
+ key: itemKey,
2823
+ stateKey,
2824
+ itemComponentId,
2825
+ itemPath,
2826
+ localIndex,
2827
+ arraySetter,
2828
+ rebuildStateShape,
2829
+ renderFn: callbackfn,
2830
+ });
2831
+ });
2832
+
2833
+ return <>{returnValue}</>;
2891
2834
  };
2892
2835
 
2893
2836
  return <StateListWrapper />;
@@ -2896,7 +2839,7 @@ function createProxyHandler<T>(
2896
2839
  if (prop === 'stateFlattenOn') {
2897
2840
  return (fieldName: string) => {
2898
2841
  // FIX: Get the definitive list of IDs for the current view from meta.arrayViews.
2899
- const arrayPathKey = path.join('.');
2842
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2900
2843
  const viewIds = meta?.arrayViews?.[arrayPathKey];
2901
2844
 
2902
2845
  const currentState = getGlobalStore
@@ -2916,7 +2859,7 @@ function createProxyHandler<T>(
2916
2859
  }
2917
2860
  if (prop === 'index') {
2918
2861
  return (index: number) => {
2919
- const arrayPathKey = path.join('.');
2862
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2920
2863
  const viewIds = meta?.arrayViews?.[arrayPathKey];
2921
2864
 
2922
2865
  if (viewIds) {
@@ -2945,26 +2888,17 @@ function createProxyHandler<T>(
2945
2888
  }
2946
2889
  if (prop === 'last') {
2947
2890
  return () => {
2948
- // ✅ FIX: Use getArrayData to get the keys for the current view (filtered or not).
2949
2891
  const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
2950
-
2951
- // If the array is empty, there is no last item.
2952
2892
  if (!currentViewIds || currentViewIds.length === 0) {
2953
2893
  return undefined;
2954
2894
  }
2955
-
2956
- // Get the unique ID of the last item in the current view.
2957
2895
  const lastItemKey = currentViewIds[currentViewIds.length - 1];
2958
2896
 
2959
- // If for some reason the key is invalid, return undefined.
2960
2897
  if (!lastItemKey) {
2961
2898
  return undefined;
2962
2899
  }
2963
-
2964
- // ✅ FIX: The new path uses the item's unique key, not its numerical index.
2965
2900
  const newPath = [...path, lastItemKey];
2966
2901
 
2967
- // Return a new proxy scoped to that specific item.
2968
2902
  return rebuildStateShape({
2969
2903
  path: newPath,
2970
2904
  componentId: componentId!,
@@ -3059,6 +2993,7 @@ function createProxyHandler<T>(
3059
2993
  return;
3060
2994
  }
3061
2995
  const selectedId = selectedItemKey.split('.').pop() as string;
2996
+
3062
2997
  if (!(currentViewIds as any[]).includes(selectedId!)) {
3063
2998
  return;
3064
2999
  }
@@ -3129,17 +3064,14 @@ function createProxyHandler<T>(
3129
3064
  (item) => item?.[searchKey] === searchValue
3130
3065
  );
3131
3066
 
3132
- // FIX: If found, return a proxy to the item by appending its key to the current path.
3133
3067
  if (found) {
3134
3068
  return rebuildStateShape({
3135
- path: [...path, found.key], // e.g., ['itemInstances', 'inst-1', 'properties', 'prop-b']
3069
+ path: [...path, found.key],
3136
3070
  componentId: componentId!,
3137
3071
  meta,
3138
3072
  });
3139
3073
  }
3140
3074
 
3141
- // If not found, return an 'empty' proxy that will resolve to undefined on .get()
3142
- // This prevents "cannot read property 'get' of undefined" errors.
3143
3075
  return rebuildStateShape({
3144
3076
  path: [...path, `not_found_${uuidv4()}`],
3145
3077
  componentId: componentId!,
@@ -3149,7 +3081,8 @@ function createProxyHandler<T>(
3149
3081
  }
3150
3082
  if (prop === 'cutThis') {
3151
3083
  const { value: shadowValue } = getScopedData(stateKey, path, meta);
3152
-
3084
+ const parentPath = path.slice(0, -1);
3085
+ notifySelectionComponents(stateKey, parentPath);
3153
3086
  return () => {
3154
3087
  effectiveSetState(shadowValue, path, { updateType: 'cut' });
3155
3088
  };
@@ -3172,7 +3105,6 @@ function createProxyHandler<T>(
3172
3105
  _meta: meta,
3173
3106
  });
3174
3107
  }
3175
- // in CogsState.ts -> createProxyHandler -> handler -> get
3176
3108
 
3177
3109
  if (prop === '$get') {
3178
3110
  return () =>
@@ -3190,7 +3122,6 @@ function createProxyHandler<T>(
3190
3122
  const parentPathArray = path.slice(0, -1);
3191
3123
  const parentMeta = getShadowMetadata(stateKey, parentPathArray);
3192
3124
 
3193
- // FIX: Check if the parent is an array by looking for arrayKeys in its metadata.
3194
3125
  if (parentMeta?.arrayKeys) {
3195
3126
  const fullParentKey = stateKey + '.' + parentPathArray.join('.');
3196
3127
  const selectedItemKey = getGlobalStore
@@ -3199,14 +3130,11 @@ function createProxyHandler<T>(
3199
3130
 
3200
3131
  const fullItemKey = stateKey + '.' + path.join('.');
3201
3132
 
3202
- // Logic remains the same.
3203
- notifySelectionComponents(stateKey, parentPathArray, undefined);
3204
3133
  return selectedItemKey === fullItemKey;
3205
3134
  }
3206
3135
  return undefined;
3207
3136
  }
3208
3137
 
3209
- // Then use it in both:
3210
3138
  if (prop === 'setSelected') {
3211
3139
  return (value: boolean) => {
3212
3140
  const parentPath = path.slice(0, -1);
@@ -3246,6 +3174,7 @@ function createProxyHandler<T>(
3246
3174
  .getState()
3247
3175
  .setSelectedIndex(fullParentKey, fullItemKey);
3248
3176
  }
3177
+ notifySelectionComponents(stateKey, parentPath);
3249
3178
  };
3250
3179
  }
3251
3180
  if (prop === '_componentId') {
@@ -3423,17 +3352,10 @@ function createProxyHandler<T>(
3423
3352
  if (prop === '_stateKey') return stateKey;
3424
3353
  if (prop === '_path') return path;
3425
3354
  if (prop === 'update') {
3426
- // This method is now greatly simplified.
3427
- // All the complex batching logic has been removed because our new,
3428
- // universal `createEffectiveSetState` function handles it automatically for all operations.
3429
3355
  return (payload: UpdateArg<T>) => {
3430
- // Simply call effectiveSetState. It will automatically handle queuing
3431
- // this operation in the batch for efficient processing.
3356
+ console.log('udpating', payload, path);
3432
3357
  effectiveSetState(payload as any, path, { updateType: 'update' });
3433
3358
 
3434
- // The .synced() method is a useful feature that allows developers
3435
- // to manually mark a piece of state as "synced with the server"
3436
- // after an update. This part of the functionality is preserved.
3437
3359
  return {
3438
3360
  synced: () => {
3439
3361
  const shadowMeta = getGlobalStore
@@ -3628,7 +3550,8 @@ function SignalRenderer({
3628
3550
  const instanceIdRef = useRef<string | null>(null);
3629
3551
  const isSetupRef = useRef(false);
3630
3552
  const signalId = `${proxy._stateKey}-${proxy._path.join('.')}`;
3631
- const arrayPathKey = proxy._path.join('.');
3553
+
3554
+ const arrayPathKey = proxy._path.length > 0 ? proxy._path.join('.') : 'root';
3632
3555
  const viewIds = proxy._meta?.arrayViews?.[arrayPathKey];
3633
3556
 
3634
3557
  const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
@@ -3721,482 +3644,3 @@ function SignalRenderer({
3721
3644
  'data-signal-id': signalId,
3722
3645
  });
3723
3646
  }
3724
-
3725
- const MemoizedCogsItemWrapper = memo(
3726
- ListItemWrapper,
3727
- (prevProps, nextProps) => {
3728
- // Re-render if any of these change:
3729
- return (
3730
- prevProps.itemPath.join('.') === nextProps.itemPath.join('.') &&
3731
- prevProps.stateKey === nextProps.stateKey &&
3732
- prevProps.itemComponentId === nextProps.itemComponentId &&
3733
- prevProps.localIndex === nextProps.localIndex
3734
- );
3735
- }
3736
- );
3737
-
3738
- const useImageLoaded = (ref: RefObject<HTMLElement>): boolean => {
3739
- const [loaded, setLoaded] = useState(false);
3740
-
3741
- useLayoutEffect(() => {
3742
- if (!ref.current) {
3743
- setLoaded(true);
3744
- return;
3745
- }
3746
-
3747
- const images = Array.from(ref.current.querySelectorAll('img'));
3748
-
3749
- // If there are no images, we are "loaded" immediately.
3750
- if (images.length === 0) {
3751
- setLoaded(true);
3752
- return;
3753
- }
3754
-
3755
- let loadedCount = 0;
3756
- const handleImageLoad = () => {
3757
- loadedCount++;
3758
- if (loadedCount === images.length) {
3759
- setLoaded(true);
3760
- }
3761
- };
3762
-
3763
- images.forEach((image) => {
3764
- if (image.complete) {
3765
- handleImageLoad();
3766
- } else {
3767
- image.addEventListener('load', handleImageLoad);
3768
- image.addEventListener('error', handleImageLoad);
3769
- }
3770
- });
3771
-
3772
- return () => {
3773
- images.forEach((image) => {
3774
- image.removeEventListener('load', handleImageLoad);
3775
- image.removeEventListener('error', handleImageLoad);
3776
- });
3777
- };
3778
- }, [ref.current]);
3779
-
3780
- return loaded;
3781
- };
3782
-
3783
- function ListItemWrapper({
3784
- stateKey,
3785
- itemComponentId,
3786
- itemPath,
3787
- localIndex,
3788
- arraySetter,
3789
- rebuildStateShape,
3790
- renderFn,
3791
- }: {
3792
- stateKey: string;
3793
- itemComponentId: string;
3794
- itemPath: string[];
3795
- localIndex: number;
3796
- arraySetter: any;
3797
-
3798
- rebuildStateShape: (options: {
3799
- currentState: any;
3800
- path: string[];
3801
- componentId: string;
3802
- meta?: any;
3803
- }) => any;
3804
- renderFn: (
3805
- setter: any,
3806
- index: number,
3807
-
3808
- arraySetter: any
3809
- ) => React.ReactNode;
3810
- }) {
3811
- const [, forceUpdate] = useState({});
3812
- const { ref: inViewRef, inView } = useInView();
3813
- const elementRef = useRef<HTMLDivElement | null>(null);
3814
-
3815
- const imagesLoaded = useImageLoaded(elementRef);
3816
- const hasReportedInitialHeight = useRef(false);
3817
- const fullKey = [stateKey, ...itemPath].join('.');
3818
- useRegisterComponent(stateKey, itemComponentId, forceUpdate);
3819
-
3820
- const setRefs = useCallback(
3821
- (element: HTMLDivElement | null) => {
3822
- elementRef.current = element;
3823
- inViewRef(element); // This is the ref from useInView
3824
- },
3825
- [inViewRef]
3826
- );
3827
-
3828
- useEffect(() => {
3829
- subscribeToPath(fullKey, (e) => {
3830
- forceUpdate({});
3831
- });
3832
- }, []);
3833
- useEffect(() => {
3834
- if (!inView || !imagesLoaded || hasReportedInitialHeight.current) {
3835
- return;
3836
- }
3837
-
3838
- const element = elementRef.current;
3839
- if (element && element.offsetHeight > 0) {
3840
- hasReportedInitialHeight.current = true;
3841
- const newHeight = element.offsetHeight;
3842
-
3843
- setShadowMetadata(stateKey, itemPath, {
3844
- virtualizer: {
3845
- itemHeight: newHeight,
3846
- domRef: element,
3847
- },
3848
- });
3849
-
3850
- const arrayPath = itemPath.slice(0, -1);
3851
- const arrayPathKey = [stateKey, ...arrayPath].join('.');
3852
- notifyPathSubscribers(arrayPathKey, {
3853
- type: 'ITEMHEIGHT',
3854
- itemKey: itemPath.join('.'),
3855
-
3856
- ref: elementRef.current,
3857
- });
3858
- }
3859
- }, [inView, imagesLoaded, stateKey, itemPath]);
3860
-
3861
- const itemValue = getShadowValue(stateKey, itemPath);
3862
-
3863
- if (itemValue === undefined) {
3864
- return null;
3865
- }
3866
-
3867
- const itemSetter = rebuildStateShape({
3868
- currentState: itemValue,
3869
- path: itemPath,
3870
- componentId: itemComponentId,
3871
- });
3872
- const children = renderFn(itemSetter, localIndex, arraySetter);
3873
-
3874
- return <div ref={setRefs}>{children}</div>;
3875
- }
3876
-
3877
- function FormElementWrapper({
3878
- stateKey,
3879
- path,
3880
- rebuildStateShape,
3881
- renderFn,
3882
- formOpts,
3883
- setState,
3884
- }: {
3885
- stateKey: string;
3886
- path: string[];
3887
- rebuildStateShape: (options: {
3888
- path: string[];
3889
- componentId: string;
3890
- meta?: any;
3891
- }) => any;
3892
- renderFn: (params: FormElementParams<any>) => React.ReactNode;
3893
- formOpts?: FormOptsType;
3894
- setState: any;
3895
- }) {
3896
- const [componentId] = useState(() => uuidv4());
3897
- const [, forceUpdate] = useState({});
3898
-
3899
- const stateKeyPathKey = [stateKey, ...path].join('.');
3900
- useRegisterComponent(stateKey, componentId, forceUpdate);
3901
- const globalStateValue = getShadowValue(stateKey, path);
3902
- const [localValue, setLocalValue] = useState<any>(globalStateValue);
3903
- const isCurrentlyDebouncing = useRef(false);
3904
- const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
3905
-
3906
- useEffect(() => {
3907
- if (
3908
- !isCurrentlyDebouncing.current &&
3909
- !isDeepEqual(globalStateValue, localValue)
3910
- ) {
3911
- setLocalValue(globalStateValue);
3912
- }
3913
- }, [globalStateValue]);
3914
-
3915
- useEffect(() => {
3916
- const unsubscribe = getGlobalStore
3917
- .getState()
3918
- .subscribeToPath(stateKeyPathKey, (newValue) => {
3919
- if (!isCurrentlyDebouncing.current && localValue !== newValue) {
3920
- forceUpdate({});
3921
- }
3922
- });
3923
- return () => {
3924
- unsubscribe();
3925
- if (debounceTimeoutRef.current) {
3926
- clearTimeout(debounceTimeoutRef.current);
3927
- isCurrentlyDebouncing.current = false;
3928
- }
3929
- };
3930
- }, []);
3931
-
3932
- const debouncedUpdate = useCallback(
3933
- (newValue: any) => {
3934
- const currentType = typeof globalStateValue;
3935
- if (currentType === 'number' && typeof newValue === 'string') {
3936
- newValue = newValue === '' ? 0 : Number(newValue);
3937
- }
3938
- setLocalValue(newValue);
3939
- isCurrentlyDebouncing.current = true;
3940
-
3941
- if (debounceTimeoutRef.current) {
3942
- clearTimeout(debounceTimeoutRef.current);
3943
- }
3944
-
3945
- const debounceTime = formOpts?.debounceTime ?? 200;
3946
-
3947
- debounceTimeoutRef.current = setTimeout(() => {
3948
- isCurrentlyDebouncing.current = false;
3949
- setState(newValue, path, { updateType: 'update' });
3950
-
3951
- // NEW: Check if validation is enabled via features
3952
- const rootMeta = getGlobalStore
3953
- .getState()
3954
- .getShadowMetadata(stateKey, []);
3955
- if (!rootMeta?.features?.validationEnabled) return;
3956
-
3957
- const validationOptions = getInitialOptions(stateKey)?.validation;
3958
- const zodSchema =
3959
- validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
3960
-
3961
- if (zodSchema) {
3962
- const fullState = getShadowValue(stateKey, []);
3963
- const result = zodSchema.safeParse(fullState);
3964
- const currentMeta = getShadowMetadata(stateKey, path) || {};
3965
-
3966
- if (!result.success) {
3967
- const errors =
3968
- 'issues' in result.error
3969
- ? result.error.issues
3970
- : (result.error as any).errors;
3971
-
3972
- const pathErrors = errors.filter(
3973
- (error: any) =>
3974
- JSON.stringify(error.path) === JSON.stringify(path)
3975
- );
3976
-
3977
- if (pathErrors.length > 0) {
3978
- setShadowMetadata(stateKey, path, {
3979
- ...currentMeta,
3980
- validation: {
3981
- status: 'INVALID',
3982
- errors: [
3983
- {
3984
- source: 'client',
3985
- message: pathErrors[0]?.message,
3986
- severity: 'warning', // Gentle error during typing
3987
- },
3988
- ],
3989
- lastValidated: Date.now(),
3990
- validatedValue: newValue,
3991
- },
3992
- });
3993
- } else {
3994
- setShadowMetadata(stateKey, path, {
3995
- ...currentMeta,
3996
- validation: {
3997
- status: 'VALID',
3998
- errors: [],
3999
- lastValidated: Date.now(),
4000
- validatedValue: newValue,
4001
- },
4002
- });
4003
- }
4004
- } else {
4005
- setShadowMetadata(stateKey, path, {
4006
- ...currentMeta,
4007
- validation: {
4008
- status: 'VALID',
4009
- errors: [],
4010
- lastValidated: Date.now(),
4011
- validatedValue: newValue,
4012
- },
4013
- });
4014
- }
4015
- }
4016
- }, debounceTime);
4017
- forceUpdate({});
4018
- },
4019
- [setState, path, formOpts?.debounceTime, stateKey]
4020
- );
4021
-
4022
- // --- NEW onBlur HANDLER ---
4023
- // This replaces the old commented-out method with a modern approach.
4024
- const handleBlur = useCallback(async () => {
4025
- console.log('handleBlur triggered');
4026
-
4027
- // Commit any pending changes
4028
- if (debounceTimeoutRef.current) {
4029
- clearTimeout(debounceTimeoutRef.current);
4030
- debounceTimeoutRef.current = null;
4031
- isCurrentlyDebouncing.current = false;
4032
- setState(localValue, path, { updateType: 'update' });
4033
- }
4034
- const rootMeta = getShadowMetadata(stateKey, []);
4035
- if (!rootMeta?.features?.validationEnabled) return;
4036
- const { getInitialOptions } = getGlobalStore.getState();
4037
- const validationOptions = getInitialOptions(stateKey)?.validation;
4038
- const zodSchema =
4039
- validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
4040
-
4041
- if (!zodSchema) return;
4042
-
4043
- // Get the full path including stateKey
4044
-
4045
- // Update validation state to "validating"
4046
- const currentMeta = getShadowMetadata(stateKey, path);
4047
-
4048
- setShadowMetadata(stateKey, path, {
4049
- ...currentMeta,
4050
- validation: {
4051
- status: 'VALIDATING',
4052
- errors: [],
4053
- lastValidated: Date.now(),
4054
- validatedValue: localValue,
4055
- },
4056
- });
4057
-
4058
- // Validate full state
4059
- const fullState = getShadowValue(stateKey, []);
4060
- const result = zodSchema.safeParse(fullState);
4061
- console.log('result ', result);
4062
- if (!result.success) {
4063
- const errors =
4064
- 'issues' in result.error
4065
- ? result.error.issues
4066
- : (result.error as any).errors;
4067
-
4068
- console.log('All validation errors:', errors);
4069
- console.log('Current blur path:', path);
4070
-
4071
- // Find errors for this specific path
4072
- const pathErrors = errors.filter((error: any) => {
4073
- console.log('Processing error:', error);
4074
-
4075
- // For array paths, we need to translate indices to ULIDs
4076
- if (path.some((p) => p.startsWith('id:'))) {
4077
- console.log('Detected array path with ULID');
4078
-
4079
- // This is an array item path like ["id:xyz", "name"]
4080
- const parentPath = path[0]!.startsWith('id:')
4081
- ? []
4082
- : path.slice(0, -1);
4083
-
4084
- console.log('Parent path:', parentPath);
4085
-
4086
- const arrayMeta = getGlobalStore
4087
- .getState()
4088
- .getShadowMetadata(stateKey, parentPath);
4089
-
4090
- console.log('Array metadata:', arrayMeta);
4091
-
4092
- if (arrayMeta?.arrayKeys) {
4093
- const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
4094
- const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
4095
-
4096
- console.log('Item key:', itemKey, 'Index:', itemIndex);
4097
-
4098
- // Compare with Zod path
4099
- const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
4100
- const match =
4101
- JSON.stringify(error.path) === JSON.stringify(zodPath);
4102
-
4103
- console.log('Zod path comparison:', {
4104
- zodPath,
4105
- errorPath: error.path,
4106
- match,
4107
- });
4108
- return match;
4109
- }
4110
- }
4111
-
4112
- const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
4113
- console.log('Direct path comparison:', {
4114
- errorPath: error.path,
4115
- currentPath: path,
4116
- match: directMatch,
4117
- });
4118
- return directMatch;
4119
- });
4120
-
4121
- console.log('Filtered path errors:', pathErrors);
4122
- // Update shadow metadata with validation result
4123
- setShadowMetadata(stateKey, path, {
4124
- ...currentMeta,
4125
- validation: {
4126
- status: 'INVALID',
4127
- errors: pathErrors.map((err: any) => ({
4128
- source: 'client' as const,
4129
- message: err.message,
4130
- severity: 'error' as const, // Hard error on blur
4131
- })),
4132
- lastValidated: Date.now(),
4133
- validatedValue: localValue,
4134
- },
4135
- });
4136
- } else {
4137
- // Validation passed
4138
- setShadowMetadata(stateKey, path, {
4139
- ...currentMeta,
4140
- validation: {
4141
- status: 'VALID',
4142
- errors: [],
4143
- lastValidated: Date.now(),
4144
- validatedValue: localValue,
4145
- },
4146
- });
4147
- }
4148
- forceUpdate({});
4149
- }, [stateKey, path, localValue, setState]);
4150
-
4151
- const baseState = rebuildStateShape({
4152
- path: path,
4153
- componentId: componentId,
4154
- });
4155
-
4156
- const stateWithInputProps = new Proxy(baseState, {
4157
- get(target, prop) {
4158
- if (prop === 'inputProps') {
4159
- return {
4160
- value: localValue ?? '',
4161
- onChange: (e: any) => {
4162
- debouncedUpdate(e.target.value);
4163
- },
4164
- // 5. Wire the new onBlur handler to the input props.
4165
- onBlur: handleBlur,
4166
- ref: formRefStore
4167
- .getState()
4168
- .getFormRef(stateKey + '.' + path.join('.')),
4169
- };
4170
- }
4171
-
4172
- return target[prop];
4173
- },
4174
- });
4175
-
4176
- return (
4177
- <ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
4178
- {renderFn(stateWithInputProps)}
4179
- </ValidationWrapper>
4180
- );
4181
- }
4182
- function useRegisterComponent(
4183
- stateKey: string,
4184
- componentId: string,
4185
- forceUpdate: (o: object) => void
4186
- ) {
4187
- const fullComponentId = `${stateKey}////${componentId}`;
4188
-
4189
- useLayoutEffect(() => {
4190
- // Call the safe, centralized function to register
4191
- registerComponent(stateKey, fullComponentId, {
4192
- forceUpdate: () => forceUpdate({}),
4193
- paths: new Set(),
4194
- reactiveType: ['component'],
4195
- });
4196
-
4197
- // The cleanup now calls the safe, centralized unregister function
4198
- return () => {
4199
- unregisterComponent(stateKey, fullComponentId);
4200
- };
4201
- }, [stateKey, fullComponentId]); // Dependencies are stable and correct
4202
- }