cogsbox-state 0.5.464 → 0.5.465

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/CogsState.tsx CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  } from 'react';
17
17
  import { createRoot } from 'react-dom/client';
18
18
  import {
19
- debounce,
20
19
  getDifferences,
21
20
  isArray,
22
21
  isFunction,
@@ -28,8 +27,11 @@ import superjson from 'superjson';
28
27
  import { v4 as uuidv4 } from 'uuid';
29
28
 
30
29
  import {
30
+ buildShadowNode,
31
31
  formRefStore,
32
32
  getGlobalStore,
33
+ METADATA_KEYS,
34
+ ValidationError,
33
35
  ValidationStatus,
34
36
  type ComponentsType,
35
37
  } from './store.js';
@@ -38,7 +40,6 @@ import { Operation } from 'fast-json-patch';
38
40
  import { useInView } from 'react-intersection-observer';
39
41
  import * as z3 from 'zod/v3';
40
42
  import * as z4 from 'zod/v4';
41
- import z from 'zod';
42
43
 
43
44
  type Prettify<T> = T extends any ? { [K in keyof T]: T[K] } : never;
44
45
 
@@ -245,10 +246,7 @@ export type UpdateType<T> = (payload: UpdateArg<T>) => { synced: () => void };
245
246
 
246
247
  export type InsertType<T> = (payload: InsertParams<T>, index?: number) => void;
247
248
  export type InsertTypeObj<T> = (payload: InsertParams<T>) => void;
248
- export type ValidationError = {
249
- path: (string | number)[];
250
- message: string;
251
- };
249
+
252
250
  type EffectFunction<T, R> = (state: T, deps: any[]) => R;
253
251
  export type EndType<T, IsArrayElement = false> = {
254
252
  addZodValidation: (errors: ValidationError[]) => void;
@@ -259,7 +257,7 @@ export type EndType<T, IsArrayElement = false> = {
259
257
  _stateKey: string;
260
258
  formElement: (control: FormControl<T>, opts?: FormOptsType) => JSX.Element;
261
259
  get: () => T;
262
- getState: () => T;
260
+
263
261
  $get: () => T;
264
262
  $derive: <R>(fn: EffectFunction<T, R>) => R;
265
263
 
@@ -306,7 +304,7 @@ export type StateObject<T> = (T extends any[]
306
304
  _isLoading: boolean;
307
305
  _serverState: T;
308
306
  revertToInitialState: (obj?: { validationKey?: string }) => T;
309
- getDifferences: () => string[];
307
+
310
308
  middleware: (
311
309
  middles: ({
312
310
  updateLog,
@@ -337,7 +335,8 @@ type UpdateOptions = {
337
335
  type EffectiveSetState<TStateObject> = (
338
336
  newStateOrFunction:
339
337
  | EffectiveSetStateArg<TStateObject, 'update'>
340
- | EffectiveSetStateArg<TStateObject, 'insert'>,
338
+ | EffectiveSetStateArg<TStateObject, 'insert'>
339
+ | null,
341
340
  path: string[],
342
341
  updateObj: UpdateOptions,
343
342
  validationKey?: string
@@ -449,10 +448,13 @@ type FormsElementsType<T> = {
449
448
  children: React.ReactNode;
450
449
  status: ValidationStatus; // Instead of 'active' boolean
451
450
 
451
+ hasErrors: boolean;
452
+ hasWarnings: boolean;
453
+ allErrors: ValidationError[];
454
+
452
455
  path: string[];
453
456
  message?: string;
454
- data?: T;
455
- key?: string;
457
+ getData?: () => T;
456
458
  }) => React.ReactNode;
457
459
  syncRender?: (options: SyncRenderOptions<T>) => React.ReactNode;
458
460
  };
@@ -476,11 +478,74 @@ export type TransformedStateType<T> = {
476
478
  [P in keyof T]: T[P] extends CogsInitialState<infer U> ? U : T[P];
477
479
  };
478
480
 
479
- function setAndMergeOptions(stateKey: string, newOptions: OptionsType<any>) {
480
- const getInitialOptions = getGlobalStore.getState().getInitialOptions;
481
- const setInitialStateOptions =
482
- getGlobalStore.getState().setInitialStateOptions;
481
+ const {
482
+ getInitialOptions,
483
+ updateInitialStateGlobal,
484
+ // ALIAS THE NEW FUNCTIONS TO THE OLD NAMES
485
+ getShadowMetadata,
486
+ setShadowMetadata,
487
+ getShadowValue,
488
+ initializeShadowState,
489
+ updateShadowAtPath,
490
+ insertShadowArrayElement,
491
+ removeShadowArrayElement,
492
+ getSelectedIndex,
493
+ setInitialStateOptions,
494
+ setServerStateUpdate,
495
+ markAsDirty,
496
+ registerComponent,
497
+ unregisterComponent,
498
+ addPathComponent,
499
+ clearSelectedIndexesForState,
500
+ addStateLog,
501
+ setSyncInfo,
502
+ clearSelectedIndex,
503
+ getSyncInfo,
504
+ notifyPathSubscribers,
505
+ subscribeToPath,
506
+ // Note: The old functions are no longer imported under their original names
507
+ } = getGlobalStore.getState();
508
+ function getArrayData(stateKey: string, path: string[], meta?: MetaData) {
509
+ const shadowMeta = getShadowMetadata(stateKey, path);
510
+ const isArray = !!shadowMeta?.arrayKeys;
483
511
 
512
+ if (!isArray) {
513
+ const value = getGlobalStore.getState().getShadowValue(stateKey, path);
514
+ return { isArray: false, value, keys: [] };
515
+ }
516
+
517
+ const arrayPathKey = path.join('.');
518
+ const viewIds = meta?.arrayViews?.[arrayPathKey] ?? shadowMeta.arrayKeys;
519
+
520
+ // FIX: If the derived view is empty, return an empty array and keys.
521
+ if (Array.isArray(viewIds) && viewIds.length === 0) {
522
+ return { isArray: true, value: [], keys: [] };
523
+ }
524
+
525
+ const value = getGlobalStore
526
+ .getState()
527
+ .getShadowValue(stateKey, path, viewIds);
528
+
529
+ return { isArray: true, value, keys: viewIds ?? [] };
530
+ }
531
+
532
+ function findArrayItem(
533
+ array: any[],
534
+ keys: string[],
535
+ predicate: (item: any, index: number) => boolean
536
+ ): { key: string; index: number; value: any } | null {
537
+ for (let i = 0; i < array.length; i++) {
538
+ if (predicate(array[i], i)) {
539
+ const key = keys[i];
540
+ if (key) {
541
+ return { key, index: i, value: array[i] };
542
+ }
543
+ }
544
+ }
545
+ return null;
546
+ }
547
+
548
+ function setAndMergeOptions(stateKey: string, newOptions: OptionsType<any>) {
484
549
  const initialOptions = getInitialOptions(stateKey as string) || {};
485
550
 
486
551
  setInitialStateOptions(stateKey as string, {
@@ -499,8 +564,7 @@ function setOptions<StateKey, Opt>({
499
564
  }) {
500
565
  const initialOptions = getInitialOptions(stateKey as string) || {};
501
566
  const initialOptionsPartState = initialOptionsPart[stateKey as string] || {};
502
- const setInitialStateOptions =
503
- getGlobalStore.getState().setInitialStateOptions;
567
+
504
568
  const mergedOptions = { ...initialOptionsPartState, ...initialOptions };
505
569
 
506
570
  let needToAdd = false;
@@ -621,10 +685,10 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
621
685
  const existingGlobalOptions = getInitialOptions(key);
622
686
 
623
687
  if (!existingGlobalOptions) {
624
- getGlobalStore.getState().setInitialStateOptions(key, mergedOptions);
688
+ setInitialStateOptions(key, mergedOptions);
625
689
  } else {
626
690
  // Merge with existing global options
627
- getGlobalStore.getState().setInitialStateOptions(key, {
691
+ setInitialStateOptions(key, {
628
692
  ...existingGlobalOptions,
629
693
  ...mergedOptions,
630
694
  });
@@ -633,9 +697,9 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
633
697
  });
634
698
 
635
699
  Object.keys(statePart).forEach((key) => {
636
- getGlobalStore.getState().initializeShadowState(key, statePart[key]);
700
+ initializeShadowState(key, statePart[key]);
637
701
  });
638
-
702
+ console.log('new stateObject ', getGlobalStore.getState().shadowStateStore);
639
703
  type StateKeys = keyof typeof statePart;
640
704
 
641
705
  const useCogsState = <StateKey extends StateKeys>(
@@ -650,8 +714,7 @@ export const createCogsState = <State extends Record<StateKeys, unknown>>(
650
714
  initialOptionsPart,
651
715
  });
652
716
  const thiState =
653
- getGlobalStore.getState().getShadowValue(stateKey as string) ||
654
- statePart[stateKey as string];
717
+ getShadowValue(stateKey as string, []) || statePart[stateKey as string];
655
718
  const partialState = options?.modifyState
656
719
  ? options.modifyState(thiState)
657
720
  : thiState;
@@ -774,12 +837,7 @@ export function createCogsStateFromSync<
774
837
  __syncSchemas: schemas,
775
838
  }) as any;
776
839
  }
777
- const {
778
- getInitialOptions,
779
840
 
780
- addStateLog,
781
- updateInitialStateGlobal,
782
- } = getGlobalStore.getState();
783
841
  const saveToLocalStorage = <T,>(
784
842
  state: T,
785
843
  thisKey: string,
@@ -811,7 +869,7 @@ const saveToLocalStorage = <T,>(
811
869
  } catch {
812
870
  // Ignore errors, will use undefined
813
871
  }
814
- const shadowMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
872
+ const shadowMeta = getShadowMetadata(thisKey, []);
815
873
 
816
874
  const data: LocalStorageData<T> = {
817
875
  state,
@@ -847,7 +905,7 @@ const loadFromLocalStorage = (localStorageKey: string) => {
847
905
  }
848
906
  };
849
907
  const loadAndApplyLocalStorage = (stateKey: string, options: any) => {
850
- const currentState = getGlobalStore.getState().getShadowValue(stateKey);
908
+ const currentState = getShadowValue(stateKey, []);
851
909
  const { sessionId } = useCogsConfig();
852
910
  const localkey = isFunction(options?.localStorage?.key)
853
911
  ? options.localStorage.key(currentState)
@@ -878,7 +936,7 @@ type LocalStorageData<T> = {
878
936
  };
879
937
 
880
938
  const notifyComponents = (thisKey: string) => {
881
- const stateEntry = getGlobalStore.getState().getShadowMetadata(thisKey, []);
939
+ const stateEntry = getShadowMetadata(thisKey, []);
882
940
  if (!stateEntry) return;
883
941
 
884
942
  // Batch component updates
@@ -900,40 +958,15 @@ const notifyComponents = (thisKey: string) => {
900
958
  });
901
959
  };
902
960
 
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
961
  function markEntireStateAsServerSynced(
927
962
  stateKey: string,
928
963
  path: string[],
929
964
  data: any,
930
965
  timestamp: number
931
966
  ) {
932
- const store = getGlobalStore.getState();
933
-
934
967
  // Mark current path as synced
935
- const currentMeta = store.getShadowMetadata(stateKey, path);
936
- store.setShadowMetadata(stateKey, path, {
968
+ const currentMeta = getShadowMetadata(stateKey, path);
969
+ setShadowMetadata(stateKey, path, {
937
970
  ...currentMeta,
938
971
  isDirty: false,
939
972
  stateSource: 'server',
@@ -942,10 +975,11 @@ function markEntireStateAsServerSynced(
942
975
 
943
976
  // If it's an array, mark each item as synced
944
977
  if (Array.isArray(data)) {
945
- const arrayMeta = store.getShadowMetadata(stateKey, path);
978
+ const arrayMeta = getShadowMetadata(stateKey, path);
946
979
  if (arrayMeta?.arrayKeys) {
947
980
  arrayMeta.arrayKeys.forEach((itemKey, index) => {
948
- const itemPath = itemKey.split('.').slice(1);
981
+ // Fix: Don't split the itemKey, just use it directly
982
+ const itemPath = [...path, itemKey];
949
983
  const itemData = data[index];
950
984
  if (itemData !== undefined) {
951
985
  markEntireStateAsServerSynced(
@@ -967,8 +1001,346 @@ function markEntireStateAsServerSynced(
967
1001
  });
968
1002
  }
969
1003
  }
970
- let updateBatchQueue = new Map<string, Array<UpdateArg<any>>>();
971
- let batchFlushScheduled = false;
1004
+ // 5. Batch queue
1005
+ let updateBatchQueue: any[] = [];
1006
+ let isFlushScheduled = false;
1007
+
1008
+ function scheduleFlush() {
1009
+ if (!isFlushScheduled) {
1010
+ isFlushScheduled = true;
1011
+ queueMicrotask(flushQueue);
1012
+ }
1013
+ }
1014
+ function handleUpdate(
1015
+ stateKey: string,
1016
+ path: string[],
1017
+ payload: any
1018
+ ): { type: 'update'; oldValue: any; newValue: any; shadowMeta: any } {
1019
+ // ✅ FIX: Get the old value before the update.
1020
+ const oldValue = getGlobalStore.getState().getShadowValue(stateKey, path);
1021
+
1022
+ const newValue = isFunction(payload) ? payload(oldValue) : payload;
1023
+
1024
+ // ✅ FIX: The new `updateShadowAtPath` handles metadata preservation automatically.
1025
+ // The manual loop has been removed.
1026
+ updateShadowAtPath(stateKey, path, newValue);
1027
+
1028
+ markAsDirty(stateKey, path, { bubble: true });
1029
+
1030
+ // Return the metadata of the node *after* the update.
1031
+ const newShadowMeta = getShadowMetadata(stateKey, path);
1032
+
1033
+ return {
1034
+ type: 'update',
1035
+ oldValue: oldValue,
1036
+ newValue,
1037
+ shadowMeta: newShadowMeta,
1038
+ };
1039
+ }
1040
+ // 2. Update signals
1041
+ function updateSignals(shadowMeta: any, displayValue: any) {
1042
+ if (!shadowMeta?.signals?.length) return;
1043
+
1044
+ shadowMeta.signals.forEach(({ parentId, position, effect }: any) => {
1045
+ const parent = document.querySelector(`[data-parent-id="${parentId}"]`);
1046
+ if (!parent) return;
1047
+
1048
+ const childNodes = Array.from(parent.childNodes);
1049
+ if (!childNodes[position]) return;
1050
+
1051
+ let finalDisplayValue = displayValue;
1052
+ if (effect && displayValue !== null) {
1053
+ try {
1054
+ finalDisplayValue = new Function('state', `return (${effect})(state)`)(
1055
+ displayValue
1056
+ );
1057
+ } catch (err) {
1058
+ console.error('Error evaluating effect function:', err);
1059
+ }
1060
+ }
1061
+
1062
+ if (finalDisplayValue !== null && typeof finalDisplayValue === 'object') {
1063
+ finalDisplayValue = JSON.stringify(finalDisplayValue);
1064
+ }
1065
+
1066
+ childNodes[position].textContent = String(finalDisplayValue ?? '');
1067
+ });
1068
+ }
1069
+
1070
+ function getComponentNotifications(
1071
+ stateKey: string,
1072
+ path: string[],
1073
+ result: any
1074
+ ): Set<any> {
1075
+ const rootMeta = getShadowMetadata(stateKey, []);
1076
+
1077
+ if (!rootMeta?.components) {
1078
+ return new Set();
1079
+ }
1080
+
1081
+ const componentsToNotify = new Set<any>();
1082
+
1083
+ // --- PASS 1: Notify specific subscribers based on update type ---
1084
+
1085
+ if (result.type === 'update') {
1086
+ // --- Bubble-up Notification ---
1087
+ // An update to `user.address.street` notifies listeners of `street`, `address`, and `user`.
1088
+ let currentPath = [...path];
1089
+ while (true) {
1090
+ const pathMeta = getShadowMetadata(stateKey, currentPath);
1091
+
1092
+ if (pathMeta?.pathComponents) {
1093
+ pathMeta.pathComponents.forEach((componentId: string) => {
1094
+ const component = rootMeta.components?.get(componentId);
1095
+ // NEW: Add component to the set instead of calling forceUpdate()
1096
+ if (component) {
1097
+ const reactiveTypes = Array.isArray(component.reactiveType)
1098
+ ? component.reactiveType
1099
+ : [component.reactiveType || 'component'];
1100
+ if (!reactiveTypes.includes('none')) {
1101
+ componentsToNotify.add(component);
1102
+ }
1103
+ }
1104
+ });
1105
+ }
1106
+
1107
+ if (currentPath.length === 0) break;
1108
+ currentPath.pop(); // Go up one level
1109
+ }
1110
+
1111
+ // --- Deep Object Change Notification ---
1112
+ // If the new value is an object, notify components subscribed to sub-paths that changed.
1113
+ if (
1114
+ result.newValue &&
1115
+ typeof result.newValue === 'object' &&
1116
+ !isArray(result.newValue)
1117
+ ) {
1118
+ const changedSubPaths = getDifferences(result.newValue, result.oldValue);
1119
+
1120
+ changedSubPaths.forEach((subPathString: string) => {
1121
+ const subPath = subPathString.split('.');
1122
+ const fullSubPath = [...path, ...subPath];
1123
+ const subPathMeta = getShadowMetadata(stateKey, fullSubPath);
1124
+
1125
+ if (subPathMeta?.pathComponents) {
1126
+ subPathMeta.pathComponents.forEach((componentId: string) => {
1127
+ const component = rootMeta.components?.get(componentId);
1128
+ // NEW: Add component to the set
1129
+ if (component) {
1130
+ const reactiveTypes = Array.isArray(component.reactiveType)
1131
+ ? component.reactiveType
1132
+ : [component.reactiveType || 'component'];
1133
+ if (!reactiveTypes.includes('none')) {
1134
+ componentsToNotify.add(component);
1135
+ }
1136
+ }
1137
+ });
1138
+ }
1139
+ });
1140
+ }
1141
+ } else if (result.type === 'insert' || result.type === 'cut') {
1142
+ // For array structural changes (add/remove), notify components listening to the parent array.
1143
+ const parentArrayPath = result.type === 'insert' ? path : path.slice(0, -1);
1144
+ const parentMeta = getShadowMetadata(stateKey, parentArrayPath);
1145
+
1146
+ if (parentMeta?.pathComponents) {
1147
+ parentMeta.pathComponents.forEach((componentId: string) => {
1148
+ const component = rootMeta.components?.get(componentId);
1149
+ // NEW: Add component to the set
1150
+ if (component) {
1151
+ componentsToNotify.add(component);
1152
+ }
1153
+ });
1154
+ }
1155
+ }
1156
+
1157
+ // --- PASS 2: Handle 'all' and 'deps' reactivity types ---
1158
+ // Iterate over all components for this stateKey that haven't been notified yet.
1159
+ rootMeta.components.forEach((component, componentId) => {
1160
+ // If we've already added this component, skip it.
1161
+ if (componentsToNotify.has(component)) {
1162
+ return;
1163
+ }
1164
+
1165
+ const reactiveTypes = Array.isArray(component.reactiveType)
1166
+ ? component.reactiveType
1167
+ : [component.reactiveType || 'component'];
1168
+
1169
+ if (reactiveTypes.includes('all')) {
1170
+ componentsToNotify.add(component);
1171
+ } else if (reactiveTypes.includes('deps') && component.depsFunction) {
1172
+ const currentState = getShadowValue(stateKey, []);
1173
+ const newDeps = component.depsFunction(currentState);
1174
+
1175
+ if (
1176
+ newDeps === true ||
1177
+ (Array.isArray(newDeps) && !isDeepEqual(component.prevDeps, newDeps))
1178
+ ) {
1179
+ component.prevDeps = newDeps as any; // Update the dependencies for the next check
1180
+ componentsToNotify.add(component);
1181
+ }
1182
+ }
1183
+ });
1184
+
1185
+ return componentsToNotify;
1186
+ }
1187
+
1188
+ function handleInsert(
1189
+ stateKey: string,
1190
+ path: string[],
1191
+ payload: any
1192
+ ): { type: 'insert'; newValue: any; shadowMeta: any } {
1193
+ let newValue;
1194
+ if (isFunction(payload)) {
1195
+ const { value: currentValue } = getScopedData(stateKey, path);
1196
+ newValue = payload({ state: currentValue, uuid: uuidv4() });
1197
+ } else {
1198
+ newValue = payload;
1199
+ }
1200
+
1201
+ insertShadowArrayElement(stateKey, path, newValue);
1202
+ markAsDirty(stateKey, path, { bubble: true });
1203
+
1204
+ const updatedMeta = getShadowMetadata(stateKey, path);
1205
+ if (updatedMeta?.arrayKeys) {
1206
+ const newItemKey = updatedMeta.arrayKeys[updatedMeta.arrayKeys.length - 1];
1207
+ if (newItemKey) {
1208
+ const newItemPath = newItemKey.split('.').slice(1);
1209
+ markAsDirty(stateKey, newItemPath, { bubble: false });
1210
+ }
1211
+ }
1212
+
1213
+ return { type: 'insert', newValue, shadowMeta: updatedMeta };
1214
+ }
1215
+
1216
+ function handleCut(
1217
+ stateKey: string,
1218
+ path: string[]
1219
+ ): { type: 'cut'; oldValue: any; parentPath: string[] } {
1220
+ const parentArrayPath = path.slice(0, -1);
1221
+ const oldValue = getShadowValue(stateKey, path);
1222
+ removeShadowArrayElement(stateKey, path);
1223
+ markAsDirty(stateKey, parentArrayPath, { bubble: true });
1224
+ return { type: 'cut', oldValue: oldValue, parentPath: parentArrayPath };
1225
+ }
1226
+
1227
+ function flushQueue() {
1228
+ const allComponentsToNotify = new Set<any>();
1229
+ const signalUpdates: { shadowMeta: any; displayValue: any }[] = [];
1230
+
1231
+ const logsToAdd: UpdateTypeDetail[] = [];
1232
+
1233
+ for (const item of updateBatchQueue) {
1234
+ if (item.status && item.updateType) {
1235
+ logsToAdd.push(item as UpdateTypeDetail);
1236
+ continue;
1237
+ }
1238
+
1239
+ const result = item;
1240
+
1241
+ const displayValue = result.type === 'cut' ? null : result.newValue;
1242
+ if (result.shadowMeta?.signals?.length > 0) {
1243
+ signalUpdates.push({ shadowMeta: result.shadowMeta, displayValue });
1244
+ }
1245
+
1246
+ const componentNotifications = getComponentNotifications(
1247
+ result.stateKey,
1248
+ result.path,
1249
+ result
1250
+ );
1251
+
1252
+ componentNotifications.forEach((component) => {
1253
+ allComponentsToNotify.add(component);
1254
+ });
1255
+ }
1256
+
1257
+ if (logsToAdd.length > 0) {
1258
+ addStateLog(logsToAdd);
1259
+ }
1260
+
1261
+ signalUpdates.forEach(({ shadowMeta, displayValue }) => {
1262
+ updateSignals(shadowMeta, displayValue);
1263
+ });
1264
+
1265
+ allComponentsToNotify.forEach((component) => {
1266
+ component.forceUpdate();
1267
+ });
1268
+
1269
+ // --- Step 3: CLEANUP ---
1270
+ // Clear the queue for the next batch of updates.
1271
+ updateBatchQueue = [];
1272
+ isFlushScheduled = false;
1273
+ }
1274
+
1275
+ function createEffectiveSetState<T>(
1276
+ thisKey: string,
1277
+ syncApiRef: React.MutableRefObject<any>,
1278
+ sessionId: string | undefined,
1279
+ latestInitialOptionsRef: React.MutableRefObject<OptionsType<T> | null>
1280
+ ): EffectiveSetState<T> {
1281
+ // The returned function is the core setter that gets called by all state operations.
1282
+ // It is now much simpler, delegating all work to the executeUpdate function.
1283
+ return (newStateOrFunction, path, updateObj, validationKey?) => {
1284
+ executeUpdate(thisKey, path, newStateOrFunction, updateObj);
1285
+ };
1286
+
1287
+ // This inner function handles the logic for a single state update.
1288
+ function executeUpdate(
1289
+ stateKey: string,
1290
+ path: string[],
1291
+ payload: any,
1292
+ options: UpdateOptions
1293
+ ) {
1294
+ // --- Step 1: Execute the core state change (Synchronous & Fast) ---
1295
+ // This part modifies the in-memory state representation immediately.
1296
+ let result: any;
1297
+ switch (options.updateType) {
1298
+ case 'update':
1299
+ result = handleUpdate(stateKey, path, payload);
1300
+ break;
1301
+ case 'insert':
1302
+ result = handleInsert(stateKey, path, payload);
1303
+ break;
1304
+ case 'cut':
1305
+ result = handleCut(stateKey, path);
1306
+ break;
1307
+ }
1308
+
1309
+ result.stateKey = stateKey;
1310
+ result.path = path;
1311
+ updateBatchQueue.push(result);
1312
+ scheduleFlush();
1313
+
1314
+ const newUpdate: UpdateTypeDetail = {
1315
+ timeStamp: Date.now(),
1316
+ stateKey,
1317
+ path,
1318
+ updateType: options.updateType,
1319
+ status: 'new',
1320
+ oldValue: result.oldValue,
1321
+ newValue: result.newValue ?? null,
1322
+ };
1323
+
1324
+ updateBatchQueue.push(newUpdate);
1325
+
1326
+ if (result.newValue !== undefined) {
1327
+ saveToLocalStorage(
1328
+ result.newValue,
1329
+ stateKey,
1330
+ latestInitialOptionsRef.current,
1331
+ sessionId
1332
+ );
1333
+ }
1334
+
1335
+ if (latestInitialOptionsRef.current?.middleware) {
1336
+ latestInitialOptionsRef.current.middleware({ update: newUpdate });
1337
+ }
1338
+
1339
+ if (options.sync !== false && syncApiRef.current?.connected) {
1340
+ syncApiRef.current.updateState({ operation: newUpdate });
1341
+ }
1342
+ }
1343
+ }
972
1344
 
973
1345
  export function useCogsStateFn<TStateObject extends unknown>(
974
1346
  stateObject: TStateObject,
@@ -1007,7 +1379,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1007
1379
  useEffect(() => {
1008
1380
  if (syncUpdate && syncUpdate.stateKey === thisKey && syncUpdate.path?.[0]) {
1009
1381
  const syncKey = `${syncUpdate.stateKey}:${syncUpdate.path.join('.')}`;
1010
- getGlobalStore.getState().setSyncInfo(syncKey, {
1382
+ setSyncInfo(syncKey, {
1011
1383
  timeStamp: syncUpdate.timeStamp!,
1012
1384
  userId: syncUpdate.userId!,
1013
1385
  });
@@ -1077,7 +1449,7 @@ export function useCogsStateFn<TStateObject extends unknown>(
1077
1449
 
1078
1450
  // Effect 1: When this component's serverState prop changes, broadcast it
1079
1451
  useEffect(() => {
1080
- getGlobalStore.getState().setServerStateUpdate(thisKey, serverState);
1452
+ setServerStateUpdate(thisKey, serverState);
1081
1453
  }, [serverState, thisKey]);
1082
1454
 
1083
1455
  // Effect 2: Listen for server state updates from ANY component
@@ -1087,115 +1459,79 @@ export function useCogsStateFn<TStateObject extends unknown>(
1087
1459
  .subscribeToPath(thisKey, (event) => {
1088
1460
  if (event?.type === 'SERVER_STATE_UPDATE') {
1089
1461
  const serverStateData = event.serverState;
1090
- console.log('SERVER_STATE_UPDATE', event);
1462
+
1091
1463
  if (
1092
- serverStateData?.status === 'success' &&
1093
- serverStateData.data !== undefined
1464
+ serverStateData?.status !== 'success' ||
1465
+ serverStateData.data === undefined
1094
1466
  ) {
1095
- const newOptions = { serverState: serverStateData };
1096
- setAndMergeOptions(thisKey, newOptions);
1097
-
1098
- const mergeConfig =
1099
- typeof serverStateData.merge === 'object'
1100
- ? serverStateData.merge
1101
- : serverStateData.merge === true
1102
- ? { strategy: 'append' }
1103
- : null;
1467
+ return; // Ignore if no valid data
1468
+ }
1104
1469
 
1105
- const currentState = getGlobalStore
1106
- .getState()
1107
- .getShadowValue(thisKey);
1108
- const incomingData = serverStateData.data;
1109
- if (
1110
- mergeConfig &&
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
- );
1470
+ console.log(
1471
+ '✅ SERVER_STATE_UPDATE received with data:',
1472
+ serverStateData
1473
+ );
1118
1474
 
1119
- const newUniqueItems = incomingData.filter((item: any) => {
1120
- return !existingIds.has(item[keyField]);
1121
- });
1475
+ setAndMergeOptions(thisKey, { serverState: serverStateData });
1122
1476
 
1123
- if (newUniqueItems.length > 0) {
1124
- newUniqueItems.forEach((item) => {
1125
- getGlobalStore
1126
- .getState()
1127
- .insertShadowArrayElement(thisKey, [], item);
1477
+ const mergeConfig =
1478
+ typeof serverStateData.merge === 'object'
1479
+ ? serverStateData.merge
1480
+ : serverStateData.merge === true
1481
+ ? { strategy: 'append' }
1482
+ : null;
1128
1483
 
1129
- // MARK NEW SERVER ITEMS AS SYNCED
1130
- const arrayMeta = getGlobalStore
1131
- .getState()
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
- });
1484
+ // FIX 1: The path for the root value is now `[]`.
1485
+ const currentState = getShadowValue(thisKey, []);
1486
+ const incomingData = serverStateData.data;
1149
1487
 
1150
- // Also mark all its child fields as synced if it's an object
1151
- const itemValue = getGlobalStore
1152
- .getState()
1153
- .getShadowValue(newItemKey);
1154
- if (
1155
- itemValue &&
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
1488
+ if (
1489
+ mergeConfig &&
1490
+ mergeConfig.strategy === 'append' &&
1491
+ 'key' in mergeConfig && // Type guard for key
1492
+ Array.isArray(currentState) &&
1493
+ Array.isArray(incomingData)
1494
+ ) {
1495
+ const keyField = mergeConfig.key;
1496
+ if (!keyField) {
1497
+ console.error(
1498
+ "CogsState: Merge strategy 'append' requires a 'key' field."
1187
1499
  );
1500
+ return;
1188
1501
  }
1502
+ console.log('SERVER_STATE_UPDATE 2');
1503
+ const existingIds = new Set(
1504
+ currentState.map((item: any) => item[keyField])
1505
+ );
1189
1506
 
1190
- const meta = getGlobalStore
1191
- .getState()
1192
- .getShadowMetadata(thisKey, []);
1193
- getGlobalStore.getState().setShadowMetadata(thisKey, [], {
1194
- ...meta,
1195
- stateSource: 'server',
1196
- lastServerSync: serverStateData.timestamp || Date.now(),
1197
- isDirty: false,
1198
- });
1507
+ const newUniqueItems = incomingData.filter(
1508
+ (item: any) => !existingIds.has(item[keyField])
1509
+ );
1510
+
1511
+ if (newUniqueItems.length > 0) {
1512
+ newUniqueItems.forEach((item) => {
1513
+ insertShadowArrayElement(thisKey, [], item);
1514
+ });
1515
+ }
1516
+
1517
+ // Mark the entire final state as synced
1518
+ const finalState = getShadowValue(thisKey, []);
1519
+ markEntireStateAsServerSynced(
1520
+ thisKey,
1521
+ [],
1522
+ finalState,
1523
+ serverStateData.timestamp
1524
+ );
1525
+ } else {
1526
+ // This handles the "replace" strategy (initial load)
1527
+ initializeShadowState(thisKey, incomingData);
1528
+
1529
+ markEntireStateAsServerSynced(
1530
+ thisKey,
1531
+ [],
1532
+ incomingData,
1533
+ serverStateData.timestamp
1534
+ );
1199
1535
  }
1200
1536
  }
1201
1537
  });
@@ -1213,6 +1549,17 @@ export function useCogsStateFn<TStateObject extends unknown>(
1213
1549
 
1214
1550
  const options = getInitialOptions(thisKey as string);
1215
1551
 
1552
+ const features = {
1553
+ syncEnabled: !!cogsSyncFn && !!syncOpt,
1554
+ validationEnabled: !!(
1555
+ options?.validation?.zodSchemaV4 || options?.validation?.zodSchemaV3
1556
+ ),
1557
+ localStorageEnabled: !!options?.localStorage?.key,
1558
+ };
1559
+ setShadowMetadata(thisKey, [], {
1560
+ ...existingMeta,
1561
+ features,
1562
+ });
1216
1563
  if (options?.defaultState !== undefined || defaultState !== undefined) {
1217
1564
  const finalDefaultState = options?.defaultState || defaultState;
1218
1565
 
@@ -1225,10 +1572,10 @@ export function useCogsStateFn<TStateObject extends unknown>(
1225
1572
 
1226
1573
  const { value: resolvedState, source, timestamp } = resolveInitialState();
1227
1574
 
1228
- getGlobalStore.getState().initializeShadowState(thisKey, resolvedState);
1575
+ initializeShadowState(thisKey, resolvedState);
1229
1576
 
1230
1577
  // Set shadow metadata with the correct source info
1231
- getGlobalStore.getState().setShadowMetadata(thisKey, [], {
1578
+ setShadowMetadata(thisKey, [], {
1232
1579
  stateSource: source,
1233
1580
  lastServerSync: source === 'server' ? timestamp : undefined,
1234
1581
  isDirty: false,
@@ -1252,29 +1599,27 @@ export function useCogsStateFn<TStateObject extends unknown>(
1252
1599
  const componentKey = `${thisKey}////${componentIdRef.current}`;
1253
1600
 
1254
1601
  // Register component in shadow metadata at root level
1255
- const rootMeta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1602
+ const rootMeta = getShadowMetadata(thisKey, []);
1256
1603
  const components = rootMeta?.components || new Map();
1257
1604
 
1258
1605
  components.set(componentKey, {
1259
1606
  forceUpdate: () => forceUpdate({}),
1260
- reactiveType: reactiveType ?? ['component', 'deps'],
1607
+ reactiveType: reactiveType ?? ['component'],
1261
1608
  paths: new Set(),
1262
1609
  depsFunction: reactiveDeps || undefined,
1263
- deps: reactiveDeps
1264
- ? reactiveDeps(getGlobalStore.getState().getShadowValue(thisKey))
1265
- : [],
1610
+ deps: reactiveDeps ? reactiveDeps(getShadowValue(thisKey, [])) : [],
1266
1611
  prevDeps: reactiveDeps // Initialize prevDeps with the same initial value
1267
- ? reactiveDeps(getGlobalStore.getState().getShadowValue(thisKey))
1612
+ ? reactiveDeps(getShadowValue(thisKey, []))
1268
1613
  : [],
1269
1614
  });
1270
1615
 
1271
- getGlobalStore.getState().setShadowMetadata(thisKey, [], {
1616
+ setShadowMetadata(thisKey, [], {
1272
1617
  ...rootMeta,
1273
1618
  components,
1274
1619
  });
1275
1620
  forceUpdate({});
1276
1621
  return () => {
1277
- const meta = getGlobalStore.getState().getShadowMetadata(thisKey, []);
1622
+ const meta = getShadowMetadata(thisKey, []);
1278
1623
  const component = meta?.components?.get(componentKey);
1279
1624
 
1280
1625
  // Remove from each path we registered to
@@ -1302,448 +1647,18 @@ export function useCogsStateFn<TStateObject extends unknown>(
1302
1647
 
1303
1648
  // Remove from root components
1304
1649
  if (meta?.components) {
1305
- getGlobalStore.getState().setShadowMetadata(thisKey, [], meta);
1650
+ setShadowMetadata(thisKey, [], meta);
1306
1651
  }
1307
1652
  };
1308
1653
  }, []);
1309
1654
 
1310
1655
  const syncApiRef = useRef<SyncApi | null>(null);
1311
-
1312
- const effectiveSetState = (
1313
- newStateOrFunction: UpdateArg<TStateObject> | InsertParams<TStateObject>,
1314
- path: string[],
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;
1322
-
1323
- const payload = (
1324
- updateObj.updateType === 'insert' && isFunction(newStateOrFunction)
1325
- ? newStateOrFunction({ state: nestedShadowValue, uuid: uuidv4() })
1326
- : isFunction(newStateOrFunction)
1327
- ? newStateOrFunction(nestedShadowValue)
1328
- : newStateOrFunction
1329
- ) as TStateObject;
1330
-
1331
- const timeStamp = Date.now();
1332
-
1333
- const newUpdate = {
1334
- timeStamp,
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);
1361
-
1362
- store.removeShadowArrayElement(thisKey, path);
1363
- store.markAsDirty(thisKey, parentArrayPath, { bubble: true });
1364
- break;
1365
- }
1366
- case 'update': {
1367
- store.updateShadowAtPath(thisKey, path, newUpdate.newValue);
1368
- store.markAsDirty(thisKey, path, { bubble: true });
1369
- break;
1370
- }
1371
- }
1372
-
1373
- const shouldSync = updateObj.sync !== false;
1374
-
1375
- if (shouldSync && syncApiRef.current && syncApiRef.current.connected) {
1376
- syncApiRef.current.updateState({ operation: newUpdate });
1377
- }
1378
- // Handle signals - reuse shadowMeta from the beginning
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
- }
1746
- };
1656
+ const effectiveSetState = createEffectiveSetState(
1657
+ thisKey,
1658
+ syncApiRef,
1659
+ sessionId,
1660
+ latestInitialOptionsRef
1661
+ );
1747
1662
 
1748
1663
  if (!getGlobalStore.getState().initialStateGlobal[thisKey]) {
1749
1664
  updateInitialStateGlobal(thisKey, stateObject);
@@ -1773,30 +1688,15 @@ export function useCogsStateFn<TStateObject extends unknown>(
1773
1688
  return updaterFinal;
1774
1689
  }
1775
1690
 
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
- */
1691
+ type MetaData = {
1692
+ // Map array paths to their filtered/sorted ID order
1693
+ arrayViews?: {
1694
+ [arrayPath: string]: string[]; // e.g. { "todos": ["id:xxx", "id:yyy"], "todos.id:xxx.subtasks": ["id:aaa"] }
1695
+ };
1797
1696
  transforms?: Array<{
1798
1697
  type: 'filter' | 'sort';
1799
1698
  fn: Function;
1699
+ path: string[]; // Which array this transform applies to
1800
1700
  }>;
1801
1701
  };
1802
1702
 
@@ -1811,35 +1711,40 @@ function hashTransforms(transforms: any[]) {
1811
1711
  )
1812
1712
  .join('');
1813
1713
  }
1714
+
1814
1715
  const applyTransforms = (
1815
1716
  stateKey: string,
1816
1717
  path: string[],
1817
1718
  transforms?: Array<{ type: 'filter' | 'sort'; fn: Function }>
1818
1719
  ): string[] => {
1819
- let arrayKeys =
1820
- getGlobalStore.getState().getShadowMetadata(stateKey, path)?.arrayKeys ||
1821
- [];
1822
-
1720
+ let ids = getShadowMetadata(stateKey, path)?.arrayKeys || [];
1721
+ console.log('ids', ids);
1823
1722
  if (!transforms || transforms.length === 0) {
1824
- return arrayKeys;
1723
+ return ids;
1825
1724
  }
1826
1725
 
1827
- let itemsWithKeys = arrayKeys.map((key) => ({
1828
- key,
1829
- value: getGlobalStore.getState().getShadowValue(key),
1830
- }));
1831
-
1726
+ // Apply each transform using just IDs
1832
1727
  for (const transform of transforms) {
1833
1728
  if (transform.type === 'filter') {
1834
- itemsWithKeys = itemsWithKeys.filter(({ value }, index) =>
1835
- transform.fn(value, index)
1836
- );
1729
+ const filtered: any[] = [];
1730
+ ids.forEach((id, index) => {
1731
+ const value = getShadowValue(stateKey, [...path, id]);
1732
+
1733
+ if (transform.fn(value, index)) {
1734
+ filtered.push(id);
1735
+ }
1736
+ });
1737
+ ids = filtered;
1837
1738
  } else if (transform.type === 'sort') {
1838
- itemsWithKeys.sort((a, b) => transform.fn(a.value, b.value));
1739
+ ids.sort((a, b) => {
1740
+ const aValue = getShadowValue(stateKey, [...path, a]);
1741
+ const bValue = getShadowValue(stateKey, [...path, b]);
1742
+ return transform.fn(aValue, bValue);
1743
+ });
1839
1744
  }
1840
1745
  }
1841
1746
 
1842
- return itemsWithKeys.map(({ key }) => key);
1747
+ return ids;
1843
1748
  };
1844
1749
  const registerComponentDependency = (
1845
1750
  stateKey: string,
@@ -1847,7 +1752,6 @@ const registerComponentDependency = (
1847
1752
  dependencyPath: string[]
1848
1753
  ) => {
1849
1754
  const fullComponentId = `${stateKey}////${componentId}`;
1850
- const { addPathComponent, getShadowMetadata } = getGlobalStore.getState();
1851
1755
 
1852
1756
  const rootMeta = getShadowMetadata(stateKey, []);
1853
1757
  const component = rootMeta?.components?.get(fullComponentId);
@@ -1911,6 +1815,28 @@ const notifySelectionComponents = (
1911
1815
  }
1912
1816
  }
1913
1817
  };
1818
+ function getScopedData(stateKey: string, path: string[], meta?: MetaData) {
1819
+ const shadowMeta = getShadowMetadata(stateKey, path);
1820
+ const arrayPathKey = path.join('.');
1821
+ const arrayKeys = meta?.arrayViews?.[arrayPathKey];
1822
+
1823
+ // FIX: If the derived view is empty, return an empty array directly.
1824
+ if (Array.isArray(arrayKeys) && arrayKeys.length === 0) {
1825
+ return {
1826
+ shadowMeta,
1827
+ value: [],
1828
+ arrayKeys: shadowMeta?.arrayKeys,
1829
+ };
1830
+ }
1831
+
1832
+ const value = getShadowValue(stateKey, path, arrayKeys);
1833
+
1834
+ return {
1835
+ shadowMeta,
1836
+ value,
1837
+ arrayKeys: shadowMeta?.arrayKeys,
1838
+ };
1839
+ }
1914
1840
 
1915
1841
  function createProxyHandler<T>(
1916
1842
  stateKey: string,
@@ -1921,7 +1847,57 @@ function createProxyHandler<T>(
1921
1847
  const proxyCache = new Map<string, any>();
1922
1848
  let stateVersion = 0;
1923
1849
 
1924
- let recursionTimerName: string | null = null;
1850
+ const methodNames = new Set([
1851
+ 'sync',
1852
+ 'getStatus',
1853
+ 'removeStorage',
1854
+ 'showValidationErrors',
1855
+ 'getSelected',
1856
+ 'getSelectedIndex',
1857
+ 'clearSelected',
1858
+ 'useVirtualView',
1859
+ 'stateMap',
1860
+ '$stateMap',
1861
+ 'stateFind',
1862
+ 'stateFilter',
1863
+ 'stateSort',
1864
+ 'stream',
1865
+ 'stateList',
1866
+ 'stateFlattenOn',
1867
+ 'index',
1868
+ 'last',
1869
+ 'insert',
1870
+ 'uniqueInsert',
1871
+ 'cut',
1872
+ 'cutSelected',
1873
+ 'cutByValue',
1874
+ 'toggleByValue',
1875
+ 'findWith',
1876
+ 'cutThis',
1877
+ 'get',
1878
+ 'getState',
1879
+ '$derive',
1880
+ '$get',
1881
+ 'lastSynced',
1882
+ 'getLocalStorage',
1883
+ 'isSelected',
1884
+ 'setSelected',
1885
+ 'toggleSelected',
1886
+ '_componentId',
1887
+ 'addZodValidation',
1888
+ 'clearZodValidation',
1889
+ 'applyJsonPatch',
1890
+ 'getComponents',
1891
+ 'getAllFormRefs',
1892
+ 'getFormRef',
1893
+ 'validationWrapper',
1894
+ '_stateKey',
1895
+ '_path',
1896
+ 'update',
1897
+ 'toggle',
1898
+ 'formElement',
1899
+ // Add ANY other method names here
1900
+ ]);
1925
1901
 
1926
1902
  function rebuildStateShape({
1927
1903
  path = [],
@@ -1933,64 +1909,34 @@ function createProxyHandler<T>(
1933
1909
  meta?: MetaData;
1934
1910
  }): any {
1935
1911
  const derivationSignature = meta
1936
- ? JSON.stringify(meta.validIds || meta.transforms)
1912
+ ? JSON.stringify(meta.arrayViews || meta.transforms)
1937
1913
  : '';
1938
1914
  const cacheKey = path.join('.') + ':' + derivationSignature;
1939
- console.log('PROXY CACHE KEY ', cacheKey);
1940
1915
  if (proxyCache.has(cacheKey)) {
1941
- console.log('PROXY CACHE HIT');
1942
1916
  return proxyCache.get(cacheKey);
1943
1917
  }
1944
1918
  const stateKeyPathKey = [stateKey, ...path].join('.');
1945
1919
 
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
1920
  // We attach baseObj properties *inside* the get trap now to avoid recursion
1957
1921
  // This is a placeholder for the proxy.
1958
1922
 
1959
1923
  const handler = {
1960
1924
  get(target: any, prop: string) {
1925
+ if (path.length === 0 && prop in rootLevelMethods) {
1926
+ return rootLevelMethods[prop as keyof typeof rootLevelMethods];
1927
+ }
1928
+ if (!methodNames.has(prop)) {
1929
+ const nextPath = [...path, prop];
1930
+ return rebuildStateShape({
1931
+ path: nextPath,
1932
+ componentId: componentId!,
1933
+ meta,
1934
+ });
1935
+ }
1961
1936
  if (prop === '_rebuildStateShape') {
1962
1937
  return rebuildStateShape;
1963
1938
  }
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
1939
 
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
-
1991
- return getDifferences(currentState, baseState);
1992
- };
1993
- }
1994
1940
  if (prop === 'sync' && path.length === 0) {
1995
1941
  return async function () {
1996
1942
  const options = getGlobalStore
@@ -2031,7 +1977,7 @@ function createProxyHandler<T>(
2031
1977
  const shadowMeta = getGlobalStore
2032
1978
  .getState()
2033
1979
  .getShadowMetadata(stateKey, []);
2034
- getGlobalStore.getState().setShadowMetadata(stateKey, [], {
1980
+ setShadowMetadata(stateKey, [], {
2035
1981
  ...shadowMeta,
2036
1982
  isDirty: false,
2037
1983
  lastServerSync: Date.now(),
@@ -2055,56 +2001,46 @@ function createProxyHandler<T>(
2055
2001
  // Fixed getStatus function in createProxyHandler
2056
2002
  if (prop === '_status' || prop === 'getStatus') {
2057
2003
  const getStatusFunc = () => {
2058
- const shadowMeta = getGlobalStore
2059
- .getState()
2060
- .getShadowMetadata(stateKey, path);
2061
- const value = getGlobalStore
2062
- .getState()
2063
- .getShadowValue(stateKeyPathKey);
2004
+ // Use the optimized helper to get all data in one efficient call
2005
+ const { shadowMeta, value } = getScopedData(stateKey, path, meta);
2064
2006
 
2065
- // Priority 1: Explicitly dirty items
2007
+ // Priority 1: Explicitly dirty items. This is the most important status.
2066
2008
  if (shadowMeta?.isDirty === true) {
2067
2009
  return 'dirty';
2068
2010
  }
2069
2011
 
2070
- // Priority 2: Explicitly synced items (isDirty: false)
2071
- if (shadowMeta?.isDirty === false) {
2072
- return 'synced';
2073
- }
2074
-
2075
- // Priority 3: Items from server source (should be synced even without explicit isDirty flag)
2076
- if (shadowMeta?.stateSource === 'server') {
2012
+ // Priority 2: Synced items. This condition is now cleaner.
2013
+ // An item is considered synced if it came from the server OR was explicitly
2014
+ // marked as not dirty (isDirty: false), covering all sync-related cases.
2015
+ if (
2016
+ shadowMeta?.stateSource === 'server' ||
2017
+ shadowMeta?.isDirty === false
2018
+ ) {
2077
2019
  return 'synced';
2078
2020
  }
2079
2021
 
2080
- // Priority 4: Items restored from localStorage
2022
+ // Priority 3: Items restored from localStorage.
2081
2023
  if (shadowMeta?.stateSource === 'localStorage') {
2082
2024
  return 'restored';
2083
2025
  }
2084
2026
 
2085
- // Priority 5: Items from default/initial state
2027
+ // Priority 4: Items from default/initial state.
2086
2028
  if (shadowMeta?.stateSource === 'default') {
2087
2029
  return 'fresh';
2088
2030
  }
2089
2031
 
2090
- // Priority 6: Check if this is part of initial server load
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
- }
2032
+ // REMOVED the redundant "root" check. The item's own `stateSource` is sufficient.
2098
2033
 
2099
- // Priority 7: If no metadata exists but value exists, it's probably fresh
2034
+ // Priority 5: A value exists but has no metadata. This is a fallback.
2100
2035
  if (value !== undefined && !shadowMeta) {
2101
2036
  return 'fresh';
2102
2037
  }
2103
2038
 
2104
- // Fallback
2039
+ // Fallback if no other condition is met.
2105
2040
  return 'unknown';
2106
2041
  };
2107
2042
 
2043
+ // This part remains the same
2108
2044
  return prop === '_status' ? getStatusFunc() : getStatusFunc;
2109
2045
  }
2110
2046
  if (prop === 'removeStorage') {
@@ -2121,14 +2057,15 @@ function createProxyHandler<T>(
2121
2057
  }
2122
2058
  if (prop === 'showValidationErrors') {
2123
2059
  return () => {
2124
- const meta = getGlobalStore
2125
- .getState()
2126
- .getShadowMetadata(stateKey, path);
2060
+ const { shadowMeta } = getScopedData(stateKey, path, meta);
2127
2061
  if (
2128
- meta?.validation?.status === 'VALIDATION_FAILED' &&
2129
- meta.validation.message
2062
+ shadowMeta?.validation?.status === 'INVALID' &&
2063
+ shadowMeta.validation.errors.length > 0
2130
2064
  ) {
2131
- return [meta.validation.message];
2065
+ // Return only error-severity messages (not warnings)
2066
+ return shadowMeta.validation.errors
2067
+ .filter((err) => err.severity === 'error')
2068
+ .map((err) => err.message);
2132
2069
  }
2133
2070
  return [];
2134
2071
  };
@@ -2136,55 +2073,77 @@ function createProxyHandler<T>(
2136
2073
 
2137
2074
  if (prop === 'getSelected') {
2138
2075
  return () => {
2139
- const fullKey = stateKey + '.' + path.join('.');
2076
+ const arrayKey = [stateKey, ...path].join('.');
2140
2077
  registerComponentDependency(stateKey, componentId, [
2141
2078
  ...path,
2142
2079
  'getSelected',
2143
2080
  ]);
2144
2081
 
2145
- const selectedIndicesMap =
2146
- getGlobalStore.getState().selectedIndicesMap;
2147
- if (!selectedIndicesMap || !selectedIndicesMap.has(fullKey)) {
2082
+ const selectedItemKey = getGlobalStore
2083
+ .getState()
2084
+ .selectedIndicesMap.get(arrayKey);
2085
+ if (!selectedItemKey) {
2148
2086
  return undefined;
2149
2087
  }
2150
2088
 
2151
- const selectedItemKey = selectedIndicesMap.get(fullKey)!;
2152
- if (meta?.validIds) {
2153
- if (!meta.validIds.includes(selectedItemKey)) {
2154
- return undefined;
2155
- }
2156
- }
2089
+ const viewKey = path.join('.');
2090
+ const currentViewIds = meta?.arrayViews?.[viewKey];
2091
+ const selectedItemId = selectedItemKey.split('.').pop();
2157
2092
 
2158
- const value = getGlobalStore
2159
- .getState()
2160
- .getShadowValue(selectedItemKey);
2093
+ // FIX: Only return the selected item if it exists in the current filtered/sorted view.
2094
+ if (currentViewIds && !currentViewIds.includes(selectedItemId!)) {
2095
+ return undefined;
2096
+ }
2161
2097
 
2162
- if (!value) {
2098
+ const value = getShadowValue(
2099
+ stateKey,
2100
+ selectedItemKey.split('.').slice(1)
2101
+ );
2102
+ if (value === undefined) {
2163
2103
  return undefined;
2164
2104
  }
2165
2105
 
2166
2106
  return rebuildStateShape({
2167
2107
  path: selectedItemKey.split('.').slice(1) as string[],
2168
2108
  componentId: componentId!,
2109
+ meta,
2169
2110
  });
2170
2111
  };
2171
2112
  }
2172
2113
  if (prop === 'getSelectedIndex') {
2173
2114
  return () => {
2174
- const selectedIndex = getGlobalStore
2115
+ // Key for the array in the global selection map (e.g., "myState.products")
2116
+ const arrayKey = stateKey + '.' + path.join('.');
2117
+ // Key for this specific view in the meta object (e.g., "products")
2118
+ const viewKey = path.join('.');
2119
+
2120
+ // Get the full path of the selected item (e.g., "myState.products.id:abc")
2121
+ const selectedItemKey = getGlobalStore
2175
2122
  .getState()
2176
- .getSelectedIndex(
2177
- stateKey + '.' + path.join('.'),
2178
- meta?.validIds
2179
- );
2123
+ .selectedIndicesMap.get(arrayKey);
2180
2124
 
2181
- return selectedIndex;
2125
+ if (!selectedItemKey) {
2126
+ return -1; // Nothing is selected for this array.
2127
+ }
2128
+
2129
+ // Get the list of item IDs for the current filtered/sorted view.
2130
+ const { keys: viewIds } = getArrayData(stateKey, path, meta);
2131
+
2132
+ if (!viewIds) {
2133
+ return -1; // Should not happen if it's an array, but a safe guard.
2134
+ }
2135
+
2136
+ // FIX: Extract just the ID from the full selected item path.
2137
+ const selectedId = selectedItemKey.split('.').pop();
2138
+
2139
+ // Return the index of that ID within the current view's list of IDs.
2140
+ return (viewIds as string[]).indexOf(selectedId as string);
2182
2141
  };
2183
2142
  }
2184
2143
  if (prop === 'clearSelected') {
2185
2144
  notifySelectionComponents(stateKey, path);
2186
2145
  return () => {
2187
- getGlobalStore.getState().clearSelectedIndex({
2146
+ clearSelectedIndex({
2188
2147
  arrayKey: stateKey + '.' + path.join('.'),
2189
2148
  });
2190
2149
  };
@@ -2238,9 +2197,7 @@ function createProxyHandler<T>(
2238
2197
  });
2239
2198
  }, [rerender, stickToBottom]);
2240
2199
 
2241
- const arrayKeys =
2242
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2243
- ?.arrayKeys || [];
2200
+ const { arrayKeys = [] } = getScopedData(stateKey, path, meta);
2244
2201
 
2245
2202
  // Calculate total height and offsets
2246
2203
  const { totalHeight, itemOffsets } = useMemo(() => {
@@ -2518,9 +2475,7 @@ function createProxyHandler<T>(
2518
2475
  // Create virtual state
2519
2476
  const virtualState = useMemo(() => {
2520
2477
  const store = getGlobalStore.getState();
2521
- const sourceArray = store.getShadowValue(
2522
- [stateKey, ...path].join('.')
2523
- ) as any[];
2478
+ const sourceArray = store.getShadowValue(stateKey, path) as any[];
2524
2479
  const currentKeys =
2525
2480
  store.getShadowMetadata(stateKey, path)?.arrayKeys || [];
2526
2481
 
@@ -2532,11 +2487,11 @@ function createProxyHandler<T>(
2532
2487
  range.startIndex,
2533
2488
  range.endIndex + 1
2534
2489
  );
2535
-
2490
+ const arrayPath = path.length > 0 ? path.join('.') : 'root';
2536
2491
  return rebuildStateShape({
2537
2492
  path,
2538
2493
  componentId: componentId!,
2539
- meta: { ...meta, validIds: slicedIds },
2494
+ meta: { ...meta, arrayViews: { [arrayPath]: slicedIds } },
2540
2495
  });
2541
2496
  }, [range.startIndex, range.endIndex, arrayKeys.length]);
2542
2497
 
@@ -2583,150 +2538,84 @@ function createProxyHandler<T>(
2583
2538
  }
2584
2539
  if (prop === 'stateMap') {
2585
2540
  return (
2586
- callbackfn: (
2587
- setter: any,
2588
- index: number,
2589
-
2590
- arraySetter: any
2591
- ) => void
2541
+ callbackfn: (setter: any, index: number, arraySetter: any) => void
2592
2542
  ) => {
2593
- const [arrayKeys, setArrayKeys] = useState<any>(
2594
- meta?.validIds ??
2595
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2596
- ?.arrayKeys
2543
+ // FIX: Use getArrayData to reliably get both the value and the keys of the current view.
2544
+ const { value: shadowValue, keys: arrayKeys } = getArrayData(
2545
+ stateKey,
2546
+ path,
2547
+ meta
2597
2548
  );
2598
- // getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
2599
- // console.log(
2600
- // "stateKeyPathKeyccccccccccccccccc",
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');
2549
+
2550
+ if (!arrayKeys || !Array.isArray(shadowValue)) {
2551
+ return []; // It's valid to map over an empty array.
2613
2552
  }
2553
+
2614
2554
  const arraySetter = rebuildStateShape({
2615
2555
  path,
2616
2556
  componentId: componentId!,
2617
2557
  meta,
2618
- });
2619
-
2620
- return shadowValue.map((item, index) => {
2621
- const itemPath = arrayKeys[index]?.split('.').slice(1);
2622
- const itemSetter = rebuildStateShape({
2623
- path: itemPath as any,
2624
- componentId: componentId!,
2625
- meta,
2626
- });
2627
-
2628
- return callbackfn(
2629
- itemSetter,
2630
- index,
2631
-
2632
- arraySetter
2633
- );
2634
- });
2635
- };
2636
- }
2637
-
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
2558
+ });
2669
2559
 
2670
- const itemValue = getGlobalStore
2671
- .getState()
2672
- .getShadowValue(itemKey);
2560
+ return shadowValue.map((_item, index) => {
2561
+ const itemKey = arrayKeys[index];
2562
+ if (!itemKey) return undefined;
2673
2563
 
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);
2564
+ // FIX: Construct the correct path to the item in the original store.
2565
+ // The path is the array's path plus the specific item's unique key.
2566
+ const itemPath = [...path, itemKey];
2678
2567
 
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
- }
2568
+ const itemSetter = rebuildStateShape({
2569
+ path: itemPath, // This now correctly points to the item in the shadow store.
2570
+ componentId: componentId!,
2571
+ meta,
2572
+ });
2687
2573
 
2688
- // 5. If the loop finishes without finding anything, return undefined.
2689
- return undefined;
2574
+ return callbackfn(itemSetter, index, arraySetter);
2575
+ });
2690
2576
  };
2691
2577
  }
2578
+
2692
2579
  if (prop === 'stateFilter') {
2693
2580
  return (callbackfn: (value: any, index: number) => boolean) => {
2694
- const currentState = getGlobalStore
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;
2581
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2582
+
2583
+ // FIX: Get keys from `getArrayData` which correctly resolves them from meta or the full list.
2584
+ const { keys: currentViewIds, value: array } = getArrayData(
2585
+ stateKey,
2586
+ path,
2587
+ meta
2588
+ );
2702
2589
 
2703
- if (!arrayKeys) {
2704
- throw new Error('No array keys found for filtering.');
2590
+ if (!Array.isArray(array)) {
2591
+ throw new Error('stateFilter can only be used on arrays');
2705
2592
  }
2706
2593
 
2707
- const newValidIds: string[] = [];
2708
- const filteredArray = currentState.filter(
2709
- (val: any, index: number) => {
2710
- const didPass = callbackfn(val, index);
2711
- if (didPass) {
2712
- newValidIds.push(arrayKeys[index]!);
2713
- return true;
2594
+ // Filter the array and collect the IDs of the items that pass
2595
+ const filteredIds: string[] = [];
2596
+ array.forEach((item, index) => {
2597
+ if (callbackfn(item, index)) {
2598
+ // currentViewIds[index] is the original ID before filtering
2599
+ const id = currentViewIds[index];
2600
+ if (id) {
2601
+ filteredIds.push(id);
2714
2602
  }
2715
- return false;
2716
2603
  }
2717
- );
2604
+ });
2718
2605
 
2606
+ // The rest is the same...
2719
2607
  return rebuildStateShape({
2720
2608
  path,
2721
2609
  componentId: componentId!,
2722
2610
  meta: {
2723
- validIds: newValidIds,
2611
+ ...meta,
2612
+ arrayViews: {
2613
+ ...(meta?.arrayViews || {}),
2614
+ [arrayPathKey]: filteredIds,
2615
+ },
2724
2616
  transforms: [
2725
2617
  ...(meta?.transforms || []),
2726
- {
2727
- type: 'filter',
2728
- fn: callbackfn,
2729
- },
2618
+ { type: 'filter', fn: callbackfn, path },
2730
2619
  ],
2731
2620
  },
2732
2621
  });
@@ -2734,34 +2623,39 @@ function createProxyHandler<T>(
2734
2623
  }
2735
2624
  if (prop === 'stateSort') {
2736
2625
  return (compareFn: (a: any, b: any) => number) => {
2737
- const currentState = getGlobalStore
2738
- .getState()
2739
- .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
2740
- if (!Array.isArray(currentState)) return []; // Guard clause
2741
- const arrayKeys =
2742
- meta?.validIds ??
2743
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
2744
- ?.arrayKeys;
2745
- if (!arrayKeys) {
2626
+ const arrayPathKey = path.join('.');
2627
+
2628
+ // FIX: Use the more robust `getArrayData` which always correctly resolves the keys for a view.
2629
+ const { value: currentArray, keys: currentViewIds } = getArrayData(
2630
+ stateKey,
2631
+ path,
2632
+ meta
2633
+ );
2634
+
2635
+ if (!Array.isArray(currentArray) || !currentViewIds) {
2746
2636
  throw new Error('No array keys found for sorting');
2747
2637
  }
2748
- const itemsWithIds = currentState.map((item, index) => ({
2638
+
2639
+ // ... (rest of the function is the same and now works)
2640
+ const itemsWithIds = currentArray.map((item, index) => ({
2749
2641
  item,
2750
- key: arrayKeys[index],
2642
+ key: currentViewIds[index],
2751
2643
  }));
2752
-
2753
- itemsWithIds
2754
- .sort((a, b) => compareFn(a.item, b.item))
2755
- .filter(Boolean);
2644
+ itemsWithIds.sort((a, b) => compareFn(a.item, b.item));
2645
+ const sortedIds = itemsWithIds.map((i) => i.key as string);
2756
2646
 
2757
2647
  return rebuildStateShape({
2758
2648
  path,
2759
2649
  componentId: componentId!,
2760
2650
  meta: {
2761
- validIds: itemsWithIds.map((i) => i.key) as string[],
2651
+ ...meta,
2652
+ arrayViews: {
2653
+ ...(meta?.arrayViews || {}),
2654
+ [arrayPathKey]: sortedIds,
2655
+ },
2762
2656
  transforms: [
2763
2657
  ...(meta?.transforms || []),
2764
- { type: 'sort', fn: compareFn },
2658
+ { type: 'sort', fn: compareFn, path },
2765
2659
  ],
2766
2660
  },
2767
2661
  });
@@ -2835,12 +2729,11 @@ function createProxyHandler<T>(
2835
2729
  }
2836
2730
 
2837
2731
  const streamId = uuidv4();
2838
- const currentMeta =
2839
- getGlobalStore.getState().getShadowMetadata(stateKey, path) || {};
2732
+ const currentMeta = getShadowMetadata(stateKey, path) || {};
2840
2733
  const streams = currentMeta.streams || new Map();
2841
2734
  streams.set(streamId, { buffer, flushTimer });
2842
2735
 
2843
- getGlobalStore.getState().setShadowMetadata(stateKey, path, {
2736
+ setShadowMetadata(stateKey, path, {
2844
2737
  ...currentMeta,
2845
2738
  streams,
2846
2739
  });
@@ -2879,58 +2772,45 @@ function createProxyHandler<T>(
2879
2772
  arraySetter: any
2880
2773
  ) => ReactNode
2881
2774
  ) => {
2775
+ console.log('meta outside', JSON.stringify(meta));
2882
2776
  const StateListWrapper = () => {
2883
2777
  const componentIdsRef = useRef<Map<string, string>>(new Map());
2884
2778
 
2885
- const cacheKey =
2886
- meta?.transforms && meta.transforms.length > 0
2887
- ? `${componentId}-${hashTransforms(meta.transforms)}`
2888
- : `${componentId}-base`;
2889
-
2890
2779
  const [updateTrigger, forceUpdate] = useState({});
2891
2780
 
2892
- const { validIds, arrayValues } = useMemo(() => {
2893
- const cached = getGlobalStore
2894
- .getState()
2895
- .getShadowMetadata(stateKey, path)
2896
- ?.transformCaches?.get(cacheKey);
2897
-
2898
- let freshValidIds: string[];
2899
-
2900
- if (cached && cached.validIds) {
2901
- freshValidIds = cached.validIds;
2902
- } else {
2903
- freshValidIds = applyTransforms(
2904
- stateKey,
2905
- path,
2906
- meta?.transforms
2907
- );
2781
+ console.log('updateTrigger updateTrigger updateTrigger');
2908
2782
 
2909
- getGlobalStore
2910
- .getState()
2911
- .setTransformCache(stateKey, path, cacheKey, {
2912
- validIds: freshValidIds,
2913
- computedAt: Date.now(),
2914
- transforms: meta?.transforms || [],
2915
- });
2916
- }
2783
+ const validIds = applyTransforms(
2784
+ stateKey,
2785
+ path,
2786
+ meta?.transforms
2787
+ );
2788
+ //the above get the new coorect valid ids i need ot udpate the meta object with this info
2789
+ const arrayPathKey = path.length > 0 ? path.join('.') : 'root';
2790
+ const updatedMeta = {
2791
+ ...meta,
2792
+ arrayViews: {
2793
+ ...(meta?.arrayViews || {}),
2794
+ [arrayPathKey]: validIds, // Update the arrayViews with the new valid IDs
2795
+ },
2796
+ };
2917
2797
 
2918
- const freshValues = getGlobalStore
2919
- .getState()
2920
- .getShadowValue(stateKeyPathKey, freshValidIds);
2798
+ // Now use the updated meta when getting array data
2799
+ const { value: arrayValues } = getArrayData(
2800
+ stateKey,
2801
+ path,
2802
+ updatedMeta
2803
+ );
2921
2804
 
2922
- return {
2923
- validIds: freshValidIds,
2924
- arrayValues: freshValues || [],
2925
- };
2926
- }, [cacheKey, updateTrigger]);
2805
+ console.log('validIds', validIds);
2806
+ console.log('arrayValues', arrayValues);
2927
2807
 
2928
2808
  useEffect(() => {
2929
2809
  const unsubscribe = getGlobalStore
2930
2810
  .getState()
2931
2811
  .subscribeToPath(stateKeyPathKey, (e) => {
2932
2812
  // A data change has occurred for the source array.
2933
-
2813
+ console.log('changed array statelist ', e);
2934
2814
  if (e.type === 'GET_SELECTED') {
2935
2815
  return;
2936
2816
  }
@@ -2970,14 +2850,13 @@ function createProxyHandler<T>(
2970
2850
  return null;
2971
2851
  }
2972
2852
 
2853
+ // Continue using updatedMeta for the rest of your logic instead of meta
2973
2854
  const arraySetter = rebuildStateShape({
2974
2855
  path,
2975
2856
  componentId: componentId!,
2976
- meta: {
2977
- ...meta,
2978
- validIds: validIds,
2979
- },
2857
+ meta: updatedMeta, // Use updated meta here
2980
2858
  });
2859
+ console.log('arrayValues', arrayValues);
2981
2860
 
2982
2861
  return (
2983
2862
  <>
@@ -2994,7 +2873,7 @@ function createProxyHandler<T>(
2994
2873
  componentIdsRef.current.set(itemKey, itemComponentId);
2995
2874
  }
2996
2875
 
2997
- const itemPath = itemKey.split('.').slice(1);
2876
+ const itemPath = [...path, itemKey];
2998
2877
 
2999
2878
  return createElement(MemoizedCogsItemWrapper, {
3000
2879
  key: itemKey,
@@ -3016,16 +2895,18 @@ function createProxyHandler<T>(
3016
2895
  }
3017
2896
  if (prop === 'stateFlattenOn') {
3018
2897
  return (fieldName: string) => {
2898
+ // FIX: Get the definitive list of IDs for the current view from meta.arrayViews.
2899
+ const arrayPathKey = path.join('.');
2900
+ const viewIds = meta?.arrayViews?.[arrayPathKey];
2901
+
3019
2902
  const currentState = getGlobalStore
3020
2903
  .getState()
3021
- .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3022
- if (!Array.isArray(currentState)) return []; // Guard clause
3023
- const arrayToMap = currentState as any[];
2904
+ .getShadowValue(stateKey, path, viewIds);
2905
+
2906
+ if (!Array.isArray(currentState)) return [];
3024
2907
 
3025
2908
  stateVersion++;
3026
- const flattenedResults = arrayToMap.flatMap(
3027
- (val: any) => val[fieldName] ?? []
3028
- );
2909
+
3029
2910
  return rebuildStateShape({
3030
2911
  path: [...path, '[*]', fieldName],
3031
2912
  componentId: componentId!,
@@ -3035,36 +2916,55 @@ function createProxyHandler<T>(
3035
2916
  }
3036
2917
  if (prop === 'index') {
3037
2918
  return (index: number) => {
3038
- const arrayKeys = getGlobalStore
3039
- .getState()
3040
- .getShadowMetadata(stateKey, path)
3041
- ?.arrayKeys?.filter(
3042
- (key) =>
3043
- !meta?.validIds ||
3044
- (meta?.validIds && meta?.validIds?.includes(key))
3045
- );
3046
- const itemId = arrayKeys?.[index];
2919
+ const arrayPathKey = path.join('.');
2920
+ const viewIds = meta?.arrayViews?.[arrayPathKey];
2921
+
2922
+ if (viewIds) {
2923
+ const itemId = viewIds[index];
2924
+ if (!itemId) return undefined;
2925
+ return rebuildStateShape({
2926
+ path: [...path, itemId],
2927
+ componentId: componentId!,
2928
+ meta,
2929
+ });
2930
+ }
2931
+
2932
+ // ✅ FIX: Get the metadata and use the `arrayKeys` property.
2933
+ const shadowMeta = getShadowMetadata(stateKey, path);
2934
+ if (!shadowMeta?.arrayKeys) return undefined;
2935
+
2936
+ const itemId = shadowMeta.arrayKeys[index];
3047
2937
  if (!itemId) return undefined;
3048
- const value = getGlobalStore
3049
- .getState()
3050
- .getShadowValue(itemId, meta?.validIds);
3051
- const state = rebuildStateShape({
3052
- path: itemId.split('.').slice(1) as string[],
2938
+
2939
+ return rebuildStateShape({
2940
+ path: [...path, itemId],
3053
2941
  componentId: componentId!,
3054
2942
  meta,
3055
2943
  });
3056
- return state;
3057
2944
  };
3058
2945
  }
3059
2946
  if (prop === 'last') {
3060
2947
  return () => {
3061
- const currentArray = getGlobalStore
3062
- .getState()
3063
- .getShadowValue(stateKey, path) as any[];
3064
- if (currentArray.length === 0) return undefined;
3065
- const lastIndex = currentArray.length - 1;
3066
- const lastValue = currentArray[lastIndex];
3067
- const newPath = [...path, lastIndex.toString()];
2948
+ // FIX: Use getArrayData to get the keys for the current view (filtered or not).
2949
+ const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
2950
+
2951
+ // If the array is empty, there is no last item.
2952
+ if (!currentViewIds || currentViewIds.length === 0) {
2953
+ return undefined;
2954
+ }
2955
+
2956
+ // Get the unique ID of the last item in the current view.
2957
+ const lastItemKey = currentViewIds[currentViewIds.length - 1];
2958
+
2959
+ // If for some reason the key is invalid, return undefined.
2960
+ if (!lastItemKey) {
2961
+ return undefined;
2962
+ }
2963
+
2964
+ // ✅ FIX: The new path uses the item's unique key, not its numerical index.
2965
+ const newPath = [...path, lastItemKey];
2966
+
2967
+ // Return a new proxy scoped to that specific item.
3068
2968
  return rebuildStateShape({
3069
2969
  path: newPath,
3070
2970
  componentId: componentId!,
@@ -3078,11 +2978,6 @@ function createProxyHandler<T>(
3078
2978
  index?: number
3079
2979
  ) => {
3080
2980
  effectiveSetState(payload as any, path, { updateType: 'insert' });
3081
- return rebuildStateShape({
3082
- path,
3083
- componentId: componentId!,
3084
- meta,
3085
- });
3086
2981
  };
3087
2982
  }
3088
2983
  if (prop === 'uniqueInsert') {
@@ -3091,9 +2986,13 @@ function createProxyHandler<T>(
3091
2986
  fields?: (keyof InferArrayElement<T>)[],
3092
2987
  onMatch?: (existingItem: any) => any
3093
2988
  ) => {
3094
- const currentArray = getGlobalStore
3095
- .getState()
3096
- .getShadowValue(stateKey, path) as any[];
2989
+ const { value: currentArray } = getScopedData(
2990
+ stateKey,
2991
+ path,
2992
+ meta
2993
+ ) as {
2994
+ value: any[];
2995
+ };
3097
2996
  const newValue = isFunction<T>(payload)
3098
2997
  ? payload(currentArray as any)
3099
2998
  : (payload as any);
@@ -3123,167 +3022,133 @@ function createProxyHandler<T>(
3123
3022
  }
3124
3023
  };
3125
3024
  }
3126
-
3127
3025
  if (prop === 'cut') {
3128
3026
  return (index?: number, options?: { waitForSync?: boolean }) => {
3129
- const currentState = getGlobalStore
3130
- .getState()
3131
- .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3132
- const validKeys =
3133
- meta?.validIds ??
3134
- getGlobalStore.getState().getShadowMetadata(stateKey, path)
3135
- ?.arrayKeys;
3136
-
3137
- if (!validKeys || validKeys.length === 0) return;
3027
+ const shadowMeta = getShadowMetadata(stateKey, path);
3028
+ if (!shadowMeta?.arrayKeys || shadowMeta.arrayKeys.length === 0)
3029
+ return;
3138
3030
 
3139
3031
  const indexToCut =
3140
- index == -1
3141
- ? validKeys.length - 1
3032
+ index === -1
3033
+ ? shadowMeta.arrayKeys.length - 1
3142
3034
  : index !== undefined
3143
3035
  ? index
3144
- : validKeys.length - 1;
3036
+ : shadowMeta.arrayKeys.length - 1;
3145
3037
 
3146
- const fullIdToCut = validKeys[indexToCut];
3147
- if (!fullIdToCut) return; // Index out of bounds
3038
+ const idToCut = shadowMeta.arrayKeys[indexToCut];
3039
+ if (!idToCut) return;
3148
3040
 
3149
- const pathForCut = fullIdToCut.split('.').slice(1);
3150
- effectiveSetState(currentState, pathForCut, {
3041
+ effectiveSetState(null, [...path, idToCut], {
3151
3042
  updateType: 'cut',
3152
3043
  });
3153
3044
  };
3154
3045
  }
3155
3046
  if (prop === 'cutSelected') {
3156
3047
  return () => {
3157
- const validKeys = applyTransforms(stateKey, path, meta?.transforms);
3158
- const currentState = getGlobalStore
3159
- .getState()
3160
- .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3161
- if (!validKeys || validKeys.length === 0) return;
3048
+ const arrayKey = [stateKey, ...path].join('.');
3162
3049
 
3163
- const indexKeyToCut = getGlobalStore
3050
+ const { keys: currentViewIds } = getArrayData(stateKey, path, meta);
3051
+ if (!currentViewIds || currentViewIds.length === 0) {
3052
+ return;
3053
+ }
3054
+ const selectedItemKey = getGlobalStore
3164
3055
  .getState()
3165
- .selectedIndicesMap.get(stateKeyPathKey);
3056
+ .selectedIndicesMap.get(arrayKey);
3166
3057
 
3167
- let indexToCut = validKeys.findIndex(
3168
- (key) => key === indexKeyToCut
3169
- );
3058
+ if (!selectedItemKey) {
3059
+ return;
3060
+ }
3061
+ const selectedId = selectedItemKey.split('.').pop() as string;
3062
+ if (!(currentViewIds as any[]).includes(selectedId!)) {
3063
+ return;
3064
+ }
3065
+ const pathForCut = selectedItemKey.split('.').slice(1);
3066
+ getGlobalStore.getState().clearSelectedIndex({ arrayKey });
3170
3067
 
3171
- const pathForCut = validKeys[
3172
- indexToCut == -1 ? validKeys.length - 1 : indexToCut
3173
- ]
3174
- ?.split('.')
3175
- .slice(1);
3176
- getGlobalStore
3177
- .getState()
3178
- .clearSelectedIndex({ arrayKey: stateKeyPathKey });
3179
- const parentPath = pathForCut?.slice(0, -1)!;
3068
+ const parentPath = pathForCut.slice(0, -1);
3180
3069
  notifySelectionComponents(stateKey, parentPath);
3181
- effectiveSetState(currentState, pathForCut!, {
3070
+
3071
+ effectiveSetState(null, pathForCut, {
3182
3072
  updateType: 'cut',
3183
3073
  });
3184
3074
  };
3185
3075
  }
3186
3076
  if (prop === 'cutByValue') {
3187
3077
  return (value: string | number | boolean) => {
3188
- // Step 1: Get the list of all unique keys for the current view.
3189
- const arrayMeta = getGlobalStore
3190
- .getState()
3191
- .getShadowMetadata(stateKey, path);
3192
- const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
3193
-
3194
- if (!relevantKeys) return;
3195
-
3196
- let keyToCut: string | null = null;
3078
+ const {
3079
+ isArray,
3080
+ value: array,
3081
+ keys,
3082
+ } = getArrayData(stateKey, path, meta);
3197
3083
 
3198
- // Step 2: Iterate through the KEYS, get the value for each, and find the match.
3199
- for (const key of relevantKeys) {
3200
- const itemValue = getGlobalStore.getState().getShadowValue(key);
3201
- if (itemValue === value) {
3202
- keyToCut = key;
3203
- break; // We found the key, no need to search further.
3204
- }
3205
- }
3084
+ if (!isArray) return;
3206
3085
 
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' });
3086
+ const found = findArrayItem(array, keys, (item) => item === value);
3087
+ if (found) {
3088
+ effectiveSetState(null, [...path, found.key], {
3089
+ updateType: 'cut',
3090
+ });
3211
3091
  }
3212
3092
  };
3213
3093
  }
3214
3094
 
3215
3095
  if (prop === 'toggleByValue') {
3216
3096
  return (value: string | number | boolean) => {
3217
- // Step 1: Get the list of all unique keys for the current view.
3218
- const arrayMeta = getGlobalStore
3219
- .getState()
3220
- .getShadowMetadata(stateKey, path);
3221
- const relevantKeys = meta?.validIds ?? arrayMeta?.arrayKeys;
3097
+ const {
3098
+ isArray,
3099
+ value: array,
3100
+ keys,
3101
+ } = getArrayData(stateKey, path, meta);
3222
3102
 
3223
- if (!relevantKeys) return;
3103
+ if (!isArray) return;
3224
3104
 
3225
- let keyToCut: string | null = null;
3105
+ const found = findArrayItem(array, keys, (item) => item === value);
3226
3106
 
3227
- // Step 2: Iterate through the KEYS to find the one matching the value. This is the robust way.
3228
- for (const key of relevantKeys) {
3229
- const itemValue = getGlobalStore.getState().getShadowValue(key);
3230
- console.log('itemValue sdasdasdasd', itemValue);
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, {
3107
+ if (found) {
3108
+ const pathForItem = [...path, found.key];
3109
+
3110
+ effectiveSetState(null, pathForItem, {
3243
3111
  updateType: 'cut',
3244
3112
  });
3245
3113
  } else {
3246
- // Item does not exist, so we INSERT it.
3247
3114
  effectiveSetState(value as any, path, { updateType: 'insert' });
3248
3115
  }
3249
3116
  };
3250
3117
  }
3251
3118
  if (prop === 'findWith') {
3252
- return (searchKey: keyof InferArrayElement<T>, searchValue: any) => {
3253
- const arrayKeys = getGlobalStore
3254
- .getState()
3255
- .getShadowMetadata(stateKey, path)?.arrayKeys;
3119
+ return (searchKey: string, searchValue: any) => {
3120
+ const { isArray, value, keys } = getArrayData(stateKey, path, meta);
3256
3121
 
3257
- if (!arrayKeys) {
3258
- throw new Error('No array keys found for sorting');
3122
+ if (!isArray) {
3123
+ throw new Error('findWith can only be used on arrays');
3259
3124
  }
3260
3125
 
3261
- let value = null;
3262
- let foundPath: string[] = [];
3126
+ const found = findArrayItem(
3127
+ value,
3128
+ keys,
3129
+ (item) => item?.[searchKey] === searchValue
3130
+ );
3263
3131
 
3264
- for (const fullPath of arrayKeys) {
3265
- let shadowValue = getGlobalStore
3266
- .getState()
3267
- .getShadowValue(fullPath, meta?.validIds);
3268
- if (shadowValue && shadowValue[searchKey] === searchValue) {
3269
- value = shadowValue;
3270
- foundPath = fullPath.split('.').slice(1);
3271
- break;
3272
- }
3132
+ // FIX: If found, return a proxy to the item by appending its key to the current path.
3133
+ if (found) {
3134
+ return rebuildStateShape({
3135
+ path: [...path, found.key], // e.g., ['itemInstances', 'inst-1', 'properties', 'prop-b']
3136
+ componentId: componentId!,
3137
+ meta,
3138
+ });
3273
3139
  }
3274
3140
 
3141
+ // If not found, return an 'empty' proxy that will resolve to undefined on .get()
3142
+ // This prevents "cannot read property 'get' of undefined" errors.
3275
3143
  return rebuildStateShape({
3276
- path: foundPath,
3144
+ path: [...path, `not_found_${uuidv4()}`],
3277
3145
  componentId: componentId!,
3278
3146
  meta,
3279
3147
  });
3280
3148
  };
3281
3149
  }
3282
-
3283
3150
  if (prop === 'cutThis') {
3284
- let shadowValue = getGlobalStore
3285
- .getState()
3286
- .getShadowValue(path.join('.'));
3151
+ const { value: shadowValue } = getScopedData(stateKey, path, meta);
3287
3152
 
3288
3153
  return () => {
3289
3154
  effectiveSetState(shadowValue, path, { updateType: 'cut' });
@@ -3293,16 +3158,8 @@ function createProxyHandler<T>(
3293
3158
  if (prop === 'get') {
3294
3159
  return () => {
3295
3160
  registerComponentDependency(stateKey, componentId, path);
3296
- return getGlobalStore
3297
- .getState()
3298
- .getShadowValue(stateKeyPathKey, meta?.validIds);
3299
- };
3300
- }
3301
- if (prop === 'getState') {
3302
- return () => {
3303
- return getGlobalStore
3304
- .getState()
3305
- .getShadowValue(stateKeyPathKey, meta?.validIds);
3161
+ const { value } = getScopedData(stateKey, path, meta);
3162
+ return value;
3306
3163
  };
3307
3164
  }
3308
3165
 
@@ -3323,32 +3180,27 @@ function createProxyHandler<T>(
3323
3180
  }
3324
3181
  if (prop === 'lastSynced') {
3325
3182
  const syncKey = `${stateKey}:${path.join('.')}`;
3326
- return getGlobalStore.getState().getSyncInfo(syncKey);
3183
+ return getSyncInfo(syncKey);
3327
3184
  }
3328
3185
  if (prop == 'getLocalStorage') {
3329
3186
  return (key: string) =>
3330
3187
  loadFromLocalStorage(sessionId + '-' + stateKey + '-' + key);
3331
3188
  }
3332
-
3333
3189
  if (prop === 'isSelected') {
3334
- const parentPath = [stateKey, ...path].slice(0, -1);
3335
- notifySelectionComponents(stateKey, path, undefined);
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('.');
3190
+ const parentPathArray = path.slice(0, -1);
3191
+ const parentMeta = getShadowMetadata(stateKey, parentPathArray);
3345
3192
 
3193
+ // FIX: Check if the parent is an array by looking for arrayKeys in its metadata.
3194
+ if (parentMeta?.arrayKeys) {
3195
+ const fullParentKey = stateKey + '.' + parentPathArray.join('.');
3346
3196
  const selectedItemKey = getGlobalStore
3347
3197
  .getState()
3348
3198
  .selectedIndicesMap.get(fullParentKey);
3349
3199
 
3350
3200
  const fullItemKey = stateKey + '.' + path.join('.');
3351
3201
 
3202
+ // Logic remains the same.
3203
+ notifySelectionComponents(stateKey, parentPathArray, undefined);
3352
3204
  return selectedItemKey === fullItemKey;
3353
3205
  }
3354
3206
  return undefined;
@@ -3402,11 +3254,6 @@ function createProxyHandler<T>(
3402
3254
  if (path.length == 0) {
3403
3255
  if (prop === 'addZodValidation') {
3404
3256
  return (zodErrors: any[]) => {
3405
- const init = getGlobalStore
3406
- .getState()
3407
- .getInitialOptions(stateKey)?.validation;
3408
-
3409
- // For each error, set shadow metadata
3410
3257
  zodErrors.forEach((error) => {
3411
3258
  const currentMeta =
3412
3259
  getGlobalStore
@@ -3418,42 +3265,38 @@ function createProxyHandler<T>(
3418
3265
  .setShadowMetadata(stateKey, error.path, {
3419
3266
  ...currentMeta,
3420
3267
  validation: {
3421
- status: 'VALIDATION_FAILED',
3422
- message: error.message,
3268
+ status: 'INVALID',
3269
+ errors: [
3270
+ {
3271
+ source: 'client',
3272
+ message: error.message,
3273
+ severity: 'error',
3274
+ code: error.code,
3275
+ },
3276
+ ],
3277
+ lastValidated: Date.now(),
3423
3278
  validatedValue: undefined,
3424
3279
  },
3425
3280
  });
3426
- getGlobalStore.getState().notifyPathSubscribers(error.path, {
3427
- type: 'VALIDATION_FAILED',
3428
- message: error.message,
3429
- validatedValue: undefined,
3430
- });
3431
3281
  });
3432
3282
  };
3433
3283
  }
3434
3284
  if (prop === 'clearZodValidation') {
3435
3285
  return (path?: string[]) => {
3436
- // Clear specific paths
3437
3286
  if (!path) {
3438
3287
  throw new Error('clearZodValidation requires a path');
3439
- return;
3440
3288
  }
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
3289
 
3451
- getGlobalStore
3452
- .getState()
3453
- .notifyPathSubscribers([stateKey, ...path].join('.'), {
3454
- type: 'VALIDATION_CLEARED',
3455
- });
3456
- }
3290
+ const currentMeta = getShadowMetadata(stateKey, path) || {};
3291
+
3292
+ setShadowMetadata(stateKey, path, {
3293
+ ...currentMeta,
3294
+ validation: {
3295
+ status: 'NOT_VALIDATED',
3296
+ errors: [],
3297
+ lastValidated: Date.now(),
3298
+ },
3299
+ });
3457
3300
  };
3458
3301
  }
3459
3302
  if (prop === 'applyJsonPatch') {
@@ -3484,6 +3327,7 @@ function createProxyHandler<T>(
3484
3327
  value: any;
3485
3328
  };
3486
3329
  store.updateShadowAtPath(stateKey, relativePath, value);
3330
+
3487
3331
  store.markAsDirty(stateKey, relativePath, { bubble: true });
3488
3332
 
3489
3333
  // Bubble up - notify components at this path and all parent paths
@@ -3548,9 +3392,7 @@ function createProxyHandler<T>(
3548
3392
  }
3549
3393
 
3550
3394
  if (prop === 'getComponents')
3551
- return () =>
3552
- getGlobalStore.getState().getShadowMetadata(stateKey, [])
3553
- ?.components;
3395
+ return () => getShadowMetadata(stateKey, [])?.components;
3554
3396
  if (prop === 'getAllFormRefs')
3555
3397
  return () =>
3556
3398
  formRefStore.getState().getFormRefsByStateKey(stateKey);
@@ -3581,80 +3423,34 @@ function createProxyHandler<T>(
3581
3423
  if (prop === '_stateKey') return stateKey;
3582
3424
  if (prop === '_path') return path;
3583
3425
  if (prop === 'update') {
3426
+ // This method is now greatly simplified.
3427
+ // All the complex batching logic has been removed because our new,
3428
+ // universal `createEffectiveSetState` function handles it automatically for all operations.
3584
3429
  return (payload: UpdateArg<T>) => {
3585
- // Check if we're in a React event handler
3586
- const error = new Error();
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
- }
3430
+ // Simply call effectiveSetState. It will automatically handle queuing
3431
+ // this operation in the batch for efficient processing.
3432
+ effectiveSetState(payload as any, path, { updateType: 'update' });
3642
3433
 
3434
+ // The .synced() method is a useful feature that allows developers
3435
+ // to manually mark a piece of state as "synced with the server"
3436
+ // after an update. This part of the functionality is preserved.
3643
3437
  return {
3644
3438
  synced: () => {
3645
3439
  const shadowMeta = getGlobalStore
3646
3440
  .getState()
3647
3441
  .getShadowMetadata(stateKey, path);
3648
3442
 
3649
- getGlobalStore.getState().setShadowMetadata(stateKey, path, {
3443
+ // Update the metadata for this specific path
3444
+ setShadowMetadata(stateKey, path, {
3650
3445
  ...shadowMeta,
3651
3446
  isDirty: false,
3652
3447
  stateSource: 'server',
3653
3448
  lastServerSync: Date.now(),
3654
3449
  });
3655
3450
 
3451
+ // Notify any components that might be subscribed to the sync status
3656
3452
  const fullPath = [stateKey, ...path].join('.');
3657
- getGlobalStore.getState().notifyPathSubscribers(fullPath, {
3453
+ notifyPathSubscribers(fullPath, {
3658
3454
  type: 'SYNC_STATUS_CHANGE',
3659
3455
  isDirty: false,
3660
3456
  });
@@ -3662,11 +3458,12 @@ function createProxyHandler<T>(
3662
3458
  };
3663
3459
  };
3664
3460
  }
3665
-
3666
3461
  if (prop === 'toggle') {
3667
- const currentValueAtPath = getGlobalStore
3668
- .getState()
3669
- .getShadowValue([stateKey, ...path].join('.'), meta?.validIds);
3462
+ const { value: currentValueAtPath } = getScopedData(
3463
+ stateKey,
3464
+ path,
3465
+ meta
3466
+ );
3670
3467
 
3671
3468
  if (typeof currentValueAtPath != 'boolean') {
3672
3469
  throw new Error('toggle() can only be used on boolean values');
@@ -3703,18 +3500,14 @@ function createProxyHandler<T>(
3703
3500
  },
3704
3501
  };
3705
3502
 
3706
- const proxyInstance = new Proxy(baseFunction, handler);
3503
+ const proxyInstance = new Proxy({}, handler);
3707
3504
  proxyCache.set(cacheKey, proxyInstance);
3708
3505
 
3709
3506
  return proxyInstance;
3710
3507
  }
3711
3508
 
3712
- const baseObj = {
3509
+ const rootLevelMethods = {
3713
3510
  revertToInitialState: (obj?: { validationKey?: string }) => {
3714
- const init = getGlobalStore
3715
- .getState()
3716
- .getInitialOptions(stateKey)?.validation;
3717
-
3718
3511
  const shadowMeta = getGlobalStore
3719
3512
  .getState()
3720
3513
  .getShadowMetadata(stateKey, []);
@@ -3730,10 +3523,10 @@ function createProxyHandler<T>(
3730
3523
  const initialState =
3731
3524
  getGlobalStore.getState().initialStateGlobal[stateKey];
3732
3525
 
3733
- getGlobalStore.getState().clearSelectedIndexesForState(stateKey);
3526
+ clearSelectedIndexesForState(stateKey);
3734
3527
 
3735
3528
  stateVersion++;
3736
- getGlobalStore.getState().initializeShadowState(stateKey, initialState);
3529
+ initializeShadowState(stateKey, initialState);
3737
3530
  rebuildStateShape({
3738
3531
  path: [],
3739
3532
  componentId: componentId!,
@@ -3783,7 +3576,8 @@ function createProxyHandler<T>(
3783
3576
  }
3784
3577
  startTransition(() => {
3785
3578
  updateInitialStateGlobal(stateKey, newState);
3786
- getGlobalStore.getState().initializeShadowState(stateKey, newState);
3579
+ initializeShadowState(stateKey, newState);
3580
+ // initializeShadowStateNEW(stateKey, newState);
3787
3581
 
3788
3582
  const stateEntry = getGlobalStore
3789
3583
  .getState()
@@ -3801,6 +3595,7 @@ function createProxyHandler<T>(
3801
3595
  };
3802
3596
  },
3803
3597
  };
3598
+
3804
3599
  const returnShape = rebuildStateShape({
3805
3600
  componentId,
3806
3601
  path: [],
@@ -3819,153 +3614,6 @@ export function $cogsSignal(proxy: {
3819
3614
  return createElement(SignalRenderer, { proxy });
3820
3615
  }
3821
3616
 
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
3617
  function SignalRenderer({
3970
3618
  proxy,
3971
3619
  }: {
@@ -3980,12 +3628,10 @@ function SignalRenderer({
3980
3628
  const instanceIdRef = useRef<string | null>(null);
3981
3629
  const isSetupRef = useRef(false);
3982
3630
  const signalId = `${proxy._stateKey}-${proxy._path.join('.')}`;
3983
- const value = getGlobalStore
3984
- .getState()
3985
- .getShadowValue(
3986
- [proxy._stateKey, ...proxy._path].join('.'),
3987
- proxy._meta?.validIds
3988
- );
3631
+ const arrayPathKey = proxy._path.join('.');
3632
+ const viewIds = proxy._meta?.arrayViews?.[arrayPathKey];
3633
+
3634
+ const value = getShadowValue(proxy._stateKey, proxy._path, viewIds);
3989
3635
 
3990
3636
  // Setup effect - runs only once
3991
3637
  useEffect(() => {
@@ -4180,7 +3826,7 @@ function ListItemWrapper({
4180
3826
  );
4181
3827
 
4182
3828
  useEffect(() => {
4183
- getGlobalStore.getState().subscribeToPath(fullKey, (e) => {
3829
+ subscribeToPath(fullKey, (e) => {
4184
3830
  forceUpdate({});
4185
3831
  });
4186
3832
  }, []);
@@ -4194,7 +3840,7 @@ function ListItemWrapper({
4194
3840
  hasReportedInitialHeight.current = true;
4195
3841
  const newHeight = element.offsetHeight;
4196
3842
 
4197
- getGlobalStore.getState().setShadowMetadata(stateKey, itemPath, {
3843
+ setShadowMetadata(stateKey, itemPath, {
4198
3844
  virtualizer: {
4199
3845
  itemHeight: newHeight,
4200
3846
  domRef: element,
@@ -4203,7 +3849,7 @@ function ListItemWrapper({
4203
3849
 
4204
3850
  const arrayPath = itemPath.slice(0, -1);
4205
3851
  const arrayPathKey = [stateKey, ...arrayPath].join('.');
4206
- getGlobalStore.getState().notifyPathSubscribers(arrayPathKey, {
3852
+ notifyPathSubscribers(arrayPathKey, {
4207
3853
  type: 'ITEMHEIGHT',
4208
3854
  itemKey: itemPath.join('.'),
4209
3855
 
@@ -4212,8 +3858,7 @@ function ListItemWrapper({
4212
3858
  }
4213
3859
  }, [inView, imagesLoaded, stateKey, itemPath]);
4214
3860
 
4215
- const fullItemPath = [stateKey, ...itemPath].join('.');
4216
- const itemValue = getGlobalStore.getState().getShadowValue(fullItemPath);
3861
+ const itemValue = getShadowValue(stateKey, itemPath);
4217
3862
 
4218
3863
  if (itemValue === undefined) {
4219
3864
  return null;
@@ -4240,7 +3885,6 @@ function FormElementWrapper({
4240
3885
  stateKey: string;
4241
3886
  path: string[];
4242
3887
  rebuildStateShape: (options: {
4243
- currentState: any;
4244
3888
  path: string[];
4245
3889
  componentId: string;
4246
3890
  meta?: any;
@@ -4254,9 +3898,7 @@ function FormElementWrapper({
4254
3898
 
4255
3899
  const stateKeyPathKey = [stateKey, ...path].join('.');
4256
3900
  useRegisterComponent(stateKey, componentId, forceUpdate);
4257
- const globalStateValue = getGlobalStore
4258
- .getState()
4259
- .getShadowValue(stateKeyPathKey);
3901
+ const globalStateValue = getShadowValue(stateKey, path);
4260
3902
  const [localValue, setLocalValue] = useState<any>(globalStateValue);
4261
3903
  const isCurrentlyDebouncing = useRef(false);
4262
3904
  const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -4304,21 +3946,21 @@ function FormElementWrapper({
4304
3946
 
4305
3947
  debounceTimeoutRef.current = setTimeout(() => {
4306
3948
  isCurrentlyDebouncing.current = false;
4307
-
4308
- // Update state
4309
3949
  setState(newValue, path, { updateType: 'update' });
4310
3950
 
4311
- // Perform LIVE validation (gentle)
4312
- const { getInitialOptions, setShadowMetadata, getShadowMetadata } =
4313
- getGlobalStore.getState();
3951
+ // NEW: Check if validation is enabled via features
3952
+ const rootMeta = getGlobalStore
3953
+ .getState()
3954
+ .getShadowMetadata(stateKey, []);
3955
+ if (!rootMeta?.features?.validationEnabled) return;
3956
+
4314
3957
  const validationOptions = getInitialOptions(stateKey)?.validation;
4315
3958
  const zodSchema =
4316
3959
  validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
4317
3960
 
4318
3961
  if (zodSchema) {
4319
- const fullState = getGlobalStore.getState().getShadowValue(stateKey);
3962
+ const fullState = getShadowValue(stateKey, []);
4320
3963
  const result = zodSchema.safeParse(fullState);
4321
-
4322
3964
  const currentMeta = getShadowMetadata(stateKey, path) || {};
4323
3965
 
4324
3966
  if (!result.success) {
@@ -4326,6 +3968,7 @@ function FormElementWrapper({
4326
3968
  'issues' in result.error
4327
3969
  ? result.error.issues
4328
3970
  : (result.error as any).errors;
3971
+
4329
3972
  const pathErrors = errors.filter(
4330
3973
  (error: any) =>
4331
3974
  JSON.stringify(error.path) === JSON.stringify(path)
@@ -4335,30 +3978,37 @@ function FormElementWrapper({
4335
3978
  setShadowMetadata(stateKey, path, {
4336
3979
  ...currentMeta,
4337
3980
  validation: {
4338
- status: 'INVALID_LIVE',
4339
- message: pathErrors[0]?.message,
3981
+ status: 'INVALID',
3982
+ errors: [
3983
+ {
3984
+ source: 'client',
3985
+ message: pathErrors[0]?.message,
3986
+ severity: 'warning', // Gentle error during typing
3987
+ },
3988
+ ],
3989
+ lastValidated: Date.now(),
4340
3990
  validatedValue: newValue,
4341
3991
  },
4342
3992
  });
4343
3993
  } else {
4344
- // This field has no errors - clear validation
4345
3994
  setShadowMetadata(stateKey, path, {
4346
3995
  ...currentMeta,
4347
3996
  validation: {
4348
- status: 'VALID_LIVE',
3997
+ status: 'VALID',
3998
+ errors: [],
3999
+ lastValidated: Date.now(),
4349
4000
  validatedValue: newValue,
4350
- message: undefined,
4351
4001
  },
4352
4002
  });
4353
4003
  }
4354
4004
  } else {
4355
- // Validation passed - clear any existing errors
4356
4005
  setShadowMetadata(stateKey, path, {
4357
4006
  ...currentMeta,
4358
4007
  validation: {
4359
- status: 'VALID_LIVE',
4008
+ status: 'VALID',
4009
+ errors: [],
4010
+ lastValidated: Date.now(),
4360
4011
  validatedValue: newValue,
4361
- message: undefined,
4362
4012
  },
4363
4013
  });
4364
4014
  }
@@ -4381,7 +4031,8 @@ function FormElementWrapper({
4381
4031
  isCurrentlyDebouncing.current = false;
4382
4032
  setState(localValue, path, { updateType: 'update' });
4383
4033
  }
4384
-
4034
+ const rootMeta = getShadowMetadata(stateKey, []);
4035
+ if (!rootMeta?.features?.validationEnabled) return;
4385
4036
  const { getInitialOptions } = getGlobalStore.getState();
4386
4037
  const validationOptions = getInitialOptions(stateKey)?.validation;
4387
4038
  const zodSchema =
@@ -4392,19 +4043,20 @@ function FormElementWrapper({
4392
4043
  // Get the full path including stateKey
4393
4044
 
4394
4045
  // Update validation state to "validating"
4395
- const currentMeta = getGlobalStore
4396
- .getState()
4397
- .getShadowMetadata(stateKey, path);
4398
- getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4046
+ const currentMeta = getShadowMetadata(stateKey, path);
4047
+
4048
+ setShadowMetadata(stateKey, path, {
4399
4049
  ...currentMeta,
4400
4050
  validation: {
4401
- status: 'DIRTY',
4051
+ status: 'VALIDATING',
4052
+ errors: [],
4053
+ lastValidated: Date.now(),
4402
4054
  validatedValue: localValue,
4403
4055
  },
4404
4056
  });
4405
4057
 
4406
4058
  // Validate full state
4407
- const fullState = getGlobalStore.getState().getShadowValue(stateKey);
4059
+ const fullState = getShadowValue(stateKey, []);
4408
4060
  const result = zodSchema.safeParse(fullState);
4409
4061
  console.log('result ', result);
4410
4062
  if (!result.success) {
@@ -4468,20 +4120,27 @@ function FormElementWrapper({
4468
4120
 
4469
4121
  console.log('Filtered path errors:', pathErrors);
4470
4122
  // Update shadow metadata with validation result
4471
- getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4123
+ setShadowMetadata(stateKey, path, {
4472
4124
  ...currentMeta,
4473
4125
  validation: {
4474
- status: 'VALIDATION_FAILED',
4475
- message: pathErrors[0]?.message,
4126
+ status: 'INVALID',
4127
+ errors: pathErrors.map((err: any) => ({
4128
+ source: 'client' as const,
4129
+ message: err.message,
4130
+ severity: 'error' as const, // Hard error on blur
4131
+ })),
4132
+ lastValidated: Date.now(),
4476
4133
  validatedValue: localValue,
4477
4134
  },
4478
4135
  });
4479
4136
  } else {
4480
4137
  // Validation passed
4481
- getGlobalStore.getState().setShadowMetadata(stateKey, path, {
4138
+ setShadowMetadata(stateKey, path, {
4482
4139
  ...currentMeta,
4483
4140
  validation: {
4484
- status: 'VALID_PENDING_SYNC',
4141
+ status: 'VALID',
4142
+ errors: [],
4143
+ lastValidated: Date.now(),
4485
4144
  validatedValue: localValue,
4486
4145
  },
4487
4146
  });
@@ -4490,7 +4149,6 @@ function FormElementWrapper({
4490
4149
  }, [stateKey, path, localValue, setState]);
4491
4150
 
4492
4151
  const baseState = rebuildStateShape({
4493
- currentState: globalStateValue,
4494
4152
  path: path,
4495
4153
  componentId: componentId,
4496
4154
  });
@@ -4529,9 +4187,6 @@ function useRegisterComponent(
4529
4187
  const fullComponentId = `${stateKey}////${componentId}`;
4530
4188
 
4531
4189
  useLayoutEffect(() => {
4532
- const { registerComponent, unregisterComponent } =
4533
- getGlobalStore.getState();
4534
-
4535
4190
  // Call the safe, centralized function to register
4536
4191
  registerComponent(stateKey, fullComponentId, {
4537
4192
  forceUpdate: () => forceUpdate({}),