cogsbox-state 0.5.435 → 0.5.436

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,9 +533,32 @@ 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
+ };
521
556
  export const createCogsState = <State extends Record<StateKeys, unknown>>(
522
557
  initialState: State,
523
- opt?: { formElements?: FormsElementsType; validation?: ValidationOptionsType }
558
+ opt?: {
559
+ formElements?: FormsElementsType<State>;
560
+ validation?: ValidationOptionsType;
561
+ }
524
562
  ) => {
525
563
  let newInitialState = initialState;
526
564
 
@@ -575,12 +613,12 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
575
613
 
576
614
  const useCogsState = <StateKey extends StateKeys>(
577
615
  stateKey: StateKey,
578
- options?: OptionsType<(typeof statePart)[StateKey]>
616
+ options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
579
617
  ) => {
580
618
  const [componentId] = useState(options?.componentId ?? uuidv4());
581
619
  setOptions({
582
620
  stateKey,
583
- options,
621
+ options: options as any,
584
622
  initialOptionsPart,
585
623
  });
586
624
 
@@ -621,7 +659,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
621
659
  notifyComponents(stateKey as string);
622
660
  }
623
661
 
624
- return { useCogsState, setCogsOptions };
662
+ return { useCogsState, setCogsOptions } as CogsApi<State>;
625
663
  };
626
664
 
627
665
  const {
@@ -1241,9 +1279,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1241
1279
  ...rootMeta,
1242
1280
  components,
1243
1281
  });
1244
-
1245
1282
  forceUpdate({});
1246
-
1247
1283
  return () => {
1248
1284
  const meta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1249
1285
  const component = meta?.components?.get(componentKey);
@@ -1282,8 +1318,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1282
1318
  const effectiveSetState = (
1283
1319
  newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
1284
1320
  path: string[],
1285
- updateObj: { updateType: 'insert' | 'cut' | 'update' },
1286
- validationKey?: string
1321
+ updateObj: UpdateOptions
1287
1322
  ) => {
1288
1323
  const fullPath = [thisKey, ...path].join('.');
1289
1324
  if (Array.isArray(path)) {
@@ -1349,9 +1384,9 @@ export function useCogsStateFn<TStateObject extends unknown>(
1349
1384
  break;
1350
1385
  }
1351
1386
  }
1387
+ const shouldSync = updateObj.sync !== false;
1352
1388
 
1353
- console.log('sdadasdasd', syncApiRef.current, newUpdate);
1354
- if (syncApiRef.current && syncApiRef.current.connected) {
1389
+ if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
1355
1390
  syncApiRef.current.updateState({ operation: newUpdate });
1356
1391
  }
1357
1392
  // Handle signals - reuse shadowMeta from the beginning
@@ -1519,59 +1554,18 @@ export function useCogsStateFn<TStateObject extends unknown>(
1519
1554
  });
1520
1555
  }
1521
1556
  }
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
-
1554
- getValidation.filter((k) => {
1555
- let length = k?.split('.').length;
1556
- const v = ''; // Placeholder as `v` is not used from getValidationErrors
1557
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
1558
  // Assumes `isDeepEqual` is available in this scope.
1569
1559
  // Assumes `isDeepEqual` is available in this scope.
1570
1560
 
1571
- const newState = store.getShadowValue(thisKey);
1572
- const rootMeta = store.getShadowMetadata(thisKey, []);
1561
+ const newState = getGlobalStore.getState().getShadowValue(thisKey);
1562
+ const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1573
1563
  const notifiedComponents = new Set<string>();
1574
-
1564
+ console.log(
1565
+ 'rootMeta',
1566
+ thisKey,
1567
+ getGlobalStore.getState().shadowStateStore
1568
+ );
1575
1569
  if (!rootMeta?.components) {
1576
1570
  return newState;
1577
1571
  }
@@ -1579,28 +1573,39 @@ export function useCogsStateFn<TStateObject extends unknown>(
1579
1573
  // --- PASS 1: Notify specific subscribers based on update type ---
1580
1574
 
1581
1575
  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'];
1576
+ // --- Bubble-up Notification ---
1577
+ // When a nested property changes, notify components listening at that exact path,
1578
+ // and also "bubble up" to notify components listening on parent paths.
1579
+ // e.g., an update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
1580
+ let currentPath = [...path]; // Create a mutable copy of the path
1581
+
1582
+ while (true) {
1583
+ const currentPathMeta = store.getShadowMetadata(thisKey, currentPath);
1584
+
1585
+ if (currentPathMeta?.pathComponents) {
1586
+ currentPathMeta.pathComponents.forEach((componentId) => {
1587
+ if (notifiedComponents.has(componentId)) {
1588
+ return; // Avoid sending redundant notifications
1589
+ }
1590
+ const component = rootMeta.components?.get(componentId);
1591
+ if (component) {
1592
+ const reactiveTypes = Array.isArray(component.reactiveType)
1593
+ ? component.reactiveType
1594
+ : [component.reactiveType || 'component'];
1596
1595
 
1597
- // Check if the component has reactivity enabled
1598
- if (!reactiveTypes.includes('none')) {
1599
- component.forceUpdate();
1600
- notifiedComponents.add(componentId);
1596
+ // This notification logic applies to components that depend on object structures.
1597
+ if (!reactiveTypes.includes('none')) {
1598
+ component.forceUpdate();
1599
+ notifiedComponents.add(componentId);
1600
+ }
1601
1601
  }
1602
- }
1603
- });
1602
+ });
1603
+ }
1604
+
1605
+ if (currentPath.length === 0) {
1606
+ break; // We've reached the root, stop bubbling.
1607
+ }
1608
+ currentPath.pop(); // Go up one level for the next iteration.
1604
1609
  }
1605
1610
 
1606
1611
  // ADDITIONALLY, if the payload is an object, perform a deep-check and
@@ -1880,12 +1885,16 @@ const registerComponentDependency = (
1880
1885
  dependencyPath: string[]
1881
1886
  ) => {
1882
1887
  const fullComponentId = `${stateKey}////${componentId}`;
1883
- const rootMeta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
1888
+ const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
1889
+
1890
+ // First, check if the component should even be registered.
1891
+ // This check is safe to do outside the setter.
1892
+ const rootMeta = getShadowMetadata(stateKey, []);
1884
1893
  const component = rootMeta?.components?.get(fullComponentId);
1885
1894
 
1886
1895
  if (
1887
1896
  !component ||
1888
- component.reactiveType == 'none' ||
1897
+ component.reactiveType === 'none' ||
1889
1898
  !(
1890
1899
  Array.isArray(component.reactiveType)
1891
1900
  ? component.reactiveType
@@ -1895,23 +1904,9 @@ const registerComponentDependency = (
1895
1904
  return;
1896
1905
  }
1897
1906
 
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
- });
1907
+ // Now, call the single, safe, atomic function to perform the update.
1908
+ addPathComponent(stateKey, dependencyPath, fullComponentId);
1913
1909
  };
1914
-
1915
1910
  const notifySelectionComponents = (
1916
1911
  stateKey: string,
1917
1912
  parentPath: string[],
@@ -2184,13 +2179,16 @@ function createProxyHandler<T>(
2184
2179
  }
2185
2180
  if (prop === 'showValidationErrors') {
2186
2181
  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
2182
+ const meta = getGlobalStore
2192
2183
  .getState()
2193
- .getValidationErrors(init.key + '.' + path.join('.'));
2184
+ .getShadowMetadata(stateKey, path);
2185
+ if (
2186
+ meta?.validation?.status === 'VALIDATION_FAILED' &&
2187
+ meta.validation.message
2188
+ ) {
2189
+ return [meta.validation.message];
2190
+ }
2191
+ return [];
2194
2192
  };
2195
2193
  }
2196
2194
  if (Array.isArray(currentState)) {
@@ -3019,7 +3017,6 @@ function createProxyHandler<T>(
3019
3017
  e.type === 'REMOVE' ||
3020
3018
  e.type === 'CLEAR_SELECTION'
3021
3019
  ) {
3022
- console.log('sssssssssssssssssssssssssssss', e);
3023
3020
  forceUpdate({});
3024
3021
  }
3025
3022
  });
@@ -3221,15 +3218,12 @@ function createProxyHandler<T>(
3221
3218
  }
3222
3219
  if (prop === 'cutSelected') {
3223
3220
  return () => {
3224
- const baseArrayKeys =
3225
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
3226
- ?.arrayKeys || [];
3227
3221
  const validKeys = applyTransforms(
3228
3222
  stateKey,
3229
3223
  path,
3230
3224
  meta?.transforms
3231
3225
  );
3232
- console.log('validKeys', validKeys);
3226
+
3233
3227
  if (!validKeys || validKeys.length === 0) return;
3234
3228
 
3235
3229
  const indexKeyToCut = getGlobalStore
@@ -3239,13 +3233,15 @@ function createProxyHandler<T>(
3239
3233
  let indexToCut = validKeys.findIndex(
3240
3234
  (key) => key === indexKeyToCut
3241
3235
  );
3242
- console.log('indexToCut', indexToCut);
3236
+
3243
3237
  const pathForCut = validKeys[
3244
3238
  indexToCut == -1 ? validKeys.length - 1 : indexToCut
3245
3239
  ]
3246
3240
  ?.split('.')
3247
3241
  .slice(1);
3248
- console.log('pathForCut', pathForCut);
3242
+ getGlobalStore
3243
+ .getState()
3244
+ .clearSelectedIndex({ arrayKey: stateKeyPathKey });
3249
3245
  effectiveSetState(currentState, pathForCut!, {
3250
3246
  updateType: 'cut',
3251
3247
  });
@@ -3371,6 +3367,14 @@ function createProxyHandler<T>(
3371
3367
  .getShadowValue(stateKeyPathKey, meta?.validIds);
3372
3368
  };
3373
3369
  }
3370
+ if (prop === 'getState') {
3371
+ return () => {
3372
+ return getGlobalStore
3373
+ .getState()
3374
+ .getShadowValue(stateKeyPathKey, meta?.validIds);
3375
+ };
3376
+ }
3377
+
3374
3378
  if (prop === '$derive') {
3375
3379
  return (fn: any) =>
3376
3380
  $cogsSignal({
@@ -3481,8 +3485,9 @@ function createProxyHandler<T>(
3481
3485
  }
3482
3486
  if (prop === 'applyJsonPatch') {
3483
3487
  return (patches: Operation[]) => {
3484
- // 1. Get the current state object that the proxy points to.
3485
3488
  const store = getGlobalStore.getState();
3489
+ const rootMeta = store.getShadowMetadata(stateKey, []);
3490
+ if (!rootMeta?.components) return;
3486
3491
 
3487
3492
  const convertPath = (jsonPath: string): string[] => {
3488
3493
  if (!jsonPath || jsonPath === '/') return [];
@@ -3492,6 +3497,8 @@ function createProxyHandler<T>(
3492
3497
  .map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
3493
3498
  };
3494
3499
 
3500
+ const notifiedComponents = new Set<string>();
3501
+
3495
3502
  for (const patch of patches) {
3496
3503
  const relativePath = convertPath(patch.path);
3497
3504
 
@@ -3504,18 +3511,64 @@ function createProxyHandler<T>(
3504
3511
  value: any;
3505
3512
  };
3506
3513
  store.updateShadowAtPath(stateKey, relativePath, value);
3507
- store.markAsDirty(stateKey, relativePath, { bubble: true }); // Or handle status differently for server patches
3514
+ store.markAsDirty(stateKey, relativePath, { bubble: true });
3515
+
3516
+ // Bubble up - notify components at this path and all parent paths
3517
+ let currentPath = [...relativePath];
3518
+ while (true) {
3519
+ const pathMeta = store.getShadowMetadata(
3520
+ stateKey,
3521
+ currentPath
3522
+ );
3523
+ console.log('pathMeta', pathMeta);
3524
+ if (pathMeta?.pathComponents) {
3525
+ pathMeta.pathComponents.forEach((componentId) => {
3526
+ if (!notifiedComponents.has(componentId)) {
3527
+ const component =
3528
+ rootMeta.components?.get(componentId);
3529
+ if (component) {
3530
+ component.forceUpdate();
3531
+ notifiedComponents.add(componentId);
3532
+ }
3533
+ }
3534
+ });
3535
+ }
3536
+
3537
+ if (currentPath.length === 0) break;
3538
+ currentPath.pop(); // Go up one level
3539
+ }
3508
3540
  break;
3509
3541
  }
3510
3542
  case 'remove': {
3511
- store.removeShadowArrayElement(stateKey, relativePath);
3512
3543
  const parentPath = relativePath.slice(0, -1);
3544
+ store.removeShadowArrayElement(stateKey, relativePath);
3513
3545
  store.markAsDirty(stateKey, parentPath, { bubble: true });
3546
+
3547
+ // Bubble up from parent path
3548
+ let currentPath = [...parentPath];
3549
+ while (true) {
3550
+ const pathMeta = store.getShadowMetadata(
3551
+ stateKey,
3552
+ currentPath
3553
+ );
3554
+ if (pathMeta?.pathComponents) {
3555
+ pathMeta.pathComponents.forEach((componentId) => {
3556
+ if (!notifiedComponents.has(componentId)) {
3557
+ const component =
3558
+ rootMeta.components?.get(componentId);
3559
+ if (component) {
3560
+ component.forceUpdate();
3561
+ notifiedComponents.add(componentId);
3562
+ }
3563
+ }
3564
+ });
3565
+ }
3566
+
3567
+ if (currentPath.length === 0) break;
3568
+ currentPath.pop();
3569
+ }
3514
3570
  break;
3515
3571
  }
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
3572
  }
3520
3573
  }
3521
3574
  };
@@ -3525,20 +3578,40 @@ function createProxyHandler<T>(
3525
3578
  const init = getGlobalStore
3526
3579
  .getState()
3527
3580
  .getInitialOptions(stateKey)?.validation;
3528
- if (!init?.zodSchema || !init?.key)
3529
- throw new Error('Zod schema or validation key not found');
3581
+
3582
+ // UPDATED: Select v4 schema, with a fallback to v3
3583
+ const zodSchema = init?.zodSchemaV4 || init?.zodSchemaV3;
3584
+
3585
+ if (!zodSchema || !init?.key) {
3586
+ throw new Error(
3587
+ 'Zod schema (v3 or v4) or validation key not found'
3588
+ );
3589
+ }
3530
3590
 
3531
3591
  removeValidationError(init.key);
3532
3592
  const thisObject = getGlobalStore
3533
3593
  .getState()
3534
3594
  .getShadowValue(stateKey);
3535
- const result = init.zodSchema.safeParse(thisObject);
3595
+
3596
+ // Use the selected schema for parsing
3597
+ const result = zodSchema.safeParse(thisObject);
3536
3598
 
3537
3599
  if (!result.success) {
3538
- result.error.errors.forEach((error) => {
3539
- const fullErrorPath = [init.key, ...error.path].join('.');
3540
- addValidationError(fullErrorPath, error.message);
3541
- });
3600
+ // This logic already handles both v3 and v4 error types correctly
3601
+ if ('issues' in result.error) {
3602
+ // Zod v4 error
3603
+ result.error.issues.forEach((error) => {
3604
+ const fullErrorPath = [init.key, ...error.path].join('.');
3605
+ addValidationError(fullErrorPath, error.message);
3606
+ });
3607
+ } else {
3608
+ // Zod v3 error
3609
+ (result.error as any).errors.forEach((error: any) => {
3610
+ const fullErrorPath = [init.key, ...error.path].join('.');
3611
+ addValidationError(fullErrorPath, error.message);
3612
+ });
3613
+ }
3614
+
3542
3615
  notifyComponents(stateKey);
3543
3616
  return false;
3544
3617
  }
@@ -3631,20 +3704,14 @@ function createProxyHandler<T>(
3631
3704
  if (prop === 'formElement') {
3632
3705
  return (child: FormControl<T>, formOpts?: FormOptsType) => {
3633
3706
  return (
3634
- <ValidationWrapper
3635
- formOpts={formOpts}
3636
- path={path}
3707
+ <FormElementWrapper
3637
3708
  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>
3709
+ path={path}
3710
+ rebuildStateShape={rebuildStateShape}
3711
+ setState={effectiveSetState}
3712
+ formOpts={formOpts}
3713
+ renderFn={child as any}
3714
+ />
3648
3715
  );
3649
3716
  };
3650
3717
  }
@@ -4203,6 +4270,7 @@ function ListItemWrapper({
4203
4270
 
4204
4271
  return <div ref={setRefs}>{children}</div>;
4205
4272
  }
4273
+
4206
4274
  function FormElementWrapper({
4207
4275
  stateKey,
4208
4276
  path,
@@ -4248,7 +4316,9 @@ function FormElementWrapper({
4248
4316
  const unsubscribe = getGlobalStore
4249
4317
  .getState()
4250
4318
  .subscribeToPath(stateKeyPathKey, (newValue) => {
4251
- forceUpdate({});
4319
+ if (!isCurrentlyDebouncing.current && localValue !== newValue) {
4320
+ forceUpdate({});
4321
+ }
4252
4322
  });
4253
4323
  return () => {
4254
4324
  unsubscribe();
@@ -4261,6 +4331,10 @@ function FormElementWrapper({
4261
4331
 
4262
4332
  const debouncedUpdate = useCallback(
4263
4333
  (newValue: any) => {
4334
+ const currentType = typeof globalStateValue;
4335
+ if (currentType === 'number' && typeof newValue === 'string') {
4336
+ newValue = newValue === '' ? 0 : Number(newValue);
4337
+ }
4264
4338
  setLocalValue(newValue);
4265
4339
  isCurrentlyDebouncing.current = true;
4266
4340
 
@@ -4272,19 +4346,188 @@ function FormElementWrapper({
4272
4346
 
4273
4347
  debounceTimeoutRef.current = setTimeout(() => {
4274
4348
  isCurrentlyDebouncing.current = false;
4349
+
4350
+ // Update state
4275
4351
  setState(newValue, path, { updateType: 'update' });
4352
+
4353
+ // Perform LIVE validation (gentle)
4354
+ const { getInitialOptions, setShadowMetadata, getShadowMetadata } =
4355
+ getGlobalStore.getState();
4356
+ const validationOptions = getInitialOptions(stateKey)?.validation;
4357
+ const zodSchema =
4358
+ validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
4359
+
4360
+ if (zodSchema) {
4361
+ const fullState = getGlobalStore.getState().getShadowValue(stateKey);
4362
+ const result = zodSchema.safeParse(fullState);
4363
+
4364
+ const currentMeta = getShadowMetadata(stateKey, path) || {};
4365
+
4366
+ if (!result.success) {
4367
+ const errors =
4368
+ 'issues' in result.error
4369
+ ? result.error.issues
4370
+ : (result.error as any).errors;
4371
+ const pathErrors = errors.filter(
4372
+ (error: any) =>
4373
+ JSON.stringify(error.path) === JSON.stringify(path)
4374
+ );
4375
+
4376
+ if (pathErrors.length > 0) {
4377
+ setShadowMetadata(stateKey, path, {
4378
+ ...currentMeta,
4379
+ validation: {
4380
+ status: 'INVALID_LIVE',
4381
+ message: pathErrors[0]?.message,
4382
+ validatedValue: newValue,
4383
+ },
4384
+ });
4385
+ } else {
4386
+ // This field has no errors - clear validation
4387
+ setShadowMetadata(stateKey, path, {
4388
+ ...currentMeta,
4389
+ validation: {
4390
+ status: 'VALID_LIVE',
4391
+ validatedValue: newValue,
4392
+ },
4393
+ });
4394
+ }
4395
+ } else {
4396
+ // Validation passed - clear any existing errors
4397
+ setShadowMetadata(stateKey, path, {
4398
+ ...currentMeta,
4399
+ validation: {
4400
+ status: 'VALID_LIVE',
4401
+ validatedValue: newValue,
4402
+ },
4403
+ });
4404
+ }
4405
+ }
4276
4406
  }, debounceTime);
4407
+ forceUpdate({});
4277
4408
  },
4278
- [setState, path, formOpts?.debounceTime]
4409
+ [setState, path, formOpts?.debounceTime, stateKey]
4279
4410
  );
4280
4411
 
4281
- const immediateUpdate = useCallback(() => {
4412
+ // --- NEW onBlur HANDLER ---
4413
+ // This replaces the old commented-out method with a modern approach.
4414
+ const handleBlur = useCallback(async () => {
4415
+ console.log('handleBlur triggered');
4416
+
4417
+ // Commit any pending changes
4282
4418
  if (debounceTimeoutRef.current) {
4283
4419
  clearTimeout(debounceTimeoutRef.current);
4420
+ debounceTimeoutRef.current = null;
4284
4421
  isCurrentlyDebouncing.current = false;
4285
4422
  setState(localValue, path, { updateType: 'update' });
4286
4423
  }
4287
- }, [setState, path, localValue]);
4424
+
4425
+ const { getInitialOptions } = getGlobalStore.getState();
4426
+ const validationOptions = getInitialOptions(stateKey)?.validation;
4427
+ const zodSchema =
4428
+ validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
4429
+
4430
+ if (!zodSchema) return;
4431
+
4432
+ // Get the full path including stateKey
4433
+
4434
+ // Update validation state to "validating"
4435
+ const currentMeta = getGlobalStore
4436
+ .getState()
4437
+ .getShadowMetadata(stateKey, path);
4438
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4439
+ ...currentMeta,
4440
+ validation: {
4441
+ status: 'DIRTY',
4442
+ validatedValue: localValue,
4443
+ },
4444
+ });
4445
+
4446
+ // Validate full state
4447
+ const fullState = getGlobalStore.getState().getShadowValue(stateKey);
4448
+ const result = zodSchema.safeParse(fullState);
4449
+ console.log('result ', result);
4450
+ if (!result.success) {
4451
+ const errors =
4452
+ 'issues' in result.error
4453
+ ? result.error.issues
4454
+ : (result.error as any).errors;
4455
+
4456
+ console.log('All validation errors:', errors);
4457
+ console.log('Current blur path:', path);
4458
+
4459
+ // Find errors for this specific path
4460
+ const pathErrors = errors.filter((error: any) => {
4461
+ console.log('Processing error:', error);
4462
+
4463
+ // For array paths, we need to translate indices to ULIDs
4464
+ if (path.some((p) => p.startsWith('id:'))) {
4465
+ console.log('Detected array path with ULID');
4466
+
4467
+ // This is an array item path like ["id:xyz", "name"]
4468
+ const parentPath = path[0]!.startsWith('id:')
4469
+ ? []
4470
+ : path.slice(0, -1);
4471
+
4472
+ console.log('Parent path:', parentPath);
4473
+
4474
+ const arrayMeta = getGlobalStore
4475
+ .getState()
4476
+ .getShadowMetadata(stateKey, parentPath);
4477
+
4478
+ console.log('Array metadata:', arrayMeta);
4479
+
4480
+ if (arrayMeta?.arrayKeys) {
4481
+ const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
4482
+ const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
4483
+
4484
+ console.log('Item key:', itemKey, 'Index:', itemIndex);
4485
+
4486
+ // Compare with Zod path
4487
+ const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
4488
+ const match =
4489
+ JSON.stringify(error.path) === JSON.stringify(zodPath);
4490
+
4491
+ console.log('Zod path comparison:', {
4492
+ zodPath,
4493
+ errorPath: error.path,
4494
+ match,
4495
+ });
4496
+ return match;
4497
+ }
4498
+ }
4499
+
4500
+ const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
4501
+ console.log('Direct path comparison:', {
4502
+ errorPath: error.path,
4503
+ currentPath: path,
4504
+ match: directMatch,
4505
+ });
4506
+ return directMatch;
4507
+ });
4508
+
4509
+ console.log('Filtered path errors:', pathErrors);
4510
+ // Update shadow metadata with validation result
4511
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4512
+ ...currentMeta,
4513
+ validation: {
4514
+ status: 'VALIDATION_FAILED',
4515
+ message: pathErrors[0]?.message,
4516
+ validatedValue: localValue,
4517
+ },
4518
+ });
4519
+ } else {
4520
+ // Validation passed
4521
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4522
+ ...currentMeta,
4523
+ validation: {
4524
+ status: 'VALID_PENDING_SYNC',
4525
+ validatedValue: localValue,
4526
+ },
4527
+ });
4528
+ }
4529
+ forceUpdate({});
4530
+ }, [stateKey, path, localValue, setState]);
4288
4531
 
4289
4532
  const baseState = rebuildStateShape({
4290
4533
  currentState: globalStateValue,
@@ -4300,7 +4543,8 @@ function FormElementWrapper({
4300
4543
  onChange: (e: any) => {
4301
4544
  debouncedUpdate(e.target.value);
4302
4545
  },
4303
- onBlur: immediateUpdate,
4546
+ // 5. Wire the new onBlur handler to the input props.
4547
+ onBlur: handleBlur,
4304
4548
  ref: formRefStore
4305
4549
  .getState()
4306
4550
  .getFormRef(stateKey + '.' + path.join('.')),
@@ -4311,9 +4555,12 @@ function FormElementWrapper({
4311
4555
  },
4312
4556
  });
4313
4557
 
4314
- return <>{renderFn(stateWithInputProps)}</>;
4558
+ return (
4559
+ <ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
4560
+ {renderFn(stateWithInputProps)}
4561
+ </ValidationWrapper>
4562
+ );
4315
4563
  }
4316
-
4317
4564
  function useRegisterComponent(
4318
4565
  stateKey: string,
4319
4566
  componentId: string,
@@ -4322,25 +4569,19 @@ function useRegisterComponent(
4322
4569
  const fullComponentId = `${stateKey}////${componentId}`;
4323
4570
 
4324
4571
  useLayoutEffect(() => {
4325
- const rootMeta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
4326
- const components = rootMeta?.components || new Map();
4572
+ const { registerComponent, unregisterComponent } =
4573
+ getGlobalStore.getState();
4327
4574
 
4328
- components.set(fullComponentId, {
4575
+ // Call the safe, centralized function to register
4576
+ registerComponent(stateKey, fullComponentId, {
4329
4577
  forceUpdate: () => forceUpdate({}),
4330
4578
  paths: new Set(),
4331
4579
  reactiveType: ['component'],
4332
4580
  });
4333
4581
 
4334
- getGlobalStore.getState().setShadowMetadata(stateKey, [], {
4335
- ...rootMeta,
4336
- components,
4337
- });
4338
-
4582
+ // The cleanup now calls the safe, centralized unregister function
4339
4583
  return () => {
4340
- const meta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
4341
- if (meta?.components) {
4342
- meta.components.delete(fullComponentId);
4343
- }
4584
+ unregisterComponent(stateKey, fullComponentId);
4344
4585
  };
4345
- }, [stateKey, fullComponentId]);
4586
+ }, [stateKey, fullComponentId]); // Dependencies are stable and correct
4346
4587
  }