cogsbox-state 0.5.464 → 0.5.466
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/CogsState.d.ts +11 -30
- package/dist/CogsState.d.ts.map +1 -1
- package/dist/CogsState.jsx +1269 -1552
- package/dist/CogsState.jsx.map +1 -1
- package/dist/Components.d.ts +39 -0
- package/dist/Components.d.ts.map +1 -0
- package/dist/Components.jsx +281 -0
- package/dist/Components.jsx.map +1 -0
- package/dist/index.js +23 -24
- package/dist/store.d.ts +36 -47
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +299 -293
- 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 +1095 -1996
- package/src/Components.tsx +541 -0
- package/src/store.ts +502 -569
- package/src/utility.ts +0 -65
- package/dist/Functions.d.ts +0 -11
- package/dist/Functions.d.ts.map +0 -1
- package/dist/Functions.jsx +0 -22
- package/dist/Functions.jsx.map +0 -1
- package/src/Functions.tsx +0 -46
package/src/CogsState.tsx
CHANGED
|
@@ -14,15 +14,18 @@ import {
|
|
|
14
14
|
type ReactNode,
|
|
15
15
|
type RefObject,
|
|
16
16
|
} from 'react';
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
import {
|
|
19
|
-
debounce,
|
|
20
19
|
getDifferences,
|
|
21
20
|
isArray,
|
|
22
21
|
isFunction,
|
|
23
22
|
type GenericObject,
|
|
24
23
|
} from './utility.js';
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
FormElementWrapper,
|
|
26
|
+
MemoizedCogsItemWrapper,
|
|
27
|
+
ValidationWrapper,
|
|
28
|
+
} from './Components.js';
|
|
26
29
|
import { isDeepEqual, transformStateFunc } from './utility.js';
|
|
27
30
|
import superjson from 'superjson';
|
|
28
31
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -30,15 +33,15 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
30
33
|
import {
|
|
31
34
|
formRefStore,
|
|
32
35
|
getGlobalStore,
|
|
36
|
+
ValidationError,
|
|
33
37
|
ValidationStatus,
|
|
34
38
|
type ComponentsType,
|
|
35
39
|
} from './store.js';
|
|
36
40
|
import { useCogsConfig } from './CogsStateClient.js';
|
|
37
41
|
import { Operation } from 'fast-json-patch';
|
|
38
|
-
|
|
42
|
+
|
|
39
43
|
import * as z3 from 'zod/v3';
|
|
40
44
|
import * as z4 from 'zod/v4';
|
|
41
|
-
import z from 'zod';
|
|
42
45
|
|
|
43
46
|
type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
|
|
44
47
|
|
|
@@ -245,10 +248,7 @@ export type UpdateType<T> = (payload: UpdateArg<T>) => { synced: () => void };
|
|
|
245
248
|
|
|
246
249
|
export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
|
|
247
250
|
export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
|
|
248
|
-
|
|
249
|
-
path: (string | number)[];
|
|
250
|
-
message: string;
|
|
251
|
-
};
|
|
251
|
+
|
|
252
252
|
type EffectFunction<T, R> = (state: T, deps: any[]) => R;
|
|
253
253
|
export type EndType<T, IsArrayElement = false> = {
|
|
254
254
|
addZodValidation: (errors: ValidationError[]) => void;
|
|
@@ -259,7 +259,7 @@ export type EndType<T, IsArrayElement = false> = {
|
|
|
259
259
|
_stateKey: string;
|
|
260
260
|
formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
|
|
261
261
|
get: () => T;
|
|
262
|
-
|
|
262
|
+
|
|
263
263
|
$get: () => T;
|
|
264
264
|
$derive: <R>(fn: EffectFunction<T, R>) => R;
|
|
265
265
|
|
|
@@ -306,7 +306,7 @@ export type StateObject<T> = (T extends any[]
|
|
|
306
306
|
_isLoading: boolean;
|
|
307
307
|
_serverState: T;
|
|
308
308
|
revertToInitialState: (obj?: { validationKey?: string }) => T;
|
|
309
|
-
|
|
309
|
+
|
|
310
310
|
middleware: (
|
|
311
311
|
middles: ({
|
|
312
312
|
updateLog,
|
|
@@ -337,7 +337,8 @@ type UpdateOptions = {
|
|
|
337
337
|
type EffectiveSetState<TStateObject> = (
|
|
338
338
|
newStateOrFunction:
|
|
339
339
|
| EffectiveSetStateArg<TStateObject, 'update'>
|
|
340
|
-
| EffectiveSetStateArg<TStateObject, 'insert'
|
|
340
|
+
| EffectiveSetStateArg<TStateObject, 'insert'>
|
|
341
|
+
| null,
|
|
341
342
|
path: string[],
|
|
342
343
|
updateObj: UpdateOptions,
|
|
343
344
|
validationKey?: string
|
|
@@ -449,10 +450,13 @@ type FormsElementsType<T> = {
|
|
|
449
450
|
children: React.ReactNode;
|
|
450
451
|
status: ValidationStatus; // Instead of 'active' boolean
|
|
451
452
|
|
|
453
|
+
hasErrors: boolean;
|
|
454
|
+
hasWarnings: boolean;
|
|
455
|
+
allErrors: ValidationError[];
|
|
456
|
+
|
|
452
457
|
path: string[];
|
|
453
458
|
message?: string;
|
|
454
|
-
|
|
455
|
-
key?: string;
|
|
459
|
+
getData?: () => T;
|
|
456
460
|
}) => React.ReactNode;
|
|
457
461
|
syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
|
|
458
462
|
};
|
|
@@ -476,11 +480,74 @@ export type TransformedStateType<T> = {
|
|
|
476
480
|
[P in keyof T]: T[P] extends CogsInitialState<infer U> ? U : T[P];
|
|
477
481
|
};
|
|
478
482
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
+
const {
|
|
484
|
+
getInitialOptions,
|
|
485
|
+
updateInitialStateGlobal,
|
|
486
|
+
// ALIAS THE NEW FUNCTIONS TO THE OLD NAMES
|
|
487
|
+
getShadowMetadata,
|
|
488
|
+
setShadowMetadata,
|
|
489
|
+
getShadowValue,
|
|
490
|
+
initializeShadowState,
|
|
491
|
+
updateShadowAtPath,
|
|
492
|
+
insertShadowArrayElement,
|
|
493
|
+
insertManyShadowArrayElements,
|
|
494
|
+
removeShadowArrayElement,
|
|
495
|
+
getSelectedIndex,
|
|
496
|
+
setInitialStateOptions,
|
|
497
|
+
setServerStateUpdate,
|
|
498
|
+
markAsDirty,
|
|
499
|
+
registerComponent,
|
|
500
|
+
unregisterComponent,
|
|
501
|
+
addPathComponent,
|
|
502
|
+
clearSelectedIndexesForState,
|
|
503
|
+
addStateLog,
|
|
504
|
+
setSyncInfo,
|
|
505
|
+
clearSelectedIndex,
|
|
506
|
+
getSyncInfo,
|
|
507
|
+
notifyPathSubscribers,
|
|
508
|
+
subscribeToPath,
|
|
509
|
+
// Note: The old functions are no longer imported under their original names
|
|
510
|
+
} = getGlobalStore.getState();
|
|
511
|
+
function getArrayData(stateKey: string, path: string[], meta?: MetaData) {
|
|
512
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
513
|
+
const isArray = !!shadowMeta?.arrayKeys;
|
|
514
|
+
|
|
515
|
+
if (!isArray) {
|
|
516
|
+
const value = getGlobalStore.getState().getShadowValue(stateKey, path);
|
|
517
|
+
return { isArray: false, value, keys: [] };
|
|
518
|
+
}
|
|
519
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
520
|
+
const viewIds = meta?.arrayViews?.[arrayPathKey] ?? shadowMeta.arrayKeys;
|
|
521
|
+
|
|
522
|
+
// FIX: If the derived view is empty, return an empty array and keys.
|
|
523
|
+
if (Array.isArray(viewIds) && viewIds.length === 0) {
|
|
524
|
+
return { isArray: true, value: [], keys: [] };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const value = getGlobalStore
|
|
528
|
+
.getState()
|
|
529
|
+
.getShadowValue(stateKey, path, viewIds);
|
|
530
|
+
|
|
531
|
+
return { isArray: true, value, keys: viewIds ?? [] };
|
|
532
|
+
}
|
|
483
533
|
|
|
534
|
+
function findArrayItem(
|
|
535
|
+
array: any[],
|
|
536
|
+
keys: string[],
|
|
537
|
+
predicate: (item: any, index: number) => boolean
|
|
538
|
+
): { key: string; index: number; value: any } | null {
|
|
539
|
+
for (let i = 0; i < array.length; i++) {
|
|
540
|
+
if (predicate(array[i], i)) {
|
|
541
|
+
const key = keys[i];
|
|
542
|
+
if (key) {
|
|
543
|
+
return { key, index: i, value: array[i] };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function setAndMergeOptions(stateKey: string, newOptions: OptionsType<any>) {
|
|
484
551
|
const initialOptions = getInitialOptions(stateKey as string) || {};
|
|
485
552
|
|
|
486
553
|
setInitialStateOptions(stateKey as string, {
|
|
@@ -499,8 +566,7 @@ function setOptions<StateKey, Opt>({
|
|
|
499
566
|
}) {
|
|
500
567
|
const initialOptions = getInitialOptions(stateKey as string) || {};
|
|
501
568
|
const initialOptionsPartState = initialOptionsPart[stateKey as string] || {};
|
|
502
|
-
|
|
503
|
-
getGlobalStore.getState().setInitialStateOptions;
|
|
569
|
+
|
|
504
570
|
const mergedOptions = { ...initialOptionsPartState, ...initialOptions };
|
|
505
571
|
|
|
506
572
|
let needToAdd = false;
|
|
@@ -621,10 +687,10 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
621
687
|
const existingGlobalOptions = getInitialOptions(key);
|
|
622
688
|
|
|
623
689
|
if (!existingGlobalOptions) {
|
|
624
|
-
|
|
690
|
+
setInitialStateOptions(key, mergedOptions);
|
|
625
691
|
} else {
|
|
626
692
|
// Merge with existing global options
|
|
627
|
-
|
|
693
|
+
setInitialStateOptions(key, {
|
|
628
694
|
...existingGlobalOptions,
|
|
629
695
|
...mergedOptions,
|
|
630
696
|
});
|
|
@@ -633,9 +699,9 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
633
699
|
});
|
|
634
700
|
|
|
635
701
|
Object.keys(statePart).forEach((key) => {
|
|
636
|
-
|
|
702
|
+
initializeShadowState(key, statePart[key]);
|
|
637
703
|
});
|
|
638
|
-
|
|
704
|
+
console.log('new stateObject ', getGlobalStore.getState().shadowStateStore);
|
|
639
705
|
type StateKeys = keyof typeof statePart;
|
|
640
706
|
|
|
641
707
|
const useCogsState = <StateKey extends StateKeys>(
|
|
@@ -650,8 +716,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
|
|
|
650
716
|
initialOptionsPart,
|
|
651
717
|
});
|
|
652
718
|
const thiState =
|
|
653
|
-
|
|
654
|
-
statePart[stateKey as string];
|
|
719
|
+
getShadowValue(stateKey as string, []) || statePart[stateKey as string];
|
|
655
720
|
const partialState = options?.modifyState
|
|
656
721
|
? options.modifyState(thiState)
|
|
657
722
|
: thiState;
|
|
@@ -774,12 +839,7 @@ export function createCogsStateFromSync<
|
|
|
774
839
|
__syncSchemas: schemas,
|
|
775
840
|
}) as any;
|
|
776
841
|
}
|
|
777
|
-
const {
|
|
778
|
-
getInitialOptions,
|
|
779
842
|
|
|
780
|
-
addStateLog,
|
|
781
|
-
updateInitialStateGlobal,
|
|
782
|
-
} = getGlobalStore.getState();
|
|
783
843
|
const saveToLocalStorage = <T,>(
|
|
784
844
|
state: T,
|
|
785
845
|
thisKey: string,
|
|
@@ -811,7 +871,7 @@ const saveToLocalStorage = <T,>(
|
|
|
811
871
|
} catch {
|
|
812
872
|
// Ignore errors, will use undefined
|
|
813
873
|
}
|
|
814
|
-
const shadowMeta =
|
|
874
|
+
const shadowMeta = getShadowMetadata(thisKey, []);
|
|
815
875
|
|
|
816
876
|
const data: LocalStorageData<T> = {
|
|
817
877
|
state,
|
|
@@ -847,7 +907,7 @@ const loadFromLocalStorage = (localStorageKey: string) => {
|
|
|
847
907
|
}
|
|
848
908
|
};
|
|
849
909
|
const loadAndApplyLocalStorage = (stateKey: string, options: any) => {
|
|
850
|
-
const currentState =
|
|
910
|
+
const currentState = getShadowValue(stateKey, []);
|
|
851
911
|
const { sessionId } = useCogsConfig();
|
|
852
912
|
const localkey = isFunction(options?.localStorage?.key)
|
|
853
913
|
? options.localStorage.key(currentState)
|
|
@@ -878,7 +938,7 @@ type LocalStorageData<T> = {
|
|
|
878
938
|
};
|
|
879
939
|
|
|
880
940
|
const notifyComponents = (thisKey: string) => {
|
|
881
|
-
const stateEntry =
|
|
941
|
+
const stateEntry = getShadowMetadata(thisKey, []);
|
|
882
942
|
if (!stateEntry) return;
|
|
883
943
|
|
|
884
944
|
// Batch component updates
|
|
@@ -900,40 +960,15 @@ const notifyComponents = (thisKey: string) => {
|
|
|
900
960
|
});
|
|
901
961
|
};
|
|
902
962
|
|
|
903
|
-
export const notifyComponent = (stateKey: string, componentId: string) => {
|
|
904
|
-
const stateEntry = getGlobalStore.getState().getShadowMetadata(stateKey, []);
|
|
905
|
-
if (stateEntry) {
|
|
906
|
-
const fullComponentId = `${stateKey}////${componentId}`;
|
|
907
|
-
const component = stateEntry?.components?.get(fullComponentId);
|
|
908
|
-
const reactiveTypes = component
|
|
909
|
-
? Array.isArray(component.reactiveType)
|
|
910
|
-
? component.reactiveType
|
|
911
|
-
: [component.reactiveType || 'component']
|
|
912
|
-
: null;
|
|
913
|
-
|
|
914
|
-
// Skip if reactivity is disabled
|
|
915
|
-
if (reactiveTypes?.includes('none')) {
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (component) {
|
|
920
|
-
// Force an update to ensure the current value is saved
|
|
921
|
-
|
|
922
|
-
component.forceUpdate();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
};
|
|
926
963
|
function markEntireStateAsServerSynced(
|
|
927
964
|
stateKey: string,
|
|
928
965
|
path: string[],
|
|
929
966
|
data: any,
|
|
930
967
|
timestamp: number
|
|
931
968
|
) {
|
|
932
|
-
const store = getGlobalStore.getState();
|
|
933
|
-
|
|
934
969
|
// Mark current path as synced
|
|
935
|
-
const currentMeta =
|
|
936
|
-
|
|
970
|
+
const currentMeta = getShadowMetadata(stateKey, path);
|
|
971
|
+
setShadowMetadata(stateKey, path, {
|
|
937
972
|
...currentMeta,
|
|
938
973
|
isDirty: false,
|
|
939
974
|
stateSource: 'server',
|
|
@@ -942,10 +977,11 @@ function markEntireStateAsServerSynced(
|
|
|
942
977
|
|
|
943
978
|
// If it's an array, mark each item as synced
|
|
944
979
|
if (Array.isArray(data)) {
|
|
945
|
-
const arrayMeta =
|
|
980
|
+
const arrayMeta = getShadowMetadata(stateKey, path);
|
|
946
981
|
if (arrayMeta?.arrayKeys) {
|
|
947
982
|
arrayMeta.arrayKeys.forEach((itemKey, index) => {
|
|
948
|
-
|
|
983
|
+
// Fix: Don't split the itemKey, just use it directly
|
|
984
|
+
const itemPath = [...path, itemKey];
|
|
949
985
|
const itemData = data[index];
|
|
950
986
|
if (itemData !== undefined) {
|
|
951
987
|
markEntireStateAsServerSynced(
|
|
@@ -967,8 +1003,346 @@ function markEntireStateAsServerSynced(
|
|
|
967
1003
|
});
|
|
968
1004
|
}
|
|
969
1005
|
}
|
|
970
|
-
|
|
971
|
-
let
|
|
1006
|
+
// 5. Batch queue
|
|
1007
|
+
let updateBatchQueue: any[] = [];
|
|
1008
|
+
let isFlushScheduled = false;
|
|
1009
|
+
|
|
1010
|
+
function scheduleFlush() {
|
|
1011
|
+
if (!isFlushScheduled) {
|
|
1012
|
+
isFlushScheduled = true;
|
|
1013
|
+
queueMicrotask(flushQueue);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function handleUpdate(
|
|
1017
|
+
stateKey: string,
|
|
1018
|
+
path: string[],
|
|
1019
|
+
payload: any
|
|
1020
|
+
): { type: 'update'; oldValue: any; newValue: any; shadowMeta: any } {
|
|
1021
|
+
// ✅ FIX: Get the old value before the update.
|
|
1022
|
+
const oldValue = getGlobalStore.getState().getShadowValue(stateKey, path);
|
|
1023
|
+
|
|
1024
|
+
const newValue = isFunction(payload) ? payload(oldValue) : payload;
|
|
1025
|
+
|
|
1026
|
+
// ✅ FIX: The new `updateShadowAtPath` handles metadata preservation automatically.
|
|
1027
|
+
// The manual loop has been removed.
|
|
1028
|
+
updateShadowAtPath(stateKey, path, newValue);
|
|
1029
|
+
|
|
1030
|
+
markAsDirty(stateKey, path, { bubble: true });
|
|
1031
|
+
|
|
1032
|
+
// Return the metadata of the node *after* the update.
|
|
1033
|
+
const newShadowMeta = getShadowMetadata(stateKey, path);
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
type: 'update',
|
|
1037
|
+
oldValue: oldValue,
|
|
1038
|
+
newValue,
|
|
1039
|
+
shadowMeta: newShadowMeta,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
// 2. Update signals
|
|
1043
|
+
function updateSignals(shadowMeta: any, displayValue: any) {
|
|
1044
|
+
if (!shadowMeta?.signals?.length) return;
|
|
1045
|
+
|
|
1046
|
+
shadowMeta.signals.forEach(({ parentId, position, effect }: any) => {
|
|
1047
|
+
const parent = document.querySelector(`[data-parent-id="${parentId}"]`);
|
|
1048
|
+
if (!parent) return;
|
|
1049
|
+
|
|
1050
|
+
const childNodes = Array.from(parent.childNodes);
|
|
1051
|
+
if (!childNodes[position]) return;
|
|
1052
|
+
|
|
1053
|
+
let finalDisplayValue = displayValue;
|
|
1054
|
+
if (effect && displayValue !== null) {
|
|
1055
|
+
try {
|
|
1056
|
+
finalDisplayValue = new Function('state', `return (${effect})(state)`)(
|
|
1057
|
+
displayValue
|
|
1058
|
+
);
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
console.error('Error evaluating effect function:', err);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (finalDisplayValue !== null && typeof finalDisplayValue === 'object') {
|
|
1065
|
+
finalDisplayValue = JSON.stringify(finalDisplayValue);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
childNodes[position].textContent = String(finalDisplayValue ?? '');
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function getComponentNotifications(
|
|
1073
|
+
stateKey: string,
|
|
1074
|
+
path: string[],
|
|
1075
|
+
result: any
|
|
1076
|
+
): Set<any> {
|
|
1077
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1078
|
+
|
|
1079
|
+
if (!rootMeta?.components) {
|
|
1080
|
+
return new Set();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const componentsToNotify = new Set<any>();
|
|
1084
|
+
|
|
1085
|
+
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
1086
|
+
|
|
1087
|
+
if (result.type === 'update') {
|
|
1088
|
+
// --- Bubble-up Notification ---
|
|
1089
|
+
// An update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
|
|
1090
|
+
let currentPath = [...path];
|
|
1091
|
+
while (true) {
|
|
1092
|
+
const pathMeta = getShadowMetadata(stateKey, currentPath);
|
|
1093
|
+
|
|
1094
|
+
if (pathMeta?.pathComponents) {
|
|
1095
|
+
pathMeta.pathComponents.forEach((componentId: string) => {
|
|
1096
|
+
const component = rootMeta.components?.get(componentId);
|
|
1097
|
+
// NEW: Add component to the set instead of calling forceUpdate()
|
|
1098
|
+
if (component) {
|
|
1099
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1100
|
+
? component.reactiveType
|
|
1101
|
+
: [component.reactiveType || 'component'];
|
|
1102
|
+
if (!reactiveTypes.includes('none')) {
|
|
1103
|
+
componentsToNotify.add(component);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (currentPath.length === 0) break;
|
|
1110
|
+
currentPath.pop(); // Go up one level
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// --- Deep Object Change Notification ---
|
|
1114
|
+
// If the new value is an object, notify components subscribed to sub-paths that changed.
|
|
1115
|
+
if (
|
|
1116
|
+
result.newValue &&
|
|
1117
|
+
typeof result.newValue === 'object' &&
|
|
1118
|
+
!isArray(result.newValue)
|
|
1119
|
+
) {
|
|
1120
|
+
const changedSubPaths = getDifferences(result.newValue, result.oldValue);
|
|
1121
|
+
|
|
1122
|
+
changedSubPaths.forEach((subPathString: string) => {
|
|
1123
|
+
const subPath = subPathString.split('.');
|
|
1124
|
+
const fullSubPath = [...path, ...subPath];
|
|
1125
|
+
const subPathMeta = getShadowMetadata(stateKey, fullSubPath);
|
|
1126
|
+
|
|
1127
|
+
if (subPathMeta?.pathComponents) {
|
|
1128
|
+
subPathMeta.pathComponents.forEach((componentId: string) => {
|
|
1129
|
+
const component = rootMeta.components?.get(componentId);
|
|
1130
|
+
// NEW: Add component to the set
|
|
1131
|
+
if (component) {
|
|
1132
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1133
|
+
? component.reactiveType
|
|
1134
|
+
: [component.reactiveType || 'component'];
|
|
1135
|
+
if (!reactiveTypes.includes('none')) {
|
|
1136
|
+
componentsToNotify.add(component);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
} else if (result.type === 'insert' || result.type === 'cut') {
|
|
1144
|
+
// For array structural changes (add/remove), notify components listening to the parent array.
|
|
1145
|
+
const parentArrayPath = result.type === 'insert' ? path : path.slice(0, -1);
|
|
1146
|
+
const parentMeta = getShadowMetadata(stateKey, parentArrayPath);
|
|
1147
|
+
|
|
1148
|
+
if (parentMeta?.pathComponents) {
|
|
1149
|
+
parentMeta.pathComponents.forEach((componentId: string) => {
|
|
1150
|
+
const component = rootMeta.components?.get(componentId);
|
|
1151
|
+
// NEW: Add component to the set
|
|
1152
|
+
if (component) {
|
|
1153
|
+
componentsToNotify.add(component);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// --- PASS 2: Handle 'all' and 'deps' reactivity types ---
|
|
1160
|
+
// Iterate over all components for this stateKey that haven't been notified yet.
|
|
1161
|
+
rootMeta.components.forEach((component, componentId) => {
|
|
1162
|
+
// If we've already added this component, skip it.
|
|
1163
|
+
if (componentsToNotify.has(component)) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1168
|
+
? component.reactiveType
|
|
1169
|
+
: [component.reactiveType || 'component'];
|
|
1170
|
+
|
|
1171
|
+
if (reactiveTypes.includes('all')) {
|
|
1172
|
+
componentsToNotify.add(component);
|
|
1173
|
+
} else if (reactiveTypes.includes('deps') && component.depsFunction) {
|
|
1174
|
+
const currentState = getShadowValue(stateKey, []);
|
|
1175
|
+
const newDeps = component.depsFunction(currentState);
|
|
1176
|
+
|
|
1177
|
+
if (
|
|
1178
|
+
newDeps === true ||
|
|
1179
|
+
(Array.isArray(newDeps) && !isDeepEqual(component.prevDeps, newDeps))
|
|
1180
|
+
) {
|
|
1181
|
+
component.prevDeps = newDeps as any; // Update the dependencies for the next check
|
|
1182
|
+
componentsToNotify.add(component);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
return componentsToNotify;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function handleInsert(
|
|
1191
|
+
stateKey: string,
|
|
1192
|
+
path: string[],
|
|
1193
|
+
payload: any
|
|
1194
|
+
): { type: 'insert'; newValue: any; shadowMeta: any } {
|
|
1195
|
+
let newValue;
|
|
1196
|
+
if (isFunction(payload)) {
|
|
1197
|
+
const { value: currentValue } = getScopedData(stateKey, path);
|
|
1198
|
+
newValue = payload({ state: currentValue, uuid: uuidv4() });
|
|
1199
|
+
} else {
|
|
1200
|
+
newValue = payload;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
insertShadowArrayElement(stateKey, path, newValue);
|
|
1204
|
+
markAsDirty(stateKey, path, { bubble: true });
|
|
1205
|
+
|
|
1206
|
+
const updatedMeta = getShadowMetadata(stateKey, path);
|
|
1207
|
+
if (updatedMeta?.arrayKeys) {
|
|
1208
|
+
const newItemKey = updatedMeta.arrayKeys[updatedMeta.arrayKeys.length - 1];
|
|
1209
|
+
if (newItemKey) {
|
|
1210
|
+
const newItemPath = newItemKey.split('.').slice(1);
|
|
1211
|
+
markAsDirty(stateKey, newItemPath, { bubble: false });
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return { type: 'insert', newValue, shadowMeta: updatedMeta };
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function handleCut(
|
|
1219
|
+
stateKey: string,
|
|
1220
|
+
path: string[]
|
|
1221
|
+
): { type: 'cut'; oldValue: any; parentPath: string[] } {
|
|
1222
|
+
const parentArrayPath = path.slice(0, -1);
|
|
1223
|
+
const oldValue = getShadowValue(stateKey, path);
|
|
1224
|
+
removeShadowArrayElement(stateKey, path);
|
|
1225
|
+
markAsDirty(stateKey, parentArrayPath, { bubble: true });
|
|
1226
|
+
return { type: 'cut', oldValue: oldValue, parentPath: parentArrayPath };
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function flushQueue() {
|
|
1230
|
+
const allComponentsToNotify = new Set<any>();
|
|
1231
|
+
const signalUpdates: { shadowMeta: any; displayValue: any }[] = [];
|
|
1232
|
+
|
|
1233
|
+
const logsToAdd: UpdateTypeDetail[] = [];
|
|
1234
|
+
|
|
1235
|
+
for (const item of updateBatchQueue) {
|
|
1236
|
+
if (item.status && item.updateType) {
|
|
1237
|
+
logsToAdd.push(item as UpdateTypeDetail);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const result = item;
|
|
1242
|
+
|
|
1243
|
+
const displayValue = result.type === 'cut' ? null : result.newValue;
|
|
1244
|
+
if (result.shadowMeta?.signals?.length > 0) {
|
|
1245
|
+
signalUpdates.push({ shadowMeta: result.shadowMeta, displayValue });
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const componentNotifications = getComponentNotifications(
|
|
1249
|
+
result.stateKey,
|
|
1250
|
+
result.path,
|
|
1251
|
+
result
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
componentNotifications.forEach((component) => {
|
|
1255
|
+
allComponentsToNotify.add(component);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (logsToAdd.length > 0) {
|
|
1260
|
+
addStateLog(logsToAdd);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
signalUpdates.forEach(({ shadowMeta, displayValue }) => {
|
|
1264
|
+
updateSignals(shadowMeta, displayValue);
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
allComponentsToNotify.forEach((component) => {
|
|
1268
|
+
component.forceUpdate();
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
// --- Step 3: CLEANUP ---
|
|
1272
|
+
// Clear the queue for the next batch of updates.
|
|
1273
|
+
updateBatchQueue = [];
|
|
1274
|
+
isFlushScheduled = false;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function createEffectiveSetState<T>(
|
|
1278
|
+
thisKey: string,
|
|
1279
|
+
syncApiRef: React.MutableRefObject<any>,
|
|
1280
|
+
sessionId: string | undefined,
|
|
1281
|
+
latestInitialOptionsRef: React.MutableRefObject<OptionsType<T> | null>
|
|
1282
|
+
): EffectiveSetState<T> {
|
|
1283
|
+
// The returned function is the core setter that gets called by all state operations.
|
|
1284
|
+
// It is now much simpler, delegating all work to the executeUpdate function.
|
|
1285
|
+
return (newStateOrFunction, path, updateObj, validationKey?) => {
|
|
1286
|
+
executeUpdate(thisKey, path, newStateOrFunction, updateObj);
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// This inner function handles the logic for a single state update.
|
|
1290
|
+
function executeUpdate(
|
|
1291
|
+
stateKey: string,
|
|
1292
|
+
path: string[],
|
|
1293
|
+
payload: any,
|
|
1294
|
+
options: UpdateOptions
|
|
1295
|
+
) {
|
|
1296
|
+
// --- Step 1: Execute the core state change (Synchronous & Fast) ---
|
|
1297
|
+
// This part modifies the in-memory state representation immediately.
|
|
1298
|
+
let result: any;
|
|
1299
|
+
switch (options.updateType) {
|
|
1300
|
+
case 'update':
|
|
1301
|
+
result = handleUpdate(stateKey, path, payload);
|
|
1302
|
+
break;
|
|
1303
|
+
case 'insert':
|
|
1304
|
+
result = handleInsert(stateKey, path, payload);
|
|
1305
|
+
break;
|
|
1306
|
+
case 'cut':
|
|
1307
|
+
result = handleCut(stateKey, path);
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
result.stateKey = stateKey;
|
|
1312
|
+
result.path = path;
|
|
1313
|
+
updateBatchQueue.push(result);
|
|
1314
|
+
scheduleFlush();
|
|
1315
|
+
|
|
1316
|
+
const newUpdate: UpdateTypeDetail = {
|
|
1317
|
+
timeStamp: Date.now(),
|
|
1318
|
+
stateKey,
|
|
1319
|
+
path,
|
|
1320
|
+
updateType: options.updateType,
|
|
1321
|
+
status: 'new',
|
|
1322
|
+
oldValue: result.oldValue,
|
|
1323
|
+
newValue: result.newValue ?? null,
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
updateBatchQueue.push(newUpdate);
|
|
1327
|
+
|
|
1328
|
+
if (result.newValue !== undefined) {
|
|
1329
|
+
saveToLocalStorage(
|
|
1330
|
+
result.newValue,
|
|
1331
|
+
stateKey,
|
|
1332
|
+
latestInitialOptionsRef.current,
|
|
1333
|
+
sessionId
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (latestInitialOptionsRef.current?.middleware) {
|
|
1338
|
+
latestInitialOptionsRef.current.middleware({ update: newUpdate });
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (options.sync !== false && syncApiRef.current?.connected) {
|
|
1342
|
+
syncApiRef.current.updateState({ operation: newUpdate });
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
972
1346
|
|
|
973
1347
|
export function useCogsStateFn<TStateObject extends unknown>(
|
|
974
1348
|
stateObject: TStateObject,
|
|
@@ -1007,7 +1381,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1007
1381
|
useEffect(() => {
|
|
1008
1382
|
if (syncUpdate && syncUpdate.stateKey === thisKey && syncUpdate.path?.[0]) {
|
|
1009
1383
|
const syncKey = `${syncUpdate.stateKey}:${syncUpdate.path.join('.')}`;
|
|
1010
|
-
|
|
1384
|
+
setSyncInfo(syncKey, {
|
|
1011
1385
|
timeStamp: syncUpdate.timeStamp!,
|
|
1012
1386
|
userId: syncUpdate.userId!,
|
|
1013
1387
|
});
|
|
@@ -1077,7 +1451,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1077
1451
|
|
|
1078
1452
|
// Effect 1: When this component's serverState prop changes, broadcast it
|
|
1079
1453
|
useEffect(() => {
|
|
1080
|
-
|
|
1454
|
+
setServerStateUpdate(thisKey, serverState);
|
|
1081
1455
|
}, [serverState, thisKey]);
|
|
1082
1456
|
|
|
1083
1457
|
// Effect 2: Listen for server state updates from ANY component
|
|
@@ -1087,115 +1461,71 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1087
1461
|
.subscribeToPath(thisKey, (event) => {
|
|
1088
1462
|
if (event?.type === 'SERVER_STATE_UPDATE') {
|
|
1089
1463
|
const serverStateData = event.serverState;
|
|
1090
|
-
|
|
1464
|
+
|
|
1091
1465
|
if (
|
|
1092
|
-
serverStateData?.status
|
|
1093
|
-
serverStateData.data
|
|
1466
|
+
serverStateData?.status !== 'success' ||
|
|
1467
|
+
serverStateData.data === undefined
|
|
1094
1468
|
) {
|
|
1095
|
-
|
|
1096
|
-
|
|
1469
|
+
return; // Ignore if no valid data
|
|
1470
|
+
}
|
|
1097
1471
|
|
|
1098
|
-
|
|
1099
|
-
typeof serverStateData.merge === 'object'
|
|
1100
|
-
? serverStateData.merge
|
|
1101
|
-
: serverStateData.merge === true
|
|
1102
|
-
? { strategy: 'append' }
|
|
1103
|
-
: null;
|
|
1472
|
+
setAndMergeOptions(thisKey, { serverState: serverStateData });
|
|
1104
1473
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
.
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
Array.isArray(currentState) &&
|
|
1112
|
-
Array.isArray(incomingData)
|
|
1113
|
-
) {
|
|
1114
|
-
const keyField = mergeConfig.key;
|
|
1115
|
-
const existingIds = new Set(
|
|
1116
|
-
currentState.map((item: any) => item[keyField])
|
|
1117
|
-
);
|
|
1474
|
+
const mergeConfig =
|
|
1475
|
+
typeof serverStateData.merge === 'object'
|
|
1476
|
+
? serverStateData.merge
|
|
1477
|
+
: serverStateData.merge === true
|
|
1478
|
+
? { strategy: 'append' }
|
|
1479
|
+
: null;
|
|
1118
1480
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
});
|
|
1481
|
+
const currentState = getShadowValue(thisKey, []);
|
|
1482
|
+
const incomingData = serverStateData.data;
|
|
1122
1483
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1484
|
+
if (
|
|
1485
|
+
mergeConfig &&
|
|
1486
|
+
mergeConfig.strategy === 'append' &&
|
|
1487
|
+
'key' in mergeConfig && // Type guard for key
|
|
1488
|
+
Array.isArray(currentState) &&
|
|
1489
|
+
Array.isArray(incomingData)
|
|
1490
|
+
) {
|
|
1491
|
+
const keyField = mergeConfig.key;
|
|
1492
|
+
if (!keyField) {
|
|
1493
|
+
console.error(
|
|
1494
|
+
"CogsState: Merge strategy 'append' requires a 'key' field."
|
|
1495
|
+
);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1128
1498
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
.getShadowMetadata(thisKey, []);
|
|
1133
|
-
|
|
1134
|
-
if (arrayMeta?.arrayKeys) {
|
|
1135
|
-
const newItemKey =
|
|
1136
|
-
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
1137
|
-
if (newItemKey) {
|
|
1138
|
-
const newItemPath = newItemKey.split('.').slice(1);
|
|
1139
|
-
|
|
1140
|
-
// Mark the new server item as synced, not dirty
|
|
1141
|
-
getGlobalStore
|
|
1142
|
-
.getState()
|
|
1143
|
-
.setShadowMetadata(thisKey, newItemPath, {
|
|
1144
|
-
isDirty: false,
|
|
1145
|
-
stateSource: 'server',
|
|
1146
|
-
lastServerSync:
|
|
1147
|
-
serverStateData.timestamp || Date.now(),
|
|
1148
|
-
});
|
|
1499
|
+
const existingIds = new Set(
|
|
1500
|
+
currentState.map((item: any) => item[keyField])
|
|
1501
|
+
);
|
|
1149
1502
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
typeof itemValue === 'object' &&
|
|
1157
|
-
!Array.isArray(itemValue)
|
|
1158
|
-
) {
|
|
1159
|
-
Object.keys(itemValue).forEach((fieldKey) => {
|
|
1160
|
-
const fieldPath = [...newItemPath, fieldKey];
|
|
1161
|
-
getGlobalStore
|
|
1162
|
-
.getState()
|
|
1163
|
-
.setShadowMetadata(thisKey, fieldPath, {
|
|
1164
|
-
isDirty: false,
|
|
1165
|
-
stateSource: 'server',
|
|
1166
|
-
lastServerSync:
|
|
1167
|
-
serverStateData.timestamp || Date.now(),
|
|
1168
|
-
});
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
} else {
|
|
1176
|
-
// For replace strategy or initial load
|
|
1177
|
-
getGlobalStore
|
|
1178
|
-
.getState()
|
|
1179
|
-
.initializeShadowState(thisKey, incomingData);
|
|
1180
|
-
|
|
1181
|
-
// Mark the entire state tree as synced from server
|
|
1182
|
-
markEntireStateAsServerSynced(
|
|
1183
|
-
thisKey,
|
|
1184
|
-
[],
|
|
1185
|
-
incomingData,
|
|
1186
|
-
serverStateData.timestamp
|
|
1187
|
-
);
|
|
1503
|
+
const newUniqueItems = incomingData.filter(
|
|
1504
|
+
(item: any) => !existingIds.has(item[keyField])
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
if (newUniqueItems.length > 0) {
|
|
1508
|
+
insertManyShadowArrayElements(thisKey, [], newUniqueItems);
|
|
1188
1509
|
}
|
|
1189
1510
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1511
|
+
// Mark the entire final state as synced
|
|
1512
|
+
const finalState = getShadowValue(thisKey, []);
|
|
1513
|
+
markEntireStateAsServerSynced(
|
|
1514
|
+
thisKey,
|
|
1515
|
+
[],
|
|
1516
|
+
finalState,
|
|
1517
|
+
serverStateData.timestamp
|
|
1518
|
+
);
|
|
1519
|
+
} else {
|
|
1520
|
+
// This handles the "replace" strategy (initial load)
|
|
1521
|
+
initializeShadowState(thisKey, incomingData);
|
|
1522
|
+
|
|
1523
|
+
markEntireStateAsServerSynced(
|
|
1524
|
+
thisKey,
|
|
1525
|
+
[],
|
|
1526
|
+
incomingData,
|
|
1527
|
+
serverStateData.timestamp
|
|
1528
|
+
);
|
|
1199
1529
|
}
|
|
1200
1530
|
}
|
|
1201
1531
|
});
|
|
@@ -1213,6 +1543,17 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1213
1543
|
|
|
1214
1544
|
const options = getInitialOptions(thisKey as string);
|
|
1215
1545
|
|
|
1546
|
+
const features = {
|
|
1547
|
+
syncEnabled: !!cogsSyncFn && !!syncOpt,
|
|
1548
|
+
validationEnabled: !!(
|
|
1549
|
+
options?.validation?.zodSchemaV4 || options?.validation?.zodSchemaV3
|
|
1550
|
+
),
|
|
1551
|
+
localStorageEnabled: !!options?.localStorage?.key,
|
|
1552
|
+
};
|
|
1553
|
+
setShadowMetadata(thisKey, [], {
|
|
1554
|
+
...existingMeta,
|
|
1555
|
+
features,
|
|
1556
|
+
});
|
|
1216
1557
|
if (options?.defaultState !== undefined || defaultState !== undefined) {
|
|
1217
1558
|
const finalDefaultState = options?.defaultState || defaultState;
|
|
1218
1559
|
|
|
@@ -1225,10 +1566,10 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1225
1566
|
|
|
1226
1567
|
const { value: resolvedState, source, timestamp } = resolveInitialState();
|
|
1227
1568
|
|
|
1228
|
-
|
|
1569
|
+
initializeShadowState(thisKey, resolvedState);
|
|
1229
1570
|
|
|
1230
1571
|
// Set shadow metadata with the correct source info
|
|
1231
|
-
|
|
1572
|
+
setShadowMetadata(thisKey, [], {
|
|
1232
1573
|
stateSource: source,
|
|
1233
1574
|
lastServerSync: source === 'server' ? timestamp : undefined,
|
|
1234
1575
|
isDirty: false,
|
|
@@ -1252,29 +1593,27 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1252
1593
|
const componentKey = `${thisKey}////${componentIdRef.current}`;
|
|
1253
1594
|
|
|
1254
1595
|
// Register component in shadow metadata at root level
|
|
1255
|
-
const rootMeta =
|
|
1596
|
+
const rootMeta = getShadowMetadata(thisKey, []);
|
|
1256
1597
|
const components = rootMeta?.components || new Map();
|
|
1257
1598
|
|
|
1258
1599
|
components.set(componentKey, {
|
|
1259
1600
|
forceUpdate: () => forceUpdate({}),
|
|
1260
|
-
reactiveType: reactiveType ?? ['component'
|
|
1601
|
+
reactiveType: reactiveType ?? ['component'],
|
|
1261
1602
|
paths: new Set(),
|
|
1262
1603
|
depsFunction: reactiveDeps || undefined,
|
|
1263
|
-
deps: reactiveDeps
|
|
1264
|
-
? reactiveDeps(getGlobalStore.getState().getShadowValue(thisKey))
|
|
1265
|
-
: [],
|
|
1604
|
+
deps: reactiveDeps ? reactiveDeps(getShadowValue(thisKey, [])) : [],
|
|
1266
1605
|
prevDeps: reactiveDeps // Initialize prevDeps with the same initial value
|
|
1267
|
-
? reactiveDeps(
|
|
1606
|
+
? reactiveDeps(getShadowValue(thisKey, []))
|
|
1268
1607
|
: [],
|
|
1269
1608
|
});
|
|
1270
1609
|
|
|
1271
|
-
|
|
1610
|
+
setShadowMetadata(thisKey, [], {
|
|
1272
1611
|
...rootMeta,
|
|
1273
1612
|
components,
|
|
1274
1613
|
});
|
|
1275
1614
|
forceUpdate({});
|
|
1276
1615
|
return () => {
|
|
1277
|
-
const meta =
|
|
1616
|
+
const meta = getShadowMetadata(thisKey, []);
|
|
1278
1617
|
const component = meta?.components?.get(componentKey);
|
|
1279
1618
|
|
|
1280
1619
|
// Remove from each path we registered to
|
|
@@ -1302,544 +1641,93 @@ export function useCogsStateFn<TStateObject extends unknown>(
|
|
|
1302
1641
|
|
|
1303
1642
|
// Remove from root components
|
|
1304
1643
|
if (meta?.components) {
|
|
1305
|
-
|
|
1644
|
+
setShadowMetadata(thisKey, [], meta);
|
|
1306
1645
|
}
|
|
1307
1646
|
};
|
|
1308
1647
|
}, []);
|
|
1309
1648
|
|
|
1310
1649
|
const syncApiRef = useRef<SyncApi | null>(null);
|
|
1650
|
+
const effectiveSetState = createEffectiveSetState(
|
|
1651
|
+
thisKey,
|
|
1652
|
+
syncApiRef,
|
|
1653
|
+
sessionId,
|
|
1654
|
+
latestInitialOptionsRef
|
|
1655
|
+
);
|
|
1311
1656
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
updateObj: UpdateOptions
|
|
1316
|
-
) => {
|
|
1317
|
-
const fullPath = [thisKey, ...path].join('.');
|
|
1318
|
-
const store = getGlobalStore.getState();
|
|
1319
|
-
|
|
1320
|
-
const shadowMeta = store.getShadowMetadata(thisKey, path);
|
|
1321
|
-
const nestedShadowValue = store.getShadowValue(fullPath) as TStateObject;
|
|
1657
|
+
if (!getGlobalStore.getState().initialStateGlobal[thisKey]) {
|
|
1658
|
+
updateInitialStateGlobal(thisKey, stateObject);
|
|
1659
|
+
}
|
|
1322
1660
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
)
|
|
1661
|
+
const updaterFinal = useMemo(() => {
|
|
1662
|
+
const handler = createProxyHandler<TStateObject>(
|
|
1663
|
+
thisKey,
|
|
1664
|
+
effectiveSetState,
|
|
1665
|
+
componentIdRef.current,
|
|
1666
|
+
sessionId
|
|
1667
|
+
);
|
|
1330
1668
|
|
|
1331
|
-
|
|
1669
|
+
return handler;
|
|
1670
|
+
}, [thisKey, sessionId]);
|
|
1332
1671
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
stateKey: thisKey,
|
|
1336
|
-
path,
|
|
1337
|
-
updateType: updateObj.updateType,
|
|
1338
|
-
status: 'new' as const,
|
|
1339
|
-
oldValue: nestedShadowValue,
|
|
1340
|
-
newValue: payload,
|
|
1341
|
-
} satisfies UpdateTypeDetail;
|
|
1342
|
-
|
|
1343
|
-
// Perform the update
|
|
1344
|
-
switch (updateObj.updateType) {
|
|
1345
|
-
case 'insert': {
|
|
1346
|
-
store.insertShadowArrayElement(thisKey, path, newUpdate.newValue);
|
|
1347
|
-
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1348
|
-
const arrayMeta = shadowMeta;
|
|
1349
|
-
if (arrayMeta?.arrayKeys) {
|
|
1350
|
-
const newItemKey =
|
|
1351
|
-
arrayMeta.arrayKeys[arrayMeta.arrayKeys.length - 1];
|
|
1352
|
-
if (newItemKey) {
|
|
1353
|
-
const newItemPath = newItemKey.split('.').slice(1); // Remove stateKey
|
|
1354
|
-
store.markAsDirty(thisKey, newItemPath, { bubble: false });
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
break;
|
|
1358
|
-
}
|
|
1359
|
-
case 'cut': {
|
|
1360
|
-
const parentArrayPath = path.slice(0, -1);
|
|
1672
|
+
const cogsSyncFn = __useSync;
|
|
1673
|
+
const syncOpt = latestInitialOptionsRef.current?.syncOptions;
|
|
1361
1674
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
store.markAsDirty(thisKey, path, { bubble: true });
|
|
1369
|
-
break;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1675
|
+
if (cogsSyncFn) {
|
|
1676
|
+
syncApiRef.current = cogsSyncFn(
|
|
1677
|
+
updaterFinal as any,
|
|
1678
|
+
syncOpt ?? ({} as any)
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1372
1681
|
|
|
1373
|
-
|
|
1682
|
+
return updaterFinal;
|
|
1683
|
+
}
|
|
1374
1684
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
//
|
|
1379
|
-
if (shadowMeta?.signals && shadowMeta.signals.length > 0) {
|
|
1380
|
-
// Use updatedShadowValue if we need the new value, otherwise use payload
|
|
1381
|
-
const displayValue = updateObj.updateType === 'cut' ? null : payload;
|
|
1382
|
-
|
|
1383
|
-
shadowMeta.signals.forEach(({ parentId, position, effect }) => {
|
|
1384
|
-
const parent = document.querySelector(`[data-parent-id="${parentId}"]`);
|
|
1385
|
-
if (parent) {
|
|
1386
|
-
const childNodes = Array.from(parent.childNodes);
|
|
1387
|
-
if (childNodes[position]) {
|
|
1388
|
-
let finalDisplayValue = displayValue;
|
|
1389
|
-
if (effect && displayValue !== null) {
|
|
1390
|
-
try {
|
|
1391
|
-
finalDisplayValue = new Function(
|
|
1392
|
-
'state',
|
|
1393
|
-
`return (${effect})(state)`
|
|
1394
|
-
)(displayValue);
|
|
1395
|
-
} catch (err) {
|
|
1396
|
-
console.error('Error evaluating effect function:', err);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
if (
|
|
1401
|
-
finalDisplayValue !== null &&
|
|
1402
|
-
finalDisplayValue !== undefined &&
|
|
1403
|
-
typeof finalDisplayValue === 'object'
|
|
1404
|
-
) {
|
|
1405
|
-
finalDisplayValue = JSON.stringify(finalDisplayValue) as any;
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
childNodes[position].textContent = String(finalDisplayValue ?? '');
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Update in effectiveSetState for insert handling:
|
|
1415
|
-
if (updateObj.updateType === 'insert') {
|
|
1416
|
-
// Use shadowMeta from beginning if it's an array
|
|
1417
|
-
if (shadowMeta?.mapWrappers && shadowMeta.mapWrappers.length > 0) {
|
|
1418
|
-
// Get fresh array keys after insert
|
|
1419
|
-
const sourceArrayKeys =
|
|
1420
|
-
store.getShadowMetadata(thisKey, path)?.arrayKeys || [];
|
|
1421
|
-
const newItemKey = sourceArrayKeys[sourceArrayKeys.length - 1]!;
|
|
1422
|
-
const newItemValue = store.getShadowValue(newItemKey);
|
|
1423
|
-
const fullSourceArray = store.getShadowValue(
|
|
1424
|
-
[thisKey, ...path].join('.')
|
|
1425
|
-
);
|
|
1426
|
-
|
|
1427
|
-
if (!newItemKey || newItemValue === undefined) return;
|
|
1428
|
-
|
|
1429
|
-
shadowMeta.mapWrappers.forEach((wrapper) => {
|
|
1430
|
-
let shouldRender = true;
|
|
1431
|
-
let insertPosition = -1;
|
|
1432
|
-
|
|
1433
|
-
// Check if wrapper has transforms
|
|
1434
|
-
if (wrapper.meta?.transforms && wrapper.meta.transforms.length > 0) {
|
|
1435
|
-
// Check if new item passes all filters
|
|
1436
|
-
for (const transform of wrapper.meta.transforms) {
|
|
1437
|
-
if (transform.type === 'filter') {
|
|
1438
|
-
if (!transform.fn(newItemValue, -1)) {
|
|
1439
|
-
shouldRender = false;
|
|
1440
|
-
break;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (shouldRender) {
|
|
1446
|
-
// Get current valid keys by applying transforms
|
|
1447
|
-
const currentValidKeys = applyTransforms(
|
|
1448
|
-
thisKey,
|
|
1449
|
-
path,
|
|
1450
|
-
wrapper.meta.transforms
|
|
1451
|
-
);
|
|
1452
|
-
|
|
1453
|
-
// Find where to insert based on sort
|
|
1454
|
-
const sortTransform = wrapper.meta.transforms.find(
|
|
1455
|
-
(t: any) => t.type === 'sort'
|
|
1456
|
-
);
|
|
1457
|
-
if (sortTransform) {
|
|
1458
|
-
// Add new item to the list and sort to find position
|
|
1459
|
-
const allItems = currentValidKeys.map((key) => ({
|
|
1460
|
-
key,
|
|
1461
|
-
value: store.getShadowValue(key),
|
|
1462
|
-
}));
|
|
1463
|
-
|
|
1464
|
-
allItems.push({ key: newItemKey, value: newItemValue });
|
|
1465
|
-
allItems.sort((a, b) => sortTransform.fn(a.value, b.value));
|
|
1466
|
-
|
|
1467
|
-
insertPosition = allItems.findIndex(
|
|
1468
|
-
(item) => item.key === newItemKey
|
|
1469
|
-
);
|
|
1470
|
-
} else {
|
|
1471
|
-
// No sort, insert at end
|
|
1472
|
-
insertPosition = currentValidKeys.length;
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
} else {
|
|
1476
|
-
// No transforms, always render at end
|
|
1477
|
-
shouldRender = true;
|
|
1478
|
-
insertPosition = sourceArrayKeys.length - 1;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
if (!shouldRender) {
|
|
1482
|
-
return; // Skip this wrapper, item doesn't pass filters
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
if (wrapper.containerRef && wrapper.containerRef.isConnected) {
|
|
1486
|
-
const itemElement = document.createElement('div');
|
|
1487
|
-
itemElement.setAttribute('data-item-path', newItemKey);
|
|
1488
|
-
|
|
1489
|
-
// Insert at correct position
|
|
1490
|
-
const children = Array.from(wrapper.containerRef.children);
|
|
1491
|
-
if (insertPosition >= 0 && insertPosition < children.length) {
|
|
1492
|
-
wrapper.containerRef.insertBefore(
|
|
1493
|
-
itemElement,
|
|
1494
|
-
children[insertPosition]!
|
|
1495
|
-
);
|
|
1496
|
-
} else {
|
|
1497
|
-
wrapper.containerRef.appendChild(itemElement);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const root = createRoot(itemElement);
|
|
1501
|
-
const componentId = uuidv4();
|
|
1502
|
-
const itemPath = newItemKey.split('.').slice(1);
|
|
1503
|
-
|
|
1504
|
-
const arraySetter = wrapper.rebuildStateShape({
|
|
1505
|
-
path: wrapper.path,
|
|
1506
|
-
currentState: fullSourceArray,
|
|
1507
|
-
componentId: wrapper.componentId,
|
|
1508
|
-
meta: wrapper.meta,
|
|
1509
|
-
});
|
|
1510
|
-
|
|
1511
|
-
root.render(
|
|
1512
|
-
createElement(MemoizedCogsItemWrapper, {
|
|
1513
|
-
stateKey: thisKey,
|
|
1514
|
-
itemComponentId: componentId,
|
|
1515
|
-
itemPath: itemPath,
|
|
1516
|
-
localIndex: insertPosition,
|
|
1517
|
-
arraySetter: arraySetter,
|
|
1518
|
-
rebuildStateShape: wrapper.rebuildStateShape,
|
|
1519
|
-
renderFn: wrapper.mapFn,
|
|
1520
|
-
})
|
|
1521
|
-
);
|
|
1522
|
-
}
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
if (updateObj.updateType === 'cut') {
|
|
1528
|
-
const arrayPath = path.slice(0, -1);
|
|
1529
|
-
const arrayMeta = store.getShadowMetadata(thisKey, arrayPath);
|
|
1530
|
-
|
|
1531
|
-
if (arrayMeta?.mapWrappers && arrayMeta.mapWrappers.length > 0) {
|
|
1532
|
-
arrayMeta.mapWrappers.forEach((wrapper) => {
|
|
1533
|
-
if (wrapper.containerRef && wrapper.containerRef.isConnected) {
|
|
1534
|
-
const elementToRemove = wrapper.containerRef.querySelector(
|
|
1535
|
-
`[data-item-path="${fullPath}"]`
|
|
1536
|
-
);
|
|
1537
|
-
if (elementToRemove) {
|
|
1538
|
-
elementToRemove.remove();
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
});
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const rootMeta = store.getShadowMetadata(thisKey, []);
|
|
1546
|
-
const notifiedComponents = new Set<string>();
|
|
1547
|
-
|
|
1548
|
-
if (!rootMeta?.components) {
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// --- PASS 1: Notify specific subscribers based on update type ---
|
|
1553
|
-
|
|
1554
|
-
if (updateObj.updateType === 'update') {
|
|
1555
|
-
// --- Bubble-up Notification ---
|
|
1556
|
-
// When a nested property changes, notify components listening at that exact path,
|
|
1557
|
-
// and also "bubble up" to notify components listening on parent paths.
|
|
1558
|
-
// e.g., an update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
|
|
1559
|
-
let currentPath = [...path]; // Create a mutable copy of the path
|
|
1560
|
-
|
|
1561
|
-
while (true) {
|
|
1562
|
-
const currentPathMeta = store.getShadowMetadata(thisKey, currentPath);
|
|
1563
|
-
|
|
1564
|
-
if (currentPathMeta?.pathComponents) {
|
|
1565
|
-
currentPathMeta.pathComponents.forEach((componentId) => {
|
|
1566
|
-
if (notifiedComponents.has(componentId)) {
|
|
1567
|
-
return; // Avoid sending redundant notifications
|
|
1568
|
-
}
|
|
1569
|
-
const component = rootMeta.components?.get(componentId);
|
|
1570
|
-
if (component) {
|
|
1571
|
-
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1572
|
-
? component.reactiveType
|
|
1573
|
-
: [component.reactiveType || 'component'];
|
|
1574
|
-
|
|
1575
|
-
// This notification logic applies to components that depend on object structures.
|
|
1576
|
-
if (!reactiveTypes.includes('none')) {
|
|
1577
|
-
component.forceUpdate();
|
|
1578
|
-
notifiedComponents.add(componentId);
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
});
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
if (currentPath.length === 0) {
|
|
1585
|
-
break; // We've reached the root, stop bubbling.
|
|
1586
|
-
}
|
|
1587
|
-
currentPath.pop(); // Go up one level for the next iteration.
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// ADDITIONALLY, if the payload is an object, perform a deep-check and
|
|
1591
|
-
// notify any components that are subscribed to specific sub-paths that changed.
|
|
1592
|
-
if (
|
|
1593
|
-
payload &&
|
|
1594
|
-
typeof payload === 'object' &&
|
|
1595
|
-
!isArray(payload) &&
|
|
1596
|
-
nestedShadowValue &&
|
|
1597
|
-
typeof nestedShadowValue === 'object' &&
|
|
1598
|
-
!isArray(nestedShadowValue)
|
|
1599
|
-
) {
|
|
1600
|
-
// Get a list of dot-separated paths that have changed (e.g., ['name', 'address.city'])
|
|
1601
|
-
const changedSubPaths = getDifferences(payload, nestedShadowValue);
|
|
1602
|
-
|
|
1603
|
-
changedSubPaths.forEach((subPathString) => {
|
|
1604
|
-
const subPath = subPathString.split('.');
|
|
1605
|
-
const fullSubPath = [...path, ...subPath];
|
|
1606
|
-
|
|
1607
|
-
// Get the metadata (and subscribers) for this specific nested path
|
|
1608
|
-
const subPathMeta = store.getShadowMetadata(thisKey, fullSubPath);
|
|
1609
|
-
if (subPathMeta?.pathComponents) {
|
|
1610
|
-
subPathMeta.pathComponents.forEach((componentId) => {
|
|
1611
|
-
// Avoid sending a redundant update
|
|
1612
|
-
if (notifiedComponents.has(componentId)) {
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
const component = rootMeta.components?.get(componentId);
|
|
1616
|
-
if (component) {
|
|
1617
|
-
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1618
|
-
? component.reactiveType
|
|
1619
|
-
: [component.reactiveType || 'component'];
|
|
1620
|
-
|
|
1621
|
-
if (!reactiveTypes.includes('none')) {
|
|
1622
|
-
component.forceUpdate();
|
|
1623
|
-
notifiedComponents.add(componentId);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
} else if (
|
|
1631
|
-
updateObj.updateType === 'insert' ||
|
|
1632
|
-
updateObj.updateType === 'cut'
|
|
1633
|
-
) {
|
|
1634
|
-
// For array structural changes, notify components listening to the parent array.
|
|
1635
|
-
const parentArrayPath =
|
|
1636
|
-
updateObj.updateType === 'insert' ? path : path.slice(0, -1);
|
|
1637
|
-
|
|
1638
|
-
const parentMeta = store.getShadowMetadata(thisKey, parentArrayPath);
|
|
1639
|
-
|
|
1640
|
-
// Handle signal updates for array length, etc.
|
|
1641
|
-
if (parentMeta?.signals && parentMeta.signals.length > 0) {
|
|
1642
|
-
const parentFullPath = [thisKey, ...parentArrayPath].join('.');
|
|
1643
|
-
const parentValue = store.getShadowValue(parentFullPath);
|
|
1644
|
-
|
|
1645
|
-
parentMeta.signals.forEach(({ parentId, position, effect }) => {
|
|
1646
|
-
const parent = document.querySelector(
|
|
1647
|
-
`[data-parent-id="${parentId}"]`
|
|
1648
|
-
);
|
|
1649
|
-
if (parent) {
|
|
1650
|
-
const childNodes = Array.from(parent.childNodes);
|
|
1651
|
-
if (childNodes[position]) {
|
|
1652
|
-
let displayValue = parentValue;
|
|
1653
|
-
if (effect) {
|
|
1654
|
-
try {
|
|
1655
|
-
displayValue = new Function(
|
|
1656
|
-
'state',
|
|
1657
|
-
`return (${effect})(state)`
|
|
1658
|
-
)(parentValue);
|
|
1659
|
-
} catch (err) {
|
|
1660
|
-
console.error('Error evaluating effect function:', err);
|
|
1661
|
-
displayValue = parentValue;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
if (
|
|
1666
|
-
displayValue !== null &&
|
|
1667
|
-
displayValue !== undefined &&
|
|
1668
|
-
typeof displayValue === 'object'
|
|
1669
|
-
) {
|
|
1670
|
-
displayValue = JSON.stringify(displayValue);
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
childNodes[position].textContent = String(displayValue ?? '');
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// Notify components subscribed to the array itself.
|
|
1680
|
-
if (parentMeta?.pathComponents) {
|
|
1681
|
-
parentMeta.pathComponents.forEach((componentId) => {
|
|
1682
|
-
if (!notifiedComponents.has(componentId)) {
|
|
1683
|
-
const component = rootMeta.components?.get(componentId);
|
|
1684
|
-
if (component) {
|
|
1685
|
-
component.forceUpdate();
|
|
1686
|
-
notifiedComponents.add(componentId);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
rootMeta.components.forEach((component, componentId) => {
|
|
1694
|
-
if (notifiedComponents.has(componentId)) {
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
const reactiveTypes = Array.isArray(component.reactiveType)
|
|
1699
|
-
? component.reactiveType
|
|
1700
|
-
: [component.reactiveType || 'component'];
|
|
1701
|
-
|
|
1702
|
-
if (reactiveTypes.includes('all')) {
|
|
1703
|
-
component.forceUpdate();
|
|
1704
|
-
notifiedComponents.add(componentId);
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
if (reactiveTypes.includes('deps')) {
|
|
1709
|
-
if (component.depsFunction) {
|
|
1710
|
-
const currentState = store.getShadowValue(thisKey);
|
|
1711
|
-
const newDeps = component.depsFunction(currentState);
|
|
1712
|
-
let shouldUpdate = false;
|
|
1713
|
-
|
|
1714
|
-
if (newDeps === true) {
|
|
1715
|
-
shouldUpdate = true;
|
|
1716
|
-
} else if (Array.isArray(newDeps)) {
|
|
1717
|
-
if (!isDeepEqual(component.prevDeps, newDeps)) {
|
|
1718
|
-
component.prevDeps = newDeps;
|
|
1719
|
-
shouldUpdate = true;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (shouldUpdate) {
|
|
1724
|
-
component.forceUpdate();
|
|
1725
|
-
notifiedComponents.add(componentId);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
});
|
|
1730
|
-
notifiedComponents.clear();
|
|
1731
|
-
|
|
1732
|
-
addStateLog(thisKey, newUpdate);
|
|
1733
|
-
|
|
1734
|
-
saveToLocalStorage(
|
|
1735
|
-
payload,
|
|
1736
|
-
thisKey,
|
|
1737
|
-
latestInitialOptionsRef.current,
|
|
1738
|
-
sessionId
|
|
1739
|
-
);
|
|
1740
|
-
|
|
1741
|
-
if (latestInitialOptionsRef.current?.middleware) {
|
|
1742
|
-
latestInitialOptionsRef.current!.middleware({
|
|
1743
|
-
update: newUpdate,
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1685
|
+
type MetaData = {
|
|
1686
|
+
// Map array paths to their filtered/sorted ID order
|
|
1687
|
+
arrayViews?: {
|
|
1688
|
+
[arrayPath: string]: string[]; // e.g. { "todos": ["id:xxx", "id:yyy"], "todos.id:xxx.subtasks": ["id:aaa"] }
|
|
1746
1689
|
};
|
|
1747
|
-
|
|
1748
|
-
if (!getGlobalStore.getState().initialStateGlobal[thisKey]) {
|
|
1749
|
-
updateInitialStateGlobal(thisKey, stateObject);
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
const updaterFinal = useMemo(() => {
|
|
1753
|
-
const handler = createProxyHandler<TStateObject>(
|
|
1754
|
-
thisKey,
|
|
1755
|
-
effectiveSetState,
|
|
1756
|
-
componentIdRef.current,
|
|
1757
|
-
sessionId
|
|
1758
|
-
);
|
|
1759
|
-
|
|
1760
|
-
return handler;
|
|
1761
|
-
}, [thisKey, sessionId]);
|
|
1762
|
-
|
|
1763
|
-
const cogsSyncFn = __useSync;
|
|
1764
|
-
const syncOpt = latestInitialOptionsRef.current?.syncOptions;
|
|
1765
|
-
|
|
1766
|
-
if (cogsSyncFn) {
|
|
1767
|
-
syncApiRef.current = cogsSyncFn(
|
|
1768
|
-
updaterFinal as any,
|
|
1769
|
-
syncOpt ?? ({} as any)
|
|
1770
|
-
);
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
return updaterFinal;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
export type MetaData = {
|
|
1777
|
-
/**
|
|
1778
|
-
* An array of the full, unique string IDs (e.g., `"stateKey.arrayName.id:123"`)
|
|
1779
|
-
* of the items that belong to the current derived "view" of an array.
|
|
1780
|
-
* This is the primary mechanism for tracking the state of filtered or sorted lists.
|
|
1781
|
-
*
|
|
1782
|
-
* - `stateFilter` populates this with only the IDs of items that passed the filter.
|
|
1783
|
-
* - `stateSort` reorders this list to match the new sort order.
|
|
1784
|
-
* - All subsequent chained operations (like `.get()`, `.index()`, or `.cut()`)
|
|
1785
|
-
* MUST consult this list first to know which items they apply to and in what order.
|
|
1786
|
-
*/
|
|
1787
|
-
validIds?: string[];
|
|
1788
|
-
|
|
1789
|
-
/**
|
|
1790
|
-
* An array of the actual filter functions that have been applied in a chain.
|
|
1791
|
-
* This is primarily used by reactive renderers like `$stateMap` to make predictions.
|
|
1792
|
-
*
|
|
1793
|
-
* For example, when a new item is inserted into the original source array, a
|
|
1794
|
-
* `$stateMap` renderer on a filtered view can use these functions to test if the
|
|
1795
|
-
* newly inserted item should be dynamically rendered in its view.
|
|
1796
|
-
*/
|
|
1797
1690
|
transforms?: Array<{
|
|
1798
1691
|
type: 'filter' | 'sort';
|
|
1799
1692
|
fn: Function;
|
|
1693
|
+
path: string[]; // Which array this transform applies to
|
|
1800
1694
|
}>;
|
|
1695
|
+
serverStateIsUpStream?: boolean;
|
|
1801
1696
|
};
|
|
1802
1697
|
|
|
1803
|
-
function hashTransforms(transforms: any[]) {
|
|
1804
|
-
if (!transforms || transforms.length === 0) {
|
|
1805
|
-
return '';
|
|
1806
|
-
}
|
|
1807
|
-
return transforms
|
|
1808
|
-
.map(
|
|
1809
|
-
(transform) =>
|
|
1810
|
-
`${transform.type}${JSON.stringify(transform.dependencies || [])}`
|
|
1811
|
-
)
|
|
1812
|
-
.join('');
|
|
1813
|
-
}
|
|
1814
1698
|
const applyTransforms = (
|
|
1815
1699
|
stateKey: string,
|
|
1816
1700
|
path: string[],
|
|
1817
|
-
|
|
1701
|
+
meta?: MetaData
|
|
1818
1702
|
): string[] => {
|
|
1819
|
-
let arrayKeys
|
|
1820
|
-
|
|
1821
|
-
[];
|
|
1822
|
-
|
|
1703
|
+
let ids = getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
1704
|
+
const transforms = meta?.transforms;
|
|
1823
1705
|
if (!transforms || transforms.length === 0) {
|
|
1824
|
-
return
|
|
1706
|
+
return ids;
|
|
1825
1707
|
}
|
|
1826
1708
|
|
|
1827
|
-
|
|
1828
|
-
key,
|
|
1829
|
-
value: getGlobalStore.getState().getShadowValue(key),
|
|
1830
|
-
}));
|
|
1831
|
-
|
|
1709
|
+
// Apply each transform using just IDs
|
|
1832
1710
|
for (const transform of transforms) {
|
|
1833
1711
|
if (transform.type === 'filter') {
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1712
|
+
const filtered: any[] = [];
|
|
1713
|
+
ids.forEach((id, index) => {
|
|
1714
|
+
const value = getShadowValue(stateKey, [...path, id]);
|
|
1715
|
+
|
|
1716
|
+
if (transform.fn(value, index)) {
|
|
1717
|
+
filtered.push(id);
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
ids = filtered;
|
|
1837
1721
|
} else if (transform.type === 'sort') {
|
|
1838
|
-
|
|
1722
|
+
ids.sort((a, b) => {
|
|
1723
|
+
const aValue = getShadowValue(stateKey, [...path, a]);
|
|
1724
|
+
const bValue = getShadowValue(stateKey, [...path, b]);
|
|
1725
|
+
return transform.fn(aValue, bValue);
|
|
1726
|
+
});
|
|
1839
1727
|
}
|
|
1840
1728
|
}
|
|
1841
1729
|
|
|
1842
|
-
return
|
|
1730
|
+
return ids;
|
|
1843
1731
|
};
|
|
1844
1732
|
const registerComponentDependency = (
|
|
1845
1733
|
stateKey: string,
|
|
@@ -1847,7 +1735,6 @@ const registerComponentDependency = (
|
|
|
1847
1735
|
dependencyPath: string[]
|
|
1848
1736
|
) => {
|
|
1849
1737
|
const fullComponentId = `${stateKey}////${componentId}`;
|
|
1850
|
-
const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
|
|
1851
1738
|
|
|
1852
1739
|
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1853
1740
|
const component = rootMeta?.components?.get(fullComponentId);
|
|
@@ -1871,8 +1758,7 @@ const notifySelectionComponents = (
|
|
|
1871
1758
|
parentPath: string[],
|
|
1872
1759
|
currentSelected?: string | undefined
|
|
1873
1760
|
) => {
|
|
1874
|
-
const
|
|
1875
|
-
const rootMeta = store.getShadowMetadata(stateKey, []);
|
|
1761
|
+
const rootMeta = getShadowMetadata(stateKey, []);
|
|
1876
1762
|
const notifiedComponents = new Set<string>();
|
|
1877
1763
|
|
|
1878
1764
|
// Handle "all" reactive components first
|
|
@@ -1889,20 +1775,18 @@ const notifySelectionComponents = (
|
|
|
1889
1775
|
});
|
|
1890
1776
|
}
|
|
1891
1777
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1778
|
+
getShadowMetadata(stateKey, [
|
|
1779
|
+
...parentPath,
|
|
1780
|
+
'getSelected',
|
|
1781
|
+
])?.pathComponents?.forEach((componentId) => {
|
|
1782
|
+
const thisComp = rootMeta?.components?.get(componentId);
|
|
1783
|
+
thisComp?.forceUpdate();
|
|
1784
|
+
});
|
|
1898
1785
|
|
|
1899
|
-
const parentMeta =
|
|
1786
|
+
const parentMeta = getShadowMetadata(stateKey, parentPath);
|
|
1900
1787
|
for (let arrayKey of parentMeta?.arrayKeys || []) {
|
|
1901
1788
|
const key = arrayKey + '.selected';
|
|
1902
|
-
const selectedItem =
|
|
1903
|
-
stateKey,
|
|
1904
|
-
key.split('.').slice(1)
|
|
1905
|
-
);
|
|
1789
|
+
const selectedItem = getShadowMetadata(stateKey, key.split('.').slice(1));
|
|
1906
1790
|
if (arrayKey == currentSelected) {
|
|
1907
1791
|
selectedItem?.pathComponents?.forEach((componentId) => {
|
|
1908
1792
|
const thisComp = rootMeta?.components?.get(componentId);
|
|
@@ -1911,6 +1795,28 @@ const notifySelectionComponents = (
|
|
|
1911
1795
|
}
|
|
1912
1796
|
}
|
|
1913
1797
|
};
|
|
1798
|
+
function getScopedData(stateKey: string, path: string[], meta?: MetaData) {
|
|
1799
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
1800
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
1801
|
+
const arrayKeys = meta?.arrayViews?.[arrayPathKey];
|
|
1802
|
+
|
|
1803
|
+
// FIX: If the derived view is empty, return an empty array directly.
|
|
1804
|
+
if (Array.isArray(arrayKeys) && arrayKeys.length === 0) {
|
|
1805
|
+
return {
|
|
1806
|
+
shadowMeta,
|
|
1807
|
+
value: [],
|
|
1808
|
+
arrayKeys: shadowMeta?.arrayKeys,
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
const value = getShadowValue(stateKey, path, arrayKeys);
|
|
1813
|
+
|
|
1814
|
+
return {
|
|
1815
|
+
shadowMeta,
|
|
1816
|
+
value,
|
|
1817
|
+
arrayKeys: shadowMeta?.arrayKeys,
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1914
1820
|
|
|
1915
1821
|
function createProxyHandler<T>(
|
|
1916
1822
|
stateKey: string,
|
|
@@ -1921,7 +1827,57 @@ function createProxyHandler<T>(
|
|
|
1921
1827
|
const proxyCache = new Map<string, any>();
|
|
1922
1828
|
let stateVersion = 0;
|
|
1923
1829
|
|
|
1924
|
-
|
|
1830
|
+
const methodNames = new Set([
|
|
1831
|
+
'sync',
|
|
1832
|
+
'getStatus',
|
|
1833
|
+
'removeStorage',
|
|
1834
|
+
'showValidationErrors',
|
|
1835
|
+
'getSelected',
|
|
1836
|
+
'getSelectedIndex',
|
|
1837
|
+
'clearSelected',
|
|
1838
|
+
'useVirtualView',
|
|
1839
|
+
'stateMap',
|
|
1840
|
+
'$stateMap',
|
|
1841
|
+
'stateFind',
|
|
1842
|
+
'stateFilter',
|
|
1843
|
+
'stateSort',
|
|
1844
|
+
'stream',
|
|
1845
|
+
'stateList',
|
|
1846
|
+
'stateFlattenOn',
|
|
1847
|
+
'index',
|
|
1848
|
+
'last',
|
|
1849
|
+
'insert',
|
|
1850
|
+
'uniqueInsert',
|
|
1851
|
+
'cut',
|
|
1852
|
+
'cutSelected',
|
|
1853
|
+
'cutByValue',
|
|
1854
|
+
'toggleByValue',
|
|
1855
|
+
'findWith',
|
|
1856
|
+
'cutThis',
|
|
1857
|
+
'get',
|
|
1858
|
+
'getState',
|
|
1859
|
+
'$derive',
|
|
1860
|
+
'$get',
|
|
1861
|
+
'lastSynced',
|
|
1862
|
+
'getLocalStorage',
|
|
1863
|
+
'isSelected',
|
|
1864
|
+
'setSelected',
|
|
1865
|
+
'toggleSelected',
|
|
1866
|
+
'_componentId',
|
|
1867
|
+
'addZodValidation',
|
|
1868
|
+
'clearZodValidation',
|
|
1869
|
+
'applyJsonPatch',
|
|
1870
|
+
'getComponents',
|
|
1871
|
+
'getAllFormRefs',
|
|
1872
|
+
'getFormRef',
|
|
1873
|
+
'validationWrapper',
|
|
1874
|
+
'_stateKey',
|
|
1875
|
+
'_path',
|
|
1876
|
+
'update',
|
|
1877
|
+
'toggle',
|
|
1878
|
+
'formElement',
|
|
1879
|
+
// Add ANY other method names here
|
|
1880
|
+
]);
|
|
1925
1881
|
|
|
1926
1882
|
function rebuildStateShape({
|
|
1927
1883
|
path = [],
|
|
@@ -1933,64 +1889,34 @@ function createProxyHandler<T>(
|
|
|
1933
1889
|
meta?: MetaData;
|
|
1934
1890
|
}): any {
|
|
1935
1891
|
const derivationSignature = meta
|
|
1936
|
-
? JSON.stringify(meta.
|
|
1892
|
+
? JSON.stringify(meta.arrayViews || meta.transforms)
|
|
1937
1893
|
: '';
|
|
1938
1894
|
const cacheKey = path.join('.') + ':' + derivationSignature;
|
|
1939
|
-
console.log('PROXY CACHE KEY ', cacheKey);
|
|
1940
1895
|
if (proxyCache.has(cacheKey)) {
|
|
1941
|
-
console.log('PROXY CACHE HIT');
|
|
1942
1896
|
return proxyCache.get(cacheKey);
|
|
1943
1897
|
}
|
|
1944
1898
|
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
1945
1899
|
|
|
1946
|
-
type CallableStateObject<T> = {
|
|
1947
|
-
(): T;
|
|
1948
|
-
} & {
|
|
1949
|
-
[key: string]: any;
|
|
1950
|
-
};
|
|
1951
|
-
|
|
1952
|
-
const baseFunction = function () {
|
|
1953
|
-
return getGlobalStore().getShadowValue(stateKey, path);
|
|
1954
|
-
} as unknown as CallableStateObject<T>;
|
|
1955
|
-
|
|
1956
1900
|
// We attach baseObj properties *inside* the get trap now to avoid recursion
|
|
1957
1901
|
// This is a placeholder for the proxy.
|
|
1958
1902
|
|
|
1959
1903
|
const handler = {
|
|
1960
1904
|
get(target: any, prop: string) {
|
|
1905
|
+
if (path.length === 0 && prop in rootLevelMethods) {
|
|
1906
|
+
return rootLevelMethods[prop as keyof typeof rootLevelMethods];
|
|
1907
|
+
}
|
|
1908
|
+
if (!methodNames.has(prop)) {
|
|
1909
|
+
const nextPath = [...path, prop];
|
|
1910
|
+
return rebuildStateShape({
|
|
1911
|
+
path: nextPath,
|
|
1912
|
+
componentId: componentId!,
|
|
1913
|
+
meta,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1961
1916
|
if (prop === '_rebuildStateShape') {
|
|
1962
1917
|
return rebuildStateShape;
|
|
1963
1918
|
}
|
|
1964
|
-
const baseObjProps = Object.getOwnPropertyNames(baseObj);
|
|
1965
|
-
if (baseObjProps.includes(prop) && path.length === 0) {
|
|
1966
|
-
return (baseObj as any)[prop];
|
|
1967
|
-
}
|
|
1968
|
-
// ^--------- END OF FIX ---------^
|
|
1969
|
-
|
|
1970
|
-
if (prop === 'getDifferences') {
|
|
1971
|
-
return () => {
|
|
1972
|
-
const shadowMeta = getGlobalStore
|
|
1973
|
-
.getState()
|
|
1974
|
-
.getShadowMetadata(stateKey, []);
|
|
1975
|
-
const currentState = getGlobalStore
|
|
1976
|
-
.getState()
|
|
1977
|
-
.getShadowValue(stateKey);
|
|
1978
|
-
|
|
1979
|
-
// Use the appropriate base state for comparison
|
|
1980
|
-
let baseState;
|
|
1981
|
-
if (
|
|
1982
|
-
shadowMeta?.stateSource === 'server' &&
|
|
1983
|
-
shadowMeta.baseServerState
|
|
1984
|
-
) {
|
|
1985
|
-
baseState = shadowMeta.baseServerState;
|
|
1986
|
-
} else {
|
|
1987
|
-
baseState =
|
|
1988
|
-
getGlobalStore.getState().initialStateGlobal[stateKey];
|
|
1989
|
-
}
|
|
1990
1919
|
|
|
1991
|
-
return getDifferences(currentState, baseState);
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
1920
|
if (prop === 'sync' && path.length === 0) {
|
|
1995
1921
|
return async function () {
|
|
1996
1922
|
const options = getGlobalStore
|
|
@@ -2031,7 +1957,7 @@ function createProxyHandler<T>(
|
|
|
2031
1957
|
const shadowMeta = getGlobalStore
|
|
2032
1958
|
.getState()
|
|
2033
1959
|
.getShadowMetadata(stateKey, []);
|
|
2034
|
-
|
|
1960
|
+
setShadowMetadata(stateKey, [], {
|
|
2035
1961
|
...shadowMeta,
|
|
2036
1962
|
isDirty: false,
|
|
2037
1963
|
lastServerSync: Date.now(),
|
|
@@ -2055,56 +1981,46 @@ function createProxyHandler<T>(
|
|
|
2055
1981
|
// Fixed getStatus function in createProxyHandler
|
|
2056
1982
|
if (prop === '_status' || prop === 'getStatus') {
|
|
2057
1983
|
const getStatusFunc = () => {
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
.getShadowMetadata(stateKey, path);
|
|
2061
|
-
const value = getGlobalStore
|
|
2062
|
-
.getState()
|
|
2063
|
-
.getShadowValue(stateKeyPathKey);
|
|
1984
|
+
// ✅ Use the optimized helper to get all data in one efficient call
|
|
1985
|
+
const { shadowMeta, value } = getScopedData(stateKey, path, meta);
|
|
2064
1986
|
|
|
2065
|
-
// Priority 1: Explicitly dirty items
|
|
1987
|
+
// Priority 1: Explicitly dirty items. This is the most important status.
|
|
2066
1988
|
if (shadowMeta?.isDirty === true) {
|
|
2067
1989
|
return 'dirty';
|
|
2068
1990
|
}
|
|
2069
1991
|
|
|
2070
|
-
// Priority 2:
|
|
2071
|
-
if
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
1992
|
+
// ✅ Priority 2: Synced items. This condition is now cleaner.
|
|
1993
|
+
// An item is considered synced if it came from the server OR was explicitly
|
|
1994
|
+
// marked as not dirty (isDirty: false), covering all sync-related cases.
|
|
1995
|
+
if (
|
|
1996
|
+
shadowMeta?.stateSource === 'server' ||
|
|
1997
|
+
shadowMeta?.isDirty === false
|
|
1998
|
+
) {
|
|
2077
1999
|
return 'synced';
|
|
2078
2000
|
}
|
|
2079
2001
|
|
|
2080
|
-
// Priority
|
|
2002
|
+
// Priority 3: Items restored from localStorage.
|
|
2081
2003
|
if (shadowMeta?.stateSource === 'localStorage') {
|
|
2082
2004
|
return 'restored';
|
|
2083
2005
|
}
|
|
2084
2006
|
|
|
2085
|
-
// Priority
|
|
2007
|
+
// Priority 4: Items from default/initial state.
|
|
2086
2008
|
if (shadowMeta?.stateSource === 'default') {
|
|
2087
2009
|
return 'fresh';
|
|
2088
2010
|
}
|
|
2089
2011
|
|
|
2090
|
-
//
|
|
2091
|
-
// Look up the tree to see if parent has server source
|
|
2092
|
-
const rootMeta = getGlobalStore
|
|
2093
|
-
.getState()
|
|
2094
|
-
.getShadowMetadata(stateKey, []);
|
|
2095
|
-
if (rootMeta?.stateSource === 'server' && !shadowMeta?.isDirty) {
|
|
2096
|
-
return 'synced';
|
|
2097
|
-
}
|
|
2012
|
+
// ✅ REMOVED the redundant "root" check. The item's own `stateSource` is sufficient.
|
|
2098
2013
|
|
|
2099
|
-
// Priority
|
|
2014
|
+
// Priority 5: A value exists but has no metadata. This is a fallback.
|
|
2100
2015
|
if (value !== undefined && !shadowMeta) {
|
|
2101
2016
|
return 'fresh';
|
|
2102
2017
|
}
|
|
2103
2018
|
|
|
2104
|
-
// Fallback
|
|
2019
|
+
// Fallback if no other condition is met.
|
|
2105
2020
|
return 'unknown';
|
|
2106
2021
|
};
|
|
2107
2022
|
|
|
2023
|
+
// This part remains the same
|
|
2108
2024
|
return prop === '_status' ? getStatusFunc() : getStatusFunc;
|
|
2109
2025
|
}
|
|
2110
2026
|
if (prop === 'removeStorage') {
|
|
@@ -2121,14 +2037,15 @@ function createProxyHandler<T>(
|
|
|
2121
2037
|
}
|
|
2122
2038
|
if (prop === 'showValidationErrors') {
|
|
2123
2039
|
return () => {
|
|
2124
|
-
const
|
|
2125
|
-
.getState()
|
|
2126
|
-
.getShadowMetadata(stateKey, path);
|
|
2040
|
+
const { shadowMeta } = getScopedData(stateKey, path, meta);
|
|
2127
2041
|
if (
|
|
2128
|
-
|
|
2129
|
-
|
|
2042
|
+
shadowMeta?.validation?.status === 'INVALID' &&
|
|
2043
|
+
shadowMeta.validation.errors.length > 0
|
|
2130
2044
|
) {
|
|
2131
|
-
|
|
2045
|
+
// Return only error-severity messages (not warnings)
|
|
2046
|
+
return shadowMeta.validation.errors
|
|
2047
|
+
.filter((err) => err.severity === 'error')
|
|
2048
|
+
.map((err) => err.message);
|
|
2132
2049
|
}
|
|
2133
2050
|
return [];
|
|
2134
2051
|
};
|
|
@@ -2136,55 +2053,77 @@ function createProxyHandler<T>(
|
|
|
2136
2053
|
|
|
2137
2054
|
if (prop === 'getSelected') {
|
|
2138
2055
|
return () => {
|
|
2139
|
-
const
|
|
2056
|
+
const arrayKey = [stateKey, ...path].join('.');
|
|
2140
2057
|
registerComponentDependency(stateKey, componentId, [
|
|
2141
2058
|
...path,
|
|
2142
2059
|
'getSelected',
|
|
2143
2060
|
]);
|
|
2144
2061
|
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2147
|
-
|
|
2062
|
+
const selectedItemKey = getGlobalStore
|
|
2063
|
+
.getState()
|
|
2064
|
+
.selectedIndicesMap.get(arrayKey);
|
|
2065
|
+
if (!selectedItemKey) {
|
|
2148
2066
|
return undefined;
|
|
2149
2067
|
}
|
|
2150
2068
|
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
return undefined;
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2069
|
+
const viewKey = path.join('.');
|
|
2070
|
+
const currentViewIds = meta?.arrayViews?.[viewKey];
|
|
2071
|
+
const selectedItemId = selectedItemKey.split('.').pop();
|
|
2157
2072
|
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2073
|
+
// FIX: Only return the selected item if it exists in the current filtered/sorted view.
|
|
2074
|
+
if (currentViewIds && !currentViewIds.includes(selectedItemId!)) {
|
|
2075
|
+
return undefined;
|
|
2076
|
+
}
|
|
2161
2077
|
|
|
2162
|
-
|
|
2078
|
+
const value = getShadowValue(
|
|
2079
|
+
stateKey,
|
|
2080
|
+
selectedItemKey.split('.').slice(1)
|
|
2081
|
+
);
|
|
2082
|
+
if (value === undefined) {
|
|
2163
2083
|
return undefined;
|
|
2164
2084
|
}
|
|
2165
2085
|
|
|
2166
2086
|
return rebuildStateShape({
|
|
2167
2087
|
path: selectedItemKey.split('.').slice(1) as string[],
|
|
2168
2088
|
componentId: componentId!,
|
|
2089
|
+
meta,
|
|
2169
2090
|
});
|
|
2170
2091
|
};
|
|
2171
2092
|
}
|
|
2172
2093
|
if (prop === 'getSelectedIndex') {
|
|
2173
2094
|
return () => {
|
|
2174
|
-
|
|
2095
|
+
// Key for the array in the global selection map (e.g., "myState.products")
|
|
2096
|
+
const arrayKey = stateKey + '.' + path.join('.');
|
|
2097
|
+
// Key for this specific view in the meta object (e.g., "products")
|
|
2098
|
+
const viewKey = path.join('.');
|
|
2099
|
+
|
|
2100
|
+
// Get the full path of the selected item (e.g., "myState.products.id:abc")
|
|
2101
|
+
const selectedItemKey = getGlobalStore
|
|
2175
2102
|
.getState()
|
|
2176
|
-
.
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2103
|
+
.selectedIndicesMap.get(arrayKey);
|
|
2104
|
+
|
|
2105
|
+
if (!selectedItemKey) {
|
|
2106
|
+
return -1; // Nothing is selected for this array.
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Get the list of item IDs for the current filtered/sorted view.
|
|
2110
|
+
const { keys: viewIds } = getArrayData(stateKey, path, meta);
|
|
2111
|
+
|
|
2112
|
+
if (!viewIds) {
|
|
2113
|
+
return -1; // Should not happen if it's an array, but a safe guard.
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// FIX: Extract just the ID from the full selected item path.
|
|
2117
|
+
const selectedId = selectedItemKey.split('.').pop();
|
|
2180
2118
|
|
|
2181
|
-
|
|
2119
|
+
// Return the index of that ID within the current view's list of IDs.
|
|
2120
|
+
return (viewIds as string[]).indexOf(selectedId as string);
|
|
2182
2121
|
};
|
|
2183
2122
|
}
|
|
2184
2123
|
if (prop === 'clearSelected') {
|
|
2185
2124
|
notifySelectionComponents(stateKey, path);
|
|
2186
2125
|
return () => {
|
|
2187
|
-
|
|
2126
|
+
clearSelectedIndex({
|
|
2188
2127
|
arrayKey: stateKey + '.' + path.join('.'),
|
|
2189
2128
|
});
|
|
2190
2129
|
};
|
|
@@ -2209,6 +2148,13 @@ function createProxyHandler<T>(
|
|
|
2209
2148
|
const [rerender, forceUpdate] = useState({});
|
|
2210
2149
|
const initialScrollRef = useRef(true);
|
|
2211
2150
|
|
|
2151
|
+
useEffect(() => {
|
|
2152
|
+
const interval = setInterval(() => {
|
|
2153
|
+
forceUpdate({});
|
|
2154
|
+
}, 1000);
|
|
2155
|
+
return () => clearInterval(interval);
|
|
2156
|
+
}, []);
|
|
2157
|
+
|
|
2212
2158
|
// Scroll state management
|
|
2213
2159
|
const scrollStateRef = useRef({
|
|
2214
2160
|
isUserScrolling: false,
|
|
@@ -2221,59 +2167,28 @@ function createProxyHandler<T>(
|
|
|
2221
2167
|
const measurementCache = useRef(
|
|
2222
2168
|
new Map<string, { height: number; offset: number }>()
|
|
2223
2169
|
);
|
|
2170
|
+
const { keys: arrayKeys } = getArrayData(stateKey, path, meta);
|
|
2224
2171
|
|
|
2225
|
-
//
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
behavior: initialScrollRef.current ? 'instant' : 'smooth',
|
|
2238
|
-
});
|
|
2239
|
-
}, [rerender, stickToBottom]);
|
|
2240
|
-
|
|
2241
|
-
const arrayKeys =
|
|
2242
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2243
|
-
?.arrayKeys || [];
|
|
2244
|
-
|
|
2245
|
-
// Calculate total height and offsets
|
|
2246
|
-
const { totalHeight, itemOffsets } = useMemo(() => {
|
|
2247
|
-
let runningOffset = 0;
|
|
2248
|
-
const offsets = new Map<
|
|
2249
|
-
string,
|
|
2250
|
-
{ height: number; offset: number }
|
|
2251
|
-
>();
|
|
2252
|
-
const allItemKeys =
|
|
2253
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2254
|
-
?.arrayKeys || [];
|
|
2255
|
-
|
|
2256
|
-
allItemKeys.forEach((itemKey) => {
|
|
2257
|
-
const itemPath = itemKey.split('.').slice(1);
|
|
2258
|
-
const measuredHeight =
|
|
2259
|
-
getGlobalStore
|
|
2260
|
-
.getState()
|
|
2261
|
-
.getShadowMetadata(stateKey, itemPath)?.virtualizer
|
|
2262
|
-
?.itemHeight || itemHeight;
|
|
2263
|
-
|
|
2264
|
-
offsets.set(itemKey, {
|
|
2265
|
-
height: measuredHeight,
|
|
2266
|
-
offset: runningOffset,
|
|
2172
|
+
// Subscribe to state changes like stateList does
|
|
2173
|
+
useEffect(() => {
|
|
2174
|
+
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
2175
|
+
const unsubscribe = getGlobalStore
|
|
2176
|
+
.getState()
|
|
2177
|
+
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2178
|
+
if (e.type === 'GET_SELECTED') {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
if (e.type === 'SERVER_STATE_UPDATE') {
|
|
2182
|
+
// forceUpdate({});
|
|
2183
|
+
}
|
|
2267
2184
|
});
|
|
2268
2185
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
return { totalHeight: runningOffset, itemOffsets: offsets };
|
|
2274
|
-
}, [arrayKeys.length, itemHeight]);
|
|
2186
|
+
return () => {
|
|
2187
|
+
unsubscribe();
|
|
2188
|
+
};
|
|
2189
|
+
}, [componentId, stateKey, path.join('.')]);
|
|
2275
2190
|
|
|
2276
|
-
//
|
|
2191
|
+
// YOUR ORIGINAL INITIAL POSITIONING - KEEPING EXACTLY AS IS
|
|
2277
2192
|
useLayoutEffect(() => {
|
|
2278
2193
|
if (
|
|
2279
2194
|
stickToBottom &&
|
|
@@ -2284,7 +2199,6 @@ function createProxyHandler<T>(
|
|
|
2284
2199
|
) {
|
|
2285
2200
|
const container = containerRef.current;
|
|
2286
2201
|
|
|
2287
|
-
// Wait for container to have dimensions
|
|
2288
2202
|
const waitForContainer = () => {
|
|
2289
2203
|
if (container.clientHeight > 0) {
|
|
2290
2204
|
const visibleCount = Math.ceil(
|
|
@@ -2298,13 +2212,11 @@ function createProxyHandler<T>(
|
|
|
2298
2212
|
|
|
2299
2213
|
setRange({ startIndex, endIndex });
|
|
2300
2214
|
|
|
2301
|
-
// Ensure scroll after range is set
|
|
2302
2215
|
requestAnimationFrame(() => {
|
|
2303
2216
|
scrollToBottom('instant');
|
|
2304
|
-
initialScrollRef.current = false;
|
|
2217
|
+
initialScrollRef.current = false;
|
|
2305
2218
|
});
|
|
2306
2219
|
} else {
|
|
2307
|
-
// Container not ready, try again
|
|
2308
2220
|
requestAnimationFrame(waitForContainer);
|
|
2309
2221
|
}
|
|
2310
2222
|
};
|
|
@@ -2313,7 +2225,16 @@ function createProxyHandler<T>(
|
|
|
2313
2225
|
}
|
|
2314
2226
|
}, [arrayKeys.length, stickToBottom, itemHeight, overscan]);
|
|
2315
2227
|
|
|
2316
|
-
|
|
2228
|
+
const rangeRef = useRef(range);
|
|
2229
|
+
useLayoutEffect(() => {
|
|
2230
|
+
rangeRef.current = range;
|
|
2231
|
+
}, [range]);
|
|
2232
|
+
|
|
2233
|
+
const arrayKeysRef = useRef(arrayKeys);
|
|
2234
|
+
useLayoutEffect(() => {
|
|
2235
|
+
arrayKeysRef.current = arrayKeys;
|
|
2236
|
+
}, [arrayKeys]);
|
|
2237
|
+
|
|
2317
2238
|
const handleScroll = useCallback(() => {
|
|
2318
2239
|
const container = containerRef.current;
|
|
2319
2240
|
if (!container) return;
|
|
@@ -2357,9 +2278,14 @@ function createProxyHandler<T>(
|
|
|
2357
2278
|
break;
|
|
2358
2279
|
}
|
|
2359
2280
|
}
|
|
2360
|
-
|
|
2281
|
+
console.log(
|
|
2282
|
+
'hadnlescroll ',
|
|
2283
|
+
measurementCache.current,
|
|
2284
|
+
newStartIndex,
|
|
2285
|
+
range
|
|
2286
|
+
);
|
|
2361
2287
|
// Only update if range actually changed
|
|
2362
|
-
if (newStartIndex !== range.startIndex) {
|
|
2288
|
+
if (newStartIndex !== range.startIndex && range.startIndex != 0) {
|
|
2363
2289
|
const visibleCount = Math.ceil(clientHeight / itemHeight);
|
|
2364
2290
|
setRange({
|
|
2365
2291
|
startIndex: Math.max(0, newStartIndex - overscan),
|
|
@@ -2380,36 +2306,34 @@ function createProxyHandler<T>(
|
|
|
2380
2306
|
// Set up scroll listener
|
|
2381
2307
|
useEffect(() => {
|
|
2382
2308
|
const container = containerRef.current;
|
|
2383
|
-
if (!container
|
|
2309
|
+
if (!container) return;
|
|
2384
2310
|
|
|
2385
2311
|
container.addEventListener('scroll', handleScroll, {
|
|
2386
2312
|
passive: true,
|
|
2387
2313
|
});
|
|
2388
|
-
|
|
2389
2314
|
return () => {
|
|
2390
2315
|
container.removeEventListener('scroll', handleScroll);
|
|
2391
2316
|
};
|
|
2392
2317
|
}, [handleScroll, stickToBottom]);
|
|
2318
|
+
|
|
2319
|
+
// YOUR ORIGINAL SCROLL TO BOTTOM FUNCTION - KEEPING EXACTLY AS IS
|
|
2393
2320
|
const scrollToBottom = useCallback(
|
|
2394
2321
|
(behavior: ScrollBehavior = 'smooth') => {
|
|
2395
2322
|
const container = containerRef.current;
|
|
2396
2323
|
if (!container) return;
|
|
2397
2324
|
|
|
2398
|
-
// Reset scroll state
|
|
2399
2325
|
scrollStateRef.current.isUserScrolling = false;
|
|
2400
2326
|
scrollStateRef.current.isNearBottom = true;
|
|
2401
2327
|
scrollStateRef.current.scrollUpCount = 0;
|
|
2402
2328
|
|
|
2403
2329
|
const performScroll = () => {
|
|
2404
|
-
// Multiple attempts to ensure we hit the bottom
|
|
2405
2330
|
const attemptScroll = (attempts = 0) => {
|
|
2406
|
-
if (attempts > 5) return;
|
|
2331
|
+
if (attempts > 5) return;
|
|
2407
2332
|
|
|
2408
2333
|
const currentHeight = container.scrollHeight;
|
|
2409
2334
|
const currentScroll = container.scrollTop;
|
|
2410
2335
|
const clientHeight = container.clientHeight;
|
|
2411
2336
|
|
|
2412
|
-
// Check if we're already at the bottom
|
|
2413
2337
|
if (currentScroll + clientHeight >= currentHeight - 1) {
|
|
2414
2338
|
return;
|
|
2415
2339
|
}
|
|
@@ -2419,12 +2343,10 @@ function createProxyHandler<T>(
|
|
|
2419
2343
|
behavior: behavior,
|
|
2420
2344
|
});
|
|
2421
2345
|
|
|
2422
|
-
// In slow environments, check again after a short delay
|
|
2423
2346
|
setTimeout(() => {
|
|
2424
2347
|
const newHeight = container.scrollHeight;
|
|
2425
2348
|
const newScroll = container.scrollTop;
|
|
2426
2349
|
|
|
2427
|
-
// If height changed or we're not at bottom, try again
|
|
2428
2350
|
if (
|
|
2429
2351
|
newHeight !== currentHeight ||
|
|
2430
2352
|
newScroll + clientHeight < newHeight - 1
|
|
@@ -2437,11 +2359,9 @@ function createProxyHandler<T>(
|
|
|
2437
2359
|
attemptScroll();
|
|
2438
2360
|
};
|
|
2439
2361
|
|
|
2440
|
-
// Use requestIdleCallback for better performance in slow environments
|
|
2441
2362
|
if ('requestIdleCallback' in window) {
|
|
2442
2363
|
requestIdleCallback(performScroll, { timeout: 100 });
|
|
2443
2364
|
} else {
|
|
2444
|
-
// Fallback to rAF chain
|
|
2445
2365
|
requestAnimationFrame(() => {
|
|
2446
2366
|
requestAnimationFrame(performScroll);
|
|
2447
2367
|
});
|
|
@@ -2449,15 +2369,14 @@ function createProxyHandler<T>(
|
|
|
2449
2369
|
},
|
|
2450
2370
|
[]
|
|
2451
2371
|
);
|
|
2452
|
-
|
|
2453
|
-
//
|
|
2372
|
+
|
|
2373
|
+
// YOUR ORIGINAL AUTO-SCROLL EFFECTS - KEEPING ALL OF THEM
|
|
2454
2374
|
useEffect(() => {
|
|
2455
2375
|
if (!stickToBottom || !containerRef.current) return;
|
|
2456
2376
|
|
|
2457
2377
|
const container = containerRef.current;
|
|
2458
2378
|
const scrollState = scrollStateRef.current;
|
|
2459
2379
|
|
|
2460
|
-
// Debounced scroll function
|
|
2461
2380
|
let scrollTimeout: NodeJS.Timeout;
|
|
2462
2381
|
const debouncedScrollToBottom = () => {
|
|
2463
2382
|
clearTimeout(scrollTimeout);
|
|
@@ -2473,7 +2392,6 @@ function createProxyHandler<T>(
|
|
|
2473
2392
|
}, 100);
|
|
2474
2393
|
};
|
|
2475
2394
|
|
|
2476
|
-
// Single MutationObserver for all DOM changes
|
|
2477
2395
|
const observer = new MutationObserver(() => {
|
|
2478
2396
|
if (!scrollState.isUserScrolling) {
|
|
2479
2397
|
debouncedScrollToBottom();
|
|
@@ -2484,24 +2402,10 @@ function createProxyHandler<T>(
|
|
|
2484
2402
|
childList: true,
|
|
2485
2403
|
subtree: true,
|
|
2486
2404
|
attributes: true,
|
|
2487
|
-
attributeFilter: ['style', 'class'],
|
|
2405
|
+
attributeFilter: ['style', 'class'],
|
|
2488
2406
|
});
|
|
2489
2407
|
|
|
2490
|
-
// Handle image loads with event delegation
|
|
2491
|
-
const handleImageLoad = (e: Event) => {
|
|
2492
|
-
if (
|
|
2493
|
-
e.target instanceof HTMLImageElement &&
|
|
2494
|
-
!scrollState.isUserScrolling
|
|
2495
|
-
) {
|
|
2496
|
-
debouncedScrollToBottom();
|
|
2497
|
-
}
|
|
2498
|
-
};
|
|
2499
|
-
|
|
2500
|
-
container.addEventListener('load', handleImageLoad, true);
|
|
2501
|
-
|
|
2502
|
-
// Initial scroll with proper timing
|
|
2503
2408
|
if (initialScrollRef.current) {
|
|
2504
|
-
// For initial load, wait for next tick to ensure DOM is ready
|
|
2505
2409
|
setTimeout(() => {
|
|
2506
2410
|
scrollToBottom('instant');
|
|
2507
2411
|
}, 0);
|
|
@@ -2512,33 +2416,28 @@ function createProxyHandler<T>(
|
|
|
2512
2416
|
return () => {
|
|
2513
2417
|
clearTimeout(scrollTimeout);
|
|
2514
2418
|
observer.disconnect();
|
|
2515
|
-
container.removeEventListener('load', handleImageLoad, true);
|
|
2516
2419
|
};
|
|
2517
2420
|
}, [stickToBottom, arrayKeys.length, scrollToBottom]);
|
|
2518
|
-
|
|
2421
|
+
|
|
2422
|
+
// Create virtual state - NO NEED to get values, only IDs!
|
|
2519
2423
|
const virtualState = useMemo(() => {
|
|
2520
|
-
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
const currentKeys =
|
|
2525
|
-
store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
|
|
2526
|
-
|
|
2527
|
-
const slicedArray = sourceArray.slice(
|
|
2528
|
-
range.startIndex,
|
|
2529
|
-
range.endIndex + 1
|
|
2530
|
-
);
|
|
2531
|
-
const slicedIds = currentKeys.slice(
|
|
2532
|
-
range.startIndex,
|
|
2533
|
-
range.endIndex + 1
|
|
2534
|
-
);
|
|
2424
|
+
// 2. Physically slice the corresponding keys.
|
|
2425
|
+
const slicedKeys = Array.isArray(arrayKeys)
|
|
2426
|
+
? arrayKeys.slice(range.startIndex, range.endIndex + 1)
|
|
2427
|
+
: [];
|
|
2535
2428
|
|
|
2429
|
+
// Use the same keying as getArrayData (empty string for root)
|
|
2430
|
+
const arrayPath = path.length > 0 ? path.join('.') : 'root';
|
|
2536
2431
|
return rebuildStateShape({
|
|
2537
2432
|
path,
|
|
2538
2433
|
componentId: componentId!,
|
|
2539
|
-
meta: {
|
|
2434
|
+
meta: {
|
|
2435
|
+
...meta,
|
|
2436
|
+
arrayViews: { [arrayPath]: slicedKeys },
|
|
2437
|
+
serverStateIsUpStream: true,
|
|
2438
|
+
},
|
|
2540
2439
|
});
|
|
2541
|
-
}, [range.startIndex, range.endIndex, arrayKeys
|
|
2440
|
+
}, [range.startIndex, range.endIndex, arrayKeys, meta]);
|
|
2542
2441
|
|
|
2543
2442
|
return {
|
|
2544
2443
|
virtualState,
|
|
@@ -2546,15 +2445,14 @@ function createProxyHandler<T>(
|
|
|
2546
2445
|
outer: {
|
|
2547
2446
|
ref: containerRef,
|
|
2548
2447
|
style: {
|
|
2549
|
-
overflowY: 'auto',
|
|
2448
|
+
overflowY: 'auto' as const,
|
|
2550
2449
|
height: '100%',
|
|
2551
|
-
position: 'relative',
|
|
2450
|
+
position: 'relative' as const,
|
|
2552
2451
|
},
|
|
2553
2452
|
},
|
|
2554
2453
|
inner: {
|
|
2555
2454
|
style: {
|
|
2556
|
-
|
|
2557
|
-
position: 'relative',
|
|
2455
|
+
position: 'relative' as const,
|
|
2558
2456
|
},
|
|
2559
2457
|
},
|
|
2560
2458
|
list: {
|
|
@@ -2583,150 +2481,84 @@ function createProxyHandler<T>(
|
|
|
2583
2481
|
}
|
|
2584
2482
|
if (prop === 'stateMap') {
|
|
2585
2483
|
return (
|
|
2586
|
-
callbackfn: (
|
|
2587
|
-
setter: any,
|
|
2588
|
-
index: number,
|
|
2589
|
-
|
|
2590
|
-
arraySetter: any
|
|
2591
|
-
) => void
|
|
2484
|
+
callbackfn: (setter: any, index: number, arraySetter: any) => void
|
|
2592
2485
|
) => {
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2486
|
+
// FIX: Use getArrayData to reliably get both the value and the keys of the current view.
|
|
2487
|
+
const { value: shadowValue, keys: arrayKeys } = getArrayData(
|
|
2488
|
+
stateKey,
|
|
2489
|
+
path,
|
|
2490
|
+
meta
|
|
2597
2491
|
);
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
// stateKeyPathKey
|
|
2602
|
-
// );
|
|
2603
|
-
// setArrayKeys(
|
|
2604
|
-
// getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2605
|
-
// );
|
|
2606
|
-
// });
|
|
2607
|
-
|
|
2608
|
-
const shadowValue = getGlobalStore
|
|
2609
|
-
.getState()
|
|
2610
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds) as any[];
|
|
2611
|
-
if (!arrayKeys) {
|
|
2612
|
-
throw new Error('No array keys found for mapping');
|
|
2492
|
+
|
|
2493
|
+
if (!arrayKeys || !Array.isArray(shadowValue)) {
|
|
2494
|
+
return []; // It's valid to map over an empty array.
|
|
2613
2495
|
}
|
|
2496
|
+
|
|
2614
2497
|
const arraySetter = rebuildStateShape({
|
|
2615
2498
|
path,
|
|
2616
2499
|
componentId: componentId!,
|
|
2617
2500
|
meta,
|
|
2618
2501
|
});
|
|
2619
2502
|
|
|
2620
|
-
return shadowValue.map((
|
|
2621
|
-
const
|
|
2503
|
+
return shadowValue.map((_item, index) => {
|
|
2504
|
+
const itemKey = arrayKeys[index];
|
|
2505
|
+
if (!itemKey) return undefined;
|
|
2506
|
+
|
|
2507
|
+
// FIX: Construct the correct path to the item in the original store.
|
|
2508
|
+
// The path is the array's path plus the specific item's unique key.
|
|
2509
|
+
const itemPath = [...path, itemKey];
|
|
2510
|
+
|
|
2622
2511
|
const itemSetter = rebuildStateShape({
|
|
2623
|
-
path: itemPath
|
|
2512
|
+
path: itemPath, // This now correctly points to the item in the shadow store.
|
|
2624
2513
|
componentId: componentId!,
|
|
2625
2514
|
meta,
|
|
2626
2515
|
});
|
|
2627
2516
|
|
|
2628
|
-
return callbackfn(
|
|
2629
|
-
itemSetter,
|
|
2630
|
-
index,
|
|
2631
|
-
|
|
2632
|
-
arraySetter
|
|
2633
|
-
);
|
|
2517
|
+
return callbackfn(itemSetter, index, arraySetter);
|
|
2634
2518
|
});
|
|
2635
2519
|
};
|
|
2636
2520
|
}
|
|
2637
2521
|
|
|
2638
|
-
if (prop === '$stateMap') {
|
|
2639
|
-
return (callbackfn: any) =>
|
|
2640
|
-
createElement(SignalMapRenderer, {
|
|
2641
|
-
proxy: {
|
|
2642
|
-
_stateKey: stateKey,
|
|
2643
|
-
_path: path,
|
|
2644
|
-
_mapFn: callbackfn,
|
|
2645
|
-
_meta: meta,
|
|
2646
|
-
},
|
|
2647
|
-
rebuildStateShape,
|
|
2648
|
-
});
|
|
2649
|
-
} // In createProxyHandler -> handler -> get -> if (Array.isArray(currentState))
|
|
2650
|
-
|
|
2651
|
-
if (prop === 'stateFind') {
|
|
2652
|
-
return (
|
|
2653
|
-
callbackfn: (value: any, index: number) => boolean
|
|
2654
|
-
): StateObject<any> | undefined => {
|
|
2655
|
-
// 1. Use the correct set of keys: filtered/sorted from meta, or all keys from the store.
|
|
2656
|
-
const arrayKeys =
|
|
2657
|
-
meta?.validIds ??
|
|
2658
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2659
|
-
?.arrayKeys;
|
|
2660
|
-
|
|
2661
|
-
if (!arrayKeys) {
|
|
2662
|
-
return undefined;
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
// 2. Iterate through the keys, get the value for each, and run the callback.
|
|
2666
|
-
for (let i = 0; i < arrayKeys.length; i++) {
|
|
2667
|
-
const itemKey = arrayKeys[i];
|
|
2668
|
-
if (!itemKey) continue; // Safety check
|
|
2669
|
-
|
|
2670
|
-
const itemValue = getGlobalStore
|
|
2671
|
-
.getState()
|
|
2672
|
-
.getShadowValue(itemKey);
|
|
2673
|
-
|
|
2674
|
-
// 3. If the callback returns true, we've found our item.
|
|
2675
|
-
if (callbackfn(itemValue, i)) {
|
|
2676
|
-
// Get the item's path relative to the stateKey (e.g., ['messages', '42'] -> ['42'])
|
|
2677
|
-
const itemPath = itemKey.split('.').slice(1);
|
|
2678
|
-
|
|
2679
|
-
// 4. Rebuild a new, fully functional StateObject for just that item and return it.
|
|
2680
|
-
return rebuildStateShape({
|
|
2681
|
-
path: itemPath,
|
|
2682
|
-
componentId: componentId,
|
|
2683
|
-
meta, // Pass along meta for potential further chaining
|
|
2684
|
-
});
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// 5. If the loop finishes without finding anything, return undefined.
|
|
2689
|
-
return undefined;
|
|
2690
|
-
};
|
|
2691
|
-
}
|
|
2692
2522
|
if (prop === 'stateFilter') {
|
|
2693
2523
|
return (callbackfn: (value: any, index: number) => boolean) => {
|
|
2694
|
-
const
|
|
2695
|
-
.getState()
|
|
2696
|
-
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
2697
|
-
if (!Array.isArray(currentState)) return [];
|
|
2698
|
-
const arrayKeys =
|
|
2699
|
-
meta?.validIds ??
|
|
2700
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
2701
|
-
?.arrayKeys;
|
|
2524
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2702
2525
|
|
|
2703
|
-
|
|
2704
|
-
|
|
2526
|
+
// ✅ FIX: Get keys from `getArrayData` which correctly resolves them from meta or the full list.
|
|
2527
|
+
const { keys: currentViewIds, value: array } = getArrayData(
|
|
2528
|
+
stateKey,
|
|
2529
|
+
path,
|
|
2530
|
+
meta
|
|
2531
|
+
);
|
|
2532
|
+
|
|
2533
|
+
if (!Array.isArray(array)) {
|
|
2534
|
+
throw new Error('stateFilter can only be used on arrays');
|
|
2705
2535
|
}
|
|
2706
2536
|
|
|
2707
|
-
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2537
|
+
// Filter the array and collect the IDs of the items that pass
|
|
2538
|
+
const filteredIds: string[] = [];
|
|
2539
|
+
array.forEach((item, index) => {
|
|
2540
|
+
if (callbackfn(item, index)) {
|
|
2541
|
+
// currentViewIds[index] is the original ID before filtering
|
|
2542
|
+
const id = currentViewIds[index];
|
|
2543
|
+
if (id) {
|
|
2544
|
+
filteredIds.push(id);
|
|
2714
2545
|
}
|
|
2715
|
-
return false;
|
|
2716
2546
|
}
|
|
2717
|
-
);
|
|
2547
|
+
});
|
|
2718
2548
|
|
|
2549
|
+
// The rest is the same...
|
|
2719
2550
|
return rebuildStateShape({
|
|
2720
2551
|
path,
|
|
2721
2552
|
componentId: componentId!,
|
|
2722
2553
|
meta: {
|
|
2723
|
-
|
|
2554
|
+
...meta,
|
|
2555
|
+
arrayViews: {
|
|
2556
|
+
...(meta?.arrayViews || {}),
|
|
2557
|
+
[arrayPathKey]: filteredIds,
|
|
2558
|
+
},
|
|
2724
2559
|
transforms: [
|
|
2725
2560
|
...(meta?.transforms || []),
|
|
2726
|
-
{
|
|
2727
|
-
type: 'filter',
|
|
2728
|
-
fn: callbackfn,
|
|
2729
|
-
},
|
|
2561
|
+
{ type: 'filter', fn: callbackfn, path },
|
|
2730
2562
|
],
|
|
2731
2563
|
},
|
|
2732
2564
|
});
|
|
@@ -2734,34 +2566,39 @@ function createProxyHandler<T>(
|
|
|
2734
2566
|
}
|
|
2735
2567
|
if (prop === 'stateSort') {
|
|
2736
2568
|
return (compareFn: (a: any, b: any) => number) => {
|
|
2737
|
-
const
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2569
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2570
|
+
|
|
2571
|
+
// FIX: Use the more robust `getArrayData` which always correctly resolves the keys for a view.
|
|
2572
|
+
const { value: currentArray, keys: currentViewIds } = getArrayData(
|
|
2573
|
+
stateKey,
|
|
2574
|
+
path,
|
|
2575
|
+
meta
|
|
2576
|
+
);
|
|
2577
|
+
|
|
2578
|
+
if (!Array.isArray(currentArray) || !currentViewIds) {
|
|
2746
2579
|
throw new Error('No array keys found for sorting');
|
|
2747
2580
|
}
|
|
2748
|
-
|
|
2581
|
+
|
|
2582
|
+
// ... (rest of the function is the same and now works)
|
|
2583
|
+
const itemsWithIds = currentArray.map((item, index) => ({
|
|
2749
2584
|
item,
|
|
2750
|
-
key:
|
|
2585
|
+
key: currentViewIds[index],
|
|
2751
2586
|
}));
|
|
2752
|
-
|
|
2753
|
-
itemsWithIds
|
|
2754
|
-
.sort((a, b) => compareFn(a.item, b.item))
|
|
2755
|
-
.filter(Boolean);
|
|
2587
|
+
itemsWithIds.sort((a, b) => compareFn(a.item, b.item));
|
|
2588
|
+
const sortedIds = itemsWithIds.map((i) => i.key as string);
|
|
2756
2589
|
|
|
2757
2590
|
return rebuildStateShape({
|
|
2758
2591
|
path,
|
|
2759
2592
|
componentId: componentId!,
|
|
2760
2593
|
meta: {
|
|
2761
|
-
|
|
2594
|
+
...meta,
|
|
2595
|
+
arrayViews: {
|
|
2596
|
+
...(meta?.arrayViews || {}),
|
|
2597
|
+
[arrayPathKey]: sortedIds,
|
|
2598
|
+
},
|
|
2762
2599
|
transforms: [
|
|
2763
2600
|
...(meta?.transforms || []),
|
|
2764
|
-
{ type: 'sort', fn: compareFn },
|
|
2601
|
+
{ type: 'sort', fn: compareFn, path },
|
|
2765
2602
|
],
|
|
2766
2603
|
},
|
|
2767
2604
|
});
|
|
@@ -2835,12 +2672,11 @@ function createProxyHandler<T>(
|
|
|
2835
2672
|
}
|
|
2836
2673
|
|
|
2837
2674
|
const streamId = uuidv4();
|
|
2838
|
-
const currentMeta =
|
|
2839
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path) || {};
|
|
2675
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
2840
2676
|
const streams = currentMeta.streams || new Map();
|
|
2841
2677
|
streams.set(streamId, { buffer, flushTimer });
|
|
2842
2678
|
|
|
2843
|
-
|
|
2679
|
+
setShadowMetadata(stateKey, path, {
|
|
2844
2680
|
...currentMeta,
|
|
2845
2681
|
streams,
|
|
2846
2682
|
});
|
|
@@ -2882,55 +2718,42 @@ function createProxyHandler<T>(
|
|
|
2882
2718
|
const StateListWrapper = () => {
|
|
2883
2719
|
const componentIdsRef = useRef<Map<string, string>>(new Map());
|
|
2884
2720
|
|
|
2885
|
-
const cacheKey =
|
|
2886
|
-
meta?.transforms && meta.transforms.length > 0
|
|
2887
|
-
? `${componentId}-${hashTransforms(meta.transforms)}`
|
|
2888
|
-
: `${componentId}-base`;
|
|
2889
|
-
|
|
2890
2721
|
const [updateTrigger, forceUpdate] = useState({});
|
|
2891
2722
|
|
|
2892
|
-
const
|
|
2893
|
-
const cached = getGlobalStore
|
|
2894
|
-
.getState()
|
|
2895
|
-
.getShadowMetadata(stateKey, path)
|
|
2896
|
-
?.transformCaches?.get(cacheKey);
|
|
2723
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2897
2724
|
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
);
|
|
2908
|
-
|
|
2909
|
-
getGlobalStore
|
|
2910
|
-
.getState()
|
|
2911
|
-
.setTransformCache(stateKey, path, cacheKey, {
|
|
2912
|
-
validIds: freshValidIds,
|
|
2913
|
-
computedAt: Date.now(),
|
|
2914
|
-
transforms: meta?.transforms || [],
|
|
2915
|
-
});
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
const freshValues = getGlobalStore
|
|
2919
|
-
.getState()
|
|
2920
|
-
.getShadowValue(stateKeyPathKey, freshValidIds);
|
|
2725
|
+
const validIds = useMemo(() => {
|
|
2726
|
+
return applyTransforms(stateKey, path, meta);
|
|
2727
|
+
}, [
|
|
2728
|
+
stateKey,
|
|
2729
|
+
path.join('.'),
|
|
2730
|
+
// Only recalculate if the underlying array keys or transforms change
|
|
2731
|
+
getShadowMetadata(stateKey, path)?.arrayKeys,
|
|
2732
|
+
meta?.transforms,
|
|
2733
|
+
]);
|
|
2921
2734
|
|
|
2735
|
+
// Memoize the updated meta to prevent creating new objects on every render
|
|
2736
|
+
const updatedMeta = useMemo(() => {
|
|
2922
2737
|
return {
|
|
2923
|
-
|
|
2924
|
-
|
|
2738
|
+
...meta,
|
|
2739
|
+
arrayViews: {
|
|
2740
|
+
...(meta?.arrayViews || {}),
|
|
2741
|
+
[arrayPathKey]: validIds,
|
|
2742
|
+
},
|
|
2925
2743
|
};
|
|
2926
|
-
}, [
|
|
2744
|
+
}, [meta, arrayPathKey, validIds]);
|
|
2745
|
+
|
|
2746
|
+
// Now use the updated meta when getting array data
|
|
2747
|
+
const { value: arrayValues } = getArrayData(
|
|
2748
|
+
stateKey,
|
|
2749
|
+
path,
|
|
2750
|
+
updatedMeta
|
|
2751
|
+
);
|
|
2927
2752
|
|
|
2928
2753
|
useEffect(() => {
|
|
2929
2754
|
const unsubscribe = getGlobalStore
|
|
2930
2755
|
.getState()
|
|
2931
2756
|
.subscribeToPath(stateKeyPathKey, (e) => {
|
|
2932
|
-
// A data change has occurred for the source array.
|
|
2933
|
-
|
|
2934
2757
|
if (e.type === 'GET_SELECTED') {
|
|
2935
2758
|
return;
|
|
2936
2759
|
}
|
|
@@ -2952,8 +2775,11 @@ function createProxyHandler<T>(
|
|
|
2952
2775
|
|
|
2953
2776
|
if (
|
|
2954
2777
|
e.type === 'INSERT' ||
|
|
2778
|
+
e.type === 'INSERT_MANY' ||
|
|
2955
2779
|
e.type === 'REMOVE' ||
|
|
2956
|
-
e.type === 'CLEAR_SELECTION'
|
|
2780
|
+
e.type === 'CLEAR_SELECTION' ||
|
|
2781
|
+
(e.type === 'SERVER_STATE_UPDATE' &&
|
|
2782
|
+
!meta?.serverStateIsUpStream)
|
|
2957
2783
|
) {
|
|
2958
2784
|
forceUpdate({});
|
|
2959
2785
|
}
|
|
@@ -2970,45 +2796,41 @@ function createProxyHandler<T>(
|
|
|
2970
2796
|
return null;
|
|
2971
2797
|
}
|
|
2972
2798
|
|
|
2799
|
+
// Continue using updatedMeta for the rest of your logic instead of meta
|
|
2973
2800
|
const arraySetter = rebuildStateShape({
|
|
2974
2801
|
path,
|
|
2975
2802
|
componentId: componentId!,
|
|
2976
|
-
meta:
|
|
2977
|
-
...meta,
|
|
2978
|
-
validIds: validIds,
|
|
2979
|
-
},
|
|
2803
|
+
meta: updatedMeta, // Use updated meta here
|
|
2980
2804
|
});
|
|
2981
2805
|
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
{arrayValues.map((item, localIndex) => {
|
|
2985
|
-
const itemKey = validIds[localIndex];
|
|
2806
|
+
const returnValue = arrayValues.map((item, localIndex) => {
|
|
2807
|
+
const itemKey = validIds[localIndex];
|
|
2986
2808
|
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2809
|
+
if (!itemKey) {
|
|
2810
|
+
return null;
|
|
2811
|
+
}
|
|
2990
2812
|
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2813
|
+
let itemComponentId = componentIdsRef.current.get(itemKey);
|
|
2814
|
+
if (!itemComponentId) {
|
|
2815
|
+
itemComponentId = uuidv4();
|
|
2816
|
+
componentIdsRef.current.set(itemKey, itemComponentId);
|
|
2817
|
+
}
|
|
2996
2818
|
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
2819
|
+
const itemPath = [...path, itemKey];
|
|
2820
|
+
|
|
2821
|
+
return createElement(MemoizedCogsItemWrapper, {
|
|
2822
|
+
key: itemKey,
|
|
2823
|
+
stateKey,
|
|
2824
|
+
itemComponentId,
|
|
2825
|
+
itemPath,
|
|
2826
|
+
localIndex,
|
|
2827
|
+
arraySetter,
|
|
2828
|
+
rebuildStateShape,
|
|
2829
|
+
renderFn: callbackfn,
|
|
2830
|
+
});
|
|
2831
|
+
});
|
|
2832
|
+
|
|
2833
|
+
return <>{returnValue}</>;
|
|
3012
2834
|
};
|
|
3013
2835
|
|
|
3014
2836
|
return <StateListWrapper />;
|
|
@@ -3016,16 +2838,18 @@ function createProxyHandler<T>(
|
|
|
3016
2838
|
}
|
|
3017
2839
|
if (prop === 'stateFlattenOn') {
|
|
3018
2840
|
return (fieldName: string) => {
|
|
2841
|
+
// FIX: Get the definitive list of IDs for the current view from meta.arrayViews.
|
|
2842
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2843
|
+
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2844
|
+
|
|
3019
2845
|
const currentState = getGlobalStore
|
|
3020
2846
|
.getState()
|
|
3021
|
-
.getShadowValue(
|
|
3022
|
-
|
|
3023
|
-
|
|
2847
|
+
.getShadowValue(stateKey, path, viewIds);
|
|
2848
|
+
|
|
2849
|
+
if (!Array.isArray(currentState)) return [];
|
|
3024
2850
|
|
|
3025
2851
|
stateVersion++;
|
|
3026
|
-
|
|
3027
|
-
(val: any) => val[fieldName] ?? []
|
|
3028
|
-
);
|
|
2852
|
+
|
|
3029
2853
|
return rebuildStateShape({
|
|
3030
2854
|
path: [...path, '[*]', fieldName],
|
|
3031
2855
|
componentId: componentId!,
|
|
@@ -3035,36 +2859,46 @@ function createProxyHandler<T>(
|
|
|
3035
2859
|
}
|
|
3036
2860
|
if (prop === 'index') {
|
|
3037
2861
|
return (index: number) => {
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
2862
|
+
const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
|
|
2863
|
+
const viewIds = meta?.arrayViews?.[arrayPathKey];
|
|
2864
|
+
|
|
2865
|
+
if (viewIds) {
|
|
2866
|
+
const itemId = viewIds[index];
|
|
2867
|
+
if (!itemId) return undefined;
|
|
2868
|
+
return rebuildStateShape({
|
|
2869
|
+
path: [...path, itemId],
|
|
2870
|
+
componentId: componentId!,
|
|
2871
|
+
meta,
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// ✅ FIX: Get the metadata and use the `arrayKeys` property.
|
|
2876
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
2877
|
+
if (!shadowMeta?.arrayKeys) return undefined;
|
|
2878
|
+
|
|
2879
|
+
const itemId = shadowMeta.arrayKeys[index];
|
|
3047
2880
|
if (!itemId) return undefined;
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
const state = rebuildStateShape({
|
|
3052
|
-
path: itemId.split('.').slice(1) as string[],
|
|
2881
|
+
|
|
2882
|
+
return rebuildStateShape({
|
|
2883
|
+
path: [...path, itemId],
|
|
3053
2884
|
componentId: componentId!,
|
|
3054
2885
|
meta,
|
|
3055
2886
|
});
|
|
3056
|
-
return state;
|
|
3057
2887
|
};
|
|
3058
2888
|
}
|
|
3059
2889
|
if (prop === 'last') {
|
|
3060
2890
|
return () => {
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
const
|
|
3066
|
-
|
|
3067
|
-
|
|
2891
|
+
const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
|
|
2892
|
+
if (!currentViewIds || currentViewIds.length === 0) {
|
|
2893
|
+
return undefined;
|
|
2894
|
+
}
|
|
2895
|
+
const lastItemKey = currentViewIds[currentViewIds.length - 1];
|
|
2896
|
+
|
|
2897
|
+
if (!lastItemKey) {
|
|
2898
|
+
return undefined;
|
|
2899
|
+
}
|
|
2900
|
+
const newPath = [...path, lastItemKey];
|
|
2901
|
+
|
|
3068
2902
|
return rebuildStateShape({
|
|
3069
2903
|
path: newPath,
|
|
3070
2904
|
componentId: componentId!,
|
|
@@ -3078,11 +2912,6 @@ function createProxyHandler<T>(
|
|
|
3078
2912
|
index?: number
|
|
3079
2913
|
) => {
|
|
3080
2914
|
effectiveSetState(payload as any, path, { updateType: 'insert' });
|
|
3081
|
-
return rebuildStateShape({
|
|
3082
|
-
path,
|
|
3083
|
-
componentId: componentId!,
|
|
3084
|
-
meta,
|
|
3085
|
-
});
|
|
3086
2915
|
};
|
|
3087
2916
|
}
|
|
3088
2917
|
if (prop === 'uniqueInsert') {
|
|
@@ -3091,9 +2920,13 @@ function createProxyHandler<T>(
|
|
|
3091
2920
|
fields?: (keyof InferArrayElement<T>)[],
|
|
3092
2921
|
onMatch?: (existingItem: any) => any
|
|
3093
2922
|
) => {
|
|
3094
|
-
const currentArray =
|
|
3095
|
-
|
|
3096
|
-
|
|
2923
|
+
const { value: currentArray } = getScopedData(
|
|
2924
|
+
stateKey,
|
|
2925
|
+
path,
|
|
2926
|
+
meta
|
|
2927
|
+
) as {
|
|
2928
|
+
value: any[];
|
|
2929
|
+
};
|
|
3097
2930
|
const newValue = isFunction<T>(payload)
|
|
3098
2931
|
? payload(currentArray as any)
|
|
3099
2932
|
: (payload as any);
|
|
@@ -3123,168 +2956,133 @@ function createProxyHandler<T>(
|
|
|
3123
2956
|
}
|
|
3124
2957
|
};
|
|
3125
2958
|
}
|
|
3126
|
-
|
|
3127
2959
|
if (prop === 'cut') {
|
|
3128
2960
|
return (index?: number, options?: { waitForSync?: boolean }) => {
|
|
3129
|
-
const
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
const validKeys =
|
|
3133
|
-
meta?.validIds ??
|
|
3134
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path)
|
|
3135
|
-
?.arrayKeys;
|
|
3136
|
-
|
|
3137
|
-
if (!validKeys || validKeys.length === 0) return;
|
|
2961
|
+
const shadowMeta = getShadowMetadata(stateKey, path);
|
|
2962
|
+
if (!shadowMeta?.arrayKeys || shadowMeta.arrayKeys.length === 0)
|
|
2963
|
+
return;
|
|
3138
2964
|
|
|
3139
2965
|
const indexToCut =
|
|
3140
|
-
index
|
|
3141
|
-
?
|
|
2966
|
+
index === -1
|
|
2967
|
+
? shadowMeta.arrayKeys.length - 1
|
|
3142
2968
|
: index !== undefined
|
|
3143
2969
|
? index
|
|
3144
|
-
:
|
|
2970
|
+
: shadowMeta.arrayKeys.length - 1;
|
|
3145
2971
|
|
|
3146
|
-
const
|
|
3147
|
-
if (!
|
|
2972
|
+
const idToCut = shadowMeta.arrayKeys[indexToCut];
|
|
2973
|
+
if (!idToCut) return;
|
|
3148
2974
|
|
|
3149
|
-
|
|
3150
|
-
effectiveSetState(currentState, pathForCut, {
|
|
2975
|
+
effectiveSetState(null, [...path, idToCut], {
|
|
3151
2976
|
updateType: 'cut',
|
|
3152
2977
|
});
|
|
3153
2978
|
};
|
|
3154
2979
|
}
|
|
3155
2980
|
if (prop === 'cutSelected') {
|
|
3156
2981
|
return () => {
|
|
3157
|
-
const
|
|
3158
|
-
const currentState = getGlobalStore
|
|
3159
|
-
.getState()
|
|
3160
|
-
.getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
|
|
3161
|
-
if (!validKeys || validKeys.length === 0) return;
|
|
2982
|
+
const arrayKey = [stateKey, ...path].join('.');
|
|
3162
2983
|
|
|
3163
|
-
const
|
|
2984
|
+
const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
|
|
2985
|
+
if (!currentViewIds || currentViewIds.length === 0) {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
const selectedItemKey = getGlobalStore
|
|
3164
2989
|
.getState()
|
|
3165
|
-
.selectedIndicesMap.get(
|
|
2990
|
+
.selectedIndicesMap.get(arrayKey);
|
|
3166
2991
|
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
2992
|
+
if (!selectedItemKey) {
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
const selectedId = selectedItemKey.split('.').pop() as string;
|
|
3170
2996
|
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
.clearSelectedIndex({ arrayKey: stateKeyPathKey });
|
|
3179
|
-
const parentPath = pathForCut?.slice(0, -1)!;
|
|
2997
|
+
if (!(currentViewIds as any[]).includes(selectedId!)) {
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
const pathForCut = selectedItemKey.split('.').slice(1);
|
|
3001
|
+
getGlobalStore.getState().clearSelectedIndex({ arrayKey });
|
|
3002
|
+
|
|
3003
|
+
const parentPath = pathForCut.slice(0, -1);
|
|
3180
3004
|
notifySelectionComponents(stateKey, parentPath);
|
|
3181
|
-
|
|
3005
|
+
|
|
3006
|
+
effectiveSetState(null, pathForCut, {
|
|
3182
3007
|
updateType: 'cut',
|
|
3183
3008
|
});
|
|
3184
3009
|
};
|
|
3185
3010
|
}
|
|
3186
3011
|
if (prop === 'cutByValue') {
|
|
3187
3012
|
return (value: string | number | boolean) => {
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
if (!relevantKeys) return;
|
|
3013
|
+
const {
|
|
3014
|
+
isArray,
|
|
3015
|
+
value: array,
|
|
3016
|
+
keys,
|
|
3017
|
+
} = getArrayData(stateKey, path, meta);
|
|
3195
3018
|
|
|
3196
|
-
|
|
3019
|
+
if (!isArray) return;
|
|
3197
3020
|
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
break; // We found the key, no need to search further.
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
|
|
3207
|
-
// Step 3: If we found a matching key, use it to perform the cut.
|
|
3208
|
-
if (keyToCut) {
|
|
3209
|
-
const itemPath = keyToCut.split('.').slice(1);
|
|
3210
|
-
effectiveSetState(null as any, itemPath, { updateType: 'cut' });
|
|
3021
|
+
const found = findArrayItem(array, keys, (item) => item === value);
|
|
3022
|
+
if (found) {
|
|
3023
|
+
effectiveSetState(null, [...path, found.key], {
|
|
3024
|
+
updateType: 'cut',
|
|
3025
|
+
});
|
|
3211
3026
|
}
|
|
3212
3027
|
};
|
|
3213
3028
|
}
|
|
3214
3029
|
|
|
3215
3030
|
if (prop === 'toggleByValue') {
|
|
3216
3031
|
return (value: string | number | boolean) => {
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3032
|
+
const {
|
|
3033
|
+
isArray,
|
|
3034
|
+
value: array,
|
|
3035
|
+
keys,
|
|
3036
|
+
} = getArrayData(stateKey, path, meta);
|
|
3222
3037
|
|
|
3223
|
-
if (!
|
|
3038
|
+
if (!isArray) return;
|
|
3224
3039
|
|
|
3225
|
-
|
|
3040
|
+
const found = findArrayItem(array, keys, (item) => item === value);
|
|
3226
3041
|
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
if (itemValue === value) {
|
|
3232
|
-
keyToCut = key;
|
|
3233
|
-
break; // Found it!
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
console.log('itemValue keyToCut', keyToCut);
|
|
3237
|
-
// Step 3: Act based on whether the key was found.
|
|
3238
|
-
if (keyToCut) {
|
|
3239
|
-
// Item exists, so we CUT it using its *actual* key.
|
|
3240
|
-
const itemPath = keyToCut.split('.').slice(1);
|
|
3241
|
-
console.log('itemValue keyToCut', keyToCut);
|
|
3242
|
-
effectiveSetState(value as any, itemPath, {
|
|
3042
|
+
if (found) {
|
|
3043
|
+
const pathForItem = [...path, found.key];
|
|
3044
|
+
|
|
3045
|
+
effectiveSetState(null, pathForItem, {
|
|
3243
3046
|
updateType: 'cut',
|
|
3244
3047
|
});
|
|
3245
3048
|
} else {
|
|
3246
|
-
// Item does not exist, so we INSERT it.
|
|
3247
3049
|
effectiveSetState(value as any, path, { updateType: 'insert' });
|
|
3248
3050
|
}
|
|
3249
3051
|
};
|
|
3250
3052
|
}
|
|
3251
3053
|
if (prop === 'findWith') {
|
|
3252
|
-
return (searchKey:
|
|
3253
|
-
const
|
|
3254
|
-
.getState()
|
|
3255
|
-
.getShadowMetadata(stateKey, path)?.arrayKeys;
|
|
3054
|
+
return (searchKey: string, searchValue: any) => {
|
|
3055
|
+
const { isArray, value, keys } = getArrayData(stateKey, path, meta);
|
|
3256
3056
|
|
|
3257
|
-
if (!
|
|
3258
|
-
throw new Error('
|
|
3057
|
+
if (!isArray) {
|
|
3058
|
+
throw new Error('findWith can only be used on arrays');
|
|
3259
3059
|
}
|
|
3260
3060
|
|
|
3261
|
-
|
|
3262
|
-
|
|
3061
|
+
const found = findArrayItem(
|
|
3062
|
+
value,
|
|
3063
|
+
keys,
|
|
3064
|
+
(item) => item?.[searchKey] === searchValue
|
|
3065
|
+
);
|
|
3263
3066
|
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
.
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
foundPath = fullPath.split('.').slice(1);
|
|
3271
|
-
break;
|
|
3272
|
-
}
|
|
3067
|
+
if (found) {
|
|
3068
|
+
return rebuildStateShape({
|
|
3069
|
+
path: [...path, found.key],
|
|
3070
|
+
componentId: componentId!,
|
|
3071
|
+
meta,
|
|
3072
|
+
});
|
|
3273
3073
|
}
|
|
3274
3074
|
|
|
3275
3075
|
return rebuildStateShape({
|
|
3276
|
-
path:
|
|
3076
|
+
path: [...path, `not_found_${uuidv4()}`],
|
|
3277
3077
|
componentId: componentId!,
|
|
3278
3078
|
meta,
|
|
3279
3079
|
});
|
|
3280
3080
|
};
|
|
3281
3081
|
}
|
|
3282
|
-
|
|
3283
3082
|
if (prop === 'cutThis') {
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3083
|
+
const { value: shadowValue } = getScopedData(stateKey, path, meta);
|
|
3084
|
+
const parentPath = path.slice(0, -1);
|
|
3085
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3288
3086
|
return () => {
|
|
3289
3087
|
effectiveSetState(shadowValue, path, { updateType: 'cut' });
|
|
3290
3088
|
};
|
|
@@ -3293,16 +3091,8 @@ function createProxyHandler<T>(
|
|
|
3293
3091
|
if (prop === 'get') {
|
|
3294
3092
|
return () => {
|
|
3295
3093
|
registerComponentDependency(stateKey, componentId, path);
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3299
|
-
};
|
|
3300
|
-
}
|
|
3301
|
-
if (prop === 'getState') {
|
|
3302
|
-
return () => {
|
|
3303
|
-
return getGlobalStore
|
|
3304
|
-
.getState()
|
|
3305
|
-
.getShadowValue(stateKeyPathKey, meta?.validIds);
|
|
3094
|
+
const { value } = getScopedData(stateKey, path, meta);
|
|
3095
|
+
return value;
|
|
3306
3096
|
};
|
|
3307
3097
|
}
|
|
3308
3098
|
|
|
@@ -3315,7 +3105,6 @@ function createProxyHandler<T>(
|
|
|
3315
3105
|
_meta: meta,
|
|
3316
3106
|
});
|
|
3317
3107
|
}
|
|
3318
|
-
// in CogsState.ts -> createProxyHandler -> handler -> get
|
|
3319
3108
|
|
|
3320
3109
|
if (prop === '$get') {
|
|
3321
3110
|
return () =>
|
|
@@ -3323,26 +3112,18 @@ function createProxyHandler<T>(
|
|
|
3323
3112
|
}
|
|
3324
3113
|
if (prop === 'lastSynced') {
|
|
3325
3114
|
const syncKey = `${stateKey}:${path.join('.')}`;
|
|
3326
|
-
return
|
|
3115
|
+
return getSyncInfo(syncKey);
|
|
3327
3116
|
}
|
|
3328
3117
|
if (prop == 'getLocalStorage') {
|
|
3329
3118
|
return (key: string) =>
|
|
3330
3119
|
loadFromLocalStorage(sessionId + '-' + stateKey + '-' + key);
|
|
3331
3120
|
}
|
|
3332
|
-
|
|
3333
3121
|
if (prop === 'isSelected') {
|
|
3334
|
-
const
|
|
3335
|
-
|
|
3336
|
-
if (
|
|
3337
|
-
Array.isArray(
|
|
3338
|
-
getGlobalStore
|
|
3339
|
-
.getState()
|
|
3340
|
-
.getShadowValue(parentPath.join('.'), meta?.validIds)
|
|
3341
|
-
)
|
|
3342
|
-
) {
|
|
3343
|
-
const itemId = path[path.length - 1];
|
|
3344
|
-
const fullParentKey = parentPath.join('.');
|
|
3122
|
+
const parentPathArray = path.slice(0, -1);
|
|
3123
|
+
const parentMeta = getShadowMetadata(stateKey, parentPathArray);
|
|
3345
3124
|
|
|
3125
|
+
if (parentMeta?.arrayKeys) {
|
|
3126
|
+
const fullParentKey = stateKey + '.' + parentPathArray.join('.');
|
|
3346
3127
|
const selectedItemKey = getGlobalStore
|
|
3347
3128
|
.getState()
|
|
3348
3129
|
.selectedIndicesMap.get(fullParentKey);
|
|
@@ -3354,7 +3135,6 @@ function createProxyHandler<T>(
|
|
|
3354
3135
|
return undefined;
|
|
3355
3136
|
}
|
|
3356
3137
|
|
|
3357
|
-
// Then use it in both:
|
|
3358
3138
|
if (prop === 'setSelected') {
|
|
3359
3139
|
return (value: boolean) => {
|
|
3360
3140
|
const parentPath = path.slice(0, -1);
|
|
@@ -3394,6 +3174,7 @@ function createProxyHandler<T>(
|
|
|
3394
3174
|
.getState()
|
|
3395
3175
|
.setSelectedIndex(fullParentKey, fullItemKey);
|
|
3396
3176
|
}
|
|
3177
|
+
notifySelectionComponents(stateKey, parentPath);
|
|
3397
3178
|
};
|
|
3398
3179
|
}
|
|
3399
3180
|
if (prop === '_componentId') {
|
|
@@ -3402,11 +3183,6 @@ function createProxyHandler<T>(
|
|
|
3402
3183
|
if (path.length == 0) {
|
|
3403
3184
|
if (prop === 'addZodValidation') {
|
|
3404
3185
|
return (zodErrors: any[]) => {
|
|
3405
|
-
const init = getGlobalStore
|
|
3406
|
-
.getState()
|
|
3407
|
-
.getInitialOptions(stateKey)?.validation;
|
|
3408
|
-
|
|
3409
|
-
// For each error, set shadow metadata
|
|
3410
3186
|
zodErrors.forEach((error) => {
|
|
3411
3187
|
const currentMeta =
|
|
3412
3188
|
getGlobalStore
|
|
@@ -3418,42 +3194,38 @@ function createProxyHandler<T>(
|
|
|
3418
3194
|
.setShadowMetadata(stateKey, error.path, {
|
|
3419
3195
|
...currentMeta,
|
|
3420
3196
|
validation: {
|
|
3421
|
-
status: '
|
|
3422
|
-
|
|
3197
|
+
status: 'INVALID',
|
|
3198
|
+
errors: [
|
|
3199
|
+
{
|
|
3200
|
+
source: 'client',
|
|
3201
|
+
message: error.message,
|
|
3202
|
+
severity: 'error',
|
|
3203
|
+
code: error.code,
|
|
3204
|
+
},
|
|
3205
|
+
],
|
|
3206
|
+
lastValidated: Date.now(),
|
|
3423
3207
|
validatedValue: undefined,
|
|
3424
3208
|
},
|
|
3425
3209
|
});
|
|
3426
|
-
getGlobalStore.getState().notifyPathSubscribers(error.path, {
|
|
3427
|
-
type: 'VALIDATION_FAILED',
|
|
3428
|
-
message: error.message,
|
|
3429
|
-
validatedValue: undefined,
|
|
3430
|
-
});
|
|
3431
3210
|
});
|
|
3432
3211
|
};
|
|
3433
3212
|
}
|
|
3434
3213
|
if (prop === 'clearZodValidation') {
|
|
3435
3214
|
return (path?: string[]) => {
|
|
3436
|
-
// Clear specific paths
|
|
3437
3215
|
if (!path) {
|
|
3438
3216
|
throw new Error('clearZodValidation requires a path');
|
|
3439
|
-
return;
|
|
3440
3217
|
}
|
|
3441
|
-
const currentMeta =
|
|
3442
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, path) ||
|
|
3443
|
-
{};
|
|
3444
|
-
|
|
3445
|
-
if (currentMeta.validation) {
|
|
3446
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
3447
|
-
...currentMeta,
|
|
3448
|
-
validation: undefined,
|
|
3449
|
-
});
|
|
3450
3218
|
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3219
|
+
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
3220
|
+
|
|
3221
|
+
setShadowMetadata(stateKey, path, {
|
|
3222
|
+
...currentMeta,
|
|
3223
|
+
validation: {
|
|
3224
|
+
status: 'NOT_VALIDATED',
|
|
3225
|
+
errors: [],
|
|
3226
|
+
lastValidated: Date.now(),
|
|
3227
|
+
},
|
|
3228
|
+
});
|
|
3457
3229
|
};
|
|
3458
3230
|
}
|
|
3459
3231
|
if (prop === 'applyJsonPatch') {
|
|
@@ -3484,6 +3256,7 @@ function createProxyHandler<T>(
|
|
|
3484
3256
|
value: any;
|
|
3485
3257
|
};
|
|
3486
3258
|
store.updateShadowAtPath(stateKey, relativePath, value);
|
|
3259
|
+
|
|
3487
3260
|
store.markAsDirty(stateKey, relativePath, { bubble: true });
|
|
3488
3261
|
|
|
3489
3262
|
// Bubble up - notify components at this path and all parent paths
|
|
@@ -3548,9 +3321,7 @@ function createProxyHandler<T>(
|
|
|
3548
3321
|
}
|
|
3549
3322
|
|
|
3550
3323
|
if (prop === 'getComponents')
|
|
3551
|
-
return () =>
|
|
3552
|
-
getGlobalStore.getState().getShadowMetadata(stateKey, [])
|
|
3553
|
-
?.components;
|
|
3324
|
+
return () => getShadowMetadata(stateKey, [])?.components;
|
|
3554
3325
|
if (prop === 'getAllFormRefs')
|
|
3555
3326
|
return () =>
|
|
3556
3327
|
formRefStore.getState().getFormRefsByStateKey(stateKey);
|
|
@@ -3582,63 +3353,8 @@ function createProxyHandler<T>(
|
|
|
3582
3353
|
if (prop === '_path') return path;
|
|
3583
3354
|
if (prop === 'update') {
|
|
3584
3355
|
return (payload: UpdateArg<T>) => {
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
const stack = error.stack || '';
|
|
3588
|
-
const inReactEvent =
|
|
3589
|
-
stack.includes('onClick') ||
|
|
3590
|
-
stack.includes('dispatchEvent') ||
|
|
3591
|
-
stack.includes('batchedUpdates');
|
|
3592
|
-
|
|
3593
|
-
// Only batch if we're in a React event
|
|
3594
|
-
if (inReactEvent) {
|
|
3595
|
-
const batchKey = `${stateKey}.${path.join('.')}`;
|
|
3596
|
-
|
|
3597
|
-
// Schedule flush if not already scheduled
|
|
3598
|
-
if (!batchFlushScheduled) {
|
|
3599
|
-
updateBatchQueue.clear();
|
|
3600
|
-
batchFlushScheduled = true;
|
|
3601
|
-
|
|
3602
|
-
queueMicrotask(() => {
|
|
3603
|
-
// Process all batched updates
|
|
3604
|
-
for (const [key, updates] of updateBatchQueue) {
|
|
3605
|
-
const parts = key.split('.');
|
|
3606
|
-
const batchStateKey = parts[0];
|
|
3607
|
-
const batchPath = parts.slice(1);
|
|
3608
|
-
|
|
3609
|
-
// Compose all updates for this path
|
|
3610
|
-
const composedUpdate = updates.reduce(
|
|
3611
|
-
(composed, update) => {
|
|
3612
|
-
if (
|
|
3613
|
-
typeof update === 'function' &&
|
|
3614
|
-
typeof composed === 'function'
|
|
3615
|
-
) {
|
|
3616
|
-
// Compose functions
|
|
3617
|
-
return (state: any) => update(composed(state));
|
|
3618
|
-
}
|
|
3619
|
-
// If not functions, last one wins
|
|
3620
|
-
return update;
|
|
3621
|
-
}
|
|
3622
|
-
);
|
|
3623
|
-
|
|
3624
|
-
// Call effectiveSetState ONCE with composed update
|
|
3625
|
-
effectiveSetState(composedUpdate as any, batchPath, {
|
|
3626
|
-
updateType: 'update',
|
|
3627
|
-
});
|
|
3628
|
-
}
|
|
3629
|
-
|
|
3630
|
-
updateBatchQueue.clear();
|
|
3631
|
-
batchFlushScheduled = false;
|
|
3632
|
-
});
|
|
3633
|
-
}
|
|
3634
|
-
|
|
3635
|
-
// Add to batch
|
|
3636
|
-
const existing = updateBatchQueue.get(batchKey) || [];
|
|
3637
|
-
existing.push(payload);
|
|
3638
|
-
updateBatchQueue.set(batchKey, existing);
|
|
3639
|
-
} else {
|
|
3640
|
-
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3641
|
-
}
|
|
3356
|
+
console.log('udpating', payload, path);
|
|
3357
|
+
effectiveSetState(payload as any, path, { updateType: 'update' });
|
|
3642
3358
|
|
|
3643
3359
|
return {
|
|
3644
3360
|
synced: () => {
|
|
@@ -3646,15 +3362,17 @@ function createProxyHandler<T>(
|
|
|
3646
3362
|
.getState()
|
|
3647
3363
|
.getShadowMetadata(stateKey, path);
|
|
3648
3364
|
|
|
3649
|
-
|
|
3365
|
+
// Update the metadata for this specific path
|
|
3366
|
+
setShadowMetadata(stateKey, path, {
|
|
3650
3367
|
...shadowMeta,
|
|
3651
3368
|
isDirty: false,
|
|
3652
3369
|
stateSource: 'server',
|
|
3653
3370
|
lastServerSync: Date.now(),
|
|
3654
3371
|
});
|
|
3655
3372
|
|
|
3373
|
+
// Notify any components that might be subscribed to the sync status
|
|
3656
3374
|
const fullPath = [stateKey, ...path].join('.');
|
|
3657
|
-
|
|
3375
|
+
notifyPathSubscribers(fullPath, {
|
|
3658
3376
|
type: 'SYNC_STATUS_CHANGE',
|
|
3659
3377
|
isDirty: false,
|
|
3660
3378
|
});
|
|
@@ -3662,11 +3380,12 @@ function createProxyHandler<T>(
|
|
|
3662
3380
|
};
|
|
3663
3381
|
};
|
|
3664
3382
|
}
|
|
3665
|
-
|
|
3666
3383
|
if (prop === 'toggle') {
|
|
3667
|
-
const currentValueAtPath =
|
|
3668
|
-
|
|
3669
|
-
|
|
3384
|
+
const { value: currentValueAtPath } = getScopedData(
|
|
3385
|
+
stateKey,
|
|
3386
|
+
path,
|
|
3387
|
+
meta
|
|
3388
|
+
);
|
|
3670
3389
|
|
|
3671
3390
|
if (typeof currentValueAtPath != 'boolean') {
|
|
3672
3391
|
throw new Error('toggle() can only be used on boolean values');
|
|
@@ -3703,18 +3422,14 @@ function createProxyHandler<T>(
|
|
|
3703
3422
|
},
|
|
3704
3423
|
};
|
|
3705
3424
|
|
|
3706
|
-
const proxyInstance = new Proxy(
|
|
3425
|
+
const proxyInstance = new Proxy({}, handler);
|
|
3707
3426
|
proxyCache.set(cacheKey, proxyInstance);
|
|
3708
3427
|
|
|
3709
3428
|
return proxyInstance;
|
|
3710
3429
|
}
|
|
3711
3430
|
|
|
3712
|
-
const
|
|
3431
|
+
const rootLevelMethods = {
|
|
3713
3432
|
revertToInitialState: (obj?: { validationKey?: string }) => {
|
|
3714
|
-
const init = getGlobalStore
|
|
3715
|
-
.getState()
|
|
3716
|
-
.getInitialOptions(stateKey)?.validation;
|
|
3717
|
-
|
|
3718
3433
|
const shadowMeta = getGlobalStore
|
|
3719
3434
|
.getState()
|
|
3720
3435
|
.getShadowMetadata(stateKey, []);
|
|
@@ -3730,10 +3445,10 @@ function createProxyHandler<T>(
|
|
|
3730
3445
|
const initialState =
|
|
3731
3446
|
getGlobalStore.getState().initialStateGlobal[stateKey];
|
|
3732
3447
|
|
|
3733
|
-
|
|
3448
|
+
clearSelectedIndexesForState(stateKey);
|
|
3734
3449
|
|
|
3735
3450
|
stateVersion++;
|
|
3736
|
-
|
|
3451
|
+
initializeShadowState(stateKey, initialState);
|
|
3737
3452
|
rebuildStateShape({
|
|
3738
3453
|
path: [],
|
|
3739
3454
|
componentId: componentId!,
|
|
@@ -3783,7 +3498,8 @@ function createProxyHandler<T>(
|
|
|
3783
3498
|
}
|
|
3784
3499
|
startTransition(() => {
|
|
3785
3500
|
updateInitialStateGlobal(stateKey, newState);
|
|
3786
|
-
|
|
3501
|
+
initializeShadowState(stateKey, newState);
|
|
3502
|
+
// initializeShadowStateNEW(stateKey, newState);
|
|
3787
3503
|
|
|
3788
3504
|
const stateEntry = getGlobalStore
|
|
3789
3505
|
.getState()
|
|
@@ -3801,6 +3517,7 @@ function createProxyHandler<T>(
|
|
|
3801
3517
|
};
|
|
3802
3518
|
},
|
|
3803
3519
|
};
|
|
3520
|
+
|
|
3804
3521
|
const returnShape = rebuildStateShape({
|
|
3805
3522
|
componentId,
|
|
3806
3523
|
path: [],
|
|
@@ -3819,153 +3536,6 @@ export function $cogsSignal(proxy: {
|
|
|
3819
3536
|
return createElement(SignalRenderer, { proxy });
|
|
3820
3537
|
}
|
|
3821
3538
|
|
|
3822
|
-
function SignalMapRenderer({
|
|
3823
|
-
proxy,
|
|
3824
|
-
rebuildStateShape,
|
|
3825
|
-
}: {
|
|
3826
|
-
proxy: {
|
|
3827
|
-
_stateKey: string;
|
|
3828
|
-
_path: string[];
|
|
3829
|
-
_meta?: MetaData;
|
|
3830
|
-
_mapFn: (
|
|
3831
|
-
setter: any,
|
|
3832
|
-
index: number,
|
|
3833
|
-
|
|
3834
|
-
arraySetter: any
|
|
3835
|
-
) => ReactNode;
|
|
3836
|
-
};
|
|
3837
|
-
rebuildStateShape: (stuff: {
|
|
3838
|
-
currentState: any;
|
|
3839
|
-
path: string[];
|
|
3840
|
-
componentId: string;
|
|
3841
|
-
meta?: MetaData;
|
|
3842
|
-
}) => any;
|
|
3843
|
-
}): JSX.Element | null {
|
|
3844
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
3845
|
-
const instanceIdRef = useRef<string>(`map-${crypto.randomUUID()}`);
|
|
3846
|
-
const isSetupRef = useRef(false);
|
|
3847
|
-
const rootsMapRef = useRef<Map<string, any>>(new Map());
|
|
3848
|
-
|
|
3849
|
-
// Setup effect - store the map function in shadow metadata
|
|
3850
|
-
useEffect(() => {
|
|
3851
|
-
const container = containerRef.current;
|
|
3852
|
-
if (!container || isSetupRef.current) return;
|
|
3853
|
-
|
|
3854
|
-
const timeoutId = setTimeout(() => {
|
|
3855
|
-
// Store map wrapper in metadata
|
|
3856
|
-
const currentMeta =
|
|
3857
|
-
getGlobalStore
|
|
3858
|
-
.getState()
|
|
3859
|
-
.getShadowMetadata(proxy._stateKey, proxy._path) || {};
|
|
3860
|
-
|
|
3861
|
-
const mapWrappers = currentMeta.mapWrappers || [];
|
|
3862
|
-
mapWrappers.push({
|
|
3863
|
-
instanceId: instanceIdRef.current,
|
|
3864
|
-
mapFn: proxy._mapFn,
|
|
3865
|
-
containerRef: container,
|
|
3866
|
-
rebuildStateShape: rebuildStateShape,
|
|
3867
|
-
path: proxy._path,
|
|
3868
|
-
componentId: instanceIdRef.current,
|
|
3869
|
-
meta: proxy._meta,
|
|
3870
|
-
});
|
|
3871
|
-
|
|
3872
|
-
getGlobalStore
|
|
3873
|
-
.getState()
|
|
3874
|
-
.setShadowMetadata(proxy._stateKey, proxy._path, {
|
|
3875
|
-
...currentMeta,
|
|
3876
|
-
mapWrappers,
|
|
3877
|
-
});
|
|
3878
|
-
|
|
3879
|
-
isSetupRef.current = true;
|
|
3880
|
-
|
|
3881
|
-
// Initial render
|
|
3882
|
-
renderInitialItems();
|
|
3883
|
-
}, 0);
|
|
3884
|
-
|
|
3885
|
-
// Cleanup
|
|
3886
|
-
return () => {
|
|
3887
|
-
clearTimeout(timeoutId);
|
|
3888
|
-
if (instanceIdRef.current) {
|
|
3889
|
-
const currentMeta =
|
|
3890
|
-
getGlobalStore
|
|
3891
|
-
.getState()
|
|
3892
|
-
.getShadowMetadata(proxy._stateKey, proxy._path) || {};
|
|
3893
|
-
if (currentMeta.mapWrappers) {
|
|
3894
|
-
currentMeta.mapWrappers = currentMeta.mapWrappers.filter(
|
|
3895
|
-
(w) => w.instanceId !== instanceIdRef.current
|
|
3896
|
-
);
|
|
3897
|
-
getGlobalStore
|
|
3898
|
-
.getState()
|
|
3899
|
-
.setShadowMetadata(proxy._stateKey, proxy._path, currentMeta);
|
|
3900
|
-
}
|
|
3901
|
-
}
|
|
3902
|
-
rootsMapRef.current.forEach((root) => root.unmount());
|
|
3903
|
-
};
|
|
3904
|
-
}, []);
|
|
3905
|
-
|
|
3906
|
-
const renderInitialItems = () => {
|
|
3907
|
-
const container = containerRef.current;
|
|
3908
|
-
if (!container) return;
|
|
3909
|
-
|
|
3910
|
-
const value = getGlobalStore
|
|
3911
|
-
.getState()
|
|
3912
|
-
.getShadowValue(
|
|
3913
|
-
[proxy._stateKey, ...proxy._path].join('.'),
|
|
3914
|
-
proxy._meta?.validIds
|
|
3915
|
-
) as any[];
|
|
3916
|
-
|
|
3917
|
-
if (!Array.isArray(value)) return;
|
|
3918
|
-
|
|
3919
|
-
// --- BUG FIX IS HERE ---
|
|
3920
|
-
// Prioritize the filtered IDs from the meta object, just like the regular `stateMap`.
|
|
3921
|
-
// This ensures the keys match the filtered data.
|
|
3922
|
-
const arrayKeys =
|
|
3923
|
-
proxy._meta?.validIds ??
|
|
3924
|
-
getGlobalStore.getState().getShadowMetadata(proxy._stateKey, proxy._path)
|
|
3925
|
-
?.arrayKeys ??
|
|
3926
|
-
[];
|
|
3927
|
-
// --- END OF FIX ---
|
|
3928
|
-
|
|
3929
|
-
const arraySetter = rebuildStateShape({
|
|
3930
|
-
currentState: value,
|
|
3931
|
-
path: proxy._path,
|
|
3932
|
-
componentId: instanceIdRef.current,
|
|
3933
|
-
meta: proxy._meta,
|
|
3934
|
-
});
|
|
3935
|
-
|
|
3936
|
-
value.forEach((item, index) => {
|
|
3937
|
-
const itemKey = arrayKeys[index]!; // Now this will be the correct key for the filtered item
|
|
3938
|
-
if (!itemKey) return; // Safeguard if there's a mismatch
|
|
3939
|
-
|
|
3940
|
-
const itemComponentId = uuidv4();
|
|
3941
|
-
const itemElement = document.createElement('div');
|
|
3942
|
-
|
|
3943
|
-
itemElement.setAttribute('data-item-path', itemKey);
|
|
3944
|
-
container.appendChild(itemElement);
|
|
3945
|
-
|
|
3946
|
-
const root = createRoot(itemElement);
|
|
3947
|
-
rootsMapRef.current.set(itemKey, root);
|
|
3948
|
-
|
|
3949
|
-
const itemPath = itemKey.split('.').slice(1) as string[];
|
|
3950
|
-
|
|
3951
|
-
// Render CogsItemWrapper instead of direct render
|
|
3952
|
-
root.render(
|
|
3953
|
-
createElement(MemoizedCogsItemWrapper, {
|
|
3954
|
-
stateKey: proxy._stateKey,
|
|
3955
|
-
itemComponentId: itemComponentId,
|
|
3956
|
-
itemPath: itemPath,
|
|
3957
|
-
localIndex: index,
|
|
3958
|
-
arraySetter: arraySetter,
|
|
3959
|
-
rebuildStateShape: rebuildStateShape,
|
|
3960
|
-
renderFn: proxy._mapFn,
|
|
3961
|
-
})
|
|
3962
|
-
);
|
|
3963
|
-
});
|
|
3964
|
-
};
|
|
3965
|
-
|
|
3966
|
-
return <div ref={containerRef} data-map-container={instanceIdRef.current} />;
|
|
3967
|
-
}
|
|
3968
|
-
|
|
3969
3539
|
function SignalRenderer({
|
|
3970
3540
|
proxy,
|
|
3971
3541
|
}: {
|
|
@@ -3980,12 +3550,11 @@ function SignalRenderer({
|
|
|
3980
3550
|
const instanceIdRef = useRef<string | null>(null);
|
|
3981
3551
|
const isSetupRef = useRef(false);
|
|
3982
3552
|
const signalId = `${proxy._stateKey}-${proxy._path.join('.')}`;
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
);
|
|
3553
|
+
|
|
3554
|
+
const arrayPathKey = proxy._path.length > 0 ? proxy._path.join('.') : 'root';
|
|
3555
|
+
const viewIds = proxy._meta?.arrayViews?.[arrayPathKey];
|
|
3556
|
+
|
|
3557
|
+
const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
|
|
3989
3558
|
|
|
3990
3559
|
// Setup effect - runs only once
|
|
3991
3560
|
useEffect(() => {
|
|
@@ -4075,473 +3644,3 @@ function SignalRenderer({
|
|
|
4075
3644
|
'data-signal-id': signalId,
|
|
4076
3645
|
});
|
|
4077
3646
|
}
|
|
4078
|
-
|
|
4079
|
-
const MemoizedCogsItemWrapper = memo(
|
|
4080
|
-
ListItemWrapper,
|
|
4081
|
-
(prevProps, nextProps) => {
|
|
4082
|
-
// Re-render if any of these change:
|
|
4083
|
-
return (
|
|
4084
|
-
prevProps.itemPath.join('.') === nextProps.itemPath.join('.') &&
|
|
4085
|
-
prevProps.stateKey === nextProps.stateKey &&
|
|
4086
|
-
prevProps.itemComponentId === nextProps.itemComponentId &&
|
|
4087
|
-
prevProps.localIndex === nextProps.localIndex
|
|
4088
|
-
);
|
|
4089
|
-
}
|
|
4090
|
-
);
|
|
4091
|
-
|
|
4092
|
-
const useImageLoaded = (ref: RefObject<HTMLElement>): boolean => {
|
|
4093
|
-
const [loaded, setLoaded] = useState(false);
|
|
4094
|
-
|
|
4095
|
-
useLayoutEffect(() => {
|
|
4096
|
-
if (!ref.current) {
|
|
4097
|
-
setLoaded(true);
|
|
4098
|
-
return;
|
|
4099
|
-
}
|
|
4100
|
-
|
|
4101
|
-
const images = Array.from(ref.current.querySelectorAll('img'));
|
|
4102
|
-
|
|
4103
|
-
// If there are no images, we are "loaded" immediately.
|
|
4104
|
-
if (images.length === 0) {
|
|
4105
|
-
setLoaded(true);
|
|
4106
|
-
return;
|
|
4107
|
-
}
|
|
4108
|
-
|
|
4109
|
-
let loadedCount = 0;
|
|
4110
|
-
const handleImageLoad = () => {
|
|
4111
|
-
loadedCount++;
|
|
4112
|
-
if (loadedCount === images.length) {
|
|
4113
|
-
setLoaded(true);
|
|
4114
|
-
}
|
|
4115
|
-
};
|
|
4116
|
-
|
|
4117
|
-
images.forEach((image) => {
|
|
4118
|
-
if (image.complete) {
|
|
4119
|
-
handleImageLoad();
|
|
4120
|
-
} else {
|
|
4121
|
-
image.addEventListener('load', handleImageLoad);
|
|
4122
|
-
image.addEventListener('error', handleImageLoad);
|
|
4123
|
-
}
|
|
4124
|
-
});
|
|
4125
|
-
|
|
4126
|
-
return () => {
|
|
4127
|
-
images.forEach((image) => {
|
|
4128
|
-
image.removeEventListener('load', handleImageLoad);
|
|
4129
|
-
image.removeEventListener('error', handleImageLoad);
|
|
4130
|
-
});
|
|
4131
|
-
};
|
|
4132
|
-
}, [ref.current]);
|
|
4133
|
-
|
|
4134
|
-
return loaded;
|
|
4135
|
-
};
|
|
4136
|
-
|
|
4137
|
-
function ListItemWrapper({
|
|
4138
|
-
stateKey,
|
|
4139
|
-
itemComponentId,
|
|
4140
|
-
itemPath,
|
|
4141
|
-
localIndex,
|
|
4142
|
-
arraySetter,
|
|
4143
|
-
rebuildStateShape,
|
|
4144
|
-
renderFn,
|
|
4145
|
-
}: {
|
|
4146
|
-
stateKey: string;
|
|
4147
|
-
itemComponentId: string;
|
|
4148
|
-
itemPath: string[];
|
|
4149
|
-
localIndex: number;
|
|
4150
|
-
arraySetter: any;
|
|
4151
|
-
|
|
4152
|
-
rebuildStateShape: (options: {
|
|
4153
|
-
currentState: any;
|
|
4154
|
-
path: string[];
|
|
4155
|
-
componentId: string;
|
|
4156
|
-
meta?: any;
|
|
4157
|
-
}) => any;
|
|
4158
|
-
renderFn: (
|
|
4159
|
-
setter: any,
|
|
4160
|
-
index: number,
|
|
4161
|
-
|
|
4162
|
-
arraySetter: any
|
|
4163
|
-
) => React.ReactNode;
|
|
4164
|
-
}) {
|
|
4165
|
-
const [, forceUpdate] = useState({});
|
|
4166
|
-
const { ref: inViewRef, inView } = useInView();
|
|
4167
|
-
const elementRef = useRef<HTMLDivElement | null>(null);
|
|
4168
|
-
|
|
4169
|
-
const imagesLoaded = useImageLoaded(elementRef);
|
|
4170
|
-
const hasReportedInitialHeight = useRef(false);
|
|
4171
|
-
const fullKey = [stateKey, ...itemPath].join('.');
|
|
4172
|
-
useRegisterComponent(stateKey, itemComponentId, forceUpdate);
|
|
4173
|
-
|
|
4174
|
-
const setRefs = useCallback(
|
|
4175
|
-
(element: HTMLDivElement | null) => {
|
|
4176
|
-
elementRef.current = element;
|
|
4177
|
-
inViewRef(element); // This is the ref from useInView
|
|
4178
|
-
},
|
|
4179
|
-
[inViewRef]
|
|
4180
|
-
);
|
|
4181
|
-
|
|
4182
|
-
useEffect(() => {
|
|
4183
|
-
getGlobalStore.getState().subscribeToPath(fullKey, (e) => {
|
|
4184
|
-
forceUpdate({});
|
|
4185
|
-
});
|
|
4186
|
-
}, []);
|
|
4187
|
-
useEffect(() => {
|
|
4188
|
-
if (!inView || !imagesLoaded || hasReportedInitialHeight.current) {
|
|
4189
|
-
return;
|
|
4190
|
-
}
|
|
4191
|
-
|
|
4192
|
-
const element = elementRef.current;
|
|
4193
|
-
if (element && element.offsetHeight > 0) {
|
|
4194
|
-
hasReportedInitialHeight.current = true;
|
|
4195
|
-
const newHeight = element.offsetHeight;
|
|
4196
|
-
|
|
4197
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, itemPath, {
|
|
4198
|
-
virtualizer: {
|
|
4199
|
-
itemHeight: newHeight,
|
|
4200
|
-
domRef: element,
|
|
4201
|
-
},
|
|
4202
|
-
});
|
|
4203
|
-
|
|
4204
|
-
const arrayPath = itemPath.slice(0, -1);
|
|
4205
|
-
const arrayPathKey = [stateKey, ...arrayPath].join('.');
|
|
4206
|
-
getGlobalStore.getState().notifyPathSubscribers(arrayPathKey, {
|
|
4207
|
-
type: 'ITEMHEIGHT',
|
|
4208
|
-
itemKey: itemPath.join('.'),
|
|
4209
|
-
|
|
4210
|
-
ref: elementRef.current,
|
|
4211
|
-
});
|
|
4212
|
-
}
|
|
4213
|
-
}, [inView, imagesLoaded, stateKey, itemPath]);
|
|
4214
|
-
|
|
4215
|
-
const fullItemPath = [stateKey, ...itemPath].join('.');
|
|
4216
|
-
const itemValue = getGlobalStore.getState().getShadowValue(fullItemPath);
|
|
4217
|
-
|
|
4218
|
-
if (itemValue === undefined) {
|
|
4219
|
-
return null;
|
|
4220
|
-
}
|
|
4221
|
-
|
|
4222
|
-
const itemSetter = rebuildStateShape({
|
|
4223
|
-
currentState: itemValue,
|
|
4224
|
-
path: itemPath,
|
|
4225
|
-
componentId: itemComponentId,
|
|
4226
|
-
});
|
|
4227
|
-
const children = renderFn(itemSetter, localIndex, arraySetter);
|
|
4228
|
-
|
|
4229
|
-
return <div ref={setRefs}>{children}</div>;
|
|
4230
|
-
}
|
|
4231
|
-
|
|
4232
|
-
function FormElementWrapper({
|
|
4233
|
-
stateKey,
|
|
4234
|
-
path,
|
|
4235
|
-
rebuildStateShape,
|
|
4236
|
-
renderFn,
|
|
4237
|
-
formOpts,
|
|
4238
|
-
setState,
|
|
4239
|
-
}: {
|
|
4240
|
-
stateKey: string;
|
|
4241
|
-
path: string[];
|
|
4242
|
-
rebuildStateShape: (options: {
|
|
4243
|
-
currentState: any;
|
|
4244
|
-
path: string[];
|
|
4245
|
-
componentId: string;
|
|
4246
|
-
meta?: any;
|
|
4247
|
-
}) => any;
|
|
4248
|
-
renderFn: (params: FormElementParams<any>) => React.ReactNode;
|
|
4249
|
-
formOpts?: FormOptsType;
|
|
4250
|
-
setState: any;
|
|
4251
|
-
}) {
|
|
4252
|
-
const [componentId] = useState(() => uuidv4());
|
|
4253
|
-
const [, forceUpdate] = useState({});
|
|
4254
|
-
|
|
4255
|
-
const stateKeyPathKey = [stateKey, ...path].join('.');
|
|
4256
|
-
useRegisterComponent(stateKey, componentId, forceUpdate);
|
|
4257
|
-
const globalStateValue = getGlobalStore
|
|
4258
|
-
.getState()
|
|
4259
|
-
.getShadowValue(stateKeyPathKey);
|
|
4260
|
-
const [localValue, setLocalValue] = useState<any>(globalStateValue);
|
|
4261
|
-
const isCurrentlyDebouncing = useRef(false);
|
|
4262
|
-
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
4263
|
-
|
|
4264
|
-
useEffect(() => {
|
|
4265
|
-
if (
|
|
4266
|
-
!isCurrentlyDebouncing.current &&
|
|
4267
|
-
!isDeepEqual(globalStateValue, localValue)
|
|
4268
|
-
) {
|
|
4269
|
-
setLocalValue(globalStateValue);
|
|
4270
|
-
}
|
|
4271
|
-
}, [globalStateValue]);
|
|
4272
|
-
|
|
4273
|
-
useEffect(() => {
|
|
4274
|
-
const unsubscribe = getGlobalStore
|
|
4275
|
-
.getState()
|
|
4276
|
-
.subscribeToPath(stateKeyPathKey, (newValue) => {
|
|
4277
|
-
if (!isCurrentlyDebouncing.current && localValue !== newValue) {
|
|
4278
|
-
forceUpdate({});
|
|
4279
|
-
}
|
|
4280
|
-
});
|
|
4281
|
-
return () => {
|
|
4282
|
-
unsubscribe();
|
|
4283
|
-
if (debounceTimeoutRef.current) {
|
|
4284
|
-
clearTimeout(debounceTimeoutRef.current);
|
|
4285
|
-
isCurrentlyDebouncing.current = false;
|
|
4286
|
-
}
|
|
4287
|
-
};
|
|
4288
|
-
}, []);
|
|
4289
|
-
|
|
4290
|
-
const debouncedUpdate = useCallback(
|
|
4291
|
-
(newValue: any) => {
|
|
4292
|
-
const currentType = typeof globalStateValue;
|
|
4293
|
-
if (currentType === 'number' && typeof newValue === 'string') {
|
|
4294
|
-
newValue = newValue === '' ? 0 : Number(newValue);
|
|
4295
|
-
}
|
|
4296
|
-
setLocalValue(newValue);
|
|
4297
|
-
isCurrentlyDebouncing.current = true;
|
|
4298
|
-
|
|
4299
|
-
if (debounceTimeoutRef.current) {
|
|
4300
|
-
clearTimeout(debounceTimeoutRef.current);
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
const debounceTime = formOpts?.debounceTime ?? 200;
|
|
4304
|
-
|
|
4305
|
-
debounceTimeoutRef.current = setTimeout(() => {
|
|
4306
|
-
isCurrentlyDebouncing.current = false;
|
|
4307
|
-
|
|
4308
|
-
// Update state
|
|
4309
|
-
setState(newValue, path, { updateType: 'update' });
|
|
4310
|
-
|
|
4311
|
-
// Perform LIVE validation (gentle)
|
|
4312
|
-
const { getInitialOptions, setShadowMetadata, getShadowMetadata } =
|
|
4313
|
-
getGlobalStore.getState();
|
|
4314
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4315
|
-
const zodSchema =
|
|
4316
|
-
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4317
|
-
|
|
4318
|
-
if (zodSchema) {
|
|
4319
|
-
const fullState = getGlobalStore.getState().getShadowValue(stateKey);
|
|
4320
|
-
const result = zodSchema.safeParse(fullState);
|
|
4321
|
-
|
|
4322
|
-
const currentMeta = getShadowMetadata(stateKey, path) || {};
|
|
4323
|
-
|
|
4324
|
-
if (!result.success) {
|
|
4325
|
-
const errors =
|
|
4326
|
-
'issues' in result.error
|
|
4327
|
-
? result.error.issues
|
|
4328
|
-
: (result.error as any).errors;
|
|
4329
|
-
const pathErrors = errors.filter(
|
|
4330
|
-
(error: any) =>
|
|
4331
|
-
JSON.stringify(error.path) === JSON.stringify(path)
|
|
4332
|
-
);
|
|
4333
|
-
|
|
4334
|
-
if (pathErrors.length > 0) {
|
|
4335
|
-
setShadowMetadata(stateKey, path, {
|
|
4336
|
-
...currentMeta,
|
|
4337
|
-
validation: {
|
|
4338
|
-
status: 'INVALID_LIVE',
|
|
4339
|
-
message: pathErrors[0]?.message,
|
|
4340
|
-
validatedValue: newValue,
|
|
4341
|
-
},
|
|
4342
|
-
});
|
|
4343
|
-
} else {
|
|
4344
|
-
// This field has no errors - clear validation
|
|
4345
|
-
setShadowMetadata(stateKey, path, {
|
|
4346
|
-
...currentMeta,
|
|
4347
|
-
validation: {
|
|
4348
|
-
status: 'VALID_LIVE',
|
|
4349
|
-
validatedValue: newValue,
|
|
4350
|
-
message: undefined,
|
|
4351
|
-
},
|
|
4352
|
-
});
|
|
4353
|
-
}
|
|
4354
|
-
} else {
|
|
4355
|
-
// Validation passed - clear any existing errors
|
|
4356
|
-
setShadowMetadata(stateKey, path, {
|
|
4357
|
-
...currentMeta,
|
|
4358
|
-
validation: {
|
|
4359
|
-
status: 'VALID_LIVE',
|
|
4360
|
-
validatedValue: newValue,
|
|
4361
|
-
message: undefined,
|
|
4362
|
-
},
|
|
4363
|
-
});
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
}, debounceTime);
|
|
4367
|
-
forceUpdate({});
|
|
4368
|
-
},
|
|
4369
|
-
[setState, path, formOpts?.debounceTime, stateKey]
|
|
4370
|
-
);
|
|
4371
|
-
|
|
4372
|
-
// --- NEW onBlur HANDLER ---
|
|
4373
|
-
// This replaces the old commented-out method with a modern approach.
|
|
4374
|
-
const handleBlur = useCallback(async () => {
|
|
4375
|
-
console.log('handleBlur triggered');
|
|
4376
|
-
|
|
4377
|
-
// Commit any pending changes
|
|
4378
|
-
if (debounceTimeoutRef.current) {
|
|
4379
|
-
clearTimeout(debounceTimeoutRef.current);
|
|
4380
|
-
debounceTimeoutRef.current = null;
|
|
4381
|
-
isCurrentlyDebouncing.current = false;
|
|
4382
|
-
setState(localValue, path, { updateType: 'update' });
|
|
4383
|
-
}
|
|
4384
|
-
|
|
4385
|
-
const { getInitialOptions } = getGlobalStore.getState();
|
|
4386
|
-
const validationOptions = getInitialOptions(stateKey)?.validation;
|
|
4387
|
-
const zodSchema =
|
|
4388
|
-
validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
|
|
4389
|
-
|
|
4390
|
-
if (!zodSchema) return;
|
|
4391
|
-
|
|
4392
|
-
// Get the full path including stateKey
|
|
4393
|
-
|
|
4394
|
-
// Update validation state to "validating"
|
|
4395
|
-
const currentMeta = getGlobalStore
|
|
4396
|
-
.getState()
|
|
4397
|
-
.getShadowMetadata(stateKey, path);
|
|
4398
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4399
|
-
...currentMeta,
|
|
4400
|
-
validation: {
|
|
4401
|
-
status: 'DIRTY',
|
|
4402
|
-
validatedValue: localValue,
|
|
4403
|
-
},
|
|
4404
|
-
});
|
|
4405
|
-
|
|
4406
|
-
// Validate full state
|
|
4407
|
-
const fullState = getGlobalStore.getState().getShadowValue(stateKey);
|
|
4408
|
-
const result = zodSchema.safeParse(fullState);
|
|
4409
|
-
console.log('result ', result);
|
|
4410
|
-
if (!result.success) {
|
|
4411
|
-
const errors =
|
|
4412
|
-
'issues' in result.error
|
|
4413
|
-
? result.error.issues
|
|
4414
|
-
: (result.error as any).errors;
|
|
4415
|
-
|
|
4416
|
-
console.log('All validation errors:', errors);
|
|
4417
|
-
console.log('Current blur path:', path);
|
|
4418
|
-
|
|
4419
|
-
// Find errors for this specific path
|
|
4420
|
-
const pathErrors = errors.filter((error: any) => {
|
|
4421
|
-
console.log('Processing error:', error);
|
|
4422
|
-
|
|
4423
|
-
// For array paths, we need to translate indices to ULIDs
|
|
4424
|
-
if (path.some((p) => p.startsWith('id:'))) {
|
|
4425
|
-
console.log('Detected array path with ULID');
|
|
4426
|
-
|
|
4427
|
-
// This is an array item path like ["id:xyz", "name"]
|
|
4428
|
-
const parentPath = path[0]!.startsWith('id:')
|
|
4429
|
-
? []
|
|
4430
|
-
: path.slice(0, -1);
|
|
4431
|
-
|
|
4432
|
-
console.log('Parent path:', parentPath);
|
|
4433
|
-
|
|
4434
|
-
const arrayMeta = getGlobalStore
|
|
4435
|
-
.getState()
|
|
4436
|
-
.getShadowMetadata(stateKey, parentPath);
|
|
4437
|
-
|
|
4438
|
-
console.log('Array metadata:', arrayMeta);
|
|
4439
|
-
|
|
4440
|
-
if (arrayMeta?.arrayKeys) {
|
|
4441
|
-
const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
|
|
4442
|
-
const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
|
|
4443
|
-
|
|
4444
|
-
console.log('Item key:', itemKey, 'Index:', itemIndex);
|
|
4445
|
-
|
|
4446
|
-
// Compare with Zod path
|
|
4447
|
-
const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
|
|
4448
|
-
const match =
|
|
4449
|
-
JSON.stringify(error.path) === JSON.stringify(zodPath);
|
|
4450
|
-
|
|
4451
|
-
console.log('Zod path comparison:', {
|
|
4452
|
-
zodPath,
|
|
4453
|
-
errorPath: error.path,
|
|
4454
|
-
match,
|
|
4455
|
-
});
|
|
4456
|
-
return match;
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
|
|
4460
|
-
const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
|
|
4461
|
-
console.log('Direct path comparison:', {
|
|
4462
|
-
errorPath: error.path,
|
|
4463
|
-
currentPath: path,
|
|
4464
|
-
match: directMatch,
|
|
4465
|
-
});
|
|
4466
|
-
return directMatch;
|
|
4467
|
-
});
|
|
4468
|
-
|
|
4469
|
-
console.log('Filtered path errors:', pathErrors);
|
|
4470
|
-
// Update shadow metadata with validation result
|
|
4471
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4472
|
-
...currentMeta,
|
|
4473
|
-
validation: {
|
|
4474
|
-
status: 'VALIDATION_FAILED',
|
|
4475
|
-
message: pathErrors[0]?.message,
|
|
4476
|
-
validatedValue: localValue,
|
|
4477
|
-
},
|
|
4478
|
-
});
|
|
4479
|
-
} else {
|
|
4480
|
-
// Validation passed
|
|
4481
|
-
getGlobalStore.getState().setShadowMetadata(stateKey, path, {
|
|
4482
|
-
...currentMeta,
|
|
4483
|
-
validation: {
|
|
4484
|
-
status: 'VALID_PENDING_SYNC',
|
|
4485
|
-
validatedValue: localValue,
|
|
4486
|
-
},
|
|
4487
|
-
});
|
|
4488
|
-
}
|
|
4489
|
-
forceUpdate({});
|
|
4490
|
-
}, [stateKey, path, localValue, setState]);
|
|
4491
|
-
|
|
4492
|
-
const baseState = rebuildStateShape({
|
|
4493
|
-
currentState: globalStateValue,
|
|
4494
|
-
path: path,
|
|
4495
|
-
componentId: componentId,
|
|
4496
|
-
});
|
|
4497
|
-
|
|
4498
|
-
const stateWithInputProps = new Proxy(baseState, {
|
|
4499
|
-
get(target, prop) {
|
|
4500
|
-
if (prop === 'inputProps') {
|
|
4501
|
-
return {
|
|
4502
|
-
value: localValue ?? '',
|
|
4503
|
-
onChange: (e: any) => {
|
|
4504
|
-
debouncedUpdate(e.target.value);
|
|
4505
|
-
},
|
|
4506
|
-
// 5. Wire the new onBlur handler to the input props.
|
|
4507
|
-
onBlur: handleBlur,
|
|
4508
|
-
ref: formRefStore
|
|
4509
|
-
.getState()
|
|
4510
|
-
.getFormRef(stateKey + '.' + path.join('.')),
|
|
4511
|
-
};
|
|
4512
|
-
}
|
|
4513
|
-
|
|
4514
|
-
return target[prop];
|
|
4515
|
-
},
|
|
4516
|
-
});
|
|
4517
|
-
|
|
4518
|
-
return (
|
|
4519
|
-
<ValidationWrapper formOpts={formOpts} path={path} stateKey={stateKey}>
|
|
4520
|
-
{renderFn(stateWithInputProps)}
|
|
4521
|
-
</ValidationWrapper>
|
|
4522
|
-
);
|
|
4523
|
-
}
|
|
4524
|
-
function useRegisterComponent(
|
|
4525
|
-
stateKey: string,
|
|
4526
|
-
componentId: string,
|
|
4527
|
-
forceUpdate: (o: object) => void
|
|
4528
|
-
) {
|
|
4529
|
-
const fullComponentId = `${stateKey}////${componentId}`;
|
|
4530
|
-
|
|
4531
|
-
useLayoutEffect(() => {
|
|
4532
|
-
const { registerComponent, unregisterComponent } =
|
|
4533
|
-
getGlobalStore.getState();
|
|
4534
|
-
|
|
4535
|
-
// Call the safe, centralized function to register
|
|
4536
|
-
registerComponent(stateKey, fullComponentId, {
|
|
4537
|
-
forceUpdate: () => forceUpdate({}),
|
|
4538
|
-
paths: new Set(),
|
|
4539
|
-
reactiveType: ['component'],
|
|
4540
|
-
});
|
|
4541
|
-
|
|
4542
|
-
// The cleanup now calls the safe, centralized unregister function
|
|
4543
|
-
return () => {
|
|
4544
|
-
unregisterComponent(stateKey, fullComponentId);
|
|
4545
|
-
};
|
|
4546
|
-
}, [stateKey, fullComponentId]); // Dependencies are stable and correct
|
|
4547
|
-
}
|