cogsbox-state 0.5.465 → 0.5.467

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
@@ -7,7 +7,7 @@ import type {
7
7
  UpdateTypeDetail,
8
8
  } from './CogsState.js';
9
9
 
10
- import { startTransition, type ReactNode } from 'react';
10
+ import { type ReactNode } from 'react';
11
11
 
12
12
  export type FreshValuesObject = {
13
13
  pathsToValues?: string[];
@@ -28,7 +28,6 @@ export type FormRefStoreState = {
28
28
  registerFormRef: (id: string, ref: React.RefObject<any>) => void;
29
29
  getFormRef: (id: string) => React.RefObject<any> | undefined;
30
30
  removeFormRef: (id: string) => void;
31
- // New method to get all refs for a stateKey
32
31
  getFormRefsByStateKey: (
33
32
  stateKey: string
34
33
  ) => Map<string, React.RefObject<any>>;
@@ -53,7 +52,6 @@ export const formRefStore = create<FormRefStoreState>((set, get) => ({
53
52
  return { formRefs: newRefs };
54
53
  }),
55
54
 
56
- // Get all refs that start with the stateKey prefix
57
55
  getFormRefsByStateKey: (stateKey) => {
58
56
  const allRefs = get().formRefs;
59
57
  const stateKeyPrefix = stateKey + '.';
@@ -83,26 +81,25 @@ export type ComponentsType = {
83
81
  };
84
82
 
85
83
  export type ValidationStatus =
86
- | 'NOT_VALIDATED' // Never run
87
- | 'VALIDATING' // Currently running
88
- | 'VALID' // Passed
89
- | 'INVALID'; // Failed
84
+ | 'NOT_VALIDATED'
85
+ | 'VALIDATING'
86
+ | 'VALID'
87
+ | 'INVALID';
90
88
 
91
89
  export type ValidationError = {
92
90
  source: 'client' | 'sync_engine' | 'api';
93
91
  message: string;
94
- severity: 'warning' | 'error'; // warning = gentle, error = blocking
95
- code?: string; // Optional error code
92
+ severity: 'warning' | 'error';
93
+ code?: string;
96
94
  };
97
95
 
98
96
  export type ValidationState = {
99
97
  status: ValidationStatus;
100
98
  errors: ValidationError[];
101
99
  lastValidated?: number;
102
- validatedValue?: any; // Value when last validated
100
+ validatedValue?: any;
103
101
  };
104
102
 
105
- // This is the new definition for the metadata object
106
103
  export type ShadowMetadata = {
107
104
  id?: string;
108
105
  stateSource?: 'default' | 'server' | 'localStorage';
@@ -156,28 +153,21 @@ export type ShadowMetadata = {
156
153
  >;
157
154
  } & ComponentsType;
158
155
 
159
- // The shadow node itself can have a value and the metadata object.
160
156
  type ShadowNode = {
161
157
  value?: any;
162
158
  _meta?: ShadowMetadata;
163
- [key: string]: any; // For nested data properties
159
+ [key: string]: any;
164
160
  };
165
161
 
166
162
  export type CogsGlobalState = {
167
- // NEW shadow store
168
- shadowStateStore: Map<string, ShadowNode>; // Changed ShadowMetadata to ShadowNode
169
163
  setTransformCache: (
170
164
  key: string,
171
165
  path: string[],
172
166
  cacheKey: string,
173
167
  cacheData: any
174
168
  ) => void;
175
- // NEW functions
176
169
  initializeShadowState: (key: string, initialState: any) => void;
177
-
178
- // REFACTORED: getShadowNode gets the whole object (data + _meta)
179
170
  getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
180
- // REFACTORED: getShadowMetadata now returns just the _meta field
181
171
  getShadowMetadata: (
182
172
  key: string,
183
173
  path: string[]
@@ -191,6 +181,18 @@ export type CogsGlobalState = {
191
181
  log?: boolean
192
182
  ) => any;
193
183
  updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
184
+ insertManyShadowArrayElements: (
185
+ key: string,
186
+ arrayPath: string[],
187
+ newItems: any[],
188
+ index?: number
189
+ ) => void;
190
+ addItemsToArrayNode: (
191
+ key: string,
192
+ arrayPath: string[],
193
+ newItems: any,
194
+ newKeys: string[]
195
+ ) => void;
194
196
  insertShadowArrayElement: (
195
197
  key: string,
196
198
  arrayPath: string[],
@@ -209,13 +211,11 @@ export type CogsGlobalState = {
209
211
  dependencyPath: string[],
210
212
  fullComponentId: string
211
213
  ) => void;
212
-
213
214
  markAsDirty: (
214
215
  key: string,
215
216
  path: string[],
216
217
  options: { bubble: boolean }
217
218
  ) => void;
218
- // These method signatures stay the same
219
219
 
220
220
  pathSubscribers: Map<string, Set<(newValue: any) => void>>;
221
221
  subscribeToPath: (
@@ -230,12 +230,8 @@ export type CogsGlobalState = {
230
230
  clearSelectedIndex: ({ arrayKey }: { arrayKey: string }) => void;
231
231
  clearSelectedIndexesForState: (stateKey: string) => void;
232
232
 
233
- // --- Core State and Updaters ---
234
-
235
233
  initialStateOptions: { [key: string]: OptionsType };
236
-
237
234
  initialStateGlobal: { [key: string]: StateValue };
238
-
239
235
  updateInitialStateGlobal: (key: string, newState: StateValue) => void;
240
236
 
241
237
  getInitialOptions: (key: string) => OptionsType | undefined;
@@ -260,51 +256,45 @@ export type CogsGlobalState = {
260
256
  getSyncInfo: (key: string) => SyncInfo | null;
261
257
  };
262
258
 
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
259
  export function buildShadowNode(value: any): ShadowNode {
273
- // Primitives and null are wrapped.
274
260
  if (value === null || typeof value !== 'object') {
275
261
  return { value };
276
262
  }
277
263
 
278
- // Arrays are converted to an object with id-keyed children and metadata in `_meta`.
279
264
  if (Array.isArray(value)) {
280
- const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } }; // Initialize with _meta and arrayKeys
265
+ const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } };
281
266
  const idKeys: string[] = [];
267
+
282
268
  value.forEach((item) => {
283
269
  const itemId = `id:${ulid()}`;
284
- arrayNode[itemId] = buildShadowNode(item); // Recurse for each item
270
+ arrayNode[itemId] = buildShadowNode(item);
285
271
  idKeys.push(itemId);
286
272
  });
287
- arrayNode._meta!.arrayKeys = idKeys; // Set the final ordered keys
273
+
274
+ arrayNode._meta!.arrayKeys = idKeys;
288
275
  return arrayNode;
289
276
  }
290
277
 
291
- // Plain objects are recursively processed.
292
278
  if (value.constructor === Object) {
293
- const objectNode: ShadowNode = { _meta: {} }; // Initialize with an empty meta object
279
+ const objectNode: ShadowNode = { _meta: {} };
294
280
  for (const key in value) {
295
281
  if (Object.prototype.hasOwnProperty.call(value, key)) {
296
- objectNode[key] = buildShadowNode(value[key]); // Recurse for each property
282
+ objectNode[key] = buildShadowNode(value[key]);
297
283
  }
298
284
  }
299
285
  return objectNode;
300
286
  }
301
287
 
302
- // Fallback for other object types (Date, etc.) - treat them as primitives.
303
288
  return { value };
304
289
  }
290
+ // store.ts - Replace the shadow store methods with mutable versions
291
+ // store.ts - Replace the shadow store methods with mutable versions
292
+
293
+ // Module-level mutable store
294
+ const shadowStateStore = new Map<string, ShadowNode>();
305
295
 
306
296
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
307
- shadowStateStore: new Map<string, ShadowNode>(),
297
+ // Remove shadowStateStore from Zustand state
308
298
 
309
299
  setTransformCache: (
310
300
  key: string,
@@ -312,7 +302,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
312
302
  cacheKey: string,
313
303
  cacheData: any
314
304
  ) => {
315
- // This function now uses setShadowMetadata which correctly places the data.
316
305
  const metadata = get().getShadowMetadata(key, path) || {};
317
306
  if (!metadata.transformCaches) {
318
307
  metadata.transformCaches = new Map();
@@ -324,48 +313,39 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
324
313
  },
325
314
 
326
315
  initializeShadowState: (key: string, initialState: any) => {
327
- set((state) => {
328
- const newShadowStore = new Map(state.shadowStateStore);
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
- }
348
-
349
- newShadowStore.delete(key);
350
- newShadowStore.delete(`[${key}`);
316
+ const existingRoot =
317
+ shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
318
+ let preservedMetadata: Partial<ShadowMetadata> = {};
319
+
320
+ if (existingRoot?._meta) {
321
+ const {
322
+ components,
323
+ features,
324
+ lastServerSync,
325
+ stateSource,
326
+ baseServerState,
327
+ } = existingRoot._meta;
328
+ if (components) preservedMetadata.components = components;
329
+ if (features) preservedMetadata.features = features;
330
+ if (lastServerSync) preservedMetadata.lastServerSync = lastServerSync;
331
+ if (stateSource) preservedMetadata.stateSource = stateSource;
332
+ if (baseServerState) preservedMetadata.baseServerState = baseServerState;
333
+ }
351
334
 
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);
335
+ shadowStateStore.delete(key);
336
+ shadowStateStore.delete(`[${key}`);
356
337
 
357
- const storageKey = Array.isArray(initialState) ? `[${key}` : key;
358
- newShadowStore.set(storageKey, newRoot);
338
+ const newRoot = buildShadowNode(initialState);
339
+ if (!newRoot._meta) newRoot._meta = {};
340
+ Object.assign(newRoot._meta, preservedMetadata);
359
341
 
360
- return { shadowStateStore: newShadowStore };
361
- });
342
+ const storageKey = Array.isArray(initialState) ? `[${key}` : key;
343
+ shadowStateStore.set(storageKey, newRoot);
362
344
  },
363
345
 
364
- // ✅ NEW HELPER: Gets the entire node (data and metadata).
365
346
  getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
366
- const store = get().shadowStateStore;
367
- let current: any = store.get(key) || store.get(`[${key}`);
368
-
347
+ let current: any =
348
+ shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
369
349
  if (!current) return undefined;
370
350
  if (path.length === 0) return current;
371
351
 
@@ -377,7 +357,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
377
357
  return current;
378
358
  },
379
359
 
380
- // ✅ REFACTORED: Returns only the `_meta` part of a node.
381
360
  getShadowMetadata: (
382
361
  key: string,
383
362
  path: string[]
@@ -386,38 +365,37 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
386
365
  return node?._meta;
387
366
  },
388
367
 
389
- // ✅ REFACTORED: Sets data within the `_meta` object.
390
368
  setShadowMetadata: (
391
369
  key: string,
392
370
  path: string[],
393
371
  newMetadata: Partial<ShadowMetadata>
394
372
  ) => {
395
- set((state) => {
396
- const newStore = new Map(state.shadowStateStore);
397
- const rootKey = newStore.has(`[${key}`) ? `[${key}` : key;
398
- let root = newStore.get(rootKey);
373
+ // Direct mutation - no cloning!
374
+ const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
375
+ let root = shadowStateStore.get(rootKey);
399
376
 
400
- if (!root) {
401
- root = {};
402
- newStore.set(rootKey, root);
403
- }
404
-
405
- const clonedRoot: any = { ...root };
406
- newStore.set(rootKey, clonedRoot);
377
+ if (!root) {
378
+ root = { _meta: newMetadata };
379
+ shadowStateStore.set(rootKey, root);
380
+ return;
381
+ }
407
382
 
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];
383
+ // Navigate to target without cloning
384
+ let current = root;
385
+ for (const segment of path) {
386
+ if (!current[segment]) {
387
+ current[segment] = {};
413
388
  }
389
+ current = current[segment];
390
+ }
414
391
 
415
- // Ensure _meta object exists and merge the new metadata into it
416
- current._meta = { ...(current._meta || {}), ...newMetadata };
417
-
418
- return { shadowStateStore: newStore };
419
- });
392
+ // Mutate metadata directly
393
+ if (!current._meta) {
394
+ current._meta = {};
395
+ }
396
+ Object.assign(current._meta, newMetadata);
420
397
  },
398
+
421
399
  getShadowValue: (
422
400
  key: string,
423
401
  path: string[],
@@ -430,9 +408,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
430
408
 
431
409
  const nodeKeys = Object.keys(node);
432
410
 
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
411
  const isPrimitiveWrapper =
437
412
  Object.prototype.hasOwnProperty.call(node, 'value') &&
438
413
  nodeKeys.every((k) => k === 'value' || k === '_meta');
@@ -441,7 +416,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
441
416
  return node.value;
442
417
  }
443
418
 
444
- // Array Check (This part is correct)
445
419
  const isArrayNode =
446
420
  node._meta &&
447
421
  Object.prototype.hasOwnProperty.call(node._meta, 'arrayKeys');
@@ -456,10 +430,8 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
456
430
  );
457
431
  }
458
432
 
459
- // Object Reconstruction (This part is also correct)
460
433
  const result: any = {};
461
434
  for (const propKey of nodeKeys) {
462
- // We correctly ignore metadata and array item keys here.
463
435
  if (propKey !== '_meta' && !propKey.startsWith('id:')) {
464
436
  result[propKey] = get().getShadowValue(key, [...path, propKey]);
465
437
  }
@@ -467,59 +439,108 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
467
439
  return result;
468
440
  },
469
441
 
470
- // ✅ REFACTORED: Correctly preserves `_meta` on updates.
471
442
  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
- };
443
+ // NO MORE set() wrapper - direct mutation!
444
+ const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
445
+ let root = shadowStateStore.get(rootKey);
446
+ if (!root) return;
447
+
448
+ // Navigate to parent without cloning
449
+ let parentNode = root;
450
+ for (let i = 0; i < path.length - 1; i++) {
451
+ if (!parentNode[path[i]!]) {
452
+ parentNode[path[i]!] = {};
453
+ }
454
+ parentNode = parentNode[path[i]!];
455
+ }
456
+
457
+ const targetNode =
458
+ path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
459
+
460
+ if (!targetNode) {
461
+ parentNode[path[path.length - 1]!] = buildShadowNode(newValue);
462
+ get().notifyPathSubscribers([key, ...path].join('.'), {
463
+ type: 'UPDATE',
464
+ newValue,
465
+ });
466
+ return;
467
+ }
468
+
469
+ function intelligentMerge(nodeToUpdate: any, plainValue: any) {
470
+ if (
471
+ typeof plainValue !== 'object' ||
472
+ plainValue === null ||
473
+ Array.isArray(plainValue)
474
+ ) {
475
+ const oldMeta = nodeToUpdate._meta;
476
+ // Clear existing properties
477
+ for (const key in nodeToUpdate) {
478
+ if (key !== '_meta') delete nodeToUpdate[key];
490
479
  }
491
- newStore.set(rootKey, newRootStructure);
492
- } else {
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];
480
+ const newNode = buildShadowNode(plainValue);
481
+ Object.assign(nodeToUpdate, newNode);
482
+ if (oldMeta) {
483
+ nodeToUpdate._meta = { ...oldMeta, ...(nodeToUpdate._meta || {}) };
498
484
  }
485
+ return;
486
+ }
499
487
 
500
- const lastSegment = path[path.length - 1]!;
501
- const existingNode = current[lastSegment] || {};
502
- const newNodeStructure = buildShadowNode(newValue);
488
+ const plainValueKeys = new Set(Object.keys(plainValue));
503
489
 
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
- };
490
+ for (const propKey of plainValueKeys) {
491
+ const childValue = plainValue[propKey];
492
+ if (nodeToUpdate[propKey]) {
493
+ intelligentMerge(nodeToUpdate[propKey], childValue);
494
+ } else {
495
+ nodeToUpdate[propKey] = buildShadowNode(childValue);
510
496
  }
511
- current[lastSegment] = newNodeStructure;
512
497
  }
513
498
 
514
- get().notifyPathSubscribers([key, ...path].join('.'), {
515
- type: 'UPDATE',
516
- newValue,
517
- });
518
- return { shadowStateStore: newStore };
499
+ for (const nodeKey in nodeToUpdate) {
500
+ if (
501
+ nodeKey === '_meta' ||
502
+ !Object.prototype.hasOwnProperty.call(nodeToUpdate, nodeKey)
503
+ )
504
+ continue;
505
+
506
+ if (!plainValueKeys.has(nodeKey)) {
507
+ delete nodeToUpdate[nodeKey];
508
+ }
509
+ }
510
+ }
511
+
512
+ intelligentMerge(targetNode, newValue);
513
+
514
+ get().notifyPathSubscribers([key, ...path].join('.'), {
515
+ type: 'UPDATE',
516
+ newValue,
519
517
  });
520
518
  },
521
519
 
522
- // REFACTORED: Works with `_meta.arrayKeys`.
520
+ addItemsToArrayNode: (key, arrayPath, newItems, newKeys) => {
521
+ // Direct mutation - no cloning!
522
+ const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
523
+ let root = shadowStateStore.get(rootKey);
524
+ if (!root) {
525
+ console.error('Root not found for state key:', key);
526
+ return;
527
+ }
528
+
529
+ // Navigate without cloning
530
+ let current = root;
531
+ for (const segment of arrayPath) {
532
+ if (!current[segment]) {
533
+ current[segment] = {};
534
+ }
535
+ current = current[segment];
536
+ }
537
+
538
+ // Mutate directly
539
+ Object.assign(current, newItems);
540
+ if (!current._meta) current._meta = {};
541
+ current._meta.arrayKeys = newKeys; // Direct assignment!
542
+ },
543
+
523
544
  insertShadowArrayElement: (key, arrayPath, newItem, index) => {
524
545
  const arrayNode = get().getShadowNode(key, arrayPath);
525
546
  if (!arrayNode?._meta?.arrayKeys) {
@@ -530,53 +551,79 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
530
551
  }
531
552
 
532
553
  const newItemId = `id:${ulid()}`;
533
- const newItemNode = buildShadowNode(newItem);
554
+ const itemsToAdd = { [newItemId]: buildShadowNode(newItem) };
534
555
 
535
- // Update the `arrayKeys` in the metadata
556
+ // Mutate the array directly
536
557
  const currentKeys = arrayNode._meta.arrayKeys;
537
- const newKeys = [...currentKeys];
538
- if (index !== undefined && index >= 0 && index <= newKeys.length) {
539
- newKeys.splice(index, 0, newItemId);
558
+ const insertionPoint =
559
+ index !== undefined && index >= 0 && index <= currentKeys.length
560
+ ? index
561
+ : currentKeys.length;
562
+
563
+ if (insertionPoint >= currentKeys.length) {
564
+ currentKeys.push(newItemId); // O(1)
540
565
  } else {
541
- newKeys.push(newItemId);
566
+ currentKeys.splice(insertionPoint, 0, newItemId); // O(n) only for middle
542
567
  }
543
568
 
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
- });
569
+ // Pass the mutated array
570
+ get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
571
+
572
+ const arrayKey = [key, ...arrayPath].join('.');
573
+ get().notifyPathSubscribers(arrayKey, {
574
+ type: 'INSERT',
575
+ path: arrayKey,
576
+ itemKey: `${arrayKey}.${newItemId}`,
577
+ index: insertionPoint,
578
+ });
579
+ },
580
+
581
+ insertManyShadowArrayElements: (key, arrayPath, newItems, index) => {
582
+ if (!newItems || newItems.length === 0) {
583
+ return;
561
584
  }
562
585
 
563
- // Directly set the new item and updated metadata on the node before setting state
564
- arrayNode[newItemId] = newItemNode;
565
- arrayNode._meta.arrayKeys = newKeys;
586
+ const arrayNode = get().getShadowNode(key, arrayPath);
587
+ if (!arrayNode?._meta?.arrayKeys) {
588
+ console.error(
589
+ `Array not found at path: ${[key, ...arrayPath].join('.')}`
590
+ );
591
+ return;
592
+ }
566
593
 
567
- get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
594
+ const itemsToAdd: Record<string, any> = {};
595
+ const newIds: string[] = [];
596
+
597
+ newItems.forEach((item) => {
598
+ const newItemId = `id:${ulid()}`;
599
+ newIds.push(newItemId);
600
+ itemsToAdd[newItemId] = buildShadowNode(item);
601
+ });
602
+
603
+ // Mutate directly
604
+ const currentKeys = arrayNode._meta.arrayKeys;
605
+ const insertionPoint =
606
+ index !== undefined && index >= 0 && index <= currentKeys.length
607
+ ? index
608
+ : currentKeys.length;
609
+
610
+ if (insertionPoint >= currentKeys.length) {
611
+ currentKeys.push(...newIds); // O(k) where k is items being added
612
+ } else {
613
+ currentKeys.splice(insertionPoint, 0, ...newIds); // O(n + k)
614
+ }
615
+
616
+ get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
568
617
 
569
- // Trigger notifications
570
618
  const arrayKey = [key, ...arrayPath].join('.');
571
619
  get().notifyPathSubscribers(arrayKey, {
572
- type: 'INSERT',
620
+ type: 'INSERT_MANY',
573
621
  path: arrayKey,
574
- itemKey: `${arrayKey}.${newItemId}`,
575
- index: index ?? newKeys.length - 1,
622
+ count: newItems.length,
623
+ index: insertionPoint,
576
624
  });
577
625
  },
578
626
 
579
- // ✅ REFACTORED: Works with `_meta.arrayKeys`.
580
627
  removeShadowArrayElement: (key, itemPath) => {
581
628
  if (itemPath.length === 0) return;
582
629
 
@@ -587,14 +634,27 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
587
634
  const arrayNode = get().getShadowNode(key, arrayPath);
588
635
  if (!arrayNode?._meta?.arrayKeys) return;
589
636
 
590
- // Filter the item's ID from the `arrayKeys` metadata
591
- const newKeys = arrayNode._meta.arrayKeys.filter((k) => k !== itemId);
637
+ // Mutate directly
638
+ const currentKeys = arrayNode._meta.arrayKeys;
639
+ const indexToRemove = currentKeys.indexOf(itemId);
592
640
 
593
- // Delete the item's data from the node
641
+ if (indexToRemove === -1) return;
642
+
643
+ // O(1) for removing from end
644
+ if (indexToRemove === currentKeys.length - 1) {
645
+ currentKeys.pop();
646
+ }
647
+ // O(n) for removing from beginning or middle
648
+ else if (indexToRemove === 0) {
649
+ currentKeys.shift();
650
+ } else {
651
+ currentKeys.splice(indexToRemove, 1);
652
+ }
653
+
654
+ // Delete the actual item
594
655
  delete arrayNode[itemId];
595
656
 
596
- // Persist the modified array node back to the store
597
- get().setShadowMetadata(key, arrayPath, { arrayKeys: newKeys });
657
+ // No need to update metadata - already mutated!
598
658
 
599
659
  const arrayKey = [key, ...arrayPath].join('.');
600
660
  get().notifyPathSubscribers(arrayKey, {
@@ -604,8 +664,21 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
604
664
  });
605
665
  },
606
666
 
607
- // The rest of the functions are updated to use the new helpers (`getShadowMetadata`, `setShadowMetadata`)
608
- // which abstracts away the `_meta` implementation detail.
667
+ registerComponent: (stateKey, fullComponentId, registration) => {
668
+ const rootMeta = get().getShadowMetadata(stateKey, []) || {};
669
+ const components = new Map(rootMeta.components);
670
+ components.set(fullComponentId, registration);
671
+ get().setShadowMetadata(stateKey, [], { components });
672
+ },
673
+
674
+ unregisterComponent: (stateKey, fullComponentId) => {
675
+ const rootMeta = get().getShadowMetadata(stateKey, []);
676
+ if (!rootMeta?.components) return;
677
+ const components = new Map(rootMeta.components);
678
+ if (components.delete(fullComponentId)) {
679
+ get().setShadowMetadata(stateKey, [], { components });
680
+ }
681
+ },
609
682
 
610
683
  addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
611
684
  const metadata = get().getShadowMetadata(stateKey, dependencyPath) || {};
@@ -630,31 +703,14 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
630
703
  }
631
704
  },
632
705
 
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
- },
639
-
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
- },
648
-
649
- // ✅ REFACTORED: `markAsDirty` now correctly writes to `_meta.isDirty`.
650
706
  markAsDirty: (key, path, options = { bubble: true }) => {
651
707
  const setDirtyOnPath = (pathToMark: string[]) => {
652
708
  const node = get().getShadowNode(key, pathToMark);
653
709
  if (node?._meta?.isDirty) {
654
- return true; // Already dirty, stop bubbling
710
+ return true;
655
711
  }
656
712
  get().setShadowMetadata(key, pathToMark, { isDirty: true });
657
- return false; // Was not dirty before
713
+ return false;
658
714
  };
659
715
 
660
716
  setDirtyOnPath(path);
@@ -664,12 +720,13 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
664
720
  while (parentPath.length > 0) {
665
721
  parentPath.pop();
666
722
  if (setDirtyOnPath(parentPath)) {
667
- break; // Stop if parent was already dirty
723
+ break;
668
724
  }
669
725
  }
670
726
  }
671
727
  },
672
728
 
729
+ // Keep these in Zustand as they need React reactivity
673
730
  serverStateUpdates: new Map(),
674
731
  setServerStateUpdate: (key, serverState) => {
675
732
  set((state) => ({
@@ -701,6 +758,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
701
758
  }
702
759
  };
703
760
  },
761
+
704
762
  notifyPathSubscribers: (updatedPath, newValue) => {
705
763
  const subscribers = get().pathSubscribers;
706
764
  const subs = subscribers.get(updatedPath);
@@ -723,36 +781,49 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
723
781
  return arrayKeys ? arrayKeys.indexOf(itemKey) : -1;
724
782
  },
725
783
 
726
- setSelectedIndex: (arrayKey, itemKey) => {
784
+ setSelectedIndex: (arrayKey: string, itemKey: string | undefined) => {
727
785
  set((state) => {
728
786
  const newMap = new Map(state.selectedIndicesMap);
729
- const oldSelection = newMap.get(arrayKey);
730
- if (oldSelection) {
731
- get().notifyPathSubscribers(oldSelection, { type: 'THIS_UNSELECTED' });
732
- }
733
787
 
734
788
  if (itemKey === undefined) {
735
789
  newMap.delete(arrayKey);
736
790
  } else {
791
+ if (newMap.has(arrayKey)) {
792
+ get().notifyPathSubscribers(newMap.get(arrayKey)!, {
793
+ type: 'THIS_UNSELECTED',
794
+ });
795
+ }
737
796
  newMap.set(arrayKey, itemKey);
738
797
  get().notifyPathSubscribers(itemKey, { type: 'THIS_SELECTED' });
739
798
  }
740
799
 
741
800
  get().notifyPathSubscribers(arrayKey, { type: 'GET_SELECTED' });
742
- return { selectedIndicesMap: newMap };
801
+
802
+ return {
803
+ ...state,
804
+ selectedIndicesMap: newMap,
805
+ };
743
806
  });
744
807
  },
745
808
 
746
- clearSelectedIndex: ({ arrayKey }) => {
809
+ clearSelectedIndex: ({ arrayKey }: { arrayKey: string }): void => {
747
810
  set((state) => {
748
811
  const newMap = new Map(state.selectedIndicesMap);
749
812
  const actualKey = newMap.get(arrayKey);
750
813
  if (actualKey) {
751
- get().notifyPathSubscribers(actualKey, { type: 'CLEAR_SELECTION' });
814
+ get().notifyPathSubscribers(actualKey, {
815
+ type: 'CLEAR_SELECTION',
816
+ });
752
817
  }
818
+
753
819
  newMap.delete(arrayKey);
754
- get().notifyPathSubscribers(arrayKey, { type: 'CLEAR_SELECTION' });
755
- return { selectedIndicesMap: newMap };
820
+ get().notifyPathSubscribers(arrayKey, {
821
+ type: 'CLEAR_SELECTION',
822
+ });
823
+ return {
824
+ ...state,
825
+ selectedIndicesMap: newMap,
826
+ };
756
827
  });
757
828
  },
758
829
 
@@ -801,6 +872,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
801
872
  initialStateOptions: { ...prev.initialStateOptions, [key]: value },
802
873
  }));
803
874
  },
875
+
804
876
  updateInitialStateGlobal: (key, newState) => {
805
877
  set((prev) => ({
806
878
  initialStateGlobal: { ...prev.initialStateGlobal, [key]: newState },