cogsbox-state 0.5.471 → 0.5.473

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.
Files changed (46) hide show
  1. package/README.md +2 -5
  2. package/dist/CogsState.d.ts +105 -79
  3. package/dist/CogsState.d.ts.map +1 -1
  4. package/dist/CogsState.jsx +1082 -987
  5. package/dist/CogsState.jsx.map +1 -1
  6. package/dist/Components.d.ts.map +1 -1
  7. package/dist/Components.jsx +293 -243
  8. package/dist/Components.jsx.map +1 -1
  9. package/dist/PluginRunner.d.ts +10 -0
  10. package/dist/PluginRunner.d.ts.map +1 -0
  11. package/dist/PluginRunner.jsx +128 -0
  12. package/dist/PluginRunner.jsx.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +33 -26
  16. package/dist/index.js.map +1 -1
  17. package/dist/pluginStore.d.ts +43 -0
  18. package/dist/pluginStore.d.ts.map +1 -0
  19. package/dist/pluginStore.js +52 -0
  20. package/dist/pluginStore.js.map +1 -0
  21. package/dist/plugins.d.ts +1326 -0
  22. package/dist/plugins.d.ts.map +1 -0
  23. package/dist/plugins.js +76 -0
  24. package/dist/plugins.js.map +1 -0
  25. package/dist/store.d.ts +69 -26
  26. package/dist/store.d.ts.map +1 -1
  27. package/dist/store.js +436 -152
  28. package/dist/store.js.map +1 -1
  29. package/dist/utility.d.ts +1 -1
  30. package/dist/utility.d.ts.map +1 -1
  31. package/dist/utility.js +12 -12
  32. package/dist/utility.js.map +1 -1
  33. package/dist/validation.d.ts +7 -0
  34. package/dist/validation.d.ts.map +1 -0
  35. package/dist/validation.js +39 -0
  36. package/dist/validation.js.map +1 -0
  37. package/package.json +18 -13
  38. package/src/CogsState.tsx +719 -458
  39. package/src/Components.tsx +304 -180
  40. package/src/PluginRunner.tsx +208 -0
  41. package/src/index.ts +2 -0
  42. package/src/pluginStore.ts +159 -0
  43. package/src/plugins.ts +548 -0
  44. package/src/store.ts +881 -189
  45. package/src/utility.ts +31 -31
  46. package/src/validation.ts +84 -0
package/src/store.ts CHANGED
@@ -7,8 +7,6 @@ import type {
7
7
  UpdateTypeDetail,
8
8
  } from './CogsState.js';
9
9
 
10
- import { type ReactNode } from 'react';
11
-
12
10
  export type FreshValuesObject = {
13
11
  pathsToValues?: string[];
14
12
  prevValue?: any;
@@ -17,55 +15,16 @@ export type FreshValuesObject = {
17
15
  };
18
16
 
19
17
  type StateValue = any;
20
-
18
+ export type FormEventType = {
19
+ type: 'focus' | 'blur' | 'input';
20
+ value?: any;
21
+ path: string[];
22
+ };
21
23
  export type TrieNode = {
22
24
  subscribers: Set<string>;
23
25
  children: Map<string, TrieNode>;
24
26
  };
25
27
 
26
- export type FormRefStoreState = {
27
- formRefs: Map<string, React.RefObject<any>>;
28
- registerFormRef: (id: string, ref: React.RefObject<any>) => void;
29
- getFormRef: (id: string) => React.RefObject<any> | undefined;
30
- removeFormRef: (id: string) => void;
31
- getFormRefsByStateKey: (
32
- stateKey: string
33
- ) => Map<string, React.RefObject<any>>;
34
- };
35
-
36
- export const formRefStore = create<FormRefStoreState>((set, get) => ({
37
- formRefs: new Map(),
38
-
39
- registerFormRef: (id, ref) =>
40
- set((state) => {
41
- const newRefs = new Map(state.formRefs);
42
- newRefs.set(id, ref);
43
- return { formRefs: newRefs };
44
- }),
45
-
46
- getFormRef: (id) => get().formRefs.get(id),
47
-
48
- removeFormRef: (id) =>
49
- set((state) => {
50
- const newRefs = new Map(state.formRefs);
51
- newRefs.delete(id);
52
- return { formRefs: newRefs };
53
- }),
54
-
55
- getFormRefsByStateKey: (stateKey) => {
56
- const allRefs = get().formRefs;
57
- const stateKeyPrefix = stateKey + '.';
58
- const filteredRefs = new Map();
59
-
60
- allRefs.forEach((ref, id) => {
61
- if (id.startsWith(stateKeyPrefix) || id === stateKey) {
62
- filteredRefs.set(id, ref);
63
- }
64
- });
65
-
66
- return filteredRefs;
67
- },
68
- }));
69
28
  export type ComponentsType = {
70
29
  components?: Map<
71
30
  string,
@@ -99,9 +58,81 @@ export type ValidationState = {
99
58
  lastValidated?: number;
100
59
  validatedValue?: any;
101
60
  };
102
-
61
+ export type SchemaTypeInfo = {
62
+ type:
63
+ | 'string'
64
+ | 'number'
65
+ | 'boolean'
66
+ | 'array'
67
+ | 'object'
68
+ | 'date'
69
+ | 'unknown';
70
+ schema: any; // Store the actual Zod schema object
71
+ source: 'sync' | 'zod4' | 'zod3' | 'runtime' | 'default';
72
+ default: any;
73
+ nullable?: boolean;
74
+ optional?: boolean;
75
+ };
76
+ export type ClientActivityState = {
77
+ // ALL elements currently mounted for this path
78
+ elements: Map<
79
+ string,
80
+ {
81
+ // componentId -> element info
82
+ domRef: React.RefObject<HTMLElement>;
83
+ elementType:
84
+ | 'input'
85
+ | 'textarea'
86
+ | 'select'
87
+ | 'checkbox'
88
+ | 'radio'
89
+ | 'range'
90
+ | 'file'
91
+ | 'custom';
92
+ inputType?: string; // For input elements: 'text', 'number', 'date', etc.
93
+ mountedAt: number;
94
+
95
+ // Current activity for THIS specific element
96
+ currentActivity?: {
97
+ type:
98
+ | 'focus'
99
+ | 'blur'
100
+ | 'input'
101
+ | 'select'
102
+ | 'hover'
103
+ | 'scroll'
104
+ | 'cursor';
105
+ startTime: number;
106
+ details?: {
107
+ value?: any;
108
+ previousValue?: any;
109
+ inputLength?: number;
110
+ changeType?: 'keyboard' | 'paste' | 'drop' | 'select' | 'clear';
111
+ selectionStart?: number;
112
+ selectionEnd?: number;
113
+ selectedText?: string;
114
+ cursorPosition?: number;
115
+ scrollTop?: number;
116
+ scrollLeft?: number;
117
+ isComposing?: boolean;
118
+ keystrokeCount?: number;
119
+ key?: string;
120
+ optionCount?: number;
121
+ selectedIndex?: number;
122
+ checked?: boolean;
123
+ arrayOperation?: 'insert' | 'remove' | 'reorder';
124
+ arrayIndex?: number;
125
+ arrayLength?: number;
126
+ };
127
+ };
128
+ }
129
+ >;
130
+ };
131
+ // Update ShadowMetadata to include typeInfo
103
132
  export type ShadowMetadata = {
133
+ value?: any;
104
134
  id?: string;
135
+ typeInfo?: SchemaTypeInfo;
105
136
  stateSource?: 'default' | 'server' | 'localStorage';
106
137
  lastServerSync?: number;
107
138
  isDirty?: boolean;
@@ -115,26 +146,14 @@ export type ShadowMetadata = {
115
146
  syncInfo?: { status: string };
116
147
  validation?: ValidationState;
117
148
  features?: {
118
- syncEnabled: boolean;
119
- validationEnabled: boolean;
120
149
  localStorageEnabled: boolean;
121
150
  };
122
- lastUpdated?: number;
123
151
  signals?: Array<{
124
152
  instanceId: string;
125
153
  parentId: string;
126
154
  position: number;
127
155
  effect?: string;
128
156
  }>;
129
- mapWrappers?: Array<{
130
- instanceId: string;
131
- path: string[];
132
- componentId: string;
133
- meta?: any;
134
- mapFn: (setter: any, index: number, arraySetter: any) => ReactNode;
135
- containerRef: HTMLDivElement | null;
136
- rebuildStateShape: any;
137
- }>;
138
157
  transformCaches?: Map<
139
158
  string,
140
159
  {
@@ -144,6 +163,7 @@ export type ShadowMetadata = {
144
163
  }
145
164
  >;
146
165
  pathComponents?: Set<string>;
166
+
147
167
  streams?: Map<
148
168
  string,
149
169
  {
@@ -151,21 +171,40 @@ export type ShadowMetadata = {
151
171
  flushTimer: NodeJS.Timeout | null;
152
172
  }
153
173
  >;
174
+ pluginMetaData?: Map<string, Record<string, any>>;
175
+ // formRef?: React.RefObject<any>;
176
+ // focusedElement?: { path: string[]; ref: React.RefObject<any> } | null;
177
+ clientActivityState?: ClientActivityState;
154
178
  } & ComponentsType;
155
179
 
156
180
  type ShadowNode = {
157
- value?: any;
158
181
  _meta?: ShadowMetadata;
159
182
  [key: string]: any;
160
183
  };
161
184
 
162
185
  export type CogsGlobalState = {
186
+ getPluginMetaDataMap: (
187
+ key: string,
188
+ path: string[]
189
+ ) => Map<string, Record<string, any>> | undefined;
190
+ setPluginMetaData: (
191
+ key: string,
192
+ path: string[],
193
+ pluginName: string,
194
+ data: Record<string, any>
195
+ ) => void;
196
+ removePluginMetaData: (
197
+ key: string,
198
+ path: string[],
199
+ pluginName: string
200
+ ) => void;
163
201
  setTransformCache: (
164
202
  key: string,
165
203
  path: string[],
166
204
  cacheKey: string,
167
205
  cacheData: any
168
206
  ) => void;
207
+ initializeAndMergeShadowState: (key: string, initialState: any) => void;
169
208
  initializeShadowState: (key: string, initialState: any) => void;
170
209
  getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
171
210
  getShadowMetadata: (
@@ -190,15 +229,15 @@ export type CogsGlobalState = {
190
229
  addItemsToArrayNode: (
191
230
  key: string,
192
231
  arrayPath: string[],
193
- newItems: any,
194
- newKeys: string[]
232
+ newItems: any
195
233
  ) => void;
196
234
  insertShadowArrayElement: (
197
235
  key: string,
198
236
  arrayPath: string[],
199
237
  newItem: any,
200
- index?: number
201
- ) => void;
238
+ index?: number,
239
+ itemId?: string
240
+ ) => string;
202
241
  removeShadowArrayElement: (key: string, itemPath: string[]) => void;
203
242
  registerComponent: (
204
243
  stateKey: string,
@@ -255,51 +294,452 @@ export type CogsGlobalState = {
255
294
  setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
256
295
  getSyncInfo: (key: string) => SyncInfo | null;
257
296
  };
297
+ function getTypeFromZodSchema(
298
+ schema: any,
299
+ source: 'zod4' | 'zod3' | 'sync' = 'zod4'
300
+ ): SchemaTypeInfo | null {
301
+ if (!schema) return null;
302
+
303
+ let current = schema;
304
+ let isNullable = false;
305
+ let isOptional = false;
306
+ let defaultValue: any = undefined;
307
+ let hasDefault = false;
308
+
309
+ // This loop will now correctly navigate through any wrappers AND unions.
310
+ for (let i = 0; i < 20; i++) {
311
+ // Added a safety break for complex schemas
312
+ const def = current?.def || current?._def;
313
+ if (!def) break;
314
+
315
+ const typeIdentifier = def.typeName || def.type || current._type;
316
+
317
+ // --- START: THE CRITICAL FIX FOR ZodUnion ---
318
+ if (typeIdentifier === 'ZodUnion' || typeIdentifier === 'union') {
319
+ if (def.options && def.options.length > 0) {
320
+ current = def.options[0]; // Proceed by analyzing the FIRST option of the union
321
+ continue; // Restart the loop with the new schema
322
+ } else {
323
+ break; // Union with no options, cannot determine type
324
+ }
325
+ }
326
+ // --- END: THE CRITICAL FIX ---
327
+
328
+ if (typeIdentifier === 'ZodOptional' || typeIdentifier === 'optional') {
329
+ isOptional = true;
330
+ } else if (
331
+ typeIdentifier === 'ZodNullable' ||
332
+ typeIdentifier === 'nullable'
333
+ ) {
334
+ isNullable = true;
335
+ } else if (
336
+ typeIdentifier === 'ZodDefault' ||
337
+ typeIdentifier === 'default'
338
+ ) {
339
+ hasDefault = true;
340
+ defaultValue =
341
+ typeof def.defaultValue === 'function'
342
+ ? def.defaultValue()
343
+ : def.defaultValue;
344
+ } else if (
345
+ typeIdentifier !== 'ZodEffects' &&
346
+ typeIdentifier !== 'effects'
347
+ ) {
348
+ // This is not a wrapper we need to unwrap further, so we can exit the loop.
349
+ break;
350
+ }
258
351
 
259
- export function buildShadowNode(value: any): ShadowNode {
260
- if (value === null || typeof value !== 'object') {
261
- return { value };
352
+ const nextSchema = def.innerType || def.schema || current._inner;
353
+ if (!nextSchema || nextSchema === current) {
354
+ break; // Reached the end or a recursive schema
355
+ }
356
+ current = nextSchema;
262
357
  }
263
358
 
264
- if (Array.isArray(value)) {
265
- const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } };
266
- const idKeys: string[] = [];
359
+ const baseSchema = current;
360
+ const baseDef = baseSchema?.def || baseSchema?._def;
361
+ const baseType = baseDef?.typeName || baseDef?.type || baseSchema?._type;
362
+
363
+ if (baseType === 'ZodNumber' || baseType === 'number') {
364
+ return {
365
+ type: 'number',
366
+ schema: schema,
367
+ source,
368
+ default: hasDefault ? defaultValue : 0,
369
+ nullable: isNullable,
370
+ optional: isOptional,
371
+ };
372
+ }
373
+ if (baseType === 'ZodString' || baseType === 'string') {
374
+ return {
375
+ type: 'string',
376
+ schema: schema,
377
+ source,
378
+ default: hasDefault ? defaultValue : '',
379
+ nullable: isNullable,
380
+ optional: isOptional,
381
+ };
382
+ }
383
+ if (baseType === 'ZodBoolean' || baseType === 'boolean') {
384
+ return {
385
+ type: 'boolean',
386
+ schema: schema,
387
+ source,
388
+ default: hasDefault ? defaultValue : false,
389
+ nullable: isNullable,
390
+ optional: isOptional,
391
+ };
392
+ }
393
+ if (baseType === 'ZodArray' || baseType === 'array') {
394
+ return {
395
+ type: 'array',
396
+ schema: schema,
397
+ source,
398
+ default: hasDefault ? defaultValue : [],
399
+ nullable: isNullable,
400
+ optional: isOptional,
401
+ };
402
+ }
403
+ if (baseType === 'ZodObject' || baseType === 'object') {
404
+ return {
405
+ type: 'object',
406
+ schema: schema,
407
+ source,
408
+ default: hasDefault ? defaultValue : {},
409
+ nullable: isNullable,
410
+ optional: isOptional,
411
+ };
412
+ }
413
+ if (baseType === 'ZodDate' || baseType === 'date') {
414
+ return {
415
+ type: 'date',
416
+ schema: schema,
417
+ source,
418
+ default: hasDefault ? defaultValue : new Date(),
419
+ nullable: isNullable,
420
+ optional: isOptional,
421
+ };
422
+ }
423
+
424
+ return null;
425
+ }
426
+
427
+ // Helper to get type info from runtime value
428
+ function getTypeFromValue(value: any): SchemaTypeInfo {
429
+ if (value === null) {
430
+ return {
431
+ type: 'unknown',
432
+ schema: null,
433
+ source: 'default',
434
+ default: null,
435
+ nullable: true,
436
+ };
437
+ }
438
+
439
+ if (value === undefined) {
440
+ return {
441
+ type: 'unknown',
442
+ schema: null,
443
+ source: 'default',
444
+ default: undefined,
445
+ optional: true,
446
+ };
447
+ }
448
+
449
+ const valueType = typeof value;
450
+
451
+ if (valueType === 'number') {
452
+ return { type: 'number', schema: null, source: 'runtime', default: value };
453
+ } else if (valueType === 'string') {
454
+ return { type: 'string', schema: null, source: 'runtime', default: value };
455
+ } else if (valueType === 'boolean') {
456
+ return { type: 'boolean', schema: null, source: 'runtime', default: value };
457
+ } else if (Array.isArray(value)) {
458
+ return { type: 'array', schema: null, source: 'runtime', default: [] };
459
+ } else if (value instanceof Date) {
460
+ return { type: 'date', schema: null, source: 'runtime', default: value };
461
+ } else if (valueType === 'object') {
462
+ return { type: 'object', schema: null, source: 'runtime', default: {} };
463
+ }
464
+
465
+ return { type: 'unknown', schema: null, source: 'runtime', default: value };
466
+ }
467
+ type BuildContext = {
468
+ stateKey: string;
469
+ path: string[];
470
+ schemas: {
471
+ sync?: any;
472
+ zodV4?: any;
473
+ zodV3?: any;
474
+ };
475
+ };
267
476
 
268
- value.forEach((item) => {
269
- const itemId = `id:${generateId()}`;
270
- arrayNode[itemId] = buildShadowNode(item);
271
- idKeys.push(itemId);
477
+ export function buildShadowNode(
478
+ stateKey: string,
479
+ value: any,
480
+ context?: BuildContext
481
+ ): ShadowNode {
482
+ // Handle null/undefined/primitives (This part is already correct)
483
+ if (value === null || value === undefined || typeof value !== 'object') {
484
+ const node: ShadowNode = { _meta: { value } };
485
+ node._meta!.typeInfo = getTypeInfoForPath(value, context);
486
+ return node;
487
+ }
488
+
489
+ // Handle arrays
490
+ if (Array.isArray(value)) {
491
+ // 1. Create the node for the array.
492
+ const node: ShadowNode = { _meta: { arrayKeys: [] } };
493
+
494
+ // 2. Get the type info for the array itself ONCE, right at the start.
495
+ node._meta!.typeInfo = getTypeInfoForPath(value, context);
496
+
497
+ // 3. THEN, recursively process the children.
498
+ value.forEach((item, index) => {
499
+ const itemId = generateId(stateKey);
500
+ const itemContext = context
501
+ ? {
502
+ ...context,
503
+ path: [...context.path, index.toString()],
504
+ }
505
+ : undefined;
506
+
507
+ node[itemId] = buildShadowNode(stateKey, item, itemContext);
508
+ node._meta!.arrayKeys!.push(itemId);
272
509
  });
273
510
 
274
- arrayNode._meta!.arrayKeys = idKeys;
275
- return arrayNode;
511
+ return node;
276
512
  }
277
513
 
514
+ // Handle objects
278
515
  if (value.constructor === Object) {
279
- const objectNode: ShadowNode = { _meta: {} };
516
+ // 1. Create the node for the object.
517
+ const node: ShadowNode = { _meta: {} };
518
+
519
+ // 2. Get the type info for the object itself ONCE, right at the start.
520
+ node._meta!.typeInfo = getTypeInfoForPath(value, context);
521
+
522
+ // 3. THEN, recursively process the children.
280
523
  for (const key in value) {
281
524
  if (Object.prototype.hasOwnProperty.call(value, key)) {
282
- objectNode[key] = buildShadowNode(value[key]);
525
+ const propContext = context
526
+ ? {
527
+ ...context,
528
+ path: [...context.path, key],
529
+ }
530
+ : undefined;
531
+
532
+ node[key] = buildShadowNode(stateKey, value[key], propContext);
283
533
  }
284
534
  }
285
- return objectNode;
535
+
536
+ return node;
286
537
  }
287
538
 
288
- return { value };
539
+ // Fallback for other object types (Date, class instances, etc.)
540
+ return {
541
+ _meta: {
542
+ value: value,
543
+ typeInfo: getTypeFromValue(value),
544
+ },
545
+ };
289
546
  }
290
- // store.ts - Replace the shadow store methods with mutable versions
291
- // store.ts - Replace the shadow store methods with mutable versions
292
547
 
293
- // Module-level mutable store
294
- const shadowStateStore = new Map<string, ShadowNode>();
295
- let globalCounter = 0;
548
+ // Helper function to get type info (extracted for clarity)
549
+ function getTypeInfoForPath(
550
+ value: any,
551
+ context?: BuildContext
552
+ ): SchemaTypeInfo {
553
+ if (context) {
554
+ // Try to get schema-based type info
555
+ let typeInfo: SchemaTypeInfo | null = null;
556
+
557
+ if (context.schemas.zodV4) {
558
+ const schema =
559
+ context.path.length === 0
560
+ ? context.schemas.zodV4
561
+ : getSchemaAtPath(context.schemas.zodV4, context.path);
562
+ if (schema) {
563
+ typeInfo = getTypeFromZodSchema(schema, 'zod4');
564
+ }
565
+ }
566
+
567
+ if (!typeInfo && context.schemas.zodV3) {
568
+ const schema =
569
+ context.path.length === 0
570
+ ? context.schemas.zodV3
571
+ : getSchemaAtPath(context.schemas.zodV3, context.path);
572
+ if (schema) {
573
+ typeInfo = getTypeFromZodSchema(schema, 'zod3');
574
+ }
575
+ }
576
+
577
+ if (!typeInfo && context.schemas.sync?.[context.stateKey]) {
578
+ typeInfo = getTypeFromValue(value);
579
+ typeInfo.source = 'sync';
580
+ }
296
581
 
297
- export function generateId(prefix = 'id'): string {
298
- return `${prefix}:${(globalCounter++).toString(36)}`;
582
+ if (typeInfo) return typeInfo;
583
+ }
584
+
585
+ return getTypeFromValue(value);
586
+ }
587
+
588
+ export function updateShadowTypeInfo(
589
+ stateKey: string,
590
+ rootSchema: any,
591
+ source: 'zod4' | 'zod3'
592
+ ) {
593
+ const rootNode =
594
+ shadowStateStore.get(stateKey) || shadowStateStore.get(`[${stateKey}`);
595
+ if (!rootNode) return;
596
+
597
+ function updateNodeTypeInfo(node: any, path: string[]) {
598
+ if (!node || typeof node !== 'object') return;
599
+ const fieldSchema = getSchemaAtPath(rootSchema, path);
600
+
601
+ if (fieldSchema) {
602
+ const typeInfo = getTypeFromZodSchema(fieldSchema, source);
603
+ if (typeInfo) {
604
+ if (!node._meta) node._meta = {};
605
+ node._meta.typeInfo = {
606
+ ...typeInfo,
607
+ schema: fieldSchema,
608
+ };
609
+ }
610
+ }
611
+
612
+ // Recursively update children
613
+ if (node._meta?.arrayKeys) {
614
+ node._meta.arrayKeys.forEach((itemKey: string) => {
615
+ if (node[itemKey]) {
616
+ updateNodeTypeInfo(node[itemKey], [...path, '0']); // Use index 0 for array item schema
617
+ }
618
+ });
619
+ } else if (!node._meta?.hasOwnProperty('value')) {
620
+ // It's an object - update each property
621
+ Object.keys(node).forEach((key) => {
622
+ if (key !== '_meta') {
623
+ updateNodeTypeInfo(node[key], [...path, key]);
624
+ }
625
+ });
626
+ }
627
+ }
628
+
629
+ updateNodeTypeInfo(rootNode, []);
630
+ }
631
+
632
+ /**
633
+ * Reliably unwraps a Zod schema to its core type, handling modifiers
634
+ * from both Zod v3 and modern Zod.
635
+ */
636
+ function unwrapSchema(schema: any): any {
637
+ let current = schema;
638
+ while (current) {
639
+ // Version-agnostic way to get the definition object
640
+ const def = current.def || current._def;
641
+
642
+ // VITAL FIX: Check for `def.type` (like in your log), `def.typeName` (modern Zod), and `_type` (zod v3)
643
+ const typeIdentifier = def?.typeName || def?.type || current._type;
644
+
645
+ if (
646
+ typeIdentifier === 'ZodOptional' ||
647
+ typeIdentifier === 'optional' ||
648
+ typeIdentifier === 'ZodNullable' ||
649
+ typeIdentifier === 'nullable' ||
650
+ typeIdentifier === 'ZodDefault' ||
651
+ typeIdentifier === 'default' ||
652
+ typeIdentifier === 'ZodEffects' ||
653
+ typeIdentifier === 'effects'
654
+ ) {
655
+ // Get the inner schema, supporting multiple internal structures
656
+ current =
657
+ def.innerType || def.schema || current._inner || current.unwrap?.();
658
+ } else {
659
+ break; // Reached the base schema
660
+ }
661
+ }
662
+ return current;
299
663
  }
300
664
 
665
+ /**
666
+ * Helper function to get a nested schema at a specific path,
667
+ * correctly handling both Zod v3 and modern Zod internals.
668
+ */
669
+ function getSchemaAtPath(schema: any, path: string[]): any {
670
+ if (!schema) return null;
671
+ if (path.length === 0) return schema;
672
+
673
+ let currentSchema = schema;
674
+
675
+ for (const segment of path) {
676
+ const containerSchema = unwrapSchema(currentSchema);
677
+ if (!containerSchema) return null;
678
+
679
+ const def = containerSchema.def || containerSchema._def;
680
+
681
+ // VITAL FIX: Check for `def.type` as you discovered.
682
+ const typeIdentifier = def?.typeName || def?.type || containerSchema._type;
683
+
684
+ if (typeIdentifier === 'ZodObject' || typeIdentifier === 'object') {
685
+ // VITAL FIX: Check for `shape` inside `def` first, then on the schema itself.
686
+ const shape =
687
+ def?.shape || containerSchema.shape || containerSchema._shape;
688
+ currentSchema = shape?.[segment];
689
+ } else if (typeIdentifier === 'ZodArray' || typeIdentifier === 'array') {
690
+ // For arrays, the next schema is always the element's schema.
691
+ currentSchema = containerSchema.element || def?.type;
692
+ } else {
693
+ return null; // Not a container, cannot traverse deeper.
694
+ }
695
+
696
+ if (!currentSchema) {
697
+ return null; // Path segment does not exist in the schema.
698
+ }
699
+ }
700
+
701
+ return currentSchema;
702
+ }
703
+ export const shadowStateStore = new Map<string, ShadowNode>();
704
+ let globalCounter = 0;
705
+ const instanceId = Date.now().toString(36);
706
+
707
+ export function generateId(stateKey: string): string {
708
+ const prefix = 'local';
709
+
710
+ return `id:${prefix}_${instanceId}_${(globalCounter++).toString(36)}`;
711
+ }
301
712
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
302
- // Remove shadowStateStore from Zustand state
713
+ getPluginMetaDataMap: (
714
+ key: string,
715
+ path: string[]
716
+ ): Map<string, Record<string, any>> | undefined => {
717
+ const metadata = get().getShadowMetadata(key, path);
718
+ return metadata?.pluginMetaData;
719
+ },
720
+
721
+ setPluginMetaData: (
722
+ key: string,
723
+ path: string[], // ADD THIS PARAMETER
724
+ pluginName: string,
725
+ data: Record<string, any>
726
+ ) => {
727
+ const metadata = get().getShadowMetadata(key, path) || {}; // Use the path!
728
+ const pluginMetaData = new Map(metadata.pluginMetaData || []);
729
+ const existingData = pluginMetaData.get(pluginName) || {};
730
+ pluginMetaData.set(pluginName, { ...existingData, ...data });
731
+ get().setShadowMetadata(key, path, { ...metadata, pluginMetaData });
732
+ get().notifyPathSubscribers([key, ...path].join('.'), {
733
+ type: 'METADATA_UPDATE',
734
+ });
735
+ },
736
+ removePluginMetaData: (key: string, path: string[], pluginName: string) => {
737
+ const metadata = get().getShadowMetadata(key, path);
738
+ if (!metadata?.pluginMetaData) return;
739
+ const pluginMetaData = new Map(metadata.pluginMetaData);
740
+ pluginMetaData.delete(pluginName);
741
+ get().setShadowMetadata(key, path, { ...metadata, pluginMetaData });
742
+ },
303
743
 
304
744
  setTransformCache: (
305
745
  key: string,
@@ -316,7 +756,151 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
316
756
  transformCaches: metadata.transformCaches,
317
757
  });
318
758
  },
759
+ // Replace your entire `initializeAndMergeShadowState` function with this one.
760
+
761
+ initializeAndMergeShadowState: (key: string, shadowState: any) => {
762
+ const isArrayState = shadowState?._meta?.arrayKeys !== undefined;
763
+ const storageKey = isArrayState ? `[${key}` : key;
764
+
765
+ const existingRoot =
766
+ shadowStateStore.get(storageKey) ||
767
+ shadowStateStore.get(key) ||
768
+ shadowStateStore.get(`[${key}`);
769
+
770
+ // --- THIS LOGIC IS RESTORED ---
771
+ // This is vital for preserving component registrations and other top-level
772
+ // metadata across a full merge/replace, which is why removing it was a mistake.
773
+ let preservedMetadata: Partial<ShadowMetadata> = {};
774
+ if (existingRoot?._meta) {
775
+ const {
776
+ components,
777
+ features,
778
+ lastServerSync,
779
+ stateSource,
780
+ baseServerState,
781
+ pathComponents,
782
+ signals,
783
+ validation,
784
+ } = existingRoot._meta;
785
+
786
+ if (components) preservedMetadata.components = components;
787
+ if (features) preservedMetadata.features = features;
788
+ if (lastServerSync) preservedMetadata.lastServerSync = lastServerSync;
789
+ if (stateSource) preservedMetadata.stateSource = stateSource;
790
+ if (baseServerState) preservedMetadata.baseServerState = baseServerState;
791
+ if (pathComponents) preservedMetadata.pathComponents = pathComponents;
792
+ if (signals) preservedMetadata.signals = signals;
793
+ if (validation) preservedMetadata.validation = validation;
794
+ }
795
+ function deepMergeShadowNodes(target: ShadowNode, source: ShadowNode) {
796
+ // --- START: CORRECTED, MORE ROBUST METADATA MERGE ---
797
+ if (source._meta || target._meta) {
798
+ const existingMeta = target._meta || {};
799
+ const sourceMeta = source._meta || {};
800
+
801
+ // Combine metadata, letting the source overwrite simple, top-level properties.
802
+ const newMeta = { ...existingMeta, ...sourceMeta };
803
+
804
+ // CRITICAL FIX: Now, explicitly check and preserve the complex, valuable
805
+ // objects from the existing state if the incoming source state doesn't have
806
+ // an equally good or better version.
807
+
808
+ // 1. Preserve rich TypeInfo (with a schema) over a simple runtime one.
809
+ if (existingMeta.typeInfo?.schema && !sourceMeta.typeInfo?.schema) {
810
+ newMeta.typeInfo = existingMeta.typeInfo;
811
+ }
812
+
813
+ // 2. Preserve the existing validation state, which is computed and stored on the target.
814
+ // A source built from a plain object will never have this.
815
+ if (existingMeta.validation && !sourceMeta.validation) {
816
+ newMeta.validation = existingMeta.validation;
817
+ }
818
+
819
+ // 3. Preserve component registrations, which only exist on the live target state.
820
+ if (existingMeta.components) {
821
+ newMeta.components = existingMeta.components;
822
+ }
823
+
824
+ target._meta = newMeta;
825
+ }
826
+ // --- END: CORRECTED METADATA MERGE ---
827
+
828
+ // 2. Handle the node's data (primitive, array, or object).
829
+ if (source._meta?.hasOwnProperty('value')) {
830
+ // Source is a primitive. Clear any old child properties from target.
831
+ for (const key in target) {
832
+ if (key !== '_meta') delete target[key];
833
+ }
834
+ return; // Done with this branch
835
+ }
836
+
837
+ // Synchronize the data structure based on the source.
838
+ const sourceKeys = new Set(
839
+ Object.keys(source).filter((k) => k !== '_meta')
840
+ );
841
+ const targetKeys = new Set(
842
+ Object.keys(target).filter((k) => k !== '_meta')
843
+ );
844
+
845
+ // Delete keys that are in the target but no longer in the source.
846
+ for (const key of targetKeys) {
847
+ if (!sourceKeys.has(key)) {
848
+ delete target[key];
849
+ }
850
+ }
319
851
 
852
+ // Recursively merge or add keys from the source.
853
+ for (const key of sourceKeys) {
854
+ const sourceValue = source[key];
855
+ const targetValue = target[key];
856
+ if (
857
+ targetValue &&
858
+ typeof targetValue === 'object' &&
859
+ sourceValue &&
860
+ typeof sourceValue === 'object'
861
+ ) {
862
+ deepMergeShadowNodes(targetValue, sourceValue); // Recurse for objects
863
+ } else {
864
+ target[key] = sourceValue; // Add new or replace primitive/node
865
+ }
866
+ }
867
+ }
868
+ // --- THIS IS YOUR ORIGINAL, CORRECT MAIN LOGIC ---
869
+ if (existingRoot) {
870
+ // Merge the new shadow state into the existing one
871
+ deepMergeShadowNodes(existingRoot, shadowState);
872
+ // Restore preserved metadata
873
+ if (!existingRoot._meta) existingRoot._meta = {};
874
+ Object.assign(existingRoot._meta, preservedMetadata);
875
+ shadowStateStore.set(storageKey, existingRoot);
876
+ } else {
877
+ // The logic for when no state exists yet
878
+ if (preservedMetadata && Object.keys(preservedMetadata).length > 0) {
879
+ if (!shadowState._meta) shadowState._meta = {};
880
+ Object.assign(shadowState._meta, preservedMetadata);
881
+ }
882
+ shadowStateStore.set(storageKey, shadowState);
883
+ }
884
+
885
+ // As your logs show, this part works. It runs AFTER the merge to apply schemas.
886
+ const options = get().getInitialOptions(key);
887
+ const hasSchema =
888
+ options?.validation?.zodSchemaV4 || options?.validation?.zodSchemaV3;
889
+ if (hasSchema) {
890
+ if (options.validation?.zodSchemaV4) {
891
+ updateShadowTypeInfo(key, options.validation.zodSchemaV4, 'zod4');
892
+ } else if (options.validation?.zodSchemaV3) {
893
+ updateShadowTypeInfo(key, options.validation.zodSchemaV3, 'zod3');
894
+ }
895
+ }
896
+
897
+ // Cleanup logic is restored
898
+ if (storageKey === key) {
899
+ shadowStateStore.delete(`[${key}`);
900
+ } else {
901
+ shadowStateStore.delete(key);
902
+ }
903
+ },
320
904
  initializeShadowState: (key: string, initialState: any) => {
321
905
  const existingRoot =
322
906
  shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
@@ -340,14 +924,29 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
340
924
  shadowStateStore.delete(key);
341
925
  shadowStateStore.delete(`[${key}`);
342
926
 
343
- const newRoot = buildShadowNode(initialState);
927
+ // Get all available schemas for this state
928
+ const options = get().getInitialOptions(key);
929
+ const syncSchemas = get().getInitialOptions('__syncSchemas');
930
+
931
+ const context: BuildContext = {
932
+ stateKey: key,
933
+ path: [],
934
+ schemas: {
935
+ sync: syncSchemas,
936
+ zodV4: options?.validation?.zodSchemaV4,
937
+ zodV3: options?.validation?.zodSchemaV3,
938
+ },
939
+ };
940
+
941
+ // Build with context so type info is stored
942
+ const newRoot = buildShadowNode(key, initialState, context);
943
+
344
944
  if (!newRoot._meta) newRoot._meta = {};
345
945
  Object.assign(newRoot._meta, preservedMetadata);
346
946
 
347
947
  const storageKey = Array.isArray(initialState) ? `[${key}` : key;
348
948
  shadowStateStore.set(storageKey, newRoot);
349
949
  },
350
-
351
950
  getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
352
951
  let current: any =
353
952
  shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
@@ -398,59 +997,75 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
398
997
  if (!current._meta) {
399
998
  current._meta = {};
400
999
  }
1000
+
401
1001
  Object.assign(current._meta, newMetadata);
402
1002
  },
1003
+ getShadowValue: (key: string, path: string[], validArrayIds?: string[]) => {
1004
+ const startNode = get().getShadowNode(key, path);
403
1005
 
404
- getShadowValue: (
405
- key: string,
406
- path: string[],
407
- validArrayIds?: string[],
408
- log?: boolean
409
- ) => {
410
- const node = get().getShadowNode(key, path);
1006
+ // If the path is invalid or leads nowhere, return undefined immediately.
1007
+ if (!startNode) {
1008
+ return undefined;
1009
+ }
411
1010
 
412
- if (node === null || node === undefined) return undefined;
1011
+ // --- High-Performance Iterative Materializer ---
413
1012
 
414
- const nodeKeys = Object.keys(node);
1013
+ // A single root object to hold the final, materialized result.
1014
+ const rootResult: any = {};
415
1015
 
416
- const isPrimitiveWrapper =
417
- Object.prototype.hasOwnProperty.call(node, 'value') &&
418
- nodeKeys.every((k) => k === 'value' || k === '_meta');
1016
+ // Stack to manage the traversal without recursion.
1017
+ // Each item is [shadowNode, parentObjectInResult, keyToSetOnParent]
1018
+ const stack: [ShadowNode, any, string | number][] = [
1019
+ [startNode, rootResult, 'final'],
1020
+ ];
419
1021
 
420
- if (isPrimitiveWrapper) {
421
- return node.value;
422
- }
1022
+ while (stack.length > 0) {
1023
+ const [currentNode, parentResult, resultKey] = stack.pop()!;
423
1024
 
424
- const isArrayNode =
425
- node._meta &&
426
- Object.prototype.hasOwnProperty.call(node._meta, 'arrayKeys');
427
- if (isArrayNode) {
428
- const keysToIterate =
429
- validArrayIds !== undefined && validArrayIds.length > 0
430
- ? validArrayIds
431
- : node._meta!.arrayKeys!;
1025
+ // 1. Handle primitive values
1026
+ if (currentNode._meta?.hasOwnProperty('value')) {
1027
+ parentResult[resultKey] = currentNode._meta.value;
1028
+ continue; // Done with this branch
1029
+ }
432
1030
 
433
- return keysToIterate.map((itemKey: string) =>
434
- get().getShadowValue(key, [...path, itemKey])
435
- );
436
- }
1031
+ // 2. Handle arrays
1032
+ if (currentNode._meta?.arrayKeys) {
1033
+ const keysToIterate = validArrayIds || currentNode._meta.arrayKeys;
1034
+ const newArray: any[] = [];
1035
+ parentResult[resultKey] = newArray;
1036
+
1037
+ // Push children onto the stack in reverse order to process them from 0 to N
1038
+ for (let i = keysToIterate.length - 1; i >= 0; i--) {
1039
+ const itemKey = keysToIterate[i]!;
1040
+ if (currentNode[itemKey]) {
1041
+ // The child's result will be placed at index `i` in `newArray`
1042
+ stack.push([currentNode[itemKey], newArray, i]);
1043
+ }
1044
+ }
1045
+ continue; // Done with this branch
1046
+ }
1047
+
1048
+ // 3. Handle objects
1049
+ const newObject: any = {};
1050
+ parentResult[resultKey] = newObject;
437
1051
 
438
- const result: any = {};
439
- for (const propKey of nodeKeys) {
440
- if (propKey !== '_meta' && !propKey.startsWith('id:')) {
441
- result[propKey] = get().getShadowValue(key, [...path, propKey]);
1052
+ const objectKeys = Object.keys(currentNode);
1053
+ // Push children onto the stack (order doesn't matter for objects)
1054
+ for (const propKey of objectKeys) {
1055
+ if (propKey !== '_meta') {
1056
+ // The child's result will be set as a property on `newObject`
1057
+ stack.push([currentNode[propKey], newObject, propKey]);
1058
+ }
442
1059
  }
443
1060
  }
444
- return result;
445
- },
446
1061
 
1062
+ return rootResult.final;
1063
+ },
447
1064
  updateShadowAtPath: (key, path, newValue) => {
448
- // NO MORE set() wrapper - direct mutation!
449
1065
  const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
450
1066
  let root = shadowStateStore.get(rootKey);
451
1067
  if (!root) return;
452
1068
 
453
- // Navigate to parent without cloning
454
1069
  let parentNode = root;
455
1070
  for (let i = 0; i < path.length - 1; i++) {
456
1071
  if (!parentNode[path[i]!]) {
@@ -458,72 +1073,141 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
458
1073
  }
459
1074
  parentNode = parentNode[path[i]!];
460
1075
  }
461
-
462
1076
  const targetNode =
463
1077
  path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
464
1078
 
465
- if (!targetNode) {
466
- parentNode[path[path.length - 1]!] = buildShadowNode(newValue);
467
- get().notifyPathSubscribers([key, ...path].join('.'), {
468
- type: 'UPDATE',
469
- newValue,
470
- });
471
- return;
472
- }
473
-
474
- function intelligentMerge(nodeToUpdate: any, plainValue: any) {
1079
+ // This function is now defined inside to close over 'key' and 'path' for context
1080
+ function intelligentMerge(
1081
+ nodeToUpdate: any,
1082
+ plainValue: any,
1083
+ currentPath: string[]
1084
+ ) {
1085
+ // 1. Handle primitives (but NOT arrays)
475
1086
  if (
476
1087
  typeof plainValue !== 'object' ||
477
1088
  plainValue === null ||
478
- Array.isArray(plainValue)
1089
+ plainValue instanceof Date
479
1090
  ) {
480
- const oldMeta = nodeToUpdate._meta;
481
- // Clear existing properties
482
- for (const key in nodeToUpdate) {
483
- if (key !== '_meta') delete nodeToUpdate[key];
1091
+ const oldMeta = nodeToUpdate._meta || {};
1092
+ // Clear all child properties
1093
+ for (const prop in nodeToUpdate) {
1094
+ if (prop !== '_meta') delete nodeToUpdate[prop];
484
1095
  }
485
- const newNode = buildShadowNode(plainValue);
486
- Object.assign(nodeToUpdate, newNode);
487
- if (oldMeta) {
488
- nodeToUpdate._meta = { ...oldMeta, ...(nodeToUpdate._meta || {}) };
1096
+ // Set the new primitive value, preserving metadata
1097
+ nodeToUpdate._meta = { ...oldMeta, value: plainValue };
1098
+ return;
1099
+ }
1100
+
1101
+ // 2. Handle Arrays INTELLIGENTLY
1102
+ if (Array.isArray(plainValue)) {
1103
+ // Ensure the target is a shadow array node
1104
+ if (!nodeToUpdate._meta) nodeToUpdate._meta = {};
1105
+ if (!nodeToUpdate._meta.arrayKeys) nodeToUpdate._meta.arrayKeys = [];
1106
+
1107
+ const existingKeys = nodeToUpdate._meta.arrayKeys;
1108
+ const newValues = plainValue;
1109
+
1110
+ const updatedKeys: string[] = [];
1111
+
1112
+ // Merge existing items and add new items
1113
+ for (let i = 0; i < newValues.length; i++) {
1114
+ const newItemValue = newValues[i]!;
1115
+ if (i < existingKeys.length) {
1116
+ // Merge into existing item, preserving its key and metadata
1117
+ const existingKey = existingKeys[i]!;
1118
+ intelligentMerge(nodeToUpdate[existingKey], newItemValue, [
1119
+ ...currentPath,
1120
+ existingKey,
1121
+ ]);
1122
+ updatedKeys.push(existingKey);
1123
+ } else {
1124
+ // Add a new item
1125
+ const newItemId = generateId(key);
1126
+ const options = get().getInitialOptions(key);
1127
+ // Build the new node WITH proper context to get schema info
1128
+ const itemContext: BuildContext = {
1129
+ stateKey: key,
1130
+ path: [...currentPath, '0'], // Use '0' for array element schema lookup
1131
+ schemas: {
1132
+ zodV4: options?.validation?.zodSchemaV4,
1133
+ zodV3: options?.validation?.zodSchemaV3,
1134
+ },
1135
+ };
1136
+ nodeToUpdate[newItemId] = buildShadowNode(
1137
+ key,
1138
+ newItemValue,
1139
+ itemContext
1140
+ );
1141
+ updatedKeys.push(newItemId);
1142
+ }
1143
+ }
1144
+
1145
+ // Remove deleted items
1146
+ if (existingKeys.length > newValues.length) {
1147
+ const keysToDelete = existingKeys.slice(newValues.length);
1148
+ keysToDelete.forEach((keyToDelete: string) => {
1149
+ delete nodeToUpdate[keyToDelete];
1150
+ });
489
1151
  }
1152
+
1153
+ // Update the keys array to reflect the new state
1154
+ nodeToUpdate._meta.arrayKeys = updatedKeys;
490
1155
  return;
491
1156
  }
492
1157
 
1158
+ // 3. Handle Objects
493
1159
  const plainValueKeys = new Set(Object.keys(plainValue));
1160
+ if (nodeToUpdate._meta?.hasOwnProperty('value')) {
1161
+ // transitioning from primitive to object, clear the value
1162
+ delete nodeToUpdate._meta.value;
1163
+ }
494
1164
 
495
1165
  for (const propKey of plainValueKeys) {
496
1166
  const childValue = plainValue[propKey];
497
1167
  if (nodeToUpdate[propKey]) {
498
- intelligentMerge(nodeToUpdate[propKey], childValue);
1168
+ intelligentMerge(nodeToUpdate[propKey], childValue, [
1169
+ ...currentPath,
1170
+ propKey,
1171
+ ]);
499
1172
  } else {
500
- nodeToUpdate[propKey] = buildShadowNode(childValue);
1173
+ const options = get().getInitialOptions(key);
1174
+ const itemContext: BuildContext = {
1175
+ stateKey: key,
1176
+ path: [...currentPath, propKey],
1177
+ schemas: {
1178
+ zodV4: options?.validation?.zodSchemaV4,
1179
+ zodV3: options?.validation?.zodSchemaV3,
1180
+ },
1181
+ };
1182
+ nodeToUpdate[propKey] = buildShadowNode(key, childValue, itemContext);
501
1183
  }
502
1184
  }
503
1185
 
1186
+ // Delete keys that no longer exist
504
1187
  for (const nodeKey in nodeToUpdate) {
505
1188
  if (
506
1189
  nodeKey === '_meta' ||
507
1190
  !Object.prototype.hasOwnProperty.call(nodeToUpdate, nodeKey)
508
1191
  )
509
1192
  continue;
510
-
511
1193
  if (!plainValueKeys.has(nodeKey)) {
512
1194
  delete nodeToUpdate[nodeKey];
513
1195
  }
514
1196
  }
515
1197
  }
516
1198
 
517
- intelligentMerge(targetNode, newValue);
1199
+ if (!targetNode) {
1200
+ parentNode[path[path.length - 1]!] = buildShadowNode(key, newValue); // Build fresh if no target
1201
+ } else {
1202
+ intelligentMerge(targetNode, newValue, path); // Use the new intelligent merge
1203
+ }
518
1204
 
519
1205
  get().notifyPathSubscribers([key, ...path].join('.'), {
520
1206
  type: 'UPDATE',
521
1207
  newValue,
522
1208
  });
523
1209
  },
524
-
525
- addItemsToArrayNode: (key, arrayPath, newItems, newKeys) => {
526
- // Direct mutation - no cloning!
1210
+ addItemsToArrayNode: (key, arrayPath, newItems) => {
527
1211
  const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
528
1212
  let root = shadowStateStore.get(rootKey);
529
1213
  if (!root) {
@@ -531,7 +1215,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
531
1215
  return;
532
1216
  }
533
1217
 
534
- // Navigate without cloning
535
1218
  let current = root;
536
1219
  for (const segment of arrayPath) {
537
1220
  if (!current[segment]) {
@@ -540,23 +1223,20 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
540
1223
  current = current[segment];
541
1224
  }
542
1225
 
543
- // Mutate directly
544
1226
  Object.assign(current, newItems);
545
- if (!current._meta) current._meta = {};
546
- current._meta.arrayKeys = newKeys; // Direct assignment!
547
1227
  },
548
-
549
- insertShadowArrayElement: (key, arrayPath, newItem, index) => {
1228
+ insertShadowArrayElement: (key, arrayPath, newItem, index, itemId) => {
550
1229
  const arrayNode = get().getShadowNode(key, arrayPath);
551
1230
  if (!arrayNode?._meta?.arrayKeys) {
552
- console.error(
1231
+ throw new Error(
553
1232
  `Array not found at path: ${[key, ...arrayPath].join('.')}`
554
1233
  );
555
- return;
556
1234
  }
1235
+ console.log('OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO');
1236
+ const newItemId = itemId || `${generateId(key)}`;
557
1237
 
558
- const newItemId = `id:${generateId()}`;
559
- const itemsToAdd = { [newItemId]: buildShadowNode(newItem) };
1238
+ // BUILD AND ADD the node directly - no need for addItemsToArrayNode
1239
+ arrayNode[newItemId] = buildShadowNode(key, newItem);
560
1240
 
561
1241
  // Mutate the array directly
562
1242
  const currentKeys = arrayNode._meta.arrayKeys;
@@ -566,13 +1246,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
566
1246
  : currentKeys.length;
567
1247
 
568
1248
  if (insertionPoint >= currentKeys.length) {
569
- currentKeys.push(newItemId); // O(1)
1249
+ currentKeys.push(newItemId);
570
1250
  } else {
571
- currentKeys.splice(insertionPoint, 0, newItemId); // O(n) only for middle
1251
+ currentKeys.splice(insertionPoint, 0, newItemId);
572
1252
  }
573
1253
 
574
- // Pass the mutated array
575
- get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
1254
+ // Skip addItemsToArrayNode entirely - we already did everything it does!
576
1255
 
577
1256
  const arrayKey = [key, ...arrayPath].join('.');
578
1257
  get().notifyPathSubscribers(arrayKey, {
@@ -581,6 +1260,8 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
581
1260
  itemKey: `${arrayKey}.${newItemId}`,
582
1261
  index: insertionPoint,
583
1262
  });
1263
+
1264
+ return newItemId;
584
1265
  },
585
1266
 
586
1267
  insertManyShadowArrayElements: (key, arrayPath, newItems, index) => {
@@ -596,16 +1277,16 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
596
1277
  return;
597
1278
  }
598
1279
 
599
- const itemsToAdd: Record<string, any> = {};
600
1280
  const newIds: string[] = [];
601
1281
 
1282
+ // Build and add items directly
602
1283
  newItems.forEach((item) => {
603
- const newItemId = `id:${generateId()}`;
1284
+ const newItemId = `${generateId(key)}`;
604
1285
  newIds.push(newItemId);
605
- itemsToAdd[newItemId] = buildShadowNode(item);
1286
+ arrayNode[newItemId] = buildShadowNode(key, item); // ADD DIRECTLY!
606
1287
  });
607
1288
 
608
- // Mutate directly
1289
+ // Mutate the keys array
609
1290
  const currentKeys = arrayNode._meta.arrayKeys;
610
1291
  const insertionPoint =
611
1292
  index !== undefined && index >= 0 && index <= currentKeys.length
@@ -613,12 +1294,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
613
1294
  : currentKeys.length;
614
1295
 
615
1296
  if (insertionPoint >= currentKeys.length) {
616
- currentKeys.push(...newIds); // O(k) where k is items being added
1297
+ currentKeys.push(...newIds);
617
1298
  } else {
618
- currentKeys.splice(insertionPoint, 0, ...newIds); // O(n + k)
1299
+ currentKeys.splice(insertionPoint, 0, ...newIds);
619
1300
  }
620
1301
 
621
- get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
1302
+ // NO addItemsToArrayNode call needed!
622
1303
 
623
1304
  const arrayKey = [key, ...arrayPath].join('.');
624
1305
  get().notifyPathSubscribers(arrayKey, {
@@ -709,25 +1390,36 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
709
1390
  },
710
1391
 
711
1392
  markAsDirty: (key, path, options = { bubble: true }) => {
712
- const setDirtyOnPath = (pathToMark: string[]) => {
713
- const node = get().getShadowNode(key, pathToMark);
714
- if (node?._meta?.isDirty) {
715
- return true;
716
- }
717
- get().setShadowMetadata(key, pathToMark, { isDirty: true });
718
- return false;
719
- };
1393
+ // Start at the root node once.
1394
+ let rootNode = get().getShadowNode(key, []);
1395
+ if (!rootNode) return;
720
1396
 
721
- setDirtyOnPath(path);
1397
+ // Navigate to the target node once.
1398
+ let currentNode = rootNode;
1399
+ for (const segment of path) {
1400
+ currentNode = currentNode[segment];
1401
+ if (!currentNode) return; // Path doesn't exist, nothing to mark.
1402
+ }
722
1403
 
723
- if (options.bubble) {
724
- let parentPath = [...path];
725
- while (parentPath.length > 0) {
726
- parentPath.pop();
727
- if (setDirtyOnPath(parentPath)) {
728
- break;
729
- }
1404
+ // Mark the target node as dirty.
1405
+ if (!currentNode._meta) currentNode._meta = {};
1406
+ currentNode._meta.isDirty = true;
1407
+
1408
+ // If bubbling is disabled, we are done.
1409
+ if (!options.bubble) return;
1410
+
1411
+ // Efficiently bubble up using the path segments.
1412
+ let parentNode = rootNode;
1413
+ for (let i = 0; i < path.length; i++) {
1414
+ // The current node in the loop is the parent of the next one.
1415
+ if (parentNode._meta?.isDirty) {
1416
+ // Optimization: If a parent is already dirty, all of its ancestors are too.
1417
+ // We can stop bubbling immediately.
1418
+ return;
730
1419
  }
1420
+ if (!parentNode._meta) parentNode._meta = {};
1421
+ parentNode._meta.isDirty = true;
1422
+ parentNode = parentNode[path[i]!];
731
1423
  }
732
1424
  },
733
1425