cogsbox-state 0.5.435 → 0.5.437

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
@@ -26,12 +26,18 @@ import { ValidationWrapper } from './Functions.js';
26
26
  import { isDeepEqual, transformStateFunc } from './utility.js';
27
27
  import superjson from 'superjson';
28
28
  import { v4 as uuidv4 } from 'uuid';
29
- import { z } from 'zod';
30
29
 
31
- import { formRefStore, getGlobalStore, type ComponentsType } from './store.js';
30
+ import {
31
+ formRefStore,
32
+ getGlobalStore,
33
+ ValidationStatus,
34
+ type ComponentsType,
35
+ } from './store.js';
32
36
  import { useCogsConfig } from './CogsStateClient.js';
33
37
  import { applyPatch, compare, Operation } from 'fast-json-patch';
34
38
  import { useInView } from 'react-intersection-observer';
39
+ import * as z3 from 'zod/v3';
40
+ import * as z4 from 'zod/v4';
35
41
 
36
42
  type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
37
43
 
@@ -217,12 +223,15 @@ export type FormOptsType = {
217
223
  validation?: {
218
224
  hideMessage?: boolean;
219
225
  message?: string;
220
- stretch?: boolean;
226
+
221
227
  props?: GenericObject;
222
228
  disable?: boolean;
223
229
  };
224
230
 
225
231
  debounceTime?: number;
232
+ sync?: {
233
+ allowInvalidValues?: boolean; // default: false
234
+ };
226
235
  };
227
236
 
228
237
  export type FormControl<T> = (obj: FormElementParams<T>) => JSX.Element;
@@ -248,6 +257,7 @@ export type EndType<T, IsArrayElement = false> = {
248
257
  _stateKey: string;
249
258
  formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
250
259
  get: () => T;
260
+ getState: () => T;
251
261
  $get: () => T;
252
262
  $derive: <R>(fn: EffectFunction<T, R>) => R;
253
263
 
@@ -317,13 +327,17 @@ type EffectiveSetStateArg<
317
327
  ? InsertParams<InferArrayElement<T>>
318
328
  : never
319
329
  : UpdateArg<T>;
330
+ type UpdateOptions = {
331
+ updateType: 'insert' | 'cut' | 'update';
320
332
 
333
+ sync?: boolean;
334
+ };
321
335
  type EffectiveSetState<TStateObject> = (
322
336
  newStateOrFunction:
323
337
  | EffectiveSetStateArg<TStateObject, 'update'>
324
338
  | EffectiveSetStateArg<TStateObject, 'insert'>,
325
339
  path: string[],
326
- updateObj: { updateType: 'update' | 'insert' | 'cut' },
340
+ updateObj: UpdateOptions,
327
341
  validationKey?: string
328
342
  ) => void;
329
343
 
@@ -346,11 +360,6 @@ export type ReactivityType =
346
360
  | 'all'
347
361
  | Array<Prettify<'none' | 'component' | 'deps' | 'all'>>;
348
362
 
349
- type ValidationOptionsType = {
350
- key?: string;
351
- zodSchema?: z.ZodTypeAny;
352
- onBlur?: boolean;
353
- };
354
363
  // Define the return type of the sync hook locally
355
364
  type SyncApi = {
356
365
  updateState: (data: { operation: any }) => void;
@@ -358,6 +367,13 @@ type SyncApi = {
358
367
  clientId: string | null;
359
368
  subscribers: string[];
360
369
  };
370
+ type ValidationOptionsType = {
371
+ key?: string;
372
+ zodSchemaV3?: z3.ZodType<any, any, any>;
373
+ zodSchemaV4?: z4.ZodType<any, any, any>;
374
+
375
+ onBlur?: boolean;
376
+ };
361
377
  export type OptionsType<T extends unknown = unknown> = {
362
378
  log?: boolean;
363
379
  componentId?: string;
@@ -402,7 +418,7 @@ export type OptionsType<T extends unknown = unknown> = {
402
418
  key: string | ((state: T) => string);
403
419
  onChange?: (state: T) => void;
404
420
  };
405
- formElements?: FormsElementsType;
421
+ formElements?: FormsElementsType<T>;
406
422
 
407
423
  reactiveDeps?: (state: T) => any[] | true;
408
424
  reactiveType?: ReactivityType;
@@ -412,15 +428,6 @@ export type OptionsType<T extends unknown = unknown> = {
412
428
  dependencies?: any[];
413
429
  };
414
430
 
415
- export type ValidationWrapperOptions<T extends unknown = unknown> = {
416
- children: React.ReactNode;
417
- active: boolean;
418
- stretch?: boolean;
419
- path: string[];
420
- message?: string;
421
- data?: T;
422
- key?: string;
423
- };
424
431
  export type SyncRenderOptions<T extends unknown = unknown> = {
425
432
  children: React.ReactNode;
426
433
  time: number;
@@ -428,8 +435,16 @@ export type SyncRenderOptions<T extends unknown = unknown> = {
428
435
  key?: string;
429
436
  };
430
437
 
431
- type FormsElementsType<T extends unknown = unknown> = {
432
- validation?: (options: ValidationWrapperOptions<T>) => React.ReactNode;
438
+ type FormsElementsType<T> = {
439
+ validation?: (options: {
440
+ children: React.ReactNode;
441
+ status: ValidationStatus; // Instead of 'active' boolean
442
+
443
+ path: string[];
444
+ message?: string;
445
+ data?: T;
446
+ key?: string;
447
+ }) => React.ReactNode;
433
448
  syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
434
449
  };
435
450
 
@@ -518,15 +533,89 @@ export function addStateOptions<T extends unknown>(
518
533
  ) {
519
534
  return { initialState: initialState, formElements, validation } as T;
520
535
  }
536
+ type UseCogsStateHook<T extends Record<string, any>> = <
537
+ StateKey extends keyof TransformedStateType<T>,
538
+ >(
539
+ stateKey: StateKey,
540
+ options?: Prettify<OptionsType<TransformedStateType<T>[StateKey]>>
541
+ ) => StateObject<TransformedStateType<T>[StateKey]>;
542
+
543
+ // Define the type for the options setter using the Transformed state
544
+ type SetCogsOptionsFunc<T extends Record<string, any>> = <
545
+ StateKey extends keyof TransformedStateType<T>,
546
+ >(
547
+ stateKey: StateKey,
548
+ options: OptionsType<TransformedStateType<T>[StateKey]>
549
+ ) => void;
550
+
551
+ // Define the final API object shape
552
+ type CogsApi<T extends Record<string, any>> = {
553
+ useCogsState: UseCogsStateHook<T>;
554
+ setCogsOptions: SetCogsOptionsFunc<T>;
555
+ };
556
+
557
+ type ExtractStateFromSyncSchema<T> = T extends {
558
+ schemas: infer S;
559
+ notifications: any;
560
+ }
561
+ ? S extends Record<string, any>
562
+ ? {
563
+ [K in keyof S]: S[K] extends { rawSchema: infer R }
564
+ ? R
565
+ : S[K] extends { schemas: { defaults: infer D } }
566
+ ? D
567
+ : never;
568
+ }
569
+ : never
570
+ : never;
571
+
572
+ // Type to extract just the sync schema structure
573
+ type SyncSchemaStructure<T = any> = {
574
+ schemas: Record<
575
+ string,
576
+ {
577
+ rawSchema?: any;
578
+ schemas?: {
579
+ sql?: any;
580
+ client?: any;
581
+ validation?: any;
582
+ defaults?: any;
583
+ };
584
+ api?: {
585
+ initialData?: string;
586
+ update?: string;
587
+ };
588
+ validate?: (data: unknown, ctx: any) => any;
589
+ validateClient?: (data: unknown) => any;
590
+ serializable?: any;
591
+ }
592
+ >;
593
+ notifications: Record<string, (state: any, context: any) => any>;
594
+ };
521
595
  export const createCogsState = <State extends Record<StateKeys, unknown>>(
522
596
  initialState: State,
523
- opt?: { formElements?: FormsElementsType; validation?: ValidationOptionsType }
597
+ opt?: {
598
+ formElements?: FormsElementsType<State>;
599
+ validation?: ValidationOptionsType;
600
+ // Add this flag to indicate it's from sync schema
601
+ __fromSyncSchema?: boolean;
602
+ __syncNotifications?: Record<string, Function>;
603
+ }
524
604
  ) => {
605
+ // Keep ALL your existing code exactly the same
525
606
  let newInitialState = initialState;
526
607
 
527
608
  const [statePart, initialOptionsPart] =
528
609
  transformStateFunc<State>(newInitialState);
529
610
 
611
+ // Only addition - store notifications if provided
612
+ if (opt?.__fromSyncSchema && opt?.__syncNotifications) {
613
+ getGlobalStore
614
+ .getState()
615
+ .setInitialStateOptions('__notifications', opt.__syncNotifications);
616
+ }
617
+ // ... rest of your existing createCogsState code unchanged ...
618
+
530
619
  Object.keys(statePart).forEach((key) => {
531
620
  let existingOptions = initialOptionsPart[key] || {};
532
621
 
@@ -575,12 +664,13 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
575
664
 
576
665
  const useCogsState = <StateKey extends StateKeys>(
577
666
  stateKey: StateKey,
578
- options?: OptionsType<(typeof statePart)[StateKey]>
667
+ options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
579
668
  ) => {
669
+ // ... your existing useCogsState implementation ...
580
670
  const [componentId] = useState(options?.componentId ?? uuidv4());
581
671
  setOptions({
582
672
  stateKey,
583
- options,
673
+ options: options as any,
584
674
  initialOptionsPart,
585
675
  });
586
676
 
@@ -597,7 +687,6 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
597
687
  componentId,
598
688
  localStorage: options?.localStorage,
599
689
  middleware: options?.middleware,
600
-
601
690
  reactiveType: options?.reactiveType,
602
691
  reactiveDeps: options?.reactiveDeps,
603
692
  defaultState: options?.defaultState as any,
@@ -621,9 +710,36 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
621
710
  notifyComponents(stateKey as string);
622
711
  }
623
712
 
624
- return { useCogsState, setCogsOptions };
713
+ return { useCogsState, setCogsOptions } as CogsApi<State>;
625
714
  };
626
715
 
716
+ // Then create a simple helper that extracts state from sync schema
717
+ export function createCogsStateFromSync<
718
+ T extends Record<string, any>,
719
+ >(syncSchema: {
720
+ schemas: any;
721
+ notifications: any;
722
+ }): ReturnType<typeof createCogsState<T>> {
723
+ // Extract initial state
724
+ const initialState = {} as T;
725
+
726
+ for (const key in syncSchema.schemas) {
727
+ const entry = syncSchema.schemas[key];
728
+ if (entry.rawSchema) {
729
+ initialState[key as keyof T] = entry.rawSchema;
730
+ } else if (entry.schemas?.defaults) {
731
+ initialState[key as keyof T] = entry.schemas.defaults;
732
+ } else {
733
+ initialState[key as keyof T] = {} as any;
734
+ }
735
+ }
736
+
737
+ return createCogsState(initialState, {
738
+ __fromSyncSchema: true,
739
+ __syncNotifications: syncSchema.notifications,
740
+ }) as any;
741
+ }
742
+
627
743
  const {
628
744
  getInitialOptions,
629
745
  getValidationErrors,
@@ -1241,9 +1357,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1241
1357
  ...rootMeta,
1242
1358
  components,
1243
1359
  });
1244
-
1245
1360
  forceUpdate({});
1246
-
1247
1361
  return () => {
1248
1362
  const meta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1249
1363
  const component = meta?.components?.get(componentKey);
@@ -1282,8 +1396,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1282
1396
  const effectiveSetState = (
1283
1397
  newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
1284
1398
  path: string[],
1285
- updateObj: { updateType: 'insert' | 'cut' | 'update' },
1286
- validationKey?: string
1399
+ updateObj: UpdateOptions
1287
1400
  ) => {
1288
1401
  const fullPath = [thisKey, ...path].join('.');
1289
1402
  if (Array.isArray(path)) {
@@ -1349,9 +1462,9 @@ export function useCogsStateFn<TStateObject extends unknown>(
1349
1462
  break;
1350
1463
  }
1351
1464
  }
1465
+ const shouldSync = updateObj.sync !== false;
1352
1466
 
1353
- console.log('sdadasdasd', syncApiRef.current, newUpdate);
1354
- if (syncApiRef.current && syncApiRef.current.connected) {
1467
+ if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
1355
1468
  syncApiRef.current.updateState({ operation: newUpdate });
1356
1469
  }
1357
1470
  // Handle signals - reuse shadowMeta from the beginning
@@ -1519,59 +1632,18 @@ export function useCogsStateFn<TStateObject extends unknown>(
1519
1632
  });
1520
1633
  }
1521
1634
  }
1522
- if (
1523
- updateObj.updateType === 'update' &&
1524
- (validationKey || latestInitialOptionsRef.current?.validation?.key) &&
1525
- path
1526
- ) {
1527
- removeValidationError(
1528
- (validationKey || latestInitialOptionsRef.current?.validation?.key) +
1529
- '.' +
1530
- path.join('.')
1531
- );
1532
- }
1533
- const arrayWithoutIndex = path.slice(0, path.length - 1);
1534
- if (
1535
- updateObj.updateType === 'cut' &&
1536
- latestInitialOptionsRef.current?.validation?.key
1537
- ) {
1538
- removeValidationError(
1539
- latestInitialOptionsRef.current?.validation?.key +
1540
- '.' +
1541
- arrayWithoutIndex.join('.')
1542
- );
1543
- }
1544
- if (
1545
- updateObj.updateType === 'insert' &&
1546
- latestInitialOptionsRef.current?.validation?.key
1547
- ) {
1548
- const getValidation = getValidationErrors(
1549
- latestInitialOptionsRef.current?.validation?.key +
1550
- '.' +
1551
- arrayWithoutIndex.join('.')
1552
- );
1553
1635
 
1554
- getValidation.filter((k) => {
1555
- let length = k?.split('.').length;
1556
- const v = ''; // Placeholder as `v` is not used from getValidationErrors
1557
-
1558
- if (
1559
- k == arrayWithoutIndex.join('.') &&
1560
- length == arrayWithoutIndex.length - 1
1561
- ) {
1562
- let newKey = k + '.' + arrayWithoutIndex;
1563
- removeValidationError(k!);
1564
- addValidationError(newKey, v!);
1565
- }
1566
- });
1567
- }
1568
1636
  // Assumes `isDeepEqual` is available in this scope.
1569
1637
  // Assumes `isDeepEqual` is available in this scope.
1570
1638
 
1571
- const newState = store.getShadowValue(thisKey);
1572
- const rootMeta = store.getShadowMetadata(thisKey, []);
1639
+ const newState = getGlobalStore.getState().getShadowValue(thisKey);
1640
+ const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1573
1641
  const notifiedComponents = new Set<string>();
1574
-
1642
+ console.log(
1643
+ 'rootMeta',
1644
+ thisKey,
1645
+ getGlobalStore.getState().shadowStateStore
1646
+ );
1575
1647
  if (!rootMeta?.components) {
1576
1648
  return newState;
1577
1649
  }
@@ -1579,28 +1651,39 @@ export function useCogsStateFn<TStateObject extends unknown>(
1579
1651
  // --- PASS 1: Notify specific subscribers based on update type ---
1580
1652
 
1581
1653
  if (updateObj.updateType === 'update') {
1582
- // ALWAYS notify components subscribed to the exact path being updated.
1583
- // This is the crucial part that handles primitives, as well as updates
1584
- // to an object or array treated as a whole.
1585
- if (shadowMeta?.pathComponents) {
1586
- shadowMeta.pathComponents.forEach((componentId) => {
1587
- // If this component was already notified, skip.
1588
- if (notifiedComponents.has(componentId)) {
1589
- return;
1590
- }
1591
- const component = rootMeta.components?.get(componentId);
1592
- if (component) {
1593
- const reactiveTypes = Array.isArray(component.reactiveType)
1594
- ? component.reactiveType
1595
- : [component.reactiveType || 'component'];
1654
+ // --- Bubble-up Notification ---
1655
+ // When a nested property changes, notify components listening at that exact path,
1656
+ // and also "bubble up" to notify components listening on parent paths.
1657
+ // e.g., an update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
1658
+ let currentPath = [...path]; // Create a mutable copy of the path
1659
+
1660
+ while (true) {
1661
+ const currentPathMeta = store.getShadowMetadata(thisKey, currentPath);
1662
+
1663
+ if (currentPathMeta?.pathComponents) {
1664
+ currentPathMeta.pathComponents.forEach((componentId) => {
1665
+ if (notifiedComponents.has(componentId)) {
1666
+ return; // Avoid sending redundant notifications
1667
+ }
1668
+ const component = rootMeta.components?.get(componentId);
1669
+ if (component) {
1670
+ const reactiveTypes = Array.isArray(component.reactiveType)
1671
+ ? component.reactiveType
1672
+ : [component.reactiveType || 'component'];
1596
1673
 
1597
- // Check if the component has reactivity enabled
1598
- if (!reactiveTypes.includes('none')) {
1599
- component.forceUpdate();
1600
- notifiedComponents.add(componentId);
1674
+ // This notification logic applies to components that depend on object structures.
1675
+ if (!reactiveTypes.includes('none')) {
1676
+ component.forceUpdate();
1677
+ notifiedComponents.add(componentId);
1678
+ }
1601
1679
  }
1602
- }
1603
- });
1680
+ });
1681
+ }
1682
+
1683
+ if (currentPath.length === 0) {
1684
+ break; // We've reached the root, stop bubbling.
1685
+ }
1686
+ currentPath.pop(); // Go up one level for the next iteration.
1604
1687
  }
1605
1688
 
1606
1689
  // ADDITIONALLY, if the payload is an object, perform a deep-check and
@@ -1880,12 +1963,16 @@ const registerComponentDependency = (
1880
1963
  dependencyPath: string[]
1881
1964
  ) => {
1882
1965
  const fullComponentId = `${stateKey}////${componentId}`;
1883
- const rootMeta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
1966
+ const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
1967
+
1968
+ // First, check if the component should even be registered.
1969
+ // This check is safe to do outside the setter.
1970
+ const rootMeta = getShadowMetadata(stateKey, []);
1884
1971
  const component = rootMeta?.components?.get(fullComponentId);
1885
1972
 
1886
1973
  if (
1887
1974
  !component ||
1888
- component.reactiveType == 'none' ||
1975
+ component.reactiveType === 'none' ||
1889
1976
  !(
1890
1977
  Array.isArray(component.reactiveType)
1891
1978
  ? component.reactiveType
@@ -1895,23 +1982,9 @@ const registerComponentDependency = (
1895
1982
  return;
1896
1983
  }
1897
1984
 
1898
- const pathKey = [stateKey, ...dependencyPath].join('.');
1899
-
1900
- // Add to component's paths (existing logic)
1901
- component.paths.add(pathKey);
1902
-
1903
- // NEW: Also store componentId at the path level
1904
- const pathMeta =
1905
- getGlobalStore.getState().getShadowMetadata(stateKey, dependencyPath) || {};
1906
- const pathComponents = pathMeta.pathComponents || new Set<string>();
1907
- pathComponents.add(fullComponentId);
1908
-
1909
- getGlobalStore.getState().setShadowMetadata(stateKey, dependencyPath, {
1910
- ...pathMeta,
1911
- pathComponents,
1912
- });
1985
+ // Now, call the single, safe, atomic function to perform the update.
1986
+ addPathComponent(stateKey, dependencyPath, fullComponentId);
1913
1987
  };
1914
-
1915
1988
  const notifySelectionComponents = (
1916
1989
  stateKey: string,
1917
1990
  parentPath: string[],
@@ -2184,13 +2257,16 @@ function createProxyHandler<T>(
2184
2257
  }
2185
2258
  if (prop === 'showValidationErrors') {
2186
2259
  return () => {
2187
- const init = getGlobalStore
2188
- .getState()
2189
- .getInitialOptions(stateKey)?.validation;
2190
- if (!init?.key) throw new Error('Validation key not found');
2191
- return getGlobalStore
2260
+ const meta = getGlobalStore
2192
2261
  .getState()
2193
- .getValidationErrors(init.key + '.' + path.join('.'));
2262
+ .getShadowMetadata(stateKey, path);
2263
+ if (
2264
+ meta?.validation?.status === 'VALIDATION_FAILED' &&
2265
+ meta.validation.message
2266
+ ) {
2267
+ return [meta.validation.message];
2268
+ }
2269
+ return [];
2194
2270
  };
2195
2271
  }
2196
2272
  if (Array.isArray(currentState)) {
@@ -2988,7 +3064,7 @@ function createProxyHandler<T>(
2988
3064
  arrayValues: freshValues || [],
2989
3065
  };
2990
3066
  }, [cacheKey, updateTrigger]);
2991
- console.log('freshValues', validIds, arrayValues);
3067
+
2992
3068
  useEffect(() => {
2993
3069
  const unsubscribe = getGlobalStore
2994
3070
  .getState()
@@ -3019,7 +3095,6 @@ function createProxyHandler<T>(
3019
3095
  e.type === 'REMOVE' ||
3020
3096
  e.type === 'CLEAR_SELECTION'
3021
3097
  ) {
3022
- console.log('sssssssssssssssssssssssssssss', e);
3023
3098
  forceUpdate({});
3024
3099
  }
3025
3100
  });
@@ -3044,7 +3119,7 @@ function createProxyHandler<T>(
3044
3119
  validIds: validIds,
3045
3120
  },
3046
3121
  });
3047
- console.log('sssssssssssssssssssssssssssss', arraySetter);
3122
+
3048
3123
  return (
3049
3124
  <>
3050
3125
  {arrayValues.map((item, localIndex) => {
@@ -3221,15 +3296,12 @@ function createProxyHandler<T>(
3221
3296
  }
3222
3297
  if (prop === 'cutSelected') {
3223
3298
  return () => {
3224
- const baseArrayKeys =
3225
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
3226
- ?.arrayKeys || [];
3227
3299
  const validKeys = applyTransforms(
3228
3300
  stateKey,
3229
3301
  path,
3230
3302
  meta?.transforms
3231
3303
  );
3232
- console.log('validKeys', validKeys);
3304
+
3233
3305
  if (!validKeys || validKeys.length === 0) return;
3234
3306
 
3235
3307
  const indexKeyToCut = getGlobalStore
@@ -3239,13 +3311,17 @@ function createProxyHandler<T>(
3239
3311
  let indexToCut = validKeys.findIndex(
3240
3312
  (key) => key === indexKeyToCut
3241
3313
  );
3242
- console.log('indexToCut', indexToCut);
3314
+
3243
3315
  const pathForCut = validKeys[
3244
3316
  indexToCut == -1 ? validKeys.length - 1 : indexToCut
3245
3317
  ]
3246
3318
  ?.split('.')
3247
3319
  .slice(1);
3248
- console.log('pathForCut', pathForCut);
3320
+ getGlobalStore
3321
+ .getState()
3322
+ .clearSelectedIndex({ arrayKey: stateKeyPathKey });
3323
+ const parentPath = pathForCut?.slice(0, -1)!;
3324
+ notifySelectionComponents(stateKey, parentPath);
3249
3325
  effectiveSetState(currentState, pathForCut!, {
3250
3326
  updateType: 'cut',
3251
3327
  });
@@ -3371,6 +3447,14 @@ function createProxyHandler<T>(
3371
3447
  .getShadowValue(stateKeyPathKey, meta?.validIds);
3372
3448
  };
3373
3449
  }
3450
+ if (prop === 'getState') {
3451
+ return () => {
3452
+ return getGlobalStore
3453
+ .getState()
3454
+ .getShadowValue(stateKeyPathKey, meta?.validIds);
3455
+ };
3456
+ }
3457
+
3374
3458
  if (prop === '$derive') {
3375
3459
  return (fn: any) =>
3376
3460
  $cogsSignal({
@@ -3481,8 +3565,9 @@ function createProxyHandler<T>(
3481
3565
  }
3482
3566
  if (prop === 'applyJsonPatch') {
3483
3567
  return (patches: Operation[]) => {
3484
- // 1. Get the current state object that the proxy points to.
3485
3568
  const store = getGlobalStore.getState();
3569
+ const rootMeta = store.getShadowMetadata(stateKey, []);
3570
+ if (!rootMeta?.components) return;
3486
3571
 
3487
3572
  const convertPath = (jsonPath: string): string[] => {
3488
3573
  if (!jsonPath || jsonPath === '/') return [];
@@ -3492,6 +3577,8 @@ function createProxyHandler<T>(
3492
3577
  .map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
3493
3578
  };
3494
3579
 
3580
+ const notifiedComponents = new Set<string>();
3581
+
3495
3582
  for (const patch of patches) {
3496
3583
  const relativePath = convertPath(patch.path);
3497
3584
 
@@ -3504,18 +3591,64 @@ function createProxyHandler<T>(
3504
3591
  value: any;
3505
3592
  };
3506
3593
  store.updateShadowAtPath(stateKey, relativePath, value);
3507
- store.markAsDirty(stateKey, relativePath, { bubble: true }); // Or handle status differently for server patches
3594
+ store.markAsDirty(stateKey, relativePath, { bubble: true });
3595
+
3596
+ // Bubble up - notify components at this path and all parent paths
3597
+ let currentPath = [...relativePath];
3598
+ while (true) {
3599
+ const pathMeta = store.getShadowMetadata(
3600
+ stateKey,
3601
+ currentPath
3602
+ );
3603
+ console.log('pathMeta', pathMeta);
3604
+ if (pathMeta?.pathComponents) {
3605
+ pathMeta.pathComponents.forEach((componentId) => {
3606
+ if (!notifiedComponents.has(componentId)) {
3607
+ const component =
3608
+ rootMeta.components?.get(componentId);
3609
+ if (component) {
3610
+ component.forceUpdate();
3611
+ notifiedComponents.add(componentId);
3612
+ }
3613
+ }
3614
+ });
3615
+ }
3616
+
3617
+ if (currentPath.length === 0) break;
3618
+ currentPath.pop(); // Go up one level
3619
+ }
3508
3620
  break;
3509
3621
  }
3510
3622
  case 'remove': {
3511
- store.removeShadowArrayElement(stateKey, relativePath);
3512
3623
  const parentPath = relativePath.slice(0, -1);
3624
+ store.removeShadowArrayElement(stateKey, relativePath);
3513
3625
  store.markAsDirty(stateKey, parentPath, { bubble: true });
3626
+
3627
+ // Bubble up from parent path
3628
+ let currentPath = [...parentPath];
3629
+ while (true) {
3630
+ const pathMeta = store.getShadowMetadata(
3631
+ stateKey,
3632
+ currentPath
3633
+ );
3634
+ if (pathMeta?.pathComponents) {
3635
+ pathMeta.pathComponents.forEach((componentId) => {
3636
+ if (!notifiedComponents.has(componentId)) {
3637
+ const component =
3638
+ rootMeta.components?.get(componentId);
3639
+ if (component) {
3640
+ component.forceUpdate();
3641
+ notifiedComponents.add(componentId);
3642
+ }
3643
+ }
3644
+ });
3645
+ }
3646
+
3647
+ if (currentPath.length === 0) break;
3648
+ currentPath.pop();
3649
+ }
3514
3650
  break;
3515
3651
  }
3516
- // NOTE: 'move' and 'copy' operations should be deconstructed into 'remove' and 'add'
3517
- // by the server before broadcasting for maximum compatibility. Your server's use
3518
- // of `compare()` already does this, so we don't need to handle them here.
3519
3652
  }
3520
3653
  }
3521
3654
  };
@@ -3525,20 +3658,40 @@ function createProxyHandler<T>(
3525
3658
  const init = getGlobalStore
3526
3659
  .getState()
3527
3660
  .getInitialOptions(stateKey)?.validation;
3528
- if (!init?.zodSchema || !init?.key)
3529
- throw new Error('Zod schema or validation key not found');
3661
+
3662
+ // UPDATED: Select v4 schema, with a fallback to v3
3663
+ const zodSchema = init?.zodSchemaV4 || init?.zodSchemaV3;
3664
+
3665
+ if (!zodSchema || !init?.key) {
3666
+ throw new Error(
3667
+ 'Zod schema (v3 or v4) or validation key not found'
3668
+ );
3669
+ }
3530
3670
 
3531
3671
  removeValidationError(init.key);
3532
3672
  const thisObject = getGlobalStore
3533
3673
  .getState()
3534
3674
  .getShadowValue(stateKey);
3535
- const result = init.zodSchema.safeParse(thisObject);
3675
+
3676
+ // Use the selected schema for parsing
3677
+ const result = zodSchema.safeParse(thisObject);
3536
3678
 
3537
3679
  if (!result.success) {
3538
- result.error.errors.forEach((error) => {
3539
- const fullErrorPath = [init.key, ...error.path].join('.');
3540
- addValidationError(fullErrorPath, error.message);
3541
- });
3680
+ // This logic already handles both v3 and v4 error types correctly
3681
+ if ('issues' in result.error) {
3682
+ // Zod v4 error
3683
+ result.error.issues.forEach((error) => {
3684
+ const fullErrorPath = [init.key, ...error.path].join('.');
3685
+ addValidationError(fullErrorPath, error.message);
3686
+ });
3687
+ } else {
3688
+ // Zod v3 error
3689
+ (result.error as any).errors.forEach((error: any) => {
3690
+ const fullErrorPath = [init.key, ...error.path].join('.');
3691
+ addValidationError(fullErrorPath, error.message);
3692
+ });
3693
+ }
3694
+
3542
3695
  notifyComponents(stateKey);
3543
3696
  return false;
3544
3697
  }
@@ -3631,20 +3784,14 @@ function createProxyHandler<T>(
3631
3784
  if (prop === 'formElement') {
3632
3785
  return (child: FormControl<T>, formOpts?: FormOptsType) => {
3633
3786
  return (
3634
- <ValidationWrapper
3635
- formOpts={formOpts}
3636
- path={path}
3787
+ <FormElementWrapper
3637
3788
  stateKey={stateKey}
3638
- >
3639
- <FormElementWrapper
3640
- stateKey={stateKey}
3641
- path={path}
3642
- rebuildStateShape={rebuildStateShape}
3643
- setState={effectiveSetState}
3644
- formOpts={formOpts}
3645
- renderFn={child as any}
3646
- />
3647
- </ValidationWrapper>
3789
+ path={path}
3790
+ rebuildStateShape={rebuildStateShape}
3791
+ setState={effectiveSetState}
3792
+ formOpts={formOpts}
3793
+ renderFn={child as any}
3794
+ />
3648
3795
  );
3649
3796
  };
3650
3797
  }
@@ -4203,6 +4350,7 @@ function ListItemWrapper({
4203
4350
 
4204
4351
  return <div ref={setRefs}>{children}</div>;
4205
4352
  }
4353
+
4206
4354
  function FormElementWrapper({
4207
4355
  stateKey,
4208
4356
  path,
@@ -4248,7 +4396,9 @@ function FormElementWrapper({
4248
4396
  const unsubscribe = getGlobalStore
4249
4397
  .getState()
4250
4398
  .subscribeToPath(stateKeyPathKey, (newValue) => {
4251
- forceUpdate({});
4399
+ if (!isCurrentlyDebouncing.current && localValue !== newValue) {
4400
+ forceUpdate({});
4401
+ }
4252
4402
  });
4253
4403
  return () => {
4254
4404
  unsubscribe();
@@ -4261,6 +4411,10 @@ function FormElementWrapper({
4261
4411
 
4262
4412
  const debouncedUpdate = useCallback(
4263
4413
  (newValue: any) => {
4414
+ const currentType = typeof globalStateValue;
4415
+ if (currentType === 'number' && typeof newValue === 'string') {
4416
+ newValue = newValue === '' ? 0 : Number(newValue);
4417
+ }
4264
4418
  setLocalValue(newValue);
4265
4419
  isCurrentlyDebouncing.current = true;
4266
4420
 
@@ -4272,19 +4426,188 @@ function FormElementWrapper({
4272
4426
 
4273
4427
  debounceTimeoutRef.current = setTimeout(() => {
4274
4428
  isCurrentlyDebouncing.current = false;
4429
+
4430
+ // Update state
4275
4431
  setState(newValue, path, { updateType: 'update' });
4432
+
4433
+ // Perform LIVE validation (gentle)
4434
+ const { getInitialOptions, setShadowMetadata, getShadowMetadata } =
4435
+ getGlobalStore.getState();
4436
+ const validationOptions = getInitialOptions(stateKey)?.validation;
4437
+ const zodSchema =
4438
+ validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
4439
+
4440
+ if (zodSchema) {
4441
+ const fullState = getGlobalStore.getState().getShadowValue(stateKey);
4442
+ const result = zodSchema.safeParse(fullState);
4443
+
4444
+ const currentMeta = getShadowMetadata(stateKey, path) || {};
4445
+
4446
+ if (!result.success) {
4447
+ const errors =
4448
+ 'issues' in result.error
4449
+ ? result.error.issues
4450
+ : (result.error as any).errors;
4451
+ const pathErrors = errors.filter(
4452
+ (error: any) =>
4453
+ JSON.stringify(error.path) === JSON.stringify(path)
4454
+ );
4455
+
4456
+ if (pathErrors.length > 0) {
4457
+ setShadowMetadata(stateKey, path, {
4458
+ ...currentMeta,
4459
+ validation: {
4460
+ status: 'INVALID_LIVE',
4461
+ message: pathErrors[0]?.message,
4462
+ validatedValue: newValue,
4463
+ },
4464
+ });
4465
+ } else {
4466
+ // This field has no errors - clear validation
4467
+ setShadowMetadata(stateKey, path, {
4468
+ ...currentMeta,
4469
+ validation: {
4470
+ status: 'VALID_LIVE',
4471
+ validatedValue: newValue,
4472
+ },
4473
+ });
4474
+ }
4475
+ } else {
4476
+ // Validation passed - clear any existing errors
4477
+ setShadowMetadata(stateKey, path, {
4478
+ ...currentMeta,
4479
+ validation: {
4480
+ status: 'VALID_LIVE',
4481
+ validatedValue: newValue,
4482
+ },
4483
+ });
4484
+ }
4485
+ }
4276
4486
  }, debounceTime);
4487
+ forceUpdate({});
4277
4488
  },
4278
- [setState, path, formOpts?.debounceTime]
4489
+ [setState, path, formOpts?.debounceTime, stateKey]
4279
4490
  );
4280
4491
 
4281
- const immediateUpdate = useCallback(() => {
4492
+ // --- NEW onBlur HANDLER ---
4493
+ // This replaces the old commented-out method with a modern approach.
4494
+ const handleBlur = useCallback(async () => {
4495
+ console.log('handleBlur triggered');
4496
+
4497
+ // Commit any pending changes
4282
4498
  if (debounceTimeoutRef.current) {
4283
4499
  clearTimeout(debounceTimeoutRef.current);
4500
+ debounceTimeoutRef.current = null;
4284
4501
  isCurrentlyDebouncing.current = false;
4285
4502
  setState(localValue, path, { updateType: 'update' });
4286
4503
  }
4287
- }, [setState, path, localValue]);
4504
+
4505
+ const { getInitialOptions } = getGlobalStore.getState();
4506
+ const validationOptions = getInitialOptions(stateKey)?.validation;
4507
+ const zodSchema =
4508
+ validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
4509
+
4510
+ if (!zodSchema) return;
4511
+
4512
+ // Get the full path including stateKey
4513
+
4514
+ // Update validation state to "validating"
4515
+ const currentMeta = getGlobalStore
4516
+ .getState()
4517
+ .getShadowMetadata(stateKey, path);
4518
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4519
+ ...currentMeta,
4520
+ validation: {
4521
+ status: 'DIRTY',
4522
+ validatedValue: localValue,
4523
+ },
4524
+ });
4525
+
4526
+ // Validate full state
4527
+ const fullState = getGlobalStore.getState().getShadowValue(stateKey);
4528
+ const result = zodSchema.safeParse(fullState);
4529
+ console.log('result ', result);
4530
+ if (!result.success) {
4531
+ const errors =
4532
+ 'issues' in result.error
4533
+ ? result.error.issues
4534
+ : (result.error as any).errors;
4535
+
4536
+ console.log('All validation errors:', errors);
4537
+ console.log('Current blur path:', path);
4538
+
4539
+ // Find errors for this specific path
4540
+ const pathErrors = errors.filter((error: any) => {
4541
+ console.log('Processing error:', error);
4542
+
4543
+ // For array paths, we need to translate indices to ULIDs
4544
+ if (path.some((p) => p.startsWith('id:'))) {
4545
+ console.log('Detected array path with ULID');
4546
+
4547
+ // This is an array item path like ["id:xyz", "name"]
4548
+ const parentPath = path[0]!.startsWith('id:')
4549
+ ? []
4550
+ : path.slice(0, -1);
4551
+
4552
+ console.log('Parent path:', parentPath);
4553
+
4554
+ const arrayMeta = getGlobalStore
4555
+ .getState()
4556
+ .getShadowMetadata(stateKey, parentPath);
4557
+
4558
+ console.log('Array metadata:', arrayMeta);
4559
+
4560
+ if (arrayMeta?.arrayKeys) {
4561
+ const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
4562
+ const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
4563
+
4564
+ console.log('Item key:', itemKey, 'Index:', itemIndex);
4565
+
4566
+ // Compare with Zod path
4567
+ const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
4568
+ const match =
4569
+ JSON.stringify(error.path) === JSON.stringify(zodPath);
4570
+
4571
+ console.log('Zod path comparison:', {
4572
+ zodPath,
4573
+ errorPath: error.path,
4574
+ match,
4575
+ });
4576
+ return match;
4577
+ }
4578
+ }
4579
+
4580
+ const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
4581
+ console.log('Direct path comparison:', {
4582
+ errorPath: error.path,
4583
+ currentPath: path,
4584
+ match: directMatch,
4585
+ });
4586
+ return directMatch;
4587
+ });
4588
+
4589
+ console.log('Filtered path errors:', pathErrors);
4590
+ // Update shadow metadata with validation result
4591
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4592
+ ...currentMeta,
4593
+ validation: {
4594
+ status: 'VALIDATION_FAILED',
4595
+ message: pathErrors[0]?.message,
4596
+ validatedValue: localValue,
4597
+ },
4598
+ });
4599
+ } else {
4600
+ // Validation passed
4601
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4602
+ ...currentMeta,
4603
+ validation: {
4604
+ status: 'VALID_PENDING_SYNC',
4605
+ validatedValue: localValue,
4606
+ },
4607
+ });
4608
+ }
4609
+ forceUpdate({});
4610
+ }, [stateKey, path, localValue, setState]);
4288
4611
 
4289
4612
  const baseState = rebuildStateShape({
4290
4613
  currentState: globalStateValue,
@@ -4300,7 +4623,8 @@ function FormElementWrapper({
4300
4623
  onChange: (e: any) => {
4301
4624
  debouncedUpdate(e.target.value);
4302
4625
  },
4303
- onBlur: immediateUpdate,
4626
+ // 5. Wire the new onBlur handler to the input props.
4627
+ onBlur: handleBlur,
4304
4628
  ref: formRefStore
4305
4629
  .getState()
4306
4630
  .getFormRef(stateKey + '.' + path.join('.')),
@@ -4311,9 +4635,12 @@ function FormElementWrapper({
4311
4635
  },
4312
4636
  });
4313
4637
 
4314
- return <>{renderFn(stateWithInputProps)}</>;
4638
+ return (
4639
+ <ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
4640
+ {renderFn(stateWithInputProps)}
4641
+ </ValidationWrapper>
4642
+ );
4315
4643
  }
4316
-
4317
4644
  function useRegisterComponent(
4318
4645
  stateKey: string,
4319
4646
  componentId: string,
@@ -4322,25 +4649,19 @@ function useRegisterComponent(
4322
4649
  const fullComponentId = `${stateKey}////${componentId}`;
4323
4650
 
4324
4651
  useLayoutEffect(() => {
4325
- const rootMeta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
4326
- const components = rootMeta?.components || new Map();
4652
+ const { registerComponent, unregisterComponent } =
4653
+ getGlobalStore.getState();
4327
4654
 
4328
- components.set(fullComponentId, {
4655
+ // Call the safe, centralized function to register
4656
+ registerComponent(stateKey, fullComponentId, {
4329
4657
  forceUpdate: () => forceUpdate({}),
4330
4658
  paths: new Set(),
4331
4659
  reactiveType: ['component'],
4332
4660
  });
4333
4661
 
4334
- getGlobalStore.getState().setShadowMetadata(stateKey, [], {
4335
- ...rootMeta,
4336
- components,
4337
- });
4338
-
4662
+ // The cleanup now calls the safe, centralized unregister function
4339
4663
  return () => {
4340
- const meta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
4341
- if (meta?.components) {
4342
- meta.components.delete(fullComponentId);
4343
- }
4664
+ unregisterComponent(stateKey, fullComponentId);
4344
4665
  };
4345
- }, [stateKey, fullComponentId]);
4666
+ }, [stateKey, fullComponentId]); // Dependencies are stable and correct
4346
4667
  }