cogsbox-state 0.5.434 → 0.5.436

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/Functions.tsx CHANGED
@@ -1,260 +1,29 @@
1
1
  import { type FormOptsType } from './CogsState';
2
2
 
3
- import { useEffect, useRef, useState } from 'react';
4
3
  import React from 'react';
5
4
  import { getGlobalStore } from './store';
6
5
 
7
- export const useStoreSubscription = <T,>(
8
- fullPath: string,
9
- selector: (
10
- store: ReturnType<typeof getGlobalStore.getState>,
11
- path: string
12
- ) => T,
13
- compare: (a: T, b: T) => boolean = (a, b) =>
14
- JSON.stringify(a) === JSON.stringify(b)
15
- ) => {
16
- const [value, setValue] = useState<T>(() =>
17
- selector(getGlobalStore.getState(), fullPath)
18
- );
19
- const previousValueRef = useRef<T>(value);
20
- const fullPathRef = useRef(fullPath);
21
- useEffect(() => {
22
- fullPathRef.current = fullPath; // Ensure latest fullPath is always used
23
-
24
- setValue(selector(getGlobalStore.getState(), fullPath));
25
-
26
- const callback = (store: any) => {
27
- const newValue = selector(store, fullPathRef.current);
28
-
29
- if (!compare(previousValueRef.current, newValue)) {
30
- previousValueRef.current = newValue;
31
- setValue(newValue);
32
- }
33
- };
34
- const unsubscribe = getGlobalStore.subscribe(callback);
35
- return () => {
36
- unsubscribe();
37
- };
38
- }, [fullPath]);
39
- return value;
40
- };
41
- export const useGetValidationErrors = (
42
- validationKey: string,
43
- path: string[],
44
- validIndices?: number[]
45
- ) => {
46
- const fullPath =
47
- validationKey +
48
- '.' +
49
- (path.length > 0 ? [path.join('.')] : []) +
50
- (validIndices && validIndices.length > 0 ? '.' + validIndices : '');
51
-
52
- const returnresult = useStoreSubscription(
53
- fullPath,
54
- (store, path) => store.getValidationErrors(path) || []
55
- );
56
-
57
- return returnresult;
58
- };
59
-
60
- // Find FormControlComponent in your Functions.ts or equivalent file
61
-
62
- // export const FormControlComponent = <TStateObject,>({
63
- // setState, // This is the real effectiveSetState from the hook
64
- // path,
65
- // child,
66
- // formOpts,
67
- // stateKey,
68
- // rebuildStateShape,
69
- // }: FormControlComponentProps<TStateObject>) => {
70
- // const { registerFormRef, getFormRef } = formRefStore.getState();
71
- // const {
72
- // getValidationErrors,
73
- // addValidationError,
74
- // getInitialOptions,
75
- // removeValidationError,
76
- // } = getGlobalStore.getState();
77
- // const stateKeyPathKey = [stateKey, ...path].join('.');
78
- // const [, forceUpdate] = useState<any>();
79
- // getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
80
- // forceUpdate({});
81
- // });
82
-
83
- // const refKey = stateKey + '.' + path.join('.');
84
- // const localFormRef = useRef<HTMLInputElement>(null);
85
- // const existingRef = getFormRef(refKey);
86
- // if (!existingRef) {
87
- // registerFormRef(refKey, localFormRef);
88
- // }
89
- // const formRef = existingRef || localFormRef;
90
-
91
- // // --- START CHANGES ---
92
-
93
- // const globalStateValue = getGlobalStore
94
- // .getState()
95
- // .getShadowValue(stateKeyPathKey);
96
- // const [localValue, setLocalValue] = useState<any>(globalStateValue);
97
- // const isCurrentlyDebouncing = useRef(false);
98
- // const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
99
-
100
- // // Effect to sync local state if global state changes externally
101
- // useEffect(() => {
102
- // // Only update local if not actively debouncing a local change
103
- // if (!isCurrentlyDebouncing.current && globalStateValue !== localValue) {
104
- // setLocalValue(globalStateValue);
105
- // }
106
- // }, [globalStateValue]); // Removed localValue dependency
107
-
108
- // // Effect for cleanup
109
- // useEffect(() => {
110
- // return () => {
111
- // if (debounceTimeoutRef.current) {
112
- // clearTimeout(debounceTimeoutRef.current);
113
- // debounceTimeoutRef.current = null; // Explicitly nullify
114
- // isCurrentlyDebouncing.current = false;
115
- // }
116
- // };
117
- // }, []);
118
-
119
- // const debouncedUpdater = (payload: UpdateArg<TStateObject>) => {
120
- // setLocalValue(payload); // Update local state immediately
121
- // isCurrentlyDebouncing.current = true;
122
-
123
- // if (payload === '') {
124
- // if (debounceTimeoutRef.current) {
125
- // clearTimeout(debounceTimeoutRef.current); // Clear pending timer
126
- // debounceTimeoutRef.current = null;
127
- // }
128
-
129
- // setState(payload, path, { updateType: 'update' });
130
- // isCurrentlyDebouncing.current = false; // No longer debouncing
131
- // return; // Don't proceed to set another timeout
132
- // }
133
-
134
- // // If not empty, proceed with normal debouncing
135
- // if (debounceTimeoutRef.current) {
136
- // clearTimeout(debounceTimeoutRef.current);
137
- // }
138
-
139
- // debounceTimeoutRef.current = setTimeout(
140
- // () => {
141
- // isCurrentlyDebouncing.current = false;
142
- // console.log('debouncedUpdater', payload);
143
- // setState(payload, path, { updateType: 'update' });
144
- // },
145
- // formOpts?.debounceTime ??
146
- // (typeof globalStateValue == 'boolean' ? 20 : 200)
147
- // );
148
- // };
149
-
150
- // const initialOptions = getInitialOptions(stateKey);
151
-
152
- // const validationKey = initialOptions?.validation?.key;
153
- // const validateOnBlur = initialOptions?.validation?.onBlur === true;
154
-
155
- // const handleBlur = async () => {
156
- // // --- Ensure latest value is flushed if debouncing ---
157
- // if (debounceTimeoutRef.current) {
158
- // clearTimeout(debounceTimeoutRef.current); // Clear pending timer
159
- // debounceTimeoutRef.current = null;
160
- // isCurrentlyDebouncing.current = false;
161
- // // Ensure the absolute latest local value is committed on blur
162
- // setState(localValue, path, { updateType: 'update' });
163
- // }
164
- // // --- End modification ---
165
-
166
- // if (!initialOptions?.validation?.zodSchema || !validateOnBlur) return;
167
- // removeValidationError(validationKey + '.' + path.join('.'));
168
- // try {
169
- // // Use the potentially just flushed value
170
- // if (!validationKey) return;
171
- // const fieldValue = getGlobalStore
172
- // .getState()
173
- // .getShadowValue(stateKeyPathKey);
174
- // await validateZodPathFunc(
175
- // validationKey,
176
- // initialOptions.validation.zodSchema,
177
- // path,
178
- // fieldValue
179
- // );
180
- // // forceUpdate might be needed if validation state update doesn't trigger render
181
- // // Consider using useGetValidationErrors hook result directly for validation display
182
- // } catch (error) {
183
- // console.error('Validation error on blur:', error);
184
- // }
185
- // };
186
-
187
- // const childElement = child({
188
- // state: setter,
189
- // // --- START CHANGES ---
190
- // get: () => localValue, // Get should return the immediate local value
191
- // set: debouncedUpdater, // Use the new debounced updater
192
- // // --- END CHANGES ---
193
-
194
- // path: path,
195
- // validationErrors: () =>
196
- // getValidationErrors(validationKey + '.' + path.join('.')),
197
- // addValidationError: (message?: string) => {
198
- // removeValidationError(validationKey + '.' + path.join('.'));
199
- // addValidationError(validationKey + '.' + path.join('.'), message ?? '');
200
- // },
201
- // inputProps: {
202
- // // --- START CHANGES ---
203
- // value: localValue ?? '', // Input value is always the local state
204
- // onChange: (e: any) => debouncedUpdater(e.target.value), // Use debounced updater
205
- // // --- END CHANGES ---
206
- // onBlur: handleBlur,
207
- // ref: formRef,
208
- // },
209
- // });
210
-
211
- // return (
212
- // <>
213
- // <ValidationWrapper {...{ formOpts, path, stateKey }}>
214
- // {childElement}
215
- // </ValidationWrapper>
216
- // </>
217
- // );
218
- // };
219
6
  export type ValidationWrapperProps = {
220
7
  formOpts?: FormOptsType;
221
8
  path: string[];
222
9
  stateKey: string;
223
10
  children: React.ReactNode;
224
- validIndices?: number[];
225
11
  };
226
12
  export function ValidationWrapper({
227
13
  formOpts,
228
14
  path,
229
-
230
15
  stateKey,
231
16
  children,
232
- validIndices,
233
17
  }: ValidationWrapperProps) {
234
- const { getInitialOptions } = getGlobalStore.getState();
18
+ const { getInitialOptions, getShadowMetadata } = getGlobalStore.getState();
235
19
  const thisStateOpts = getInitialOptions(stateKey!);
236
- const validationKey = thisStateOpts?.validation?.key ?? stateKey!;
237
- const validationErrors = useGetValidationErrors(
238
- validationKey,
239
- path,
240
- validIndices
241
- );
242
- // console.log(
243
- // "validationErrors ValidationWrapper",
244
- // stateKey,
245
- // validationKey,
246
- // path,
247
- // validationErrors
248
- // );
249
- const thesMessages: string[] = [];
250
20
 
251
- if (validationErrors) {
252
- const newMessage = validationErrors!.join(', ');
253
- if (!thesMessages.includes(newMessage)) {
254
- thesMessages.push(newMessage);
255
- }
256
- }
21
+ // GET VALIDATION FROM SHADOW METADATA
22
+ const shadowMeta = getShadowMetadata(stateKey!, path);
23
+ const validationState = shadowMeta?.validation;
24
+ const status = validationState?.status || 'PRISTINE';
257
25
 
26
+ const message = validationState?.message;
258
27
  return (
259
28
  <>
260
29
  {thisStateOpts?.formElements?.validation &&
@@ -263,12 +32,10 @@ export function ValidationWrapper({
263
32
  children: (
264
33
  <React.Fragment key={path.toString()}>{children}</React.Fragment>
265
34
  ),
266
- active: validationErrors.length > 0 ? true : false,
35
+ status, // Pass status instead of active
267
36
  message: formOpts?.validation?.hideMessage
268
37
  ? ''
269
- : formOpts?.validation?.message
270
- ? formOpts?.validation?.message
271
- : thesMessages.map((m) => m).join(', '),
38
+ : formOpts?.validation?.message || message || '',
272
39
  path: path,
273
40
  })
274
41
  ) : (
@@ -1,10 +1,10 @@
1
- import { observable } from "@trpc/server/observable";
2
- import type { AnyRouter } from "@trpc/server";
3
- import type { TRPCLink } from "@trpc/client";
4
- import type { Operation } from "@trpc/client";
5
- import type { TRPCClientError } from "@trpc/client";
6
- import { getGlobalStore } from "./store";
7
- import type { Observer } from "@trpc/server/observable";
1
+ import { observable } from '@trpc/server/observable';
2
+ import type { AnyRouter } from '@trpc/server';
3
+ import type { TRPCLink } from '@trpc/client';
4
+ import type { Operation } from '@trpc/client';
5
+ import type { TRPCClientError } from '@trpc/client';
6
+ import { getGlobalStore } from './store';
7
+ import type { Observer } from '@trpc/server/observable';
8
8
  export const useCogsTrpcValidationLink = <
9
9
  TRouter extends AnyRouter,
10
10
  >(passedOpts?: {
@@ -25,27 +25,27 @@ export const useCogsTrpcValidationLink = <
25
25
  try {
26
26
  const errorObject = JSON.parse(err.message);
27
27
  if (passedOpts?.log) {
28
- console.log("errorObject", errorObject);
28
+ console.log('errorObject', errorObject);
29
29
  }
30
30
  if (Array.isArray(errorObject)) {
31
31
  errorObject.forEach(
32
32
  (error: { path: string[]; message: string }) => {
33
- const fullpath = `${op.path}.${error.path.join(".")}`;
33
+ const fullpath = `${op.path}.${error.path.join('.')}`;
34
34
  // In your TRPC link
35
35
  if (passedOpts?.log) {
36
- console.log("fullpath 1", fullpath);
36
+ console.log('fullpath 1', fullpath);
37
37
  }
38
38
  addValidationError(fullpath, error.message);
39
39
  }
40
40
  );
41
41
  } else if (
42
- typeof errorObject === "object" &&
42
+ typeof errorObject === 'object' &&
43
43
  errorObject !== null
44
44
  ) {
45
45
  Object.entries(errorObject).forEach(([key, value]) => {
46
46
  const fullpath = `${op.path}.${key}`;
47
47
  if (passedOpts?.log) {
48
- console.log("fullpath 2", fullpath);
48
+ console.log('fullpath 2', fullpath);
49
49
  }
50
50
  addValidationError(fullpath, value as string);
51
51
  });
package/src/store.ts CHANGED
@@ -105,6 +105,7 @@ export type ShadowMetadata = {
105
105
  domRef?: HTMLElement | null;
106
106
  };
107
107
  syncInfo?: { status: string };
108
+ validation?: ValidationState;
108
109
  lastUpdated?: number;
109
110
  value?: any;
110
111
  classSignals?: Array<{
@@ -151,6 +152,24 @@ export type ShadowMetadata = {
151
152
  }
152
153
  >;
153
154
  } & ComponentsType;
155
+
156
+ export type ValidationStatus =
157
+ | 'PRISTINE' // Untouched, matches initial state.
158
+ | 'DIRTY' // Changed, but no validation run yet.
159
+ | 'VALID_LIVE' // Valid while typing.
160
+ | 'INVALID_LIVE' // Gentle error during typing.
161
+ | 'VALIDATION_FAILED' // Hard error on blur/submit.
162
+ | 'VALID_PENDING_SYNC' // Passed validation, ready for sync.
163
+ | 'SYNCING' // Actively being sent to the server.
164
+ | 'SYNCED' // Server confirmed success.
165
+ | 'SYNC_FAILED'; // Server rejected the data.
166
+
167
+ export type ValidationState = {
168
+ status: ValidationStatus;
169
+ message?: string;
170
+ lastValidated?: number;
171
+ validatedValue?: any;
172
+ };
154
173
  export type CogsEvent =
155
174
  | { type: 'INSERT'; path: string; itemKey: string; index: number }
156
175
  | { type: 'REMOVE'; path: string; itemKey: string }
@@ -159,6 +178,17 @@ export type CogsEvent =
159
178
  | { type: 'RELOAD'; path: string }; // For full re-initializations
160
179
  export type CogsGlobalState = {
161
180
  // --- Shadow State and Subscription System ---
181
+ registerComponent: (
182
+ stateKey: string,
183
+ componentId: string,
184
+ registration: any
185
+ ) => void;
186
+ unregisterComponent: (stateKey: string, componentId: string) => void;
187
+ addPathComponent: (
188
+ stateKey: string,
189
+ dependencyPath: string[],
190
+ fullComponentId: string
191
+ ) => void;
162
192
  shadowStateStore: Map<string, ShadowMetadata>;
163
193
  markAsDirty: (
164
194
  key: string,
@@ -275,20 +305,103 @@ const isSimpleObject = (value: any): boolean => {
275
305
  return Array.isArray(value) || value.constructor === Object;
276
306
  };
277
307
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
308
+ addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
309
+ set((state) => {
310
+ const newShadowStore = new Map(state.shadowStateStore);
311
+ const dependencyKey = [stateKey, ...dependencyPath].join('.');
312
+
313
+ // --- Part 1: Update the path's own metadata ---
314
+ const pathMeta = newShadowStore.get(dependencyKey) || {};
315
+ // Create a *new* Set to ensure immutability
316
+ const pathComponents = new Set(pathMeta.pathComponents);
317
+ pathComponents.add(fullComponentId);
318
+ // Update the metadata for the specific path
319
+ newShadowStore.set(dependencyKey, { ...pathMeta, pathComponents });
320
+
321
+ // --- Part 2: Update the component's own list of paths ---
322
+ const rootMeta = newShadowStore.get(stateKey) || {};
323
+ const component = rootMeta.components?.get(fullComponentId);
324
+
325
+ // If the component exists, update its `paths` set immutably
326
+ if (component) {
327
+ const newPaths = new Set(component.paths);
328
+ newPaths.add(dependencyKey);
329
+
330
+ const newComponentRegistration = { ...component, paths: newPaths };
331
+ const newComponentsMap = new Map(rootMeta.components);
332
+ newComponentsMap.set(fullComponentId, newComponentRegistration);
333
+
334
+ // Update the root metadata with the new components map
335
+ newShadowStore.set(stateKey, {
336
+ ...rootMeta,
337
+ components: newComponentsMap,
338
+ });
339
+ }
340
+
341
+ // Return the final, updated state
342
+ return { shadowStateStore: newShadowStore };
343
+ });
344
+ },
345
+ registerComponent: (stateKey, fullComponentId, registration) => {
346
+ set((state) => {
347
+ // Create a new Map to ensure Zustand detects the change
348
+ const newShadowStore = new Map(state.shadowStateStore);
349
+
350
+ // Get the metadata for the ROOT of the state (where the components map lives)
351
+ const rootMeta = newShadowStore.get(stateKey) || {};
352
+
353
+ // Also clone the components map to avoid direct mutation
354
+ const components = new Map(rootMeta.components);
355
+ components.set(fullComponentId, registration);
356
+
357
+ // Update the root metadata with the new components map
358
+ newShadowStore.set(stateKey, { ...rootMeta, components });
359
+
360
+ // Return the updated state
361
+ return { shadowStateStore: newShadowStore };
362
+ });
363
+ },
364
+
365
+ unregisterComponent: (stateKey, fullComponentId) => {
366
+ set((state) => {
367
+ const newShadowStore = new Map(state.shadowStateStore);
368
+ const rootMeta = newShadowStore.get(stateKey);
369
+
370
+ // If there's no metadata or no components map, do nothing
371
+ if (!rootMeta?.components) {
372
+ return state; // Return original state, no change needed
373
+ }
374
+
375
+ const components = new Map(rootMeta.components);
376
+ const wasDeleted = components.delete(fullComponentId);
377
+
378
+ // Only update state if something was actually deleted
379
+ if (wasDeleted) {
380
+ newShadowStore.set(stateKey, { ...rootMeta, components });
381
+ return { shadowStateStore: newShadowStore };
382
+ }
383
+
384
+ return state; // Nothing changed
385
+ });
386
+ },
278
387
  markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
279
388
  const newShadowStore = new Map(get().shadowStateStore);
280
389
  let changed = false;
281
390
 
282
- // This function marks a single path as dirty if it was previously synced.
283
391
  const setDirty = (currentPath: string[]) => {
284
392
  const fullKey = [key, ...currentPath].join('.');
285
393
  const meta = newShadowStore.get(fullKey);
286
394
 
287
- // We only mark something as dirty if it was previously synced from the server.
288
- // We also check `isDirty !== true` to avoid redundant updates.
289
- if (meta && meta.stateSource === 'server' && meta.isDirty !== true) {
395
+ // We mark something as dirty if it isn't already.
396
+ // The original data source doesn't matter.
397
+ if (meta && meta.isDirty !== true) {
290
398
  newShadowStore.set(fullKey, { ...meta, isDirty: true });
291
399
  changed = true;
400
+ } else if (!meta) {
401
+ // If there's no metadata, create it and mark it as dirty.
402
+ // This handles newly created fields within an object.
403
+ newShadowStore.set(fullKey, { isDirty: true });
404
+ changed = true;
292
405
  }
293
406
  };
294
407
 
@@ -304,7 +417,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
304
417
  }
305
418
  }
306
419
 
307
- // Only update the global state if something actually changed.
308
420
  if (changed) {
309
421
  set({ shadowStateStore: newShadowStore });
310
422
  }
@@ -344,54 +456,72 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
344
456
  },
345
457
 
346
458
  notifyPathSubscribers: (updatedPath, newValue) => {
347
- // <-- Now accepts newValue
348
459
  const subscribers = get().pathSubscribers;
349
460
  const subs = subscribers.get(updatedPath);
350
461
 
351
462
  if (subs) {
352
- // Pass the newValue to every callback
353
463
  subs.forEach((callback) => callback(newValue));
354
464
  }
355
465
  },
356
- initializeShadowState: (key: string, initialState: any) => {
357
- const existingShadowStore = new Map(get().shadowStateStore);
358
-
359
- const processValue = (value: any, path: string[]) => {
360
- const nodeKey = [key, ...path].join('.');
361
-
362
- if (Array.isArray(value)) {
363
- // Handle arrays as before
364
- const childIds: string[] = [];
365
-
366
- value.forEach((item) => {
367
- const itemId = `id:${ulid()}`;
368
- childIds.push(nodeKey + '.' + itemId);
369
- });
370
466
 
371
- existingShadowStore.set(nodeKey, { arrayKeys: childIds });
467
+ initializeShadowState: (key: string, initialState: any) => {
468
+ set((state) => {
469
+ // 1. Make a copy of the current store to modify it
470
+ const newShadowStore = new Map(state.shadowStateStore);
471
+
472
+ // 2. PRESERVE the existing components map before doing anything else
473
+ const existingRootMeta = newShadowStore.get(key);
474
+ const preservedComponents = existingRootMeta?.components;
475
+
476
+ // 3. Wipe all old shadow entries for this state key
477
+ const prefixToDelete = key + '.';
478
+ for (const k of Array.from(newShadowStore.keys())) {
479
+ if (k === key || k.startsWith(prefixToDelete)) {
480
+ newShadowStore.delete(k);
481
+ }
482
+ }
372
483
 
373
- value.forEach((item, index) => {
374
- const itemId = childIds[index]!.split('.').pop();
375
- processValue(item, [...path!, itemId!]);
376
- });
377
- } else if (isSimpleObject(value)) {
378
- // Only create field mappings for simple objects
379
- const fields = Object.fromEntries(
380
- Object.keys(value).map((k) => [k, nodeKey + '.' + k])
381
- );
382
- existingShadowStore.set(nodeKey, { fields });
484
+ // 4. Run your original logic to rebuild the state tree from scratch
485
+ const processValue = (value: any, path: string[]) => {
486
+ const nodeKey = [key, ...path].join('.');
383
487
 
384
- Object.keys(value).forEach((k) => {
385
- processValue(value[k], [...path, k]);
488
+ if (Array.isArray(value)) {
489
+ const childIds: string[] = [];
490
+ value.forEach(() => {
491
+ const itemId = `id:${ulid()}`;
492
+ childIds.push(nodeKey + '.' + itemId);
493
+ });
494
+ newShadowStore.set(nodeKey, { arrayKeys: childIds });
495
+ value.forEach((item, index) => {
496
+ const itemId = childIds[index]!.split('.').pop();
497
+ processValue(item, [...path!, itemId!]);
498
+ });
499
+ } else if (isSimpleObject(value)) {
500
+ const fields = Object.fromEntries(
501
+ Object.keys(value).map((k) => [k, nodeKey + '.' + k])
502
+ );
503
+ newShadowStore.set(nodeKey, { fields });
504
+ Object.keys(value).forEach((k) => {
505
+ processValue(value[k], [...path, k]);
506
+ });
507
+ } else {
508
+ newShadowStore.set(nodeKey, { value });
509
+ }
510
+ };
511
+ processValue(initialState, []);
512
+
513
+ // 5. RESTORE the preserved components map onto the new root metadata
514
+ if (preservedComponents) {
515
+ const newRootMeta = newShadowStore.get(key) || {};
516
+ newShadowStore.set(key, {
517
+ ...newRootMeta,
518
+ components: preservedComponents,
386
519
  });
387
- } else {
388
- // Treat everything else (including Uint8Array) as primitive values
389
- existingShadowStore.set(nodeKey, { value });
390
520
  }
391
- };
392
521
 
393
- processValue(initialState, []);
394
- set({ shadowStateStore: existingShadowStore });
522
+ // 6. Return the completely updated state
523
+ return { shadowStateStore: newShadowStore };
524
+ });
395
525
  },
396
526
 
397
527
  getShadowValue: (fullKey: string, validArrayIds?: string[]) => {
@@ -440,11 +570,41 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
440
570
  return get().shadowStateStore.get(fullKey);
441
571
  },
442
572
 
443
- setShadowMetadata: (key: string, path: string[], metadata: any) => {
573
+ setShadowMetadata: (key, path, metadata) => {
444
574
  const fullKey = [key, ...path].join('.');
575
+ const existingMeta = get().shadowStateStore.get(fullKey);
576
+
577
+ // --- THIS IS THE TRAP ---
578
+ // If the existing metadata HAS a components map, but the NEW metadata DOES NOT,
579
+ // it means we are about to wipe it out. This is the bug.
580
+ if (existingMeta?.components && !metadata.components) {
581
+ console.group(
582
+ '%c🚨 RACE CONDITION DETECTED! 🚨',
583
+ 'color: red; font-size: 18px; font-weight: bold;'
584
+ );
585
+ console.error(
586
+ `An overwrite is about to happen on stateKey: "${key}" at path: [${path.join(', ')}]`
587
+ );
588
+ console.log(
589
+ 'The EXISTING metadata had a components map:',
590
+ existingMeta.components
591
+ );
592
+ console.log(
593
+ 'The NEW metadata is trying to save WITHOUT a components map:',
594
+ metadata
595
+ );
596
+ console.log(
597
+ '%cStack trace to the function that caused this overwrite:',
598
+ 'font-weight: bold;'
599
+ );
600
+ console.trace(); // This prints the call stack, leading you to the bad code.
601
+ console.groupEnd();
602
+ }
603
+ // --- END OF TRAP ---
604
+
445
605
  const newShadowStore = new Map(get().shadowStateStore);
446
- const existing = newShadowStore.get(fullKey) || { id: ulid() };
447
- newShadowStore.set(fullKey, { ...existing, ...metadata });
606
+ const finalMeta = { ...(existingMeta || {}), ...metadata };
607
+ newShadowStore.set(fullKey, finalMeta);
448
608
  set({ shadowStateStore: newShadowStore });
449
609
  },
450
610
  setTransformCache: (
@@ -648,6 +808,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
648
808
  clearSelectedIndex: ({ arrayKey }: { arrayKey: string }): void => {
649
809
  set((state) => {
650
810
  const newMap = state.selectedIndicesMap;
811
+ const acutalKey = newMap.get(arrayKey);
812
+ if (acutalKey) {
813
+ get().notifyPathSubscribers(acutalKey, {
814
+ type: 'CLEAR_SELECTION',
815
+ });
816
+ }
651
817
 
652
818
  newMap.delete(arrayKey);
653
819
  get().notifyPathSubscribers(arrayKey, {