cogsbox-state 0.5.426 → 0.5.428

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/Functions.tsx CHANGED
@@ -7,11 +7,17 @@ import {
7
7
  type UpdateOpts,
8
8
  } from "./CogsState";
9
9
 
10
- import { getNestedValue, isFunction, updateNestedProperty } from "./utility";
10
+ import {
11
+ getNestedValue,
12
+ isFunction,
13
+ updateNestedProperty,
14
+ updateNestedPropertyIds,
15
+ } from "./utility";
11
16
  import { useEffect, useRef, useState } from "react";
12
17
  import React from "react";
13
18
  import { getGlobalStore, formRefStore } from "./store";
14
19
  import { validateZodPathFunc } from "./useValidateZodPath";
20
+ import { ulid } from "ulid";
15
21
 
16
22
  export function updateFn<U>(
17
23
  setState: EffectiveSetState<U>,
@@ -23,7 +29,9 @@ export function updateFn<U>(
23
29
  (prevState) => {
24
30
  if (isFunction<U>(payload)) {
25
31
  const nestedValue = payload(getNestedValue(prevState, path));
26
- let value = updateNestedProperty(path, prevState, nestedValue);
32
+ console.group("nestedValue", path, nestedValue);
33
+ let value = updateNestedPropertyIds(path, prevState, nestedValue);
34
+ console.group("updateFn", value);
27
35
  if (typeof value == "string") {
28
36
  value = value.trim();
29
37
  }
@@ -32,7 +40,7 @@ export function updateFn<U>(
32
40
  let value =
33
41
  !path || path.length == 0
34
42
  ? payload
35
- : updateNestedProperty(path, prevState, payload);
43
+ : updateNestedPropertyIds(path, prevState, payload);
36
44
  if (typeof value == "string") {
37
45
  value = value.trim();
38
46
  }
@@ -44,7 +52,6 @@ export function updateFn<U>(
44
52
  validationKey
45
53
  );
46
54
  }
47
-
48
55
  export function pushFunc<U>(
49
56
  setState: EffectiveSetState<U>,
50
57
  payload: UpdateArg<U>,
@@ -52,68 +59,69 @@ export function pushFunc<U>(
52
59
  stateKey: string,
53
60
  index?: number
54
61
  ): void {
55
- const array = getGlobalStore.getState().getNestedState(stateKey, path) as U[];
62
+ // --- THE FIX ---
63
+ // 1. Determine the newItem and its ID BEFORE calling setState.
64
+ const arrayBeforeUpdate =
65
+ (getGlobalStore.getState().getNestedState(stateKey, path) as any[]) || [];
66
+
67
+ const newItem = isFunction<U>(payload)
68
+ ? payload(arrayBeforeUpdate as any)
69
+ : payload;
70
+
71
+ // 2. Ensure it has an ID.
72
+ if (typeof newItem === "object" && newItem !== null && !(newItem as any).id) {
73
+ (newItem as any).id = ulid();
74
+ }
75
+ const finalId = (newItem as any).id;
76
+ // --- END OF FIX ---
77
+
56
78
  setState(
57
79
  (prevState) => {
58
- let arrayToUpdate =
59
- !path || path.length == 0
60
- ? prevState
61
- : getNestedValue(prevState, [...path]);
62
- let returnedArray = [...arrayToUpdate];
63
-
64
- returnedArray.splice(
65
- index || Number(index) == 0 ? index : arrayToUpdate.length,
66
- 0,
67
- isFunction<U>(payload)
68
- ? payload(index == -1 ? undefined : arrayToUpdate)
69
- : payload
70
- );
71
- const value =
72
- path.length == 0
73
- ? returnedArray
74
- : updateNestedProperty([...path], prevState, returnedArray);
75
-
76
- return value as U;
80
+ // The logic inside here is now much simpler.
81
+ // We already have the final `newItem`.
82
+ const arrayToUpdate = getNestedValue(prevState, [...path]) || [];
83
+ const newArray = [...arrayToUpdate];
84
+ newArray.splice(index ?? newArray.length, 0, newItem);
85
+ return updateNestedPropertyIds([...path], prevState, newArray);
77
86
  },
78
- [
79
- ...path,
80
- index || index === 0 ? index?.toString() : (array!.length - 1).toString(),
81
- ],
87
+ [...path, `id:${finalId}`], // Now we use the ID that is guaranteed to be correct.
82
88
  {
83
89
  updateType: "insert",
84
90
  }
85
91
  );
86
92
  }
87
-
88
93
  export function cutFunc<U>(
89
94
  setState: EffectiveSetState<U>,
90
95
  path: string[],
91
96
  stateKey: string,
92
97
  index: number
93
98
  ): void {
94
- const array = getGlobalStore.getState().getNestedState(stateKey, path) as U[];
99
+ // Get the ordered IDs to find the ID for this index
100
+ const arrayKey = [stateKey, ...path].join(".");
101
+ const arrayMeta = getGlobalStore.getState().shadowStateStore.get(arrayKey);
102
+ const itemId = arrayMeta?.arrayKeys?.[index];
103
+
104
+ if (!itemId) {
105
+ throw new Error(`No ID found for index ${index} in array`);
106
+ }
107
+
95
108
  setState(
96
109
  (prevState) => {
97
110
  const arrayToUpdate = getNestedValue(prevState, [...path]);
98
111
  if (index < 0 || index >= arrayToUpdate?.length) {
99
112
  throw new Error(`Index ${index} does not exist in the array.`);
100
113
  }
101
- const indexToCut =
102
- index || Number(index) == 0 ? index : arrayToUpdate.length - 1;
103
114
 
104
115
  const updatedArray = [
105
- ...arrayToUpdate.slice(0, indexToCut),
106
- ...arrayToUpdate.slice(indexToCut + 1),
116
+ ...arrayToUpdate.slice(0, index),
117
+ ...arrayToUpdate.slice(index + 1),
107
118
  ] as U;
108
119
 
109
120
  return path.length == 0
110
121
  ? updatedArray
111
- : updateNestedProperty([...path], prevState, updatedArray);
122
+ : updateNestedPropertyIds([...path], prevState, updatedArray);
112
123
  },
113
- [
114
- ...path,
115
- index || index === 0 ? index?.toString() : (array!.length - 1).toString(),
116
- ],
124
+ [...path, itemId], // Use the ID here!
117
125
  { updateType: "cut" }
118
126
  );
119
127
  }
package/src/store.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { create } from "zustand";
2
+ import { ulid } from "ulid";
2
3
  import type {
3
4
  OptionsType,
4
5
  ReactivityType,
@@ -86,24 +87,21 @@ export const formRefStore = create<FormRefStoreState>((set, get) => ({
86
87
  },
87
88
  }));
88
89
 
89
- type ShadowMetadata = {
90
- virtualisedState?: { listItemHeight: number };
90
+ export type ShadowMetadata = {
91
+ id: string;
92
+ arrayKeys?: string[];
93
+ virtualizer?: {
94
+ itemHeight?: number;
95
+ domRef?: HTMLElement | null;
96
+ };
91
97
  syncInfo?: { status: string };
92
- // Add other metadata fields you need
98
+ lastUpdated?: number;
93
99
  };
94
-
95
- type ShadowState<T> =
96
- T extends Array<infer U>
97
- ? Array<ShadowState<U>> & ShadowMetadata
98
- : T extends object
99
- ? { [K in keyof T]: ShadowState<T[K]> } & ShadowMetadata
100
- : ShadowMetadata;
101
-
102
100
  export type CogsGlobalState = {
103
101
  // --- Shadow State and Subscription System ---
104
- shadowStateStore: { [key: string]: any };
105
- shadowStateSubscribers: Map<string, Set<() => void>>; // Stores subscribers for shadow state updates
106
- subscribeToShadowState: (key: string, callback: () => void) => () => void; // Subscribes a listener, returns an unsubscribe function
102
+ shadowStateStore: Map<string, ShadowMetadata>;
103
+
104
+ // These method signatures stay the same
107
105
  initializeShadowState: (key: string, initialState: any) => void;
108
106
  updateShadowAtPath: (key: string, path: string[], newValue: any) => void;
109
107
  insertShadowArrayElement: (
@@ -111,15 +109,20 @@ export type CogsGlobalState = {
111
109
  arrayPath: string[],
112
110
  newItem: any
113
111
  ) => void;
114
- removeShadowArrayElement: (
112
+ removeShadowArrayElement: (key: string, arrayPath: string[]) => void;
113
+ getShadowMetadata: (
115
114
  key: string,
116
- arrayPath: string[],
117
- index: number
115
+ path: string[]
116
+ ) => ShadowMetadata | undefined;
117
+ setShadowMetadata: (
118
+ key: string,
119
+ path: string[],
120
+ metadata: Omit<ShadowMetadata, "id">
118
121
  ) => void;
119
- getShadowMetadata: (key: string, path: string[]) => any;
120
- setShadowMetadata: (key: string, path: string[], metadata: any) => void;
121
122
 
122
- // --- Selected Item State ---
123
+ shadowStateSubscribers: Map<string, Set<() => void>>; // Stores subscribers for shadow state updates
124
+ subscribeToShadowState: (key: string, callback: () => void) => () => void;
125
+
123
126
  selectedIndicesMap: Map<string, Map<string, number>>; // stateKey -> (parentPath -> selectedIndex)
124
127
  getSelectedIndex: (
125
128
  stateKey: string,
@@ -242,192 +245,163 @@ export type CogsGlobalState = {
242
245
  };
243
246
 
244
247
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
245
- shadowStateStore: {},
246
- getShadowMetadata: (key: string, path: string[]) => {
247
- const shadow = get().shadowStateStore[key];
248
- if (!shadow) return null;
249
-
250
- let current = shadow;
251
- for (const segment of path) {
252
- current = current?.[segment];
253
- if (!current) return null;
254
- }
248
+ shadowStateStore: new Map(),
249
+ shadowStateSubscribers: new Map(),
255
250
 
256
- return current;
257
- },
251
+ subscribeToShadowState: (key: string, callback: () => void) => {
252
+ set((state) => {
253
+ const newSubs = new Map(state.shadowStateSubscribers);
254
+ const subsForKey = newSubs.get(key) || new Set();
255
+ subsForKey.add(callback);
256
+ newSubs.set(key, subsForKey);
257
+ return { shadowStateSubscribers: newSubs };
258
+ });
258
259
 
259
- initializeShadowState: (key: string, initialState: any) => {
260
- const createShadowStructure = (obj: any): any => {
261
- if (Array.isArray(obj)) {
262
- return new Array(obj.length)
263
- .fill(null)
264
- .map((_, i) => createShadowStructure(obj[i]));
265
- }
266
- if (typeof obj === "object" && obj !== null) {
267
- const shadow: any = {};
268
- for (const k in obj) {
269
- shadow[k] = createShadowStructure(obj[k]);
260
+ // Return unsubscribe function
261
+ return () => {
262
+ set((state) => {
263
+ const newSubs = new Map(state.shadowStateSubscribers);
264
+ const subsForKey = newSubs.get(key);
265
+ if (subsForKey) {
266
+ subsForKey.delete(callback);
267
+ if (subsForKey.size === 0) {
268
+ newSubs.delete(key);
269
+ }
270
270
  }
271
- return shadow;
272
- }
273
- return {}; // Leaf node - empty object for metadata
271
+ return { shadowStateSubscribers: newSubs };
272
+ });
274
273
  };
275
-
276
- set((state) => ({
277
- shadowStateStore: {
278
- ...state.shadowStateStore,
279
- [key]: createShadowStructure(initialState),
280
- },
281
- }));
282
274
  },
275
+ initializeShadowState: (key: string, initialState: any) => {
276
+ const newShadowStore = new Map<string, ShadowMetadata>();
283
277
 
284
- updateShadowAtPath: (key: string, path: string[], newValue: any) => {
285
- set((state) => {
286
- const newShadow = { ...state.shadowStateStore };
287
- if (!newShadow[key]) return state;
288
-
289
- let current = newShadow[key];
290
- const pathCopy = [...path];
291
- const lastSegment = pathCopy.pop();
292
-
293
- // Navigate to parent
294
- for (const segment of pathCopy) {
295
- if (!current[segment]) current[segment] = {};
296
- current = current[segment];
297
- }
278
+ const processValue = (value: any, path: string[]) => {
279
+ const nodeKey = [key, ...path].join(".");
298
280
 
299
- // Update shadow structure to match new value structure
300
- if (lastSegment !== undefined) {
301
- if (Array.isArray(newValue)) {
302
- current[lastSegment] = new Array(newValue.length);
303
- } else if (typeof newValue === "object" && newValue !== null) {
304
- current[lastSegment] = {};
305
- } else {
306
- current[lastSegment] = current[lastSegment] || {};
307
- }
308
- }
281
+ if (Array.isArray(value)) {
282
+ const childIds: string[] = [];
309
283
 
310
- return { shadowStateStore: newShadow };
311
- });
312
- },
284
+ value.forEach((item) => {
285
+ if (typeof item === "object" && item !== null && !item.id) {
286
+ item.id = ulid();
287
+ }
313
288
 
314
- insertShadowArrayElement: (
315
- key: string,
316
- arrayPath: string[],
317
- newItem: any
318
- ) => {
319
- set((state) => {
320
- const newShadow = { ...state.shadowStateStore };
321
- if (!newShadow[key]) return state;
289
+ const itemId = `id:${item.id}`;
290
+ childIds.push(itemId);
322
291
 
323
- newShadow[key] = JSON.parse(JSON.stringify(newShadow[key]));
292
+ const itemPath = [...path, itemId];
293
+ processValue(item, itemPath);
294
+ });
324
295
 
325
- let current: any = newShadow[key];
296
+ const arrayContainerMetadata: ShadowMetadata = {
297
+ id: ulid(),
298
+ arrayKeys: childIds,
299
+ };
300
+ newShadowStore.set(nodeKey, arrayContainerMetadata);
301
+ } else if (typeof value === "object" && value !== null) {
302
+ newShadowStore.set(nodeKey, { id: ulid() });
326
303
 
327
- for (const segment of arrayPath) {
328
- current = current[segment];
329
- if (!current) return state;
304
+ Object.keys(value).forEach((k) => {
305
+ processValue(value[k], [...path, k]);
306
+ });
307
+ } else {
308
+ newShadowStore.set(nodeKey, { id: ulid() });
330
309
  }
310
+ };
331
311
 
332
- if (Array.isArray(current)) {
333
- // Create shadow structure based on the actual new item
334
- const createShadowStructure = (obj: any): any => {
335
- if (Array.isArray(obj)) {
336
- return obj.map((item) => createShadowStructure(item));
337
- }
338
- if (typeof obj === "object" && obj !== null) {
339
- const shadow: any = {};
340
- for (const k in obj) {
341
- shadow[k] = createShadowStructure(obj[k]);
342
- }
343
- return shadow;
344
- }
345
- return {}; // Leaf nodes get empty object for metadata
346
- };
312
+ processValue(initialState, []);
347
313
 
348
- current.push(createShadowStructure(newItem));
349
- }
314
+ set({ shadowStateStore: newShadowStore });
315
+ },
316
+ getShadowMetadata: (key: string, path: string[]) => {
317
+ const fullKey = [key, ...path].join(".");
318
+ return get().shadowStateStore.get(fullKey);
319
+ },
350
320
 
351
- return { shadowStateStore: newShadow };
352
- });
321
+ setShadowMetadata: (key: string, path: string[], metadata: any) => {
322
+ const fullKey = [key, ...path].join(".");
323
+ const newShadowStore = new Map(get().shadowStateStore);
324
+ const existing = newShadowStore.get(fullKey) || { id: ulid() };
325
+ newShadowStore.set(fullKey, { ...existing, ...metadata });
326
+ set({ shadowStateStore: newShadowStore });
327
+
328
+ if (metadata.virtualizer?.itemHeight) {
329
+ const subscribers = get().shadowStateSubscribers.get(key);
330
+ subscribers?.forEach((cb) => cb());
331
+ }
353
332
  },
354
- removeShadowArrayElement: (
333
+
334
+ insertShadowArrayElement: (
355
335
  key: string,
356
336
  arrayPath: string[],
357
- index: number
337
+ newItem: any
358
338
  ) => {
359
- set((state) => {
360
- const newShadow = { ...state.shadowStateStore };
361
- let current = newShadow[key];
339
+ const newShadowStore = new Map(get().shadowStateStore);
340
+ const arrayKey = [key, ...arrayPath].join(".");
341
+ const parentMeta = newShadowStore.get(arrayKey);
342
+ const newArrayState = get().getNestedState(key, arrayPath) as any[];
362
343
 
363
- for (const segment of arrayPath) {
364
- current = current?.[segment];
365
- }
344
+ if (!parentMeta || !parentMeta.arrayKeys) return;
366
345
 
367
- if (Array.isArray(current)) {
368
- current.splice(index, 1);
369
- }
346
+ const newItemId = `id:${newItem.id}`;
347
+ const newIndex = newArrayState.findIndex((item) => item.id === newItem.id);
370
348
 
371
- return { shadowStateStore: newShadow };
372
- });
373
- },
374
- shadowStateSubscribers: new Map<string, Set<() => void>>(), // key -> Set of callbacks
349
+ if (newIndex === -1) return;
375
350
 
376
- subscribeToShadowState: (key: string, callback: () => void) => {
377
- set((state) => {
378
- const newSubs = new Map(state.shadowStateSubscribers);
379
- const subsForKey = newSubs.get(key) || new Set();
380
- subsForKey.add(callback);
381
- newSubs.set(key, subsForKey);
382
- return { shadowStateSubscribers: newSubs };
383
- });
384
- // Return an unsubscribe function
385
- return () => {
386
- set((state) => {
387
- const newSubs = new Map(state.shadowStateSubscribers);
388
- const subsForKey = newSubs.get(key);
389
- if (subsForKey) {
390
- subsForKey.delete(callback);
391
- }
392
- return { shadowStateSubscribers: newSubs };
393
- });
394
- };
395
- },
351
+ const newArrayKeys = [...parentMeta.arrayKeys];
352
+ newArrayKeys.splice(newIndex, 0, newItemId);
353
+ newShadowStore.set(arrayKey, { ...parentMeta, arrayKeys: newArrayKeys });
396
354
 
397
- setShadowMetadata: (key: string, path: string[], metadata: any) => {
398
- let hasChanged = false;
399
- set((state) => {
400
- const newShadow = { ...state.shadowStateStore };
401
- if (!newShadow[key]) return state;
355
+ const processNewItem = (value: any, path: string[]) => {
356
+ const nodeKey = [key, ...path].join(".");
357
+ if (typeof value === "object" && value !== null) {
358
+ newShadowStore.set(nodeKey, { id: ulid() });
359
+ Object.keys(value).forEach((k) => {
360
+ processNewItem(value[k], [...path, k]);
361
+ });
362
+ } else {
363
+ newShadowStore.set(nodeKey, { id: ulid() });
364
+ }
365
+ };
402
366
 
403
- newShadow[key] = JSON.parse(JSON.stringify(newShadow[key]));
367
+ processNewItem(newItem, [...arrayPath, newItemId]);
404
368
 
405
- let current: any = newShadow[key];
406
- for (const segment of path) {
407
- if (!current[segment]) current[segment] = {};
408
- current = current[segment];
409
- }
369
+ set({ shadowStateStore: newShadowStore });
370
+ },
410
371
 
411
- const oldHeight = current.virtualizer?.itemHeight;
412
- const newHeight = metadata.virtualizer?.itemHeight;
372
+ removeShadowArrayElement: (key: string, itemPath: string[]) => {
373
+ const newShadowStore = new Map(get().shadowStateStore);
374
+ const itemKey = [key, ...itemPath].join(".");
375
+ const itemIdToRemove = itemPath[itemPath.length - 1];
413
376
 
414
- if (newHeight && oldHeight !== newHeight) {
415
- hasChanged = true;
416
- if (!current.virtualizer) current.virtualizer = {};
417
- current.virtualizer.itemHeight = newHeight;
418
- }
377
+ const parentPath = itemPath.slice(0, -1);
378
+ const parentKey = [key, ...parentPath].join(".");
379
+ const parentMeta = newShadowStore.get(parentKey);
419
380
 
420
- return { shadowStateStore: newShadow };
421
- });
381
+ if (parentMeta && parentMeta.arrayKeys) {
382
+ const newArrayKeys = parentMeta.arrayKeys.filter(
383
+ (id) => id !== itemIdToRemove
384
+ );
385
+ newShadowStore.set(parentKey, { ...parentMeta, arrayKeys: newArrayKeys });
386
+ }
422
387
 
423
- // If a height value was actually changed, notify the specific subscribers.
424
- if (hasChanged) {
425
- const subscribers = get().shadowStateSubscribers.get(key);
426
- if (subscribers) {
427
- subscribers.forEach((callback) => callback());
388
+ const prefixToDelete = itemKey + ".";
389
+ for (const k of Array.from(newShadowStore.keys())) {
390
+ if (k === itemKey || k.startsWith(prefixToDelete)) {
391
+ newShadowStore.delete(k);
428
392
  }
429
393
  }
394
+
395
+ set({ shadowStateStore: newShadowStore });
396
+ },
397
+ updateShadowAtPath: (key: string, path: string[], newValue: any) => {
398
+ const fullKey = [key, ...path].join(".");
399
+ const newShadowStore = new Map(get().shadowStateStore);
400
+ const existing = newShadowStore.get(fullKey) || { id: ulid() };
401
+ newShadowStore.set(fullKey, { ...existing, lastUpdated: Date.now() });
402
+ set({ shadowStateStore: newShadowStore });
430
403
  },
404
+
431
405
  selectedIndicesMap: new Map<string, Map<string, number>>(),
432
406
 
433
407
  // Add the new methods
@@ -734,41 +708,44 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
734
708
  getNestedState: (key: string, path: string[]) => {
735
709
  const rootState = get().cogsStateStore[key];
736
710
 
737
- const getValueWithAsterisk = (obj: any, pathArray: string[]): any => {
738
- if (pathArray.length === 0) return obj;
711
+ const resolvePath = (obj: any, pathArray: string[]): any => {
712
+ if (pathArray.length === 0 || obj === undefined) {
713
+ return obj;
714
+ }
739
715
 
740
- const currentPath = pathArray[0];
716
+ const currentSegment = pathArray[0];
741
717
  const remainingPath = pathArray.slice(1);
742
718
 
743
- if (currentPath === "[*]") {
719
+ // FIX: Handle ID-based array access like 'id:xyz'
720
+ if (
721
+ Array.isArray(obj) &&
722
+ typeof currentSegment === "string" &&
723
+ currentSegment.startsWith("id:")
724
+ ) {
725
+ const targetId = currentSegment.split(":")[1];
726
+ const foundItem = obj.find(
727
+ (item) => item && String(item.id) === targetId
728
+ );
729
+ return resolvePath(foundItem, remainingPath);
730
+ }
731
+
732
+ // Handle wildcard array access: '[*]'
733
+ if (currentSegment === "[*]") {
744
734
  if (!Array.isArray(obj)) {
745
735
  console.warn("Asterisk notation used on non-array value");
746
736
  return undefined;
747
737
  }
748
-
749
738
  if (remainingPath.length === 0) return obj;
750
-
751
- // Get result for each array item
752
- const results = obj.map((item) =>
753
- getValueWithAsterisk(item, remainingPath)
754
- );
755
-
756
- // If the next path segment exists and returns arrays, flatten them
757
- if (Array.isArray(results[0])) {
758
- return results.flat();
759
- }
760
-
761
- return results;
739
+ const results = obj.map((item) => resolvePath(item, remainingPath));
740
+ return Array.isArray(results[0]) ? results.flat() : results;
762
741
  }
763
742
 
764
- const value = obj[currentPath as keyof typeof obj];
765
- if (value === undefined) return undefined;
766
-
767
- return getValueWithAsterisk(value, remainingPath);
743
+ // Handle standard object property access and numeric array indices
744
+ const nextObj = obj[currentSegment as keyof typeof obj];
745
+ return resolvePath(nextObj, remainingPath);
768
746
  };
769
747
 
770
- // This will still get the value but we need to make it reactive only to specific paths
771
- return getValueWithAsterisk(rootState, path);
748
+ return resolvePath(rootState, path);
772
749
  },
773
750
  setInitialStateOptions: (key, value) => {
774
751
  set((prev) => ({