cogsbox-state 0.5.463 → 0.5.465

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.ts CHANGED
@@ -3,17 +3,12 @@ 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
10
  import { startTransition, type ReactNode } from 'react';
12
11
 
13
- type StateUpdater<StateValue> =
14
- | StateValue
15
- | ((prevValue: StateValue) => StateValue);
16
-
17
12
  export type FreshValuesObject = {
18
13
  pathsToValues?: string[];
19
14
  prevValue?: any;
@@ -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 = {
@@ -89,16 +81,35 @@ export type ComponentsType = {
89
81
  }
90
82
  >;
91
83
  };
84
+
85
+ export type ValidationStatus =
86
+ | 'NOT_VALIDATED' // Never run
87
+ | 'VALIDATING' // Currently running
88
+ | 'VALID' // Passed
89
+ | 'INVALID'; // Failed
90
+
91
+ export type ValidationError = {
92
+ source: 'client' | 'sync_engine' | 'api';
93
+ message: string;
94
+ severity: 'warning' | 'error'; // warning = gentle, error = blocking
95
+ code?: string; // Optional error code
96
+ };
97
+
98
+ export type ValidationState = {
99
+ status: ValidationStatus;
100
+ errors: ValidationError[];
101
+ lastValidated?: number;
102
+ validatedValue?: any; // Value when last validated
103
+ };
104
+
105
+ // This is the new definition for the metadata object
92
106
  export type ShadowMetadata = {
93
107
  id?: string;
94
-
95
108
  stateSource?: 'default' | 'server' | 'localStorage';
96
109
  lastServerSync?: number;
97
110
  isDirty?: boolean;
98
111
  baseServerState?: any;
99
-
100
112
  arrayKeys?: string[];
101
-
102
113
  fields?: Record<string, any>;
103
114
  virtualizer?: {
104
115
  itemHeight?: number;
@@ -106,14 +117,12 @@ export type ShadowMetadata = {
106
117
  };
107
118
  syncInfo?: { status: string };
108
119
  validation?: ValidationState;
120
+ features?: {
121
+ syncEnabled: boolean;
122
+ validationEnabled: boolean;
123
+ localStorageEnabled: boolean;
124
+ };
109
125
  lastUpdated?: number;
110
- value?: any;
111
- classSignals?: Array<{
112
- id: string;
113
- effect: string;
114
- lastClasses: string;
115
- deps: any[];
116
- }>;
117
126
  signals?: Array<{
118
127
  instanceId: string;
119
128
  parentId: string;
@@ -125,12 +134,7 @@ export type ShadowMetadata = {
125
134
  path: string[];
126
135
  componentId: string;
127
136
  meta?: any;
128
- mapFn: (
129
- setter: any,
130
- index: number,
131
-
132
- arraySetter: any
133
- ) => ReactNode;
137
+ mapFn: (setter: any, index: number, arraySetter: any) => ReactNode;
134
138
  containerRef: HTMLDivElement | null;
135
139
  rebuildStateShape: any;
136
140
  }>;
@@ -152,36 +156,48 @@ export type ShadowMetadata = {
152
156
  >;
153
157
  } & ComponentsType;
154
158
 
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;
159
+ // The shadow node itself can have a value and the metadata object.
160
+ type ShadowNode = {
161
+ value?: any;
162
+ _meta?: ShadowMetadata;
163
+ [key: string]: any; // For nested data properties
171
164
  };
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 };
165
+
178
166
  export type CogsGlobalState = {
179
- updateQueue: Set<() => void>;
180
- isFlushScheduled: boolean;
167
+ // NEW shadow store
168
+ shadowStateStore: Map<string, ShadowNode>; // Changed ShadowMetadata to ShadowNode
169
+ setTransformCache: (
170
+ key: string,
171
+ path: string[],
172
+ cacheKey: string,
173
+ cacheData: any
174
+ ) => void;
175
+ // NEW functions
176
+ initializeShadowState: (key: string, initialState: any) => void;
181
177
 
182
- flushUpdates: () => void;
178
+ // REFACTORED: getShadowNode gets the whole object (data + _meta)
179
+ getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
180
+ // REFACTORED: getShadowMetadata now returns just the _meta field
181
+ getShadowMetadata: (
182
+ key: string,
183
+ path: string[]
184
+ ) => ShadowMetadata | undefined;
183
185
 
184
- // --- Shadow State and Subscription System ---
186
+ setShadowMetadata: (key: string, path: string[], metadata: any) => void;
187
+ getShadowValue: (
188
+ key: string,
189
+ path: string[],
190
+ validArrayIds?: string[],
191
+ log?: boolean
192
+ ) => any;
193
+ updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
194
+ insertShadowArrayElement: (
195
+ key: string,
196
+ arrayPath: string[],
197
+ newItem: any,
198
+ index?: number
199
+ ) => void;
200
+ removeShadowArrayElement: (key: string, itemPath: string[]) => void;
185
201
  registerComponent: (
186
202
  stateKey: string,
187
203
  componentId: string,
@@ -193,7 +209,6 @@ export type CogsGlobalState = {
193
209
  dependencyPath: string[],
194
210
  fullComponentId: string
195
211
  ) => void;
196
- shadowStateStore: Map<string, ShadowMetadata>;
197
212
 
198
213
  markAsDirty: (
199
214
  key: string,
@@ -201,35 +216,6 @@ export type CogsGlobalState = {
201
216
  options: { bubble: boolean }
202
217
  ) => void;
203
218
  // 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
219
 
234
220
  pathSubscribers: Map<string, Set<(newValue: any) => void>>;
235
221
  subscribeToPath: (
@@ -268,655 +254,565 @@ export type CogsGlobalState = {
268
254
 
269
255
  stateLog: Map<string, Map<string, UpdateTypeDetail>>;
270
256
  syncInfoStore: Map<string, SyncInfo>;
271
- addStateLog: (key: string, update: UpdateTypeDetail) => void;
257
+ addStateLog: (updates: UpdateTypeDetail[]) => void;
272
258
 
273
259
  setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
274
260
  getSyncInfo: (key: string) => SyncInfo | null;
275
261
  };
276
- const isSimpleObject = (value: any): boolean => {
277
- // Most common cases first
278
- if (value === null || typeof value !== 'object') return false;
279
262
 
280
- // Arrays are simple objects
281
- if (Array.isArray(value)) return true;
263
+ // CHANGE 1: `METADATA_KEYS` now only contains `_meta` and `value`.
264
+ // The other keys are now properties of the `ShadowMetadata` type.
265
+ export const METADATA_KEYS = new Set(['_meta', 'value']);
266
+
267
+ /**
268
+ * The single source of truth for converting a regular JS value/object
269
+ * into the shadow state tree format with the new `_meta` structure.
270
+ */
271
+ // ✅ CHANGE 2: `buildShadowNode` now creates the `_meta` field.
272
+ export function buildShadowNode(value: any): ShadowNode {
273
+ // Primitives and null are wrapped.
274
+ if (value === null || typeof value !== 'object') {
275
+ return { value };
276
+ }
277
+
278
+ // Arrays are converted to an object with id-keyed children and metadata in `_meta`.
279
+ if (Array.isArray(value)) {
280
+ const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } }; // Initialize with _meta and arrayKeys
281
+ const idKeys: string[] = [];
282
+ value.forEach((item) => {
283
+ const itemId = `id:${ulid()}`;
284
+ arrayNode[itemId] = buildShadowNode(item); // Recurse for each item
285
+ idKeys.push(itemId);
286
+ });
287
+ arrayNode._meta!.arrayKeys = idKeys; // Set the final ordered keys
288
+ return arrayNode;
289
+ }
290
+
291
+ // Plain objects are recursively processed.
292
+ if (value.constructor === Object) {
293
+ const objectNode: ShadowNode = { _meta: {} }; // Initialize with an empty meta object
294
+ for (const key in value) {
295
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
296
+ objectNode[key] = buildShadowNode(value[key]); // Recurse for each property
297
+ }
298
+ }
299
+ return objectNode;
300
+ }
282
301
 
283
- // Plain objects second most common
284
- if (value.constructor === Object) return true;
302
+ // Fallback for other object types (Date, etc.) - treat them as primitives.
303
+ return { value };
304
+ }
285
305
 
286
- // Everything else is not simple
287
- return false;
288
- };
289
306
  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,
307
+ shadowStateStore: new Map<string, ShadowNode>(),
293
308
 
294
- // This function is called by queueMicrotask to execute all queued updates.
295
- flushUpdates: () => {
296
- const { updateQueue } = get();
297
-
298
- if (updateQueue.size > 0) {
299
- startTransition(() => {
300
- updateQueue.forEach((updateFn) => updateFn());
301
- });
309
+ setTransformCache: (
310
+ key: string,
311
+ path: string[],
312
+ cacheKey: string,
313
+ cacheData: any
314
+ ) => {
315
+ // This function now uses setShadowMetadata which correctly places the data.
316
+ const metadata = get().getShadowMetadata(key, path) || {};
317
+ if (!metadata.transformCaches) {
318
+ metadata.transformCaches = new Map();
302
319
  }
303
-
304
- // Clear the queue and reset the flag for the next event loop tick.
305
- set({ updateQueue: new Set(), isFlushScheduled: false });
320
+ metadata.transformCaches.set(cacheKey, cacheData);
321
+ get().setShadowMetadata(key, path, {
322
+ transformCaches: metadata.transformCaches,
323
+ });
306
324
  },
307
- addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
325
+
326
+ initializeShadowState: (key: string, initialState: any) => {
308
327
  set((state) => {
309
328
  const newShadowStore = new Map(state.shadowStateStore);
310
- const dependencyKey = [stateKey, ...dependencyPath].join('.');
311
-
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 });
329
+ const existingRoot =
330
+ newShadowStore.get(key) || newShadowStore.get(`[${key}`);
331
+ let preservedMetadata: Partial<ShadowMetadata> = {};
332
+
333
+ if (existingRoot?._meta) {
334
+ const {
335
+ components,
336
+ features,
337
+ lastServerSync,
338
+ stateSource,
339
+ baseServerState,
340
+ } = existingRoot._meta;
341
+ if (components) preservedMetadata.components = components;
342
+ if (features) preservedMetadata.features = features;
343
+ if (lastServerSync) preservedMetadata.lastServerSync = lastServerSync;
344
+ if (stateSource) preservedMetadata.stateSource = stateSource;
345
+ if (baseServerState)
346
+ preservedMetadata.baseServerState = baseServerState;
347
+ }
319
348
 
320
- // --- Part 2: Update the component's own list of paths ---
321
- const rootMeta = newShadowStore.get(stateKey) || {};
322
- const component = rootMeta.components?.get(fullComponentId);
349
+ newShadowStore.delete(key);
350
+ newShadowStore.delete(`[${key}`);
323
351
 
324
- // If the component exists, update its `paths` set immutably
325
- if (component) {
326
- const newPaths = new Set(component.paths);
327
- newPaths.add(dependencyKey);
328
-
329
- const newComponentRegistration = { ...component, paths: newPaths };
330
- const newComponentsMap = new Map(rootMeta.components);
331
- newComponentsMap.set(fullComponentId, newComponentRegistration);
352
+ const newRoot = buildShadowNode(initialState);
353
+ // Ensure _meta exists before assigning to it
354
+ if (!newRoot._meta) newRoot._meta = {};
355
+ Object.assign(newRoot._meta, preservedMetadata);
332
356
 
333
- // Update the root metadata with the new components map
334
- newShadowStore.set(stateKey, {
335
- ...rootMeta,
336
- components: newComponentsMap,
337
- });
338
- }
357
+ const storageKey = Array.isArray(initialState) ? `[${key}` : key;
358
+ newShadowStore.set(storageKey, newRoot);
339
359
 
340
- // Return the final, updated state
341
- return { shadowStateStore: newShadowStore };
342
- });
343
- },
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
360
  return { shadowStateStore: newShadowStore };
352
361
  });
353
362
  },
354
363
 
355
- unregisterComponent: (stateKey, fullComponentId) => {
356
- 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
361
- }
364
+ // ✅ NEW HELPER: Gets the entire node (data and metadata).
365
+ getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
366
+ const store = get().shadowStateStore;
367
+ let current: any = store.get(key) || store.get(`[${key}`);
362
368
 
363
- const components = new Map(rootMeta.components);
364
- const wasDeleted = components.delete(fullComponentId);
369
+ if (!current) return undefined;
370
+ if (path.length === 0) return current;
365
371
 
366
- // Only update state if something was actually deleted
367
- if (wasDeleted) {
368
- newShadowStore.set(stateKey, { ...rootMeta, components });
369
- return { shadowStateStore: newShadowStore };
370
- }
372
+ for (const segment of path) {
373
+ if (typeof current !== 'object' || current === null) return undefined;
374
+ current = current[segment];
375
+ if (current === undefined) return undefined;
376
+ }
377
+ return current;
378
+ },
371
379
 
372
- return state; // Nothing changed
373
- });
380
+ // REFACTORED: Returns only the `_meta` part of a node.
381
+ getShadowMetadata: (
382
+ key: string,
383
+ path: string[]
384
+ ): ShadowMetadata | undefined => {
385
+ const node = get().getShadowNode(key, path);
386
+ return node?._meta;
374
387
  },
375
- markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
376
- const { shadowStateStore } = get();
377
- const updates = new Map<string, ShadowMetadata>();
378
388
 
379
- const setDirty = (currentPath: string[]) => {
380
- const fullKey = [key, ...currentPath].join('.');
381
- const meta = shadowStateStore.get(fullKey) || {};
389
+ // REFACTORED: Sets data within the `_meta` object.
390
+ setShadowMetadata: (
391
+ key: string,
392
+ path: string[],
393
+ newMetadata: Partial<ShadowMetadata>
394
+ ) => {
395
+ set((state) => {
396
+ const newStore = new Map(state.shadowStateStore);
397
+ const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
398
+ let root = newStore.get(rootKey);
382
399
 
383
- // If already dirty, no need to update
384
- if (meta.isDirty === true) {
385
- return true; // Return true to indicate parent is dirty
400
+ if (!root) {
401
+ root = {};
402
+ newStore.set(rootKey, root);
386
403
  }
387
404
 
388
- updates.set(fullKey, { ...meta, isDirty: true });
389
- return false; // Not previously dirty
390
- };
391
-
392
- // Mark the target path
393
- setDirty(path);
405
+ const clonedRoot: any = { ...root };
406
+ newStore.set(rootKey, clonedRoot);
394
407
 
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
- }
408
+ let current = clonedRoot;
409
+ for (const segment of path) {
410
+ const nextNode = current[segment] || {};
411
+ current[segment] = { ...nextNode }; // Clone for immutability
412
+ current = current[segment];
404
413
  }
405
- }
406
414
 
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
- });
415
- }
416
- },
417
- serverStateUpdates: new Map(),
418
- setServerStateUpdate: (key, serverState) => {
419
- set((state) => {
420
- const newMap = new Map(state.serverStateUpdates);
421
- newMap.set(key, serverState);
422
- return { serverStateUpdates: newMap };
423
- });
415
+ // Ensure _meta object exists and merge the new metadata into it
416
+ current._meta = { ...(current._meta || {}), ...newMetadata };
424
417
 
425
- // Notify all subscribers for this key
426
- get().notifyPathSubscribers(key, {
427
- type: 'SERVER_STATE_UPDATE',
428
- serverState,
418
+ return { shadowStateStore: newStore };
429
419
  });
430
420
  },
431
- shadowStateStore: new Map(),
432
- getShadowNode: (key: string) => get().shadowStateStore.get(key),
433
- pathSubscribers: new Map<string, Set<(newValue: any) => void>>(),
421
+ getShadowValue: (
422
+ key: string,
423
+ path: string[],
424
+ validArrayIds?: string[],
425
+ log?: boolean
426
+ ) => {
427
+ const node = get().getShadowNode(key, path);
434
428
 
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);
429
+ if (node === null || node === undefined) return undefined;
440
430
 
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
- }
448
- }
449
- };
450
- },
431
+ const nodeKeys = Object.keys(node);
451
432
 
452
- notifyPathSubscribers: (updatedPath, newValue) => {
453
- const subscribers = get().pathSubscribers;
454
- const subs = subscribers.get(updatedPath);
433
+ // ✅ FIX: A node is a primitive wrapper ONLY if its keys are 'value' and/or '_meta'.
434
+ // This prevents objects in your data that happen to have a "value" property from being
435
+ // incorrectly treated as wrappers.
436
+ const isPrimitiveWrapper =
437
+ Object.prototype.hasOwnProperty.call(node, 'value') &&
438
+ nodeKeys.every((k) => k === 'value' || k === '_meta');
455
439
 
456
- if (subs) {
457
- subs.forEach((callback) => callback(newValue));
440
+ if (isPrimitiveWrapper) {
441
+ return node.value;
458
442
  }
459
- },
460
443
 
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);
475
- }
476
- }
444
+ // Array Check (This part is correct)
445
+ const isArrayNode =
446
+ node._meta &&
447
+ Object.prototype.hasOwnProperty.call(node._meta, 'arrayKeys');
448
+ if (isArrayNode) {
449
+ const keysToIterate =
450
+ validArrayIds !== undefined && validArrayIds.length > 0
451
+ ? validArrayIds
452
+ : node._meta!.arrayKeys!;
453
+
454
+ return keysToIterate.map((itemKey: string) =>
455
+ get().getShadowValue(key, [...path, itemKey])
456
+ );
457
+ }
477
458
 
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('.');
481
-
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 });
503
- }
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
- });
459
+ // Object Reconstruction (This part is also correct)
460
+ const result: any = {};
461
+ for (const propKey of nodeKeys) {
462
+ // We correctly ignore metadata and array item keys here.
463
+ if (propKey !== '_meta' && !propKey.startsWith('id:')) {
464
+ result[propKey] = get().getShadowValue(key, [...path, propKey]);
514
465
  }
515
-
516
- // 6. Return the completely updated state
517
- return { shadowStateStore: newShadowStore };
518
- });
466
+ }
467
+ return result;
519
468
  },
520
469
 
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);
526
- }
527
-
528
- const shadowMeta = get().shadowStateStore.get(keyToBuild);
529
- if (!shadowMeta) {
530
- return undefined;
531
- }
532
-
533
- if (shadowMeta.value !== undefined) {
534
- return shadowMeta.value;
535
- }
536
-
537
- let result: any; // The value we are about to build.
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
- });
470
+ // ✅ REFACTORED: Correctly preserves `_meta` on updates.
471
+ updateShadowAtPath: (key, path, newValue) => {
472
+ set((state) => {
473
+ const newStore = new Map(state.shadowStateStore);
474
+ const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
475
+ let root = newStore.get(rootKey);
476
+
477
+ if (!root) return state;
478
+
479
+ const clonedRoot: any = { ...root };
480
+ newStore.set(rootKey, clonedRoot);
481
+
482
+ if (path.length === 0) {
483
+ const newRootStructure = buildShadowNode(newValue);
484
+ // Preserve the top-level metadata
485
+ if (clonedRoot._meta) {
486
+ newRootStructure._meta = {
487
+ ...(newRootStructure._meta || {}),
488
+ ...clonedRoot._meta,
489
+ };
490
+ }
491
+ newStore.set(rootKey, newRootStructure);
552
492
  } else {
553
- result = undefined;
554
- }
493
+ let current = clonedRoot;
494
+ const parentPath = path.slice(0, -1);
495
+ for (const segment of parentPath) {
496
+ current[segment] = { ...current[segment] };
497
+ current = current[segment];
498
+ }
555
499
 
556
- // Return the final, fully populated result.
557
- return result;
558
- };
500
+ const lastSegment = path[path.length - 1]!;
501
+ const existingNode = current[lastSegment] || {};
502
+ const newNodeStructure = buildShadowNode(newValue);
559
503
 
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('.');
504
+ // This merge is critical: it preserves existing metadata during an update.
505
+ if (existingNode._meta) {
506
+ newNodeStructure._meta = {
507
+ ...(newNodeStructure._meta || {}),
508
+ ...existingNode._meta,
509
+ };
510
+ }
511
+ current[lastSegment] = newNodeStructure;
512
+ }
565
513
 
566
- return get().shadowStateStore.get(fullKey);
514
+ get().notifyPathSubscribers([key, ...path].join('.'), {
515
+ type: 'UPDATE',
516
+ newValue,
517
+ });
518
+ return { shadowStateStore: newStore };
519
+ });
567
520
  },
568
521
 
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
- );
522
+ // REFACTORED: Works with `_meta.arrayKeys`.
523
+ insertShadowArrayElement: (key, arrayPath, newItem, index) => {
524
+ const arrayNode = get().getShadowNode(key, arrayPath);
525
+ if (!arrayNode?._meta?.arrayKeys) {
581
526
  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
587
- );
588
- console.log(
589
- 'The NEW metadata is trying to save WITHOUT a components map:',
590
- metadata
527
+ `Array not found at path: ${[key, ...arrayPath].join('.')}`
591
528
  );
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();
529
+ return;
598
530
  }
599
- // --- END OF TRAP ---
600
531
 
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) || {};
532
+ const newItemId = `id:${ulid()}`;
533
+ const newItemNode = buildShadowNode(newItem);
534
+
535
+ // Update the `arrayKeys` in the metadata
536
+ const currentKeys = arrayNode._meta.arrayKeys;
537
+ const newKeys = [...currentKeys];
538
+ if (index !== undefined && index >= 0 && index <= newKeys.length) {
539
+ newKeys.splice(index, 0, newItemId);
540
+ } else {
541
+ newKeys.push(newItemId);
542
+ }
615
543
 
616
- // Initialize transformCaches if it doesn't exist
617
- if (!existing.transformCaches) {
618
- existing.transformCaches = new Map();
544
+ // Update transform caches if they exist
545
+ if (arrayNode._meta.transformCaches) {
546
+ arrayNode._meta.transformCaches.forEach((cache) => {
547
+ if (cache.validIds && Array.isArray(cache.validIds)) {
548
+ const matchesFilters = cache.transforms.every((transform) =>
549
+ transform.type === 'filter' ? transform.fn(newItem) : true
550
+ );
551
+ if (matchesFilters) {
552
+ cache.validIds = [...cache.validIds];
553
+ if (index !== undefined) {
554
+ cache.validIds.splice(index, 0, newItemId);
555
+ } else {
556
+ cache.validIds.push(newItemId);
557
+ }
558
+ }
559
+ }
560
+ });
619
561
  }
620
562
 
621
- // Update just the specific cache entry
622
- existing.transformCaches.set(cacheKey, cacheData);
563
+ // Directly set the new item and updated metadata on the node before setting state
564
+ arrayNode[newItemId] = newItemNode;
565
+ arrayNode._meta.arrayKeys = newKeys;
623
566
 
624
- // Update shadow store WITHOUT notifying path subscribers
625
- newShadowStore.set(fullKey, existing);
626
- set({ shadowStateStore: newShadowStore });
567
+ get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
627
568
 
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);
569
+ // Trigger notifications
636
570
  const arrayKey = [key, ...arrayPath].join('.');
637
- const parentMeta = newShadowStore.get(arrayKey);
638
-
639
- if (!parentMeta || !parentMeta.arrayKeys) return;
640
-
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
- };
671
-
672
- processNewItem(newItem, [...arrayPath, newItemId]);
673
- set({ shadowStateStore: newShadowStore });
674
-
675
571
  get().notifyPathSubscribers(arrayKey, {
676
572
  type: 'INSERT',
677
573
  path: arrayKey,
678
- itemKey: fullItemKey,
574
+ itemKey: `${arrayKey}.${newItemId}`,
575
+ index: index ?? newKeys.length - 1,
679
576
  });
680
577
  },
681
- removeShadowArrayElement: (key: string, itemPath: string[]) => {
682
- const newShadowStore = new Map(get().shadowStateStore);
683
578
 
684
- // Get the full item key (e.g., "stateKey.products.id:xxx")
685
- const itemKey = [key, ...itemPath].join('.');
579
+ // REFACTORED: Works with `_meta.arrayKeys`.
580
+ removeShadowArrayElement: (key, itemPath) => {
581
+ if (itemPath.length === 0) return;
686
582
 
687
- // Extract parent path and item ID
688
- const parentPath = itemPath.slice(0, -1);
689
- const parentKey = [key, ...parentPath].join('.');
583
+ const arrayPath = itemPath.slice(0, -1);
584
+ const itemId = itemPath[itemPath.length - 1];
585
+ if (!itemId?.startsWith('id:')) return;
690
586
 
691
- // Get parent metadata
692
- const parentMeta = newShadowStore.get(parentKey);
587
+ const arrayNode = get().getShadowNode(key, arrayPath);
588
+ if (!arrayNode?._meta?.arrayKeys) return;
693
589
 
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
- );
590
+ // Filter the item's ID from the `arrayKeys` metadata
591
+ const newKeys = arrayNode._meta.arrayKeys.filter((k) => k !== itemId);
699
592
 
700
- if (indexToRemove !== -1) {
701
- // Create new array keys with the item removed
702
- const newArrayKeys = parentMeta.arrayKeys.filter(
703
- (arrayItemKey) => arrayItemKey !== itemKey
704
- );
705
-
706
- // Update parent with new array keys
707
- newShadowStore.set(parentKey, {
708
- ...parentMeta,
709
- arrayKeys: newArrayKeys,
710
- });
711
-
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
- }
718
- }
719
- }
720
- }
593
+ // Delete the item's data from the node
594
+ delete arrayNode[itemId];
721
595
 
722
- set({ shadowStateStore: newShadowStore });
596
+ // Persist the modified array node back to the store
597
+ get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
723
598
 
724
- get().notifyPathSubscribers(parentKey, {
599
+ const arrayKey = [key, ...arrayPath].join('.');
600
+ get().notifyPathSubscribers(arrayKey, {
725
601
  type: 'REMOVE',
726
- path: parentKey,
727
- itemKey: itemKey, // The exact ID of the removed item
602
+ path: arrayKey,
603
+ itemKey: `${arrayKey}.${itemId}`,
728
604
  });
729
605
  },
730
606
 
731
- updateShadowAtPath: (key, path, newValue) => {
732
- const fullKey = [key, ...path].join('.');
607
+ // The rest of the functions are updated to use the new helpers (`getShadowMetadata`, `setShadowMetadata`)
608
+ // which abstracts away the `_meta` implementation detail.
733
609
 
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
610
+ addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
611
+ const metadata = get().getShadowMetadata(stateKey, dependencyPath) || {};
612
+ const newPathComponents = new Set(metadata.pathComponents);
613
+ newPathComponents.add(fullComponentId);
614
+ get().setShadowMetadata(stateKey, dependencyPath, {
615
+ pathComponents: newPathComponents,
616
+ });
617
+
618
+ const rootMeta = get().getShadowMetadata(stateKey, []);
619
+ if (rootMeta?.components) {
620
+ const component = rootMeta.components.get(fullComponentId);
621
+ if (component) {
622
+ const fullPathKey = [stateKey, ...dependencyPath].join('.');
623
+ const newPaths = new Set(component.paths);
624
+ newPaths.add(fullPathKey);
625
+ const newComponentRegistration = { ...component, paths: newPaths };
626
+ const newComponentsMap = new Map(rootMeta.components);
627
+ newComponentsMap.set(fullComponentId, newComponentRegistration);
628
+ get().setShadowMetadata(stateKey, [], { components: newComponentsMap });
629
+ }
738
630
  }
631
+ },
739
632
 
740
- // CHANGE: Don't clone the entire Map, just update in place
741
- set((state) => {
742
- const store = state.shadowStateStore;
633
+ registerComponent: (stateKey, fullComponentId, registration) => {
634
+ const rootMeta = get().getShadowMetadata(stateKey, []) || {};
635
+ const components = new Map(rootMeta.components);
636
+ components.set(fullComponentId, registration);
637
+ get().setShadowMetadata(stateKey, [], { components });
638
+ },
743
639
 
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
- };
640
+ unregisterComponent: (stateKey, fullComponentId) => {
641
+ const rootMeta = get().getShadowMetadata(stateKey, []);
642
+ if (!rootMeta?.components) return;
643
+ const components = new Map(rootMeta.components);
644
+ if (components.delete(fullComponentId)) {
645
+ get().setShadowMetadata(stateKey, [], { components });
646
+ }
647
+ },
777
648
 
778
- processObject(path, newValue);
649
+ // ✅ REFACTORED: `markAsDirty` now correctly writes to `_meta.isDirty`.
650
+ markAsDirty: (key, path, options = { bubble: true }) => {
651
+ const setDirtyOnPath = (pathToMark: string[]) => {
652
+ const node = get().getShadowNode(key, pathToMark);
653
+ if (node?._meta?.isDirty) {
654
+ return true; // Already dirty, stop bubbling
779
655
  }
656
+ get().setShadowMetadata(key, pathToMark, { isDirty: true });
657
+ return false; // Was not dirty before
658
+ };
780
659
 
781
- // Only notify after all changes are made
782
- get().notifyPathSubscribers(fullKey, { type: 'UPDATE', newValue });
660
+ setDirtyOnPath(path);
783
661
 
784
- // Return same reference if using Zustand's immer middleware
785
- return state;
662
+ if (options.bubble) {
663
+ let parentPath = [...path];
664
+ while (parentPath.length > 0) {
665
+ parentPath.pop();
666
+ if (setDirtyOnPath(parentPath)) {
667
+ break; // Stop if parent was already dirty
668
+ }
669
+ }
670
+ }
671
+ },
672
+
673
+ serverStateUpdates: new Map(),
674
+ setServerStateUpdate: (key, serverState) => {
675
+ set((state) => ({
676
+ serverStateUpdates: new Map(state.serverStateUpdates).set(
677
+ key,
678
+ serverState
679
+ ),
680
+ }));
681
+ get().notifyPathSubscribers(key, {
682
+ type: 'SERVER_STATE_UPDATE',
683
+ serverState,
786
684
  });
787
685
  },
686
+
687
+ pathSubscribers: new Map<string, Set<(newValue: any) => void>>(),
688
+ subscribeToPath: (path, callback) => {
689
+ const subscribers = get().pathSubscribers;
690
+ const subsForPath = subscribers.get(path) || new Set();
691
+ subsForPath.add(callback);
692
+ subscribers.set(path, subsForPath);
693
+
694
+ return () => {
695
+ const currentSubs = get().pathSubscribers.get(path);
696
+ if (currentSubs) {
697
+ currentSubs.delete(callback);
698
+ if (currentSubs.size === 0) {
699
+ get().pathSubscribers.delete(path);
700
+ }
701
+ }
702
+ };
703
+ },
704
+ notifyPathSubscribers: (updatedPath, newValue) => {
705
+ const subscribers = get().pathSubscribers;
706
+ const subs = subscribers.get(updatedPath);
707
+ if (subs) {
708
+ subs.forEach((callback) => callback(newValue));
709
+ }
710
+ },
711
+
788
712
  selectedIndicesMap: new Map<string, string>(),
789
- getSelectedIndex: (arrayKey: string, validIds?: string[]): number => {
713
+ getSelectedIndex: (arrayKey, validIds) => {
790
714
  const itemKey = get().selectedIndicesMap.get(arrayKey);
791
-
792
715
  if (!itemKey) return -1;
793
716
 
794
- // Use validIds if provided (for filtered views), otherwise use all arrayKeys
795
- const arrayKeys =
796
- validIds ||
797
- getGlobalStore.getState().getShadowMetadata(arrayKey, [])?.arrayKeys;
798
-
799
- if (!arrayKeys) return -1;
717
+ const arrayMeta = get().getShadowMetadata(
718
+ arrayKey.split('.')[0]!,
719
+ arrayKey.split('.').slice(1)
720
+ );
721
+ const arrayKeys = validIds || arrayMeta?.arrayKeys;
800
722
 
801
- return arrayKeys.indexOf(itemKey);
723
+ return arrayKeys ? arrayKeys.indexOf(itemKey) : -1;
802
724
  },
803
725
 
804
- setSelectedIndex: (arrayKey: string, itemKey: string | undefined) => {
726
+ setSelectedIndex: (arrayKey, itemKey) => {
805
727
  set((state) => {
806
- const newMap = state.selectedIndicesMap;
728
+ const newMap = new Map(state.selectedIndicesMap);
729
+ const oldSelection = newMap.get(arrayKey);
730
+ if (oldSelection) {
731
+ get().notifyPathSubscribers(oldSelection, { type: 'THIS_UNSELECTED' });
732
+ }
807
733
 
808
734
  if (itemKey === undefined) {
809
735
  newMap.delete(arrayKey);
810
736
  } else {
811
- if (newMap.has(arrayKey)) {
812
- get().notifyPathSubscribers(newMap.get(arrayKey)!, {
813
- type: 'THIS_UNSELECTED',
814
- });
815
- }
816
737
  newMap.set(arrayKey, itemKey);
817
-
818
- get().notifyPathSubscribers(itemKey, {
819
- type: 'THIS_SELECTED',
820
- });
738
+ get().notifyPathSubscribers(itemKey, { type: 'THIS_SELECTED' });
821
739
  }
822
- get().notifyPathSubscribers(arrayKey, {
823
- type: 'GET_SELECTED',
824
- });
825
- return {
826
- ...state,
827
- selectedIndicesMap: newMap,
828
- };
740
+
741
+ get().notifyPathSubscribers(arrayKey, { type: 'GET_SELECTED' });
742
+ return { selectedIndicesMap: newMap };
829
743
  });
830
744
  },
831
- clearSelectedIndex: ({ arrayKey }: { arrayKey: string }): void => {
745
+
746
+ clearSelectedIndex: ({ arrayKey }) => {
832
747
  set((state) => {
833
- const newMap = state.selectedIndicesMap;
834
- const acutalKey = newMap.get(arrayKey);
835
- if (acutalKey) {
836
- get().notifyPathSubscribers(acutalKey, {
837
- type: 'CLEAR_SELECTION',
838
- });
748
+ const newMap = new Map(state.selectedIndicesMap);
749
+ const actualKey = newMap.get(arrayKey);
750
+ if (actualKey) {
751
+ get().notifyPathSubscribers(actualKey, { type: 'CLEAR_SELECTION' });
839
752
  }
840
-
841
753
  newMap.delete(arrayKey);
842
- get().notifyPathSubscribers(arrayKey, {
843
- type: 'CLEAR_SELECTION',
844
- });
845
- return {
846
- ...state,
847
- selectedIndicesMap: newMap,
848
- };
754
+ get().notifyPathSubscribers(arrayKey, { type: 'CLEAR_SELECTION' });
755
+ return { selectedIndicesMap: newMap };
849
756
  });
850
757
  },
851
- clearSelectedIndexesForState: (stateKey: string) => {
758
+
759
+ clearSelectedIndexesForState: (stateKey) => {
852
760
  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 {};
761
+ const newMap = new Map(state.selectedIndicesMap);
762
+ let changed = false;
763
+ for (const key of newMap.keys()) {
764
+ if (key === stateKey || key.startsWith(stateKey + '.')) {
765
+ newMap.delete(key);
766
+ changed = true;
767
+ }
859
768
  }
769
+ return changed ? { selectedIndicesMap: newMap } : {};
860
770
  });
861
771
  },
862
772
 
863
773
  initialStateOptions: {},
864
-
865
- stateTimeline: {},
866
- cogsStateStore: {},
867
774
  stateLog: new Map(),
868
-
869
775
  initialStateGlobal: {},
870
776
 
871
- validationErrors: new Map(),
872
- addStateLog: (key, update) => {
777
+ addStateLog: (updates) => {
778
+ if (!updates || updates.length === 0) return;
873
779
  set((state) => {
874
780
  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 });
781
+ const logsGroupedByKey = new Map<string, UpdateTypeDetail[]>();
782
+ for (const update of updates) {
783
+ const group = logsGroupedByKey.get(update.stateKey) || [];
784
+ group.push(update);
785
+ logsGroupedByKey.set(update.stateKey, group);
786
+ }
787
+ for (const [key, batchOfUpdates] of logsGroupedByKey.entries()) {
788
+ const newStateLogForKey = new Map(newLog.get(key));
789
+ for (const update of batchOfUpdates) {
790
+ newStateLogForKey.set(JSON.stringify(update.path), { ...update });
791
+ }
792
+ newLog.set(key, newStateLogForKey);
886
793
  }
887
-
888
- newLog.set(key, stateLogForKey);
889
794
  return { stateLog: newLog };
890
795
  });
891
796
  },
892
797
 
893
- getInitialOptions: (key) => {
894
- return get().initialStateOptions[key];
895
- },
896
-
798
+ getInitialOptions: (key) => get().initialStateOptions[key],
897
799
  setInitialStateOptions: (key, value) => {
898
800
  set((prev) => ({
899
- initialStateOptions: {
900
- ...prev.initialStateOptions,
901
- [key]: value,
902
- },
801
+ initialStateOptions: { ...prev.initialStateOptions, [key]: value },
903
802
  }));
904
803
  },
905
804
  updateInitialStateGlobal: (key, newState) => {
906
805
  set((prev) => ({
907
- initialStateGlobal: {
908
- ...prev.initialStateGlobal,
909
- [key]: newState,
910
- },
806
+ initialStateGlobal: { ...prev.initialStateGlobal, [key]: newState },
911
807
  }));
912
808
  },
913
809
 
914
810
  syncInfoStore: new Map<string, SyncInfo>(),
915
- setSyncInfo: (key: string, syncInfo: SyncInfo) =>
811
+ setSyncInfo: (key, syncInfo) =>
916
812
  set((state) => {
917
813
  const newMap = new Map(state.syncInfoStore);
918
814
  newMap.set(key, syncInfo);
919
- return { ...state, syncInfoStore: newMap };
815
+ return { syncInfoStore: newMap };
920
816
  }),
921
- getSyncInfo: (key: string) => get().syncInfoStore.get(key) || null,
817
+ getSyncInfo: (key) => get().syncInfoStore.get(key) || null,
922
818
  }));