cogsbox-state 0.5.471 → 0.5.472

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/store.ts CHANGED
@@ -99,9 +99,28 @@ export type ValidationState = {
99
99
  lastValidated?: number;
100
100
  validatedValue?: any;
101
101
  };
102
+ export type TypeInfo = {
103
+ type:
104
+ | 'string'
105
+ | 'number'
106
+ | 'boolean'
107
+ | 'array'
108
+ | 'object'
109
+ | 'date'
110
+ | 'unknown';
111
+ schema: any; // Store the actual Zod schema object
112
+ source: 'sync' | 'zod4' | 'zod3' | 'runtime' | 'default';
113
+ default: any;
114
+ nullable?: boolean;
115
+ optional?: boolean;
116
+ };
102
117
 
118
+ // Update ShadowMetadata to include typeInfo
103
119
  export type ShadowMetadata = {
120
+ value?: any;
121
+ syncArrayIdPrefix?: string;
104
122
  id?: string;
123
+ typeInfo?: TypeInfo;
105
124
  stateSource?: 'default' | 'server' | 'localStorage';
106
125
  lastServerSync?: number;
107
126
  isDirty?: boolean;
@@ -119,22 +138,13 @@ export type ShadowMetadata = {
119
138
  validationEnabled: boolean;
120
139
  localStorageEnabled: boolean;
121
140
  };
122
- lastUpdated?: number;
123
141
  signals?: Array<{
124
142
  instanceId: string;
125
143
  parentId: string;
126
144
  position: number;
127
145
  effect?: string;
128
146
  }>;
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
- }>;
147
+
138
148
  transformCaches?: Map<
139
149
  string,
140
150
  {
@@ -154,7 +164,6 @@ export type ShadowMetadata = {
154
164
  } & ComponentsType;
155
165
 
156
166
  type ShadowNode = {
157
- value?: any;
158
167
  _meta?: ShadowMetadata;
159
168
  [key: string]: any;
160
169
  };
@@ -198,7 +207,7 @@ export type CogsGlobalState = {
198
207
  arrayPath: string[],
199
208
  newItem: any,
200
209
  index?: number
201
- ) => void;
210
+ ) => string;
202
211
  removeShadowArrayElement: (key: string, itemPath: string[]) => void;
203
212
  registerComponent: (
204
213
  stateKey: string,
@@ -256,37 +265,447 @@ export type CogsGlobalState = {
256
265
  getSyncInfo: (key: string) => SyncInfo | null;
257
266
  };
258
267
 
259
- export function buildShadowNode(value: any): ShadowNode {
260
- if (value === null || typeof value !== 'object') {
261
- return { value };
268
+ function getTypeFromZodSchema(
269
+ schema: any,
270
+ source: 'zod4' | 'zod3' | 'sync' = 'zod4'
271
+ ): TypeInfo | null {
272
+ if (!schema) return null;
273
+
274
+ let baseSchema = schema;
275
+ let isNullable = false;
276
+ let isOptional = false;
277
+ let defaultValue: any = undefined;
278
+ let hasDefault = false;
279
+
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;
301
+ } else {
302
+ // We've reached the base type
303
+ break;
304
+ }
305
+ }
306
+
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
+ };
364
+ }
365
+ }
366
+
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
+ }
389
+ }
390
+
391
+ baseSchema = current;
392
+
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
+ }
448
+ }
449
+
450
+ return null;
451
+ }
452
+
453
+ // Helper to get type info from runtime value
454
+ function getTypeFromValue(value: any): TypeInfo {
455
+ if (value === null) {
456
+ return {
457
+ type: 'unknown',
458
+ schema: null,
459
+ source: 'default',
460
+ default: null,
461
+ nullable: true,
462
+ };
463
+ }
464
+
465
+ if (value === undefined) {
466
+ return {
467
+ type: 'unknown',
468
+ schema: null,
469
+ source: 'default',
470
+ default: undefined,
471
+ optional: true,
472
+ };
473
+ }
474
+
475
+ const valueType = typeof value;
476
+
477
+ if (valueType === 'number') {
478
+ return { type: 'number', schema: null, source: 'runtime', default: value };
479
+ } else if (valueType === 'string') {
480
+ return { type: 'string', schema: null, source: 'runtime', default: value };
481
+ } else if (valueType === 'boolean') {
482
+ return { type: 'boolean', schema: null, source: 'runtime', default: value };
483
+ } else if (Array.isArray(value)) {
484
+ return { type: 'array', schema: null, source: 'runtime', default: [] };
485
+ } else if (value instanceof Date) {
486
+ return { type: 'date', schema: null, source: 'runtime', default: value };
487
+ } else if (valueType === 'object') {
488
+ return { type: 'object', schema: null, source: 'runtime', default: {} };
489
+ }
490
+
491
+ return { type: 'unknown', schema: null, source: 'runtime', default: value };
492
+ }
493
+ type BuildContext = {
494
+ stateKey: string;
495
+ path: string[];
496
+ schemas: {
497
+ sync?: any;
498
+ zodV4?: any;
499
+ zodV3?: any;
500
+ };
501
+ };
502
+ // Update buildShadowNode to use the new schema storage
503
+ export function buildShadowNode(
504
+ stateKey: string,
505
+ value: any,
506
+ context?: BuildContext
507
+ ): ShadowNode {
508
+ // For primitive values
509
+ 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
+
617
+ return node;
262
618
  }
263
619
 
620
+ // For arrays
264
621
  if (Array.isArray(value)) {
265
622
  const arrayNode: ShadowNode = { _meta: { arrayKeys: [] } };
266
623
  const idKeys: string[] = [];
267
624
 
268
- value.forEach((item) => {
269
- const itemId = `id:${generateId()}`;
270
- arrayNode[itemId] = buildShadowNode(item);
625
+ value.forEach((item, index) => {
626
+ const itemId = `${generateId(stateKey)}`;
627
+ // Pass context down for array items
628
+ const itemContext = context
629
+ ? {
630
+ ...context,
631
+ path: [...context.path, index.toString()],
632
+ }
633
+ : undefined;
634
+ arrayNode[itemId] = buildShadowNode(stateKey, item, itemContext);
271
635
  idKeys.push(itemId);
272
636
  });
273
637
 
274
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
+ }
654
+
655
+ arrayNode._meta!.typeInfo = {
656
+ type: 'array',
657
+ schema: arraySchema,
658
+ source: arraySchema ? 'zod4' : 'runtime',
659
+ default: [],
660
+ };
661
+ }
275
662
  return arrayNode;
276
663
  }
277
664
 
665
+ // For objects
278
666
  if (value.constructor === Object) {
279
667
  const objectNode: ShadowNode = { _meta: {} };
280
668
  for (const key in value) {
281
669
  if (Object.prototype.hasOwnProperty.call(value, key)) {
282
- objectNode[key] = buildShadowNode(value[key]);
670
+ // Pass context down for object properties
671
+ const propContext = context
672
+ ? {
673
+ ...context,
674
+ path: [...context.path, key],
675
+ }
676
+ : undefined;
677
+ objectNode[key] = buildShadowNode(stateKey, value[key], propContext);
283
678
  }
284
679
  }
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;
694
+ }
695
+
696
+ objectNode._meta!.typeInfo = {
697
+ type: 'object',
698
+ schema: objectSchema,
699
+ source: objectSchema ? 'zod4' : 'runtime',
700
+ default: {},
701
+ };
702
+ }
285
703
  return objectNode;
286
704
  }
287
705
 
288
706
  return { value };
289
707
  }
708
+
290
709
  // store.ts - Replace the shadow store methods with mutable versions
291
710
  // store.ts - Replace the shadow store methods with mutable versions
292
711
 
@@ -294,10 +713,11 @@ export function buildShadowNode(value: any): ShadowNode {
294
713
  const shadowStateStore = new Map<string, ShadowNode>();
295
714
  let globalCounter = 0;
296
715
 
297
- export function generateId(prefix = 'id'): string {
298
- return `${prefix}:${(globalCounter++).toString(36)}`;
716
+ 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)}`;
299
720
  }
300
-
301
721
  export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
302
722
  // Remove shadowStateStore from Zustand state
303
723
 
@@ -340,14 +760,29 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
340
760
  shadowStateStore.delete(key);
341
761
  shadowStateStore.delete(`[${key}`);
342
762
 
343
- const newRoot = buildShadowNode(initialState);
763
+ // Get all available schemas for this state
764
+ const options = get().getInitialOptions(key);
765
+ const syncSchemas = get().getInitialOptions('__syncSchemas');
766
+
767
+ const context: BuildContext = {
768
+ stateKey: key,
769
+ path: [],
770
+ schemas: {
771
+ sync: syncSchemas,
772
+ zodV4: options?.validation?.zodSchemaV4,
773
+ zodV3: options?.validation?.zodSchemaV3,
774
+ },
775
+ };
776
+
777
+ // Build with context so type info is stored
778
+ const newRoot = buildShadowNode(key, initialState, context);
779
+
344
780
  if (!newRoot._meta) newRoot._meta = {};
345
781
  Object.assign(newRoot._meta, preservedMetadata);
346
782
 
347
783
  const storageKey = Array.isArray(initialState) ? `[${key}` : key;
348
784
  shadowStateStore.set(storageKey, newRoot);
349
785
  },
350
-
351
786
  getShadowNode: (key: string, path: string[]): ShadowNode | undefined => {
352
787
  let current: any =
353
788
  shadowStateStore.get(key) || shadowStateStore.get(`[${key}`);
@@ -413,12 +848,13 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
413
848
 
414
849
  const nodeKeys = Object.keys(node);
415
850
 
416
- const isPrimitiveWrapper =
417
- Object.prototype.hasOwnProperty.call(node, 'value') &&
418
- nodeKeys.every((k) => k === 'value' || k === '_meta');
419
-
420
- if (isPrimitiveWrapper) {
421
- return node.value;
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;
422
858
  }
423
859
 
424
860
  const isArrayNode =
@@ -463,7 +899,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
463
899
  path.length === 0 ? parentNode : parentNode[path[path.length - 1]!];
464
900
 
465
901
  if (!targetNode) {
466
- parentNode[path[path.length - 1]!] = buildShadowNode(newValue);
902
+ parentNode[path[path.length - 1]!] = buildShadowNode(key, newValue);
467
903
  get().notifyPathSubscribers([key, ...path].join('.'), {
468
904
  type: 'UPDATE',
469
905
  newValue,
@@ -482,7 +918,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
482
918
  for (const key in nodeToUpdate) {
483
919
  if (key !== '_meta') delete nodeToUpdate[key];
484
920
  }
485
- const newNode = buildShadowNode(plainValue);
921
+ const newNode = buildShadowNode(key, plainValue);
486
922
  Object.assign(nodeToUpdate, newNode);
487
923
  if (oldMeta) {
488
924
  nodeToUpdate._meta = { ...oldMeta, ...(nodeToUpdate._meta || {}) };
@@ -497,7 +933,7 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
497
933
  if (nodeToUpdate[propKey]) {
498
934
  intelligentMerge(nodeToUpdate[propKey], childValue);
499
935
  } else {
500
- nodeToUpdate[propKey] = buildShadowNode(childValue);
936
+ nodeToUpdate[propKey] = buildShadowNode(key, childValue);
501
937
  }
502
938
  }
503
939
 
@@ -549,14 +985,13 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
549
985
  insertShadowArrayElement: (key, arrayPath, newItem, index) => {
550
986
  const arrayNode = get().getShadowNode(key, arrayPath);
551
987
  if (!arrayNode?._meta?.arrayKeys) {
552
- console.error(
988
+ throw new Error(
553
989
  `Array not found at path: ${[key, ...arrayPath].join('.')}`
554
990
  );
555
- return;
556
991
  }
557
992
 
558
- const newItemId = `id:${generateId()}`;
559
- const itemsToAdd = { [newItemId]: buildShadowNode(newItem) };
993
+ const newItemId = `${generateId(key)}`;
994
+ const itemsToAdd = { [newItemId]: buildShadowNode(key, newItem) };
560
995
 
561
996
  // Mutate the array directly
562
997
  const currentKeys = arrayNode._meta.arrayKeys;
@@ -581,6 +1016,8 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
581
1016
  itemKey: `${arrayKey}.${newItemId}`,
582
1017
  index: insertionPoint,
583
1018
  });
1019
+
1020
+ return newItemId;
584
1021
  },
585
1022
 
586
1023
  insertManyShadowArrayElements: (key, arrayPath, newItems, index) => {
@@ -600,9 +1037,9 @@ export const getGlobalStore = create<CogsGlobalState>((set, get) => ({
600
1037
  const newIds: string[] = [];
601
1038
 
602
1039
  newItems.forEach((item) => {
603
- const newItemId = `id:${generateId()}`;
1040
+ const newItemId = `${generateId(key)}`;
604
1041
  newIds.push(newItemId);
605
- itemsToAdd[newItemId] = buildShadowNode(item);
1042
+ itemsToAdd[newItemId] = buildShadowNode(key, item);
606
1043
  });
607
1044
 
608
1045
  // Mutate directly