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/dist/CogsState.d.ts +13 -21
- package/dist/CogsState.jsx +1149 -1042
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Functions.jsx +17 -18
- package/dist/Functions.jsx.map +1 -1
- package/dist/store.js +8 -6
- package/dist/store.js.map +1 -1
- package/package.json +1 -1
- package/src/CogsState.tsx +411 -47
- package/src/store.ts +14 -5
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1906
|
-
|
|
1907
|
-
} else if (shadowMeta?.isDirty) {
|
|
2128
|
+
// Priority 1: Explicitly dirty items
|
|
2129
|
+
if (shadowMeta?.isDirty === true) {
|
|
1908
2130
|
return 'dirty';
|
|
1909
|
-
}
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
const
|
|
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
|
-
|
|
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;
|