@voidhash/mimic 0.0.1-alpha.5 → 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.
@@ -26,10 +26,12 @@ interface StringPrimitiveSchema {
26
26
  readonly validators: readonly Validator<string>[];
27
27
  }
28
28
 
29
- export class StringPrimitive<TDefined extends boolean = false> implements Primitive<string, StringProxy<TDefined>> {
29
+ export class StringPrimitive<TDefined extends boolean = false, THasDefault extends boolean = false> implements Primitive<string, StringProxy<TDefined>, TDefined, THasDefault> {
30
30
  readonly _tag = "StringPrimitive" as const;
31
31
  readonly _State!: string;
32
32
  readonly _Proxy!: StringProxy<TDefined>;
33
+ readonly _TDefined!: TDefined;
34
+ readonly _THasDefault!: THasDefault;
33
35
 
34
36
  private readonly _schema: StringPrimitiveSchema;
35
37
 
@@ -47,7 +49,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
47
49
  }
48
50
 
49
51
  /** Mark this string as required */
50
- required(): StringPrimitive<true> {
52
+ required(): StringPrimitive<true, THasDefault> {
51
53
  return new StringPrimitive({
52
54
  ...this._schema,
53
55
  required: true,
@@ -55,7 +57,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
55
57
  }
56
58
 
57
59
  /** Set a default value for this string */
58
- default(defaultValue: string): StringPrimitive<true> {
60
+ default(defaultValue: string): StringPrimitive<true, true> {
59
61
  return new StringPrimitive({
60
62
  ...this._schema,
61
63
  defaultValue,
@@ -63,7 +65,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
63
65
  }
64
66
 
65
67
  /** Add a custom validation rule */
66
- refine(fn: (value: string) => boolean, message: string): StringPrimitive<TDefined> {
68
+ refine(fn: (value: string) => boolean, message: string): StringPrimitive<TDefined, THasDefault> {
67
69
  return new StringPrimitive({
68
70
  ...this._schema,
69
71
  validators: [...this._schema.validators, { validate: fn, message }],
@@ -71,7 +73,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
71
73
  }
72
74
 
73
75
  /** Minimum string length */
74
- min(length: number): StringPrimitive<TDefined> {
76
+ min(length: number): StringPrimitive<TDefined, THasDefault> {
75
77
  return this.refine(
76
78
  (v) => v.length >= length,
77
79
  `String must be at least ${length} characters`
@@ -79,7 +81,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
79
81
  }
80
82
 
81
83
  /** Maximum string length */
82
- max(length: number): StringPrimitive<TDefined> {
84
+ max(length: number): StringPrimitive<TDefined, THasDefault> {
83
85
  return this.refine(
84
86
  (v) => v.length <= length,
85
87
  `String must be at most ${length} characters`
@@ -87,7 +89,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
87
89
  }
88
90
 
89
91
  /** Exact string length */
90
- length(exact: number): StringPrimitive<TDefined> {
92
+ length(exact: number): StringPrimitive<TDefined, THasDefault> {
91
93
  return this.refine(
92
94
  (v) => v.length === exact,
93
95
  `String must be exactly ${exact} characters`
@@ -95,7 +97,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
95
97
  }
96
98
 
97
99
  /** Match a regex pattern */
98
- regex(pattern: RegExp, message?: string): StringPrimitive<TDefined> {
100
+ regex(pattern: RegExp, message?: string): StringPrimitive<TDefined, THasDefault> {
99
101
  return this.refine(
100
102
  (v) => pattern.test(v),
101
103
  message ?? `String must match pattern ${pattern}`
@@ -103,7 +105,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
103
105
  }
104
106
 
105
107
  /** Validate as email format */
106
- email(): StringPrimitive<TDefined> {
108
+ email(): StringPrimitive<TDefined, THasDefault> {
107
109
  // Simple email regex - covers most common cases
108
110
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
109
111
  return this.refine(
@@ -113,7 +115,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
113
115
  }
114
116
 
115
117
  /** Validate as URL format */
116
- url(): StringPrimitive<TDefined> {
118
+ url(): StringPrimitive<TDefined, THasDefault> {
117
119
  return this.refine(
118
120
  (v) => {
119
121
  try {
@@ -147,7 +149,7 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
147
149
  };
148
150
  },
149
151
 
150
- applyOperation: (state: string | undefined, operation: Operation.Operation<any, any, any>): string => {
152
+ applyOperation: (_state: string | undefined, operation: Operation.Operation<any, any, any>): string => {
151
153
  if (!isCompatibleOperation(operation, this._opDefinitions)) {
152
154
  throw new ValidationError(`StringPrimitive cannot apply operation of kind: ${operation.kind}`);
153
155
  }
@@ -184,6 +186,6 @@ export class StringPrimitive<TDefined extends boolean = false> implements Primit
184
186
  }
185
187
 
186
188
  /** Creates a new StringPrimitive */
187
- export const String = (): StringPrimitive<false> =>
189
+ export const String = (): StringPrimitive<false, false> =>
188
190
  new StringPrimitive({ required: false, defaultValue: undefined, validators: [] });
189
191
 
@@ -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>;
@@ -149,14 +195,17 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
149
195
 
150
196
  // Use a JavaScript Proxy to intercept field access
151
197
  return new globalThis.Proxy(base as StructProxy<TFields, TDefined>, {
152
- get: (target, prop, receiver) => {
153
- // Return base methods (get, set, toSnapshot)
198
+ get: (target, prop, _receiver) => {
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
  }
@@ -175,8 +224,8 @@ export class StructPrimitive<TFields extends Record<string, AnyPrimitive>, TDefi
175
224
 
176
225
  return undefined;
177
226
  },
178
- has: (target, prop) => {
179
- if (prop === "get" || prop === "set" || prop === "toSnapshot") return true;
227
+ has: (_target, prop) => {
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
  /**
@@ -37,7 +37,7 @@ export interface TypedTreeNodeState<TNode extends AnyTreeNodePrimitive> {
37
37
  /**
38
38
  * The state type for trees - a flat array of nodes
39
39
  */
40
- export type TreeState<TRoot extends AnyTreeNodePrimitive> = readonly TreeNodeState[];
40
+ export type TreeState<_TRoot extends AnyTreeNodePrimitive> = readonly TreeNodeState[];
41
41
 
42
42
  /**
43
43
  * Helper to get children sorted by position
@@ -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,12 +134,14 @@ 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
  /**
123
142
  * Node proxy with type narrowing capabilities
124
143
  */
125
- export interface TreeNodeProxyBase<TRoot extends AnyTreeNodePrimitive> {
144
+ export interface TreeNodeProxyBase<_TRoot extends AnyTreeNodePrimitive> {
126
145
  /** The node ID */
127
146
  readonly id: string;
128
147
  /** The node type (string) */
@@ -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,