cogsbox-state 0.5.463 → 0.5.465
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 +10 -30
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1417 -1461
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Functions.d.ts.map +1 -1
- package/dist/Functions.jsx +23 -16
- package/dist/Functions.jsx.map +1 -1
- package/dist/index.js +26 -26
- package/dist/store.d.ts +39 -47
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +259 -295
- package/dist/store.js.map +1 -1
- package/dist/utility.d.ts +0 -1
- package/dist/utility.d.ts.map +1 -1
- package/dist/utility.js +121 -158
- package/dist/utility.js.map +1 -1
- package/package.json +5 -4
- package/src/CogsState.tsx +1047 -1428
- package/src/Functions.tsx +27 -7
- package/src/store.ts +489 -593
- package/src/utility.ts +0 -65
package/src/CogsState.tsx
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
} from 'react';
|
|
17
17
|
import { createRoot } from 'react-dom/client';
|
|
18
18
|
import {
|
|
19
|
-
debounce,
|
|
20
19
|
getDifferences,
|
|
21
20
|
isArray,
|
|
22
21
|
isFunction,
|
|
@@ -28,8 +27,11 @@ import superjson from 'superjson';
|
|
|
28
27
|
import { v4 as uuidv4 } from 'uuid';
|
|
29
28
|
|
|
30
29
|
import {
|
|
30
|
+
buildShadowNode,
|
|
31
31
|
formRefStore,
|
|
32
32
|
getGlobalStore,
|
|
33
|
+
METADATA_KEYS,
|
|
34
|
+
ValidationError,
|
|
33
35
|
ValidationStatus,
|
|
34
36
|
type ComponentsType,
|
|
35
37
|
} from './store.js';
|
|
@@ -38,7 +40,6 @@ import { Operation } from 'fast-json-patch';
|
|
|
38
40
|
import { useInView } from 'react-intersection-observer';
|
|
39
41
|
import * as z3 from 'zod/v3';
|
|
40
42
|
import * as z4 from 'zod/v4';
|
|
41
|
-
import z from 'zod';
|
|
42
43
|
|
|
43
44
|
type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
|
|
44
45
|
|
|
@@ -245,10 +246,7 @@ export type UpdateType<T> = (payload: UpdateArg<T>) => { synced: () => void };
|
|
|
245
246
|
|
|
246
247
|
export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
|
|
247
248
|
export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
|
|
248
|
-
|
|
249
|
-
path: (string | number)[];
|
|
250
|
-
message: string;
|
|
251
|
-
};
|
|
249
|
+
|
|
252
250
|
type EffectFunction<T, R> = (state: T, deps: any[]) => R;
|
|
253
251
|
export type EndType<T, IsArrayElement = false> = {
|
|
254
252
|
addZodValidation: (errors: ValidationError[]) => void;
|
|
@@ -259,7 +257,7 @@ export type EndType<T, IsArrayElement = false> = {
|
|
|
259
257
|
_stateKey: string;
|
|
260
258
|
formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
|
|
261
259
|
get: () => T;
|
|
262
|
-
|
|
260
|
+
|
|
263
261
|
$get: () => T;
|
|
264
262
|
$derive: <R>(fn: EffectFunction<T, R>) => R;
|
|
265
263
|
|
|
@@ -306,7 +304,7 @@ export type StateObject<T> = (T extends any[]
|
|
|
306
304
|
_isLoading: boolean;
|
|
307
305
|
_serverState: T;
|
|
308
306
|
revertToInitialState: (obj?: { validationKey?: string }) => T;
|
|
309
|
-
|
|
307
|
+
|
|
310
308
|
middleware: (
|
|
311
309
|
middles: ({
|
|
312
310
|
updateLog,
|
|
@@ -337,7 +335,8 @@ type UpdateOptions = {
|
|
|
337
335
|
type EffectiveSetState<TStateObject> = (
|
|
338
336
|
newStateOrFunction:
|
|
339
337
|
| EffectiveSetStateArg<TStateObject, 'update'>
|
|
340
|
-
| EffectiveSetStateArg<TStateObject, 'insert'
|
|
338
|
+
| EffectiveSetStateArg<TStateObject, 'insert'>
|
|
339
|
+
| null,
|
|
341
340
|
path: string[],
|
|
342
341
|
updateObj: UpdateOptions,
|
|
343
342
|
validationKey?: string
|
|
@@ -449,10 +448,13 @@ type FormsElementsType<T> = {
|
|
|
449
448
|
children: React.ReactNode;
|
|
450
449
|
status: ValidationStatus; // Instead of 'active' boolean
|
|
451
450
|
|
|
451
|
+
hasErrors: boolean;
|
|
452
|
+
hasWarnings: boolean;
|
|
453
|
+
allErrors: ValidationError[];
|
|
454
|
+
|
|
452
455
|
path: string[];
|
|
453
456
|
message?: string;
|
|
454
|
-
|
|
455
|
-
key?: string;
|
|
457
|
+
getData?: () => T;
|
|
456
458
|
}) => React.ReactNode;
|
|
457
459
|
syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
|
|
458
460
|
};
|
|
@@ -476,11 +478,74 @@ export type TransformedStateType<T> = {
|
|
|
476
478
|
[P in keyof T]: T[P] extends CogsInitialState<infer U> ? U : T[P];
|
|
477
479
|
};
|
|
478
480
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
481
|
+
const {
|
|
482
|
+
getInitialOptions,
|
|
483
|
+
updateInitialStateGlobal,
|
|
484
|
+
// ALIAS THE NEW FUNCTIONS TO THE OLD NAMES
|
|
485
|
+
getShadowMetadata,
|
|
486
|
+
setShadowMetadata,
|
|
487
|
+
getShadowValue,
|
|
488
|
+
initializeShadowState,
|
|
489
|
+
updateShadowAtPath,
|
|
490
|
+
insertShadowArrayElement,
|
|
491
|
+
removeShadowArrayElement,
|
|
492
|
+
getSelectedIndex,
|
|
493
|
+
setInitialStateOptions,
|
|
494
|
+
setServerStateUpdate,
|
|
495
|
+
markAsDirty,
|
|
496
|
+
registerComponent,
|
|
497
|
+
unregisterComponent,
|
|
498
|
+
addPathComponent,
|
|
499
|
+
clearSelectedIndexesForState,
|
|
500
|
+
addStateLog,
|
|
501
|
+
setSyncInfo,
|
|
502
|
+
clearSelectedIndex,
|
|
503
|
+
getSyncInfo,
|
|
504
|
+
notifyPathSubscribers,
|
|
505
|
+
subscribeToPath,
|
|
506
|
+
// Note: The old functions are no longer imported under their original names
|
|
507
|
+
} = getGlobalStore.getState();
|
|
508
|
+
function getArrayData(stateKey: string, path: string[], meta?: MetaData) {
|
|
509
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
510
|
+
const isArray = !!shadowMeta?.arrayKeys;
|
|
483
511
|
|
|
512
|
+
if (!isArray) {
|
|
513
|
+
const value = getGlobalStore.getState().getShadowValue(stateKey, path);
|
|
514
|
+
return { isArray: false, value, keys: [] };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const arrayPathKey = path.join('.');
|
|
518
|
+
const viewIds = meta?.arrayViews?.[arrayPathKey] ?? shadowMeta.arrayKeys;
|
|
519
|
+
|
|
520
|
+
// FIX: If the derived view is empty, return an empty array and keys.
|
|
521
|
+
if (Array.isArray(viewIds) && viewIds.length === 0) {
|
|
522
|
+
return { isArray: true, value: [], keys: [] };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const value = getGlobalStore
|
|
526
|
+
.getState()
|
|
527
|
+
.getShadowValue(stateKey, path, viewIds);
|
|
528
|
+
|
|
529
|
+
return { isArray: true, value, keys: viewIds ?? [] };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function findArrayItem(
|
|
533
|
+
array: any[],
|
|
534
|
+
keys: string[],
|
|
535
|
+
predicate: (item: any, index: number) => boolean
|
|
536
|
+
): { key: string; index: number; value: any } | null {
|
|
537
|
+
for (let i = 0; i < array.length; i++) {
|
|
538
|
+
if (predicate(array[i], i)) {
|
|
539
|
+
const key = keys[i];
|
|
540
|
+
if (key) {
|
|
541
|
+
return { key, index: i, value: array[i] };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function setAndMergeOptions(stateKey: string, newOptions: OptionsType<any>) {
|
|
484
549
|
const initialOptions = getInitialOptions(stateKey as string) || {};
|
|
485
550
|
|
|
486
551
|
setInitialStateOptions(stateKey as string, {
|
|
@@ -499,8 +564,7 @@ function setOptions<StateKey, Opt>({
|
|
|
499
564
|
}) {
|
|
500
565
|
const initialOptions = getInitialOptions(stateKey as string) || {};
|
|
501
566
|
const initialOptionsPartState = initialOptionsPart[stateKey as string] || {};
|
|
502
|
-
|
|
503
|
-
getGlobalStore.getState().setInitialStateOptions;
|
|
567
|
+
|
|
504
568
|
const mergedOptions = { ...initialOptionsPartState, ...initialOptions };
|
|
505
569
|
|
|
506
570
|
let needToAdd = false;
|
|
@@ -621,10 +685,10 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
621
685
|
const existingGlobalOptions = getInitialOptions(key);
|
|
622
686
|
|
|
623
687
|
if (!existingGlobalOptions) {
|
|
624
|
-
|
|
688
|
+
setInitialStateOptions(key, mergedOptions);
|
|
625
689
|
} else {
|
|
626
690
|
// Merge with existing global options
|
|
627
|
-
|
|
691
|
+
setInitialStateOptions(key, {
|
|
628
692
|
...existingGlobalOptions,
|
|
629
693
|
...mergedOptions,
|
|
630
694
|
});
|
|
@@ -633,16 +697,15 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
633
697
|
});
|
|
634
698
|
|
|
635
699
|
Object.keys(statePart).forEach((key) => {
|
|
636
|
-
|
|
700
|
+
initializeShadowState(key, statePart[key]);
|
|
637
701
|
});
|
|
638
|
-
|
|
702
|
+
console.log('new stateObject ', getGlobalStore.getState().shadowStateStore);
|
|
639
703
|
type StateKeys = keyof typeof statePart;
|
|
640
704
|
|
|
641
705
|
const useCogsState = <StateKey extends StateKeys>(
|
|
642
706
|
stateKey: StateKey,
|
|
643
707
|
options?: Prettify<OptionsType<(typeof statePart)[StateKey]>>
|
|
644
708
|
) => {
|
|
645
|
-
console.time('useCogsState');
|
|
646
709
|
const [componentId] = useState(options?.componentId ?? uuidv4());
|
|
647
710
|
|
|
648
711
|
setOptions({
|
|
@@ -651,14 +714,11 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
651
714
|
initialOptionsPart,
|
|
652
715
|
});
|
|
653
716
|
const thiState =
|
|
654
|
-
|
|
655
|
-
statePart[stateKey as string];
|
|
717
|
+
getShadowValue(stateKey as string, []) || statePart[stateKey as string];
|
|
656
718
|
const partialState = options?.modifyState
|
|
657
719
|
? options.modifyState(thiState)
|
|
658
720
|
: thiState;
|
|
659
721
|
|
|
660
|
-
console.timeEnd('useCogsState');
|
|
661
|
-
|
|
662
722
|
const updater = useCogsStateFn<(typeof statePart)[StateKey]>(partialState, {
|
|
663
723
|
stateKey: stateKey as string,
|
|
664
724
|
syncUpdate: options?.syncUpdate,
|
|
@@ -777,12 +837,7 @@ export function createCogsStateFromSync<
|
|
|
777
837
|
__syncSchemas: schemas,
|
|
778
838
|
}) as any;
|
|
779
839
|
}
|
|
780
|
-
const {
|
|
781
|
-
getInitialOptions,
|
|
782
840
|
|
|
783
|
-
addStateLog,
|
|
784
|
-
updateInitialStateGlobal,
|
|
785
|
-
} = getGlobalStore.getState();
|
|
786
841
|
const saveToLocalStorage = <T,>(
|
|
787
842
|
state: T,
|
|
788
843
|
thisKey: string,
|
|
@@ -814,7 +869,7 @@ const saveToLocalStorage = <T,>(
|
|
|
814
869
|
} catch {
|
|
815
870
|
// Ignore errors, will use undefined
|
|
816
871
|
}
|
|
817
|
-
const shadowMeta =
|
|
872
|
+
const shadowMeta = getShadowMetadata(thisKey, []);
|
|
818
873
|
|
|
819
874
|
const data: LocalStorageData<T> = {
|
|
820
875
|
state,
|
|
@@ -850,7 +905,7 @@ const loadFromLocalStorage = (localStorageKey: string) => {
|
|
|
850
905
|
}
|
|
851
906
|
};
|
|
852
907
|
const loadAndApplyLocalStorage = (stateKey: string, options: any) => {
|
|
853
|
-
const currentState =
|
|
908
|
+
const currentState = getShadowValue(stateKey, []);
|
|
854
909
|
const { sessionId } = useCogsConfig();
|
|
855
910
|
const localkey = isFunction(options?.localStorage?.key)
|
|
856
911
|
? options.localStorage.key(currentState)
|
|
@@ -881,7 +936,7 @@ type LocalStorageData<T> = {
|
|
|
881
936
|
};
|
|
882
937
|
|
|
883
938
|
const notifyComponents = (thisKey: string) => {
|
|
884
|
-
const stateEntry =
|
|
939
|
+
const stateEntry = getShadowMetadata(thisKey, []);
|
|
885
940
|
if (!stateEntry) return;
|
|
886
941
|
|
|
887
942
|
// Batch component updates
|
|
@@ -903,40 +958,15 @@ const notifyComponents = (thisKey: string) => {
|
|
|
903
958
|
});
|
|
904
959
|
};
|
|
905
960
|
|
|
906
|
-
export const notifyComponent = (stateKey: string, componentId: string) => {
|
|
907
|
-
const stateEntry = getGlobalStore.getState().getShadowMetadata(stateKey, []);
|
|
908
|
-
if (stateEntry) {
|
|
909
|
-
const fullComponentId = `${stateKey}////${componentId}`;
|
|
910
|
-
const component = stateEntry?.components?.get(fullComponentId);
|
|
911
|
-
const reactiveTypes = component
|
|
912
|
-
? Array.isArray(component.reactiveType)
|
|
913
|
-
? component.reactiveType
|
|
914
|
-
: [component.reactiveType || 'component']
|
|
915
|
-
: null;
|
|
916
|
-
|
|
917
|
-
// Skip if reactivity is disabled
|
|
918
|
-
if (reactiveTypes?.includes('none')) {
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
if (component) {
|
|
923
|
-
// Force an update to ensure the current value is saved
|
|
924
|
-
|
|
925
|
-
component.forceUpdate();
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
961
|
function markEntireStateAsServerSynced(
|
|
930
962
|
stateKey: string,
|
|
931
963
|
path: string[],
|
|
932
964
|
data: any,
|
|
933
965
|
timestamp: number
|
|
934
966
|
) {
|
|
935
|
-
const store = getGlobalStore.getState();
|
|
936
|
-
|
|
937
967
|
// Mark current path as synced
|
|
938
|
-
const currentMeta =
|
|
939
|
-
|
|
968
|
+
const currentMeta = getShadowMetadata(stateKey, path);
|
|
969
|
+
setShadowMetadata(stateKey, path, {
|
|
940
970
|
...currentMeta,
|
|
941
971
|
isDirty: false,
|
|
942
972
|
stateSource: 'server',
|
|
@@ -945,10 +975,11 @@ function markEntireStateAsServerSynced(
|
|
|
945
975
|
|
|
946
976
|
// If it's an array, mark each item as synced
|
|
947
977
|
if (Array.isArray(data)) {
|
|
948
|
-
const arrayMeta =
|
|
978
|
+
const arrayMeta = getShadowMetadata(stateKey, path);
|
|
949
979
|
if (arrayMeta?.arrayKeys) {
|
|
950
980
|
arrayMeta.arrayKeys.forEach((itemKey, index) => {
|
|
951
|
-
|
|
981
|
+
// Fix: Don't split the itemKey, just use it directly
|
|
982
|
+
const itemPath = [...path, itemKey];
|
|
952
983
|
const itemData = data[index];
|
|
953
984
|
if (itemData !== undefined) {
|
|
954
985
|
markEntireStateAsServerSynced(
|
|
@@ -970,8 +1001,346 @@ function markEntireStateAsServerSynced(
|
|
|
970
1001
|
});
|
|
971
1002
|
}
|
|
972
1003
|
}
|
|
973
|
-
|
|
974
|
-
let
|
|
1004
|
+
// 5. Batch queue
|
|
1005
|
+
let updateBatchQueue: any[] = [];
|
|
1006
|
+
let isFlushScheduled = false;
|
|
1007
|
+
|
|
1008
|
+
function scheduleFlush() {
|
|
1009
|
+
if (!isFlushScheduled) {
|
|
1010
|
+
isFlushScheduled = true;
|
|
1011
|
+
queueMicrotask(flushQueue);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function handleUpdate(
|
|
1015
|
+
stateKey: string,
|
|
1016
|
+
path: string[],
|
|
1017
|
+
payload: any
|
|
1018
|
+
): { type: 'update'; oldValue: any; newValue: any; shadowMeta: any } {
|
|
1019
|
+
// ✅ FIX: Get the old value before the update.
|
|
1020
|
+
const oldValue = getGlobalStore.getState().getShadowValue(stateKey, path);
|
|
1021
|
+
|
|
1022
|
+
const newValue = isFunction(payload) ? payload(oldValue) : payload;
|
|
1023
|
+
|
|
1024
|
+
// ✅ FIX: The new `updateShadowAtPath` handles metadata preservation automatically.
|
|
1025
|
+
// The manual loop has been removed.
|
|
1026
|
+
updateShadowAtPath(stateKey, path, newValue);
|
|
1027
|
+
|
|
1028
|
+
markAsDirty(stateKey, path, { bubble: true });
|
|
1029
|
+
|
|
1030
|
+
// Return the metadata of the node *after* the update.
|
|
1031
|
+
const newShadowMeta = getShadowMetadata(stateKey, path);
|
|
1032
|
+
|
|
1033
|
+
return {
|
|
1034
|
+
type: 'update',
|
|
1035
|
+
oldValue: oldValue,
|
|
1036
|
+
newValue,
|
|
1037
|
+
shadowMeta: newShadowMeta,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
// 2. Update signals
|
|
1041
|
+
function updateSignals(shadowMeta: any, displayValue: any) {
|
|
1042
|
+
if (!shadowMeta?.signals?.length) return;
|
|
1043
|
+
|
|
1044
|
+
shadowMeta.signals.forEach(({ parentId, position, effect }: any) => {
|
|
1045
|
+
const parent = document.querySelector(`[data-parent-id="${parentId}"]`);
|
|
1046
|
+
if (!parent) return;
|
|
1047
|
+
|
|
1048
|
+
const childNodes = Array.from(parent.childNodes);
|
|
1049
|
+
if (!childNodes[position]) return;
|
|
1050
|
+
|
|
1051
|
+
let finalDisplayValue = displayValue;
|
|
1052
|
+
if (effect && displayValue !== null) {
|
|
1053
|
+
try {
|
|
1054
|
+
finalDisplayValue = new Function('state', `return (${effect})(state)`)(
|
|
1055
|
+
displayValue
|
|
1056
|
+
);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
console.error('Error evaluating effect function:', err);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (finalDisplayValue !== null && typeof finalDisplayValue === 'object') {
|
|
1063
|
+
finalDisplayValue = JSON.stringify(finalDisplayValue);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
childNodes[position].textContent = String(finalDisplayValue ?? '');
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function getComponentNotifications(
|
|
1071
|
+
stateKey: string,
|
|
1072
|
+
path: string[],
|
|
1073
|
+
result: any
|
|
1074
|
+
): Set<any> {
|
|
1075
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1076
|
+
|
|
1077
|
+
if (!rootMeta?.components) {
|
|
1078
|
+
return new Set();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const componentsToNotify = new Set<any>();
|
|
1082
|
+
|
|
1083
|
+
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
1084
|
+
|
|
1085
|
+
if (result.type === 'update') {
|
|
1086
|
+
// --- Bubble-up Notification ---
|
|
1087
|
+
// An update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
|
|
1088
|
+
let currentPath = [...path];
|
|
1089
|
+
while (true) {
|
|
1090
|
+
const pathMeta = getShadowMetadata(stateKey, currentPath);
|
|
1091
|
+
|
|
1092
|
+
if (pathMeta?.pathComponents) {
|
|
1093
|
+
pathMeta.pathComponents.forEach((componentId: string) => {
|
|
1094
|
+
const component = rootMeta.components?.get(componentId);
|
|
1095
|
+
// NEW: Add component to the set instead of calling forceUpdate()
|
|
1096
|
+
if (component) {
|
|
1097
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1098
|
+
? component.reactiveType
|
|
1099
|
+
: [component.reactiveType || 'component'];
|
|
1100
|
+
if (!reactiveTypes.includes('none')) {
|
|
1101
|
+
componentsToNotify.add(component);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (currentPath.length === 0) break;
|
|
1108
|
+
currentPath.pop(); // Go up one level
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// --- Deep Object Change Notification ---
|
|
1112
|
+
// If the new value is an object, notify components subscribed to sub-paths that changed.
|
|
1113
|
+
if (
|
|
1114
|
+
result.newValue &&
|
|
1115
|
+
typeof result.newValue === 'object' &&
|
|
1116
|
+
!isArray(result.newValue)
|
|
1117
|
+
) {
|
|
1118
|
+
const changedSubPaths = getDifferences(result.newValue, result.oldValue);
|
|
1119
|
+
|
|
1120
|
+
changedSubPaths.forEach((subPathString: string) => {
|
|
1121
|
+
const subPath = subPathString.split('.');
|
|
1122
|
+
const fullSubPath = [...path, ...subPath];
|
|
1123
|
+
const subPathMeta = getShadowMetadata(stateKey, fullSubPath);
|
|
1124
|
+
|
|
1125
|
+
if (subPathMeta?.pathComponents) {
|
|
1126
|
+
subPathMeta.pathComponents.forEach((componentId: string) => {
|
|
1127
|
+
const component = rootMeta.components?.get(componentId);
|
|
1128
|
+
// NEW: Add component to the set
|
|
1129
|
+
if (component) {
|
|
1130
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1131
|
+
? component.reactiveType
|
|
1132
|
+
: [component.reactiveType || 'component'];
|
|
1133
|
+
if (!reactiveTypes.includes('none')) {
|
|
1134
|
+
componentsToNotify.add(component);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
} else if (result.type === 'insert' || result.type === 'cut') {
|
|
1142
|
+
// For array structural changes (add/remove), notify components listening to the parent array.
|
|
1143
|
+
const parentArrayPath = result.type === 'insert' ? path : path.slice(0, -1);
|
|
1144
|
+
const parentMeta = getShadowMetadata(stateKey, parentArrayPath);
|
|
1145
|
+
|
|
1146
|
+
if (parentMeta?.pathComponents) {
|
|
1147
|
+
parentMeta.pathComponents.forEach((componentId: string) => {
|
|
1148
|
+
const component = rootMeta.components?.get(componentId);
|
|
1149
|
+
// NEW: Add component to the set
|
|
1150
|
+
if (component) {
|
|
1151
|
+
componentsToNotify.add(component);
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// --- PASS 2: Handle 'all' and 'deps' reactivity types ---
|
|
1158
|
+
// Iterate over all components for this stateKey that haven't been notified yet.
|
|
1159
|
+
rootMeta.components.forEach((component, componentId) => {
|
|
1160
|
+
// If we've already added this component, skip it.
|
|
1161
|
+
if (componentsToNotify.has(component)) {
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1166
|
+
? component.reactiveType
|
|
1167
|
+
: [component.reactiveType || 'component'];
|
|
1168
|
+
|
|
1169
|
+
if (reactiveTypes.includes('all')) {
|
|
1170
|
+
componentsToNotify.add(component);
|
|
1171
|
+
} else if (reactiveTypes.includes('deps') && component.depsFunction) {
|
|
1172
|
+
const currentState = getShadowValue(stateKey, []);
|
|
1173
|
+
const newDeps = component.depsFunction(currentState);
|
|
1174
|
+
|
|
1175
|
+
if (
|
|
1176
|
+
newDeps === true ||
|
|
1177
|
+
(Array.isArray(newDeps) && !isDeepEqual(component.prevDeps, newDeps))
|
|
1178
|
+
) {
|
|
1179
|
+
component.prevDeps = newDeps as any; // Update the dependencies for the next check
|
|
1180
|
+
componentsToNotify.add(component);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
return componentsToNotify;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function handleInsert(
|
|
1189
|
+
stateKey: string,
|
|
1190
|
+
path: string[],
|
|
1191
|
+
payload: any
|
|
1192
|
+
): { type: 'insert'; newValue: any; shadowMeta: any } {
|
|
1193
|
+
let newValue;
|
|
1194
|
+
if (isFunction(payload)) {
|
|
1195
|
+
const { value: currentValue } = getScopedData(stateKey, path);
|
|
1196
|
+
newValue = payload({ state: currentValue, uuid: uuidv4() });
|
|
1197
|
+
} else {
|
|
1198
|
+
newValue = payload;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
insertShadowArrayElement(stateKey, path, newValue);
|
|
1202
|
+
markAsDirty(stateKey, path, { bubble: true });
|
|
1203
|
+
|
|
1204
|
+
const updatedMeta = getShadowMetadata(stateKey, path);
|
|
1205
|
+
if (updatedMeta?.arrayKeys) {
|
|
1206
|
+
const newItemKey = updatedMeta.arrayKeys[updatedMeta.arrayKeys.length - 1];
|
|
1207
|
+
if (newItemKey) {
|
|
1208
|
+
const newItemPath = newItemKey.split('.').slice(1);
|
|
1209
|
+
markAsDirty(stateKey, newItemPath, { bubble: false });
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return { type: 'insert', newValue, shadowMeta: updatedMeta };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function handleCut(
|
|
1217
|
+
stateKey: string,
|
|
1218
|
+
path: string[]
|
|
1219
|
+
): { type: 'cut'; oldValue: any; parentPath: string[] } {
|
|
1220
|
+
const parentArrayPath = path.slice(0, -1);
|
|
1221
|
+
const oldValue = getShadowValue(stateKey, path);
|
|
1222
|
+
removeShadowArrayElement(stateKey, path);
|
|
1223
|
+
markAsDirty(stateKey, parentArrayPath, { bubble: true });
|
|
1224
|
+
return { type: 'cut', oldValue: oldValue, parentPath: parentArrayPath };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function flushQueue() {
|
|
1228
|
+
const allComponentsToNotify = new Set<any>();
|
|
1229
|
+
const signalUpdates: { shadowMeta: any; displayValue: any }[] = [];
|
|
1230
|
+
|
|
1231
|
+
const logsToAdd: UpdateTypeDetail[] = [];
|
|
1232
|
+
|
|
1233
|
+
for (const item of updateBatchQueue) {
|
|
1234
|
+
if (item.status && item.updateType) {
|
|
1235
|
+
logsToAdd.push(item as UpdateTypeDetail);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const result = item;
|
|
1240
|
+
|
|
1241
|
+
const displayValue = result.type === 'cut' ? null : result.newValue;
|
|
1242
|
+
if (result.shadowMeta?.signals?.length > 0) {
|
|
1243
|
+
signalUpdates.push({ shadowMeta: result.shadowMeta, displayValue });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const componentNotifications = getComponentNotifications(
|
|
1247
|
+
result.stateKey,
|
|
1248
|
+
result.path,
|
|
1249
|
+
result
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
componentNotifications.forEach((component) => {
|
|
1253
|
+
allComponentsToNotify.add(component);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (logsToAdd.length > 0) {
|
|
1258
|
+
addStateLog(logsToAdd);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
signalUpdates.forEach(({ shadowMeta, displayValue }) => {
|
|
1262
|
+
updateSignals(shadowMeta, displayValue);
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
allComponentsToNotify.forEach((component) => {
|
|
1266
|
+
component.forceUpdate();
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
// --- Step 3: CLEANUP ---
|
|
1270
|
+
// Clear the queue for the next batch of updates.
|
|
1271
|
+
updateBatchQueue = [];
|
|
1272
|
+
isFlushScheduled = false;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function createEffectiveSetState<T>(
|
|
1276
|
+
thisKey: string,
|
|
1277
|
+
syncApiRef: React.MutableRefObject<any>,
|
|
1278
|
+
sessionId: string | undefined,
|
|
1279
|
+
latestInitialOptionsRef: React.MutableRefObject<OptionsType<T> | null>
|
|
1280
|
+
): EffectiveSetState<T> {
|
|
1281
|
+
// The returned function is the core setter that gets called by all state operations.
|
|
1282
|
+
// It is now much simpler, delegating all work to the executeUpdate function.
|
|
1283
|
+
return (newStateOrFunction, path, updateObj, validationKey?) => {
|
|
1284
|
+
executeUpdate(thisKey, path, newStateOrFunction, updateObj);
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// This inner function handles the logic for a single state update.
|
|
1288
|
+
function executeUpdate(
|
|
1289
|
+
stateKey: string,
|
|
1290
|
+
path: string[],
|
|
1291
|
+
payload: any,
|
|
1292
|
+
options: UpdateOptions
|
|
1293
|
+
) {
|
|
1294
|
+
// --- Step 1: Execute the core state change (Synchronous & Fast) ---
|
|
1295
|
+
// This part modifies the in-memory state representation immediately.
|
|
1296
|
+
let result: any;
|
|
1297
|
+
switch (options.updateType) {
|
|
1298
|
+
case 'update':
|
|
1299
|
+
result = handleUpdate(stateKey, path, payload);
|
|
1300
|
+
break;
|
|
1301
|
+
case 'insert':
|
|
1302
|
+
result = handleInsert(stateKey, path, payload);
|
|
1303
|
+
break;
|
|
1304
|
+
case 'cut':
|
|
1305
|
+
result = handleCut(stateKey, path);
|
|
1306
|
+
break;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
result.stateKey = stateKey;
|
|
1310
|
+
result.path = path;
|
|
1311
|
+
updateBatchQueue.push(result);
|
|
1312
|
+
scheduleFlush();
|
|
1313
|
+
|
|
1314
|
+
const newUpdate: UpdateTypeDetail = {
|
|
1315
|
+
timeStamp: Date.now(),
|
|
1316
|
+
stateKey,
|
|
1317
|
+
path,
|
|
1318
|
+
updateType: options.updateType,
|
|
1319
|
+
status: 'new',
|
|
1320
|
+
oldValue: result.oldValue,
|
|
1321
|
+
newValue: result.newValue ?? null,
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
updateBatchQueue.push(newUpdate);
|
|
1325
|
+
|
|
1326
|
+
if (result.newValue !== undefined) {
|
|
1327
|
+
saveToLocalStorage(
|
|
1328
|
+
result.newValue,
|
|
1329
|
+
stateKey,
|
|
1330
|
+
latestInitialOptionsRef.current,
|
|
1331
|
+
sessionId
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (latestInitialOptionsRef.current?.middleware) {
|
|
1336
|
+
latestInitialOptionsRef.current.middleware({ update: newUpdate });
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (options.sync !== false && syncApiRef.current?.connected) {
|
|
1340
|
+
syncApiRef.current.updateState({ operation: newUpdate });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
975
1344
|
|
|
976
1345
|
export function useCogsStateFn<TStateObject extends unknown>(
|
|
977
1346
|
stateObject: TStateObject,
|
|
@@ -996,7 +1365,6 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
996
1365
|
syncOptions?: SyncOptionsType<any>;
|
|
997
1366
|
} & OptionsType<TStateObject> = {}
|
|
998
1367
|
) {
|
|
999
|
-
console.time('useCogsStateFn top');
|
|
1000
1368
|
const [reactiveForce, forceUpdate] = useState({}); //this is the key to reactivity
|
|
1001
1369
|
const { sessionId } = useCogsConfig();
|
|
1002
1370
|
let noStateKey = stateKey ? false : true;
|
|
@@ -1011,7 +1379,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1011
1379
|
useEffect(() => {
|
|
1012
1380
|
if (syncUpdate && syncUpdate.stateKey === thisKey && syncUpdate.path?.[0]) {
|
|
1013
1381
|
const syncKey = `${syncUpdate.stateKey}:${syncUpdate.path.join('.')}`;
|
|
1014
|
-
|
|
1382
|
+
setSyncInfo(syncKey, {
|
|
1015
1383
|
timeStamp: syncUpdate.timeStamp!,
|
|
1016
1384
|
userId: syncUpdate.userId!,
|
|
1017
1385
|
});
|
|
@@ -1081,7 +1449,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1081
1449
|
|
|
1082
1450
|
// Effect 1: When this component's serverState prop changes, broadcast it
|
|
1083
1451
|
useEffect(() => {
|
|
1084
|
-
|
|
1452
|
+
setServerStateUpdate(thisKey, serverState);
|
|
1085
1453
|
}, [serverState, thisKey]);
|
|
1086
1454
|
|
|
1087
1455
|
// Effect 2: Listen for server state updates from ANY component
|
|
@@ -1091,115 +1459,79 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1091
1459
|
.subscribeToPath(thisKey, (event) => {
|
|
1092
1460
|
if (event?.type === 'SERVER_STATE_UPDATE') {
|
|
1093
1461
|
const serverStateData = event.serverState;
|
|
1094
|
-
|
|
1462
|
+
|
|
1095
1463
|
if (
|
|
1096
|
-
serverStateData?.status
|
|
1097
|
-
serverStateData.data
|
|
1464
|
+
serverStateData?.status !== 'success' ||
|
|
1465
|
+
serverStateData.data === undefined
|
|
1098
1466
|
) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const mergeConfig =
|
|
1103
|
-
typeof serverStateData.merge === 'object'
|
|
1104
|
-
? serverStateData.merge
|
|
1105
|
-
: serverStateData.merge === true
|
|
1106
|
-
? { strategy: 'append' }
|
|
1107
|
-
: null;
|
|
1467
|
+
return; // Ignore if no valid data
|
|
1468
|
+
}
|
|
1108
1469
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1114
|
-
mergeConfig &&
|
|
1115
|
-
Array.isArray(currentState) &&
|
|
1116
|
-
Array.isArray(incomingData)
|
|
1117
|
-
) {
|
|
1118
|
-
const keyField = mergeConfig.key;
|
|
1119
|
-
const existingIds = new Set(
|
|
1120
|
-
currentState.map((item: any) => item[keyField])
|
|
1121
|
-
);
|
|
1470
|
+
console.log(
|
|
1471
|
+
'✅ SERVER_STATE_UPDATE received with data:',
|
|
1472
|
+
serverStateData
|
|
1473
|
+
);
|
|
1122
1474
|
|
|
1123
|
-
|
|
1124
|
-
return !existingIds.has(item[keyField]);
|
|
1125
|
-
});
|
|
1475
|
+
setAndMergeOptions(thisKey, { serverState: serverStateData });
|
|
1126
1476
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1477
|
+
const mergeConfig =
|
|
1478
|
+
typeof serverStateData.merge === 'object'
|
|
1479
|
+
? serverStateData.merge
|
|
1480
|
+
: serverStateData.merge === true
|
|
1481
|
+
? { strategy: 'append' }
|
|
1482
|
+
: null;
|
|
1132
1483
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
.getShadowMetadata(thisKey, []);
|
|
1137
|
-
|
|
1138
|
-
if (arrayMeta?.arrayKeys) {
|
|
1139
|
-
const newItemKey =
|
|
1140
|
-
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
1141
|
-
if (newItemKey) {
|
|
1142
|
-
const newItemPath = newItemKey.split('.').slice(1);
|
|
1143
|
-
|
|
1144
|
-
// Mark the new server item as synced, not dirty
|
|
1145
|
-
getGlobalStore
|
|
1146
|
-
.getState()
|
|
1147
|
-
.setShadowMetadata(thisKey, newItemPath, {
|
|
1148
|
-
isDirty: false,
|
|
1149
|
-
stateSource: 'server',
|
|
1150
|
-
lastServerSync:
|
|
1151
|
-
serverStateData.timestamp || Date.now(),
|
|
1152
|
-
});
|
|
1484
|
+
// ✅ FIX 1: The path for the root value is now `[]`.
|
|
1485
|
+
const currentState = getShadowValue(thisKey, []);
|
|
1486
|
+
const incomingData = serverStateData.data;
|
|
1153
1487
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
getGlobalStore
|
|
1166
|
-
.getState()
|
|
1167
|
-
.setShadowMetadata(thisKey, fieldPath, {
|
|
1168
|
-
isDirty: false,
|
|
1169
|
-
stateSource: 'server',
|
|
1170
|
-
lastServerSync:
|
|
1171
|
-
serverStateData.timestamp || Date.now(),
|
|
1172
|
-
});
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
} else {
|
|
1180
|
-
// For replace strategy or initial load
|
|
1181
|
-
getGlobalStore
|
|
1182
|
-
.getState()
|
|
1183
|
-
.initializeShadowState(thisKey, incomingData);
|
|
1184
|
-
|
|
1185
|
-
// Mark the entire state tree as synced from server
|
|
1186
|
-
markEntireStateAsServerSynced(
|
|
1187
|
-
thisKey,
|
|
1188
|
-
[],
|
|
1189
|
-
incomingData,
|
|
1190
|
-
serverStateData.timestamp
|
|
1488
|
+
if (
|
|
1489
|
+
mergeConfig &&
|
|
1490
|
+
mergeConfig.strategy === 'append' &&
|
|
1491
|
+
'key' in mergeConfig && // Type guard for key
|
|
1492
|
+
Array.isArray(currentState) &&
|
|
1493
|
+
Array.isArray(incomingData)
|
|
1494
|
+
) {
|
|
1495
|
+
const keyField = mergeConfig.key;
|
|
1496
|
+
if (!keyField) {
|
|
1497
|
+
console.error(
|
|
1498
|
+
"CogsState: Merge strategy 'append' requires a 'key' field."
|
|
1191
1499
|
);
|
|
1500
|
+
return;
|
|
1192
1501
|
}
|
|
1502
|
+
console.log('SERVER_STATE_UPDATE 2');
|
|
1503
|
+
const existingIds = new Set(
|
|
1504
|
+
currentState.map((item: any) => item[keyField])
|
|
1505
|
+
);
|
|
1193
1506
|
|
|
1194
|
-
const
|
|
1195
|
-
.
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
}
|
|
1507
|
+
const newUniqueItems = incomingData.filter(
|
|
1508
|
+
(item: any) => !existingIds.has(item[keyField])
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
if (newUniqueItems.length > 0) {
|
|
1512
|
+
newUniqueItems.forEach((item) => {
|
|
1513
|
+
insertShadowArrayElement(thisKey, [], item);
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Mark the entire final state as synced
|
|
1518
|
+
const finalState = getShadowValue(thisKey, []);
|
|
1519
|
+
markEntireStateAsServerSynced(
|
|
1520
|
+
thisKey,
|
|
1521
|
+
[],
|
|
1522
|
+
finalState,
|
|
1523
|
+
serverStateData.timestamp
|
|
1524
|
+
);
|
|
1525
|
+
} else {
|
|
1526
|
+
// This handles the "replace" strategy (initial load)
|
|
1527
|
+
initializeShadowState(thisKey, incomingData);
|
|
1528
|
+
|
|
1529
|
+
markEntireStateAsServerSynced(
|
|
1530
|
+
thisKey,
|
|
1531
|
+
[],
|
|
1532
|
+
incomingData,
|
|
1533
|
+
serverStateData.timestamp
|
|
1534
|
+
);
|
|
1203
1535
|
}
|
|
1204
1536
|
}
|
|
1205
1537
|
});
|
|
@@ -1217,6 +1549,17 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1217
1549
|
|
|
1218
1550
|
const options = getInitialOptions(thisKey as string);
|
|
1219
1551
|
|
|
1552
|
+
const features = {
|
|
1553
|
+
syncEnabled: !!cogsSyncFn && !!syncOpt,
|
|
1554
|
+
validationEnabled: !!(
|
|
1555
|
+
options?.validation?.zodSchemaV4 || options?.validation?.zodSchemaV3
|
|
1556
|
+
),
|
|
1557
|
+
localStorageEnabled: !!options?.localStorage?.key,
|
|
1558
|
+
};
|
|
1559
|
+
setShadowMetadata(thisKey, [], {
|
|
1560
|
+
...existingMeta,
|
|
1561
|
+
features,
|
|
1562
|
+
});
|
|
1220
1563
|
if (options?.defaultState !== undefined || defaultState !== undefined) {
|
|
1221
1564
|
const finalDefaultState = options?.defaultState || defaultState;
|
|
1222
1565
|
|
|
@@ -1229,10 +1572,10 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1229
1572
|
|
|
1230
1573
|
const { value: resolvedState, source, timestamp } = resolveInitialState();
|
|
1231
1574
|
|
|
1232
|
-
|
|
1575
|
+
initializeShadowState(thisKey, resolvedState);
|
|
1233
1576
|
|
|
1234
1577
|
// Set shadow metadata with the correct source info
|
|
1235
|
-
|
|
1578
|
+
setShadowMetadata(thisKey, [], {
|
|
1236
1579
|
stateSource: source,
|
|
1237
1580
|
lastServerSync: source === 'server' ? timestamp : undefined,
|
|
1238
1581
|
isDirty: false,
|
|
@@ -1256,29 +1599,27 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1256
1599
|
const componentKey = `${thisKey}////${componentIdRef.current}`;
|
|
1257
1600
|
|
|
1258
1601
|
// Register component in shadow metadata at root level
|
|
1259
|
-
const rootMeta =
|
|
1602
|
+
const rootMeta = getShadowMetadata(thisKey, []);
|
|
1260
1603
|
const components = rootMeta?.components || new Map();
|
|
1261
1604
|
|
|
1262
1605
|
components.set(componentKey, {
|
|
1263
1606
|
forceUpdate: () => forceUpdate({}),
|
|
1264
|
-
reactiveType: reactiveType ?? ['component'
|
|
1607
|
+
reactiveType: reactiveType ?? ['component'],
|
|
1265
1608
|
paths: new Set(),
|
|
1266
1609
|
depsFunction: reactiveDeps || undefined,
|
|
1267
|
-
deps: reactiveDeps
|
|
1268
|
-
? reactiveDeps(getGlobalStore.getState().getShadowValue(thisKey))
|
|
1269
|
-
: [],
|
|
1610
|
+
deps: reactiveDeps ? reactiveDeps(getShadowValue(thisKey, [])) : [],
|
|
1270
1611
|
prevDeps: reactiveDeps // Initialize prevDeps with the same initial value
|
|
1271
|
-
? reactiveDeps(
|
|
1612
|
+
? reactiveDeps(getShadowValue(thisKey, []))
|
|
1272
1613
|
: [],
|
|
1273
1614
|
});
|
|
1274
1615
|
|
|
1275
|
-
|
|
1616
|
+
setShadowMetadata(thisKey, [], {
|
|
1276
1617
|
...rootMeta,
|
|
1277
1618
|
components,
|
|
1278
1619
|
});
|
|
1279
1620
|
forceUpdate({});
|
|
1280
1621
|
return () => {
|
|
1281
|
-
const meta =
|
|
1622
|
+
const meta = getShadowMetadata(thisKey, []);
|
|
1282
1623
|
const component = meta?.components?.get(componentKey);
|
|
1283
1624
|
|
|
1284
1625
|
// Remove from each path we registered to
|
|
@@ -1306,473 +1647,31 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1306
1647
|
|
|
1307
1648
|
// Remove from root components
|
|
1308
1649
|
if (meta?.components) {
|
|
1309
|
-
|
|
1650
|
+
setShadowMetadata(thisKey, [], meta);
|
|
1310
1651
|
}
|
|
1311
1652
|
};
|
|
1312
1653
|
}, []);
|
|
1313
1654
|
|
|
1314
1655
|
const syncApiRef = useRef<SyncApi | null>(null);
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
) => {
|
|
1322
|
-
console.time('top of effectiveSetState');
|
|
1323
|
-
const fullPath = [thisKey, ...path].join('.');
|
|
1324
|
-
const store = getGlobalStore.getState();
|
|
1325
|
-
|
|
1326
|
-
const shadowMeta = store.getShadowMetadata(thisKey, path);
|
|
1327
|
-
const nestedShadowValue = store.getShadowValue(fullPath) as TStateObject;
|
|
1328
|
-
console.timeEnd('top of effectiveSetState');
|
|
1329
|
-
|
|
1330
|
-
console.time('top of payload');
|
|
1331
|
-
const payload = (
|
|
1332
|
-
updateObj.updateType === 'insert' && isFunction(newStateOrFunction)
|
|
1333
|
-
? newStateOrFunction({ state: nestedShadowValue, uuid: uuidv4() })
|
|
1334
|
-
: isFunction(newStateOrFunction)
|
|
1335
|
-
? newStateOrFunction(nestedShadowValue)
|
|
1336
|
-
: newStateOrFunction
|
|
1337
|
-
) as TStateObject;
|
|
1338
|
-
|
|
1339
|
-
const timeStamp = Date.now();
|
|
1340
|
-
|
|
1341
|
-
const newUpdate = {
|
|
1342
|
-
timeStamp,
|
|
1343
|
-
stateKey: thisKey,
|
|
1344
|
-
path,
|
|
1345
|
-
updateType: updateObj.updateType,
|
|
1346
|
-
status: 'new' as const,
|
|
1347
|
-
oldValue: nestedShadowValue,
|
|
1348
|
-
newValue: payload,
|
|
1349
|
-
} satisfies UpdateTypeDetail;
|
|
1350
|
-
console.timeEnd('top of payload');
|
|
1351
|
-
|
|
1352
|
-
console.time('switch in effectiveSetState');
|
|
1353
|
-
// Perform the update
|
|
1354
|
-
switch (updateObj.updateType) {
|
|
1355
|
-
case 'insert': {
|
|
1356
|
-
store.insertShadowArrayElement(thisKey, path, newUpdate.newValue);
|
|
1357
|
-
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1358
|
-
const arrayMeta = shadowMeta;
|
|
1359
|
-
if (arrayMeta?.arrayKeys) {
|
|
1360
|
-
const newItemKey =
|
|
1361
|
-
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
1362
|
-
if (newItemKey) {
|
|
1363
|
-
const newItemPath = newItemKey.split('.').slice(1); // Remove stateKey
|
|
1364
|
-
store.markAsDirty(thisKey, newItemPath, { bubble: false });
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
break;
|
|
1368
|
-
}
|
|
1369
|
-
case 'cut': {
|
|
1370
|
-
const parentArrayPath = path.slice(0, -1);
|
|
1371
|
-
|
|
1372
|
-
store.removeShadowArrayElement(thisKey, path);
|
|
1373
|
-
store.markAsDirty(thisKey, parentArrayPath, { bubble: true });
|
|
1374
|
-
break;
|
|
1375
|
-
}
|
|
1376
|
-
case 'update': {
|
|
1377
|
-
store.updateShadowAtPath(thisKey, path, newUpdate.newValue);
|
|
1378
|
-
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1379
|
-
break;
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
console.timeEnd('switch in effectiveSetState');
|
|
1383
|
-
const shouldSync = updateObj.sync !== false;
|
|
1384
|
-
console.time('signals');
|
|
1385
|
-
|
|
1386
|
-
if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
|
|
1387
|
-
syncApiRef.current.updateState({ operation: newUpdate });
|
|
1388
|
-
}
|
|
1389
|
-
// Handle signals - reuse shadowMeta from the beginning
|
|
1390
|
-
if (shadowMeta?.signals && shadowMeta.signals.length > 0) {
|
|
1391
|
-
// Use updatedShadowValue if we need the new value, otherwise use payload
|
|
1392
|
-
const displayValue = updateObj.updateType === 'cut' ? null : payload;
|
|
1393
|
-
|
|
1394
|
-
shadowMeta.signals.forEach(({ parentId, position, effect }) => {
|
|
1395
|
-
const parent = document.querySelector(`[data-parent-id="${parentId}"]`);
|
|
1396
|
-
if (parent) {
|
|
1397
|
-
const childNodes = Array.from(parent.childNodes);
|
|
1398
|
-
if (childNodes[position]) {
|
|
1399
|
-
let finalDisplayValue = displayValue;
|
|
1400
|
-
if (effect && displayValue !== null) {
|
|
1401
|
-
try {
|
|
1402
|
-
finalDisplayValue = new Function(
|
|
1403
|
-
'state',
|
|
1404
|
-
`return (${effect})(state)`
|
|
1405
|
-
)(displayValue);
|
|
1406
|
-
} catch (err) {
|
|
1407
|
-
console.error('Error evaluating effect function:', err);
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
if (
|
|
1412
|
-
finalDisplayValue !== null &&
|
|
1413
|
-
finalDisplayValue !== undefined &&
|
|
1414
|
-
typeof finalDisplayValue === 'object'
|
|
1415
|
-
) {
|
|
1416
|
-
finalDisplayValue = JSON.stringify(finalDisplayValue) as any;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
childNodes[position].textContent = String(finalDisplayValue ?? '');
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
// Update in effectiveSetState for insert handling:
|
|
1426
|
-
if (updateObj.updateType === 'insert') {
|
|
1427
|
-
// Use shadowMeta from beginning if it's an array
|
|
1428
|
-
if (shadowMeta?.mapWrappers && shadowMeta.mapWrappers.length > 0) {
|
|
1429
|
-
// Get fresh array keys after insert
|
|
1430
|
-
const sourceArrayKeys =
|
|
1431
|
-
store.getShadowMetadata(thisKey, path)?.arrayKeys || [];
|
|
1432
|
-
const newItemKey = sourceArrayKeys[sourceArrayKeys.length - 1]!;
|
|
1433
|
-
const newItemValue = store.getShadowValue(newItemKey);
|
|
1434
|
-
const fullSourceArray = store.getShadowValue(
|
|
1435
|
-
[thisKey, ...path].join('.')
|
|
1436
|
-
);
|
|
1437
|
-
|
|
1438
|
-
if (!newItemKey || newItemValue === undefined) return;
|
|
1439
|
-
|
|
1440
|
-
shadowMeta.mapWrappers.forEach((wrapper) => {
|
|
1441
|
-
let shouldRender = true;
|
|
1442
|
-
let insertPosition = -1;
|
|
1443
|
-
|
|
1444
|
-
// Check if wrapper has transforms
|
|
1445
|
-
if (wrapper.meta?.transforms && wrapper.meta.transforms.length > 0) {
|
|
1446
|
-
// Check if new item passes all filters
|
|
1447
|
-
for (const transform of wrapper.meta.transforms) {
|
|
1448
|
-
if (transform.type === 'filter') {
|
|
1449
|
-
if (!transform.fn(newItemValue, -1)) {
|
|
1450
|
-
shouldRender = false;
|
|
1451
|
-
break;
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
if (shouldRender) {
|
|
1457
|
-
// Get current valid keys by applying transforms
|
|
1458
|
-
const currentValidKeys = applyTransforms(
|
|
1459
|
-
thisKey,
|
|
1460
|
-
path,
|
|
1461
|
-
wrapper.meta.transforms
|
|
1462
|
-
);
|
|
1463
|
-
|
|
1464
|
-
// Find where to insert based on sort
|
|
1465
|
-
const sortTransform = wrapper.meta.transforms.find(
|
|
1466
|
-
(t: any) => t.type === 'sort'
|
|
1467
|
-
);
|
|
1468
|
-
if (sortTransform) {
|
|
1469
|
-
// Add new item to the list and sort to find position
|
|
1470
|
-
const allItems = currentValidKeys.map((key) => ({
|
|
1471
|
-
key,
|
|
1472
|
-
value: store.getShadowValue(key),
|
|
1473
|
-
}));
|
|
1474
|
-
|
|
1475
|
-
allItems.push({ key: newItemKey, value: newItemValue });
|
|
1476
|
-
allItems.sort((a, b) => sortTransform.fn(a.value, b.value));
|
|
1477
|
-
|
|
1478
|
-
insertPosition = allItems.findIndex(
|
|
1479
|
-
(item) => item.key === newItemKey
|
|
1480
|
-
);
|
|
1481
|
-
} else {
|
|
1482
|
-
// No sort, insert at end
|
|
1483
|
-
insertPosition = currentValidKeys.length;
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
} else {
|
|
1487
|
-
// No transforms, always render at end
|
|
1488
|
-
shouldRender = true;
|
|
1489
|
-
insertPosition = sourceArrayKeys.length - 1;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
if (!shouldRender) {
|
|
1493
|
-
return; // Skip this wrapper, item doesn't pass filters
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
if (wrapper.containerRef && wrapper.containerRef.isConnected) {
|
|
1497
|
-
const itemElement = document.createElement('div');
|
|
1498
|
-
itemElement.setAttribute('data-item-path', newItemKey);
|
|
1499
|
-
|
|
1500
|
-
// Insert at correct position
|
|
1501
|
-
const children = Array.from(wrapper.containerRef.children);
|
|
1502
|
-
if (insertPosition >= 0 && insertPosition < children.length) {
|
|
1503
|
-
wrapper.containerRef.insertBefore(
|
|
1504
|
-
itemElement,
|
|
1505
|
-
children[insertPosition]!
|
|
1506
|
-
);
|
|
1507
|
-
} else {
|
|
1508
|
-
wrapper.containerRef.appendChild(itemElement);
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
const root = createRoot(itemElement);
|
|
1512
|
-
const componentId = uuidv4();
|
|
1513
|
-
const itemPath = newItemKey.split('.').slice(1);
|
|
1514
|
-
|
|
1515
|
-
const arraySetter = wrapper.rebuildStateShape({
|
|
1516
|
-
path: wrapper.path,
|
|
1517
|
-
currentState: fullSourceArray,
|
|
1518
|
-
componentId: wrapper.componentId,
|
|
1519
|
-
meta: wrapper.meta,
|
|
1520
|
-
});
|
|
1521
|
-
|
|
1522
|
-
root.render(
|
|
1523
|
-
createElement(MemoizedCogsItemWrapper, {
|
|
1524
|
-
stateKey: thisKey,
|
|
1525
|
-
itemComponentId: componentId,
|
|
1526
|
-
itemPath: itemPath,
|
|
1527
|
-
localIndex: insertPosition,
|
|
1528
|
-
arraySetter: arraySetter,
|
|
1529
|
-
rebuildStateShape: wrapper.rebuildStateShape,
|
|
1530
|
-
renderFn: wrapper.mapFn,
|
|
1531
|
-
})
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
if (updateObj.updateType === 'cut') {
|
|
1539
|
-
const arrayPath = path.slice(0, -1);
|
|
1540
|
-
const arrayMeta = store.getShadowMetadata(thisKey, arrayPath);
|
|
1541
|
-
|
|
1542
|
-
if (arrayMeta?.mapWrappers && arrayMeta.mapWrappers.length > 0) {
|
|
1543
|
-
arrayMeta.mapWrappers.forEach((wrapper) => {
|
|
1544
|
-
if (wrapper.containerRef && wrapper.containerRef.isConnected) {
|
|
1545
|
-
const elementToRemove = wrapper.containerRef.querySelector(
|
|
1546
|
-
`[data-item-path="${fullPath}"]`
|
|
1547
|
-
);
|
|
1548
|
-
if (elementToRemove) {
|
|
1549
|
-
elementToRemove.remove();
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
console.timeEnd('signals');
|
|
1556
|
-
|
|
1557
|
-
console.time('notify');
|
|
1558
|
-
const rootMeta = store.getShadowMetadata(thisKey, []);
|
|
1559
|
-
const notifiedComponents = new Set<string>();
|
|
1560
|
-
|
|
1561
|
-
if (!rootMeta?.components) {
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
1566
|
-
|
|
1567
|
-
if (updateObj.updateType === 'update') {
|
|
1568
|
-
// --- Bubble-up Notification ---
|
|
1569
|
-
// When a nested property changes, notify components listening at that exact path,
|
|
1570
|
-
// and also "bubble up" to notify components listening on parent paths.
|
|
1571
|
-
// e.g., an update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
|
|
1572
|
-
let currentPath = [...path]; // Create a mutable copy of the path
|
|
1573
|
-
|
|
1574
|
-
while (true) {
|
|
1575
|
-
const currentPathMeta = store.getShadowMetadata(thisKey, currentPath);
|
|
1576
|
-
|
|
1577
|
-
if (currentPathMeta?.pathComponents) {
|
|
1578
|
-
currentPathMeta.pathComponents.forEach((componentId) => {
|
|
1579
|
-
if (notifiedComponents.has(componentId)) {
|
|
1580
|
-
return; // Avoid sending redundant notifications
|
|
1581
|
-
}
|
|
1582
|
-
const component = rootMeta.components?.get(componentId);
|
|
1583
|
-
if (component) {
|
|
1584
|
-
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1585
|
-
? component.reactiveType
|
|
1586
|
-
: [component.reactiveType || 'component'];
|
|
1587
|
-
|
|
1588
|
-
// This notification logic applies to components that depend on object structures.
|
|
1589
|
-
if (!reactiveTypes.includes('none')) {
|
|
1590
|
-
component.forceUpdate();
|
|
1591
|
-
notifiedComponents.add(componentId);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
if (currentPath.length === 0) {
|
|
1598
|
-
break; // We've reached the root, stop bubbling.
|
|
1599
|
-
}
|
|
1600
|
-
currentPath.pop(); // Go up one level for the next iteration.
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
// ADDITIONALLY, if the payload is an object, perform a deep-check and
|
|
1604
|
-
// notify any components that are subscribed to specific sub-paths that changed.
|
|
1605
|
-
if (
|
|
1606
|
-
payload &&
|
|
1607
|
-
typeof payload === 'object' &&
|
|
1608
|
-
!isArray(payload) &&
|
|
1609
|
-
nestedShadowValue &&
|
|
1610
|
-
typeof nestedShadowValue === 'object' &&
|
|
1611
|
-
!isArray(nestedShadowValue)
|
|
1612
|
-
) {
|
|
1613
|
-
// Get a list of dot-separated paths that have changed (e.g., ['name', 'address.city'])
|
|
1614
|
-
const changedSubPaths = getDifferences(payload, nestedShadowValue);
|
|
1615
|
-
|
|
1616
|
-
changedSubPaths.forEach((subPathString) => {
|
|
1617
|
-
const subPath = subPathString.split('.');
|
|
1618
|
-
const fullSubPath = [...path, ...subPath];
|
|
1619
|
-
|
|
1620
|
-
// Get the metadata (and subscribers) for this specific nested path
|
|
1621
|
-
const subPathMeta = store.getShadowMetadata(thisKey, fullSubPath);
|
|
1622
|
-
if (subPathMeta?.pathComponents) {
|
|
1623
|
-
subPathMeta.pathComponents.forEach((componentId) => {
|
|
1624
|
-
// Avoid sending a redundant update
|
|
1625
|
-
if (notifiedComponents.has(componentId)) {
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
const component = rootMeta.components?.get(componentId);
|
|
1629
|
-
if (component) {
|
|
1630
|
-
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1631
|
-
? component.reactiveType
|
|
1632
|
-
: [component.reactiveType || 'component'];
|
|
1633
|
-
|
|
1634
|
-
if (!reactiveTypes.includes('none')) {
|
|
1635
|
-
component.forceUpdate();
|
|
1636
|
-
notifiedComponents.add(componentId);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
});
|
|
1642
|
-
}
|
|
1643
|
-
} else if (
|
|
1644
|
-
updateObj.updateType === 'insert' ||
|
|
1645
|
-
updateObj.updateType === 'cut'
|
|
1646
|
-
) {
|
|
1647
|
-
// For array structural changes, notify components listening to the parent array.
|
|
1648
|
-
const parentArrayPath =
|
|
1649
|
-
updateObj.updateType === 'insert' ? path : path.slice(0, -1);
|
|
1650
|
-
|
|
1651
|
-
const parentMeta = store.getShadowMetadata(thisKey, parentArrayPath);
|
|
1652
|
-
|
|
1653
|
-
// Handle signal updates for array length, etc.
|
|
1654
|
-
if (parentMeta?.signals && parentMeta.signals.length > 0) {
|
|
1655
|
-
const parentFullPath = [thisKey, ...parentArrayPath].join('.');
|
|
1656
|
-
const parentValue = store.getShadowValue(parentFullPath);
|
|
1657
|
-
|
|
1658
|
-
parentMeta.signals.forEach(({ parentId, position, effect }) => {
|
|
1659
|
-
const parent = document.querySelector(
|
|
1660
|
-
`[data-parent-id="${parentId}"]`
|
|
1661
|
-
);
|
|
1662
|
-
if (parent) {
|
|
1663
|
-
const childNodes = Array.from(parent.childNodes);
|
|
1664
|
-
if (childNodes[position]) {
|
|
1665
|
-
let displayValue = parentValue;
|
|
1666
|
-
if (effect) {
|
|
1667
|
-
try {
|
|
1668
|
-
displayValue = new Function(
|
|
1669
|
-
'state',
|
|
1670
|
-
`return (${effect})(state)`
|
|
1671
|
-
)(parentValue);
|
|
1672
|
-
} catch (err) {
|
|
1673
|
-
console.error('Error evaluating effect function:', err);
|
|
1674
|
-
displayValue = parentValue;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
if (
|
|
1679
|
-
displayValue !== null &&
|
|
1680
|
-
displayValue !== undefined &&
|
|
1681
|
-
typeof displayValue === 'object'
|
|
1682
|
-
) {
|
|
1683
|
-
displayValue = JSON.stringify(displayValue);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
childNodes[position].textContent = String(displayValue ?? '');
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// Notify components subscribed to the array itself.
|
|
1693
|
-
if (parentMeta?.pathComponents) {
|
|
1694
|
-
parentMeta.pathComponents.forEach((componentId) => {
|
|
1695
|
-
if (!notifiedComponents.has(componentId)) {
|
|
1696
|
-
const component = rootMeta.components?.get(componentId);
|
|
1697
|
-
if (component) {
|
|
1698
|
-
component.forceUpdate();
|
|
1699
|
-
notifiedComponents.add(componentId);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
});
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
rootMeta.components.forEach((component, componentId) => {
|
|
1707
|
-
if (notifiedComponents.has(componentId)) {
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1712
|
-
? component.reactiveType
|
|
1713
|
-
: [component.reactiveType || 'component'];
|
|
1714
|
-
|
|
1715
|
-
if (reactiveTypes.includes('all')) {
|
|
1716
|
-
component.forceUpdate();
|
|
1717
|
-
notifiedComponents.add(componentId);
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
if (reactiveTypes.includes('deps')) {
|
|
1722
|
-
if (component.depsFunction) {
|
|
1723
|
-
const currentState = store.getShadowValue(thisKey);
|
|
1724
|
-
const newDeps = component.depsFunction(currentState);
|
|
1725
|
-
let shouldUpdate = false;
|
|
1726
|
-
|
|
1727
|
-
if (newDeps === true) {
|
|
1728
|
-
shouldUpdate = true;
|
|
1729
|
-
} else if (Array.isArray(newDeps)) {
|
|
1730
|
-
if (!isDeepEqual(component.prevDeps, newDeps)) {
|
|
1731
|
-
component.prevDeps = newDeps;
|
|
1732
|
-
shouldUpdate = true;
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
if (shouldUpdate) {
|
|
1737
|
-
component.forceUpdate();
|
|
1738
|
-
notifiedComponents.add(componentId);
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
});
|
|
1743
|
-
notifiedComponents.clear();
|
|
1744
|
-
console.timeEnd('notify');
|
|
1745
|
-
console.time('end stuff');
|
|
1746
|
-
addStateLog(thisKey, newUpdate);
|
|
1747
|
-
|
|
1748
|
-
saveToLocalStorage(
|
|
1749
|
-
payload,
|
|
1750
|
-
thisKey,
|
|
1751
|
-
latestInitialOptionsRef.current,
|
|
1752
|
-
sessionId
|
|
1753
|
-
);
|
|
1754
|
-
|
|
1755
|
-
if (latestInitialOptionsRef.current?.middleware) {
|
|
1756
|
-
latestInitialOptionsRef.current!.middleware({
|
|
1757
|
-
update: newUpdate,
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
console.timeEnd('end stuff');
|
|
1761
|
-
};
|
|
1656
|
+
const effectiveSetState = createEffectiveSetState(
|
|
1657
|
+
thisKey,
|
|
1658
|
+
syncApiRef,
|
|
1659
|
+
sessionId,
|
|
1660
|
+
latestInitialOptionsRef
|
|
1661
|
+
);
|
|
1762
1662
|
|
|
1763
1663
|
if (!getGlobalStore.getState().initialStateGlobal[thisKey]) {
|
|
1764
1664
|
updateInitialStateGlobal(thisKey, stateObject);
|
|
1765
1665
|
}
|
|
1766
1666
|
|
|
1767
1667
|
const updaterFinal = useMemo(() => {
|
|
1768
|
-
console.time('createProxyHandler');
|
|
1769
1668
|
const handler = createProxyHandler<TStateObject>(
|
|
1770
1669
|
thisKey,
|
|
1771
1670
|
effectiveSetState,
|
|
1772
1671
|
componentIdRef.current,
|
|
1773
1672
|
sessionId
|
|
1774
1673
|
);
|
|
1775
|
-
|
|
1674
|
+
|
|
1776
1675
|
return handler;
|
|
1777
1676
|
}, [thisKey, sessionId]);
|
|
1778
1677
|
|
|
@@ -1789,30 +1688,15 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1789
1688
|
return updaterFinal;
|
|
1790
1689
|
}
|
|
1791
1690
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
*
|
|
1798
|
-
* - `stateFilter` populates this with only the IDs of items that passed the filter.
|
|
1799
|
-
* - `stateSort` reorders this list to match the new sort order.
|
|
1800
|
-
* - All subsequent chained operations (like `.get()`, `.index()`, or `.cut()`)
|
|
1801
|
-
* MUST consult this list first to know which items they apply to and in what order.
|
|
1802
|
-
*/
|
|
1803
|
-
validIds?: string[];
|
|
1804
|
-
|
|
1805
|
-
/**
|
|
1806
|
-
* An array of the actual filter functions that have been applied in a chain.
|
|
1807
|
-
* This is primarily used by reactive renderers like `$stateMap` to make predictions.
|
|
1808
|
-
*
|
|
1809
|
-
* For example, when a new item is inserted into the original source array, a
|
|
1810
|
-
* `$stateMap` renderer on a filtered view can use these functions to test if the
|
|
1811
|
-
* newly inserted item should be dynamically rendered in its view.
|
|
1812
|
-
*/
|
|
1691
|
+
type MetaData = {
|
|
1692
|
+
// Map array paths to their filtered/sorted ID order
|
|
1693
|
+
arrayViews?: {
|
|
1694
|
+
[arrayPath: string]: string[]; // e.g. { "todos": ["id:xxx", "id:yyy"], "todos.id:xxx.subtasks": ["id:aaa"] }
|
|
1695
|
+
};
|
|
1813
1696
|
transforms?: Array<{
|
|
1814
1697
|
type: 'filter' | 'sort';
|
|
1815
1698
|
fn: Function;
|
|
1699
|
+
path: string[]; // Which array this transform applies to
|
|
1816
1700
|
}>;
|
|
1817
1701
|
};
|
|
1818
1702
|
|
|
@@ -1827,35 +1711,40 @@ function hashTransforms(transforms: any[]) {
|
|
|
1827
1711
|
)
|
|
1828
1712
|
.join('');
|
|
1829
1713
|
}
|
|
1714
|
+
|
|
1830
1715
|
const applyTransforms = (
|
|
1831
1716
|
stateKey: string,
|
|
1832
1717
|
path: string[],
|
|
1833
1718
|
transforms?: Array<{ type: 'filter' | 'sort'; fn: Function }>
|
|
1834
1719
|
): string[] => {
|
|
1835
|
-
let arrayKeys
|
|
1836
|
-
|
|
1837
|
-
[];
|
|
1838
|
-
|
|
1720
|
+
let ids = getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
1721
|
+
console.log('ids', ids);
|
|
1839
1722
|
if (!transforms || transforms.length === 0) {
|
|
1840
|
-
return
|
|
1723
|
+
return ids;
|
|
1841
1724
|
}
|
|
1842
1725
|
|
|
1843
|
-
|
|
1844
|
-
key,
|
|
1845
|
-
value: getGlobalStore.getState().getShadowValue(key),
|
|
1846
|
-
}));
|
|
1847
|
-
|
|
1726
|
+
// Apply each transform using just IDs
|
|
1848
1727
|
for (const transform of transforms) {
|
|
1849
1728
|
if (transform.type === 'filter') {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1729
|
+
const filtered: any[] = [];
|
|
1730
|
+
ids.forEach((id, index) => {
|
|
1731
|
+
const value = getShadowValue(stateKey, [...path, id]);
|
|
1732
|
+
|
|
1733
|
+
if (transform.fn(value, index)) {
|
|
1734
|
+
filtered.push(id);
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
ids = filtered;
|
|
1853
1738
|
} else if (transform.type === 'sort') {
|
|
1854
|
-
|
|
1739
|
+
ids.sort((a, b) => {
|
|
1740
|
+
const aValue = getShadowValue(stateKey, [...path, a]);
|
|
1741
|
+
const bValue = getShadowValue(stateKey, [...path, b]);
|
|
1742
|
+
return transform.fn(aValue, bValue);
|
|
1743
|
+
});
|
|
1855
1744
|
}
|
|
1856
1745
|
}
|
|
1857
1746
|
|
|
1858
|
-
return
|
|
1747
|
+
return ids;
|
|
1859
1748
|
};
|
|
1860
1749
|
const registerComponentDependency = (
|
|
1861
1750
|
stateKey: string,
|
|
@@ -1863,7 +1752,6 @@ const registerComponentDependency = (
|
|
|
1863
1752
|
dependencyPath: string[]
|
|
1864
1753
|
) => {
|
|
1865
1754
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
1866
|
-
const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
|
|
1867
1755
|
|
|
1868
1756
|
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1869
1757
|
const component = rootMeta?.components?.get(fullComponentId);
|
|
@@ -1927,6 +1815,28 @@ const notifySelectionComponents = (
|
|
|
1927
1815
|
}
|
|
1928
1816
|
}
|
|
1929
1817
|
};
|
|
1818
|
+
function getScopedData(stateKey: string, path: string[], meta?: MetaData) {
|
|
1819
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
1820
|
+
const arrayPathKey = path.join('.');
|
|
1821
|
+
const arrayKeys = meta?.arrayViews?.[arrayPathKey];
|
|
1822
|
+
|
|
1823
|
+
// FIX: If the derived view is empty, return an empty array directly.
|
|
1824
|
+
if (Array.isArray(arrayKeys) && arrayKeys.length === 0) {
|
|
1825
|
+
return {
|
|
1826
|
+
shadowMeta,
|
|
1827
|
+
value: [],
|
|
1828
|
+
arrayKeys: shadowMeta?.arrayKeys,
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const value = getShadowValue(stateKey, path, arrayKeys);
|
|
1833
|
+
|
|
1834
|
+
return {
|
|
1835
|
+
shadowMeta,
|
|
1836
|
+
value,
|
|
1837
|
+
arrayKeys: shadowMeta?.arrayKeys,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1930
1840
|
|
|
1931
1841
|
function createProxyHandler<T>(
|
|
1932
1842
|
stateKey: string,
|
|
@@ -1936,9 +1846,58 @@ function createProxyHandler<T>(
|
|
|
1936
1846
|
): StateObject<T> {
|
|
1937
1847
|
const proxyCache = new Map<string, any>();
|
|
1938
1848
|
let stateVersion = 0;
|
|
1939
|
-
console.time('rebuildStateShape Outer');
|
|
1940
1849
|
|
|
1941
|
-
|
|
1850
|
+
const methodNames = new Set([
|
|
1851
|
+
'sync',
|
|
1852
|
+
'getStatus',
|
|
1853
|
+
'removeStorage',
|
|
1854
|
+
'showValidationErrors',
|
|
1855
|
+
'getSelected',
|
|
1856
|
+
'getSelectedIndex',
|
|
1857
|
+
'clearSelected',
|
|
1858
|
+
'useVirtualView',
|
|
1859
|
+
'stateMap',
|
|
1860
|
+
'$stateMap',
|
|
1861
|
+
'stateFind',
|
|
1862
|
+
'stateFilter',
|
|
1863
|
+
'stateSort',
|
|
1864
|
+
'stream',
|
|
1865
|
+
'stateList',
|
|
1866
|
+
'stateFlattenOn',
|
|
1867
|
+
'index',
|
|
1868
|
+
'last',
|
|
1869
|
+
'insert',
|
|
1870
|
+
'uniqueInsert',
|
|
1871
|
+
'cut',
|
|
1872
|
+
'cutSelected',
|
|
1873
|
+
'cutByValue',
|
|
1874
|
+
'toggleByValue',
|
|
1875
|
+
'findWith',
|
|
1876
|
+
'cutThis',
|
|
1877
|
+
'get',
|
|
1878
|
+
'getState',
|
|
1879
|
+
'$derive',
|
|
1880
|
+
'$get',
|
|
1881
|
+
'lastSynced',
|
|
1882
|
+
'getLocalStorage',
|
|
1883
|
+
'isSelected',
|
|
1884
|
+
'setSelected',
|
|
1885
|
+
'toggleSelected',
|
|
1886
|
+
'_componentId',
|
|
1887
|
+
'addZodValidation',
|
|
1888
|
+
'clearZodValidation',
|
|
1889
|
+
'applyJsonPatch',
|
|
1890
|
+
'getComponents',
|
|
1891
|
+
'getAllFormRefs',
|
|
1892
|
+
'getFormRef',
|
|
1893
|
+
'validationWrapper',
|
|
1894
|
+
'_stateKey',
|
|
1895
|
+
'_path',
|
|
1896
|
+
'update',
|
|
1897
|
+
'toggle',
|
|
1898
|
+
'formElement',
|
|
1899
|
+
// Add ANY other method names here
|
|
1900
|
+
]);
|
|
1942
1901
|
|
|
1943
1902
|
function rebuildStateShape({
|
|
1944
1903
|
path = [],
|
|
@@ -1949,75 +1908,35 @@ function createProxyHandler<T>(
|
|
|
1949
1908
|
componentId: string;
|
|
1950
1909
|
meta?: MetaData;
|
|
1951
1910
|
}): any {
|
|
1952
|
-
console.time('rebuildStateShape Inner');
|
|
1953
1911
|
const derivationSignature = meta
|
|
1954
|
-
? JSON.stringify(meta.
|
|
1912
|
+
? JSON.stringify(meta.arrayViews || meta.transforms)
|
|
1955
1913
|
: '';
|
|
1956
1914
|
const cacheKey = path.join('.') + ':' + derivationSignature;
|
|
1957
|
-
console.log('PROXY CACHE KEY ', cacheKey);
|
|
1958
1915
|
if (proxyCache.has(cacheKey)) {
|
|
1959
|
-
console.log('PROXY CACHE HIT');
|
|
1960
1916
|
return proxyCache.get(cacheKey);
|
|
1961
1917
|
}
|
|
1962
1918
|
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
1963
1919
|
|
|
1964
|
-
type CallableStateObject<T> = {
|
|
1965
|
-
(): T;
|
|
1966
|
-
} & {
|
|
1967
|
-
[key: string]: any;
|
|
1968
|
-
};
|
|
1969
|
-
|
|
1970
|
-
const baseFunction = function () {
|
|
1971
|
-
return getGlobalStore().getShadowValue(stateKey, path);
|
|
1972
|
-
} as unknown as CallableStateObject<T>;
|
|
1973
|
-
|
|
1974
1920
|
// We attach baseObj properties *inside* the get trap now to avoid recursion
|
|
1975
1921
|
// This is a placeholder for the proxy.
|
|
1976
1922
|
|
|
1977
1923
|
const handler = {
|
|
1978
|
-
apply(target: any, thisArg: any, args: any[]) {
|
|
1979
|
-
//return getGlobalStore().getShadowValue(stateKey, path);
|
|
1980
|
-
},
|
|
1981
|
-
|
|
1982
1924
|
get(target: any, prop: string) {
|
|
1983
|
-
if (path.length === 0) {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1925
|
+
if (path.length === 0 && prop in rootLevelMethods) {
|
|
1926
|
+
return rootLevelMethods[prop as keyof typeof rootLevelMethods];
|
|
1927
|
+
}
|
|
1928
|
+
if (!methodNames.has(prop)) {
|
|
1929
|
+
const nextPath = [...path, prop];
|
|
1930
|
+
return rebuildStateShape({
|
|
1931
|
+
path: nextPath,
|
|
1932
|
+
componentId: componentId!,
|
|
1933
|
+
meta,
|
|
1934
|
+
});
|
|
1987
1935
|
}
|
|
1988
1936
|
if (prop === '_rebuildStateShape') {
|
|
1989
1937
|
return rebuildStateShape;
|
|
1990
1938
|
}
|
|
1991
|
-
const baseObjProps = Object.getOwnPropertyNames(baseObj);
|
|
1992
|
-
if (baseObjProps.includes(prop) && path.length === 0) {
|
|
1993
|
-
return (baseObj as any)[prop];
|
|
1994
|
-
}
|
|
1995
|
-
// ^--------- END OF FIX ---------^
|
|
1996
|
-
|
|
1997
|
-
if (prop === 'getDifferences') {
|
|
1998
|
-
return () => {
|
|
1999
|
-
const shadowMeta = getGlobalStore
|
|
2000
|
-
.getState()
|
|
2001
|
-
.getShadowMetadata(stateKey, []);
|
|
2002
|
-
const currentState = getGlobalStore
|
|
2003
|
-
.getState()
|
|
2004
|
-
.getShadowValue(stateKey);
|
|
2005
1939
|
|
|
2006
|
-
// Use the appropriate base state for comparison
|
|
2007
|
-
let baseState;
|
|
2008
|
-
if (
|
|
2009
|
-
shadowMeta?.stateSource === 'server' &&
|
|
2010
|
-
shadowMeta.baseServerState
|
|
2011
|
-
) {
|
|
2012
|
-
baseState = shadowMeta.baseServerState;
|
|
2013
|
-
} else {
|
|
2014
|
-
baseState =
|
|
2015
|
-
getGlobalStore.getState().initialStateGlobal[stateKey];
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
return getDifferences(currentState, baseState);
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
1940
|
if (prop === 'sync' && path.length === 0) {
|
|
2022
1941
|
return async function () {
|
|
2023
1942
|
const options = getGlobalStore
|
|
@@ -2058,7 +1977,7 @@ function createProxyHandler<T>(
|
|
|
2058
1977
|
const shadowMeta = getGlobalStore
|
|
2059
1978
|
.getState()
|
|
2060
1979
|
.getShadowMetadata(stateKey, []);
|
|
2061
|
-
|
|
1980
|
+
setShadowMetadata(stateKey, [], {
|
|
2062
1981
|
...shadowMeta,
|
|
2063
1982
|
isDirty: false,
|
|
2064
1983
|
lastServerSync: Date.now(),
|
|
@@ -2082,56 +2001,46 @@ function createProxyHandler<T>(
|
|
|
2082
2001
|
// Fixed getStatus function in createProxyHandler
|
|
2083
2002
|
if (prop === '_status' || prop === 'getStatus') {
|
|
2084
2003
|
const getStatusFunc = () => {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
.getShadowMetadata(stateKey, path);
|
|
2088
|
-
const value = getGlobalStore
|
|
2089
|
-
.getState()
|
|
2090
|
-
.getShadowValue(stateKeyPathKey);
|
|
2004
|
+
// ✅ Use the optimized helper to get all data in one efficient call
|
|
2005
|
+
const { shadowMeta, value } = getScopedData(stateKey, path, meta);
|
|
2091
2006
|
|
|
2092
|
-
// Priority 1: Explicitly dirty items
|
|
2007
|
+
// Priority 1: Explicitly dirty items. This is the most important status.
|
|
2093
2008
|
if (shadowMeta?.isDirty === true) {
|
|
2094
2009
|
return 'dirty';
|
|
2095
2010
|
}
|
|
2096
2011
|
|
|
2097
|
-
// Priority 2:
|
|
2098
|
-
if
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2012
|
+
// ✅ Priority 2: Synced items. This condition is now cleaner.
|
|
2013
|
+
// An item is considered synced if it came from the server OR was explicitly
|
|
2014
|
+
// marked as not dirty (isDirty: false), covering all sync-related cases.
|
|
2015
|
+
if (
|
|
2016
|
+
shadowMeta?.stateSource === 'server' ||
|
|
2017
|
+
shadowMeta?.isDirty === false
|
|
2018
|
+
) {
|
|
2104
2019
|
return 'synced';
|
|
2105
2020
|
}
|
|
2106
2021
|
|
|
2107
|
-
// Priority
|
|
2022
|
+
// Priority 3: Items restored from localStorage.
|
|
2108
2023
|
if (shadowMeta?.stateSource === 'localStorage') {
|
|
2109
2024
|
return 'restored';
|
|
2110
2025
|
}
|
|
2111
2026
|
|
|
2112
|
-
// Priority
|
|
2027
|
+
// Priority 4: Items from default/initial state.
|
|
2113
2028
|
if (shadowMeta?.stateSource === 'default') {
|
|
2114
2029
|
return 'fresh';
|
|
2115
2030
|
}
|
|
2116
2031
|
|
|
2117
|
-
//
|
|
2118
|
-
// Look up the tree to see if parent has server source
|
|
2119
|
-
const rootMeta = getGlobalStore
|
|
2120
|
-
.getState()
|
|
2121
|
-
.getShadowMetadata(stateKey, []);
|
|
2122
|
-
if (rootMeta?.stateSource === 'server' && !shadowMeta?.isDirty) {
|
|
2123
|
-
return 'synced';
|
|
2124
|
-
}
|
|
2032
|
+
// ✅ REMOVED the redundant "root" check. The item's own `stateSource` is sufficient.
|
|
2125
2033
|
|
|
2126
|
-
// Priority
|
|
2034
|
+
// Priority 5: A value exists but has no metadata. This is a fallback.
|
|
2127
2035
|
if (value !== undefined && !shadowMeta) {
|
|
2128
2036
|
return 'fresh';
|
|
2129
2037
|
}
|
|
2130
2038
|
|
|
2131
|
-
// Fallback
|
|
2039
|
+
// Fallback if no other condition is met.
|
|
2132
2040
|
return 'unknown';
|
|
2133
2041
|
};
|
|
2134
2042
|
|
|
2043
|
+
// This part remains the same
|
|
2135
2044
|
return prop === '_status' ? getStatusFunc() : getStatusFunc;
|
|
2136
2045
|
}
|
|
2137
2046
|
if (prop === 'removeStorage') {
|
|
@@ -2148,14 +2057,15 @@ function createProxyHandler<T>(
|
|
|
2148
2057
|
}
|
|
2149
2058
|
if (prop === 'showValidationErrors') {
|
|
2150
2059
|
return () => {
|
|
2151
|
-
const
|
|
2152
|
-
.getState()
|
|
2153
|
-
.getShadowMetadata(stateKey, path);
|
|
2060
|
+
const { shadowMeta } = getScopedData(stateKey, path, meta);
|
|
2154
2061
|
if (
|
|
2155
|
-
|
|
2156
|
-
|
|
2062
|
+
shadowMeta?.validation?.status === 'INVALID' &&
|
|
2063
|
+
shadowMeta.validation.errors.length > 0
|
|
2157
2064
|
) {
|
|
2158
|
-
|
|
2065
|
+
// Return only error-severity messages (not warnings)
|
|
2066
|
+
return shadowMeta.validation.errors
|
|
2067
|
+
.filter((err) => err.severity === 'error')
|
|
2068
|
+
.map((err) => err.message);
|
|
2159
2069
|
}
|
|
2160
2070
|
return [];
|
|
2161
2071
|
};
|
|
@@ -2163,55 +2073,77 @@ function createProxyHandler<T>(
|
|
|
2163
2073
|
|
|
2164
2074
|
if (prop === 'getSelected') {
|
|
2165
2075
|
return () => {
|
|
2166
|
-
const
|
|
2076
|
+
const arrayKey = [stateKey, ...path].join('.');
|
|
2167
2077
|
registerComponentDependency(stateKey, componentId, [
|
|
2168
2078
|
...path,
|
|
2169
2079
|
'getSelected',
|
|
2170
2080
|
]);
|
|
2171
2081
|
|
|
2172
|
-
const
|
|
2173
|
-
|
|
2174
|
-
|
|
2082
|
+
const selectedItemKey = getGlobalStore
|
|
2083
|
+
.getState()
|
|
2084
|
+
.selectedIndicesMap.get(arrayKey);
|
|
2085
|
+
if (!selectedItemKey) {
|
|
2175
2086
|
return undefined;
|
|
2176
2087
|
}
|
|
2177
2088
|
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
return undefined;
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2089
|
+
const viewKey = path.join('.');
|
|
2090
|
+
const currentViewIds = meta?.arrayViews?.[viewKey];
|
|
2091
|
+
const selectedItemId = selectedItemKey.split('.').pop();
|
|
2184
2092
|
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2093
|
+
// FIX: Only return the selected item if it exists in the current filtered/sorted view.
|
|
2094
|
+
if (currentViewIds && !currentViewIds.includes(selectedItemId!)) {
|
|
2095
|
+
return undefined;
|
|
2096
|
+
}
|
|
2188
2097
|
|
|
2189
|
-
|
|
2098
|
+
const value = getShadowValue(
|
|
2099
|
+
stateKey,
|
|
2100
|
+
selectedItemKey.split('.').slice(1)
|
|
2101
|
+
);
|
|
2102
|
+
if (value === undefined) {
|
|
2190
2103
|
return undefined;
|
|
2191
2104
|
}
|
|
2192
2105
|
|
|
2193
2106
|
return rebuildStateShape({
|
|
2194
2107
|
path: selectedItemKey.split('.').slice(1) as string[],
|
|
2195
2108
|
componentId: componentId!,
|
|
2109
|
+
meta,
|
|
2196
2110
|
});
|
|
2197
2111
|
};
|
|
2198
2112
|
}
|
|
2199
2113
|
if (prop === 'getSelectedIndex') {
|
|
2200
2114
|
return () => {
|
|
2201
|
-
|
|
2115
|
+
// Key for the array in the global selection map (e.g., "myState.products")
|
|
2116
|
+
const arrayKey = stateKey + '.' + path.join('.');
|
|
2117
|
+
// Key for this specific view in the meta object (e.g., "products")
|
|
2118
|
+
const viewKey = path.join('.');
|
|
2119
|
+
|
|
2120
|
+
// Get the full path of the selected item (e.g., "myState.products.id:abc")
|
|
2121
|
+
const selectedItemKey = getGlobalStore
|
|
2202
2122
|
.getState()
|
|
2203
|
-
.
|
|
2204
|
-
stateKey + '.' + path.join('.'),
|
|
2205
|
-
meta?.validIds
|
|
2206
|
-
);
|
|
2123
|
+
.selectedIndicesMap.get(arrayKey);
|
|
2207
2124
|
|
|
2208
|
-
|
|
2125
|
+
if (!selectedItemKey) {
|
|
2126
|
+
return -1; // Nothing is selected for this array.
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// Get the list of item IDs for the current filtered/sorted view.
|
|
2130
|
+
const { keys: viewIds } = getArrayData(stateKey, path, meta);
|
|
2131
|
+
|
|
2132
|
+
if (!viewIds) {
|
|
2133
|
+
return -1; // Should not happen if it's an array, but a safe guard.
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// FIX: Extract just the ID from the full selected item path.
|
|
2137
|
+
const selectedId = selectedItemKey.split('.').pop();
|
|
2138
|
+
|
|
2139
|
+
// Return the index of that ID within the current view's list of IDs.
|
|
2140
|
+
return (viewIds as string[]).indexOf(selectedId as string);
|
|
2209
2141
|
};
|
|
2210
2142
|
}
|
|
2211
2143
|
if (prop === 'clearSelected') {
|
|
2212
2144
|
notifySelectionComponents(stateKey, path);
|
|
2213
2145
|
return () => {
|
|
2214
|
-
|
|
2146
|
+
clearSelectedIndex({
|
|
2215
2147
|
arrayKey: stateKey + '.' + path.join('.'),
|
|
2216
2148
|
});
|
|
2217
2149
|
};
|
|
@@ -2265,9 +2197,7 @@ function createProxyHandler<T>(
|
|
|
2265
2197
|
});
|
|
2266
2198
|
}, [rerender, stickToBottom]);
|
|
2267
2199
|
|
|
2268
|
-
const arrayKeys =
|
|
2269
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2270
|
-
?.arrayKeys || [];
|
|
2200
|
+
const { arrayKeys = [] } = getScopedData(stateKey, path, meta);
|
|
2271
2201
|
|
|
2272
2202
|
// Calculate total height and offsets
|
|
2273
2203
|
const { totalHeight, itemOffsets } = useMemo(() => {
|
|
@@ -2545,9 +2475,7 @@ function createProxyHandler<T>(
|
|
|
2545
2475
|
// Create virtual state
|
|
2546
2476
|
const virtualState = useMemo(() => {
|
|
2547
2477
|
const store = getGlobalStore.getState();
|
|
2548
|
-
const sourceArray = store.getShadowValue(
|
|
2549
|
-
[stateKey, ...path].join('.')
|
|
2550
|
-
) as any[];
|
|
2478
|
+
const sourceArray = store.getShadowValue(stateKey, path) as any[];
|
|
2551
2479
|
const currentKeys =
|
|
2552
2480
|
store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
2553
2481
|
|
|
@@ -2559,11 +2487,11 @@ function createProxyHandler<T>(
|
|
|
2559
2487
|
range.startIndex,
|
|
2560
2488
|
range.endIndex + 1
|
|
2561
2489
|
);
|
|
2562
|
-
|
|
2490
|
+
const arrayPath = path.length > 0 ? path.join('.') : 'root';
|
|
2563
2491
|
return rebuildStateShape({
|
|
2564
2492
|
path,
|
|
2565
2493
|
componentId: componentId!,
|
|
2566
|
-
meta: { ...meta,
|
|
2494
|
+
meta: { ...meta, arrayViews: { [arrayPath]: slicedIds } },
|
|
2567
2495
|
});
|
|
2568
2496
|
}, [range.startIndex, range.endIndex, arrayKeys.length]);
|
|
2569
2497
|
|
|
@@ -2610,150 +2538,84 @@ function createProxyHandler<T>(
|
|
|
2610
2538
|
}
|
|
2611
2539
|
if (prop === 'stateMap') {
|
|
2612
2540
|
return (
|
|
2613
|
-
callbackfn: (
|
|
2614
|
-
setter: any,
|
|
2615
|
-
index: number,
|
|
2616
|
-
|
|
2617
|
-
arraySetter: any
|
|
2618
|
-
) => void
|
|
2541
|
+
callbackfn: (setter: any, index: number, arraySetter: any) => void
|
|
2619
2542
|
) => {
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2543
|
+
// FIX: Use getArrayData to reliably get both the value and the keys of the current view.
|
|
2544
|
+
const { value: shadowValue, keys: arrayKeys } = getArrayData(
|
|
2545
|
+
stateKey,
|
|
2546
|
+
path,
|
|
2547
|
+
meta
|
|
2624
2548
|
);
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
// stateKeyPathKey
|
|
2629
|
-
// );
|
|
2630
|
-
// setArrayKeys(
|
|
2631
|
-
// getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2632
|
-
// );
|
|
2633
|
-
// });
|
|
2634
|
-
|
|
2635
|
-
const shadowValue = getGlobalStore
|
|
2636
|
-
.getState()
|
|
2637
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds) as any[];
|
|
2638
|
-
if (!arrayKeys) {
|
|
2639
|
-
throw new Error('No array keys found for mapping');
|
|
2549
|
+
|
|
2550
|
+
if (!arrayKeys || !Array.isArray(shadowValue)) {
|
|
2551
|
+
return []; // It's valid to map over an empty array.
|
|
2640
2552
|
}
|
|
2553
|
+
|
|
2641
2554
|
const arraySetter = rebuildStateShape({
|
|
2642
2555
|
path,
|
|
2643
2556
|
componentId: componentId!,
|
|
2644
2557
|
meta,
|
|
2645
2558
|
});
|
|
2646
2559
|
|
|
2647
|
-
return shadowValue.map((
|
|
2648
|
-
const
|
|
2649
|
-
|
|
2650
|
-
path: itemPath as any,
|
|
2651
|
-
componentId: componentId!,
|
|
2652
|
-
meta,
|
|
2653
|
-
});
|
|
2654
|
-
|
|
2655
|
-
return callbackfn(
|
|
2656
|
-
itemSetter,
|
|
2657
|
-
index,
|
|
2658
|
-
|
|
2659
|
-
arraySetter
|
|
2660
|
-
);
|
|
2661
|
-
});
|
|
2662
|
-
};
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
if (prop === '$stateMap') {
|
|
2666
|
-
return (callbackfn: any) =>
|
|
2667
|
-
createElement(SignalMapRenderer, {
|
|
2668
|
-
proxy: {
|
|
2669
|
-
_stateKey: stateKey,
|
|
2670
|
-
_path: path,
|
|
2671
|
-
_mapFn: callbackfn,
|
|
2672
|
-
_meta: meta,
|
|
2673
|
-
},
|
|
2674
|
-
rebuildStateShape,
|
|
2675
|
-
});
|
|
2676
|
-
} // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
|
|
2560
|
+
return shadowValue.map((_item, index) => {
|
|
2561
|
+
const itemKey = arrayKeys[index];
|
|
2562
|
+
if (!itemKey) return undefined;
|
|
2677
2563
|
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
): StateObject<any> | undefined => {
|
|
2682
|
-
// 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
|
|
2683
|
-
const arrayKeys =
|
|
2684
|
-
meta?.validIds ??
|
|
2685
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2686
|
-
?.arrayKeys;
|
|
2687
|
-
|
|
2688
|
-
if (!arrayKeys) {
|
|
2689
|
-
return undefined;
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
// 2. Iterate through the keys, get the value for each, and run the callback.
|
|
2693
|
-
for (let i = 0; i < arrayKeys.length; i++) {
|
|
2694
|
-
const itemKey = arrayKeys[i];
|
|
2695
|
-
if (!itemKey) continue; // Safety check
|
|
2696
|
-
|
|
2697
|
-
const itemValue = getGlobalStore
|
|
2698
|
-
.getState()
|
|
2699
|
-
.getShadowValue(itemKey);
|
|
2700
|
-
|
|
2701
|
-
// 3. If the callback returns true, we've found our item.
|
|
2702
|
-
if (callbackfn(itemValue, i)) {
|
|
2703
|
-
// Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
|
|
2704
|
-
const itemPath = itemKey.split('.').slice(1);
|
|
2564
|
+
// FIX: Construct the correct path to the item in the original store.
|
|
2565
|
+
// The path is the array's path plus the specific item's unique key.
|
|
2566
|
+
const itemPath = [...path, itemKey];
|
|
2705
2567
|
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
});
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2568
|
+
const itemSetter = rebuildStateShape({
|
|
2569
|
+
path: itemPath, // This now correctly points to the item in the shadow store.
|
|
2570
|
+
componentId: componentId!,
|
|
2571
|
+
meta,
|
|
2572
|
+
});
|
|
2714
2573
|
|
|
2715
|
-
|
|
2716
|
-
|
|
2574
|
+
return callbackfn(itemSetter, index, arraySetter);
|
|
2575
|
+
});
|
|
2717
2576
|
};
|
|
2718
2577
|
}
|
|
2578
|
+
|
|
2719
2579
|
if (prop === 'stateFilter') {
|
|
2720
2580
|
return (callbackfn: (value: any, index: number) => boolean) => {
|
|
2721
|
-
const
|
|
2722
|
-
.getState()
|
|
2723
|
-
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
2724
|
-
if (!Array.isArray(currentState)) return [];
|
|
2725
|
-
const arrayKeys =
|
|
2726
|
-
meta?.validIds ??
|
|
2727
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2728
|
-
?.arrayKeys;
|
|
2581
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2729
2582
|
|
|
2730
|
-
|
|
2731
|
-
|
|
2583
|
+
// ✅ FIX: Get keys from `getArrayData` which correctly resolves them from meta or the full list.
|
|
2584
|
+
const { keys: currentViewIds, value: array } = getArrayData(
|
|
2585
|
+
stateKey,
|
|
2586
|
+
path,
|
|
2587
|
+
meta
|
|
2588
|
+
);
|
|
2589
|
+
|
|
2590
|
+
if (!Array.isArray(array)) {
|
|
2591
|
+
throw new Error('stateFilter can only be used on arrays');
|
|
2732
2592
|
}
|
|
2733
2593
|
|
|
2734
|
-
|
|
2735
|
-
const
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2594
|
+
// Filter the array and collect the IDs of the items that pass
|
|
2595
|
+
const filteredIds: string[] = [];
|
|
2596
|
+
array.forEach((item, index) => {
|
|
2597
|
+
if (callbackfn(item, index)) {
|
|
2598
|
+
// currentViewIds[index] is the original ID before filtering
|
|
2599
|
+
const id = currentViewIds[index];
|
|
2600
|
+
if (id) {
|
|
2601
|
+
filteredIds.push(id);
|
|
2741
2602
|
}
|
|
2742
|
-
return false;
|
|
2743
2603
|
}
|
|
2744
|
-
);
|
|
2604
|
+
});
|
|
2745
2605
|
|
|
2606
|
+
// The rest is the same...
|
|
2746
2607
|
return rebuildStateShape({
|
|
2747
2608
|
path,
|
|
2748
2609
|
componentId: componentId!,
|
|
2749
2610
|
meta: {
|
|
2750
|
-
|
|
2611
|
+
...meta,
|
|
2612
|
+
arrayViews: {
|
|
2613
|
+
...(meta?.arrayViews || {}),
|
|
2614
|
+
[arrayPathKey]: filteredIds,
|
|
2615
|
+
},
|
|
2751
2616
|
transforms: [
|
|
2752
2617
|
...(meta?.transforms || []),
|
|
2753
|
-
{
|
|
2754
|
-
type: 'filter',
|
|
2755
|
-
fn: callbackfn,
|
|
2756
|
-
},
|
|
2618
|
+
{ type: 'filter', fn: callbackfn, path },
|
|
2757
2619
|
],
|
|
2758
2620
|
},
|
|
2759
2621
|
});
|
|
@@ -2761,34 +2623,39 @@ function createProxyHandler<T>(
|
|
|
2761
2623
|
}
|
|
2762
2624
|
if (prop === 'stateSort') {
|
|
2763
2625
|
return (compareFn: (a: any, b: any) => number) => {
|
|
2764
|
-
const
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2626
|
+
const arrayPathKey = path.join('.');
|
|
2627
|
+
|
|
2628
|
+
// FIX: Use the more robust `getArrayData` which always correctly resolves the keys for a view.
|
|
2629
|
+
const { value: currentArray, keys: currentViewIds } = getArrayData(
|
|
2630
|
+
stateKey,
|
|
2631
|
+
path,
|
|
2632
|
+
meta
|
|
2633
|
+
);
|
|
2634
|
+
|
|
2635
|
+
if (!Array.isArray(currentArray) || !currentViewIds) {
|
|
2773
2636
|
throw new Error('No array keys found for sorting');
|
|
2774
2637
|
}
|
|
2775
|
-
|
|
2638
|
+
|
|
2639
|
+
// ... (rest of the function is the same and now works)
|
|
2640
|
+
const itemsWithIds = currentArray.map((item, index) => ({
|
|
2776
2641
|
item,
|
|
2777
|
-
key:
|
|
2642
|
+
key: currentViewIds[index],
|
|
2778
2643
|
}));
|
|
2779
|
-
|
|
2780
|
-
itemsWithIds
|
|
2781
|
-
.sort((a, b) => compareFn(a.item, b.item))
|
|
2782
|
-
.filter(Boolean);
|
|
2644
|
+
itemsWithIds.sort((a, b) => compareFn(a.item, b.item));
|
|
2645
|
+
const sortedIds = itemsWithIds.map((i) => i.key as string);
|
|
2783
2646
|
|
|
2784
2647
|
return rebuildStateShape({
|
|
2785
2648
|
path,
|
|
2786
2649
|
componentId: componentId!,
|
|
2787
2650
|
meta: {
|
|
2788
|
-
|
|
2651
|
+
...meta,
|
|
2652
|
+
arrayViews: {
|
|
2653
|
+
...(meta?.arrayViews || {}),
|
|
2654
|
+
[arrayPathKey]: sortedIds,
|
|
2655
|
+
},
|
|
2789
2656
|
transforms: [
|
|
2790
2657
|
...(meta?.transforms || []),
|
|
2791
|
-
{ type: 'sort', fn: compareFn },
|
|
2658
|
+
{ type: 'sort', fn: compareFn, path },
|
|
2792
2659
|
],
|
|
2793
2660
|
},
|
|
2794
2661
|
});
|
|
@@ -2862,12 +2729,11 @@ function createProxyHandler<T>(
|
|
|
2862
2729
|
}
|
|
2863
2730
|
|
|
2864
2731
|
const streamId = uuidv4();
|
|
2865
|
-
const currentMeta =
|
|
2866
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path) || {};
|
|
2732
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
2867
2733
|
const streams = currentMeta.streams || new Map();
|
|
2868
2734
|
streams.set(streamId, { buffer, flushTimer });
|
|
2869
2735
|
|
|
2870
|
-
|
|
2736
|
+
setShadowMetadata(stateKey, path, {
|
|
2871
2737
|
...currentMeta,
|
|
2872
2738
|
streams,
|
|
2873
2739
|
});
|
|
@@ -2906,58 +2772,45 @@ function createProxyHandler<T>(
|
|
|
2906
2772
|
arraySetter: any
|
|
2907
2773
|
) => ReactNode
|
|
2908
2774
|
) => {
|
|
2775
|
+
console.log('meta outside', JSON.stringify(meta));
|
|
2909
2776
|
const StateListWrapper = () => {
|
|
2910
2777
|
const componentIdsRef = useRef<Map<string, string>>(new Map());
|
|
2911
2778
|
|
|
2912
|
-
const cacheKey =
|
|
2913
|
-
meta?.transforms && meta.transforms.length > 0
|
|
2914
|
-
? `${componentId}-${hashTransforms(meta.transforms)}`
|
|
2915
|
-
: `${componentId}-base`;
|
|
2916
|
-
|
|
2917
2779
|
const [updateTrigger, forceUpdate] = useState({});
|
|
2918
2780
|
|
|
2919
|
-
|
|
2920
|
-
const cached = getGlobalStore
|
|
2921
|
-
.getState()
|
|
2922
|
-
.getShadowMetadata(stateKey, path)
|
|
2923
|
-
?.transformCaches?.get(cacheKey);
|
|
2924
|
-
|
|
2925
|
-
let freshValidIds: string[];
|
|
2781
|
+
console.log('updateTrigger updateTrigger updateTrigger');
|
|
2926
2782
|
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
transforms: meta?.transforms || [],
|
|
2942
|
-
});
|
|
2943
|
-
}
|
|
2783
|
+
const validIds = applyTransforms(
|
|
2784
|
+
stateKey,
|
|
2785
|
+
path,
|
|
2786
|
+
meta?.transforms
|
|
2787
|
+
);
|
|
2788
|
+
//the above get the new coorect valid ids i need ot udpate the meta object with this info
|
|
2789
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2790
|
+
const updatedMeta = {
|
|
2791
|
+
...meta,
|
|
2792
|
+
arrayViews: {
|
|
2793
|
+
...(meta?.arrayViews || {}),
|
|
2794
|
+
[arrayPathKey]: validIds, // Update the arrayViews with the new valid IDs
|
|
2795
|
+
},
|
|
2796
|
+
};
|
|
2944
2797
|
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2798
|
+
// Now use the updated meta when getting array data
|
|
2799
|
+
const { value: arrayValues } = getArrayData(
|
|
2800
|
+
stateKey,
|
|
2801
|
+
path,
|
|
2802
|
+
updatedMeta
|
|
2803
|
+
);
|
|
2948
2804
|
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
arrayValues: freshValues || [],
|
|
2952
|
-
};
|
|
2953
|
-
}, [cacheKey, updateTrigger]);
|
|
2805
|
+
console.log('validIds', validIds);
|
|
2806
|
+
console.log('arrayValues', arrayValues);
|
|
2954
2807
|
|
|
2955
2808
|
useEffect(() => {
|
|
2956
2809
|
const unsubscribe = getGlobalStore
|
|
2957
2810
|
.getState()
|
|
2958
2811
|
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2959
2812
|
// A data change has occurred for the source array.
|
|
2960
|
-
|
|
2813
|
+
console.log('changed array statelist ', e);
|
|
2961
2814
|
if (e.type === 'GET_SELECTED') {
|
|
2962
2815
|
return;
|
|
2963
2816
|
}
|
|
@@ -2997,14 +2850,13 @@ function createProxyHandler<T>(
|
|
|
2997
2850
|
return null;
|
|
2998
2851
|
}
|
|
2999
2852
|
|
|
2853
|
+
// Continue using updatedMeta for the rest of your logic instead of meta
|
|
3000
2854
|
const arraySetter = rebuildStateShape({
|
|
3001
2855
|
path,
|
|
3002
2856
|
componentId: componentId!,
|
|
3003
|
-
meta:
|
|
3004
|
-
...meta,
|
|
3005
|
-
validIds: validIds,
|
|
3006
|
-
},
|
|
2857
|
+
meta: updatedMeta, // Use updated meta here
|
|
3007
2858
|
});
|
|
2859
|
+
console.log('arrayValues', arrayValues);
|
|
3008
2860
|
|
|
3009
2861
|
return (
|
|
3010
2862
|
<>
|
|
@@ -3021,7 +2873,7 @@ function createProxyHandler<T>(
|
|
|
3021
2873
|
componentIdsRef.current.set(itemKey, itemComponentId);
|
|
3022
2874
|
}
|
|
3023
2875
|
|
|
3024
|
-
const itemPath = itemKey
|
|
2876
|
+
const itemPath = [...path, itemKey];
|
|
3025
2877
|
|
|
3026
2878
|
return createElement(MemoizedCogsItemWrapper, {
|
|
3027
2879
|
key: itemKey,
|
|
@@ -3043,16 +2895,18 @@ function createProxyHandler<T>(
|
|
|
3043
2895
|
}
|
|
3044
2896
|
if (prop === 'stateFlattenOn') {
|
|
3045
2897
|
return (fieldName: string) => {
|
|
2898
|
+
// FIX: Get the definitive list of IDs for the current view from meta.arrayViews.
|
|
2899
|
+
const arrayPathKey = path.join('.');
|
|
2900
|
+
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2901
|
+
|
|
3046
2902
|
const currentState = getGlobalStore
|
|
3047
2903
|
.getState()
|
|
3048
|
-
.getShadowValue(
|
|
3049
|
-
|
|
3050
|
-
|
|
2904
|
+
.getShadowValue(stateKey, path, viewIds);
|
|
2905
|
+
|
|
2906
|
+
if (!Array.isArray(currentState)) return [];
|
|
3051
2907
|
|
|
3052
2908
|
stateVersion++;
|
|
3053
|
-
|
|
3054
|
-
(val: any) => val[fieldName] ?? []
|
|
3055
|
-
);
|
|
2909
|
+
|
|
3056
2910
|
return rebuildStateShape({
|
|
3057
2911
|
path: [...path, '[*]', fieldName],
|
|
3058
2912
|
componentId: componentId!,
|
|
@@ -3062,36 +2916,55 @@ function createProxyHandler<T>(
|
|
|
3062
2916
|
}
|
|
3063
2917
|
if (prop === 'index') {
|
|
3064
2918
|
return (index: number) => {
|
|
3065
|
-
const
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
2919
|
+
const arrayPathKey = path.join('.');
|
|
2920
|
+
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2921
|
+
|
|
2922
|
+
if (viewIds) {
|
|
2923
|
+
const itemId = viewIds[index];
|
|
2924
|
+
if (!itemId) return undefined;
|
|
2925
|
+
return rebuildStateShape({
|
|
2926
|
+
path: [...path, itemId],
|
|
2927
|
+
componentId: componentId!,
|
|
2928
|
+
meta,
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// ✅ FIX: Get the metadata and use the `arrayKeys` property.
|
|
2933
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
2934
|
+
if (!shadowMeta?.arrayKeys) return undefined;
|
|
2935
|
+
|
|
2936
|
+
const itemId = shadowMeta.arrayKeys[index];
|
|
3074
2937
|
if (!itemId) return undefined;
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
const state = rebuildStateShape({
|
|
3079
|
-
path: itemId.split('.').slice(1) as string[],
|
|
2938
|
+
|
|
2939
|
+
return rebuildStateShape({
|
|
2940
|
+
path: [...path, itemId],
|
|
3080
2941
|
componentId: componentId!,
|
|
3081
2942
|
meta,
|
|
3082
2943
|
});
|
|
3083
|
-
return state;
|
|
3084
2944
|
};
|
|
3085
2945
|
}
|
|
3086
2946
|
if (prop === 'last') {
|
|
3087
2947
|
return () => {
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
2948
|
+
// ✅ FIX: Use getArrayData to get the keys for the current view (filtered or not).
|
|
2949
|
+
const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
|
|
2950
|
+
|
|
2951
|
+
// If the array is empty, there is no last item.
|
|
2952
|
+
if (!currentViewIds || currentViewIds.length === 0) {
|
|
2953
|
+
return undefined;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// Get the unique ID of the last item in the current view.
|
|
2957
|
+
const lastItemKey = currentViewIds[currentViewIds.length - 1];
|
|
2958
|
+
|
|
2959
|
+
// If for some reason the key is invalid, return undefined.
|
|
2960
|
+
if (!lastItemKey) {
|
|
2961
|
+
return undefined;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// ✅ FIX: The new path uses the item's unique key, not its numerical index.
|
|
2965
|
+
const newPath = [...path, lastItemKey];
|
|
2966
|
+
|
|
2967
|
+
// Return a new proxy scoped to that specific item.
|
|
3095
2968
|
return rebuildStateShape({
|
|
3096
2969
|
path: newPath,
|
|
3097
2970
|
componentId: componentId!,
|
|
@@ -3105,11 +2978,6 @@ function createProxyHandler<T>(
|
|
|
3105
2978
|
index?: number
|
|
3106
2979
|
) => {
|
|
3107
2980
|
effectiveSetState(payload as any, path, { updateType: 'insert' });
|
|
3108
|
-
return rebuildStateShape({
|
|
3109
|
-
path,
|
|
3110
|
-
componentId: componentId!,
|
|
3111
|
-
meta,
|
|
3112
|
-
});
|
|
3113
2981
|
};
|
|
3114
2982
|
}
|
|
3115
2983
|
if (prop === 'uniqueInsert') {
|
|
@@ -3118,9 +2986,13 @@ function createProxyHandler<T>(
|
|
|
3118
2986
|
fields?: (keyof InferArrayElement<T>)[],
|
|
3119
2987
|
onMatch?: (existingItem: any) => any
|
|
3120
2988
|
) => {
|
|
3121
|
-
const currentArray =
|
|
3122
|
-
|
|
3123
|
-
|
|
2989
|
+
const { value: currentArray } = getScopedData(
|
|
2990
|
+
stateKey,
|
|
2991
|
+
path,
|
|
2992
|
+
meta
|
|
2993
|
+
) as {
|
|
2994
|
+
value: any[];
|
|
2995
|
+
};
|
|
3124
2996
|
const newValue = isFunction<T>(payload)
|
|
3125
2997
|
? payload(currentArray as any)
|
|
3126
2998
|
: (payload as any);
|
|
@@ -3150,167 +3022,133 @@ function createProxyHandler<T>(
|
|
|
3150
3022
|
}
|
|
3151
3023
|
};
|
|
3152
3024
|
}
|
|
3153
|
-
|
|
3154
3025
|
if (prop === 'cut') {
|
|
3155
3026
|
return (index?: number, options?: { waitForSync?: boolean }) => {
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
const validKeys =
|
|
3160
|
-
meta?.validIds ??
|
|
3161
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
3162
|
-
?.arrayKeys;
|
|
3163
|
-
|
|
3164
|
-
if (!validKeys || validKeys.length === 0) return;
|
|
3027
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
3028
|
+
if (!shadowMeta?.arrayKeys || shadowMeta.arrayKeys.length === 0)
|
|
3029
|
+
return;
|
|
3165
3030
|
|
|
3166
3031
|
const indexToCut =
|
|
3167
|
-
index
|
|
3168
|
-
?
|
|
3032
|
+
index === -1
|
|
3033
|
+
? shadowMeta.arrayKeys.length - 1
|
|
3169
3034
|
: index !== undefined
|
|
3170
3035
|
? index
|
|
3171
|
-
:
|
|
3036
|
+
: shadowMeta.arrayKeys.length - 1;
|
|
3172
3037
|
|
|
3173
|
-
const
|
|
3174
|
-
if (!
|
|
3038
|
+
const idToCut = shadowMeta.arrayKeys[indexToCut];
|
|
3039
|
+
if (!idToCut) return;
|
|
3175
3040
|
|
|
3176
|
-
|
|
3177
|
-
effectiveSetState(currentState, pathForCut, {
|
|
3041
|
+
effectiveSetState(null, [...path, idToCut], {
|
|
3178
3042
|
updateType: 'cut',
|
|
3179
3043
|
});
|
|
3180
3044
|
};
|
|
3181
3045
|
}
|
|
3182
3046
|
if (prop === 'cutSelected') {
|
|
3183
3047
|
return () => {
|
|
3184
|
-
const
|
|
3185
|
-
const currentState = getGlobalStore
|
|
3186
|
-
.getState()
|
|
3187
|
-
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
3188
|
-
if (!validKeys || validKeys.length === 0) return;
|
|
3048
|
+
const arrayKey = [stateKey, ...path].join('.');
|
|
3189
3049
|
|
|
3190
|
-
const
|
|
3050
|
+
const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
|
|
3051
|
+
if (!currentViewIds || currentViewIds.length === 0) {
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
const selectedItemKey = getGlobalStore
|
|
3191
3055
|
.getState()
|
|
3192
|
-
.selectedIndicesMap.get(
|
|
3056
|
+
.selectedIndicesMap.get(arrayKey);
|
|
3193
3057
|
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3058
|
+
if (!selectedItemKey) {
|
|
3059
|
+
return;
|
|
3060
|
+
}
|
|
3061
|
+
const selectedId = selectedItemKey.split('.').pop() as string;
|
|
3062
|
+
if (!(currentViewIds as any[]).includes(selectedId!)) {
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
const pathForCut = selectedItemKey.split('.').slice(1);
|
|
3066
|
+
getGlobalStore.getState().clearSelectedIndex({ arrayKey });
|
|
3197
3067
|
|
|
3198
|
-
const
|
|
3199
|
-
indexToCut == -1 ? validKeys.length - 1 : indexToCut
|
|
3200
|
-
]
|
|
3201
|
-
?.split('.')
|
|
3202
|
-
.slice(1);
|
|
3203
|
-
getGlobalStore
|
|
3204
|
-
.getState()
|
|
3205
|
-
.clearSelectedIndex({ arrayKey: stateKeyPathKey });
|
|
3206
|
-
const parentPath = pathForCut?.slice(0, -1)!;
|
|
3068
|
+
const parentPath = pathForCut.slice(0, -1);
|
|
3207
3069
|
notifySelectionComponents(stateKey, parentPath);
|
|
3208
|
-
|
|
3070
|
+
|
|
3071
|
+
effectiveSetState(null, pathForCut, {
|
|
3209
3072
|
updateType: 'cut',
|
|
3210
3073
|
});
|
|
3211
3074
|
};
|
|
3212
3075
|
}
|
|
3213
3076
|
if (prop === 'cutByValue') {
|
|
3214
3077
|
return (value: string | number | boolean) => {
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
if (!relevantKeys) return;
|
|
3222
|
-
|
|
3223
|
-
let keyToCut: string | null = null;
|
|
3078
|
+
const {
|
|
3079
|
+
isArray,
|
|
3080
|
+
value: array,
|
|
3081
|
+
keys,
|
|
3082
|
+
} = getArrayData(stateKey, path, meta);
|
|
3224
3083
|
|
|
3225
|
-
|
|
3226
|
-
for (const key of relevantKeys) {
|
|
3227
|
-
const itemValue = getGlobalStore.getState().getShadowValue(key);
|
|
3228
|
-
if (itemValue === value) {
|
|
3229
|
-
keyToCut = key;
|
|
3230
|
-
break; // We found the key, no need to search further.
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3084
|
+
if (!isArray) return;
|
|
3233
3085
|
|
|
3234
|
-
|
|
3235
|
-
if (
|
|
3236
|
-
|
|
3237
|
-
|
|
3086
|
+
const found = findArrayItem(array, keys, (item) => item === value);
|
|
3087
|
+
if (found) {
|
|
3088
|
+
effectiveSetState(null, [...path, found.key], {
|
|
3089
|
+
updateType: 'cut',
|
|
3090
|
+
});
|
|
3238
3091
|
}
|
|
3239
3092
|
};
|
|
3240
3093
|
}
|
|
3241
3094
|
|
|
3242
3095
|
if (prop === 'toggleByValue') {
|
|
3243
3096
|
return (value: string | number | boolean) => {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3097
|
+
const {
|
|
3098
|
+
isArray,
|
|
3099
|
+
value: array,
|
|
3100
|
+
keys,
|
|
3101
|
+
} = getArrayData(stateKey, path, meta);
|
|
3249
3102
|
|
|
3250
|
-
if (!
|
|
3103
|
+
if (!isArray) return;
|
|
3251
3104
|
|
|
3252
|
-
|
|
3105
|
+
const found = findArrayItem(array, keys, (item) => item === value);
|
|
3253
3106
|
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
if (itemValue === value) {
|
|
3259
|
-
keyToCut = key;
|
|
3260
|
-
break; // Found it!
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
console.log('itemValue keyToCut', keyToCut);
|
|
3264
|
-
// Step 3: Act based on whether the key was found.
|
|
3265
|
-
if (keyToCut) {
|
|
3266
|
-
// Item exists, so we CUT it using its *actual* key.
|
|
3267
|
-
const itemPath = keyToCut.split('.').slice(1);
|
|
3268
|
-
console.log('itemValue keyToCut', keyToCut);
|
|
3269
|
-
effectiveSetState(value as any, itemPath, {
|
|
3107
|
+
if (found) {
|
|
3108
|
+
const pathForItem = [...path, found.key];
|
|
3109
|
+
|
|
3110
|
+
effectiveSetState(null, pathForItem, {
|
|
3270
3111
|
updateType: 'cut',
|
|
3271
3112
|
});
|
|
3272
3113
|
} else {
|
|
3273
|
-
// Item does not exist, so we INSERT it.
|
|
3274
3114
|
effectiveSetState(value as any, path, { updateType: 'insert' });
|
|
3275
3115
|
}
|
|
3276
3116
|
};
|
|
3277
3117
|
}
|
|
3278
3118
|
if (prop === 'findWith') {
|
|
3279
|
-
return (searchKey:
|
|
3280
|
-
const
|
|
3281
|
-
.getState()
|
|
3282
|
-
.getShadowMetadata(stateKey, path)?.arrayKeys;
|
|
3119
|
+
return (searchKey: string, searchValue: any) => {
|
|
3120
|
+
const { isArray, value, keys } = getArrayData(stateKey, path, meta);
|
|
3283
3121
|
|
|
3284
|
-
if (!
|
|
3285
|
-
throw new Error('
|
|
3122
|
+
if (!isArray) {
|
|
3123
|
+
throw new Error('findWith can only be used on arrays');
|
|
3286
3124
|
}
|
|
3287
3125
|
|
|
3288
|
-
|
|
3289
|
-
|
|
3126
|
+
const found = findArrayItem(
|
|
3127
|
+
value,
|
|
3128
|
+
keys,
|
|
3129
|
+
(item) => item?.[searchKey] === searchValue
|
|
3130
|
+
);
|
|
3290
3131
|
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
.
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
break;
|
|
3299
|
-
}
|
|
3132
|
+
// FIX: If found, return a proxy to the item by appending its key to the current path.
|
|
3133
|
+
if (found) {
|
|
3134
|
+
return rebuildStateShape({
|
|
3135
|
+
path: [...path, found.key], // e.g., ['itemInstances', 'inst-1', 'properties', 'prop-b']
|
|
3136
|
+
componentId: componentId!,
|
|
3137
|
+
meta,
|
|
3138
|
+
});
|
|
3300
3139
|
}
|
|
3301
3140
|
|
|
3141
|
+
// If not found, return an 'empty' proxy that will resolve to undefined on .get()
|
|
3142
|
+
// This prevents "cannot read property 'get' of undefined" errors.
|
|
3302
3143
|
return rebuildStateShape({
|
|
3303
|
-
path:
|
|
3144
|
+
path: [...path, `not_found_${uuidv4()}`],
|
|
3304
3145
|
componentId: componentId!,
|
|
3305
3146
|
meta,
|
|
3306
3147
|
});
|
|
3307
3148
|
};
|
|
3308
3149
|
}
|
|
3309
|
-
|
|
3310
3150
|
if (prop === 'cutThis') {
|
|
3311
|
-
|
|
3312
|
-
.getState()
|
|
3313
|
-
.getShadowValue(path.join('.'));
|
|
3151
|
+
const { value: shadowValue } = getScopedData(stateKey, path, meta);
|
|
3314
3152
|
|
|
3315
3153
|
return () => {
|
|
3316
3154
|
effectiveSetState(shadowValue, path, { updateType: 'cut' });
|
|
@@ -3320,16 +3158,8 @@ function createProxyHandler<T>(
|
|
|
3320
3158
|
if (prop === 'get') {
|
|
3321
3159
|
return () => {
|
|
3322
3160
|
registerComponentDependency(stateKey, componentId, path);
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3326
|
-
};
|
|
3327
|
-
}
|
|
3328
|
-
if (prop === 'getState') {
|
|
3329
|
-
return () => {
|
|
3330
|
-
return getGlobalStore
|
|
3331
|
-
.getState()
|
|
3332
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3161
|
+
const { value } = getScopedData(stateKey, path, meta);
|
|
3162
|
+
return value;
|
|
3333
3163
|
};
|
|
3334
3164
|
}
|
|
3335
3165
|
|
|
@@ -3350,32 +3180,27 @@ function createProxyHandler<T>(
|
|
|
3350
3180
|
}
|
|
3351
3181
|
if (prop === 'lastSynced') {
|
|
3352
3182
|
const syncKey = `${stateKey}:${path.join('.')}`;
|
|
3353
|
-
return
|
|
3183
|
+
return getSyncInfo(syncKey);
|
|
3354
3184
|
}
|
|
3355
3185
|
if (prop == 'getLocalStorage') {
|
|
3356
3186
|
return (key: string) =>
|
|
3357
3187
|
loadFromLocalStorage(sessionId + '-' + stateKey + '-' + key);
|
|
3358
3188
|
}
|
|
3359
|
-
|
|
3360
3189
|
if (prop === 'isSelected') {
|
|
3361
|
-
const
|
|
3362
|
-
|
|
3363
|
-
if (
|
|
3364
|
-
Array.isArray(
|
|
3365
|
-
getGlobalStore
|
|
3366
|
-
.getState()
|
|
3367
|
-
.getShadowValue(parentPath.join('.'), meta?.validIds)
|
|
3368
|
-
)
|
|
3369
|
-
) {
|
|
3370
|
-
const itemId = path[path.length - 1];
|
|
3371
|
-
const fullParentKey = parentPath.join('.');
|
|
3190
|
+
const parentPathArray = path.slice(0, -1);
|
|
3191
|
+
const parentMeta = getShadowMetadata(stateKey, parentPathArray);
|
|
3372
3192
|
|
|
3193
|
+
// FIX: Check if the parent is an array by looking for arrayKeys in its metadata.
|
|
3194
|
+
if (parentMeta?.arrayKeys) {
|
|
3195
|
+
const fullParentKey = stateKey + '.' + parentPathArray.join('.');
|
|
3373
3196
|
const selectedItemKey = getGlobalStore
|
|
3374
3197
|
.getState()
|
|
3375
3198
|
.selectedIndicesMap.get(fullParentKey);
|
|
3376
3199
|
|
|
3377
3200
|
const fullItemKey = stateKey + '.' + path.join('.');
|
|
3378
3201
|
|
|
3202
|
+
// Logic remains the same.
|
|
3203
|
+
notifySelectionComponents(stateKey, parentPathArray, undefined);
|
|
3379
3204
|
return selectedItemKey === fullItemKey;
|
|
3380
3205
|
}
|
|
3381
3206
|
return undefined;
|
|
@@ -3429,11 +3254,6 @@ function createProxyHandler<T>(
|
|
|
3429
3254
|
if (path.length == 0) {
|
|
3430
3255
|
if (prop === 'addZodValidation') {
|
|
3431
3256
|
return (zodErrors: any[]) => {
|
|
3432
|
-
const init = getGlobalStore
|
|
3433
|
-
.getState()
|
|
3434
|
-
.getInitialOptions(stateKey)?.validation;
|
|
3435
|
-
|
|
3436
|
-
// For each error, set shadow metadata
|
|
3437
3257
|
zodErrors.forEach((error) => {
|
|
3438
3258
|
const currentMeta =
|
|
3439
3259
|
getGlobalStore
|
|
@@ -3445,42 +3265,38 @@ function createProxyHandler<T>(
|
|
|
3445
3265
|
.setShadowMetadata(stateKey, error.path, {
|
|
3446
3266
|
...currentMeta,
|
|
3447
3267
|
validation: {
|
|
3448
|
-
status: '
|
|
3449
|
-
|
|
3268
|
+
status: 'INVALID',
|
|
3269
|
+
errors: [
|
|
3270
|
+
{
|
|
3271
|
+
source: 'client',
|
|
3272
|
+
message: error.message,
|
|
3273
|
+
severity: 'error',
|
|
3274
|
+
code: error.code,
|
|
3275
|
+
},
|
|
3276
|
+
],
|
|
3277
|
+
lastValidated: Date.now(),
|
|
3450
3278
|
validatedValue: undefined,
|
|
3451
3279
|
},
|
|
3452
3280
|
});
|
|
3453
|
-
getGlobalStore.getState().notifyPathSubscribers(error.path, {
|
|
3454
|
-
type: 'VALIDATION_FAILED',
|
|
3455
|
-
message: error.message,
|
|
3456
|
-
validatedValue: undefined,
|
|
3457
|
-
});
|
|
3458
3281
|
});
|
|
3459
3282
|
};
|
|
3460
3283
|
}
|
|
3461
3284
|
if (prop === 'clearZodValidation') {
|
|
3462
3285
|
return (path?: string[]) => {
|
|
3463
|
-
// Clear specific paths
|
|
3464
3286
|
if (!path) {
|
|
3465
3287
|
throw new Error('clearZodValidation requires a path');
|
|
3466
|
-
return;
|
|
3467
3288
|
}
|
|
3468
|
-
const currentMeta =
|
|
3469
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
3470
|
-
{};
|
|
3471
|
-
|
|
3472
|
-
if (currentMeta.validation) {
|
|
3473
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
3474
|
-
...currentMeta,
|
|
3475
|
-
validation: undefined,
|
|
3476
|
-
});
|
|
3477
3289
|
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3290
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
3291
|
+
|
|
3292
|
+
setShadowMetadata(stateKey, path, {
|
|
3293
|
+
...currentMeta,
|
|
3294
|
+
validation: {
|
|
3295
|
+
status: 'NOT_VALIDATED',
|
|
3296
|
+
errors: [],
|
|
3297
|
+
lastValidated: Date.now(),
|
|
3298
|
+
},
|
|
3299
|
+
});
|
|
3484
3300
|
};
|
|
3485
3301
|
}
|
|
3486
3302
|
if (prop === 'applyJsonPatch') {
|
|
@@ -3511,6 +3327,7 @@ function createProxyHandler<T>(
|
|
|
3511
3327
|
value: any;
|
|
3512
3328
|
};
|
|
3513
3329
|
store.updateShadowAtPath(stateKey, relativePath, value);
|
|
3330
|
+
|
|
3514
3331
|
store.markAsDirty(stateKey, relativePath, { bubble: true });
|
|
3515
3332
|
|
|
3516
3333
|
// Bubble up - notify components at this path and all parent paths
|
|
@@ -3575,9 +3392,7 @@ function createProxyHandler<T>(
|
|
|
3575
3392
|
}
|
|
3576
3393
|
|
|
3577
3394
|
if (prop === 'getComponents')
|
|
3578
|
-
return () =>
|
|
3579
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, [])
|
|
3580
|
-
?.components;
|
|
3395
|
+
return () => getShadowMetadata(stateKey, [])?.components;
|
|
3581
3396
|
if (prop === 'getAllFormRefs')
|
|
3582
3397
|
return () =>
|
|
3583
3398
|
formRefStore.getState().getFormRefsByStateKey(stateKey);
|
|
@@ -3608,88 +3423,34 @@ function createProxyHandler<T>(
|
|
|
3608
3423
|
if (prop === '_stateKey') return stateKey;
|
|
3609
3424
|
if (prop === '_path') return path;
|
|
3610
3425
|
if (prop === 'update') {
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3426
|
+
// This method is now greatly simplified.
|
|
3427
|
+
// All the complex batching logic has been removed because our new,
|
|
3428
|
+
// universal `createEffectiveSetState` function handles it automatically for all operations.
|
|
3616
3429
|
return (payload: UpdateArg<T>) => {
|
|
3617
|
-
//
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
const inReactEvent =
|
|
3621
|
-
stack.includes('onClick') ||
|
|
3622
|
-
stack.includes('dispatchEvent') ||
|
|
3623
|
-
stack.includes('batchedUpdates');
|
|
3624
|
-
|
|
3625
|
-
// Only batch if we're in a React event
|
|
3626
|
-
if (inReactEvent) {
|
|
3627
|
-
const batchKey = `${stateKey}.${path.join('.')}`;
|
|
3628
|
-
|
|
3629
|
-
// Schedule flush if not already scheduled
|
|
3630
|
-
if (!batchFlushScheduled) {
|
|
3631
|
-
updateBatchQueue.clear();
|
|
3632
|
-
batchFlushScheduled = true;
|
|
3633
|
-
|
|
3634
|
-
queueMicrotask(() => {
|
|
3635
|
-
// Process all batched updates
|
|
3636
|
-
for (const [key, updates] of updateBatchQueue) {
|
|
3637
|
-
const parts = key.split('.');
|
|
3638
|
-
const batchStateKey = parts[0];
|
|
3639
|
-
const batchPath = parts.slice(1);
|
|
3640
|
-
|
|
3641
|
-
// Compose all updates for this path
|
|
3642
|
-
const composedUpdate = updates.reduce(
|
|
3643
|
-
(composed, update) => {
|
|
3644
|
-
if (
|
|
3645
|
-
typeof update === 'function' &&
|
|
3646
|
-
typeof composed === 'function'
|
|
3647
|
-
) {
|
|
3648
|
-
// Compose functions
|
|
3649
|
-
return (state: any) => update(composed(state));
|
|
3650
|
-
}
|
|
3651
|
-
// If not functions, last one wins
|
|
3652
|
-
return update;
|
|
3653
|
-
}
|
|
3654
|
-
);
|
|
3655
|
-
|
|
3656
|
-
// Call effectiveSetState ONCE with composed update
|
|
3657
|
-
effectiveSetState(composedUpdate as any, batchPath, {
|
|
3658
|
-
updateType: 'update',
|
|
3659
|
-
});
|
|
3660
|
-
}
|
|
3661
|
-
|
|
3662
|
-
updateBatchQueue.clear();
|
|
3663
|
-
batchFlushScheduled = false;
|
|
3664
|
-
});
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
// Add to batch
|
|
3668
|
-
const existing = updateBatchQueue.get(batchKey) || [];
|
|
3669
|
-
existing.push(payload);
|
|
3670
|
-
updateBatchQueue.set(batchKey, existing);
|
|
3671
|
-
} else {
|
|
3672
|
-
// NOT in React event - execute immediately
|
|
3673
|
-
console.time('update inner');
|
|
3674
|
-
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3675
|
-
console.timeEnd('update inner');
|
|
3676
|
-
}
|
|
3430
|
+
// Simply call effectiveSetState. It will automatically handle queuing
|
|
3431
|
+
// this operation in the batch for efficient processing.
|
|
3432
|
+
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3677
3433
|
|
|
3434
|
+
// The .synced() method is a useful feature that allows developers
|
|
3435
|
+
// to manually mark a piece of state as "synced with the server"
|
|
3436
|
+
// after an update. This part of the functionality is preserved.
|
|
3678
3437
|
return {
|
|
3679
3438
|
synced: () => {
|
|
3680
3439
|
const shadowMeta = getGlobalStore
|
|
3681
3440
|
.getState()
|
|
3682
3441
|
.getShadowMetadata(stateKey, path);
|
|
3683
3442
|
|
|
3684
|
-
|
|
3443
|
+
// Update the metadata for this specific path
|
|
3444
|
+
setShadowMetadata(stateKey, path, {
|
|
3685
3445
|
...shadowMeta,
|
|
3686
3446
|
isDirty: false,
|
|
3687
3447
|
stateSource: 'server',
|
|
3688
3448
|
lastServerSync: Date.now(),
|
|
3689
3449
|
});
|
|
3690
3450
|
|
|
3451
|
+
// Notify any components that might be subscribed to the sync status
|
|
3691
3452
|
const fullPath = [stateKey, ...path].join('.');
|
|
3692
|
-
|
|
3453
|
+
notifyPathSubscribers(fullPath, {
|
|
3693
3454
|
type: 'SYNC_STATUS_CHANGE',
|
|
3694
3455
|
isDirty: false,
|
|
3695
3456
|
});
|
|
@@ -3697,11 +3458,12 @@ function createProxyHandler<T>(
|
|
|
3697
3458
|
};
|
|
3698
3459
|
};
|
|
3699
3460
|
}
|
|
3700
|
-
|
|
3701
3461
|
if (prop === 'toggle') {
|
|
3702
|
-
const currentValueAtPath =
|
|
3703
|
-
|
|
3704
|
-
|
|
3462
|
+
const { value: currentValueAtPath } = getScopedData(
|
|
3463
|
+
stateKey,
|
|
3464
|
+
path,
|
|
3465
|
+
meta
|
|
3466
|
+
);
|
|
3705
3467
|
|
|
3706
3468
|
if (typeof currentValueAtPath != 'boolean') {
|
|
3707
3469
|
throw new Error('toggle() can only be used on boolean values');
|
|
@@ -3738,19 +3500,14 @@ function createProxyHandler<T>(
|
|
|
3738
3500
|
},
|
|
3739
3501
|
};
|
|
3740
3502
|
|
|
3741
|
-
const proxyInstance = new Proxy(
|
|
3503
|
+
const proxyInstance = new Proxy({}, handler);
|
|
3742
3504
|
proxyCache.set(cacheKey, proxyInstance);
|
|
3743
|
-
|
|
3505
|
+
|
|
3744
3506
|
return proxyInstance;
|
|
3745
3507
|
}
|
|
3746
|
-
console.timeEnd('rebuildStateShape Outer');
|
|
3747
3508
|
|
|
3748
|
-
const
|
|
3509
|
+
const rootLevelMethods = {
|
|
3749
3510
|
revertToInitialState: (obj?: { validationKey?: string }) => {
|
|
3750
|
-
const init = getGlobalStore
|
|
3751
|
-
.getState()
|
|
3752
|
-
.getInitialOptions(stateKey)?.validation;
|
|
3753
|
-
|
|
3754
3511
|
const shadowMeta = getGlobalStore
|
|
3755
3512
|
.getState()
|
|
3756
3513
|
.getShadowMetadata(stateKey, []);
|
|
@@ -3766,10 +3523,10 @@ function createProxyHandler<T>(
|
|
|
3766
3523
|
const initialState =
|
|
3767
3524
|
getGlobalStore.getState().initialStateGlobal[stateKey];
|
|
3768
3525
|
|
|
3769
|
-
|
|
3526
|
+
clearSelectedIndexesForState(stateKey);
|
|
3770
3527
|
|
|
3771
3528
|
stateVersion++;
|
|
3772
|
-
|
|
3529
|
+
initializeShadowState(stateKey, initialState);
|
|
3773
3530
|
rebuildStateShape({
|
|
3774
3531
|
path: [],
|
|
3775
3532
|
componentId: componentId!,
|
|
@@ -3819,7 +3576,8 @@ function createProxyHandler<T>(
|
|
|
3819
3576
|
}
|
|
3820
3577
|
startTransition(() => {
|
|
3821
3578
|
updateInitialStateGlobal(stateKey, newState);
|
|
3822
|
-
|
|
3579
|
+
initializeShadowState(stateKey, newState);
|
|
3580
|
+
// initializeShadowStateNEW(stateKey, newState);
|
|
3823
3581
|
|
|
3824
3582
|
const stateEntry = getGlobalStore
|
|
3825
3583
|
.getState()
|
|
@@ -3837,6 +3595,7 @@ function createProxyHandler<T>(
|
|
|
3837
3595
|
};
|
|
3838
3596
|
},
|
|
3839
3597
|
};
|
|
3598
|
+
|
|
3840
3599
|
const returnShape = rebuildStateShape({
|
|
3841
3600
|
componentId,
|
|
3842
3601
|
path: [],
|
|
@@ -3855,153 +3614,6 @@ export function $cogsSignal(proxy: {
|
|
|
3855
3614
|
return createElement(SignalRenderer, { proxy });
|
|
3856
3615
|
}
|
|
3857
3616
|
|
|
3858
|
-
function SignalMapRenderer({
|
|
3859
|
-
proxy,
|
|
3860
|
-
rebuildStateShape,
|
|
3861
|
-
}: {
|
|
3862
|
-
proxy: {
|
|
3863
|
-
_stateKey: string;
|
|
3864
|
-
_path: string[];
|
|
3865
|
-
_meta?: MetaData;
|
|
3866
|
-
_mapFn: (
|
|
3867
|
-
setter: any,
|
|
3868
|
-
index: number,
|
|
3869
|
-
|
|
3870
|
-
arraySetter: any
|
|
3871
|
-
) => ReactNode;
|
|
3872
|
-
};
|
|
3873
|
-
rebuildStateShape: (stuff: {
|
|
3874
|
-
currentState: any;
|
|
3875
|
-
path: string[];
|
|
3876
|
-
componentId: string;
|
|
3877
|
-
meta?: MetaData;
|
|
3878
|
-
}) => any;
|
|
3879
|
-
}): JSX.Element | null {
|
|
3880
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
3881
|
-
const instanceIdRef = useRef<string>(`map-${crypto.randomUUID()}`);
|
|
3882
|
-
const isSetupRef = useRef(false);
|
|
3883
|
-
const rootsMapRef = useRef<Map<string, any>>(new Map());
|
|
3884
|
-
|
|
3885
|
-
// Setup effect - store the map function in shadow metadata
|
|
3886
|
-
useEffect(() => {
|
|
3887
|
-
const container = containerRef.current;
|
|
3888
|
-
if (!container || isSetupRef.current) return;
|
|
3889
|
-
|
|
3890
|
-
const timeoutId = setTimeout(() => {
|
|
3891
|
-
// Store map wrapper in metadata
|
|
3892
|
-
const currentMeta =
|
|
3893
|
-
getGlobalStore
|
|
3894
|
-
.getState()
|
|
3895
|
-
.getShadowMetadata(proxy._stateKey, proxy._path) || {};
|
|
3896
|
-
|
|
3897
|
-
const mapWrappers = currentMeta.mapWrappers || [];
|
|
3898
|
-
mapWrappers.push({
|
|
3899
|
-
instanceId: instanceIdRef.current,
|
|
3900
|
-
mapFn: proxy._mapFn,
|
|
3901
|
-
containerRef: container,
|
|
3902
|
-
rebuildStateShape: rebuildStateShape,
|
|
3903
|
-
path: proxy._path,
|
|
3904
|
-
componentId: instanceIdRef.current,
|
|
3905
|
-
meta: proxy._meta,
|
|
3906
|
-
});
|
|
3907
|
-
|
|
3908
|
-
getGlobalStore
|
|
3909
|
-
.getState()
|
|
3910
|
-
.setShadowMetadata(proxy._stateKey, proxy._path, {
|
|
3911
|
-
...currentMeta,
|
|
3912
|
-
mapWrappers,
|
|
3913
|
-
});
|
|
3914
|
-
|
|
3915
|
-
isSetupRef.current = true;
|
|
3916
|
-
|
|
3917
|
-
// Initial render
|
|
3918
|
-
renderInitialItems();
|
|
3919
|
-
}, 0);
|
|
3920
|
-
|
|
3921
|
-
// Cleanup
|
|
3922
|
-
return () => {
|
|
3923
|
-
clearTimeout(timeoutId);
|
|
3924
|
-
if (instanceIdRef.current) {
|
|
3925
|
-
const currentMeta =
|
|
3926
|
-
getGlobalStore
|
|
3927
|
-
.getState()
|
|
3928
|
-
.getShadowMetadata(proxy._stateKey, proxy._path) || {};
|
|
3929
|
-
if (currentMeta.mapWrappers) {
|
|
3930
|
-
currentMeta.mapWrappers = currentMeta.mapWrappers.filter(
|
|
3931
|
-
(w) => w.instanceId !== instanceIdRef.current
|
|
3932
|
-
);
|
|
3933
|
-
getGlobalStore
|
|
3934
|
-
.getState()
|
|
3935
|
-
.setShadowMetadata(proxy._stateKey, proxy._path, currentMeta);
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
3938
|
-
rootsMapRef.current.forEach((root) => root.unmount());
|
|
3939
|
-
};
|
|
3940
|
-
}, []);
|
|
3941
|
-
|
|
3942
|
-
const renderInitialItems = () => {
|
|
3943
|
-
const container = containerRef.current;
|
|
3944
|
-
if (!container) return;
|
|
3945
|
-
|
|
3946
|
-
const value = getGlobalStore
|
|
3947
|
-
.getState()
|
|
3948
|
-
.getShadowValue(
|
|
3949
|
-
[proxy._stateKey, ...proxy._path].join('.'),
|
|
3950
|
-
proxy._meta?.validIds
|
|
3951
|
-
) as any[];
|
|
3952
|
-
|
|
3953
|
-
if (!Array.isArray(value)) return;
|
|
3954
|
-
|
|
3955
|
-
// --- BUG FIX IS HERE ---
|
|
3956
|
-
// Prioritize the filtered IDs from the meta object, just like the regular `stateMap`.
|
|
3957
|
-
// This ensures the keys match the filtered data.
|
|
3958
|
-
const arrayKeys =
|
|
3959
|
-
proxy._meta?.validIds ??
|
|
3960
|
-
getGlobalStore.getState().getShadowMetadata(proxy._stateKey, proxy._path)
|
|
3961
|
-
?.arrayKeys ??
|
|
3962
|
-
[];
|
|
3963
|
-
// --- END OF FIX ---
|
|
3964
|
-
|
|
3965
|
-
const arraySetter = rebuildStateShape({
|
|
3966
|
-
currentState: value,
|
|
3967
|
-
path: proxy._path,
|
|
3968
|
-
componentId: instanceIdRef.current,
|
|
3969
|
-
meta: proxy._meta,
|
|
3970
|
-
});
|
|
3971
|
-
|
|
3972
|
-
value.forEach((item, index) => {
|
|
3973
|
-
const itemKey = arrayKeys[index]!; // Now this will be the correct key for the filtered item
|
|
3974
|
-
if (!itemKey) return; // Safeguard if there's a mismatch
|
|
3975
|
-
|
|
3976
|
-
const itemComponentId = uuidv4();
|
|
3977
|
-
const itemElement = document.createElement('div');
|
|
3978
|
-
|
|
3979
|
-
itemElement.setAttribute('data-item-path', itemKey);
|
|
3980
|
-
container.appendChild(itemElement);
|
|
3981
|
-
|
|
3982
|
-
const root = createRoot(itemElement);
|
|
3983
|
-
rootsMapRef.current.set(itemKey, root);
|
|
3984
|
-
|
|
3985
|
-
const itemPath = itemKey.split('.').slice(1) as string[];
|
|
3986
|
-
|
|
3987
|
-
// Render CogsItemWrapper instead of direct render
|
|
3988
|
-
root.render(
|
|
3989
|
-
createElement(MemoizedCogsItemWrapper, {
|
|
3990
|
-
stateKey: proxy._stateKey,
|
|
3991
|
-
itemComponentId: itemComponentId,
|
|
3992
|
-
itemPath: itemPath,
|
|
3993
|
-
localIndex: index,
|
|
3994
|
-
arraySetter: arraySetter,
|
|
3995
|
-
rebuildStateShape: rebuildStateShape,
|
|
3996
|
-
renderFn: proxy._mapFn,
|
|
3997
|
-
})
|
|
3998
|
-
);
|
|
3999
|
-
});
|
|
4000
|
-
};
|
|
4001
|
-
|
|
4002
|
-
return <div ref={containerRef} data-map-container={instanceIdRef.current} />;
|
|
4003
|
-
}
|
|
4004
|
-
|
|
4005
3617
|
function SignalRenderer({
|
|
4006
3618
|
proxy,
|
|
4007
3619
|
}: {
|
|
@@ -4016,12 +3628,10 @@ function SignalRenderer({
|
|
|
4016
3628
|
const instanceIdRef = useRef<string | null>(null);
|
|
4017
3629
|
const isSetupRef = useRef(false);
|
|
4018
3630
|
const signalId = `${proxy._stateKey}-${proxy._path.join('.')}`;
|
|
4019
|
-
const
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
proxy._meta?.validIds
|
|
4024
|
-
);
|
|
3631
|
+
const arrayPathKey = proxy._path.join('.');
|
|
3632
|
+
const viewIds = proxy._meta?.arrayViews?.[arrayPathKey];
|
|
3633
|
+
|
|
3634
|
+
const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
|
|
4025
3635
|
|
|
4026
3636
|
// Setup effect - runs only once
|
|
4027
3637
|
useEffect(() => {
|
|
@@ -4216,7 +3826,7 @@ function ListItemWrapper({
|
|
|
4216
3826
|
);
|
|
4217
3827
|
|
|
4218
3828
|
useEffect(() => {
|
|
4219
|
-
|
|
3829
|
+
subscribeToPath(fullKey, (e) => {
|
|
4220
3830
|
forceUpdate({});
|
|
4221
3831
|
});
|
|
4222
3832
|
}, []);
|
|
@@ -4230,7 +3840,7 @@ function ListItemWrapper({
|
|
|
4230
3840
|
hasReportedInitialHeight.current = true;
|
|
4231
3841
|
const newHeight = element.offsetHeight;
|
|
4232
3842
|
|
|
4233
|
-
|
|
3843
|
+
setShadowMetadata(stateKey, itemPath, {
|
|
4234
3844
|
virtualizer: {
|
|
4235
3845
|
itemHeight: newHeight,
|
|
4236
3846
|
domRef: element,
|
|
@@ -4239,7 +3849,7 @@ function ListItemWrapper({
|
|
|
4239
3849
|
|
|
4240
3850
|
const arrayPath = itemPath.slice(0, -1);
|
|
4241
3851
|
const arrayPathKey = [stateKey, ...arrayPath].join('.');
|
|
4242
|
-
|
|
3852
|
+
notifyPathSubscribers(arrayPathKey, {
|
|
4243
3853
|
type: 'ITEMHEIGHT',
|
|
4244
3854
|
itemKey: itemPath.join('.'),
|
|
4245
3855
|
|
|
@@ -4248,8 +3858,7 @@ function ListItemWrapper({
|
|
|
4248
3858
|
}
|
|
4249
3859
|
}, [inView, imagesLoaded, stateKey, itemPath]);
|
|
4250
3860
|
|
|
4251
|
-
const
|
|
4252
|
-
const itemValue = getGlobalStore.getState().getShadowValue(fullItemPath);
|
|
3861
|
+
const itemValue = getShadowValue(stateKey, itemPath);
|
|
4253
3862
|
|
|
4254
3863
|
if (itemValue === undefined) {
|
|
4255
3864
|
return null;
|
|
@@ -4276,7 +3885,6 @@ function FormElementWrapper({
|
|
|
4276
3885
|
stateKey: string;
|
|
4277
3886
|
path: string[];
|
|
4278
3887
|
rebuildStateShape: (options: {
|
|
4279
|
-
currentState: any;
|
|
4280
3888
|
path: string[];
|
|
4281
3889
|
componentId: string;
|
|
4282
3890
|
meta?: any;
|
|
@@ -4290,9 +3898,7 @@ function FormElementWrapper({
|
|
|
4290
3898
|
|
|
4291
3899
|
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
4292
3900
|
useRegisterComponent(stateKey, componentId, forceUpdate);
|
|
4293
|
-
const globalStateValue =
|
|
4294
|
-
.getState()
|
|
4295
|
-
.getShadowValue(stateKeyPathKey);
|
|
3901
|
+
const globalStateValue = getShadowValue(stateKey, path);
|
|
4296
3902
|
const [localValue, setLocalValue] = useState<any>(globalStateValue);
|
|
4297
3903
|
const isCurrentlyDebouncing = useRef(false);
|
|
4298
3904
|
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
@@ -4340,21 +3946,21 @@ function FormElementWrapper({
|
|
|
4340
3946
|
|
|
4341
3947
|
debounceTimeoutRef.current = setTimeout(() => {
|
|
4342
3948
|
isCurrentlyDebouncing.current = false;
|
|
4343
|
-
|
|
4344
|
-
// Update state
|
|
4345
3949
|
setState(newValue, path, { updateType: 'update' });
|
|
4346
3950
|
|
|
4347
|
-
//
|
|
4348
|
-
const
|
|
4349
|
-
|
|
3951
|
+
// NEW: Check if validation is enabled via features
|
|
3952
|
+
const rootMeta = getGlobalStore
|
|
3953
|
+
.getState()
|
|
3954
|
+
.getShadowMetadata(stateKey, []);
|
|
3955
|
+
if (!rootMeta?.features?.validationEnabled) return;
|
|
3956
|
+
|
|
4350
3957
|
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4351
3958
|
const zodSchema =
|
|
4352
3959
|
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4353
3960
|
|
|
4354
3961
|
if (zodSchema) {
|
|
4355
|
-
const fullState =
|
|
3962
|
+
const fullState = getShadowValue(stateKey, []);
|
|
4356
3963
|
const result = zodSchema.safeParse(fullState);
|
|
4357
|
-
|
|
4358
3964
|
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
4359
3965
|
|
|
4360
3966
|
if (!result.success) {
|
|
@@ -4362,6 +3968,7 @@ function FormElementWrapper({
|
|
|
4362
3968
|
'issues' in result.error
|
|
4363
3969
|
? result.error.issues
|
|
4364
3970
|
: (result.error as any).errors;
|
|
3971
|
+
|
|
4365
3972
|
const pathErrors = errors.filter(
|
|
4366
3973
|
(error: any) =>
|
|
4367
3974
|
JSON.stringify(error.path) === JSON.stringify(path)
|
|
@@ -4371,30 +3978,37 @@ function FormElementWrapper({
|
|
|
4371
3978
|
setShadowMetadata(stateKey, path, {
|
|
4372
3979
|
...currentMeta,
|
|
4373
3980
|
validation: {
|
|
4374
|
-
status: '
|
|
4375
|
-
|
|
3981
|
+
status: 'INVALID',
|
|
3982
|
+
errors: [
|
|
3983
|
+
{
|
|
3984
|
+
source: 'client',
|
|
3985
|
+
message: pathErrors[0]?.message,
|
|
3986
|
+
severity: 'warning', // Gentle error during typing
|
|
3987
|
+
},
|
|
3988
|
+
],
|
|
3989
|
+
lastValidated: Date.now(),
|
|
4376
3990
|
validatedValue: newValue,
|
|
4377
3991
|
},
|
|
4378
3992
|
});
|
|
4379
3993
|
} else {
|
|
4380
|
-
// This field has no errors - clear validation
|
|
4381
3994
|
setShadowMetadata(stateKey, path, {
|
|
4382
3995
|
...currentMeta,
|
|
4383
3996
|
validation: {
|
|
4384
|
-
status: '
|
|
3997
|
+
status: 'VALID',
|
|
3998
|
+
errors: [],
|
|
3999
|
+
lastValidated: Date.now(),
|
|
4385
4000
|
validatedValue: newValue,
|
|
4386
|
-
message: undefined,
|
|
4387
4001
|
},
|
|
4388
4002
|
});
|
|
4389
4003
|
}
|
|
4390
4004
|
} else {
|
|
4391
|
-
// Validation passed - clear any existing errors
|
|
4392
4005
|
setShadowMetadata(stateKey, path, {
|
|
4393
4006
|
...currentMeta,
|
|
4394
4007
|
validation: {
|
|
4395
|
-
status: '
|
|
4008
|
+
status: 'VALID',
|
|
4009
|
+
errors: [],
|
|
4010
|
+
lastValidated: Date.now(),
|
|
4396
4011
|
validatedValue: newValue,
|
|
4397
|
-
message: undefined,
|
|
4398
4012
|
},
|
|
4399
4013
|
});
|
|
4400
4014
|
}
|
|
@@ -4417,7 +4031,8 @@ function FormElementWrapper({
|
|
|
4417
4031
|
isCurrentlyDebouncing.current = false;
|
|
4418
4032
|
setState(localValue, path, { updateType: 'update' });
|
|
4419
4033
|
}
|
|
4420
|
-
|
|
4034
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
4035
|
+
if (!rootMeta?.features?.validationEnabled) return;
|
|
4421
4036
|
const { getInitialOptions } = getGlobalStore.getState();
|
|
4422
4037
|
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4423
4038
|
const zodSchema =
|
|
@@ -4428,19 +4043,20 @@ function FormElementWrapper({
|
|
|
4428
4043
|
// Get the full path including stateKey
|
|
4429
4044
|
|
|
4430
4045
|
// Update validation state to "validating"
|
|
4431
|
-
const currentMeta =
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4046
|
+
const currentMeta = getShadowMetadata(stateKey, path);
|
|
4047
|
+
|
|
4048
|
+
setShadowMetadata(stateKey, path, {
|
|
4435
4049
|
...currentMeta,
|
|
4436
4050
|
validation: {
|
|
4437
|
-
status: '
|
|
4051
|
+
status: 'VALIDATING',
|
|
4052
|
+
errors: [],
|
|
4053
|
+
lastValidated: Date.now(),
|
|
4438
4054
|
validatedValue: localValue,
|
|
4439
4055
|
},
|
|
4440
4056
|
});
|
|
4441
4057
|
|
|
4442
4058
|
// Validate full state
|
|
4443
|
-
const fullState =
|
|
4059
|
+
const fullState = getShadowValue(stateKey, []);
|
|
4444
4060
|
const result = zodSchema.safeParse(fullState);
|
|
4445
4061
|
console.log('result ', result);
|
|
4446
4062
|
if (!result.success) {
|
|
@@ -4504,20 +4120,27 @@ function FormElementWrapper({
|
|
|
4504
4120
|
|
|
4505
4121
|
console.log('Filtered path errors:', pathErrors);
|
|
4506
4122
|
// Update shadow metadata with validation result
|
|
4507
|
-
|
|
4123
|
+
setShadowMetadata(stateKey, path, {
|
|
4508
4124
|
...currentMeta,
|
|
4509
4125
|
validation: {
|
|
4510
|
-
status: '
|
|
4511
|
-
|
|
4126
|
+
status: 'INVALID',
|
|
4127
|
+
errors: pathErrors.map((err: any) => ({
|
|
4128
|
+
source: 'client' as const,
|
|
4129
|
+
message: err.message,
|
|
4130
|
+
severity: 'error' as const, // Hard error on blur
|
|
4131
|
+
})),
|
|
4132
|
+
lastValidated: Date.now(),
|
|
4512
4133
|
validatedValue: localValue,
|
|
4513
4134
|
},
|
|
4514
4135
|
});
|
|
4515
4136
|
} else {
|
|
4516
4137
|
// Validation passed
|
|
4517
|
-
|
|
4138
|
+
setShadowMetadata(stateKey, path, {
|
|
4518
4139
|
...currentMeta,
|
|
4519
4140
|
validation: {
|
|
4520
|
-
status: '
|
|
4141
|
+
status: 'VALID',
|
|
4142
|
+
errors: [],
|
|
4143
|
+
lastValidated: Date.now(),
|
|
4521
4144
|
validatedValue: localValue,
|
|
4522
4145
|
},
|
|
4523
4146
|
});
|
|
@@ -4526,7 +4149,6 @@ function FormElementWrapper({
|
|
|
4526
4149
|
}, [stateKey, path, localValue, setState]);
|
|
4527
4150
|
|
|
4528
4151
|
const baseState = rebuildStateShape({
|
|
4529
|
-
currentState: globalStateValue,
|
|
4530
4152
|
path: path,
|
|
4531
4153
|
componentId: componentId,
|
|
4532
4154
|
});
|
|
@@ -4565,9 +4187,6 @@ function useRegisterComponent(
|
|
|
4565
4187
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
4566
4188
|
|
|
4567
4189
|
useLayoutEffect(() => {
|
|
4568
|
-
const { registerComponent, unregisterComponent } =
|
|
4569
|
-
getGlobalStore.getState();
|
|
4570
|
-
|
|
4571
4190
|
// Call the safe, centralized function to register
|
|
4572
4191
|
registerComponent(stateKey, fullComponentId, {
|
|
4573
4192
|
forceUpdate: () => forceUpdate({}),
|