cogsbox-state 0.5.461 → 0.5.463

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
@@ -8,7 +8,7 @@ import type {
8
8
  UpdateTypeDetail,
9
9
  } from './CogsState.js';
10
10
 
11
- import type { ReactNode } from 'react';
11
+ import { startTransition, type ReactNode } from 'react';
12
12
 
13
13
  type StateUpdater<StateValue> =
14
14
  | StateValue
@@ -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,9 +173,14 @@ 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 = {
179
+ updateQueue: Set<() => void>;
180
+ isFlushScheduled: boolean;
181
+
182
+ flushUpdates: () => void;
183
+
180
184
  // --- Shadow State and Subscription System ---
181
185
  registerComponent: (
182
186
  stateKey: string,
@@ -190,6 +194,7 @@ export type CogsGlobalState = {
190
194
  fullComponentId: string
191
195
  ) => void;
192
196
  shadowStateStore: Map<string, ShadowMetadata>;
197
+
193
198
  markAsDirty: (
194
199
  key: string,
195
200
  path: string[],
@@ -250,10 +255,6 @@ export type CogsGlobalState = {
250
255
  getInitialOptions: (key: string) => OptionsType | undefined;
251
256
  setInitialStateOptions: (key: string, value: OptionsType) => void;
252
257
 
253
- // --- Validation ---
254
-
255
- // --- Server Sync and Logging ---
256
-
257
258
  serverStateUpdates: Map<
258
259
  string,
259
260
  {
@@ -265,42 +266,44 @@ export type CogsGlobalState = {
265
266
 
266
267
  setServerStateUpdate: (key: string, serverState: any) => void;
267
268
 
268
- stateLog: { [key: string]: UpdateTypeDetail[] };
269
+ stateLog: Map<string, Map<string, UpdateTypeDetail>>;
269
270
  syncInfoStore: Map<string, SyncInfo>;
271
+ addStateLog: (key: string, update: UpdateTypeDetail) => void;
270
272
 
271
- setStateLog: (
272
- key: string,
273
- updater: (prevUpdates: UpdateTypeDetail[]) => UpdateTypeDetail[]
274
- ) => void;
275
273
  setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
276
274
  getSyncInfo: (key: string) => SyncInfo | null;
277
275
  };
278
276
  const isSimpleObject = (value: any): boolean => {
277
+ // Most common cases first
279
278
  if (value === null || typeof value !== 'object') return false;
280
279
 
281
- // Handle special cases that should be treated as primitives
282
- if (
283
- value instanceof Uint8Array ||
284
- value instanceof Int8Array ||
285
- value instanceof Uint16Array ||
286
- value instanceof Int16Array ||
287
- value instanceof Uint32Array ||
288
- value instanceof Int32Array ||
289
- value instanceof Float32Array ||
290
- value instanceof Float64Array ||
291
- value instanceof ArrayBuffer ||
292
- value instanceof Date ||
293
- value instanceof RegExp ||
294
- value instanceof Map ||
295
- value instanceof Set
296
- ) {
297
- return false; // Treat as primitive
298
- }
299
-
300
- // Arrays and plain objects are complex
301
- 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;
302
288
  };
303
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,
293
+
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
+ });
302
+ }
303
+
304
+ // Clear the queue and reset the flag for the next event loop tick.
305
+ set({ updateQueue: new Set(), isFlushScheduled: false });
306
+ },
304
307
  addPathComponent: (stateKey, dependencyPath, fullComponentId) => {
305
308
  set((state) => {
306
309
  const newShadowStore = new Map(state.shadowStateStore);
@@ -340,20 +343,11 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
340
343
  },
341
344
  registerComponent: (stateKey, fullComponentId, registration) => {
342
345
  set((state) => {
343
- // Create a new Map to ensure Zustand detects the change
344
346
  const newShadowStore = new Map(state.shadowStateStore);
345
-
346
- // Get the metadata for the ROOT of the state (where the components map lives)
347
347
  const rootMeta = newShadowStore.get(stateKey) || {};
348
-
349
- // Also clone the components map to avoid direct mutation
350
348
  const components = new Map(rootMeta.components);
351
349
  components.set(fullComponentId, registration);
352
-
353
- // Update the root metadata with the new components map
354
350
  newShadowStore.set(stateKey, { ...rootMeta, components });
355
-
356
- // Return the updated state
357
351
  return { shadowStateStore: newShadowStore };
358
352
  });
359
353
  },
@@ -362,8 +356,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
362
356
  set((state) => {
363
357
  const newShadowStore = new Map(state.shadowStateStore);
364
358
  const rootMeta = newShadowStore.get(stateKey);
365
-
366
- // If there's no metadata or no components map, do nothing
367
359
  if (!rootMeta?.components) {
368
360
  return state; // Return original state, no change needed
369
361
  }
@@ -381,40 +373,45 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
381
373
  });
382
374
  },
383
375
  markAsDirty: (key: string, path: string[], options = { bubble: true }) => {
384
- const newShadowStore = new Map(get().shadowStateStore);
385
- let changed = false;
376
+ const { shadowStateStore } = get();
377
+ const updates = new Map<string, ShadowMetadata>();
386
378
 
387
379
  const setDirty = (currentPath: string[]) => {
388
380
  const fullKey = [key, ...currentPath].join('.');
389
- const meta = newShadowStore.get(fullKey);
390
-
391
- // We mark something as dirty if it isn't already.
392
- // The original data source doesn't matter.
393
- if (meta && meta.isDirty !== true) {
394
- newShadowStore.set(fullKey, { ...meta, isDirty: true });
395
- changed = true;
396
- } else if (!meta) {
397
- // If there's no metadata, create it and mark it as dirty.
398
- // This handles newly created fields within an object.
399
- newShadowStore.set(fullKey, { isDirty: true });
400
- 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
401
386
  }
387
+
388
+ updates.set(fullKey, { ...meta, isDirty: true });
389
+ return false; // Not previously dirty
402
390
  };
403
391
 
404
- // 1. Mark the target path itself as dirty.
392
+ // Mark the target path
405
393
  setDirty(path);
406
394
 
407
- // 2. If `bubble` is true, walk up the path and mark all parents as dirty.
395
+ // Bubble up if requested
408
396
  if (options.bubble) {
409
397
  let parentPath = [...path];
410
398
  while (parentPath.length > 0) {
411
399
  parentPath.pop();
412
- setDirty(parentPath);
400
+ const wasDirty = setDirty(parentPath);
401
+ if (wasDirty) {
402
+ break; // Stop bubbling if parent was already dirty
403
+ }
413
404
  }
414
405
  }
415
406
 
416
- if (changed) {
417
- 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
+ });
418
415
  }
419
416
  },
420
417
  serverStateUpdates: new Map(),
@@ -432,6 +429,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
432
429
  });
433
430
  },
434
431
  shadowStateStore: new Map(),
432
+ getShadowNode: (key: string) => get().shadowStateStore.get(key),
435
433
  pathSubscribers: new Map<string, Set<(newValue: any) => void>>(),
436
434
 
437
435
  subscribeToPath: (path, callback) => {
@@ -464,7 +462,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
464
462
  set((state) => {
465
463
  // 1. Make a copy of the current store to modify it
466
464
  const newShadowStore = new Map(state.shadowStateStore);
467
-
465
+ console.log('initializeShadowState');
468
466
  // 2. PRESERVE the existing components map before doing anything else
469
467
  const existingRootMeta = newShadowStore.get(key);
470
468
  const preservedComponents = existingRootMeta?.components;
@@ -521,47 +519,49 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
521
519
  },
522
520
 
523
521
  getShadowValue: (fullKey: string, validArrayIds?: string[]) => {
524
- const shadowMeta = get().shadowStateStore.get(fullKey);
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
+ }
525
527
 
526
- // If no metadata found, return undefined
527
- if (!shadowMeta) {
528
- return undefined;
529
- }
528
+ const shadowMeta = get().shadowStateStore.get(keyToBuild);
529
+ if (!shadowMeta) {
530
+ return undefined;
531
+ }
530
532
 
531
- // For primitive values, return the value
532
- if (shadowMeta.value !== undefined) {
533
- return shadowMeta.value;
534
- }
533
+ if (shadowMeta.value !== undefined) {
534
+ return shadowMeta.value;
535
+ }
535
536
 
536
- // For arrays, reconstruct with possible validArrayIds
537
- if (shadowMeta.arrayKeys) {
538
- const arrayKeys = validArrayIds ?? shadowMeta.arrayKeys;
539
- const items = arrayKeys.map((itemKey) => {
540
- // RECURSIVELY call getShadowValue for each item
541
- return get().getShadowValue(itemKey);
542
- });
543
- return items;
544
- }
537
+ let result: any; // The value we are about to build.
545
538
 
546
- // For objects with fields, reconstruct object
547
- if (shadowMeta.fields) {
548
- const reconstructedObject: any = {};
549
- Object.entries(shadowMeta.fields).forEach(([key, fieldPath]) => {
550
- // RECURSIVELY call getShadowValue for each field
551
- reconstructedObject[key] = get().getShadowValue(fieldPath as string);
552
- });
553
- return reconstructedObject;
554
- }
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
+ }
555
+
556
+ // Return the final, fully populated result.
557
+ return result;
558
+ };
555
559
 
556
- return undefined;
560
+ // Start the process by calling the inner function on the root key.
561
+ return reconstruct(fullKey, validArrayIds);
557
562
  },
558
- getShadowMetadata: (
559
- key: string,
560
- path: string[],
561
- validArrayIds?: string[]
562
- ) => {
563
+ getShadowMetadata: (key: string, path: string[]) => {
563
564
  const fullKey = [key, ...path].join('.');
564
- let data = get().shadowStateStore.get(fullKey);
565
565
 
566
566
  return get().shadowStateStore.get(fullKey);
567
567
  },
@@ -727,36 +727,63 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
727
727
  itemKey: itemKey, // The exact ID of the removed item
728
728
  });
729
729
  },
730
+
730
731
  updateShadowAtPath: (key, path, newValue) => {
731
- const newShadowStore = new Map(get().shadowStateStore);
732
732
  const fullKey = [key, ...path].join('.');
733
733
 
734
- const updateValue = (currentKey: string, valueToSet: any) => {
735
- 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
+ }
736
739
 
737
- // If it's a simple object with fields, update recursively
738
- if (isSimpleObject(valueToSet) && meta && meta.fields) {
739
- for (const fieldKey in valueToSet) {
740
- if (Object.prototype.hasOwnProperty.call(valueToSet, fieldKey)) {
741
- const childPath = meta.fields[fieldKey];
742
- const childValue = valueToSet[fieldKey];
740
+ // CHANGE: Don't clone the entire Map, just update in place
741
+ set((state) => {
742
+ const store = state.shadowStateStore;
743
743
 
744
- if (childPath) {
745
- 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
+ }
746
774
  }
747
775
  }
748
- }
749
- } else {
750
- // For primitives (including Uint8Array), just replace the value
751
- // This gives you useState-like behavior
752
- const existing = newShadowStore.get(currentKey) || {};
753
- newShadowStore.set(currentKey, { ...existing, value: valueToSet });
776
+ };
777
+
778
+ processObject(path, newValue);
754
779
  }
755
- };
756
780
 
757
- updateValue(fullKey, newValue);
758
- get().notifyPathSubscribers(fullKey, { type: 'UPDATE', newValue });
759
- 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
+ });
760
787
  },
761
788
  selectedIndicesMap: new Map<string, string>(),
762
789
  getSelectedIndex: (arrayKey: string, validIds?: string[]): number => {
@@ -837,25 +864,29 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
837
864
 
838
865
  stateTimeline: {},
839
866
  cogsStateStore: {},
840
- stateLog: {},
867
+ stateLog: new Map(),
841
868
 
842
869
  initialStateGlobal: {},
843
870
 
844
871
  validationErrors: new Map(),
872
+ addStateLog: (key, update) => {
873
+ set((state) => {
874
+ 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 });
886
+ }
845
887
 
846
- setStateLog: (
847
- key: string,
848
- updater: (prevUpdates: UpdateTypeDetail[]) => UpdateTypeDetail[]
849
- ) => {
850
- set((prev) => {
851
- const currentUpdates = prev.stateLog[key] ?? [];
852
- const newUpdates = updater(currentUpdates);
853
- return {
854
- stateLog: {
855
- ...prev.stateLog,
856
- [key]: newUpdates,
857
- },
858
- };
888
+ newLog.set(key, stateLogForKey);
889
+ return { stateLog: newLog };
859
890
  });
860
891
  },
861
892