cogsbox-state 0.5.472 → 0.5.474

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 +48 -18
  2. package/dist/CogsState.d.ts +98 -82
  3. package/dist/CogsState.d.ts.map +1 -1
  4. package/dist/CogsState.jsx +1030 -960
  5. package/dist/CogsState.jsx.map +1 -1
  6. package/dist/Components.d.ts.map +1 -1
  7. package/dist/Components.jsx +299 -219
  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 +122 -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 +81 -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 +1323 -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 +50 -15
  26. package/dist/store.d.ts.map +1 -1
  27. package/dist/store.js +509 -470
  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 +13 -3
  38. package/src/CogsState.tsx +657 -457
  39. package/src/Components.tsx +291 -194
  40. package/src/PluginRunner.tsx +203 -0
  41. package/src/index.ts +2 -0
  42. package/src/pluginStore.ts +176 -0
  43. package/src/plugins.ts +544 -0
  44. package/src/store.ts +748 -493
  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,7 +58,7 @@ export type ValidationState = {
99
58
  lastValidated?: number;
100
59
  validatedValue?: any;
101
60
  };
102
- export type TypeInfo = {
61
+ export type SchemaTypeInfo = {
103
62
  type:
104
63
  | 'string'
105
64
  | 'number'
@@ -114,13 +73,66 @@ export type TypeInfo = {
114
73
  nullable?: boolean;
115
74
  optional?: boolean;
116
75
  };
117
-
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
+ };
118
131
  // Update ShadowMetadata to include typeInfo
119
132
  export type ShadowMetadata = {
120
133
  value?: any;
121
- syncArrayIdPrefix?: string;
122
134
  id?: string;
123
- typeInfo?: TypeInfo;
135
+ typeInfo?: SchemaTypeInfo;
124
136
  stateSource?: 'default' | 'server' | 'localStorage';
125
137
  lastServerSync?: number;
126
138
  isDirty?: boolean;
@@ -134,8 +146,6 @@ export type ShadowMetadata = {
134
146
  syncInfo?: { status: string };
135
147
  validation?: ValidationState;
136
148
  features?: {
137
- syncEnabled: boolean;
138
- validationEnabled: boolean;
139
149
  localStorageEnabled: boolean;
140
150
  };
141
151
  signals?: Array<{
@@ -144,7 +154,6 @@ export type ShadowMetadata = {
144
154
  position: number;
145
155
  effect?: string;
146
156
  }>;
147
-
148
157
  transformCaches?: Map<
149
158
  string,
150
159
  {
@@ -154,6 +163,7 @@ export type ShadowMetadata = {
154
163
  }
155
164
  >;
156
165
  pathComponents?: Set<string>;
166
+
157
167
  streams?: Map<
158
168
  string,
159
169
  {
@@ -161,6 +171,10 @@ export type ShadowMetadata = {
161
171
  flushTimer: NodeJS.Timeout | null;
162
172
  }
163
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;
164
178
  } & ComponentsType;
165
179
 
166
180
  type ShadowNode = {
@@ -169,12 +183,28 @@ type ShadowNode = {
169
183
  };
170
184
 
171
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;
172
201
  setTransformCache: (
173
202
  key: string,
174
203
  path: string[],
175
204
  cacheKey: string,
176
205
  cacheData: any
177
206
  ) => void;
207
+ initializeAndMergeShadowState: (key: string, initialState: any) => void;
178
208
  initializeShadowState: (key: string, initialState: any) => void;
179
209
  getShadowNode: (key: string, path: string[]) => ShadowNode | undefined;
180
210
  getShadowMetadata: (
@@ -199,14 +229,14 @@ export type CogsGlobalState = {
199
229
  addItemsToArrayNode: (
200
230
  key: string,
201
231
  arrayPath: string[],
202
- newItems: any,
203
- newKeys: string[]
232
+ newItems: any
204
233
  ) => void;
205
234
  insertShadowArrayElement: (
206
235
  key: string,
207
236
  arrayPath: string[],
208
237
  newItem: any,
209
- index?: number
238
+ index?: number,
239
+ itemId?: string
210
240
  ) => string;
211
241
  removeShadowArrayElement: (key: string, itemPath: string[]) => void;
212
242
  registerComponent: (
@@ -264,194 +294,138 @@ export type CogsGlobalState = {
264
294
  setSyncInfo: (key: string, syncInfo: SyncInfo) => void;
265
295
  getSyncInfo: (key: string) => SyncInfo | null;
266
296
  };
267
-
268
297
  function getTypeFromZodSchema(
269
298
  schema: any,
270
299
  source: 'zod4' | 'zod3' | 'sync' = 'zod4'
271
- ): TypeInfo | null {
300
+ ): SchemaTypeInfo | null {
272
301
  if (!schema) return null;
273
302
 
274
- let baseSchema = schema;
303
+ let current = schema;
275
304
  let isNullable = false;
276
305
  let isOptional = false;
277
306
  let defaultValue: any = undefined;
278
307
  let hasDefault = false;
279
308
 
280
- // Zod v4 unwrapping
281
- if (schema._def) {
282
- let current = schema;
283
-
284
- // Keep unwrapping until we get to the base type
285
- while (current._def) {
286
- const typeName = current._def.typeName;
287
-
288
- if (typeName === 'ZodOptional') {
289
- isOptional = true;
290
- current = current._def.innerType || current.unwrap();
291
- } else if (typeName === 'ZodNullable') {
292
- isNullable = true;
293
- current = current._def.innerType || current.unwrap();
294
- } else if (typeName === 'ZodDefault') {
295
- hasDefault = true;
296
- defaultValue = current._def.defaultValue();
297
- current = current._def.innerType;
298
- } else if (typeName === 'ZodEffects') {
299
- // Handle .refine(), .transform() etc
300
- current = current._def.schema;
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
301
322
  } else {
302
- // We've reached the base type
303
- break;
323
+ break; // Union with no options, cannot determine type
304
324
  }
305
325
  }
326
+ // --- END: THE CRITICAL FIX ---
306
327
 
307
- baseSchema = current;
308
- const typeName = baseSchema._def?.typeName;
309
-
310
- if (typeName === 'ZodNumber') {
311
- return {
312
- type: 'number',
313
- schema: schema, // Store the original schema with wrappers
314
- source,
315
- default: hasDefault ? defaultValue : 0,
316
- nullable: isNullable,
317
- optional: isOptional,
318
- };
319
- } else if (typeName === 'ZodString') {
320
- return {
321
- type: 'string',
322
- schema: schema,
323
- source,
324
- default: hasDefault ? defaultValue : '',
325
- nullable: isNullable,
326
- optional: isOptional,
327
- };
328
- } else if (typeName === 'ZodBoolean') {
329
- return {
330
- type: 'boolean',
331
- schema: schema,
332
- source,
333
- default: hasDefault ? defaultValue : false,
334
- nullable: isNullable,
335
- optional: isOptional,
336
- };
337
- } else if (typeName === 'ZodArray') {
338
- return {
339
- type: 'array',
340
- schema: schema,
341
- source,
342
- default: hasDefault ? defaultValue : [],
343
- nullable: isNullable,
344
- optional: isOptional,
345
- };
346
- } else if (typeName === 'ZodObject') {
347
- return {
348
- type: 'object',
349
- schema: schema,
350
- source,
351
- default: hasDefault ? defaultValue : {},
352
- nullable: isNullable,
353
- optional: isOptional,
354
- };
355
- } else if (typeName === 'ZodDate') {
356
- return {
357
- type: 'date',
358
- schema: schema,
359
- source,
360
- default: hasDefault ? defaultValue : new Date(),
361
- nullable: isNullable,
362
- optional: isOptional,
363
- };
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;
364
350
  }
365
- }
366
351
 
367
- // Zod v3 unwrapping
368
- if (schema._type) {
369
- let current = schema;
370
-
371
- // Check for wrappers in v3
372
- while (current) {
373
- if (current._type === 'optional') {
374
- isOptional = true;
375
- current = current._def?.innerType || current._inner;
376
- } else if (current._type === 'nullable') {
377
- isNullable = true;
378
- current = current._def?.innerType || current._inner;
379
- } else if (current._def?.defaultValue !== undefined) {
380
- hasDefault = true;
381
- defaultValue =
382
- typeof current._def.defaultValue === 'function'
383
- ? current._def.defaultValue()
384
- : current._def.defaultValue;
385
- break;
386
- } else {
387
- break;
388
- }
352
+ const nextSchema = def.innerType || def.schema || current._inner;
353
+ if (!nextSchema || nextSchema === current) {
354
+ break; // Reached the end or a recursive schema
389
355
  }
356
+ current = nextSchema;
357
+ }
390
358
 
391
- baseSchema = current;
359
+ const baseSchema = current;
360
+ const baseDef = baseSchema?.def || baseSchema?._def;
361
+ const baseType = baseDef?.typeName || baseDef?.type || baseSchema?._type;
392
362
 
393
- if (baseSchema._type === 'number') {
394
- return {
395
- type: 'number',
396
- schema: schema,
397
- source,
398
- default: hasDefault ? defaultValue : 0,
399
- nullable: isNullable,
400
- optional: isOptional,
401
- };
402
- } else if (baseSchema._type === 'string') {
403
- return {
404
- type: 'string',
405
- schema: schema,
406
- source,
407
- default: hasDefault ? defaultValue : '',
408
- nullable: isNullable,
409
- optional: isOptional,
410
- };
411
- } else if (baseSchema._type === 'boolean') {
412
- return {
413
- type: 'boolean',
414
- schema: schema,
415
- source,
416
- default: hasDefault ? defaultValue : false,
417
- nullable: isNullable,
418
- optional: isOptional,
419
- };
420
- } else if (baseSchema._type === 'array') {
421
- return {
422
- type: 'array',
423
- schema: schema,
424
- source,
425
- default: hasDefault ? defaultValue : [],
426
- nullable: isNullable,
427
- optional: isOptional,
428
- };
429
- } else if (baseSchema._type === 'object') {
430
- return {
431
- type: 'object',
432
- schema: schema,
433
- source,
434
- default: hasDefault ? defaultValue : {},
435
- nullable: isNullable,
436
- optional: isOptional,
437
- };
438
- } else if (baseSchema._type === 'date') {
439
- return {
440
- type: 'date',
441
- schema: schema,
442
- source,
443
- default: hasDefault ? defaultValue : new Date(),
444
- nullable: isNullable,
445
- optional: isOptional,
446
- };
447
- }
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
+ };
448
422
  }
449
423
 
450
424
  return null;
451
425
  }
452
426
 
453
427
  // Helper to get type info from runtime value
454
- function getTypeFromValue(value: any): TypeInfo {
428
+ function getTypeFromValue(value: any): SchemaTypeInfo {
455
429
  if (value === null) {
456
430
  return {
457
431
  type: 'unknown',
@@ -499,227 +473,273 @@ type BuildContext = {
499
473
  zodV3?: any;
500
474
  };
501
475
  };
502
- // Update buildShadowNode to use the new schema storage
476
+
503
477
  export function buildShadowNode(
504
478
  stateKey: string,
505
479
  value: any,
506
480
  context?: BuildContext
507
481
  ): ShadowNode {
508
- // For primitive values
482
+ // Handle null/undefined/primitives (This part is already correct)
509
483
  if (value === null || value === undefined || typeof value !== 'object') {
510
- const node: ShadowNode = { _meta: {} };
511
- node._meta!.value = value;
512
- if (context) {
513
- let typeInfo: TypeInfo | null = null;
514
-
515
- // 1. Try to get type from sync schema
516
- if (context.schemas.sync && context.schemas.sync[context.stateKey]) {
517
- const syncEntry = context.schemas.sync[context.stateKey];
518
- if (syncEntry.schemas?.validation) {
519
- // Navigate to the field in the validation schema
520
- let fieldSchema = syncEntry.schemas.validation;
521
- for (const segment of context.path) {
522
- if (fieldSchema?.shape) {
523
- fieldSchema = fieldSchema.shape[segment];
524
- } else if (fieldSchema?._def?.shape) {
525
- fieldSchema = fieldSchema._def.shape()[segment];
526
- }
527
- }
528
-
529
- if (fieldSchema) {
530
- typeInfo = getTypeFromZodSchema(fieldSchema, 'sync');
531
- if (typeInfo) {
532
- // Use the default from sync schema if available
533
- if (syncEntry.schemas.defaults) {
534
- let defaultValue = syncEntry.schemas.defaults;
535
- for (const segment of context.path) {
536
- if (defaultValue && typeof defaultValue === 'object') {
537
- defaultValue = defaultValue[segment];
538
- }
539
- }
540
- if (defaultValue !== undefined) {
541
- typeInfo.default = defaultValue;
542
- // If no value provided and not optional, use the default
543
- if (
544
- (value === undefined || value === null) &&
545
- !typeInfo.optional
546
- ) {
547
- node._meta!.value = defaultValue;
548
- }
549
- }
550
- }
551
- }
552
- }
553
- }
554
- }
555
-
556
- // 2. If no sync schema, try Zod v4
557
- if (!typeInfo && context.schemas.zodV4) {
558
- let fieldSchema = context.schemas.zodV4;
559
- for (const segment of context.path) {
560
- if (fieldSchema?.shape) {
561
- fieldSchema = fieldSchema.shape[segment];
562
- } else if (fieldSchema?._def?.shape) {
563
- fieldSchema = fieldSchema._def.shape()[segment];
564
- }
565
- }
566
-
567
- if (fieldSchema) {
568
- typeInfo = getTypeFromZodSchema(fieldSchema, 'zod4');
569
- if (typeInfo && (value === undefined || value === null)) {
570
- // Only use default if the field is not optional/nullable
571
- if (!typeInfo.optional && !typeInfo.nullable) {
572
- node.value = typeInfo.default;
573
- }
574
- }
575
- }
576
- }
577
-
578
- // 3. If no Zod v4, try Zod v3
579
- if (!typeInfo && context.schemas.zodV3) {
580
- let fieldSchema = context.schemas.zodV3;
581
- for (const segment of context.path) {
582
- if (fieldSchema?.shape) {
583
- fieldSchema = fieldSchema.shape[segment];
584
- } else if (fieldSchema?._shape) {
585
- fieldSchema = fieldSchema._shape[segment];
586
- }
587
- }
588
-
589
- if (fieldSchema) {
590
- typeInfo = getTypeFromZodSchema(fieldSchema, 'zod3');
591
- if (typeInfo && (value === undefined || value === null)) {
592
- // Only use default if the field is not optional/nullable
593
- if (!typeInfo.optional && !typeInfo.nullable) {
594
- node.value = typeInfo.default;
595
- }
596
- }
597
- }
598
- }
599
-
600
- // 4. Fall back to runtime type
601
- if (!typeInfo) {
602
- typeInfo = getTypeFromValue(node._meta!.value);
603
- }
604
-
605
- // Store the type info
606
- if (typeInfo) {
607
- if (!node._meta) node._meta = {};
608
- node._meta.typeInfo = typeInfo;
609
- }
610
- } else {
611
- // No context, just use runtime type
612
- const typeInfo = getTypeFromValue(value);
613
- if (!node._meta) node._meta = {};
614
- node._meta.typeInfo = typeInfo;
615
- }
616
-
484
+ const node: ShadowNode = { _meta: { value } };
485
+ node._meta!.typeInfo = getTypeInfoForPath(value, context);
617
486
  return node;
618
487
  }
619
488
 
620
- // For arrays
489
+ // Handle arrays
621
490
  if (Array.isArray(value)) {
622
- const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } };
623
- const idKeys: string[] = [];
491
+ // 1. Create the node for the array.
492
+ const node: ShadowNode = { _meta: { arrayKeys: [] } };
624
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.
625
498
  value.forEach((item, index) => {
626
- const itemId = `${generateId(stateKey)}`;
627
- // Pass context down for array items
499
+ const itemId = generateId(stateKey);
628
500
  const itemContext = context
629
501
  ? {
630
502
  ...context,
631
503
  path: [...context.path, index.toString()],
632
504
  }
633
505
  : undefined;
634
- arrayNode[itemId] = buildShadowNode(stateKey, item, itemContext);
635
- idKeys.push(itemId);
636
- });
637
506
 
638
- arrayNode._meta!.arrayKeys = idKeys;
639
- if (context) {
640
- // Try to get the array schema
641
- let arraySchema = null;
642
-
643
- if (context.schemas.zodV4) {
644
- let fieldSchema = context.schemas.zodV4;
645
- for (const segment of context.path) {
646
- if (fieldSchema?.shape) {
647
- fieldSchema = fieldSchema.shape[segment];
648
- } else if (fieldSchema?._def?.shape) {
649
- fieldSchema = fieldSchema._def.shape()[segment];
650
- }
651
- }
652
- arraySchema = fieldSchema;
653
- }
507
+ node[itemId] = buildShadowNode(stateKey, item, itemContext);
508
+ node._meta!.arrayKeys!.push(itemId);
509
+ });
654
510
 
655
- arrayNode._meta!.typeInfo = {
656
- type: 'array',
657
- schema: arraySchema,
658
- source: arraySchema ? 'zod4' : 'runtime',
659
- default: [],
660
- };
661
- }
662
- return arrayNode;
511
+ return node;
663
512
  }
664
513
 
665
- // For objects
514
+ // Handle objects
666
515
  if (value.constructor === Object) {
667
- 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.
668
523
  for (const key in value) {
669
524
  if (Object.prototype.hasOwnProperty.call(value, key)) {
670
- // Pass context down for object properties
671
525
  const propContext = context
672
526
  ? {
673
527
  ...context,
674
528
  path: [...context.path, key],
675
529
  }
676
530
  : undefined;
677
- objectNode[key] = buildShadowNode(stateKey, value[key], propContext);
531
+
532
+ node[key] = buildShadowNode(stateKey, value[key], propContext);
678
533
  }
679
534
  }
680
- if (context) {
681
- // Try to get the object schema
682
- let objectSchema = null;
683
-
684
- if (context.schemas.zodV4) {
685
- let fieldSchema = context.schemas.zodV4;
686
- for (const segment of context.path) {
687
- if (fieldSchema?.shape) {
688
- fieldSchema = fieldSchema.shape[segment];
689
- } else if (fieldSchema?._def?.shape) {
690
- fieldSchema = fieldSchema._def.shape()[segment];
691
- }
692
- }
693
- objectSchema = fieldSchema;
535
+
536
+ return node;
537
+ }
538
+
539
+ // Fallback for other object types (Date, class instances, etc.)
540
+ return {
541
+ _meta: {
542
+ value: value,
543
+ typeInfo: getTypeFromValue(value),
544
+ },
545
+ };
546
+ }
547
+
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');
694
564
  }
565
+ }
695
566
 
696
- objectNode._meta!.typeInfo = {
697
- type: 'object',
698
- schema: objectSchema,
699
- source: objectSchema ? 'zod4' : 'runtime',
700
- default: {},
701
- };
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
+ }
702
575
  }
703
- return objectNode;
576
+
577
+ if (!typeInfo && context.schemas.sync?.[context.stateKey]) {
578
+ typeInfo = getTypeFromValue(value);
579
+ typeInfo.source = 'sync';
580
+ }
581
+
582
+ if (typeInfo) return typeInfo;
704
583
  }
705
584
 
706
- return { value };
585
+ return getTypeFromValue(value);
707
586
  }
708
587
 
709
- // store.ts - Replace the shadow store methods with mutable versions
710
- // store.ts - Replace the shadow store methods with mutable versions
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;
711
644
 
712
- // Module-level mutable store
713
- const shadowStateStore = new Map<string, ShadowNode>();
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;
663
+ }
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>();
714
704
  let globalCounter = 0;
705
+ const instanceId = Date.now().toString(36);
715
706
 
716
707
  export function generateId(stateKey: string): string {
717
- const rootMeta = getGlobalStore.getState().getShadowMetadata(stateKey, []);
718
- const prefix = rootMeta?.syncArrayIdPrefix || 'local';
719
- return `id:${prefix}_${(globalCounter++).toString(36)}`;
708
+ const prefix = 'local';
709
+
710
+ return `id:${prefix}_${instanceId}_${(globalCounter++).toString(36)}`;
720
711
  }
721
712
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
722
- // 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
+ },
723
743
 
724
744
  setTransformCache: (
725
745
  key: string,
@@ -736,7 +756,151 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
736
756
  transformCaches: metadata.transformCaches,
737
757
  });
738
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
+ }
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
+ }
739
896
 
897
+ // Cleanup logic is restored
898
+ if (storageKey === key) {
899
+ shadowStateStore.delete(`[${key}`);
900
+ } else {
901
+ shadowStateStore.delete(key);
902
+ }
903
+ },
740
904
  initializeShadowState: (key: string, initialState: any) => {
741
905
  const existingRoot =
742
906
  shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
@@ -833,60 +997,75 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
833
997
  if (!current._meta) {
834
998
  current._meta = {};
835
999
  }
1000
+
836
1001
  Object.assign(current._meta, newMetadata);
837
1002
  },
1003
+ getShadowValue: (key: string, path: string[], validArrayIds?: string[]) => {
1004
+ const startNode = get().getShadowNode(key, path);
838
1005
 
839
- getShadowValue: (
840
- key: string,
841
- path: string[],
842
- validArrayIds?: string[],
843
- log?: boolean
844
- ) => {
845
- 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
+ }
846
1010
 
847
- if (node === null || node === undefined) return undefined;
1011
+ // --- High-Performance Iterative Materializer ---
848
1012
 
849
- const nodeKeys = Object.keys(node);
1013
+ // A single root object to hold the final, materialized result.
1014
+ const rootResult: any = {};
850
1015
 
851
- if (
852
- node._meta &&
853
- Object.prototype.hasOwnProperty.call(node._meta, 'value') &&
854
- nodeKeys.length === 1 &&
855
- nodeKeys[0] === '_meta'
856
- ) {
857
- return node._meta.value;
858
- }
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
+ ];
859
1021
 
860
- const isArrayNode =
861
- node._meta &&
862
- Object.prototype.hasOwnProperty.call(node._meta, 'arrayKeys');
863
- if (isArrayNode) {
864
- const keysToIterate =
865
- validArrayIds !== undefined && validArrayIds.length > 0
866
- ? validArrayIds
867
- : node._meta!.arrayKeys!;
868
-
869
- return keysToIterate.map((itemKey: string) =>
870
- get().getShadowValue(key, [...path, itemKey])
871
- );
872
- }
1022
+ while (stack.length > 0) {
1023
+ const [currentNode, parentResult, resultKey] = stack.pop()!;
1024
+
1025
+ // 1. Handle primitive values
1026
+ if (currentNode._meta?.hasOwnProperty('value')) {
1027
+ parentResult[resultKey] = currentNode._meta.value;
1028
+ continue; // Done with this branch
1029
+ }
1030
+
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;
873
1051
 
874
- const result: any = {};
875
- for (const propKey of nodeKeys) {
876
- if (propKey !== '_meta' && !propKey.startsWith('id:')) {
877
- 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
+ }
878
1059
  }
879
1060
  }
880
- return result;
881
- },
882
1061
 
1062
+ return rootResult.final;
1063
+ },
883
1064
  updateShadowAtPath: (key, path, newValue) => {
884
- // NO MORE set() wrapper - direct mutation!
885
1065
  const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
886
1066
  let root = shadowStateStore.get(rootKey);
887
1067
  if (!root) return;
888
1068
 
889
- // Navigate to parent without cloning
890
1069
  let parentNode = root;
891
1070
  for (let i = 0; i < path.length - 1; i++) {
892
1071
  if (!parentNode[path[i]!]) {
@@ -894,72 +1073,141 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
894
1073
  }
895
1074
  parentNode = parentNode[path[i]!];
896
1075
  }
897
-
898
1076
  const targetNode =
899
1077
  path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
900
1078
 
901
- if (!targetNode) {
902
- parentNode[path[path.length - 1]!] = buildShadowNode(key, newValue);
903
- get().notifyPathSubscribers([key, ...path].join('.'), {
904
- type: 'UPDATE',
905
- newValue,
906
- });
907
- return;
908
- }
909
-
910
- 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)
911
1086
  if (
912
1087
  typeof plainValue !== 'object' ||
913
1088
  plainValue === null ||
914
- Array.isArray(plainValue)
1089
+ plainValue instanceof Date
915
1090
  ) {
916
- const oldMeta = nodeToUpdate._meta;
917
- // Clear existing properties
918
- for (const key in nodeToUpdate) {
919
- 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];
1095
+ }
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
+ }
920
1143
  }
921
- const newNode = buildShadowNode(key, plainValue);
922
- Object.assign(nodeToUpdate, newNode);
923
- if (oldMeta) {
924
- nodeToUpdate._meta = { ...oldMeta, ...(nodeToUpdate._meta || {}) };
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
+ });
925
1151
  }
1152
+
1153
+ // Update the keys array to reflect the new state
1154
+ nodeToUpdate._meta.arrayKeys = updatedKeys;
926
1155
  return;
927
1156
  }
928
1157
 
1158
+ // 3. Handle Objects
929
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
+ }
930
1164
 
931
1165
  for (const propKey of plainValueKeys) {
932
1166
  const childValue = plainValue[propKey];
933
1167
  if (nodeToUpdate[propKey]) {
934
- intelligentMerge(nodeToUpdate[propKey], childValue);
1168
+ intelligentMerge(nodeToUpdate[propKey], childValue, [
1169
+ ...currentPath,
1170
+ propKey,
1171
+ ]);
935
1172
  } else {
936
- nodeToUpdate[propKey] = buildShadowNode(key, 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);
937
1183
  }
938
1184
  }
939
1185
 
1186
+ // Delete keys that no longer exist
940
1187
  for (const nodeKey in nodeToUpdate) {
941
1188
  if (
942
1189
  nodeKey === '_meta' ||
943
1190
  !Object.prototype.hasOwnProperty.call(nodeToUpdate, nodeKey)
944
1191
  )
945
1192
  continue;
946
-
947
1193
  if (!plainValueKeys.has(nodeKey)) {
948
1194
  delete nodeToUpdate[nodeKey];
949
1195
  }
950
1196
  }
951
1197
  }
952
1198
 
953
- 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
+ }
954
1204
 
955
1205
  get().notifyPathSubscribers([key, ...path].join('.'), {
956
1206
  type: 'UPDATE',
957
1207
  newValue,
958
1208
  });
959
1209
  },
960
-
961
- addItemsToArrayNode: (key, arrayPath, newItems, newKeys) => {
962
- // Direct mutation - no cloning!
1210
+ addItemsToArrayNode: (key, arrayPath, newItems) => {
963
1211
  const rootKey = shadowStateStore.has(`[${key}`) ? `[${key}` : key;
964
1212
  let root = shadowStateStore.get(rootKey);
965
1213
  if (!root) {
@@ -967,7 +1215,6 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
967
1215
  return;
968
1216
  }
969
1217
 
970
- // Navigate without cloning
971
1218
  let current = root;
972
1219
  for (const segment of arrayPath) {
973
1220
  if (!current[segment]) {
@@ -976,22 +1223,20 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
976
1223
  current = current[segment];
977
1224
  }
978
1225
 
979
- // Mutate directly
980
1226
  Object.assign(current, newItems);
981
- if (!current._meta) current._meta = {};
982
- current._meta.arrayKeys = newKeys; // Direct assignment!
983
1227
  },
984
-
985
- insertShadowArrayElement: (key, arrayPath, newItem, index) => {
1228
+ insertShadowArrayElement: (key, arrayPath, newItem, index, itemId) => {
986
1229
  const arrayNode = get().getShadowNode(key, arrayPath);
987
1230
  if (!arrayNode?._meta?.arrayKeys) {
988
1231
  throw new Error(
989
1232
  `Array not found at path: ${[key, ...arrayPath].join('.')}`
990
1233
  );
991
1234
  }
1235
+ console.log('OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO');
1236
+ const newItemId = itemId || `${generateId(key)}`;
992
1237
 
993
- const newItemId = `${generateId(key)}`;
994
- const itemsToAdd = { [newItemId]: buildShadowNode(key, newItem) };
1238
+ // BUILD AND ADD the node directly - no need for addItemsToArrayNode
1239
+ arrayNode[newItemId] = buildShadowNode(key, newItem);
995
1240
 
996
1241
  // Mutate the array directly
997
1242
  const currentKeys = arrayNode._meta.arrayKeys;
@@ -1001,13 +1246,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
1001
1246
  : currentKeys.length;
1002
1247
 
1003
1248
  if (insertionPoint >= currentKeys.length) {
1004
- currentKeys.push(newItemId); // O(1)
1249
+ currentKeys.push(newItemId);
1005
1250
  } else {
1006
- currentKeys.splice(insertionPoint, 0, newItemId); // O(n) only for middle
1251
+ currentKeys.splice(insertionPoint, 0, newItemId);
1007
1252
  }
1008
1253
 
1009
- // Pass the mutated array
1010
- get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
1254
+ // Skip addItemsToArrayNode entirely - we already did everything it does!
1011
1255
 
1012
1256
  const arrayKey = [key, ...arrayPath].join('.');
1013
1257
  get().notifyPathSubscribers(arrayKey, {
@@ -1033,16 +1277,16 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
1033
1277
  return;
1034
1278
  }
1035
1279
 
1036
- const itemsToAdd: Record<string, any> = {};
1037
1280
  const newIds: string[] = [];
1038
1281
 
1282
+ // Build and add items directly
1039
1283
  newItems.forEach((item) => {
1040
1284
  const newItemId = `${generateId(key)}`;
1041
1285
  newIds.push(newItemId);
1042
- itemsToAdd[newItemId] = buildShadowNode(key, item);
1286
+ arrayNode[newItemId] = buildShadowNode(key, item); // ADD DIRECTLY!
1043
1287
  });
1044
1288
 
1045
- // Mutate directly
1289
+ // Mutate the keys array
1046
1290
  const currentKeys = arrayNode._meta.arrayKeys;
1047
1291
  const insertionPoint =
1048
1292
  index !== undefined && index >= 0 && index <= currentKeys.length
@@ -1050,12 +1294,12 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
1050
1294
  : currentKeys.length;
1051
1295
 
1052
1296
  if (insertionPoint >= currentKeys.length) {
1053
- currentKeys.push(...newIds); // O(k) where k is items being added
1297
+ currentKeys.push(...newIds);
1054
1298
  } else {
1055
- currentKeys.splice(insertionPoint, 0, ...newIds); // O(n + k)
1299
+ currentKeys.splice(insertionPoint, 0, ...newIds);
1056
1300
  }
1057
1301
 
1058
- get().addItemsToArrayNode(key, arrayPath, itemsToAdd, currentKeys);
1302
+ // NO addItemsToArrayNode call needed!
1059
1303
 
1060
1304
  const arrayKey = [key, ...arrayPath].join('.');
1061
1305
  get().notifyPathSubscribers(arrayKey, {
@@ -1146,25 +1390,36 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
1146
1390
  },
1147
1391
 
1148
1392
  markAsDirty: (key, path, options = { bubble: true }) => {
1149
- const setDirtyOnPath = (pathToMark: string[]) => {
1150
- const node = get().getShadowNode(key, pathToMark);
1151
- if (node?._meta?.isDirty) {
1152
- return true;
1153
- }
1154
- get().setShadowMetadata(key, pathToMark, { isDirty: true });
1155
- return false;
1156
- };
1393
+ // Start at the root node once.
1394
+ let rootNode = get().getShadowNode(key, []);
1395
+ if (!rootNode) return;
1157
1396
 
1158
- 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
+ }
1159
1403
 
1160
- if (options.bubble) {
1161
- let parentPath = [...path];
1162
- while (parentPath.length > 0) {
1163
- parentPath.pop();
1164
- if (setDirtyOnPath(parentPath)) {
1165
- break;
1166
- }
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;
1167
1419
  }
1420
+ if (!parentNode._meta) parentNode._meta = {};
1421
+ parentNode._meta.isDirty = true;
1422
+ parentNode = parentNode[path[i]!];
1168
1423
  }
1169
1424
  },
1170
1425