cogsbox-state 0.5.434 → 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
- import { applyPatch } from 'fast-json-patch';
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;
@@ -231,7 +240,7 @@ export type UpdateArg<S> = S | ((prevState: S) => S);
231
240
  export type InsertParams<S> =
232
241
  | S
233
242
  | ((prevState: { state: S; uuid: string }) => S);
234
- export type UpdateType<T> = (payload: UpdateArg<T>) => void;
243
+ export type UpdateType<T> = (payload: UpdateArg<T>) => { synced: () => void };
235
244
 
236
245
  export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
237
246
  export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
@@ -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,16 +360,24 @@ export type ReactivityType =
346
360
  | 'all'
347
361
  | Array<Prettify<'none' | 'component' | 'deps' | 'all'>>;
348
362
 
363
+ // Define the return type of the sync hook locally
364
+ type SyncApi = {
365
+ updateState: (data: { operation: any }) => void;
366
+ connected: boolean;
367
+ clientId: string | null;
368
+ subscribers: string[];
369
+ };
349
370
  type ValidationOptionsType = {
350
371
  key?: string;
351
- zodSchema?: z.ZodTypeAny;
372
+ zodSchemaV3?: z3.ZodType<any, any, any>;
373
+ zodSchemaV4?: z4.ZodType<any, any, any>;
374
+
352
375
  onBlur?: boolean;
353
376
  };
354
-
355
377
  export type OptionsType<T extends unknown = unknown> = {
356
378
  log?: boolean;
357
379
  componentId?: string;
358
- serverSync?: ServerSyncType<T>;
380
+ cogsSync?: (stateObject: StateObject<T>) => SyncApi;
359
381
  validation?: ValidationOptionsType;
360
382
 
361
383
  serverState?: {
@@ -396,7 +418,7 @@ export type OptionsType<T extends unknown = unknown> = {
396
418
  key: string | ((state: T) => string);
397
419
  onChange?: (state: T) => void;
398
420
  };
399
- formElements?: FormsElementsType;
421
+ formElements?: FormsElementsType<T>;
400
422
 
401
423
  reactiveDeps?: (state: T) => any[] | true;
402
424
  reactiveType?: ReactivityType;
@@ -405,29 +427,7 @@ export type OptionsType<T extends unknown = unknown> = {
405
427
  defaultState?: T;
406
428
  dependencies?: any[];
407
429
  };
408
- export type ServerSyncType<T> = {
409
- testKey?: string;
410
- syncKey: (({ state }: { state: T }) => string) | string;
411
- syncFunction: ({ state }: { state: T }) => void;
412
- debounce?: number;
413
-
414
- snapshot?: {
415
- name: (({ state }: { state: T }) => string) | string;
416
- stateKeys: StateKeys[];
417
- currentUrl: string;
418
- currentParams?: URLSearchParams;
419
- };
420
- };
421
430
 
422
- export type ValidationWrapperOptions<T extends unknown = unknown> = {
423
- children: React.ReactNode;
424
- active: boolean;
425
- stretch?: boolean;
426
- path: string[];
427
- message?: string;
428
- data?: T;
429
- key?: string;
430
- };
431
431
  export type SyncRenderOptions<T extends unknown = unknown> = {
432
432
  children: React.ReactNode;
433
433
  time: number;
@@ -435,8 +435,16 @@ export type SyncRenderOptions<T extends unknown = unknown> = {
435
435
  key?: string;
436
436
  };
437
437
 
438
- type FormsElementsType<T extends unknown = unknown> = {
439
- 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;
440
448
  syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
441
449
  };
442
450
 
@@ -525,9 +533,32 @@ export function addStateOptions<T extends unknown>(
525
533
  ) {
526
534
  return { initialState: initialState, formElements, validation } as T;
527
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
+ };
528
556
  export const createCogsState = <State extends Record<StateKeys, unknown>>(
529
557
  initialState: State,
530
- opt?: { formElements?: FormsElementsType; validation?: ValidationOptionsType }
558
+ opt?: {
559
+ formElements?: FormsElementsType<State>;
560
+ validation?: ValidationOptionsType;
561
+ }
531
562
  ) => {
532
563
  let newInitialState = initialState;
533
564
 
@@ -582,12 +613,12 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
582
613
 
583
614
  const useCogsState = <StateKey extends StateKeys>(
584
615
  stateKey: StateKey,
585
- options?: OptionsType<(typeof statePart)[StateKey]>
616
+ options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
586
617
  ) => {
587
618
  const [componentId] = useState(options?.componentId ?? uuidv4());
588
619
  setOptions({
589
620
  stateKey,
590
- options,
621
+ options: options as any,
591
622
  initialOptionsPart,
592
623
  });
593
624
 
@@ -628,7 +659,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
628
659
  notifyComponents(stateKey as string);
629
660
  }
630
661
 
631
- return { useCogsState, setCogsOptions };
662
+ return { useCogsState, setCogsOptions } as CogsApi<State>;
632
663
  };
633
664
 
634
665
  const {
@@ -782,12 +813,172 @@ export const notifyComponent = (stateKey: string, componentId: string) => {
782
813
  }
783
814
  }
784
815
  };
816
+ function markEntireStateAsServerSynced(
817
+ stateKey: string,
818
+ path: string[],
819
+ data: any,
820
+ timestamp: number
821
+ ) {
822
+ const store = getGlobalStore.getState();
823
+
824
+ // Mark current path as synced
825
+ const currentMeta = store.getShadowMetadata(stateKey, path);
826
+ store.setShadowMetadata(stateKey, path, {
827
+ ...currentMeta,
828
+ isDirty: false,
829
+ stateSource: 'server',
830
+ lastServerSync: timestamp || Date.now(),
831
+ });
832
+
833
+ // If it's an array, mark each item as synced
834
+ if (Array.isArray(data)) {
835
+ const arrayMeta = store.getShadowMetadata(stateKey, path);
836
+ if (arrayMeta?.arrayKeys) {
837
+ arrayMeta.arrayKeys.forEach((itemKey, index) => {
838
+ const itemPath = itemKey.split('.').slice(1);
839
+ const itemData = data[index];
840
+ if (itemData !== undefined) {
841
+ markEntireStateAsServerSynced(
842
+ stateKey,
843
+ itemPath,
844
+ itemData,
845
+ timestamp
846
+ );
847
+ }
848
+ });
849
+ }
850
+ }
851
+ // If it's an object, mark each field as synced
852
+ else if (data && typeof data === 'object' && data.constructor === Object) {
853
+ Object.keys(data).forEach((key) => {
854
+ const fieldPath = [...path, key];
855
+ const fieldData = data[key];
856
+ markEntireStateAsServerSynced(stateKey, fieldPath, fieldData, timestamp);
857
+ });
858
+ }
859
+ }
860
+
861
+ const _notifySubscribedComponents = (
862
+ stateKey: string,
863
+ path: string[],
864
+ updateType: 'update' | 'insert' | 'cut',
865
+ oldValue: any,
866
+ newValue: any
867
+ ) => {
868
+ const store = getGlobalStore.getState();
869
+ const rootMeta = store.getShadowMetadata(stateKey, []);
870
+ if (!rootMeta?.components) {
871
+ return;
872
+ }
873
+
874
+ const notifiedComponents = new Set<string>();
875
+ const shadowMeta = store.getShadowMetadata(stateKey, path);
876
+
877
+ // --- PASS 1: Notify specific subscribers based on update type ---
878
+
879
+ if (updateType === 'update') {
880
+ if (shadowMeta?.pathComponents) {
881
+ shadowMeta.pathComponents.forEach((componentId) => {
882
+ if (notifiedComponents.has(componentId)) return;
883
+ const component = rootMeta.components?.get(componentId);
884
+ if (component) {
885
+ const reactiveTypes = Array.isArray(component.reactiveType)
886
+ ? component.reactiveType
887
+ : [component.reactiveType || 'component'];
888
+ if (!reactiveTypes.includes('none')) {
889
+ component.forceUpdate();
890
+ notifiedComponents.add(componentId);
891
+ }
892
+ }
893
+ });
894
+ }
895
+
896
+ if (
897
+ newValue &&
898
+ typeof newValue === 'object' &&
899
+ !isArray(newValue) &&
900
+ oldValue &&
901
+ typeof oldValue === 'object' &&
902
+ !isArray(oldValue)
903
+ ) {
904
+ const changedSubPaths = getDifferences(newValue, oldValue);
905
+ changedSubPaths.forEach((subPathString) => {
906
+ const subPath = subPathString.split('.');
907
+ const fullSubPath = [...path, ...subPath];
908
+ const subPathMeta = store.getShadowMetadata(stateKey, fullSubPath);
909
+ if (subPathMeta?.pathComponents) {
910
+ subPathMeta.pathComponents.forEach((componentId) => {
911
+ if (notifiedComponents.has(componentId)) return;
912
+ const component = rootMeta.components?.get(componentId);
913
+ if (component) {
914
+ const reactiveTypes = Array.isArray(component.reactiveType)
915
+ ? component.reactiveType
916
+ : [component.reactiveType || 'component'];
917
+ if (!reactiveTypes.includes('none')) {
918
+ component.forceUpdate();
919
+ notifiedComponents.add(componentId);
920
+ }
921
+ }
922
+ });
923
+ }
924
+ });
925
+ }
926
+ } else if (updateType === 'insert' || updateType === 'cut') {
927
+ const parentArrayPath = updateType === 'insert' ? path : path.slice(0, -1);
928
+ const parentMeta = store.getShadowMetadata(stateKey, parentArrayPath);
929
+ if (parentMeta?.pathComponents) {
930
+ parentMeta.pathComponents.forEach((componentId) => {
931
+ if (!notifiedComponents.has(componentId)) {
932
+ const component = rootMeta.components?.get(componentId);
933
+ if (component) {
934
+ component.forceUpdate();
935
+ notifiedComponents.add(componentId);
936
+ }
937
+ }
938
+ });
939
+ }
940
+ }
941
+
942
+ // --- PASS 2: Notify global subscribers ('all', 'deps') ---
943
+
944
+ rootMeta.components.forEach((component, componentId) => {
945
+ if (notifiedComponents.has(componentId)) {
946
+ return;
947
+ }
948
+
949
+ const reactiveTypes = Array.isArray(component.reactiveType)
950
+ ? component.reactiveType
951
+ : [component.reactiveType || 'component'];
952
+
953
+ if (reactiveTypes.includes('all')) {
954
+ component.forceUpdate();
955
+ notifiedComponents.add(componentId);
956
+ return;
957
+ }
958
+
959
+ if (reactiveTypes.includes('deps')) {
960
+ if (component.depsFunction) {
961
+ const currentState = store.getShadowValue(stateKey);
962
+ const newDeps = component.depsFunction(currentState);
963
+ let shouldUpdate = false;
964
+ if (newDeps === true || !isDeepEqual(component.prevDeps, newDeps)) {
965
+ if (Array.isArray(newDeps)) component.prevDeps = newDeps;
966
+ shouldUpdate = true;
967
+ }
968
+ if (shouldUpdate) {
969
+ component.forceUpdate();
970
+ notifiedComponents.add(componentId);
971
+ }
972
+ }
973
+ }
974
+ });
975
+ };
785
976
 
786
977
  export function useCogsStateFn<TStateObject extends unknown>(
787
978
  stateObject: TStateObject,
788
979
  {
789
980
  stateKey,
790
- serverSync,
981
+
791
982
  localStorage,
792
983
  formElements,
793
984
  reactiveDeps,
@@ -901,7 +1092,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
901
1092
  const unsubscribe = getGlobalStore
902
1093
  .getState()
903
1094
  .subscribeToPath(thisKey, (event) => {
904
- // REPLACEMENT STARTS HERE
905
1095
  if (event?.type === 'SERVER_STATE_UPDATE') {
906
1096
  const serverStateData = event.serverState;
907
1097
 
@@ -912,7 +1102,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
912
1102
  const newOptions = { serverState: serverStateData };
913
1103
  setAndMergeOptions(thisKey, newOptions);
914
1104
 
915
- // Check for a merge request.
916
1105
  const mergeConfig =
917
1106
  typeof serverStateData.merge === 'object'
918
1107
  ? serverStateData.merge
@@ -920,20 +1109,17 @@ export function useCogsStateFn<TStateObject extends unknown>(
920
1109
  ? { strategy: 'append' }
921
1110
  : null;
922
1111
 
923
- // Get the current array and the new data.
924
1112
  const currentState = getGlobalStore
925
1113
  .getState()
926
1114
  .getShadowValue(thisKey);
927
1115
  const incomingData = serverStateData.data;
928
1116
 
929
- // *** THE REAL FIX: PERFORM INCREMENTAL INSERTS ***
930
1117
  if (
931
1118
  mergeConfig &&
932
1119
  Array.isArray(currentState) &&
933
1120
  Array.isArray(incomingData)
934
1121
  ) {
935
1122
  const keyField = mergeConfig.key || 'id';
936
-
937
1123
  const existingIds = new Set(
938
1124
  currentState.map((item: any) => item[keyField])
939
1125
  );
@@ -941,21 +1127,72 @@ export function useCogsStateFn<TStateObject extends unknown>(
941
1127
  const newUniqueItems = incomingData.filter((item: any) => {
942
1128
  return !existingIds.has(item[keyField]);
943
1129
  });
944
- console.log('newUniqueItems', newUniqueItems);
1130
+
945
1131
  if (newUniqueItems.length > 0) {
946
1132
  newUniqueItems.forEach((item) => {
947
1133
  getGlobalStore
948
1134
  .getState()
949
1135
  .insertShadowArrayElement(thisKey, [], item);
1136
+
1137
+ // MARK NEW SERVER ITEMS AS SYNCED
1138
+ const arrayMeta = getGlobalStore
1139
+ .getState()
1140
+ .getShadowMetadata(thisKey, []);
1141
+
1142
+ if (arrayMeta?.arrayKeys) {
1143
+ const newItemKey =
1144
+ arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
1145
+ if (newItemKey) {
1146
+ const newItemPath = newItemKey.split('.').slice(1);
1147
+
1148
+ // Mark the new server item as synced, not dirty
1149
+ getGlobalStore
1150
+ .getState()
1151
+ .setShadowMetadata(thisKey, newItemPath, {
1152
+ isDirty: false,
1153
+ stateSource: 'server',
1154
+ lastServerSync:
1155
+ serverStateData.timestamp || Date.now(),
1156
+ });
1157
+
1158
+ // Also mark all its child fields as synced if it's an object
1159
+ const itemValue = getGlobalStore
1160
+ .getState()
1161
+ .getShadowValue(newItemKey);
1162
+ if (
1163
+ itemValue &&
1164
+ typeof itemValue === 'object' &&
1165
+ !Array.isArray(itemValue)
1166
+ ) {
1167
+ Object.keys(itemValue).forEach((fieldKey) => {
1168
+ const fieldPath = [...newItemPath, fieldKey];
1169
+ getGlobalStore
1170
+ .getState()
1171
+ .setShadowMetadata(thisKey, fieldPath, {
1172
+ isDirty: false,
1173
+ stateSource: 'server',
1174
+ lastServerSync:
1175
+ serverStateData.timestamp || Date.now(),
1176
+ });
1177
+ });
1178
+ }
1179
+ }
1180
+ }
950
1181
  });
951
- } else {
952
- // No new items, no need to do anything.
953
- return;
954
1182
  }
955
1183
  } else {
1184
+ // For replace strategy or initial load
956
1185
  getGlobalStore
957
1186
  .getState()
958
1187
  .initializeShadowState(thisKey, incomingData);
1188
+
1189
+ // Mark the entire state tree as synced from server
1190
+ markEntireStateAsServerSynced(
1191
+ thisKey,
1192
+ [],
1193
+ incomingData,
1194
+ serverStateData.timestamp
1195
+ );
959
1196
  }
960
1197
 
961
1198
  const meta = getGlobalStore
@@ -1012,7 +1249,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
1012
1249
  useLayoutEffect(() => {
1013
1250
  if (noStateKey) {
1014
1251
  setAndMergeOptions(thisKey as string, {
1015
- serverSync,
1016
1252
  formElements,
1017
1253
  defaultState,
1018
1254
  localStorage,
@@ -1043,9 +1279,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1043
1279
  ...rootMeta,
1044
1280
  components,
1045
1281
  });
1046
-
1047
1282
  forceUpdate({});
1048
-
1049
1283
  return () => {
1050
1284
  const meta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1051
1285
  const component = meta?.components?.get(componentKey);
@@ -1079,11 +1313,12 @@ export function useCogsStateFn<TStateObject extends unknown>(
1079
1313
  }
1080
1314
  };
1081
1315
  }, []);
1316
+
1317
+ const syncApiRef = useRef<SyncApi | null>(null);
1082
1318
  const effectiveSetState = (
1083
1319
  newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
1084
1320
  path: string[],
1085
- updateObj: { updateType: 'insert' | 'cut' | 'update' },
1086
- validationKey?: string
1321
+ updateObj: UpdateOptions
1087
1322
  ) => {
1088
1323
  const fullPath = [thisKey, ...path].join('.');
1089
1324
  if (Array.isArray(path)) {
@@ -1122,24 +1357,38 @@ export function useCogsStateFn<TStateObject extends unknown>(
1122
1357
  store.insertShadowArrayElement(thisKey, path, newUpdate.newValue);
1123
1358
  // The array at `path` has been modified. Mark it AND all its parents as dirty.
1124
1359
  store.markAsDirty(thisKey, path, { bubble: true });
1360
+
1361
+ // ALSO mark the newly inserted item itself as dirty
1362
+ // Get the new item's path and mark it as dirty
1363
+ const arrayMeta = store.getShadowMetadata(thisKey, path);
1364
+ if (arrayMeta?.arrayKeys) {
1365
+ const newItemKey =
1366
+ arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
1367
+ if (newItemKey) {
1368
+ const newItemPath = newItemKey.split('.').slice(1); // Remove stateKey
1369
+ store.markAsDirty(thisKey, newItemPath, { bubble: false });
1370
+ }
1371
+ }
1125
1372
  break;
1126
1373
  }
1127
1374
  case 'cut': {
1128
- // The item is at `path`, so the parent array is at `path.slice(0, -1)`
1129
1375
  const parentArrayPath = path.slice(0, -1);
1376
+
1130
1377
  store.removeShadowArrayElement(thisKey, path);
1131
- // The parent array has been modified. Mark it AND all its parents as dirty.
1132
1378
  store.markAsDirty(thisKey, parentArrayPath, { bubble: true });
1133
1379
  break;
1134
1380
  }
1135
1381
  case 'update': {
1136
1382
  store.updateShadowAtPath(thisKey, path, newUpdate.newValue);
1137
- // The item at `path` was updated. Mark it AND all its parents as dirty.
1138
1383
  store.markAsDirty(thisKey, path, { bubble: true });
1139
1384
  break;
1140
1385
  }
1141
1386
  }
1387
+ const shouldSync = updateObj.sync !== false;
1142
1388
 
1389
+ if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
1390
+ syncApiRef.current.updateState({ operation: newUpdate });
1391
+ }
1143
1392
  // Handle signals - reuse shadowMeta from the beginning
1144
1393
  if (shadowMeta?.signals && shadowMeta.signals.length > 0) {
1145
1394
  // Use updatedShadowValue if we need the new value, otherwise use payload
@@ -1305,59 +1554,18 @@ export function useCogsStateFn<TStateObject extends unknown>(
1305
1554
  });
1306
1555
  }
1307
1556
  }
1308
- if (
1309
- updateObj.updateType === 'update' &&
1310
- (validationKey || latestInitialOptionsRef.current?.validation?.key) &&
1311
- path
1312
- ) {
1313
- removeValidationError(
1314
- (validationKey || latestInitialOptionsRef.current?.validation?.key) +
1315
- '.' +
1316
- path.join('.')
1317
- );
1318
- }
1319
- const arrayWithoutIndex = path.slice(0, path.length - 1);
1320
- if (
1321
- updateObj.updateType === 'cut' &&
1322
- latestInitialOptionsRef.current?.validation?.key
1323
- ) {
1324
- removeValidationError(
1325
- latestInitialOptionsRef.current?.validation?.key +
1326
- '.' +
1327
- arrayWithoutIndex.join('.')
1328
- );
1329
- }
1330
- if (
1331
- updateObj.updateType === 'insert' &&
1332
- latestInitialOptionsRef.current?.validation?.key
1333
- ) {
1334
- const getValidation = getValidationErrors(
1335
- latestInitialOptionsRef.current?.validation?.key +
1336
- '.' +
1337
- arrayWithoutIndex.join('.')
1338
- );
1339
-
1340
- getValidation.filter((k) => {
1341
- let length = k?.split('.').length;
1342
- const v = ''; // Placeholder as `v` is not used from getValidationErrors
1343
1557
 
1344
- if (
1345
- k == arrayWithoutIndex.join('.') &&
1346
- length == arrayWithoutIndex.length - 1
1347
- ) {
1348
- let newKey = k + '.' + arrayWithoutIndex;
1349
- removeValidationError(k!);
1350
- addValidationError(newKey, v!);
1351
- }
1352
- });
1353
- }
1354
1558
  // Assumes `isDeepEqual` is available in this scope.
1355
1559
  // Assumes `isDeepEqual` is available in this scope.
1356
1560
 
1357
- const newState = store.getShadowValue(thisKey);
1358
- const rootMeta = store.getShadowMetadata(thisKey, []);
1561
+ const newState = getGlobalStore.getState().getShadowValue(thisKey);
1562
+ const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1359
1563
  const notifiedComponents = new Set<string>();
1360
-
1564
+ console.log(
1565
+ 'rootMeta',
1566
+ thisKey,
1567
+ getGlobalStore.getState().shadowStateStore
1568
+ );
1361
1569
  if (!rootMeta?.components) {
1362
1570
  return newState;
1363
1571
  }
@@ -1365,28 +1573,39 @@ export function useCogsStateFn<TStateObject extends unknown>(
1365
1573
  // --- PASS 1: Notify specific subscribers based on update type ---
1366
1574
 
1367
1575
  if (updateObj.updateType === 'update') {
1368
- // ALWAYS notify components subscribed to the exact path being updated.
1369
- // This is the crucial part that handles primitives, as well as updates
1370
- // to an object or array treated as a whole.
1371
- if (shadowMeta?.pathComponents) {
1372
- shadowMeta.pathComponents.forEach((componentId) => {
1373
- // If this component was already notified, skip.
1374
- if (notifiedComponents.has(componentId)) {
1375
- return;
1376
- }
1377
- const component = rootMeta.components?.get(componentId);
1378
- if (component) {
1379
- const reactiveTypes = Array.isArray(component.reactiveType)
1380
- ? component.reactiveType
1381
- : [component.reactiveType || 'component'];
1382
-
1383
- // Check if the component has reactivity enabled
1384
- if (!reactiveTypes.includes('none')) {
1385
- component.forceUpdate();
1386
- notifiedComponents.add(componentId);
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
1387
1589
  }
1388
- }
1389
- });
1590
+ const component = rootMeta.components?.get(componentId);
1591
+ if (component) {
1592
+ const reactiveTypes = Array.isArray(component.reactiveType)
1593
+ ? component.reactiveType
1594
+ : [component.reactiveType || 'component'];
1595
+
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
+ }
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.
1390
1609
  }
1391
1610
 
1392
1611
  // ADDITIONALLY, if the payload is an object, perform a deep-check and
@@ -1581,6 +1800,11 @@ export function useCogsStateFn<TStateObject extends unknown>(
1581
1800
  );
1582
1801
  }, [thisKey, sessionId]);
1583
1802
 
1803
+ const cogsSyncFn = latestInitialOptionsRef.current?.cogsSync;
1804
+ if (cogsSyncFn) {
1805
+ syncApiRef.current = cogsSyncFn(updaterFinal);
1806
+ }
1807
+
1584
1808
  return updaterFinal;
1585
1809
  }
1586
1810
 
@@ -1661,12 +1885,16 @@ const registerComponentDependency = (
1661
1885
  dependencyPath: string[]
1662
1886
  ) => {
1663
1887
  const fullComponentId = `${stateKey}////${componentId}`;
1664
- 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, []);
1665
1893
  const component = rootMeta?.components?.get(fullComponentId);
1666
1894
 
1667
1895
  if (
1668
1896
  !component ||
1669
- component.reactiveType == 'none' ||
1897
+ component.reactiveType === 'none' ||
1670
1898
  !(
1671
1899
  Array.isArray(component.reactiveType)
1672
1900
  ? component.reactiveType
@@ -1676,23 +1904,9 @@ const registerComponentDependency = (
1676
1904
  return;
1677
1905
  }
1678
1906
 
1679
- const pathKey = [stateKey, ...dependencyPath].join('.');
1680
-
1681
- // Add to component's paths (existing logic)
1682
- component.paths.add(pathKey);
1683
-
1684
- // NEW: Also store componentId at the path level
1685
- const pathMeta =
1686
- getGlobalStore.getState().getShadowMetadata(stateKey, dependencyPath) || {};
1687
- const pathComponents = pathMeta.pathComponents || new Set<string>();
1688
- pathComponents.add(fullComponentId);
1689
-
1690
- getGlobalStore.getState().setShadowMetadata(stateKey, dependencyPath, {
1691
- ...pathMeta,
1692
- pathComponents,
1693
- });
1907
+ // Now, call the single, safe, atomic function to perform the update.
1908
+ addPathComponent(stateKey, dependencyPath, fullComponentId);
1694
1909
  };
1695
-
1696
1910
  const notifySelectionComponents = (
1697
1911
  stateKey: string,
1698
1912
  parentPath: string[],
@@ -1896,21 +2110,56 @@ function createProxyHandler<T>(
1896
2110
  }
1897
2111
  };
1898
2112
  }
2113
+ // Fixed getStatus function in createProxyHandler
1899
2114
  if (prop === '_status' || prop === 'getStatus') {
1900
2115
  const getStatusFunc = () => {
1901
2116
  const shadowMeta = getGlobalStore
1902
2117
  .getState()
1903
- .getShadowMetadata(stateKey, []);
2118
+ .getShadowMetadata(stateKey, path);
2119
+ const value = getGlobalStore
2120
+ .getState()
2121
+ .getShadowValue(stateKeyPathKey);
1904
2122
 
1905
- if (shadowMeta?.stateSource === 'server' && !shadowMeta.isDirty) {
1906
- return 'synced';
1907
- } else if (shadowMeta?.isDirty) {
2123
+ // Priority 1: Explicitly dirty items
2124
+ if (shadowMeta?.isDirty === true) {
1908
2125
  return 'dirty';
1909
- } else if (shadowMeta?.stateSource === 'localStorage') {
2126
+ }
2127
+
2128
+ // Priority 2: Explicitly synced items (isDirty: false)
2129
+ if (shadowMeta?.isDirty === false) {
2130
+ return 'synced';
2131
+ }
2132
+
2133
+ // Priority 3: Items from server source (should be synced even without explicit isDirty flag)
2134
+ if (shadowMeta?.stateSource === 'server') {
2135
+ return 'synced';
2136
+ }
2137
+
2138
+ // Priority 4: Items restored from localStorage
2139
+ if (shadowMeta?.stateSource === 'localStorage') {
1910
2140
  return 'restored';
1911
- } else if (shadowMeta?.stateSource === 'default') {
2141
+ }
2142
+
2143
+ // Priority 5: Items from default/initial state
2144
+ if (shadowMeta?.stateSource === 'default') {
2145
+ return 'fresh';
2146
+ }
2147
+
2148
+ // Priority 6: Check if this is part of initial server load
2149
+ // Look up the tree to see if parent has server source
2150
+ const rootMeta = getGlobalStore
2151
+ .getState()
2152
+ .getShadowMetadata(stateKey, []);
2153
+ if (rootMeta?.stateSource === 'server' && !shadowMeta?.isDirty) {
2154
+ return 'synced';
2155
+ }
2156
+
2157
+ // Priority 7: If no metadata exists but value exists, it's probably fresh
2158
+ if (value !== undefined && !shadowMeta) {
1912
2159
  return 'fresh';
1913
2160
  }
2161
+
2162
+ // Fallback
1914
2163
  return 'unknown';
1915
2164
  };
1916
2165
 
@@ -1930,13 +2179,16 @@ function createProxyHandler<T>(
1930
2179
  }
1931
2180
  if (prop === 'showValidationErrors') {
1932
2181
  return () => {
1933
- const init = getGlobalStore
1934
- .getState()
1935
- .getInitialOptions(stateKey)?.validation;
1936
- if (!init?.key) throw new Error('Validation key not found');
1937
- return getGlobalStore
2182
+ const meta = getGlobalStore
1938
2183
  .getState()
1939
- .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 [];
1940
2192
  };
1941
2193
  }
1942
2194
  if (Array.isArray(currentState)) {
@@ -2457,6 +2709,49 @@ function createProxyHandler<T>(
2457
2709
  },
2458
2710
  rebuildStateShape,
2459
2711
  });
2712
+ } // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
2713
+
2714
+ if (prop === 'stateFind') {
2715
+ return (
2716
+ callbackfn: (value: any, index: number) => boolean
2717
+ ): StateObject<any> | undefined => {
2718
+ // 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
2719
+ const arrayKeys =
2720
+ meta?.validIds ??
2721
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2722
+ ?.arrayKeys;
2723
+
2724
+ if (!arrayKeys) {
2725
+ return undefined;
2726
+ }
2727
+
2728
+ // 2. Iterate through the keys, get the value for each, and run the callback.
2729
+ for (let i = 0; i < arrayKeys.length; i++) {
2730
+ const itemKey = arrayKeys[i];
2731
+ if (!itemKey) continue; // Safety check
2732
+
2733
+ const itemValue = getGlobalStore
2734
+ .getState()
2735
+ .getShadowValue(itemKey);
2736
+
2737
+ // 3. If the callback returns true, we've found our item.
2738
+ if (callbackfn(itemValue, i)) {
2739
+ // Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
2740
+ const itemPath = itemKey.split('.').slice(1);
2741
+
2742
+ // 4. Rebuild a new, fully functional StateObject for just that item and return it.
2743
+ return rebuildStateShape({
2744
+ currentState: itemValue,
2745
+ path: itemPath,
2746
+ componentId: componentId,
2747
+ meta, // Pass along meta for potential further chaining
2748
+ });
2749
+ }
2750
+ }
2751
+
2752
+ // 5. If the loop finishes without finding anything, return undefined.
2753
+ return undefined;
2754
+ };
2460
2755
  }
2461
2756
  if (prop === 'stateFilter') {
2462
2757
  return (callbackfn: (value: any, index: number) => boolean) => {
@@ -2691,13 +2986,13 @@ function createProxyHandler<T>(
2691
2986
  arrayValues: freshValues || [],
2692
2987
  };
2693
2988
  }, [cacheKey, updateTrigger]);
2694
-
2989
+ console.log('freshValues', validIds, arrayValues);
2695
2990
  useEffect(() => {
2696
2991
  const unsubscribe = getGlobalStore
2697
2992
  .getState()
2698
2993
  .subscribeToPath(stateKeyPathKey, (e) => {
2699
2994
  // A data change has occurred for the source array.
2700
- console.log('statelsit subscribed to path', e);
2995
+
2701
2996
  if (e.type === 'GET_SELECTED') {
2702
2997
  return;
2703
2998
  }
@@ -2716,7 +3011,12 @@ function createProxyHandler<T>(
2716
3011
  }
2717
3012
  }
2718
3013
  }
2719
- if (e.type === 'INSERT') {
3014
+
3015
+ if (
3016
+ e.type === 'INSERT' ||
3017
+ e.type === 'REMOVE' ||
3018
+ e.type === 'CLEAR_SELECTION'
3019
+ ) {
2720
3020
  forceUpdate({});
2721
3021
  }
2722
3022
  });
@@ -2741,7 +3041,7 @@ function createProxyHandler<T>(
2741
3041
  validIds: validIds,
2742
3042
  },
2743
3043
  });
2744
-
3044
+ console.log('sssssssssssssssssssssssssssss', arraySetter);
2745
3045
  return (
2746
3046
  <>
2747
3047
  {arrayValues.map((item, localIndex) => {
@@ -2918,15 +3218,12 @@ function createProxyHandler<T>(
2918
3218
  }
2919
3219
  if (prop === 'cutSelected') {
2920
3220
  return () => {
2921
- const baseArrayKeys =
2922
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2923
- ?.arrayKeys || [];
2924
3221
  const validKeys = applyTransforms(
2925
3222
  stateKey,
2926
3223
  path,
2927
3224
  meta?.transforms
2928
3225
  );
2929
- console.log('validKeys', validKeys);
3226
+
2930
3227
  if (!validKeys || validKeys.length === 0) return;
2931
3228
 
2932
3229
  const indexKeyToCut = getGlobalStore
@@ -2936,13 +3233,15 @@ function createProxyHandler<T>(
2936
3233
  let indexToCut = validKeys.findIndex(
2937
3234
  (key) => key === indexKeyToCut
2938
3235
  );
2939
- console.log('indexToCut', indexToCut);
3236
+
2940
3237
  const pathForCut = validKeys[
2941
3238
  indexToCut == -1 ? validKeys.length - 1 : indexToCut
2942
3239
  ]
2943
3240
  ?.split('.')
2944
3241
  .slice(1);
2945
- console.log('pathForCut', pathForCut);
3242
+ getGlobalStore
3243
+ .getState()
3244
+ .clearSelectedIndex({ arrayKey: stateKeyPathKey });
2946
3245
  effectiveSetState(currentState, pathForCut!, {
2947
3246
  updateType: 'cut',
2948
3247
  });
@@ -3068,6 +3367,14 @@ function createProxyHandler<T>(
3068
3367
  .getShadowValue(stateKeyPathKey, meta?.validIds);
3069
3368
  };
3070
3369
  }
3370
+ if (prop === 'getState') {
3371
+ return () => {
3372
+ return getGlobalStore
3373
+ .getState()
3374
+ .getShadowValue(stateKeyPathKey, meta?.validIds);
3375
+ };
3376
+ }
3377
+
3071
3378
  if (prop === '$derive') {
3072
3379
  return (fn: any) =>
3073
3380
  $cogsSignal({
@@ -3177,13 +3484,93 @@ function createProxyHandler<T>(
3177
3484
  };
3178
3485
  }
3179
3486
  if (prop === 'applyJsonPatch') {
3180
- return (patches: any[]) => {
3181
- const currentState = getGlobalStore
3182
- .getState()
3183
- .getShadowValue(stateKeyPathKey, meta?.validIds);
3184
- const newState = applyPatch(currentState, patches).newDocument;
3487
+ return (patches: Operation[]) => {
3488
+ const store = getGlobalStore.getState();
3489
+ const rootMeta = store.getShadowMetadata(stateKey, []);
3490
+ if (!rootMeta?.components) return;
3491
+
3492
+ const convertPath = (jsonPath: string): string[] => {
3493
+ if (!jsonPath || jsonPath === '/') return [];
3494
+ return jsonPath
3495
+ .split('/')
3496
+ .slice(1)
3497
+ .map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
3498
+ };
3185
3499
 
3186
- notifyComponents(stateKey);
3500
+ const notifiedComponents = new Set<string>();
3501
+
3502
+ for (const patch of patches) {
3503
+ const relativePath = convertPath(patch.path);
3504
+
3505
+ switch (patch.op) {
3506
+ case 'add':
3507
+ case 'replace': {
3508
+ const { value } = patch as {
3509
+ op: 'add' | 'replace';
3510
+ path: string;
3511
+ value: any;
3512
+ };
3513
+ store.updateShadowAtPath(stateKey, relativePath, value);
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
+ }
3540
+ break;
3541
+ }
3542
+ case 'remove': {
3543
+ const parentPath = relativePath.slice(0, -1);
3544
+ store.removeShadowArrayElement(stateKey, relativePath);
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
+ }
3570
+ break;
3571
+ }
3572
+ }
3573
+ }
3187
3574
  };
3188
3575
  }
3189
3576
  if (prop === 'validateZodSchema') {
@@ -3191,20 +3578,40 @@ function createProxyHandler<T>(
3191
3578
  const init = getGlobalStore
3192
3579
  .getState()
3193
3580
  .getInitialOptions(stateKey)?.validation;
3194
- if (!init?.zodSchema || !init?.key)
3195
- 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
+ }
3196
3590
 
3197
3591
  removeValidationError(init.key);
3198
3592
  const thisObject = getGlobalStore
3199
3593
  .getState()
3200
3594
  .getShadowValue(stateKey);
3201
- const result = init.zodSchema.safeParse(thisObject);
3595
+
3596
+ // Use the selected schema for parsing
3597
+ const result = zodSchema.safeParse(thisObject);
3202
3598
 
3203
3599
  if (!result.success) {
3204
- result.error.errors.forEach((error) => {
3205
- const fullErrorPath = [init.key, ...error.path].join('.');
3206
- addValidationError(fullErrorPath, error.message);
3207
- });
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
+
3208
3615
  notifyComponents(stateKey);
3209
3616
  return false;
3210
3617
  }
@@ -3247,9 +3654,38 @@ function createProxyHandler<T>(
3247
3654
  if (prop === '_path') return path;
3248
3655
  if (prop === 'update') {
3249
3656
  return (payload: UpdateArg<T>) => {
3657
+ // Step 1: This is the same. It performs the data update.
3250
3658
  effectiveSetState(payload as any, path, { updateType: 'update' });
3659
+
3660
+ return {
3661
+ /**
3662
+ * Marks this specific item, which was just updated, as 'synced' (not dirty).
3663
+ */
3664
+ synced: () => {
3665
+ // This function "remembers" the path of the item that was just updated.
3666
+ const shadowMeta = getGlobalStore
3667
+ .getState()
3668
+ .getShadowMetadata(stateKey, path);
3669
+
3670
+ // It updates ONLY the metadata for that specific item.
3671
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
3672
+ ...shadowMeta,
3673
+ isDirty: false, // EXPLICITLY set to false, not just undefined
3674
+ stateSource: 'server', // Mark as coming from server
3675
+ lastServerSync: Date.now(), // Add timestamp
3676
+ });
3677
+
3678
+ // Force a re-render for components watching this path
3679
+ const fullPath = [stateKey, ...path].join('.');
3680
+ getGlobalStore.getState().notifyPathSubscribers(fullPath, {
3681
+ type: 'SYNC_STATUS_CHANGE',
3682
+ isDirty: false,
3683
+ });
3684
+ },
3685
+ };
3251
3686
  };
3252
3687
  }
3688
+
3253
3689
  if (prop === 'toggle') {
3254
3690
  const currentValueAtPath = getGlobalStore
3255
3691
  .getState()
@@ -3268,20 +3704,14 @@ function createProxyHandler<T>(
3268
3704
  if (prop === 'formElement') {
3269
3705
  return (child: FormControl<T>, formOpts?: FormOptsType) => {
3270
3706
  return (
3271
- <ValidationWrapper
3272
- formOpts={formOpts}
3273
- path={path}
3707
+ <FormElementWrapper
3274
3708
  stateKey={stateKey}
3275
- >
3276
- <FormElementWrapper
3277
- stateKey={stateKey}
3278
- path={path}
3279
- rebuildStateShape={rebuildStateShape}
3280
- setState={effectiveSetState}
3281
- formOpts={formOpts}
3282
- renderFn={child as any}
3283
- />
3284
- </ValidationWrapper>
3709
+ path={path}
3710
+ rebuildStateShape={rebuildStateShape}
3711
+ setState={effectiveSetState}
3712
+ formOpts={formOpts}
3713
+ renderFn={child as any}
3714
+ />
3285
3715
  );
3286
3716
  };
3287
3717
  }
@@ -3782,6 +4212,7 @@ function ListItemWrapper({
3782
4212
  const hasReportedInitialHeight = useRef(false);
3783
4213
  const fullKey = [stateKey, ...itemPath].join('.');
3784
4214
  useRegisterComponent(stateKey, itemComponentId, forceUpdate);
4215
+
3785
4216
  const setRefs = useCallback(
3786
4217
  (element: HTMLDivElement | null) => {
3787
4218
  elementRef.current = element;
@@ -3839,6 +4270,7 @@ function ListItemWrapper({
3839
4270
 
3840
4271
  return <div ref={setRefs}>{children}</div>;
3841
4272
  }
4273
+
3842
4274
  function FormElementWrapper({
3843
4275
  stateKey,
3844
4276
  path,
@@ -3884,7 +4316,9 @@ function FormElementWrapper({
3884
4316
  const unsubscribe = getGlobalStore
3885
4317
  .getState()
3886
4318
  .subscribeToPath(stateKeyPathKey, (newValue) => {
3887
- forceUpdate({});
4319
+ if (!isCurrentlyDebouncing.current && localValue !== newValue) {
4320
+ forceUpdate({});
4321
+ }
3888
4322
  });
3889
4323
  return () => {
3890
4324
  unsubscribe();
@@ -3897,6 +4331,10 @@ function FormElementWrapper({
3897
4331
 
3898
4332
  const debouncedUpdate = useCallback(
3899
4333
  (newValue: any) => {
4334
+ const currentType = typeof globalStateValue;
4335
+ if (currentType === 'number' && typeof newValue === 'string') {
4336
+ newValue = newValue === '' ? 0 : Number(newValue);
4337
+ }
3900
4338
  setLocalValue(newValue);
3901
4339
  isCurrentlyDebouncing.current = true;
3902
4340
 
@@ -3908,19 +4346,188 @@ function FormElementWrapper({
3908
4346
 
3909
4347
  debounceTimeoutRef.current = setTimeout(() => {
3910
4348
  isCurrentlyDebouncing.current = false;
4349
+
4350
+ // Update state
3911
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
+ }
3912
4406
  }, debounceTime);
4407
+ forceUpdate({});
3913
4408
  },
3914
- [setState, path, formOpts?.debounceTime]
4409
+ [setState, path, formOpts?.debounceTime, stateKey]
3915
4410
  );
3916
4411
 
3917
- 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
3918
4418
  if (debounceTimeoutRef.current) {
3919
4419
  clearTimeout(debounceTimeoutRef.current);
4420
+ debounceTimeoutRef.current = null;
3920
4421
  isCurrentlyDebouncing.current = false;
3921
4422
  setState(localValue, path, { updateType: 'update' });
3922
4423
  }
3923
- }, [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]);
3924
4531
 
3925
4532
  const baseState = rebuildStateShape({
3926
4533
  currentState: globalStateValue,
@@ -3936,7 +4543,8 @@ function FormElementWrapper({
3936
4543
  onChange: (e: any) => {
3937
4544
  debouncedUpdate(e.target.value);
3938
4545
  },
3939
- onBlur: immediateUpdate,
4546
+ // 5. Wire the new onBlur handler to the input props.
4547
+ onBlur: handleBlur,
3940
4548
  ref: formRefStore
3941
4549
  .getState()
3942
4550
  .getFormRef(stateKey + '.' + path.join('.')),
@@ -3947,9 +4555,12 @@ function FormElementWrapper({
3947
4555
  },
3948
4556
  });
3949
4557
 
3950
- return <>{renderFn(stateWithInputProps)}</>;
4558
+ return (
4559
+ <ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
4560
+ {renderFn(stateWithInputProps)}
4561
+ </ValidationWrapper>
4562
+ );
3951
4563
  }
3952
-
3953
4564
  function useRegisterComponent(
3954
4565
  stateKey: string,
3955
4566
  componentId: string,
@@ -3958,25 +4569,19 @@ function useRegisterComponent(
3958
4569
  const fullComponentId = `${stateKey}////${componentId}`;
3959
4570
 
3960
4571
  useLayoutEffect(() => {
3961
- const rootMeta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
3962
- const components = rootMeta?.components || new Map();
4572
+ const { registerComponent, unregisterComponent } =
4573
+ getGlobalStore.getState();
3963
4574
 
3964
- components.set(fullComponentId, {
4575
+ // Call the safe, centralized function to register
4576
+ registerComponent(stateKey, fullComponentId, {
3965
4577
  forceUpdate: () => forceUpdate({}),
3966
4578
  paths: new Set(),
3967
4579
  reactiveType: ['component'],
3968
4580
  });
3969
4581
 
3970
- getGlobalStore.getState().setShadowMetadata(stateKey, [], {
3971
- ...rootMeta,
3972
- components,
3973
- });
3974
-
4582
+ // The cleanup now calls the safe, centralized unregister function
3975
4583
  return () => {
3976
- const meta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
3977
- if (meta?.components) {
3978
- meta.components.delete(fullComponentId);
3979
- }
4584
+ unregisterComponent(stateKey, fullComponentId);
3980
4585
  };
3981
- }, [stateKey, fullComponentId]);
4586
+ }, [stateKey, fullComponentId]); // Dependencies are stable and correct
3982
4587
  }