@voidhash/mimic 0.0.1-alpha.6 → 0.0.1-alpha.7

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.
@@ -6,7 +6,7 @@ import * as ProxyEnvironment from "../ProxyEnvironment";
6
6
  import * as Transform from "../Transform";
7
7
  import type { Primitive, PrimitiveInternal, MaybeUndefined, AnyPrimitive, Validator, InferState, InferProxy, InferSnapshot } from "../Primitive";
8
8
  import { ValidationError } from "../Primitive";
9
- import { runValidators } from "./shared";
9
+ import { runValidators, applyDefaults, StructSetInput } from "./shared";
10
10
 
11
11
 
12
12
  /**
@@ -25,6 +25,16 @@ export type InferStructSnapshot<TFields extends Record<string, AnyPrimitive>> =
25
25
  readonly [K in keyof TFields]: InferSnapshot<TFields[K]>;
26
26
  };
27
27
 
28
+ /**
29
+ * Maps a schema definition to a partial update type.
30
+ * For nested structs, allows recursive partial updates.
31
+ */
32
+ export type StructUpdateValue<TFields extends Record<string, AnyPrimitive>> = {
33
+ readonly [K in keyof TFields]?: TFields[K] extends StructPrimitive<infer F, any>
34
+ ? StructUpdateValue<F> | InferState<TFields[K]>
35
+ : InferState<TFields[K]>;
36
+ };
37
+
28
38
  /**
29
39
  * Maps a schema definition to its proxy type.
30
40
  * Provides nested field access + get()/set()/toSnapshot() methods for the whole struct.
@@ -34,8 +44,10 @@ export type StructProxy<TFields extends Record<string, AnyPrimitive>, TDefined e
34
44
  } & {
35
45
  /** Gets the entire struct value */
36
46
  get(): MaybeUndefined<InferStructState<TFields>, TDefined>;
37
- /** Sets the entire struct value */
38
- set(value: InferStructState<TFields>): void;
47
+ /** Sets the entire struct value (only fields that are required without defaults must be provided) */
48
+ set(value: StructSetInput<TFields>): void;
49
+ /** Updates only the specified fields (partial update, handles nested structs recursively) */
50
+ update(value: StructUpdateValue<TFields>): void;
39
51
  /** Returns a readonly snapshot of the struct for rendering */
40
52
  toSnapshot(): MaybeUndefined<InferStructSnapshot<TFields>, TDefined>;
41
53
  };
@@ -47,12 +59,14 @@ interface StructPrimitiveSchema<TFields extends Record<string, AnyPrimitive>> {
47
59
  readonly validators: readonly Validator<InferStructState<TFields>>[];
48
60
  }
49
61
 
50
- export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefined extends boolean = false>
51
- implements Primitive<InferStructState<TFields>, StructProxy<TFields, TDefined>>
62
+ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefined extends boolean = false, THasDefault extends boolean = false>
63
+ implements Primitive<InferStructState<TFields>, StructProxy<TFields, TDefined>, TDefined, THasDefault>
52
64
  {
53
65
  readonly _tag = "StructPrimitive" as const;
54
66
  readonly _State!: InferStructState<TFields>;
55
67
  readonly _Proxy!: StructProxy<TFields, TDefined>;
68
+ readonly _TDefined!: TDefined;
69
+ readonly _THasDefault!: THasDefault;
56
70
 
57
71
  private readonly _schema: StructPrimitiveSchema<TFields>;
58
72
 
@@ -70,7 +84,7 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
70
84
  }
71
85
 
72
86
  /** Mark this struct as required */
73
- required(): StructPrimitive<TFields, true> {
87
+ required(): StructPrimitive<TFields, true, THasDefault> {
74
88
  return new StructPrimitive({
75
89
  ...this._schema,
76
90
  required: true,
@@ -78,10 +92,12 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
78
92
  }
79
93
 
80
94
  /** Set a default value for this struct */
81
- default(defaultValue: InferStructState<TFields>): StructPrimitive<TFields, true> {
95
+ default(defaultValue: StructSetInput<TFields>): StructPrimitive<TFields, true, true> {
96
+ // Apply defaults to the provided value
97
+ const merged = applyDefaults(this as AnyPrimitive, defaultValue as Partial<InferStructState<TFields>>) as InferStructState<TFields>;
82
98
  return new StructPrimitive({
83
99
  ...this._schema,
84
- defaultValue,
100
+ defaultValue: merged,
85
101
  });
86
102
  }
87
103
 
@@ -91,7 +107,7 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
91
107
  }
92
108
 
93
109
  /** Add a custom validation rule (useful for cross-field validation) */
94
- refine(fn: (value: InferStructState<TFields>) => boolean, message: string): StructPrimitive<TFields, TDefined> {
110
+ refine(fn: (value: InferStructState<TFields>) => boolean, message: string): StructPrimitive<TFields, TDefined, THasDefault> {
95
111
  return new StructPrimitive({
96
112
  ...this._schema,
97
113
  validators: [...this._schema.validators, { validate: fn, message }],
@@ -130,17 +146,47 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
130
146
  return snapshot as InferStructSnapshot<TFields>;
131
147
  };
132
148
 
133
- // Create the base object with get/set/toSnapshot methods
149
+ // Create the base object with get/set/update/toSnapshot methods
134
150
  const base = {
135
151
  get: (): MaybeUndefined<InferStructState<TFields>, TDefined> => {
136
152
  const state = env.getState(operationPath) as InferStructState<TFields> | undefined;
137
153
  return (state ?? defaultValue) as MaybeUndefined<InferStructState<TFields>, TDefined>;
138
154
  },
139
- set: (value: InferStructState<TFields>) => {
155
+ set: (value: StructSetInput<TFields>) => {
156
+ // Apply defaults for missing fields
157
+ const merged = applyDefaults(this as AnyPrimitive, value as Partial<InferStructState<TFields>>) as InferStructState<TFields>;
140
158
  env.addOperation(
141
- Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
159
+ Operation.fromDefinition(operationPath, this._opDefinitions.set, merged)
142
160
  );
143
161
  },
162
+ update: (value: StructUpdateValue<TFields>) => {
163
+ for (const key in value) {
164
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
165
+ const fieldValue = value[key as keyof TFields];
166
+ if (fieldValue === undefined) continue; // Skip undefined values
167
+
168
+ const fieldPrimitive = fields[key as keyof TFields];
169
+ if (!fieldPrimitive) continue; // Skip unknown fields
170
+
171
+ const fieldPath = operationPath.append(key);
172
+ const fieldProxy = fieldPrimitive._internal.createProxy(env, fieldPath);
173
+
174
+ // Check if this is a nested struct and value is a plain object (partial update)
175
+ if (
176
+ fieldPrimitive._tag === "StructPrimitive" &&
177
+ typeof fieldValue === "object" &&
178
+ fieldValue !== null &&
179
+ !Array.isArray(fieldValue)
180
+ ) {
181
+ // Recursively update nested struct
182
+ (fieldProxy as { update: (v: unknown) => void }).update(fieldValue);
183
+ } else {
184
+ // Set the field value directly
185
+ (fieldProxy as { set: (v: unknown) => void }).set(fieldValue);
186
+ }
187
+ }
188
+ }
189
+ },
144
190
  toSnapshot: (): MaybeUndefined<InferStructSnapshot<TFields>, TDefined> => {
145
191
  const snapshot = buildSnapshot();
146
192
  return snapshot as MaybeUndefined<InferStructSnapshot<TFields>, TDefined>;
@@ -150,13 +196,16 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
150
196
  // Use a JavaScript Proxy to intercept field access
151
197
  return new globalThis.Proxy(base as StructProxy<TFields, TDefined>, {
152
198
  get: (target, prop, _receiver) => {
153
- // Return base methods (get, set, toSnapshot)
199
+ // Return base methods (get, set, update, toSnapshot)
154
200
  if (prop === "get") {
155
201
  return target.get;
156
202
  }
157
203
  if (prop === "set") {
158
204
  return target.set;
159
205
  }
206
+ if (prop === "update") {
207
+ return target.update;
208
+ }
160
209
  if (prop === "toSnapshot") {
161
210
  return target.toSnapshot;
162
211
  }
@@ -176,7 +225,7 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
176
225
  return undefined;
177
226
  },
178
227
  has: (_target, prop) => {
179
- if (prop === "get" || prop === "set" || prop === "toSnapshot") return true;
228
+ if (prop === "get" || prop === "set" || prop === "update" || prop === "toSnapshot") return true;
180
229
  if (typeof prop === "string" && prop in fields) return true;
181
230
  return false;
182
231
  },
@@ -343,6 +392,6 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
343
392
  /** Creates a new StructPrimitive with the given fields */
344
393
  export const Struct = <TFields extends Record<string, AnyPrimitive>>(
345
394
  fields: TFields
346
- ): StructPrimitive<TFields, false> =>
395
+ ): StructPrimitive<TFields, false, false> =>
347
396
  new StructPrimitive({ required: false, defaultValue: undefined, fields, validators: [] });
348
397
 
@@ -5,11 +5,11 @@ import * as OperationPath from "../OperationPath";
5
5
  import * as ProxyEnvironment from "../ProxyEnvironment";
6
6
  import * as Transform from "../Transform";
7
7
  import * as FractionalIndex from "../FractionalIndex";
8
- import type { Primitive, PrimitiveInternal, Validator, InferProxy } from "./shared";
9
- import { ValidationError } from "./shared";
8
+ import type { Primitive, PrimitiveInternal, Validator, InferProxy, AnyPrimitive, StructSetInput } from "./shared";
9
+ import { ValidationError, applyDefaults } from "./shared";
10
10
  import { runValidators } from "./shared";
11
11
  import type { AnyTreeNodePrimitive, InferTreeNodeType, InferTreeNodeDataState, InferTreeNodeChildren } from "./TreeNode";
12
- import { InferStructState } from "./Struct";
12
+ import { InferStructState, StructUpdateValue, StructPrimitive } from "./Struct";
13
13
 
14
14
 
15
15
  /**
@@ -105,6 +105,23 @@ export type TreeNodeSnapshot<TNode extends AnyTreeNodePrimitive> = {
105
105
  export type InferTreeSnapshot<T extends TreePrimitive<any>> =
106
106
  T extends TreePrimitive<infer TRoot> ? TreeNodeSnapshot<TRoot> : never;
107
107
 
108
+ /**
109
+ * Helper type to infer the update value type from a TreeNode's data
110
+ */
111
+ export type TreeNodeUpdateValue<TNode extends AnyTreeNodePrimitive> =
112
+ TNode["data"] extends StructPrimitive<infer TFields, any, any>
113
+ ? StructUpdateValue<TFields>
114
+ : never;
115
+
116
+ /**
117
+ * Helper type to infer the input type for node data (with defaults applied).
118
+ * Uses StructSetInput for struct data, making fields with defaults optional.
119
+ */
120
+ export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
121
+ TNode["data"] extends StructPrimitive<infer TFields, any, any>
122
+ ? StructSetInput<TFields>
123
+ : InferTreeNodeDataState<TNode>;
124
+
108
125
  /**
109
126
  * Typed proxy for a specific node type - provides type-safe data access
110
127
  */
@@ -117,6 +134,8 @@ export interface TypedNodeProxy<TNode extends AnyTreeNodePrimitive> {
117
134
  readonly data: InferProxy<TNode["data"]>;
118
135
  /** Get the raw node state */
119
136
  get(): TypedTreeNodeState<TNode>;
137
+ /** Updates only the specified data fields (partial update, handles nested structs recursively) */
138
+ update(value: TreeNodeUpdateValue<TNode>): void;
120
139
  }
121
140
 
122
141
  /**
@@ -158,40 +177,40 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
158
177
  /** Gets a node proxy by ID with type narrowing capabilities */
159
178
  node(id: string): TreeNodeProxyBase<TRoot> | undefined;
160
179
 
161
- /** Insert a new node as the first child */
180
+ /** Insert a new node as the first child (applies defaults for node data) */
162
181
  insertFirst<TNode extends AnyTreeNodePrimitive>(
163
182
  parentId: string | null,
164
183
  nodeType: TNode,
165
- data: InferTreeNodeDataState<TNode>
184
+ data: TreeNodeDataSetInput<TNode>
166
185
  ): string;
167
186
 
168
- /** Insert a new node as the last child */
187
+ /** Insert a new node as the last child (applies defaults for node data) */
169
188
  insertLast<TNode extends AnyTreeNodePrimitive>(
170
189
  parentId: string | null,
171
190
  nodeType: TNode,
172
- data: InferTreeNodeDataState<TNode>
191
+ data: TreeNodeDataSetInput<TNode>
173
192
  ): string;
174
193
 
175
- /** Insert a new node at a specific index among siblings */
194
+ /** Insert a new node at a specific index among siblings (applies defaults for node data) */
176
195
  insertAt<TNode extends AnyTreeNodePrimitive>(
177
196
  parentId: string | null,
178
197
  index: number,
179
198
  nodeType: TNode,
180
- data: InferTreeNodeDataState<TNode>
199
+ data: TreeNodeDataSetInput<TNode>
181
200
  ): string;
182
201
 
183
- /** Insert a new node after a sibling */
202
+ /** Insert a new node after a sibling (applies defaults for node data) */
184
203
  insertAfter<TNode extends AnyTreeNodePrimitive>(
185
204
  siblingId: string,
186
205
  nodeType: TNode,
187
- data: InferTreeNodeDataState<TNode>
206
+ data: TreeNodeDataSetInput<TNode>
188
207
  ): string;
189
208
 
190
- /** Insert a new node before a sibling */
209
+ /** Insert a new node before a sibling (applies defaults for node data) */
191
210
  insertBefore<TNode extends AnyTreeNodePrimitive>(
192
211
  siblingId: string,
193
212
  nodeType: TNode,
194
- data: InferTreeNodeDataState<TNode>
213
+ data: TreeNodeDataSetInput<TNode>
195
214
  ): string;
196
215
 
197
216
  /** Remove a node and all its descendants */
@@ -218,6 +237,13 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
218
237
  nodeType: TNode
219
238
  ): InferProxy<TNode["data"]>;
220
239
 
240
+ /** Updates only the specified data fields of a node (partial update) */
241
+ updateAt<TNode extends AnyTreeNodePrimitive>(
242
+ id: string,
243
+ nodeType: TNode,
244
+ value: TreeNodeUpdateValue<TNode>
245
+ ): void;
246
+
221
247
  /** Convert tree to a nested snapshot for UI rendering */
222
248
  toSnapshot(): TreeNodeSnapshot<TRoot> | undefined;
223
249
  }
@@ -229,12 +255,14 @@ interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
229
255
  readonly validators: readonly Validator<TreeState<TRoot>>[];
230
256
  }
231
257
 
232
- export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
233
- implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>>
258
+ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TDefined extends boolean = false, THasDefault extends boolean = false>
259
+ implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>, TDefined, THasDefault>
234
260
  {
235
261
  readonly _tag = "TreePrimitive" as const;
236
262
  readonly _State!: TreeState<TRoot>;
237
263
  readonly _Proxy!: TreeProxy<TRoot>;
264
+ readonly _TDefined!: TDefined;
265
+ readonly _THasDefault!: THasDefault;
238
266
 
239
267
  private readonly _schema: TreePrimitiveSchema<TRoot>;
240
268
  private _nodeTypeRegistry: Map<string, AnyTreeNodePrimitive> | undefined;
@@ -271,7 +299,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
271
299
  }
272
300
 
273
301
  /** Mark this tree as required */
274
- required(): TreePrimitive<TRoot> {
302
+ required(): TreePrimitive<TRoot, true, THasDefault> {
275
303
  return new TreePrimitive({
276
304
  ...this._schema,
277
305
  required: true,
@@ -279,7 +307,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
279
307
  }
280
308
 
281
309
  /** Set a default value for this tree */
282
- default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot> {
310
+ default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot, true, true> {
283
311
  return new TreePrimitive({
284
312
  ...this._schema,
285
313
  defaultValue,
@@ -292,7 +320,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
292
320
  }
293
321
 
294
322
  /** Add a custom validation rule */
295
- refine(fn: (value: TreeState<TRoot>) => boolean, message: string): TreePrimitive<TRoot> {
323
+ refine(fn: (value: TreeState<TRoot>) => boolean, message: string): TreePrimitive<TRoot, TDefined, THasDefault> {
296
324
  return new TreePrimitive({
297
325
  ...this._schema,
298
326
  validators: [...this._schema.validators, { validate: fn, message }],
@@ -404,11 +432,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
404
432
  );
405
433
  }
406
434
  const nodePath = operationPath.append(nodeState.id);
435
+ const dataProxy = nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>;
407
436
  return {
408
437
  id: nodeState.id,
409
438
  type: nodeType.type as InferTreeNodeType<TNode>,
410
- data: nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>,
439
+ data: dataProxy,
411
440
  get: () => nodeState as TypedTreeNodeState<TNode>,
441
+ update: (value: TreeNodeUpdateValue<TNode>) => {
442
+ // Delegate to the data proxy's update method
443
+ (dataProxy as { update: (v: unknown) => void }).update(value);
444
+ },
412
445
  };
413
446
  },
414
447
 
@@ -474,7 +507,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
474
507
  insertFirst: <TNode extends AnyTreeNodePrimitive>(
475
508
  parentId: string | null,
476
509
  nodeType: TNode,
477
- data: InferTreeNodeDataState<TNode>
510
+ data: TreeNodeDataSetInput<TNode>
478
511
  ): string => {
479
512
  const state = getCurrentState();
480
513
  const siblings = getOrderedChildren(state, parentId);
@@ -496,13 +529,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
496
529
  throw new ValidationError("Tree already has a root node");
497
530
  }
498
531
 
532
+ // Apply defaults to node data
533
+ const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
534
+
499
535
  env.addOperation(
500
536
  Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
501
537
  id,
502
538
  type: nodeType.type,
503
539
  parentId,
504
540
  pos,
505
- data,
541
+ data: mergedData,
506
542
  })
507
543
  );
508
544
 
@@ -512,7 +548,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
512
548
  insertLast: <TNode extends AnyTreeNodePrimitive>(
513
549
  parentId: string | null,
514
550
  nodeType: TNode,
515
- data: InferTreeNodeDataState<TNode>
551
+ data: TreeNodeDataSetInput<TNode>
516
552
  ): string => {
517
553
  const state = getCurrentState();
518
554
  const siblings = getOrderedChildren(state, parentId);
@@ -534,13 +570,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
534
570
  throw new ValidationError("Tree already has a root node");
535
571
  }
536
572
 
573
+ // Apply defaults to node data
574
+ const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
575
+
537
576
  env.addOperation(
538
577
  Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
539
578
  id,
540
579
  type: nodeType.type,
541
580
  parentId,
542
581
  pos,
543
- data,
582
+ data: mergedData,
544
583
  })
545
584
  );
546
585
 
@@ -551,7 +590,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
551
590
  parentId: string | null,
552
591
  index: number,
553
592
  nodeType: TNode,
554
- data: InferTreeNodeDataState<TNode>
593
+ data: TreeNodeDataSetInput<TNode>
555
594
  ): string => {
556
595
  const state = getCurrentState();
557
596
  const siblings = getOrderedChildren(state, parentId);
@@ -575,13 +614,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
575
614
  throw new ValidationError("Tree already has a root node");
576
615
  }
577
616
 
617
+ // Apply defaults to node data
618
+ const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
619
+
578
620
  env.addOperation(
579
621
  Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
580
622
  id,
581
623
  type: nodeType.type,
582
624
  parentId,
583
625
  pos,
584
- data,
626
+ data: mergedData,
585
627
  })
586
628
  );
587
629
 
@@ -591,7 +633,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
591
633
  insertAfter: <TNode extends AnyTreeNodePrimitive>(
592
634
  siblingId: string,
593
635
  nodeType: TNode,
594
- data: InferTreeNodeDataState<TNode>
636
+ data: TreeNodeDataSetInput<TNode>
595
637
  ): string => {
596
638
  const state = getCurrentState();
597
639
  const sibling = state.find(n => n.id === siblingId);
@@ -610,13 +652,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
610
652
  const parentType = getParentType(parentId);
611
653
  this._validateChildType(parentType, nodeType.type);
612
654
 
655
+ // Apply defaults to node data
656
+ const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
657
+
613
658
  env.addOperation(
614
659
  Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
615
660
  id,
616
661
  type: nodeType.type,
617
662
  parentId,
618
663
  pos,
619
- data,
664
+ data: mergedData,
620
665
  })
621
666
  );
622
667
 
@@ -626,7 +671,7 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
626
671
  insertBefore: <TNode extends AnyTreeNodePrimitive>(
627
672
  siblingId: string,
628
673
  nodeType: TNode,
629
- data: InferTreeNodeDataState<TNode>
674
+ data: TreeNodeDataSetInput<TNode>
630
675
  ): string => {
631
676
  const state = getCurrentState();
632
677
  const sibling = state.find(n => n.id === siblingId);
@@ -645,13 +690,16 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
645
690
  const parentType = getParentType(parentId);
646
691
  this._validateChildType(parentType, nodeType.type);
647
692
 
693
+ // Apply defaults to node data
694
+ const mergedData = applyDefaults(nodeType.data as AnyPrimitive, data as Partial<InferTreeNodeDataState<TNode>>) as InferTreeNodeDataState<TNode>;
695
+
648
696
  env.addOperation(
649
697
  Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
650
698
  id,
651
699
  type: nodeType.type,
652
700
  parentId,
653
701
  pos,
654
- data,
702
+ data: mergedData,
655
703
  })
656
704
  );
657
705
 
@@ -890,6 +938,29 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
890
938
  return nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>;
891
939
  },
892
940
 
941
+ updateAt: <TNode extends AnyTreeNodePrimitive>(
942
+ id: string,
943
+ nodeType: TNode,
944
+ value: TreeNodeUpdateValue<TNode>
945
+ ): void => {
946
+ // Get the node to verify its type
947
+ const state = getCurrentState();
948
+ const node = state.find(n => n.id === id);
949
+ if (!node) {
950
+ throw new ValidationError(`Node not found: ${id}`);
951
+ }
952
+ if (node.type !== nodeType.type) {
953
+ throw new ValidationError(
954
+ `Node is of type "${node.type}", not "${nodeType.type}"`
955
+ );
956
+ }
957
+
958
+ const nodePath = operationPath.append(id);
959
+ const dataProxy = nodeType.data._internal.createProxy(env, nodePath);
960
+ // Delegate to the data proxy's update method
961
+ (dataProxy as { update: (v: unknown) => void }).update(value);
962
+ },
963
+
893
964
  toSnapshot: (): TreeNodeSnapshot<TRoot> | undefined => {
894
965
  const state = getCurrentState();
895
966
  const rootNode = state.find(n => n.parentId === null);
@@ -1111,7 +1182,7 @@ export interface TreeOptions<TRoot extends AnyTreeNodePrimitive> {
1111
1182
  /** Creates a new TreePrimitive with the given root node type */
1112
1183
  export const Tree = <TRoot extends AnyTreeNodePrimitive>(
1113
1184
  options: TreeOptions<TRoot>
1114
- ): TreePrimitive<TRoot> =>
1185
+ ): TreePrimitive<TRoot, false, false> =>
1115
1186
  new TreePrimitive({
1116
1187
  required: false,
1117
1188
  defaultValue: undefined,
@@ -7,14 +7,14 @@ import * as Transform from "../Transform";
7
7
  import type { Primitive, PrimitiveInternal, MaybeUndefined, AnyPrimitive, InferState, InferProxy, InferSnapshot } from "../Primitive";
8
8
  import { ValidationError } from "../Primitive";
9
9
  import { LiteralPrimitive } from "./Literal";
10
- import { StructPrimitive } from "./Struct";
11
- import { runValidators } from "./shared";
10
+ import { StructPrimitive, InferStructState } from "./Struct";
11
+ import { runValidators, applyDefaults, StructSetInput } from "./shared";
12
12
 
13
13
 
14
14
  /**
15
15
  * Type constraint for union variants - must be struct primitives
16
16
  */
17
- export type UnionVariants = Record<string, StructPrimitive<any, any>>;
17
+ export type UnionVariants = Record<string, StructPrimitive<any, any, any>>;
18
18
 
19
19
  /**
20
20
  * Infer the union state type from variants
@@ -30,6 +30,16 @@ export type InferUnionSnapshot<TVariants extends UnionVariants> = {
30
30
  [K in keyof TVariants]: InferSnapshot<TVariants[K]>;
31
31
  }[keyof TVariants];
32
32
 
33
+ /**
34
+ * Compute the input type for union.set() operations.
35
+ * For each variant, uses StructSetInput to make fields with defaults optional.
36
+ */
37
+ export type UnionSetInput<TVariants extends UnionVariants> = {
38
+ [K in keyof TVariants]: TVariants[K] extends StructPrimitive<infer TFields, any, any>
39
+ ? StructSetInput<TFields>
40
+ : InferState<TVariants[K]>;
41
+ }[keyof TVariants];
42
+
33
43
  /**
34
44
  * Proxy for accessing union variants
35
45
  */
@@ -37,8 +47,8 @@ export interface UnionProxy<TVariants extends UnionVariants, _TDiscriminator ext
37
47
  /** Gets the current union value */
38
48
  get(): MaybeUndefined<InferUnionState<TVariants>, TDefined>;
39
49
 
40
- /** Sets the entire union value */
41
- set(value: InferUnionState<TVariants>): void;
50
+ /** Sets the entire union value (applies defaults for variant fields) */
51
+ set(value: UnionSetInput<TVariants>): void;
42
52
 
43
53
  /** Access a specific variant's proxy (assumes the variant is active) */
44
54
  as<K extends keyof TVariants>(variant: K): InferProxy<TVariants[K]>;
@@ -59,12 +69,14 @@ interface UnionPrimitiveSchema<TVariants extends UnionVariants, TDiscriminator e
59
69
  readonly variants: TVariants;
60
70
  }
61
71
 
62
- export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator extends string = "type", TDefined extends boolean = false>
63
- implements Primitive<InferUnionState<TVariants>, UnionProxy<TVariants, TDiscriminator, TDefined>>
72
+ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator extends string = "type", TDefined extends boolean = false, THasDefault extends boolean = false>
73
+ implements Primitive<InferUnionState<TVariants>, UnionProxy<TVariants, TDiscriminator, TDefined>, TDefined, THasDefault>
64
74
  {
65
75
  readonly _tag = "UnionPrimitive" as const;
66
76
  readonly _State!: InferUnionState<TVariants>;
67
77
  readonly _Proxy!: UnionProxy<TVariants, TDiscriminator, TDefined>;
78
+ readonly _TDefined!: TDefined;
79
+ readonly _THasDefault!: THasDefault;
68
80
 
69
81
  private readonly _schema: UnionPrimitiveSchema<TVariants, TDiscriminator>;
70
82
 
@@ -82,7 +94,7 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
82
94
  }
83
95
 
84
96
  /** Mark this union as required */
85
- required(): UnionPrimitive<TVariants, TDiscriminator, true> {
97
+ required(): UnionPrimitive<TVariants, TDiscriminator, true, THasDefault> {
86
98
  return new UnionPrimitive({
87
99
  ...this._schema,
88
100
  required: true,
@@ -90,10 +102,12 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
90
102
  }
91
103
 
92
104
  /** Set a default value for this union */
93
- default(defaultValue: InferUnionState<TVariants>): UnionPrimitive<TVariants, TDiscriminator, true> {
105
+ default(defaultValue: UnionSetInput<TVariants>): UnionPrimitive<TVariants, TDiscriminator, true, true> {
106
+ // Apply defaults to the variant
107
+ const merged = this._applyVariantDefaults(defaultValue as Partial<InferUnionState<TVariants>>);
94
108
  return new UnionPrimitive({
95
109
  ...this._schema,
96
- defaultValue,
110
+ defaultValue: merged,
97
111
  });
98
112
  }
99
113
 
@@ -119,7 +133,7 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
119
133
  const variant = this._schema.variants[key]!;
120
134
  const discriminatorField = variant.fields[this._schema.discriminator];
121
135
  if (discriminatorField && discriminatorField._tag === "LiteralPrimitive") {
122
- const literalPrimitive = discriminatorField as LiteralPrimitive<any, any>;
136
+ const literalPrimitive = discriminatorField as LiteralPrimitive<any, any, any>;
123
137
  if (literalPrimitive.literal === discriminatorValue) {
124
138
  return key;
125
139
  }
@@ -128,6 +142,17 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
128
142
  return undefined;
129
143
  }
130
144
 
145
+ /** Apply defaults to a variant value based on the discriminator */
146
+ private _applyVariantDefaults(value: Partial<InferUnionState<TVariants>>): InferUnionState<TVariants> {
147
+ const variantKey = this._findVariantKey(value as InferUnionState<TVariants>);
148
+ if (!variantKey) {
149
+ return value as InferUnionState<TVariants>;
150
+ }
151
+
152
+ const variantPrimitive = this._schema.variants[variantKey]!;
153
+ return applyDefaults(variantPrimitive as AnyPrimitive, value) as InferUnionState<TVariants>;
154
+ }
155
+
131
156
  readonly _internal: PrimitiveInternal<InferUnionState<TVariants>, UnionProxy<TVariants, TDiscriminator, TDefined>> = {
132
157
  createProxy: (
133
158
  env: ProxyEnvironment.ProxyEnvironment,
@@ -141,9 +166,11 @@ export class UnionPrimitive<TVariants extends UnionVariants, TDiscriminator exte
141
166
  const state = env.getState(operationPath) as InferUnionState<TVariants> | undefined;
142
167
  return (state ?? defaultValue) as MaybeUndefined<InferUnionState<TVariants>, TDefined>;
143
168
  },
144
- set: (value: InferUnionState<TVariants>) => {
169
+ set: (value: UnionSetInput<TVariants>) => {
170
+ // Apply defaults for the variant
171
+ const merged = this._applyVariantDefaults(value as Partial<InferUnionState<TVariants>>);
145
172
  env.addOperation(
146
- Operation.fromDefinition(operationPath, this._opDefinitions.set, value)
173
+ Operation.fromDefinition(operationPath, this._opDefinitions.set, merged)
147
174
  );
148
175
  },
149
176
  as: <K extends keyof TVariants>(variant: K): InferProxy<TVariants[K]> => {
@@ -311,13 +338,13 @@ export interface UnionOptions<TVariants extends UnionVariants, TDiscriminator ex
311
338
  /** Creates a new UnionPrimitive with the given variants */
312
339
  export function Union<TVariants extends UnionVariants>(
313
340
  options: UnionOptions<TVariants, "type">
314
- ): UnionPrimitive<TVariants, "type", false>;
341
+ ): UnionPrimitive<TVariants, "type", false, false>;
315
342
  export function Union<TVariants extends UnionVariants, TDiscriminator extends string>(
316
343
  options: UnionOptions<TVariants, TDiscriminator>
317
- ): UnionPrimitive<TVariants, TDiscriminator, false>;
344
+ ): UnionPrimitive<TVariants, TDiscriminator, false, false>;
318
345
  export function Union<TVariants extends UnionVariants, TDiscriminator extends string = "type">(
319
346
  options: UnionOptions<TVariants, TDiscriminator>
320
- ): UnionPrimitive<TVariants, TDiscriminator, false> {
347
+ ): UnionPrimitive<TVariants, TDiscriminator, false, false> {
321
348
  const discriminator = (options.discriminator ?? "type") as TDiscriminator;
322
349
  return new UnionPrimitive({
323
350
  required: false,