cogsbox-state 0.5.434 → 0.5.435

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
@@ -30,7 +30,7 @@ import { z } from 'zod';
30
30
 
31
31
  import { formRefStore, getGlobalStore, type ComponentsType } from './store.js';
32
32
  import { useCogsConfig } from './CogsStateClient.js';
33
- import { applyPatch } from 'fast-json-patch';
33
+ import { applyPatch, compare, Operation } from 'fast-json-patch';
34
34
  import { useInView } from 'react-intersection-observer';
35
35
 
36
36
  type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
@@ -231,7 +231,7 @@ export type UpdateArg<S> = S | ((prevState: S) => S);
231
231
  export type InsertParams<S> =
232
232
  | S
233
233
  | ((prevState: { state: S; uuid: string }) => S);
234
- export type UpdateType<T> = (payload: UpdateArg<T>) => void;
234
+ export type UpdateType<T> = (payload: UpdateArg<T>) => { synced: () => void };
235
235
 
236
236
  export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
237
237
  export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
@@ -351,11 +351,17 @@ type ValidationOptionsType = {
351
351
  zodSchema?: z.ZodTypeAny;
352
352
  onBlur?: boolean;
353
353
  };
354
-
354
+ // Define the return type of the sync hook locally
355
+ type SyncApi = {
356
+ updateState: (data: { operation: any }) => void;
357
+ connected: boolean;
358
+ clientId: string | null;
359
+ subscribers: string[];
360
+ };
355
361
  export type OptionsType<T extends unknown = unknown> = {
356
362
  log?: boolean;
357
363
  componentId?: string;
358
- serverSync?: ServerSyncType<T>;
364
+ cogsSync?: (stateObject: StateObject<T>) => SyncApi;
359
365
  validation?: ValidationOptionsType;
360
366
 
361
367
  serverState?: {
@@ -405,19 +411,6 @@ export type OptionsType<T extends unknown = unknown> = {
405
411
  defaultState?: T;
406
412
  dependencies?: any[];
407
413
  };
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
414
 
422
415
  export type ValidationWrapperOptions<T extends unknown = unknown> = {
423
416
  children: React.ReactNode;
@@ -782,12 +775,172 @@ export const notifyComponent = (stateKey: string, componentId: string) => {
782
775
  }
783
776
  }
784
777
  };
778
+ function markEntireStateAsServerSynced(
779
+ stateKey: string,
780
+ path: string[],
781
+ data: any,
782
+ timestamp: number
783
+ ) {
784
+ const store = getGlobalStore.getState();
785
+
786
+ // Mark current path as synced
787
+ const currentMeta = store.getShadowMetadata(stateKey, path);
788
+ store.setShadowMetadata(stateKey, path, {
789
+ ...currentMeta,
790
+ isDirty: false,
791
+ stateSource: 'server',
792
+ lastServerSync: timestamp || Date.now(),
793
+ });
794
+
795
+ // If it's an array, mark each item as synced
796
+ if (Array.isArray(data)) {
797
+ const arrayMeta = store.getShadowMetadata(stateKey, path);
798
+ if (arrayMeta?.arrayKeys) {
799
+ arrayMeta.arrayKeys.forEach((itemKey, index) => {
800
+ const itemPath = itemKey.split('.').slice(1);
801
+ const itemData = data[index];
802
+ if (itemData !== undefined) {
803
+ markEntireStateAsServerSynced(
804
+ stateKey,
805
+ itemPath,
806
+ itemData,
807
+ timestamp
808
+ );
809
+ }
810
+ });
811
+ }
812
+ }
813
+ // If it's an object, mark each field as synced
814
+ else if (data && typeof data === 'object' && data.constructor === Object) {
815
+ Object.keys(data).forEach((key) => {
816
+ const fieldPath = [...path, key];
817
+ const fieldData = data[key];
818
+ markEntireStateAsServerSynced(stateKey, fieldPath, fieldData, timestamp);
819
+ });
820
+ }
821
+ }
822
+
823
+ const _notifySubscribedComponents = (
824
+ stateKey: string,
825
+ path: string[],
826
+ updateType: 'update' | 'insert' | 'cut',
827
+ oldValue: any,
828
+ newValue: any
829
+ ) => {
830
+ const store = getGlobalStore.getState();
831
+ const rootMeta = store.getShadowMetadata(stateKey, []);
832
+ if (!rootMeta?.components) {
833
+ return;
834
+ }
835
+
836
+ const notifiedComponents = new Set<string>();
837
+ const shadowMeta = store.getShadowMetadata(stateKey, path);
838
+
839
+ // --- PASS 1: Notify specific subscribers based on update type ---
840
+
841
+ if (updateType === 'update') {
842
+ if (shadowMeta?.pathComponents) {
843
+ shadowMeta.pathComponents.forEach((componentId) => {
844
+ if (notifiedComponents.has(componentId)) return;
845
+ const component = rootMeta.components?.get(componentId);
846
+ if (component) {
847
+ const reactiveTypes = Array.isArray(component.reactiveType)
848
+ ? component.reactiveType
849
+ : [component.reactiveType || 'component'];
850
+ if (!reactiveTypes.includes('none')) {
851
+ component.forceUpdate();
852
+ notifiedComponents.add(componentId);
853
+ }
854
+ }
855
+ });
856
+ }
857
+
858
+ if (
859
+ newValue &&
860
+ typeof newValue === 'object' &&
861
+ !isArray(newValue) &&
862
+ oldValue &&
863
+ typeof oldValue === 'object' &&
864
+ !isArray(oldValue)
865
+ ) {
866
+ const changedSubPaths = getDifferences(newValue, oldValue);
867
+ changedSubPaths.forEach((subPathString) => {
868
+ const subPath = subPathString.split('.');
869
+ const fullSubPath = [...path, ...subPath];
870
+ const subPathMeta = store.getShadowMetadata(stateKey, fullSubPath);
871
+ if (subPathMeta?.pathComponents) {
872
+ subPathMeta.pathComponents.forEach((componentId) => {
873
+ if (notifiedComponents.has(componentId)) return;
874
+ const component = rootMeta.components?.get(componentId);
875
+ if (component) {
876
+ const reactiveTypes = Array.isArray(component.reactiveType)
877
+ ? component.reactiveType
878
+ : [component.reactiveType || 'component'];
879
+ if (!reactiveTypes.includes('none')) {
880
+ component.forceUpdate();
881
+ notifiedComponents.add(componentId);
882
+ }
883
+ }
884
+ });
885
+ }
886
+ });
887
+ }
888
+ } else if (updateType === 'insert' || updateType === 'cut') {
889
+ const parentArrayPath = updateType === 'insert' ? path : path.slice(0, -1);
890
+ const parentMeta = store.getShadowMetadata(stateKey, parentArrayPath);
891
+ if (parentMeta?.pathComponents) {
892
+ parentMeta.pathComponents.forEach((componentId) => {
893
+ if (!notifiedComponents.has(componentId)) {
894
+ const component = rootMeta.components?.get(componentId);
895
+ if (component) {
896
+ component.forceUpdate();
897
+ notifiedComponents.add(componentId);
898
+ }
899
+ }
900
+ });
901
+ }
902
+ }
903
+
904
+ // --- PASS 2: Notify global subscribers ('all', 'deps') ---
905
+
906
+ rootMeta.components.forEach((component, componentId) => {
907
+ if (notifiedComponents.has(componentId)) {
908
+ return;
909
+ }
910
+
911
+ const reactiveTypes = Array.isArray(component.reactiveType)
912
+ ? component.reactiveType
913
+ : [component.reactiveType || 'component'];
914
+
915
+ if (reactiveTypes.includes('all')) {
916
+ component.forceUpdate();
917
+ notifiedComponents.add(componentId);
918
+ return;
919
+ }
920
+
921
+ if (reactiveTypes.includes('deps')) {
922
+ if (component.depsFunction) {
923
+ const currentState = store.getShadowValue(stateKey);
924
+ const newDeps = component.depsFunction(currentState);
925
+ let shouldUpdate = false;
926
+ if (newDeps === true || !isDeepEqual(component.prevDeps, newDeps)) {
927
+ if (Array.isArray(newDeps)) component.prevDeps = newDeps;
928
+ shouldUpdate = true;
929
+ }
930
+ if (shouldUpdate) {
931
+ component.forceUpdate();
932
+ notifiedComponents.add(componentId);
933
+ }
934
+ }
935
+ }
936
+ });
937
+ };
785
938
 
786
939
  export function useCogsStateFn<TStateObject extends unknown>(
787
940
  stateObject: TStateObject,
788
941
  {
789
942
  stateKey,
790
- serverSync,
943
+
791
944
  localStorage,
792
945
  formElements,
793
946
  reactiveDeps,
@@ -901,7 +1054,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
901
1054
  const unsubscribe = getGlobalStore
902
1055
  .getState()
903
1056
  .subscribeToPath(thisKey, (event) => {
904
- // REPLACEMENT STARTS HERE
905
1057
  if (event?.type === 'SERVER_STATE_UPDATE') {
906
1058
  const serverStateData = event.serverState;
907
1059
 
@@ -912,7 +1064,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
912
1064
  const newOptions = { serverState: serverStateData };
913
1065
  setAndMergeOptions(thisKey, newOptions);
914
1066
 
915
- // Check for a merge request.
916
1067
  const mergeConfig =
917
1068
  typeof serverStateData.merge === 'object'
918
1069
  ? serverStateData.merge
@@ -920,20 +1071,17 @@ export function useCogsStateFn<TStateObject extends unknown>(
920
1071
  ? { strategy: 'append' }
921
1072
  : null;
922
1073
 
923
- // Get the current array and the new data.
924
1074
  const currentState = getGlobalStore
925
1075
  .getState()
926
1076
  .getShadowValue(thisKey);
927
1077
  const incomingData = serverStateData.data;
928
1078
 
929
- // *** THE REAL FIX: PERFORM INCREMENTAL INSERTS ***
930
1079
  if (
931
1080
  mergeConfig &&
932
1081
  Array.isArray(currentState) &&
933
1082
  Array.isArray(incomingData)
934
1083
  ) {
935
1084
  const keyField = mergeConfig.key || 'id';
936
-
937
1085
  const existingIds = new Set(
938
1086
  currentState.map((item: any) => item[keyField])
939
1087
  );
@@ -941,21 +1089,72 @@ export function useCogsStateFn<TStateObject extends unknown>(
941
1089
  const newUniqueItems = incomingData.filter((item: any) => {
942
1090
  return !existingIds.has(item[keyField]);
943
1091
  });
944
- console.log('newUniqueItems', newUniqueItems);
1092
+
945
1093
  if (newUniqueItems.length > 0) {
946
1094
  newUniqueItems.forEach((item) => {
947
1095
  getGlobalStore
948
1096
  .getState()
949
1097
  .insertShadowArrayElement(thisKey, [], item);
1098
+
1099
+ // MARK NEW SERVER ITEMS AS SYNCED
1100
+ const arrayMeta = getGlobalStore
1101
+ .getState()
1102
+ .getShadowMetadata(thisKey, []);
1103
+
1104
+ if (arrayMeta?.arrayKeys) {
1105
+ const newItemKey =
1106
+ arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
1107
+ if (newItemKey) {
1108
+ const newItemPath = newItemKey.split('.').slice(1);
1109
+
1110
+ // Mark the new server item as synced, not dirty
1111
+ getGlobalStore
1112
+ .getState()
1113
+ .setShadowMetadata(thisKey, newItemPath, {
1114
+ isDirty: false,
1115
+ stateSource: 'server',
1116
+ lastServerSync:
1117
+ serverStateData.timestamp || Date.now(),
1118
+ });
1119
+
1120
+ // Also mark all its child fields as synced if it's an object
1121
+ const itemValue = getGlobalStore
1122
+ .getState()
1123
+ .getShadowValue(newItemKey);
1124
+ if (
1125
+ itemValue &&
1126
+ typeof itemValue === 'object' &&
1127
+ !Array.isArray(itemValue)
1128
+ ) {
1129
+ Object.keys(itemValue).forEach((fieldKey) => {
1130
+ const fieldPath = [...newItemPath, fieldKey];
1131
+ getGlobalStore
1132
+ .getState()
1133
+ .setShadowMetadata(thisKey, fieldPath, {
1134
+ isDirty: false,
1135
+ stateSource: 'server',
1136
+ lastServerSync:
1137
+ serverStateData.timestamp || Date.now(),
1138
+ });
1139
+ });
1140
+ }
1141
+ }
1142
+ }
950
1143
  });
951
- } else {
952
- // No new items, no need to do anything.
953
- return;
954
1144
  }
955
1145
  } else {
1146
+ // For replace strategy or initial load
956
1147
  getGlobalStore
957
1148
  .getState()
958
1149
  .initializeShadowState(thisKey, incomingData);
1150
+
1151
+ // Mark the entire state tree as synced from server
1152
+ markEntireStateAsServerSynced(
1153
+ thisKey,
1154
+ [],
1155
+ incomingData,
1156
+ serverStateData.timestamp
1157
+ );
959
1158
  }
960
1159
 
961
1160
  const meta = getGlobalStore
@@ -1012,7 +1211,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
1012
1211
  useLayoutEffect(() => {
1013
1212
  if (noStateKey) {
1014
1213
  setAndMergeOptions(thisKey as string, {
1015
- serverSync,
1016
1214
  formElements,
1017
1215
  defaultState,
1018
1216
  localStorage,
@@ -1079,6 +1277,8 @@ export function useCogsStateFn<TStateObject extends unknown>(
1079
1277
  }
1080
1278
  };
1081
1279
  }, []);
1280
+
1281
+ const syncApiRef = useRef<SyncApi | null>(null);
1082
1282
  const effectiveSetState = (
1083
1283
  newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
1084
1284
  path: string[],
@@ -1122,24 +1322,38 @@ export function useCogsStateFn<TStateObject extends unknown>(
1122
1322
  store.insertShadowArrayElement(thisKey, path, newUpdate.newValue);
1123
1323
  // The array at `path` has been modified. Mark it AND all its parents as dirty.
1124
1324
  store.markAsDirty(thisKey, path, { bubble: true });
1325
+
1326
+ // ALSO mark the newly inserted item itself as dirty
1327
+ // Get the new item's path and mark it as dirty
1328
+ const arrayMeta = store.getShadowMetadata(thisKey, path);
1329
+ if (arrayMeta?.arrayKeys) {
1330
+ const newItemKey =
1331
+ arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
1332
+ if (newItemKey) {
1333
+ const newItemPath = newItemKey.split('.').slice(1); // Remove stateKey
1334
+ store.markAsDirty(thisKey, newItemPath, { bubble: false });
1335
+ }
1336
+ }
1125
1337
  break;
1126
1338
  }
1127
1339
  case 'cut': {
1128
- // The item is at `path`, so the parent array is at `path.slice(0, -1)`
1129
1340
  const parentArrayPath = path.slice(0, -1);
1341
+
1130
1342
  store.removeShadowArrayElement(thisKey, path);
1131
- // The parent array has been modified. Mark it AND all its parents as dirty.
1132
1343
  store.markAsDirty(thisKey, parentArrayPath, { bubble: true });
1133
1344
  break;
1134
1345
  }
1135
1346
  case 'update': {
1136
1347
  store.updateShadowAtPath(thisKey, path, newUpdate.newValue);
1137
- // The item at `path` was updated. Mark it AND all its parents as dirty.
1138
1348
  store.markAsDirty(thisKey, path, { bubble: true });
1139
1349
  break;
1140
1350
  }
1141
1351
  }
1142
1352
 
1353
+ console.log('sdadasdasd', syncApiRef.current, newUpdate);
1354
+ if (syncApiRef.current && syncApiRef.current.connected) {
1355
+ syncApiRef.current.updateState({ operation: newUpdate });
1356
+ }
1143
1357
  // Handle signals - reuse shadowMeta from the beginning
1144
1358
  if (shadowMeta?.signals && shadowMeta.signals.length > 0) {
1145
1359
  // Use updatedShadowValue if we need the new value, otherwise use payload
@@ -1581,6 +1795,11 @@ export function useCogsStateFn<TStateObject extends unknown>(
1581
1795
  );
1582
1796
  }, [thisKey, sessionId]);
1583
1797
 
1798
+ const cogsSyncFn = latestInitialOptionsRef.current?.cogsSync;
1799
+ if (cogsSyncFn) {
1800
+ syncApiRef.current = cogsSyncFn(updaterFinal);
1801
+ }
1802
+
1584
1803
  return updaterFinal;
1585
1804
  }
1586
1805
 
@@ -1896,21 +2115,56 @@ function createProxyHandler<T>(
1896
2115
  }
1897
2116
  };
1898
2117
  }
2118
+ // Fixed getStatus function in createProxyHandler
1899
2119
  if (prop === '_status' || prop === 'getStatus') {
1900
2120
  const getStatusFunc = () => {
1901
2121
  const shadowMeta = getGlobalStore
1902
2122
  .getState()
1903
- .getShadowMetadata(stateKey, []);
2123
+ .getShadowMetadata(stateKey, path);
2124
+ const value = getGlobalStore
2125
+ .getState()
2126
+ .getShadowValue(stateKeyPathKey);
1904
2127
 
1905
- if (shadowMeta?.stateSource === 'server' && !shadowMeta.isDirty) {
1906
- return 'synced';
1907
- } else if (shadowMeta?.isDirty) {
2128
+ // Priority 1: Explicitly dirty items
2129
+ if (shadowMeta?.isDirty === true) {
1908
2130
  return 'dirty';
1909
- } else if (shadowMeta?.stateSource === 'localStorage') {
2131
+ }
2132
+
2133
+ // Priority 2: Explicitly synced items (isDirty: false)
2134
+ if (shadowMeta?.isDirty === false) {
2135
+ return 'synced';
2136
+ }
2137
+
2138
+ // Priority 3: Items from server source (should be synced even without explicit isDirty flag)
2139
+ if (shadowMeta?.stateSource === 'server') {
2140
+ return 'synced';
2141
+ }
2142
+
2143
+ // Priority 4: Items restored from localStorage
2144
+ if (shadowMeta?.stateSource === 'localStorage') {
1910
2145
  return 'restored';
1911
- } else if (shadowMeta?.stateSource === 'default') {
2146
+ }
2147
+
2148
+ // Priority 5: Items from default/initial state
2149
+ if (shadowMeta?.stateSource === 'default') {
1912
2150
  return 'fresh';
1913
2151
  }
2152
+
2153
+ // Priority 6: Check if this is part of initial server load
2154
+ // Look up the tree to see if parent has server source
2155
+ const rootMeta = getGlobalStore
2156
+ .getState()
2157
+ .getShadowMetadata(stateKey, []);
2158
+ if (rootMeta?.stateSource === 'server' && !shadowMeta?.isDirty) {
2159
+ return 'synced';
2160
+ }
2161
+
2162
+ // Priority 7: If no metadata exists but value exists, it's probably fresh
2163
+ if (value !== undefined && !shadowMeta) {
2164
+ return 'fresh';
2165
+ }
2166
+
2167
+ // Fallback
1914
2168
  return 'unknown';
1915
2169
  };
1916
2170
 
@@ -2457,6 +2711,49 @@ function createProxyHandler<T>(
2457
2711
  },
2458
2712
  rebuildStateShape,
2459
2713
  });
2714
+ } // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
2715
+
2716
+ if (prop === 'stateFind') {
2717
+ return (
2718
+ callbackfn: (value: any, index: number) => boolean
2719
+ ): StateObject<any> | undefined => {
2720
+ // 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
2721
+ const arrayKeys =
2722
+ meta?.validIds ??
2723
+ getGlobalStore.getState().getShadowMetadata(stateKey, path)
2724
+ ?.arrayKeys;
2725
+
2726
+ if (!arrayKeys) {
2727
+ return undefined;
2728
+ }
2729
+
2730
+ // 2. Iterate through the keys, get the value for each, and run the callback.
2731
+ for (let i = 0; i < arrayKeys.length; i++) {
2732
+ const itemKey = arrayKeys[i];
2733
+ if (!itemKey) continue; // Safety check
2734
+
2735
+ const itemValue = getGlobalStore
2736
+ .getState()
2737
+ .getShadowValue(itemKey);
2738
+
2739
+ // 3. If the callback returns true, we've found our item.
2740
+ if (callbackfn(itemValue, i)) {
2741
+ // Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
2742
+ const itemPath = itemKey.split('.').slice(1);
2743
+
2744
+ // 4. Rebuild a new, fully functional StateObject for just that item and return it.
2745
+ return rebuildStateShape({
2746
+ currentState: itemValue,
2747
+ path: itemPath,
2748
+ componentId: componentId,
2749
+ meta, // Pass along meta for potential further chaining
2750
+ });
2751
+ }
2752
+ }
2753
+
2754
+ // 5. If the loop finishes without finding anything, return undefined.
2755
+ return undefined;
2756
+ };
2460
2757
  }
2461
2758
  if (prop === 'stateFilter') {
2462
2759
  return (callbackfn: (value: any, index: number) => boolean) => {
@@ -2691,13 +2988,13 @@ function createProxyHandler<T>(
2691
2988
  arrayValues: freshValues || [],
2692
2989
  };
2693
2990
  }, [cacheKey, updateTrigger]);
2694
-
2991
+ console.log('freshValues', validIds, arrayValues);
2695
2992
  useEffect(() => {
2696
2993
  const unsubscribe = getGlobalStore
2697
2994
  .getState()
2698
2995
  .subscribeToPath(stateKeyPathKey, (e) => {
2699
2996
  // A data change has occurred for the source array.
2700
- console.log('statelsit subscribed to path', e);
2997
+
2701
2998
  if (e.type === 'GET_SELECTED') {
2702
2999
  return;
2703
3000
  }
@@ -2716,7 +3013,13 @@ function createProxyHandler<T>(
2716
3013
  }
2717
3014
  }
2718
3015
  }
2719
- if (e.type === 'INSERT') {
3016
+
3017
+ if (
3018
+ e.type === 'INSERT' ||
3019
+ e.type === 'REMOVE' ||
3020
+ e.type === 'CLEAR_SELECTION'
3021
+ ) {
3022
+ console.log('sssssssssssssssssssssssssssss', e);
2720
3023
  forceUpdate({});
2721
3024
  }
2722
3025
  });
@@ -2741,7 +3044,7 @@ function createProxyHandler<T>(
2741
3044
  validIds: validIds,
2742
3045
  },
2743
3046
  });
2744
-
3047
+ console.log('sssssssssssssssssssssssssssss', arraySetter);
2745
3048
  return (
2746
3049
  <>
2747
3050
  {arrayValues.map((item, localIndex) => {
@@ -3177,13 +3480,44 @@ function createProxyHandler<T>(
3177
3480
  };
3178
3481
  }
3179
3482
  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;
3483
+ return (patches: Operation[]) => {
3484
+ // 1. Get the current state object that the proxy points to.
3485
+ const store = getGlobalStore.getState();
3486
+
3487
+ const convertPath = (jsonPath: string): string[] => {
3488
+ if (!jsonPath || jsonPath === '/') return [];
3489
+ return jsonPath
3490
+ .split('/')
3491
+ .slice(1)
3492
+ .map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
3493
+ };
3185
3494
 
3186
- notifyComponents(stateKey);
3495
+ for (const patch of patches) {
3496
+ const relativePath = convertPath(patch.path);
3497
+
3498
+ switch (patch.op) {
3499
+ case 'add':
3500
+ case 'replace': {
3501
+ const { value } = patch as {
3502
+ op: 'add' | 'replace';
3503
+ path: string;
3504
+ value: any;
3505
+ };
3506
+ store.updateShadowAtPath(stateKey, relativePath, value);
3507
+ store.markAsDirty(stateKey, relativePath, { bubble: true }); // Or handle status differently for server patches
3508
+ break;
3509
+ }
3510
+ case 'remove': {
3511
+ store.removeShadowArrayElement(stateKey, relativePath);
3512
+ const parentPath = relativePath.slice(0, -1);
3513
+ store.markAsDirty(stateKey, parentPath, { bubble: true });
3514
+ break;
3515
+ }
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
+ }
3520
+ }
3187
3521
  };
3188
3522
  }
3189
3523
  if (prop === 'validateZodSchema') {
@@ -3247,9 +3581,38 @@ function createProxyHandler<T>(
3247
3581
  if (prop === '_path') return path;
3248
3582
  if (prop === 'update') {
3249
3583
  return (payload: UpdateArg<T>) => {
3584
+ // Step 1: This is the same. It performs the data update.
3250
3585
  effectiveSetState(payload as any, path, { updateType: 'update' });
3586
+
3587
+ return {
3588
+ /**
3589
+ * Marks this specific item, which was just updated, as 'synced' (not dirty).
3590
+ */
3591
+ synced: () => {
3592
+ // This function "remembers" the path of the item that was just updated.
3593
+ const shadowMeta = getGlobalStore
3594
+ .getState()
3595
+ .getShadowMetadata(stateKey, path);
3596
+
3597
+ // It updates ONLY the metadata for that specific item.
3598
+ getGlobalStore.getState().setShadowMetadata(stateKey, path, {
3599
+ ...shadowMeta,
3600
+ isDirty: false, // EXPLICITLY set to false, not just undefined
3601
+ stateSource: 'server', // Mark as coming from server
3602
+ lastServerSync: Date.now(), // Add timestamp
3603
+ });
3604
+
3605
+ // Force a re-render for components watching this path
3606
+ const fullPath = [stateKey, ...path].join('.');
3607
+ getGlobalStore.getState().notifyPathSubscribers(fullPath, {
3608
+ type: 'SYNC_STATUS_CHANGE',
3609
+ isDirty: false,
3610
+ });
3611
+ },
3612
+ };
3251
3613
  };
3252
3614
  }
3615
+
3253
3616
  if (prop === 'toggle') {
3254
3617
  const currentValueAtPath = getGlobalStore
3255
3618
  .getState()
@@ -3782,6 +4145,7 @@ function ListItemWrapper({
3782
4145
  const hasReportedInitialHeight = useRef(false);
3783
4146
  const fullKey = [stateKey, ...itemPath].join('.');
3784
4147
  useRegisterComponent(stateKey, itemComponentId, forceUpdate);
4148
+
3785
4149
  const setRefs = useCallback(
3786
4150
  (element: HTMLDivElement | null) => {
3787
4151
  elementRef.current = element;