cogsbox-state 0.5.470 → 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/README.md +2 -51
- package/dist/CogsState.d.ts +15 -5
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +915 -890
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts.map +1 -1
- package/dist/Components.jsx +214 -223
- package/dist/Components.jsx.map +1 -1
- package/dist/store.d.ts +23 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +476 -231
- package/dist/store.js.map +1 -1
- package/package.json +8 -13
- package/src/CogsState.tsx +221 -161
- package/src/Components.tsx +215 -169
- package/src/store.ts +476 -39
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?: (
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
568
|
+
// Start with the base options
|
|
569
|
+
let mergedOptions = { ...initialOptionsPartState, ...initialOptions };
|
|
559
570
|
let needToAdd = false;
|
|
571
|
+
|
|
560
572
|
if (options) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
//
|
|
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: {
|
|
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]['
|
|
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
|
-
|
|
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
|
-
//
|
|
1073
|
-
|
|
1074
|
-
if (result.type === '
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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 {
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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;
|
|
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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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()
|