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/store.ts CHANGED
@@ -3,16 +3,11 @@ import { ulid } from 'ulid';
3
3
  import type {
4
4
  OptionsType,
5
5
  ReactivityType,
6
- StateKeys,
7
6
  SyncInfo,
8
7
  UpdateTypeDetail,
9
8
  } from './CogsState.js';
10
9
 
11
- import { startTransition, type ReactNode } from 'react';
12
-
13
- type StateUpdater<StateValue> =
14
- | StateValue
15
- | ((prevValue: StateValue) => StateValue);
10
+ import { type ReactNode } from 'react';
16
11
 
17
12
  export type FreshValuesObject = {
18
13
  pathsToValues?: string[];
@@ -21,9 +16,6 @@ export type FreshValuesObject = {
21
16
  timeStamp: number;
22
17
  };
23
18
 
24
- type SyncLogType = {
25
- timeStamp: number;
26
- };
27
19
  type StateValue = any;
28
20
 
29
21
  export type TrieNode = {
@@ -36,7 +28,6 @@ export type FormRefStoreState = {
36
28
  registerFormRef: (id: string, ref: React.RefObject<any>) => void;
37
29
  getFormRef: (id: string) => React.RefObject<any> | undefined;
38
30
  removeFormRef: (id: string) => void;
39
- // New method to get all refs for a stateKey
40
31
  getFormRefsByStateKey: (
41
32
  stateKey: string
42
33
  ) => Map<string, React.RefObject<any>>;
@@ -61,7 +52,6 @@ export const formRefStore = create<FormRefStoreState>((set, get) => ({
61
52
  return { formRefs: newRefs };
62
53
  }),
63
54
 
64
- // Get all refs that start with the stateKey prefix
65
55
  getFormRefsByStateKey: (stateKey) => {
66
56
  const allRefs = get().formRefs;
67
57
  const stateKeyPrefix = stateKey + '.';
@@ -89,16 +79,34 @@ export type ComponentsType = {
89
79
  }
90
80
  >;
91
81
  };
82
+
83
+ export type ValidationStatus =
84
+ | 'NOT_VALIDATED'
85
+ | 'VALIDATING'
86
+ | 'VALID'
87
+ | 'INVALID';
88
+
89
+ export type ValidationError = {
90
+ source: 'client' | 'sync_engine' | 'api';
91
+ message: string;
92
+ severity: 'warning' | 'error';
93
+ code?: string;
94
+ };
95
+
96
+ export type ValidationState = {
97
+ status: ValidationStatus;
98
+ errors: ValidationError[];
99
+ lastValidated?: number;
100
+ validatedValue?: any;
101
+ };
102
+
92
103
  export type ShadowMetadata = {
93
104
  id?: string;
94
-
95
105
  stateSource?: 'default' | 'server' | 'localStorage';
96
106
  lastServerSync?: number;
97
107
  isDirty?: boolean;
98
108
  baseServerState?: any;
99
-
100
109
  arrayKeys?: string[];
101
-
102
110
  fields?: Record<string, any>;
103
111
  virtualizer?: {
104
112
  itemHeight?: number;
@@ -106,14 +114,12 @@ export type ShadowMetadata = {
106
114
  };
107
115
  syncInfo?: { status: string };
108
116
  validation?: ValidationState;
117
+ features?: {
118
+ syncEnabled: boolean;
119
+ validationEnabled: boolean;
120
+ localStorageEnabled: boolean;
121
+ };
109
122
  lastUpdated?: number;
110
- value?: any;
111
- classSignals?: Array<{
112
- id: string;
113
- effect: string;
114
- lastClasses: string;
115
- deps: any[];
116
- }>;
117
123
  signals?: Array<{
118
124
  instanceId: string;
119
125
  parentId: string;
@@ -125,12 +131,7 @@ export type ShadowMetadata = {
125
131
  path: string[];
126
132
  componentId: string;
127
133
  meta?: any;
128
- mapFn: (
129
- setter: any,
130
- index: number,
131
-
132
- arraySetter: any
133
- ) => ReactNode;
134
+ mapFn: (setter: any, index: number, arraySetter: any) => ReactNode;
134
135
  containerRef: HTMLDivElement | null;
135
136
  rebuildStateShape: any;
136
137
  }>;
@@ -152,36 +153,55 @@ export type ShadowMetadata = {
152
153
  >;
153
154
  } & ComponentsType;
154
155
 
155
- export type ValidationStatus =
156
- | 'PRISTINE' // Untouched, matches initial state.
157
- | 'DIRTY' // Changed, but no validation run yet.
158
- | 'VALID_LIVE' // Valid while typing.
159
- | 'INVALID_LIVE' // Gentle error during typing.
160
- | 'VALIDATION_FAILED' // Hard error on blur/submit.
161
- | 'VALID_PENDING_SYNC' // Passed validation, ready for sync.
162
- | 'SYNCING' // Actively being sent to the server.
163
- | 'SYNCED' // Server confirmed success.
164
- | 'SYNC_FAILED'; // Server rejected the data.
165
-
166
- export type ValidationState = {
167
- status: ValidationStatus;
168
- message?: string;
169
- lastValidated?: number;
170
- validatedValue?: any;
156
+ type ShadowNode = {
157
+ value?: any;
158
+ _meta?: ShadowMetadata;
159
+ [key: string]: any;
171
160
  };
172
- export type CogsEvent =
173
- | { type: 'INSERT'; path: string; itemKey: string; index: number }
174
- | { type: 'REMOVE'; path: string; itemKey: string }
175
- | { type: 'UPDATE'; path: string; newValue: any }
176
- | { type: 'ITEMHEIGHT'; itemKey: string; height: number }
177
- | { type: 'RELOAD'; path: string };
178
- export type CogsGlobalState = {
179
- updateQueue: Set<() => void>;
180
- isFlushScheduled: boolean;
181
161
 
182
- flushUpdates: () => void;
162
+ export type CogsGlobalState = {
163
+ // NEW shadow store
164
+ shadowStateStore: Map<string, ShadowNode>;
165
+ setTransformCache: (
166
+ key: string,
167
+ path: string[],
168
+ cacheKey: string,
169
+ cacheData: any
170
+ ) => void;
171
+ initializeShadowState: (key: string, initialState: any) => void;
172
+ getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
173
+ getShadowMetadata: (
174
+ key: string,
175
+ path: string[]
176
+ ) => ShadowMetadata | undefined;
183
177
 
184
- // --- Shadow State and Subscription System ---
178
+ setShadowMetadata: (key: string, path: string[], metadata: any) => void;
179
+ getShadowValue: (
180
+ key: string,
181
+ path: string[],
182
+ validArrayIds?: string[],
183
+ log?: boolean
184
+ ) => any;
185
+ updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
186
+ insertManyShadowArrayElements: (
187
+ key: string,
188
+ arrayPath: string[],
189
+ newItems: any[],
190
+ index?: number
191
+ ) => void;
192
+ addItemsToArrayNode: (
193
+ key: string,
194
+ arrayPath: string[],
195
+ newItems: any,
196
+ newKeys: string[]
197
+ ) => void;
198
+ insertShadowArrayElement: (
199
+ key: string,
200
+ arrayPath: string[],
201
+ newItem: any,
202
+ index?: number
203
+ ) => void;
204
+ removeShadowArrayElement: (key: string, itemPath: string[]) => void;
185
205
  registerComponent: (
186
206
  stateKey: string,
187
207
  componentId: string,
@@ -193,43 +213,11 @@ export type CogsGlobalState = {
193
213
  dependencyPath: string[],
194
214
  fullComponentId: string
195
215
  ) => void;
196
- shadowStateStore: Map<string, ShadowMetadata>;
197
-
198
216
  markAsDirty: (
199
217
  key: string,
200
218
  path: string[],
201
219
  options: { bubble: boolean }
202
220
  ) => void;
203
- // These method signatures stay the same
204
- initializeShadowState: (key: string, initialState: any) => void;
205
- updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
206
- insertShadowArrayElement: (
207
- key: string,
208
- arrayPath: string[],
209
- newItem: any
210
- ) => void;
211
- removeShadowArrayElement: (key: string, arrayPath: string[]) => void;
212
- getShadowValue: (
213
- key: string,
214
-
215
- validArrayIds?: string[]
216
- ) => any;
217
-
218
- getShadowMetadata: (
219
- key: string,
220
- path: string[]
221
- ) => ShadowMetadata | undefined;
222
- setShadowMetadata: (
223
- key: string,
224
- path: string[],
225
- metadata: Omit<ShadowMetadata, 'id'>
226
- ) => void;
227
- setTransformCache: (
228
- key: string,
229
- path: string[],
230
- cacheKey: string,
231
- cacheData: any
232
- ) => void;
233
221
 
234
222
  pathSubscribers: Map<string, Set<(newValue: any) => void>>;
235
223
  subscribeToPath: (
@@ -244,12 +232,8 @@ export type CogsGlobalState = {
244
232
  clearSelectedIndex: ({ arrayKey }: { arrayKey: string }) => void;
245
233
  clearSelectedIndexesForState: (stateKey: string) => void;
246
234
 
247
- // --- Core State and Updaters ---
248
-
249
235
  initialStateOptions: { [key: string]: OptionsType };
250
-
251
236
  initialStateGlobal: { [key: string]: StateValue };
252
-
253
237
  updateInitialStateGlobal: (key: string, newState: StateValue) => void;
254
238
 
255
239
  getInitialOptions: (key: string) => OptionsType | undefined;
@@ -268,542 +252,505 @@ export type CogsGlobalState = {
268
252
 
269
253
  stateLog: Map<string, Map<string, UpdateTypeDetail>>;
270
254
  syncInfoStore: Map<string, SyncInfo>;
271
- addStateLog: (key: string, update: UpdateTypeDetail) => void;
255
+ addStateLog: (updates: UpdateTypeDetail[]) => void;
272
256
 
273
257
  setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
274
258
  getSyncInfo: (key: string) => SyncInfo | null;
275
259
  };
276
- const isSimpleObject = (value: any): boolean => {
277
- // Most common cases first
278
- if (value === null || typeof value !== 'object') return false;
279
260
 
280
- // Arrays are simple objects
281
- if (Array.isArray(value)) return true;
261
+ export function buildShadowNode(value: any): ShadowNode {
262
+ if (value === null || typeof value !== 'object') {
263
+ return { value };
264
+ }
282
265
 
283
- // Plain objects second most common
284
- if (value.constructor === Object) return true;
266
+ if (Array.isArray(value)) {
267
+ const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } };
268
+ const idKeys: string[] = [];
285
269
 
286
- // Everything else is not simple
287
- return false;
288
- };
289
- export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
290
- updateQueue: new Set<() => void>(),
291
- // A flag to ensure we only schedule the flush once per event-loop tick.
292
- isFlushScheduled: false,
270
+ value.forEach((item) => {
271
+ const itemId = `id:${ulid()}`;
272
+ arrayNode[itemId] = buildShadowNode(item);
273
+ idKeys.push(itemId);
274
+ });
293
275
 
294
- // This function is called by queueMicrotask to execute all queued updates.
295
- flushUpdates: () => {
296
- const { updateQueue } = get();
276
+ arrayNode._meta!.arrayKeys = idKeys;
277
+ return arrayNode;
278
+ }
297
279
 
298
- if (updateQueue.size > 0) {
299
- startTransition(() => {
300
- updateQueue.forEach((updateFn) => updateFn());
301
- });
280
+ if (value.constructor === Object) {
281
+ const objectNode: ShadowNode = { _meta: {} };
282
+ for (const key in value) {
283
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
284
+ objectNode[key] = buildShadowNode(value[key]);
285
+ }
302
286
  }
287
+ return objectNode;
288
+ }
289
+
290
+ return { value };
291
+ }
292
+
293
+ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
294
+ shadowStateStore: new Map<string, ShadowNode>(),
303
295
 
304
- // Clear the queue and reset the flag for the next event loop tick.
305
- set({ updateQueue: new Set(), isFlushScheduled: false });
296
+ setTransformCache: (
297
+ key: string,
298
+ path: string[],
299
+ cacheKey: string,
300
+ cacheData: any
301
+ ) => {
302
+ const metadata = get().getShadowMetadata(key, path) || {};
303
+ if (!metadata.transformCaches) {
304
+ metadata.transformCaches = new Map();
305
+ }
306
+ metadata.transformCaches.set(cacheKey, cacheData);
307
+ get().setShadowMetadata(key, path, {
308
+ transformCaches: metadata.transformCaches,
309
+ });
306
310
  },
307
- addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
311
+
312
+ initializeShadowState: (key: string, initialState: any) => {
308
313
  set((state) => {
309
314
  const newShadowStore = new Map(state.shadowStateStore);
310
- const dependencyKey = [stateKey, ...dependencyPath].join('.');
315
+ const existingRoot =
316
+ newShadowStore.get(key) || newShadowStore.get(`[${key}`);
317
+ let preservedMetadata: Partial<ShadowMetadata> = {};
318
+
319
+ if (existingRoot?._meta) {
320
+ const {
321
+ components,
322
+ features,
323
+ lastServerSync,
324
+ stateSource,
325
+ baseServerState,
326
+ } = existingRoot._meta;
327
+ if (components) preservedMetadata.components = components;
328
+ if (features) preservedMetadata.features = features;
329
+ if (lastServerSync) preservedMetadata.lastServerSync = lastServerSync;
330
+ if (stateSource) preservedMetadata.stateSource = stateSource;
331
+ if (baseServerState)
332
+ preservedMetadata.baseServerState = baseServerState;
333
+ }
311
334
 
312
- // --- Part 1: Update the path's own metadata ---
313
- const pathMeta = newShadowStore.get(dependencyKey) || {};
314
- // Create a *new* Set to ensure immutability
315
- const pathComponents = new Set(pathMeta.pathComponents);
316
- pathComponents.add(fullComponentId);
317
- // Update the metadata for the specific path
318
- newShadowStore.set(dependencyKey, { ...pathMeta, pathComponents });
335
+ newShadowStore.delete(key);
336
+ newShadowStore.delete(`[${key}`);
319
337
 
320
- // --- Part 2: Update the component's own list of paths ---
321
- const rootMeta = newShadowStore.get(stateKey) || {};
322
- const component = rootMeta.components?.get(fullComponentId);
338
+ const newRoot = buildShadowNode(initialState);
323
339
 
324
- // If the component exists, update its `paths` set immutably
325
- if (component) {
326
- const newPaths = new Set(component.paths);
327
- newPaths.add(dependencyKey);
340
+ if (!newRoot._meta) newRoot._meta = {};
341
+ Object.assign(newRoot._meta, preservedMetadata);
328
342
 
329
- const newComponentRegistration = { ...component, paths: newPaths };
330
- const newComponentsMap = new Map(rootMeta.components);
331
- newComponentsMap.set(fullComponentId, newComponentRegistration);
332
-
333
- // Update the root metadata with the new components map
334
- newShadowStore.set(stateKey, {
335
- ...rootMeta,
336
- components: newComponentsMap,
337
- });
338
- }
343
+ const storageKey = Array.isArray(initialState) ? `[${key}` : key;
344
+ newShadowStore.set(storageKey, newRoot);
339
345
 
340
- // Return the final, updated state
341
346
  return { shadowStateStore: newShadowStore };
342
347
  });
343
348
  },
344
- registerComponent: (stateKey, fullComponentId, registration) => {
345
- set((state) => {
346
- const newShadowStore = new Map(state.shadowStateStore);
347
- const rootMeta = newShadowStore.get(stateKey) || {};
348
- const components = new Map(rootMeta.components);
349
- components.set(fullComponentId, registration);
350
- newShadowStore.set(stateKey, { ...rootMeta, components });
351
- return { shadowStateStore: newShadowStore };
352
- });
349
+
350
+ getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
351
+ const store = get().shadowStateStore;
352
+ let current: any = store.get(key) || store.get(`[${key}`);
353
+
354
+ if (!current) return undefined;
355
+ if (path.length === 0) return current;
356
+
357
+ for (const segment of path) {
358
+ if (typeof current !== 'object' || current === null) return undefined;
359
+ current = current[segment];
360
+ if (current === undefined) return undefined;
361
+ }
362
+ return current;
353
363
  },
354
364
 
355
- unregisterComponent: (stateKey, fullComponentId) => {
365
+ getShadowMetadata: (
366
+ key: string,
367
+ path: string[]
368
+ ): ShadowMetadata | undefined => {
369
+ const node = get().getShadowNode(key, path);
370
+ return node?._meta;
371
+ },
372
+
373
+ setShadowMetadata: (
374
+ key: string,
375
+ path: string[],
376
+ newMetadata: Partial<ShadowMetadata>
377
+ ) => {
356
378
  set((state) => {
357
- const newShadowStore = new Map(state.shadowStateStore);
358
- const rootMeta = newShadowStore.get(stateKey);
359
- if (!rootMeta?.components) {
360
- return state; // Return original state, no change needed
379
+ const newStore = new Map(state.shadowStateStore);
380
+ const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
381
+ let root = newStore.get(rootKey);
382
+
383
+ if (!root) {
384
+ root = {};
385
+ newStore.set(rootKey, root);
361
386
  }
362
387
 
363
- const components = new Map(rootMeta.components);
364
- const wasDeleted = components.delete(fullComponentId);
388
+ const clonedRoot: any = { ...root };
389
+ newStore.set(rootKey, clonedRoot);
365
390
 
366
- // Only update state if something was actually deleted
367
- if (wasDeleted) {
368
- newShadowStore.set(stateKey, { ...rootMeta, components });
369
- return { shadowStateStore: newShadowStore };
391
+ let current = clonedRoot;
392
+ for (const segment of path) {
393
+ const nextNode = current[segment] || {};
394
+ current[segment] = { ...nextNode };
395
+ current = current[segment];
370
396
  }
371
397
 
372
- return state; // Nothing changed
398
+ current._meta = { ...(current._meta || {}), ...newMetadata };
399
+
400
+ return { shadowStateStore: newStore };
373
401
  });
374
402
  },
375
- markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
376
- const { shadowStateStore } = get();
377
- const updates = new Map<string, ShadowMetadata>();
403
+ getShadowValue: (
404
+ key: string,
405
+ path: string[],
406
+ validArrayIds?: string[],
407
+ log?: boolean
408
+ ) => {
409
+ const node = get().getShadowNode(key, path);
378
410
 
379
- const setDirty = (currentPath: string[]) => {
380
- const fullKey = [key, ...currentPath].join('.');
381
- const meta = shadowStateStore.get(fullKey) || {};
411
+ if (node === null || node === undefined) return undefined;
382
412
 
383
- // If already dirty, no need to update
384
- if (meta.isDirty === true) {
385
- return true; // Return true to indicate parent is dirty
386
- }
413
+ const nodeKeys = Object.keys(node);
387
414
 
388
- updates.set(fullKey, { ...meta, isDirty: true });
389
- return false; // Not previously dirty
390
- };
415
+ const isPrimitiveWrapper =
416
+ Object.prototype.hasOwnProperty.call(node, 'value') &&
417
+ nodeKeys.every((k) => k === 'value' || k === '_meta');
391
418
 
392
- // Mark the target path
393
- setDirty(path);
419
+ if (isPrimitiveWrapper) {
420
+ return node.value;
421
+ }
394
422
 
395
- // Bubble up if requested
396
- if (options.bubble) {
397
- let parentPath = [...path];
398
- while (parentPath.length > 0) {
399
- parentPath.pop();
400
- const wasDirty = setDirty(parentPath);
401
- if (wasDirty) {
402
- break; // Stop bubbling if parent was already dirty
403
- }
404
- }
423
+ // Array Check (This part is correct)
424
+ const isArrayNode =
425
+ node._meta &&
426
+ Object.prototype.hasOwnProperty.call(node._meta, 'arrayKeys');
427
+ if (isArrayNode) {
428
+ const keysToIterate =
429
+ validArrayIds !== undefined && validArrayIds.length > 0
430
+ ? validArrayIds
431
+ : node._meta!.arrayKeys!;
432
+
433
+ return keysToIterate.map((itemKey: string) =>
434
+ get().getShadowValue(key, [...path, itemKey])
435
+ );
405
436
  }
406
437
 
407
- // Apply all updates at once
408
- if (updates.size > 0) {
409
- set((state) => {
410
- updates.forEach((meta, key) => {
411
- state.shadowStateStore.set(key, meta);
412
- });
413
- return state;
414
- });
438
+ const result: any = {};
439
+ for (const propKey of nodeKeys) {
440
+ if (propKey !== '_meta' && !propKey.startsWith('id:')) {
441
+ result[propKey] = get().getShadowValue(key, [...path, propKey]);
442
+ }
415
443
  }
444
+ return result;
416
445
  },
417
- serverStateUpdates: new Map(),
418
- setServerStateUpdate: (key, serverState) => {
446
+ updateShadowAtPath: (key, path, newValue) => {
419
447
  set((state) => {
420
- const newMap = new Map(state.serverStateUpdates);
421
- newMap.set(key, serverState);
422
- return { serverStateUpdates: newMap };
423
- });
424
-
425
- // Notify all subscribers for this key
426
- get().notifyPathSubscribers(key, {
427
- type: 'SERVER_STATE_UPDATE',
428
- serverState,
429
- });
430
- },
431
- shadowStateStore: new Map(),
432
- getShadowNode: (key: string) => get().shadowStateStore.get(key),
433
- pathSubscribers: new Map<string, Set<(newValue: any) => void>>(),
434
-
435
- subscribeToPath: (path, callback) => {
436
- const subscribers = get().pathSubscribers;
437
- const subsForPath = subscribers.get(path) || new Set();
438
- subsForPath.add(callback);
439
- subscribers.set(path, subsForPath);
448
+ const newStore = new Map(state.shadowStateStore);
449
+ const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
450
+ let root = newStore.get(rootKey);
451
+ if (!root) return state;
452
+ const clonedRoot: any = { ...root };
453
+ newStore.set(rootKey, clonedRoot);
454
+ let parentNode = clonedRoot;
455
+ for (let i = 0; i < path.length - 1; i++) {
456
+ parentNode[path[i]!] = { ...(parentNode[path[i]!] || {}) };
457
+ parentNode = parentNode[path[i]!];
458
+ }
459
+ const targetNode =
460
+ path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
440
461
 
441
- return () => {
442
- const currentSubs = get().pathSubscribers.get(path);
443
- if (currentSubs) {
444
- currentSubs.delete(callback);
445
- if (currentSubs.size === 0) {
446
- get().pathSubscribers.delete(path);
447
- }
462
+ if (!targetNode) {
463
+ parentNode[path[path.length - 1]!] = buildShadowNode(newValue);
464
+ return { shadowStateStore: newStore };
448
465
  }
449
- };
450
- },
451
466
 
452
- notifyPathSubscribers: (updatedPath, newValue) => {
453
- const subscribers = get().pathSubscribers;
454
- const subs = subscribers.get(updatedPath);
467
+ function intelligentMerge(nodeToUpdate: any, plainValue: any) {
468
+ if (
469
+ typeof plainValue !== 'object' ||
470
+ plainValue === null ||
471
+ Array.isArray(plainValue)
472
+ ) {
473
+ const oldMeta = nodeToUpdate._meta;
474
+ const newNode = buildShadowNode(plainValue);
475
+ if (oldMeta) {
476
+ newNode._meta = { ...oldMeta, ...(newNode._meta || {}) };
477
+ }
478
+ Object.keys(nodeToUpdate).forEach((key) => delete nodeToUpdate[key]);
479
+ Object.assign(nodeToUpdate, newNode);
480
+ return;
481
+ }
455
482
 
456
- if (subs) {
457
- subs.forEach((callback) => callback(newValue));
458
- }
459
- },
483
+ const plainValueKeys = new Set(Object.keys(plainValue));
460
484
 
461
- initializeShadowState: (key: string, initialState: any) => {
462
- set((state) => {
463
- // 1. Make a copy of the current store to modify it
464
- const newShadowStore = new Map(state.shadowStateStore);
465
- console.log('initializeShadowState');
466
- // 2. PRESERVE the existing components map before doing anything else
467
- const existingRootMeta = newShadowStore.get(key);
468
- const preservedComponents = existingRootMeta?.components;
469
-
470
- // 3. Wipe all old shadow entries for this state key
471
- const prefixToDelete = key + '.';
472
- for (const k of Array.from(newShadowStore.keys())) {
473
- if (k === key || k.startsWith(prefixToDelete)) {
474
- newShadowStore.delete(k);
485
+ for (const propKey of plainValueKeys) {
486
+ const childValue = plainValue[propKey];
487
+ if (nodeToUpdate[propKey]) {
488
+ intelligentMerge(nodeToUpdate[propKey], childValue);
489
+ } else {
490
+ nodeToUpdate[propKey] = buildShadowNode(childValue);
491
+ }
475
492
  }
476
- }
477
493
 
478
- // 4. Run your original logic to rebuild the state tree from scratch
479
- const processValue = (value: any, path: string[]) => {
480
- const nodeKey = [key, ...path].join('.');
494
+ for (const nodeKey in nodeToUpdate) {
495
+ if (
496
+ nodeKey === '_meta' ||
497
+ !Object.prototype.hasOwnProperty.call(nodeToUpdate, nodeKey)
498
+ )
499
+ continue;
481
500
 
482
- if (Array.isArray(value)) {
483
- const childIds: string[] = [];
484
- value.forEach(() => {
485
- const itemId = `id:${ulid()}`;
486
- childIds.push(nodeKey + '.' + itemId);
487
- });
488
- newShadowStore.set(nodeKey, { arrayKeys: childIds });
489
- value.forEach((item, index) => {
490
- const itemId = childIds[index]!.split('.').pop();
491
- processValue(item, [...path!, itemId!]);
492
- });
493
- } else if (isSimpleObject(value)) {
494
- const fields = Object.fromEntries(
495
- Object.keys(value).map((k) => [k, nodeKey + '.' + k])
496
- );
497
- newShadowStore.set(nodeKey, { fields });
498
- Object.keys(value).forEach((k) => {
499
- processValue(value[k], [...path, k]);
500
- });
501
- } else {
502
- newShadowStore.set(nodeKey, { value });
501
+ if (!plainValueKeys.has(nodeKey)) {
502
+ delete nodeToUpdate[nodeKey];
503
+ }
503
504
  }
504
- };
505
- processValue(initialState, []);
506
-
507
- // 5. RESTORE the preserved components map onto the new root metadata
508
- if (preservedComponents) {
509
- const newRootMeta = newShadowStore.get(key) || {};
510
- newShadowStore.set(key, {
511
- ...newRootMeta,
512
- components: preservedComponents,
513
- });
514
505
  }
515
506
 
516
- // 6. Return the completely updated state
517
- return { shadowStateStore: newShadowStore };
507
+ intelligentMerge(targetNode, newValue);
508
+
509
+ get().notifyPathSubscribers([key, ...path].join('.'), {
510
+ type: 'UPDATE',
511
+ newValue,
512
+ });
513
+ return { shadowStateStore: newStore };
518
514
  });
519
515
  },
520
-
521
- getShadowValue: (fullKey: string, validArrayIds?: string[]) => {
522
- const memo = new Map<string, any>();
523
- const reconstruct = (keyToBuild: string, ids?: string[]): any => {
524
- if (memo.has(keyToBuild)) {
525
- return memo.get(keyToBuild);
516
+ addItemsToArrayNode: (key, arrayPath, newItems, newKeys) => {
517
+ set((state) => {
518
+ const newStore = new Map(state.shadowStateStore);
519
+ const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
520
+ let root = newStore.get(rootKey);
521
+ if (!root) {
522
+ console.error('Root not found for state key:', key);
523
+ return state;
526
524
  }
527
525
 
528
- const shadowMeta = get().shadowStateStore.get(keyToBuild);
529
- if (!shadowMeta) {
530
- return undefined;
531
- }
526
+ const clonedRoot = { ...root };
527
+ newStore.set(rootKey, clonedRoot);
532
528
 
533
- if (shadowMeta.value !== undefined) {
534
- return shadowMeta.value;
529
+ let current = clonedRoot;
530
+ for (const segment of arrayPath) {
531
+ const nextNode = current[segment] || {};
532
+ current[segment] = { ...nextNode };
533
+ current = current[segment];
535
534
  }
536
535
 
537
- let result: any; // The value we are about to build.
536
+ Object.assign(current, newItems);
537
+ current._meta = { ...(current._meta || {}), arrayKeys: newKeys };
538
538
 
539
- if (shadowMeta.arrayKeys) {
540
- const keys = ids ?? shadowMeta.arrayKeys;
541
- result = [];
542
- memo.set(keyToBuild, result);
543
- keys.forEach((itemKey) => {
544
- result.push(reconstruct(itemKey));
545
- });
546
- } else if (shadowMeta.fields) {
547
- result = {};
548
- memo.set(keyToBuild, result);
549
- Object.entries(shadowMeta.fields).forEach(([key, fieldPath]) => {
550
- result[key] = reconstruct(fieldPath as string);
551
- });
552
- } else {
553
- result = undefined;
554
- }
539
+ return { shadowStateStore: newStore };
540
+ });
541
+ },
555
542
 
556
- // Return the final, fully populated result.
557
- return result;
558
- };
543
+ insertShadowArrayElement: (key, arrayPath, newItem, index) => {
544
+ const arrayNode = get().getShadowNode(key, arrayPath);
545
+ if (!arrayNode?._meta?.arrayKeys) {
546
+ console.error(
547
+ `Array not found at path: ${[key, ...arrayPath].join('.')}`
548
+ );
549
+ return;
550
+ }
559
551
 
560
- // Start the process by calling the inner function on the root key.
561
- return reconstruct(fullKey, validArrayIds);
562
- },
563
- getShadowMetadata: (key: string, path: string[]) => {
564
- const fullKey = [key, ...path].join('.');
552
+ const newItemId = `id:${ulid()}`;
553
+ const itemsToAdd = { [newItemId]: buildShadowNode(newItem) };
554
+
555
+ const currentKeys = arrayNode._meta.arrayKeys;
556
+ const newKeys = [...currentKeys];
557
+ const insertionPoint =
558
+ index !== undefined && index >= 0 && index <= newKeys.length
559
+ ? index
560
+ : newKeys.length;
561
+ newKeys.splice(insertionPoint, 0, newItemId);
562
+
563
+ get().addItemsToArrayNode(key, arrayPath, itemsToAdd, newKeys);
565
564
 
566
- return get().shadowStateStore.get(fullKey);
565
+ const arrayKey = [key, ...arrayPath].join('.');
566
+ get().notifyPathSubscribers(arrayKey, {
567
+ type: 'INSERT',
568
+ path: arrayKey,
569
+ itemKey: `${arrayKey}.${newItemId}`,
570
+ index: insertionPoint,
571
+ });
567
572
  },
573
+ insertManyShadowArrayElements: (key, arrayPath, newItems, index) => {
574
+ if (!newItems || newItems.length === 0) {
575
+ return;
576
+ }
568
577
 
569
- setShadowMetadata: (key, path, metadata) => {
570
- const fullKey = [key, ...path].join('.');
571
- const existingMeta = get().shadowStateStore.get(fullKey);
572
-
573
- // --- THIS IS THE TRAP ---
574
- // If the existing metadata HAS a components map, but the NEW metadata DOES NOT,
575
- // it means we are about to wipe it out. This is the bug.
576
- if (existingMeta?.components && !metadata.components) {
577
- console.group(
578
- '%c🚨 RACE CONDITION DETECTED! 🚨',
579
- 'color: red; font-size: 18px; font-weight: bold;'
580
- );
578
+ const arrayNode = get().getShadowNode(key, arrayPath);
579
+ if (!arrayNode?._meta?.arrayKeys) {
581
580
  console.error(
582
- `An overwrite is about to happen on stateKey: "${key}" at path: [${path.join(', ')}]`
583
- );
584
- console.log(
585
- 'The EXISTING metadata had a components map:',
586
- existingMeta.components
581
+ `Array not found at path: ${[key, ...arrayPath].join('.')}`
587
582
  );
588
- console.log(
589
- 'The NEW metadata is trying to save WITHOUT a components map:',
590
- metadata
591
- );
592
- console.log(
593
- '%cStack trace to the function that caused this overwrite:',
594
- 'font-weight: bold;'
595
- );
596
- console.trace(); // This prints the call stack, leading you to the bad code.
597
- console.groupEnd();
583
+ return;
598
584
  }
599
- // --- END OF TRAP ---
600
585
 
601
- const newShadowStore = new Map(get().shadowStateStore);
602
- const finalMeta = { ...(existingMeta || {}), ...metadata };
603
- newShadowStore.set(fullKey, finalMeta);
604
- set({ shadowStateStore: newShadowStore });
605
- },
606
- setTransformCache: (
607
- key: string,
608
- path: string[],
609
- cacheKey: string,
610
- cacheData: any
611
- ) => {
612
- const fullKey = [key, ...path].join('.');
613
- const newShadowStore = new Map(get().shadowStateStore);
614
- const existing = newShadowStore.get(fullKey) || {};
586
+ const itemsToAdd: Record<string, any> = {};
587
+ const newIds: string[] = [];
615
588
 
616
- // Initialize transformCaches if it doesn't exist
617
- if (!existing.transformCaches) {
618
- existing.transformCaches = new Map();
619
- }
589
+ newItems.forEach((item) => {
590
+ const newItemId = `id:${ulid()}`;
591
+ newIds.push(newItemId);
592
+ itemsToAdd[newItemId] = buildShadowNode(item);
593
+ });
620
594
 
621
- // Update just the specific cache entry
622
- existing.transformCaches.set(cacheKey, cacheData);
595
+ const currentKeys = arrayNode._meta.arrayKeys;
596
+ const finalKeys = [...currentKeys];
597
+ const insertionPoint =
598
+ index !== undefined && index >= 0 && index <= finalKeys.length
599
+ ? index
600
+ : finalKeys.length;
601
+ finalKeys.splice(insertionPoint, 0, ...newIds);
623
602
 
624
- // Update shadow store WITHOUT notifying path subscribers
625
- newShadowStore.set(fullKey, existing);
626
- set({ shadowStateStore: newShadowStore });
603
+ get().addItemsToArrayNode(key, arrayPath, itemsToAdd, finalKeys);
627
604
 
628
- // Don't call notifyPathSubscribers here - cache updates shouldn't trigger renders
629
- },
630
- insertShadowArrayElement: (
631
- key: string,
632
- arrayPath: string[],
633
- newItem: any
634
- ) => {
635
- const newShadowStore = new Map(get().shadowStateStore);
636
605
  const arrayKey = [key, ...arrayPath].join('.');
637
- const parentMeta = newShadowStore.get(arrayKey);
606
+ get().notifyPathSubscribers(arrayKey, {
607
+ type: 'INSERT_MANY',
608
+ path: arrayKey,
609
+ count: newItems.length,
610
+ index: insertionPoint,
611
+ });
612
+ },
638
613
 
639
- if (!parentMeta || !parentMeta.arrayKeys) return;
614
+ removeShadowArrayElement: (key, itemPath) => {
615
+ if (itemPath.length === 0) return;
640
616
 
641
- const newItemId = `id:${ulid()}`;
642
- const fullItemKey = arrayKey + '.' + newItemId;
643
-
644
- // Just add to the end (or at a specific index if provided)
645
- const newArrayKeys = [...parentMeta.arrayKeys];
646
- newArrayKeys.push(fullItemKey); // Or use splice if you have an index
647
- newShadowStore.set(arrayKey, { ...parentMeta, arrayKeys: newArrayKeys });
648
-
649
- // Process the new item - but use the correct logic
650
- const processNewItem = (value: any, path: string[]) => {
651
- const nodeKey = [key, ...path].join('.');
652
-
653
- if (Array.isArray(value)) {
654
- // Handle arrays...
655
- } else if (typeof value === 'object' && value !== null) {
656
- // Create fields mapping
657
- const fields = Object.fromEntries(
658
- Object.keys(value).map((k) => [k, nodeKey + '.' + k])
659
- );
660
- newShadowStore.set(nodeKey, { fields });
661
-
662
- // Process each field
663
- Object.entries(value).forEach(([k, v]) => {
664
- processNewItem(v, [...path, k]);
665
- });
666
- } else {
667
- // Primitive value
668
- newShadowStore.set(nodeKey, { value });
669
- }
670
- };
617
+ const arrayPath = itemPath.slice(0, -1);
618
+ const itemId = itemPath[itemPath.length - 1];
619
+ if (!itemId?.startsWith('id:')) return;
620
+
621
+ const arrayNode = get().getShadowNode(key, arrayPath);
622
+ if (!arrayNode?._meta?.arrayKeys) return;
623
+
624
+ const newKeys = arrayNode._meta.arrayKeys.filter((k) => k !== itemId);
625
+ delete arrayNode[itemId];
671
626
 
672
- processNewItem(newItem, [...arrayPath, newItemId]);
673
- set({ shadowStateStore: newShadowStore });
627
+ get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
674
628
 
629
+ const arrayKey = [key, ...arrayPath].join('.');
675
630
  get().notifyPathSubscribers(arrayKey, {
676
- type: 'INSERT',
631
+ type: 'REMOVE',
677
632
  path: arrayKey,
678
- itemKey: fullItemKey,
633
+ itemKey: `${arrayKey}.${itemId}`,
679
634
  });
680
635
  },
681
- removeShadowArrayElement: (key: string, itemPath: string[]) => {
682
- const newShadowStore = new Map(get().shadowStateStore);
683
636
 
684
- // Get the full item key (e.g., "stateKey.products.id:xxx")
685
- const itemKey = [key, ...itemPath].join('.');
637
+ addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
638
+ const metadata = get().getShadowMetadata(stateKey, dependencyPath) || {};
639
+ const newPathComponents = new Set(metadata.pathComponents);
640
+ newPathComponents.add(fullComponentId);
641
+ get().setShadowMetadata(stateKey, dependencyPath, {
642
+ pathComponents: newPathComponents,
643
+ });
686
644
 
687
- // Extract parent path and item ID
688
- const parentPath = itemPath.slice(0, -1);
689
- const parentKey = [key, ...parentPath].join('.');
645
+ const rootMeta = get().getShadowMetadata(stateKey, []);
646
+ if (rootMeta?.components) {
647
+ const component = rootMeta.components.get(fullComponentId);
648
+ if (component) {
649
+ const fullPathKey = [stateKey, ...dependencyPath].join('.');
650
+ const newPaths = new Set(component.paths);
651
+ newPaths.add(fullPathKey);
652
+ const newComponentRegistration = { ...component, paths: newPaths };
653
+ const newComponentsMap = new Map(rootMeta.components);
654
+ newComponentsMap.set(fullComponentId, newComponentRegistration);
655
+ get().setShadowMetadata(stateKey, [], { components: newComponentsMap });
656
+ }
657
+ }
658
+ },
690
659
 
691
- // Get parent metadata
692
- const parentMeta = newShadowStore.get(parentKey);
660
+ registerComponent: (stateKey, fullComponentId, registration) => {
661
+ const rootMeta = get().getShadowMetadata(stateKey, []) || {};
662
+ const components = new Map(rootMeta.components);
663
+ components.set(fullComponentId, registration);
664
+ get().setShadowMetadata(stateKey, [], { components });
665
+ },
693
666
 
694
- if (parentMeta && parentMeta.arrayKeys) {
695
- // Find the index of the item to remove
696
- const indexToRemove = parentMeta.arrayKeys.findIndex(
697
- (arrayItemKey) => arrayItemKey === itemKey
698
- );
667
+ unregisterComponent: (stateKey, fullComponentId) => {
668
+ const rootMeta = get().getShadowMetadata(stateKey, []);
669
+ if (!rootMeta?.components) return;
670
+ const components = new Map(rootMeta.components);
671
+ if (components.delete(fullComponentId)) {
672
+ get().setShadowMetadata(stateKey, [], { components });
673
+ }
674
+ },
699
675
 
700
- if (indexToRemove !== -1) {
701
- // Create new array keys with the item removed
702
- const newArrayKeys = parentMeta.arrayKeys.filter(
703
- (arrayItemKey) => arrayItemKey !== itemKey
704
- );
676
+ markAsDirty: (key, path, options = { bubble: true }) => {
677
+ const setDirtyOnPath = (pathToMark: string[]) => {
678
+ const node = get().getShadowNode(key, pathToMark);
679
+ if (node?._meta?.isDirty) {
680
+ return true;
681
+ }
682
+ get().setShadowMetadata(key, pathToMark, { isDirty: true });
683
+ return false;
684
+ };
705
685
 
706
- // Update parent with new array keys
707
- newShadowStore.set(parentKey, {
708
- ...parentMeta,
709
- arrayKeys: newArrayKeys,
710
- });
686
+ setDirtyOnPath(path);
711
687
 
712
- // Delete all data associated with the removed item
713
- const prefixToDelete = itemKey + '.';
714
- for (const k of Array.from(newShadowStore.keys())) {
715
- if (k === itemKey || k.startsWith(prefixToDelete)) {
716
- newShadowStore.delete(k);
717
- }
688
+ if (options.bubble) {
689
+ let parentPath = [...path];
690
+ while (parentPath.length > 0) {
691
+ parentPath.pop();
692
+ if (setDirtyOnPath(parentPath)) {
693
+ break;
718
694
  }
719
695
  }
720
696
  }
697
+ },
721
698
 
722
- set({ shadowStateStore: newShadowStore });
723
-
724
- get().notifyPathSubscribers(parentKey, {
725
- type: 'REMOVE',
726
- path: parentKey,
727
- itemKey: itemKey, // The exact ID of the removed item
699
+ serverStateUpdates: new Map(),
700
+ setServerStateUpdate: (key, serverState) => {
701
+ set((state) => ({
702
+ serverStateUpdates: new Map(state.serverStateUpdates).set(
703
+ key,
704
+ serverState
705
+ ),
706
+ }));
707
+ get().notifyPathSubscribers(key, {
708
+ type: 'SERVER_STATE_UPDATE',
709
+ serverState,
728
710
  });
729
711
  },
730
712
 
731
- updateShadowAtPath: (key, path, newValue) => {
732
- const fullKey = [key, ...path].join('.');
733
-
734
- // Optimization: Only update if value actually changed
735
- const existingMeta = get().shadowStateStore.get(fullKey);
736
- if (existingMeta?.value === newValue && !isSimpleObject(newValue)) {
737
- return; // Skip update for unchanged primitives
738
- }
739
-
740
- // CHANGE: Don't clone the entire Map, just update in place
741
- set((state) => {
742
- const store = state.shadowStateStore;
743
-
744
- if (!isSimpleObject(newValue)) {
745
- const meta = store.get(fullKey) || {};
746
- store.set(fullKey, { ...meta, value: newValue });
747
- } else {
748
- // Handle objects by iterating
749
- const processObject = (currentPath: string[], objectToSet: any) => {
750
- const currentFullKey = [key, ...currentPath].join('.');
751
- const meta = store.get(currentFullKey);
752
-
753
- if (meta && meta.fields) {
754
- for (const fieldKey in objectToSet) {
755
- if (Object.prototype.hasOwnProperty.call(objectToSet, fieldKey)) {
756
- const childValue = objectToSet[fieldKey];
757
- const childFullPath = meta.fields[fieldKey];
758
-
759
- if (childFullPath) {
760
- if (isSimpleObject(childValue)) {
761
- processObject(
762
- childFullPath.split('.').slice(1),
763
- childValue
764
- );
765
- } else {
766
- const existingChildMeta = store.get(childFullPath) || {};
767
- store.set(childFullPath, {
768
- ...existingChildMeta,
769
- value: childValue,
770
- });
771
- }
772
- }
773
- }
774
- }
775
- }
776
- };
713
+ pathSubscribers: new Map<string, Set<(newValue: any) => void>>(),
714
+ subscribeToPath: (path, callback) => {
715
+ const subscribers = get().pathSubscribers;
716
+ const subsForPath = subscribers.get(path) || new Set();
717
+ subsForPath.add(callback);
718
+ subscribers.set(path, subsForPath);
777
719
 
778
- processObject(path, newValue);
720
+ return () => {
721
+ const currentSubs = get().pathSubscribers.get(path);
722
+ if (currentSubs) {
723
+ currentSubs.delete(callback);
724
+ if (currentSubs.size === 0) {
725
+ get().pathSubscribers.delete(path);
726
+ }
779
727
  }
780
-
781
- // Only notify after all changes are made
782
- get().notifyPathSubscribers(fullKey, { type: 'UPDATE', newValue });
783
-
784
- // Return same reference if using Zustand's immer middleware
785
- return state;
786
- });
728
+ };
729
+ },
730
+ notifyPathSubscribers: (updatedPath, newValue) => {
731
+ const subscribers = get().pathSubscribers;
732
+ const subs = subscribers.get(updatedPath);
733
+ if (subs) {
734
+ subs.forEach((callback) => callback(newValue));
735
+ }
787
736
  },
788
737
  selectedIndicesMap: new Map<string, string>(),
789
- getSelectedIndex: (arrayKey: string, validIds?: string[]): number => {
738
+ getSelectedIndex: (arrayKey, validIds) => {
790
739
  const itemKey = get().selectedIndicesMap.get(arrayKey);
791
-
792
740
  if (!itemKey) return -1;
793
741
 
794
- // Use validIds if provided (for filtered views), otherwise use all arrayKeys
795
- const arrayKeys =
796
- validIds ||
797
- getGlobalStore.getState().getShadowMetadata(arrayKey, [])?.arrayKeys;
742
+ const arrayMeta = get().getShadowMetadata(
743
+ arrayKey.split('.')[0]!,
744
+ arrayKey.split('.').slice(1)
745
+ );
746
+ const arrayKeys = validIds || arrayMeta?.arrayKeys;
798
747
 
799
- if (!arrayKeys) return -1;
800
-
801
- return arrayKeys.indexOf(itemKey);
748
+ return arrayKeys ? arrayKeys.indexOf(itemKey) : -1;
802
749
  },
803
750
 
804
751
  setSelectedIndex: (arrayKey: string, itemKey: string | undefined) => {
805
752
  set((state) => {
806
- const newMap = state.selectedIndicesMap;
753
+ const newMap = new Map(state.selectedIndicesMap); // CREATE A NEW MAP!
807
754
 
808
755
  if (itemKey === undefined) {
809
756
  newMap.delete(arrayKey);
@@ -814,26 +761,24 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
814
761
  });
815
762
  }
816
763
  newMap.set(arrayKey, itemKey);
817
-
818
- get().notifyPathSubscribers(itemKey, {
819
- type: 'THIS_SELECTED',
820
- });
764
+ get().notifyPathSubscribers(itemKey, { type: 'THIS_SELECTED' });
821
765
  }
822
- get().notifyPathSubscribers(arrayKey, {
823
- type: 'GET_SELECTED',
824
- });
766
+
767
+ get().notifyPathSubscribers(arrayKey, { type: 'GET_SELECTED' });
768
+
825
769
  return {
826
770
  ...state,
827
771
  selectedIndicesMap: newMap,
828
772
  };
829
773
  });
830
774
  },
775
+
831
776
  clearSelectedIndex: ({ arrayKey }: { arrayKey: string }): void => {
832
777
  set((state) => {
833
- const newMap = state.selectedIndicesMap;
834
- const acutalKey = newMap.get(arrayKey);
835
- if (acutalKey) {
836
- get().notifyPathSubscribers(acutalKey, {
778
+ const newMap = new Map(state.selectedIndicesMap); // CREATE A NEW MAP!
779
+ const actualKey = newMap.get(arrayKey);
780
+ if (actualKey) {
781
+ get().notifyPathSubscribers(actualKey, {
837
782
  type: 'CLEAR_SELECTION',
838
783
  });
839
784
  }
@@ -848,75 +793,63 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
848
793
  };
849
794
  });
850
795
  },
851
- clearSelectedIndexesForState: (stateKey: string) => {
796
+ clearSelectedIndexesForState: (stateKey) => {
852
797
  set((state) => {
853
- const newOuterMap = new Map(state.selectedIndicesMap);
854
- const changed = newOuterMap.delete(stateKey);
855
- if (changed) {
856
- return { selectedIndicesMap: newOuterMap };
857
- } else {
858
- return {};
798
+ const newMap = new Map(state.selectedIndicesMap);
799
+ let changed = false;
800
+ for (const key of newMap.keys()) {
801
+ if (key === stateKey || key.startsWith(stateKey + '.')) {
802
+ newMap.delete(key);
803
+ changed = true;
804
+ }
859
805
  }
806
+ return changed ? { selectedIndicesMap: newMap } : {};
860
807
  });
861
808
  },
862
809
 
863
810
  initialStateOptions: {},
864
-
865
- stateTimeline: {},
866
- cogsStateStore: {},
867
811
  stateLog: new Map(),
868
-
869
812
  initialStateGlobal: {},
870
813
 
871
- validationErrors: new Map(),
872
- addStateLog: (key, update) => {
814
+ addStateLog: (updates) => {
815
+ if (!updates || updates.length === 0) return;
873
816
  set((state) => {
874
817
  const newLog = new Map(state.stateLog);
875
- const stateLogForKey = new Map(newLog.get(key));
876
- const uniquePathKey = JSON.stringify(update.path);
877
-
878
- const existing = stateLogForKey.get(uniquePathKey);
879
- if (existing) {
880
- // If an update for this path already exists, just modify it. (Fast)
881
- existing.newValue = update.newValue;
882
- existing.timeStamp = update.timeStamp;
883
- } else {
884
- // Otherwise, add the new update. (Fast)
885
- stateLogForKey.set(uniquePathKey, { ...update });
818
+ const logsGroupedByKey = new Map<string, UpdateTypeDetail[]>();
819
+ for (const update of updates) {
820
+ const group = logsGroupedByKey.get(update.stateKey) || [];
821
+ group.push(update);
822
+ logsGroupedByKey.set(update.stateKey, group);
823
+ }
824
+ for (const [key, batchOfUpdates] of logsGroupedByKey.entries()) {
825
+ const newStateLogForKey = new Map(newLog.get(key));
826
+ for (const update of batchOfUpdates) {
827
+ newStateLogForKey.set(JSON.stringify(update.path), { ...update });
828
+ }
829
+ newLog.set(key, newStateLogForKey);
886
830
  }
887
-
888
- newLog.set(key, stateLogForKey);
889
831
  return { stateLog: newLog };
890
832
  });
891
833
  },
892
834
 
893
- getInitialOptions: (key) => {
894
- return get().initialStateOptions[key];
895
- },
896
-
835
+ getInitialOptions: (key) => get().initialStateOptions[key],
897
836
  setInitialStateOptions: (key, value) => {
898
837
  set((prev) => ({
899
- initialStateOptions: {
900
- ...prev.initialStateOptions,
901
- [key]: value,
902
- },
838
+ initialStateOptions: { ...prev.initialStateOptions, [key]: value },
903
839
  }));
904
840
  },
905
841
  updateInitialStateGlobal: (key, newState) => {
906
842
  set((prev) => ({
907
- initialStateGlobal: {
908
- ...prev.initialStateGlobal,
909
- [key]: newState,
910
- },
843
+ initialStateGlobal: { ...prev.initialStateGlobal, [key]: newState },
911
844
  }));
912
845
  },
913
846
 
914
847
  syncInfoStore: new Map<string, SyncInfo>(),
915
- setSyncInfo: (key: string, syncInfo: SyncInfo) =>
848
+ setSyncInfo: (key, syncInfo) =>
916
849
  set((state) => {
917
850
  const newMap = new Map(state.syncInfoStore);
918
851
  newMap.set(key, syncInfo);
919
- return { ...state, syncInfoStore: newMap };
852
+ return { syncInfoStore: newMap };
920
853
  }),
921
- getSyncInfo: (key: string) => get().syncInfoStore.get(key) || null,
854
+ getSyncInfo: (key) => get().syncInfoStore.get(key) || null,
922
855
  }));