cogsbox-state 0.5.462 → 0.5.464

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
@@ -109,7 +109,6 @@ export type ShadowMetadata = {
109
109
  lastUpdated?: number;
110
110
  value?: any;
111
111
  classSignals?: Array<{
112
- // <-- ADD THIS BLOCK
113
112
  id: string;
114
113
  effect: string;
115
114
  lastClasses: string;
@@ -174,8 +173,8 @@ export type CogsEvent =
174
173
  | { type: 'INSERT'; path: string; itemKey: string; index: number }
175
174
  | { type: 'REMOVE'; path: string; itemKey: string }
176
175
  | { type: 'UPDATE'; path: string; newValue: any }
177
- | { type: 'ITEMHEIGHT'; itemKey: string; height: number } // For full re-initializations (e.g., when a component is removed)
178
- | { type: 'RELOAD'; path: string }; // For full re-initializations
176
+ | { type: 'ITEMHEIGHT'; itemKey: string; height: number }
177
+ | { type: 'RELOAD'; path: string };
179
178
  export type CogsGlobalState = {
180
179
  updateQueue: Set<() => void>;
181
180
  isFlushScheduled: boolean;
@@ -256,10 +255,6 @@ export type CogsGlobalState = {
256
255
  getInitialOptions: (key: string) => OptionsType | undefined;
257
256
  setInitialStateOptions: (key: string, value: OptionsType) => void;
258
257
 
259
- // --- Validation ---
260
-
261
- // --- Server Sync and Logging ---
262
-
263
258
  serverStateUpdates: Map<
264
259
  string,
265
260
  {
@@ -279,29 +274,17 @@ export type CogsGlobalState = {
279
274
  getSyncInfo: (key: string) => SyncInfo | null;
280
275
  };
281
276
  const isSimpleObject = (value: any): boolean => {
277
+ // Most common cases first
282
278
  if (value === null || typeof value !== 'object') return false;
283
279
 
284
- // Handle special cases that should be treated as primitives
285
- if (
286
- value instanceof Uint8Array ||
287
- value instanceof Int8Array ||
288
- value instanceof Uint16Array ||
289
- value instanceof Int16Array ||
290
- value instanceof Uint32Array ||
291
- value instanceof Int32Array ||
292
- value instanceof Float32Array ||
293
- value instanceof Float64Array ||
294
- value instanceof ArrayBuffer ||
295
- value instanceof Date ||
296
- value instanceof RegExp ||
297
- value instanceof Map ||
298
- value instanceof Set
299
- ) {
300
- return false; // Treat as primitive
301
- }
302
-
303
- // Arrays and plain objects are complex
304
- return Array.isArray(value) || value.constructor === Object;
280
+ // Arrays are simple objects
281
+ if (Array.isArray(value)) return true;
282
+
283
+ // Plain objects second most common
284
+ if (value.constructor === Object) return true;
285
+
286
+ // Everything else is not simple
287
+ return false;
305
288
  };
306
289
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
307
290
  updateQueue: new Set<() => void>(),
@@ -360,20 +343,11 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
360
343
  },
361
344
  registerComponent: (stateKey, fullComponentId, registration) => {
362
345
  set((state) => {
363
- // Create a new Map to ensure Zustand detects the change
364
346
  const newShadowStore = new Map(state.shadowStateStore);
365
-
366
- // Get the metadata for the ROOT of the state (where the components map lives)
367
347
  const rootMeta = newShadowStore.get(stateKey) || {};
368
-
369
- // Also clone the components map to avoid direct mutation
370
348
  const components = new Map(rootMeta.components);
371
349
  components.set(fullComponentId, registration);
372
-
373
- // Update the root metadata with the new components map
374
350
  newShadowStore.set(stateKey, { ...rootMeta, components });
375
-
376
- // Return the updated state
377
351
  return { shadowStateStore: newShadowStore };
378
352
  });
379
353
  },
@@ -382,8 +356,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
382
356
  set((state) => {
383
357
  const newShadowStore = new Map(state.shadowStateStore);
384
358
  const rootMeta = newShadowStore.get(stateKey);
385
-
386
- // If there's no metadata or no components map, do nothing
387
359
  if (!rootMeta?.components) {
388
360
  return state; // Return original state, no change needed
389
361
  }
@@ -401,40 +373,45 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
401
373
  });
402
374
  },
403
375
  markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
404
- const newShadowStore = new Map(get().shadowStateStore);
405
- let changed = false;
376
+ const { shadowStateStore } = get();
377
+ const updates = new Map<string, ShadowMetadata>();
406
378
 
407
379
  const setDirty = (currentPath: string[]) => {
408
380
  const fullKey = [key, ...currentPath].join('.');
409
- const meta = newShadowStore.get(fullKey);
410
-
411
- // We mark something as dirty if it isn't already.
412
- // The original data source doesn't matter.
413
- if (meta && meta.isDirty !== true) {
414
- newShadowStore.set(fullKey, { ...meta, isDirty: true });
415
- changed = true;
416
- } else if (!meta) {
417
- // If there's no metadata, create it and mark it as dirty.
418
- // This handles newly created fields within an object.
419
- newShadowStore.set(fullKey, { isDirty: true });
420
- changed = true;
381
+ const meta = shadowStateStore.get(fullKey) || {};
382
+
383
+ // If already dirty, no need to update
384
+ if (meta.isDirty === true) {
385
+ return true; // Return true to indicate parent is dirty
421
386
  }
387
+
388
+ updates.set(fullKey, { ...meta, isDirty: true });
389
+ return false; // Not previously dirty
422
390
  };
423
391
 
424
- // 1. Mark the target path itself as dirty.
392
+ // Mark the target path
425
393
  setDirty(path);
426
394
 
427
- // 2. If `bubble` is true, walk up the path and mark all parents as dirty.
395
+ // Bubble up if requested
428
396
  if (options.bubble) {
429
397
  let parentPath = [...path];
430
398
  while (parentPath.length > 0) {
431
399
  parentPath.pop();
432
- setDirty(parentPath);
400
+ const wasDirty = setDirty(parentPath);
401
+ if (wasDirty) {
402
+ break; // Stop bubbling if parent was already dirty
403
+ }
433
404
  }
434
405
  }
435
406
 
436
- if (changed) {
437
- set({ shadowStateStore: newShadowStore });
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
415
  }
439
416
  },
440
417
  serverStateUpdates: new Map(),
@@ -485,7 +462,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
485
462
  set((state) => {
486
463
  // 1. Make a copy of the current store to modify it
487
464
  const newShadowStore = new Map(state.shadowStateStore);
488
-
465
+ console.log('initializeShadowState');
489
466
  // 2. PRESERVE the existing components map before doing anything else
490
467
  const existingRootMeta = newShadowStore.get(key);
491
468
  const preservedComponents = existingRootMeta?.components;
@@ -542,19 +519,13 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
542
519
  },
543
520
 
544
521
  getShadowValue: (fullKey: string, validArrayIds?: string[]) => {
545
- // The cache is created here. It's temporary and exists only for this one top-level call.
546
522
  const memo = new Map<string, any>();
547
-
548
- // This is the inner recursive function that does the real work.
549
523
  const reconstruct = (keyToBuild: string, ids?: string[]): any => {
550
- // --- STEP 1: Check the cache first ---
551
- // If we have already built the object for this path *during this call*, return it instantly.
552
524
  if (memo.has(keyToBuild)) {
553
525
  return memo.get(keyToBuild);
554
526
  }
555
527
 
556
528
  const shadowMeta = get().shadowStateStore.get(keyToBuild);
557
-
558
529
  if (!shadowMeta) {
559
530
  return undefined;
560
531
  }
@@ -567,19 +538,14 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
567
538
 
568
539
  if (shadowMeta.arrayKeys) {
569
540
  const keys = ids ?? shadowMeta.arrayKeys;
570
- // --- IMPORTANT: Set the cache BEFORE the recursive calls ---
571
- // This handles circular references gracefully. We put an empty array in the cache now.
572
541
  result = [];
573
542
  memo.set(keyToBuild, result);
574
- // Now, fill the array with the recursively constructed items.
575
543
  keys.forEach((itemKey) => {
576
- result.push(reconstruct(itemKey)); // Pass the memo cache along implicitly via closure
544
+ result.push(reconstruct(itemKey));
577
545
  });
578
546
  } else if (shadowMeta.fields) {
579
- // --- IMPORTANT: Set the cache BEFORE the recursive calls ---
580
547
  result = {};
581
548
  memo.set(keyToBuild, result);
582
- // Now, fill the object with the recursively constructed items.
583
549
  Object.entries(shadowMeta.fields).forEach(([key, fieldPath]) => {
584
550
  result[key] = reconstruct(fieldPath as string);
585
551
  });
@@ -594,13 +560,8 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
594
560
  // Start the process by calling the inner function on the root key.
595
561
  return reconstruct(fullKey, validArrayIds);
596
562
  },
597
- getShadowMetadata: (
598
- key: string,
599
- path: string[],
600
- validArrayIds?: string[]
601
- ) => {
563
+ getShadowMetadata: (key: string, path: string[]) => {
602
564
  const fullKey = [key, ...path].join('.');
603
- let data = get().shadowStateStore.get(fullKey);
604
565
 
605
566
  return get().shadowStateStore.get(fullKey);
606
567
  },
@@ -766,36 +727,63 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
766
727
  itemKey: itemKey, // The exact ID of the removed item
767
728
  });
768
729
  },
730
+
769
731
  updateShadowAtPath: (key, path, newValue) => {
770
- const newShadowStore = new Map(get().shadowStateStore);
771
732
  const fullKey = [key, ...path].join('.');
772
733
 
773
- const updateValue = (currentKey: string, valueToSet: any) => {
774
- const meta = newShadowStore.get(currentKey);
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
+ }
775
739
 
776
- // If it's a simple object with fields, update recursively
777
- if (isSimpleObject(valueToSet) && meta && meta.fields) {
778
- for (const fieldKey in valueToSet) {
779
- if (Object.prototype.hasOwnProperty.call(valueToSet, fieldKey)) {
780
- const childPath = meta.fields[fieldKey];
781
- const childValue = valueToSet[fieldKey];
740
+ // CHANGE: Don't clone the entire Map, just update in place
741
+ set((state) => {
742
+ const store = state.shadowStateStore;
782
743
 
783
- if (childPath) {
784
- updateValue(childPath as string, childValue);
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
+ }
785
774
  }
786
775
  }
787
- }
788
- } else {
789
- // For primitives (including Uint8Array), just replace the value
790
- // This gives you useState-like behavior
791
- const existing = newShadowStore.get(currentKey) || {};
792
- newShadowStore.set(currentKey, { ...existing, value: valueToSet });
776
+ };
777
+
778
+ processObject(path, newValue);
793
779
  }
794
- };
795
780
 
796
- updateValue(fullKey, newValue);
797
- get().notifyPathSubscribers(fullKey, { type: 'UPDATE', newValue });
798
- set({ shadowStateStore: newShadowStore });
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
+ });
799
787
  },
800
788
  selectedIndicesMap: new Map<string, string>(),
801
789
  getSelectedIndex: (arrayKey: string, validIds?: string[]): number => {