cogsbox-state 0.5.464 → 0.5.466

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