cogsbox-state 0.5.471 → 0.5.472

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
@@ -80,7 +80,11 @@ export type FormElementParams<T> = StateObject<T> & {
80
80
  HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
81
81
  >
82
82
  ) => void;
83
- onBlur?: () => void;
83
+ onBlur?: (
84
+ event: React.FocusEvent<
85
+ HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
86
+ >
87
+ ) => void;
84
88
  };
85
89
  };
86
90
 
@@ -243,8 +247,12 @@ export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
243
247
 
244
248
  type EffectFunction<T, R> = (state: T, deps: any[]) => R;
245
249
  export type EndType<T, IsArrayElement = false> = {
246
- $addZodValidation: (errors: ValidationError[]) => void;
250
+ $addZodValidation: (
251
+ errors: ValidationError[],
252
+ source?: 'client' | 'sync_engine' | 'api'
253
+ ) => void;
247
254
  $clearZodValidation: (paths?: string[]) => void;
255
+ $applyOperation: (operation: UpdateTypeDetail) => void;
248
256
  $applyJsonPatch: (patches: any[]) => void;
249
257
  $update: UpdateType<T>;
250
258
  $_path: string[];
@@ -345,8 +353,9 @@ export type UpdateTypeDetail = {
345
353
  oldValue: any;
346
354
  newValue: any;
347
355
  userId?: number;
356
+ itemId?: string; // For insert: the new item's ID
357
+ insertAfterId?: string; // For insert: ID to insert after (null = beginning)
348
358
  };
349
-
350
359
  export type ReactivityUnion = 'none' | 'component' | 'deps' | 'all';
351
360
  export type ReactivityType =
352
361
  | 'none'
@@ -366,8 +375,9 @@ type ValidationOptionsType = {
366
375
  key?: string;
367
376
  zodSchemaV3?: z3.ZodType<any, any, any>;
368
377
  zodSchemaV4?: z4.ZodType<any, any, any>;
369
-
370
- onBlur?: boolean;
378
+ onBlur?: 'error' | 'warning';
379
+ onChange?: 'error' | 'warning';
380
+ blockSync?: boolean;
371
381
  };
372
382
  type UseSyncType<T> = (state: T, a: SyncOptionsType<any>) => SyncApi;
373
383
  type SyncOptionsType<TApiParams> = {
@@ -542,6 +552,7 @@ function setAndMergeOptions(stateKey: string, newOptions: OptionsType<any>) {
542
552
  ...newOptions,
543
553
  });
544
554
  }
555
+
545
556
  function setOptions<StateKey, Opt>({
546
557
  stateKey,
547
558
  options,
@@ -554,44 +565,62 @@ function setOptions<StateKey, Opt>({
554
565
  const initialOptions = getInitialOptions(stateKey as string) || {};
555
566
  const initialOptionsPartState = initialOptionsPart[stateKey as string] || {};
556
567
 
557
- const mergedOptions = { ...initialOptionsPartState, ...initialOptions };
558
-
568
+ // Start with the base options
569
+ let mergedOptions = { ...initialOptionsPartState, ...initialOptions };
559
570
  let needToAdd = false;
571
+
560
572
  if (options) {
561
- for (const key in options) {
562
- if (!mergedOptions.hasOwnProperty(key)) {
563
- needToAdd = true;
564
- mergedOptions[key] = options[key as keyof typeof options];
565
- } else {
566
- if (
567
- key == 'localStorage' &&
568
- options[key] &&
569
- mergedOptions[key].key !== options[key]?.key
570
- ) {
571
- needToAdd = true;
572
- mergedOptions[key] = options[key];
573
- }
574
- if (
575
- key == 'defaultState' &&
576
- options[key] &&
577
- mergedOptions[key] !== options[key] &&
578
- !isDeepEqual(mergedOptions[key], options[key])
579
- ) {
580
- needToAdd = true;
581
- mergedOptions[key] = options[key];
573
+ // A function to recursively merge properties
574
+ const deepMerge = (target: any, source: any) => {
575
+ for (const key in source) {
576
+ if (source.hasOwnProperty(key)) {
577
+ // If the property is an object (and not an array), recurse
578
+ if (
579
+ source[key] instanceof Object &&
580
+ !Array.isArray(source[key]) &&
581
+ target[key] instanceof Object
582
+ ) {
583
+ // Check for changes before merging to set `needToAdd`
584
+ if (!isDeepEqual(target[key], source[key])) {
585
+ deepMerge(target[key], source[key]);
586
+ needToAdd = true;
587
+ }
588
+ } else {
589
+ // Overwrite if the value is different
590
+ if (target[key] !== source[key]) {
591
+ target[key] = source[key];
592
+ needToAdd = true;
593
+ }
594
+ }
582
595
  }
583
596
  }
584
- }
597
+ return target;
598
+ };
599
+
600
+ // Perform a deep merge
601
+ mergedOptions = deepMerge(mergedOptions, options);
585
602
  }
586
603
 
587
- // Always preserve syncOptions if it exists in mergedOptions but not in options
604
+ // Your existing logic for defaults and preservation can follow
588
605
  if (
589
606
  mergedOptions.syncOptions &&
590
607
  (!options || !options.hasOwnProperty('syncOptions'))
591
608
  ) {
592
609
  needToAdd = true;
593
610
  }
611
+ if (
612
+ (mergedOptions.validation && mergedOptions?.validation?.zodSchemaV4) ||
613
+ mergedOptions?.validation?.zodSchemaV3
614
+ ) {
615
+ // Only set default if onBlur wasn't explicitly provided
616
+ const wasOnBlurProvided =
617
+ options?.validation?.hasOwnProperty('onBlur') ||
618
+ initialOptions?.validation?.hasOwnProperty('onBlur');
594
619
 
620
+ if (!wasOnBlurProvided) {
621
+ mergedOptions.validation.onBlur = 'error'; // Default to error on blur
622
+ }
623
+ }
595
624
  if (needToAdd) {
596
625
  setInitialStateOptions(stateKey as string, mergedOptions);
597
626
  }
@@ -781,7 +810,8 @@ export function createCogsStateFromSync<
781
810
  schemas: Record<
782
811
  string,
783
812
  {
784
- schemas: { defaultValues: any };
813
+ schemas: { defaults: any };
814
+ relations?: any;
785
815
  api?: {
786
816
  queryData?: any;
787
817
  };
@@ -795,7 +825,13 @@ export function createCogsStateFromSync<
795
825
  useSync: UseSyncType<any>
796
826
  ): CogsApi<
797
827
  {
798
- [K in keyof TSyncSchema['schemas']]: TSyncSchema['schemas'][K]['schemas']['defaultValues'];
828
+ [K in keyof TSyncSchema['schemas']]: TSyncSchema['schemas'][K]['relations'] extends object
829
+ ? TSyncSchema['schemas'][K] extends {
830
+ schemas: { defaults: infer D };
831
+ }
832
+ ? D
833
+ : TSyncSchema['schemas'][K]['schemas']['defaults']
834
+ : TSyncSchema['schemas'][K]['schemas']['defaults'];
799
835
  },
800
836
  {
801
837
  [K in keyof TSyncSchema['schemas']]: GetParamType<
@@ -810,7 +846,16 @@ export function createCogsStateFromSync<
810
846
  // Extract defaultValues AND apiParams from each entry
811
847
  for (const key in schemas) {
812
848
  const entry = schemas[key];
813
- initialState[key] = entry?.schemas?.defaultValues || {};
849
+
850
+ // Check if we have relations and thus view defaults
851
+ if (entry?.relations && entry?.schemas?.defaults) {
852
+ // Use the view defaults when relations are present
853
+ initialState[key] = entry.schemas.defaults;
854
+ } else {
855
+ // Fall back to regular defaultValues
856
+ initialState[key] = entry?.schemas?.defaults || {};
857
+ }
858
+ console.log('initialState', initialState);
814
859
 
815
860
  // Extract apiParams from the api.queryData._paramType
816
861
  if (entry?.api?.queryData?._paramType) {
@@ -1069,78 +1114,34 @@ function getComponentNotifications(
1069
1114
 
1070
1115
  const componentsToNotify = new Set<any>();
1071
1116
 
1072
- // --- PASS 1: Notify specific subscribers based on update type ---
1073
-
1074
- if (result.type === 'update') {
1075
- // --- Bubble-up Notification ---
1076
- // An update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
1077
- let currentPath = [...path];
1078
- while (true) {
1079
- const pathMeta = getShadowMetadata(stateKey, currentPath);
1080
-
1081
- if (pathMeta?.pathComponents) {
1082
- pathMeta.pathComponents.forEach((componentId: string) => {
1083
- const component = rootMeta.components?.get(componentId);
1084
- // NEW: Add component to the set instead of calling forceUpdate()
1085
- if (component) {
1086
- const reactiveTypes = Array.isArray(component.reactiveType)
1087
- ? component.reactiveType
1088
- : [component.reactiveType || 'component'];
1089
- if (!reactiveTypes.includes('none')) {
1090
- componentsToNotify.add(component);
1091
- }
1092
- }
1093
- });
1094
- }
1117
+ // For insert operations, use the array path not the item path
1118
+ let notificationPath = path;
1119
+ if (result.type === 'insert' && result.itemId) {
1120
+ // We have the new structure with itemId separate
1121
+ notificationPath = path; // Already the array path
1122
+ }
1095
1123
 
1096
- if (currentPath.length === 0) break;
1097
- currentPath.pop(); // Go up one level
1098
- }
1124
+ // BUBBLE UP: Notify components at this path and all parent paths
1125
+ let currentPath = [...notificationPath];
1126
+ while (true) {
1127
+ const pathMeta = getShadowMetadata(stateKey, currentPath);
1099
1128
 
1100
- // --- Deep Object Change Notification ---
1101
- // If the new value is an object, notify components subscribed to sub-paths that changed.
1102
- if (
1103
- result.newValue &&
1104
- typeof result.newValue === 'object' &&
1105
- !isArray(result.newValue)
1106
- ) {
1107
- const changedSubPaths = getDifferences(result.newValue, result.oldValue);
1108
-
1109
- changedSubPaths.forEach((subPathString: string) => {
1110
- const subPath = subPathString.split('.');
1111
- const fullSubPath = [...path, ...subPath];
1112
- const subPathMeta = getShadowMetadata(stateKey, fullSubPath);
1113
-
1114
- if (subPathMeta?.pathComponents) {
1115
- subPathMeta.pathComponents.forEach((componentId: string) => {
1116
- const component = rootMeta.components?.get(componentId);
1117
- // NEW: Add component to the set
1118
- if (component) {
1119
- const reactiveTypes = Array.isArray(component.reactiveType)
1120
- ? component.reactiveType
1121
- : [component.reactiveType || 'component'];
1122
- if (!reactiveTypes.includes('none')) {
1123
- componentsToNotify.add(component);
1124
- }
1125
- }
1126
- });
1127
- }
1128
- });
1129
- }
1130
- } else if (result.type === 'insert' || result.type === 'cut') {
1131
- // For array structural changes (add/remove), notify components listening to the parent array.
1132
- const parentArrayPath = result.type === 'insert' ? path : path.slice(0, -1);
1133
- const parentMeta = getShadowMetadata(stateKey, parentArrayPath);
1134
-
1135
- if (parentMeta?.pathComponents) {
1136
- parentMeta.pathComponents.forEach((componentId: string) => {
1129
+ if (pathMeta?.pathComponents) {
1130
+ pathMeta.pathComponents.forEach((componentId: string) => {
1137
1131
  const component = rootMeta.components?.get(componentId);
1138
- // NEW: Add component to the set
1139
1132
  if (component) {
1140
- componentsToNotify.add(component);
1133
+ const reactiveTypes = Array.isArray(component.reactiveType)
1134
+ ? component.reactiveType
1135
+ : [component.reactiveType || 'component'];
1136
+ if (!reactiveTypes.includes('none')) {
1137
+ componentsToNotify.add(component);
1138
+ }
1141
1139
  }
1142
1140
  });
1143
1141
  }
1142
+
1143
+ if (currentPath.length === 0) break;
1144
+ currentPath.pop(); // Go up one level
1144
1145
  }
1145
1146
 
1146
1147
  // --- PASS 2: Handle 'all' and 'deps' reactivity types ---
@@ -1177,8 +1178,16 @@ function getComponentNotifications(
1177
1178
  function handleInsert(
1178
1179
  stateKey: string,
1179
1180
  path: string[],
1180
- payload: any
1181
- ): { type: 'insert'; newValue: any; shadowMeta: any } {
1181
+ payload: any,
1182
+ index?: number
1183
+ ): {
1184
+ type: 'insert';
1185
+ newValue: any;
1186
+ shadowMeta: any;
1187
+ path: string[];
1188
+ itemId: string;
1189
+ insertAfterId?: string;
1190
+ } {
1182
1191
  let newValue;
1183
1192
  if (isFunction(payload)) {
1184
1193
  const { value: currentValue } = getScopedData(stateKey, path);
@@ -1187,21 +1196,26 @@ function handleInsert(
1187
1196
  newValue = payload;
1188
1197
  }
1189
1198
 
1190
- insertShadowArrayElement(stateKey, path, newValue);
1199
+ const itemId = insertShadowArrayElement(stateKey, path, newValue, index);
1191
1200
  markAsDirty(stateKey, path, { bubble: true });
1192
1201
 
1193
1202
  const updatedMeta = getShadowMetadata(stateKey, path);
1194
- if (updatedMeta?.arrayKeys) {
1195
- const newItemKey = updatedMeta.arrayKeys[updatedMeta.arrayKeys.length - 1];
1196
- if (newItemKey) {
1197
- const newItemPath = newItemKey.split('.').slice(1);
1198
- markAsDirty(stateKey, newItemPath, { bubble: false });
1199
- }
1203
+
1204
+ // Find the ID that comes before this insertion point
1205
+ let insertAfterId: string | undefined;
1206
+ if (updatedMeta?.arrayKeys && index !== undefined && index > 0) {
1207
+ insertAfterId = updatedMeta.arrayKeys[index - 1];
1200
1208
  }
1201
1209
 
1202
- return { type: 'insert', newValue, shadowMeta: updatedMeta };
1210
+ return {
1211
+ type: 'insert',
1212
+ newValue,
1213
+ shadowMeta: updatedMeta,
1214
+ path: path, // Just the array path now
1215
+ itemId: itemId,
1216
+ insertAfterId: insertAfterId,
1217
+ };
1203
1218
  }
1204
-
1205
1219
  function handleCut(
1206
1220
  stateKey: string,
1207
1221
  path: string[]
@@ -1286,12 +1300,15 @@ function createEffectiveSetState<T>(
1286
1300
  switch (options.updateType) {
1287
1301
  case 'update':
1288
1302
  result = handleUpdate(stateKey, path, payload);
1303
+
1289
1304
  break;
1290
1305
  case 'insert':
1291
1306
  result = handleInsert(stateKey, path, payload);
1307
+
1292
1308
  break;
1293
1309
  case 'cut':
1294
1310
  result = handleCut(stateKey, path);
1311
+
1295
1312
  break;
1296
1313
  }
1297
1314
 
@@ -1308,6 +1325,8 @@ function createEffectiveSetState<T>(
1308
1325
  status: 'new',
1309
1326
  oldValue: result.oldValue,
1310
1327
  newValue: result.newValue ?? null,
1328
+ itemId: result.itemId,
1329
+ insertAfterId: result.insertAfterId,
1311
1330
  };
1312
1331
 
1313
1332
  updateBatchQueue.push(newUpdate);
@@ -1438,9 +1457,13 @@ export function useCogsStateFn<TStateObject extends unknown>(
1438
1457
 
1439
1458
  // Effect 1: When this component's serverState prop changes, broadcast it
1440
1459
  useEffect(() => {
1441
- setServerStateUpdate(thisKey, serverState);
1442
- }, [serverState, thisKey]);
1460
+ if (!serverState) return;
1443
1461
 
1462
+ // Only broadcast if we have valid server data
1463
+ if (serverState.status === 'success' && serverState.data !== undefined) {
1464
+ setServerStateUpdate(thisKey, serverState);
1465
+ }
1466
+ }, [serverState, thisKey]);
1444
1467
  // Effect 2: Listen for server state updates from ANY component
1445
1468
  useEffect(() => {
1446
1469
  const unsubscribe = getGlobalStore
@@ -1456,13 +1479,14 @@ export function useCogsStateFn<TStateObject extends unknown>(
1456
1479
  return; // Ignore if no valid data
1457
1480
  }
1458
1481
 
1482
+ // Store the server state in options for future reference
1459
1483
  setAndMergeOptions(thisKey, { serverState: serverStateData });
1460
1484
 
1461
1485
  const mergeConfig =
1462
1486
  typeof serverStateData.merge === 'object'
1463
1487
  ? serverStateData.merge
1464
1488
  : serverStateData.merge === true
1465
- ? { strategy: 'append' }
1489
+ ? { strategy: 'append' as const, key: 'id' }
1466
1490
  : null;
1467
1491
 
1468
1492
  const currentState = getShadowValue(thisKey, []);
@@ -1471,7 +1495,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1471
1495
  if (
1472
1496
  mergeConfig &&
1473
1497
  mergeConfig.strategy === 'append' &&
1474
- 'key' in mergeConfig && // Type guard for key
1498
+ 'key' in mergeConfig &&
1475
1499
  Array.isArray(currentState) &&
1476
1500
  Array.isArray(incomingData)
1477
1501
  ) {
@@ -1483,49 +1507,57 @@ export function useCogsStateFn<TStateObject extends unknown>(
1483
1507
  return;
1484
1508
  }
1485
1509
 
1510
+ // Get existing IDs to check for duplicates
1486
1511
  const existingIds = new Set(
1487
1512
  currentState.map((item: any) => item[keyField])
1488
1513
  );
1489
1514
 
1515
+ // Filter out duplicates from incoming data
1490
1516
  const newUniqueItems = incomingData.filter(
1491
1517
  (item: any) => !existingIds.has(item[keyField])
1492
1518
  );
1493
1519
 
1494
1520
  if (newUniqueItems.length > 0) {
1521
+ // Insert only the new unique items
1495
1522
  insertManyShadowArrayElements(thisKey, [], newUniqueItems);
1496
1523
  }
1497
1524
 
1498
- // Mark the entire final state as synced
1525
+ // Mark the entire merged state as synced
1499
1526
  const finalState = getShadowValue(thisKey, []);
1500
1527
  markEntireStateAsServerSynced(
1501
1528
  thisKey,
1502
1529
  [],
1503
1530
  finalState,
1504
- serverStateData.timestamp
1531
+ serverStateData.timestamp || Date.now()
1505
1532
  );
1506
1533
  } else {
1507
- // This handles the "replace" strategy (initial load)
1534
+ // Replace strategy (default) - completely replace the state
1508
1535
  initializeShadowState(thisKey, incomingData);
1509
1536
 
1537
+ // Mark as synced from server
1510
1538
  markEntireStateAsServerSynced(
1511
1539
  thisKey,
1512
1540
  [],
1513
1541
  incomingData,
1514
- serverStateData.timestamp
1542
+ serverStateData.timestamp || Date.now()
1515
1543
  );
1516
1544
  }
1545
+
1546
+ // Notify all components subscribed to this state
1547
+ notifyComponents(thisKey);
1517
1548
  }
1518
1549
  });
1519
1550
 
1520
1551
  return unsubscribe;
1521
- }, [thisKey, resolveInitialState]);
1522
-
1552
+ }, [thisKey]);
1523
1553
  useEffect(() => {
1524
1554
  const existingMeta = getGlobalStore
1525
1555
  .getState()
1526
1556
  .getShadowMetadata(thisKey, []);
1557
+
1558
+ // Skip if already initialized
1527
1559
  if (existingMeta && existingMeta.stateSource) {
1528
- return; // Already initialized, bail out.
1560
+ return;
1529
1561
  }
1530
1562
 
1531
1563
  const options = getInitialOptions(thisKey as string);
@@ -1537,34 +1569,35 @@ export function useCogsStateFn<TStateObject extends unknown>(
1537
1569
  ),
1538
1570
  localStorageEnabled: !!options?.localStorage?.key,
1539
1571
  };
1572
+
1540
1573
  setShadowMetadata(thisKey, [], {
1541
1574
  ...existingMeta,
1542
1575
  features,
1543
1576
  });
1577
+
1544
1578
  if (options?.defaultState !== undefined || defaultState !== undefined) {
1545
1579
  const finalDefaultState = options?.defaultState || defaultState;
1546
-
1547
- // Only set defaultState if it's not already set
1548
1580
  if (!options?.defaultState) {
1549
1581
  setAndMergeOptions(thisKey as string, {
1550
1582
  defaultState: finalDefaultState,
1551
1583
  });
1552
1584
  }
1585
+ }
1553
1586
 
1554
- const { value: resolvedState, source, timestamp } = resolveInitialState();
1555
-
1556
- initializeShadowState(thisKey, resolvedState);
1557
-
1558
- // Set shadow metadata with the correct source info
1559
- setShadowMetadata(thisKey, [], {
1560
- stateSource: source,
1561
- lastServerSync: source === 'server' ? timestamp : undefined,
1562
- isDirty: false,
1563
- baseServerState: source === 'server' ? resolvedState : undefined,
1564
- });
1587
+ const { value: resolvedState, source, timestamp } = resolveInitialState();
1588
+ initializeShadowState(thisKey, resolvedState);
1589
+ setShadowMetadata(thisKey, [], {
1590
+ stateSource: source,
1591
+ lastServerSync: source === 'server' ? timestamp : undefined,
1592
+ isDirty: source === 'server' ? false : undefined,
1593
+ baseServerState: source === 'server' ? resolvedState : undefined,
1594
+ });
1565
1595
 
1566
- notifyComponents(thisKey);
1596
+ if (source === 'server' && serverState) {
1597
+ setServerStateUpdate(thisKey, serverState);
1567
1598
  }
1599
+
1600
+ notifyComponents(thisKey);
1568
1601
  }, [thisKey, ...(dependencies || [])]);
1569
1602
 
1570
1603
  useLayoutEffect(() => {
@@ -1658,7 +1691,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1658
1691
 
1659
1692
  const cogsSyncFn = __useSync;
1660
1693
  const syncOpt = latestInitialOptionsRef.current?.syncOptions;
1661
-
1694
+ console.log('syncOpt', syncOpt);
1662
1695
  if (cogsSyncFn) {
1663
1696
  syncApiRef.current = cogsSyncFn(
1664
1697
  updaterFinal as any,
@@ -1919,15 +1952,11 @@ function createProxyHandler<T>(
1919
1952
  const getStatusFunc = () => {
1920
1953
  // ✅ Use the optimized helper to get all data in one efficient call
1921
1954
  const { shadowMeta, value } = getScopedData(stateKey, path, meta);
1922
-
1923
- // Priority 1: Explicitly dirty items. This is the most important status.
1955
+ console.log('getStatusFunc', path, shadowMeta, value);
1924
1956
  if (shadowMeta?.isDirty === true) {
1925
1957
  return 'dirty';
1926
1958
  }
1927
1959
 
1928
- // ✅ Priority 2: Synced items. This condition is now cleaner.
1929
- // An item is considered synced if it came from the server OR was explicitly
1930
- // marked as not dirty (isDirty: false), covering all sync-related cases.
1931
1960
  if (
1932
1961
  shadowMeta?.stateSource === 'server' ||
1933
1962
  shadowMeta?.isDirty === false
@@ -1935,20 +1964,15 @@ function createProxyHandler<T>(
1935
1964
  return 'synced';
1936
1965
  }
1937
1966
 
1938
- // Priority 3: Items restored from localStorage.
1939
1967
  if (shadowMeta?.stateSource === 'localStorage') {
1940
1968
  return 'restored';
1941
1969
  }
1942
1970
 
1943
- // Priority 4: Items from default/initial state.
1944
1971
  if (shadowMeta?.stateSource === 'default') {
1945
1972
  return 'fresh';
1946
1973
  }
1947
1974
 
1948
- // REMOVED the redundant "root" check. The item's own `stateSource` is sufficient.
1949
-
1950
- // Priority 5: A value exists but has no metadata. This is a fallback.
1951
- if (value !== undefined && !shadowMeta) {
1975
+ if (value !== undefined) {
1952
1976
  return 'fresh';
1953
1977
  }
1954
1978
 
@@ -2425,7 +2449,7 @@ function createProxyHandler<T>(
2425
2449
  path,
2426
2450
  meta
2427
2451
  );
2428
-
2452
+ registerComponentDependency(stateKey, componentId, path);
2429
2453
  if (!arrayKeys || !Array.isArray(shadowValue)) {
2430
2454
  return []; // It's valid to map over an empty array.
2431
2455
  }
@@ -3110,7 +3134,10 @@ function createProxyHandler<T>(
3110
3134
  }
3111
3135
  if (path.length == 0) {
3112
3136
  if (prop === '$addZodValidation') {
3113
- return (zodErrors: any[]) => {
3137
+ return (
3138
+ zodErrors: any[],
3139
+ source: 'client' | 'sync_engine' | 'api'
3140
+ ) => {
3114
3141
  zodErrors.forEach((error) => {
3115
3142
  const currentMeta =
3116
3143
  getGlobalStore
@@ -3125,7 +3152,7 @@ function createProxyHandler<T>(
3125
3152
  status: 'INVALID',
3126
3153
  errors: [
3127
3154
  {
3128
- source: 'client',
3155
+ source: source || 'client',
3129
3156
  message: error.message,
3130
3157
  severity: 'error',
3131
3158
  code: error.code,
@@ -3156,6 +3183,51 @@ function createProxyHandler<T>(
3156
3183
  });
3157
3184
  };
3158
3185
  }
3186
+
3187
+ if (prop === '$applyOperation') {
3188
+ return (operation: UpdateTypeDetail & { validation?: any[] }) => {
3189
+ const validationErrorsFromServer = operation.validation || [];
3190
+
3191
+ if (!operation || !operation.path) {
3192
+ console.error(
3193
+ 'Invalid operation received by $applyOperation:',
3194
+ operation
3195
+ );
3196
+ return;
3197
+ }
3198
+
3199
+ const updatePath = operation.path;
3200
+
3201
+ const currentMeta =
3202
+ getGlobalStore
3203
+ .getState()
3204
+ .getShadowMetadata(stateKey, updatePath) || {};
3205
+
3206
+ const newErrors: ValidationError[] =
3207
+ validationErrorsFromServer.map((err) => ({
3208
+ source: 'sync_engine',
3209
+ message: err.message,
3210
+ severity: 'warning',
3211
+ code: err.code,
3212
+ }));
3213
+
3214
+ console.log('updatePath', updatePath);
3215
+ getGlobalStore
3216
+ .getState()
3217
+ .setShadowMetadata(stateKey, updatePath, {
3218
+ ...currentMeta,
3219
+ validation: {
3220
+ status: newErrors.length > 0 ? 'INVALID' : 'VALID',
3221
+ errors: newErrors,
3222
+ lastValidated: Date.now(),
3223
+ },
3224
+ });
3225
+ effectiveSetState(operation.newValue, updatePath, {
3226
+ updateType: operation.updateType,
3227
+ sync: false,
3228
+ });
3229
+ };
3230
+ }
3159
3231
  if (prop === '$applyJsonPatch') {
3160
3232
  return (patches: Operation[]) => {
3161
3233
  const store = getGlobalStore.getState();
@@ -3289,7 +3361,6 @@ function createProxyHandler<T>(
3289
3361
  .getState()
3290
3362
  .getShadowMetadata(stateKey, path);
3291
3363
 
3292
- // Update the metadata for this specific path
3293
3364
  setShadowMetadata(stateKey, path, {
3294
3365
  ...shadowMeta,
3295
3366
  isDirty: false,
@@ -3297,7 +3368,6 @@ function createProxyHandler<T>(
3297
3368
  lastServerSync: Date.now(),
3298
3369
  });
3299
3370
 
3300
- // Notify any components that might be subscribed to the sync status
3301
3371
  const fullPath = [stateKey, ...path].join('.');
3302
3372
  notifyPathSubscribers(fullPath, {
3303
3373
  type: 'SYNC_STATUS_CHANGE',
@@ -3371,21 +3441,15 @@ function createProxyHandler<T>(
3371
3441
  .getShadowMetadata(stateKey, []);
3372
3442
  let revertState;
3373
3443
 
3374
- // Determine the correct state to revert to (same logic as before)
3375
3444
  if (shadowMeta?.stateSource === 'server' && shadowMeta.baseServerState) {
3376
3445
  revertState = shadowMeta.baseServerState;
3377
3446
  } else {
3378
3447
  revertState = getGlobalStore.getState().initialStateGlobal[stateKey];
3379
3448
  }
3380
3449
 
3381
- // Perform necessary cleanup
3382
3450
  clearSelectedIndexesForState(stateKey);
3383
-
3384
- // FIX 1: Use the IMMEDIATE, SYNCHRONOUS state reset function.
3385
- // This is what your tests expect for a clean slate.
3386
3451
  initializeShadowState(stateKey, revertState);
3387
3452
 
3388
- // Rebuild the proxy's internal shape after the reset
3389
3453
  rebuildStateShape({
3390
3454
  path: [],
3391
3455
  componentId: outerComponentId!,
@@ -3401,8 +3465,6 @@ function createProxyHandler<T>(
3401
3465
  localStorage.removeItem(storageKey);
3402
3466
  }
3403
3467
 
3404
- // FIX 2: Use the library's BATCHED notification system instead of a manual forceUpdate loop.
3405
- // This fixes the original infinite loop bug safely.
3406
3468
  notifyComponents(stateKey);
3407
3469
 
3408
3470
  return revertState;
@@ -3488,7 +3550,6 @@ function SignalRenderer({
3488
3550
 
3489
3551
  const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
3490
3552
 
3491
- // Setup effect - runs only once
3492
3553
  useEffect(() => {
3493
3554
  const element = elementRef.current;
3494
3555
  if (!element || isSetupRef.current) return;
@@ -3511,7 +3572,6 @@ function SignalRenderer({
3511
3572
 
3512
3573
  instanceIdRef.current = `instance-${crypto.randomUUID()}`;
3513
3574
 
3514
- // Store signal info in shadow metadata
3515
3575
  const currentMeta =
3516
3576
  getGlobalStore
3517
3577
  .getState()