@voidhash/mimic 0.0.4 → 0.0.5

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 (52) hide show
  1. package/.turbo/turbo-build.log +202 -198
  2. package/dist/Document.cjs +1 -2
  3. package/dist/Document.d.cts +9 -3
  4. package/dist/Document.d.cts.map +1 -1
  5. package/dist/Document.d.mts +9 -3
  6. package/dist/Document.d.mts.map +1 -1
  7. package/dist/Document.mjs +1 -2
  8. package/dist/Document.mjs.map +1 -1
  9. package/dist/Primitive.d.cts +2 -2
  10. package/dist/Primitive.d.mts +2 -2
  11. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.cjs +15 -0
  12. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.mjs +15 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.cjs +14 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.mjs +13 -0
  15. package/dist/client/ClientDocument.cjs +17 -12
  16. package/dist/client/ClientDocument.d.mts.map +1 -1
  17. package/dist/client/ClientDocument.mjs +17 -12
  18. package/dist/client/ClientDocument.mjs.map +1 -1
  19. package/dist/client/WebSocketTransport.cjs +6 -6
  20. package/dist/client/WebSocketTransport.mjs +6 -6
  21. package/dist/client/WebSocketTransport.mjs.map +1 -1
  22. package/dist/primitives/Tree.cjs +55 -7
  23. package/dist/primitives/Tree.d.cts +97 -10
  24. package/dist/primitives/Tree.d.cts.map +1 -1
  25. package/dist/primitives/Tree.d.mts +97 -10
  26. package/dist/primitives/Tree.d.mts.map +1 -1
  27. package/dist/primitives/Tree.mjs +55 -7
  28. package/dist/primitives/Tree.mjs.map +1 -1
  29. package/dist/primitives/shared.d.cts +9 -0
  30. package/dist/primitives/shared.d.cts.map +1 -1
  31. package/dist/primitives/shared.d.mts +9 -0
  32. package/dist/primitives/shared.d.mts.map +1 -1
  33. package/dist/primitives/shared.mjs.map +1 -1
  34. package/dist/server/ServerDocument.cjs +1 -1
  35. package/dist/server/ServerDocument.d.cts +3 -3
  36. package/dist/server/ServerDocument.d.cts.map +1 -1
  37. package/dist/server/ServerDocument.d.mts +3 -3
  38. package/dist/server/ServerDocument.d.mts.map +1 -1
  39. package/dist/server/ServerDocument.mjs +1 -1
  40. package/dist/server/ServerDocument.mjs.map +1 -1
  41. package/package.json +2 -2
  42. package/src/Document.ts +18 -5
  43. package/src/client/ClientDocument.ts +20 -21
  44. package/src/client/WebSocketTransport.ts +9 -9
  45. package/src/primitives/Tree.ts +209 -19
  46. package/src/primitives/shared.ts +10 -1
  47. package/src/server/ServerDocument.ts +4 -3
  48. package/tests/client/ClientDocument.test.ts +309 -2
  49. package/tests/client/WebSocketTransport.test.ts +228 -3
  50. package/tests/primitives/Tree.test.ts +291 -17
  51. package/tests/server/ServerDocument.test.ts +1 -1
  52. package/tsconfig.json +1 -1
package/src/Document.ts CHANGED
@@ -80,8 +80,14 @@ export interface Document<TSchema extends Primitive.AnyPrimitive> {
80
80
  // =============================================================================
81
81
 
82
82
  export interface DocumentOptions<TSchema extends Primitive.AnyPrimitive> {
83
- /** Initial state for the document */
84
- readonly initial?: Primitive.InferState<TSchema>;
83
+ /** Initial value for the document (using set input format) */
84
+ readonly initial?: Primitive.InferSetInput<TSchema>;
85
+ /**
86
+ * Raw initial state for the document (already in internal state format).
87
+ * Use this when loading state from the server or storage.
88
+ * Takes precedence over `initial` if both are provided.
89
+ */
90
+ readonly initialState?: Primitive.InferState<TSchema>;
85
91
  }
86
92
 
87
93
  // =============================================================================
@@ -95,9 +101,16 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
95
101
  schema: TSchema,
96
102
  options?: DocumentOptions<TSchema>
97
103
  ): Document<TSchema> => {
98
- // Internal state
99
- let _state: Primitive.InferState<TSchema> | undefined =
100
- options?.initial ?? schema._internal.getInitialState();
104
+ // Internal state - determine initial state based on options
105
+ // Priority: initialState (raw) > initial (needs conversion) > schema defaults
106
+ let _state: Primitive.InferState<TSchema> | undefined =
107
+ options?.initialState !== undefined
108
+ ? options.initialState
109
+ : options?.initial !== undefined
110
+ ? (schema._internal.convertSetInputToState
111
+ ? schema._internal.convertSetInputToState(options.initial)
112
+ : options.initial as Primitive.InferState<TSchema>)
113
+ : schema._internal.getInitialState();
101
114
 
102
115
  // Pending operations buffer (local changes not yet flushed)
103
116
  let _pending: Operation.Operation<any, any, any>[] = [];
@@ -265,8 +265,8 @@ export const make = <
265
265
  let _serverTransactionHistory: Transaction.Transaction[] = [];
266
266
  const MAX_HISTORY_SIZE = 100;
267
267
 
268
- // The underlying document for optimistic state
269
- let _optimisticDoc = Document.make(schema, { initial: _serverState });
268
+ // The underlying document for optimistic state (use initialState for raw state format)
269
+ let _optimisticDoc = Document.make(schema, { initialState: _serverState });
270
270
 
271
271
  // Subscription cleanup
272
272
  let _unsubscribe: (() => void) | null = null;
@@ -467,8 +467,8 @@ export const make = <
467
467
  serverState: _serverState,
468
468
  });
469
469
 
470
- // Create fresh document from server state
471
- _optimisticDoc = Document.make(schema, { initial: _serverState });
470
+ // Create fresh document from server state (use initialState for raw state format)
471
+ _optimisticDoc = Document.make(schema, { initialState: _serverState });
472
472
 
473
473
  // Apply all pending transactions
474
474
  for (const pending of _pending) {
@@ -484,16 +484,14 @@ export const make = <
484
484
 
485
485
  /**
486
486
  * Adds a transaction to pending queue and sends to server.
487
+ * If disconnected, the transport will queue it for later submission.
487
488
  */
488
489
  const submitTransaction = (tx: Transaction.Transaction): void => {
489
- if (!transport.isConnected()) {
490
- throw new NotConnectedError();
491
- }
492
-
493
490
  debugLog("submitTransaction", {
494
491
  txId: tx.id,
495
492
  ops: tx.ops,
496
493
  pendingCount: _pending.length + 1,
494
+ isConnected: transport.isConnected(),
497
495
  });
498
496
 
499
497
  const pending: PendingTransaction = {
@@ -504,15 +502,18 @@ export const make = <
504
502
 
505
503
  _pending.push(pending);
506
504
 
507
- // Set timeout for this transaction
508
- const timeoutHandle = setTimeout(() => {
509
- handleTransactionTimeout(tx.id);
510
- }, transactionTimeout);
511
- _timeoutHandles.set(tx.id, timeoutHandle);
505
+ // Only set timeout if connected - otherwise the transport queues it
506
+ // and the timeout will start when the message is actually sent on reconnect
507
+ if (transport.isConnected()) {
508
+ const timeoutHandle = setTimeout(() => {
509
+ handleTransactionTimeout(tx.id);
510
+ }, transactionTimeout);
511
+ _timeoutHandles.set(tx.id, timeoutHandle);
512
+ }
512
513
 
513
- // Send to server
514
+ // Send to server (transport queues if disconnected)
514
515
  transport.send(tx);
515
- debugLog("submitTransaction: sent to server", { txId: tx.id });
516
+ debugLog("submitTransaction: sent to transport", { txId: tx.id, queued: !transport.isConnected() });
516
517
  };
517
518
 
518
519
  /**
@@ -585,7 +586,7 @@ export const make = <
585
586
  _pending.splice(pendingIndex, 1);
586
587
 
587
588
  // Apply to server state
588
- const tempDoc = Document.make(schema, { initial: _serverState });
589
+ const tempDoc = Document.make(schema, { initialState: _serverState });
589
590
  tempDoc.apply(serverTx.ops);
590
591
  _serverState = tempDoc.get();
591
592
 
@@ -605,7 +606,7 @@ export const make = <
605
606
  });
606
607
 
607
608
  // Apply to server state
608
- const tempDoc = Document.make(schema, { initial: _serverState });
609
+ const tempDoc = Document.make(schema, { initialState: _serverState });
609
610
  tempDoc.apply(serverTx.ops);
610
611
  _serverState = tempDoc.get();
611
612
 
@@ -937,10 +938,8 @@ export const make = <
937
938
  pendingCount: _pending.length,
938
939
  });
939
940
 
940
- if (!transport.isConnected()) {
941
- throw new NotConnectedError();
942
- }
943
-
941
+ // Allow transactions even when disconnected - they will be queued
942
+ // Only require that we have been initialized at least once (have server state)
944
943
  if (_initState.type !== "ready") {
945
944
  throw new InvalidStateError("Client is not ready. Wait for initialization to complete.");
946
945
  }
@@ -458,11 +458,10 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
458
458
 
459
459
  if (_state.type === "connected") {
460
460
  sendRaw(message);
461
- } else if (_state.type === "reconnecting") {
462
- // Queue message for when we reconnect
461
+ } else {
462
+ // Queue message for when we reconnect (works for both reconnecting and disconnected states)
463
463
  _messageQueue.push(message);
464
464
  }
465
- // If disconnected, silently drop (caller should check isConnected)
466
465
  },
467
466
 
468
467
  requestSnapshot: (): void => {
@@ -470,7 +469,8 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
470
469
 
471
470
  if (_state.type === "connected") {
472
471
  sendRaw(message);
473
- } else if (_state.type === "reconnecting") {
472
+ } else {
473
+ // Queue for later
474
474
  _messageQueue.push(message);
475
475
  }
476
476
  },
@@ -546,9 +546,9 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
546
546
 
547
547
  if (_state.type === "connected") {
548
548
  sendRaw(message);
549
- } else if (_state.type === "reconnecting") {
550
- // Remove all set messages from the message queue
551
- _messageQueue = _messageQueue.filter((message) => message.type !== "presence_set");
549
+ } else {
550
+ // Remove all set messages from the message queue (only keep latest)
551
+ _messageQueue = _messageQueue.filter((m) => m.type !== "presence_set");
552
552
  // Add the new presence set message to the queue
553
553
  _messageQueue.push(message);
554
554
  }
@@ -559,9 +559,9 @@ export const make = (options: WebSocketTransportOptions): Transport.Transport =>
559
559
 
560
560
  if (_state.type === "connected") {
561
561
  sendRaw(message);
562
- } else if (_state.type === "reconnecting") {
562
+ } else {
563
563
  // Remove all clear messages from the message queue
564
- _messageQueue = _messageQueue.filter((message) => message.type !== "presence_clear");
564
+ _messageQueue = _messageQueue.filter((m) => m.type !== "presence_clear");
565
565
  // Add the new presence clear message to the queue
566
566
  _messageQueue.push(message);
567
567
  }
@@ -121,15 +121,128 @@ export type TreeNodeUpdateValue<TNode extends AnyTreeNodePrimitive> =
121
121
  * Uses StructSetInput directly so that:
122
122
  * - Fields that are required AND have no default must be provided
123
123
  * - Fields that are optional OR have defaults can be omitted
124
- *
124
+ *
125
125
  * This bypasses the struct-level NeedsValue wrapper since tree inserts
126
126
  * always require a data object (even if empty for all-optional fields).
127
127
  */
128
- export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
128
+ export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
129
129
  TNode["data"] extends StructPrimitive<infer TFields, any, any>
130
130
  ? StructSetInput<TFields>
131
131
  : InferSetInput<TNode["data"]>;
132
132
 
133
+ // =============================================================================
134
+ // Nested Input Types for set() and default()
135
+ // =============================================================================
136
+
137
+ /**
138
+ * Type guard to check if a type is `any` or unknown (structural check).
139
+ * Returns true if T is `any`, false otherwise.
140
+ */
141
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
142
+
143
+ /**
144
+ * Get children types, with special handling for self-referential nodes.
145
+ * When InferTreeNodeChildren returns `any` (from TreeNodePrimitive<..., any>),
146
+ * we fall back to using TNode itself as its own child type.
147
+ * This handles the common case of self-referential nodes like:
148
+ * FolderNode = TreeNode("folder", { children: [TreeNodeSelf, FileNode] })
149
+ * Where FolderNode's TChildren is resolved to FolderNode | FileNode,
150
+ * but the self-referenced FolderNode has TChildren = any.
151
+ */
152
+ type ResolveChildrenForInput<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
153
+ IsAny<InferTreeNodeChildren<TNode>> extends true
154
+ ? TOriginalNode // Self-reference: use the original node's type
155
+ : InferTreeNodeChildren<TNode>;
156
+
157
+ /**
158
+ * Helper type that creates a properly typed node input for a specific node type.
159
+ * This is the "strict" version that enforces exact property matching.
160
+ *
161
+ * The TOriginalNode parameter is used to track the original root node type
162
+ * for proper self-reference resolution in deeply nested structures.
163
+ */
164
+ type TreeNodeSetInputStrictWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> = {
165
+ readonly type: InferTreeNodeType<TNode>;
166
+ readonly id?: string;
167
+ readonly children: TreeNodeSetInputUnionWithRoot<ResolveChildrenForInput<TNode, TOriginalNode>, TOriginalNode>[];
168
+ } & TreeNodeDataSetInput<TNode>;
169
+
170
+ /**
171
+ * Distributive conditional type that creates a union of TreeNodeSetInputStrict
172
+ * for each node type in the union. This ensures proper type discrimination.
173
+ *
174
+ * When TNode is a union (e.g., FolderNode | FileNode), this distributes to:
175
+ * TreeNodeSetInputStrict<FolderNode> | TreeNodeSetInputStrict<FileNode>
176
+ *
177
+ * This creates a proper discriminated union where excess property checking works.
178
+ */
179
+ type TreeNodeSetInputUnionWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
180
+ TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrictWithRoot<TNode, TOriginalNode> : never;
181
+
182
+ /**
183
+ * Helper type for single-parameter usage - uses TNode as its own original.
184
+ */
185
+ type TreeNodeSetInputStrict<TNode extends AnyTreeNodePrimitive> =
186
+ TreeNodeSetInputStrictWithRoot<TNode, TNode>;
187
+
188
+ /**
189
+ * Distributive conditional for single-parameter usage.
190
+ */
191
+ export type TreeNodeSetInputUnion<TNode extends AnyTreeNodePrimitive> =
192
+ TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrict<TNode> : never;
193
+
194
+ /**
195
+ * Input type for a single node in a nested tree set/default operation.
196
+ *
197
+ * - `type` is REQUIRED - explicit type discriminator for the node
198
+ * - `id` is optional - auto-generated if not provided
199
+ * - `children` is a typed array of allowed child node inputs
200
+ * - Data fields are spread at the node level (like TreeNodeSnapshot)
201
+ *
202
+ * When TNode is a union type (e.g., from InferTreeNodeChildren), this properly
203
+ * distributes to create a discriminated union where:
204
+ * - Each variant has its specific `type` literal
205
+ * - Each variant has its specific data fields
206
+ * - Excess property checking works correctly
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const input: TreeNodeSetInput<BoardNode> = {
211
+ * type: "board",
212
+ * name: "My Board",
213
+ * children: [
214
+ * { type: "column", name: "Todo", children: [] }
215
+ * ]
216
+ * };
217
+ * ```
218
+ */
219
+ export type TreeNodeSetInput<TNode extends AnyTreeNodePrimitive> = TreeNodeSetInputUnion<TNode>;
220
+
221
+ /**
222
+ * Input type for tree set() and default() operations.
223
+ * Accepts a nested tree structure that will be converted to flat TreeState internally.
224
+ */
225
+ export type TreeSetInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
226
+
227
+ /**
228
+ * Infer the set input type for a tree primitive.
229
+ */
230
+ export type InferTreeSetInput<T extends TreePrimitive<any>> =
231
+ T extends TreePrimitive<infer TRoot> ? TreeSetInput<TRoot> : never;
232
+
233
+ /**
234
+ * Internal type for processing any node input during conversion.
235
+ * This is only used internally in _convertNestedToFlat and is not exported.
236
+ * Allows us to keep the public TreeNodeSetInput<TNode> fully type-safe while
237
+ * having a runtime-compatible type for internal processing.
238
+ */
239
+ type InternalNodeInput = {
240
+ readonly type: string;
241
+ readonly id?: string;
242
+ readonly children: InternalNodeInput[];
243
+ readonly [key: string]: unknown;
244
+ };
245
+
133
246
  /**
134
247
  * Typed proxy for a specific node type - provides type-safe data access
135
248
  */
@@ -172,9 +285,9 @@ export interface TreeNodeProxyBase<_TRoot extends AnyTreeNodePrimitive> {
172
285
  export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
173
286
  /** Gets the entire tree state (flat array of nodes) */
174
287
  get(): TreeState<TRoot>;
175
-
176
- /** Replaces the entire tree */
177
- set(nodes: TreeState<TRoot>): void;
288
+
289
+ /** Replaces the entire tree with a nested input structure */
290
+ set(input: TreeSetInput<TRoot>): void;
178
291
 
179
292
  /** Gets the root node state */
180
293
  root(): TypedTreeNodeState<TRoot> | undefined;
@@ -258,16 +371,13 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
258
371
 
259
372
  interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
260
373
  readonly required: boolean;
261
- readonly defaultValue: TreeState<TRoot> | undefined;
374
+ readonly defaultInput: TreeNodeSetInput<TRoot> | undefined;
262
375
  readonly root: TRoot;
263
376
  readonly validators: readonly Validator<TreeState<TRoot>>[];
264
377
  }
265
378
 
266
- /** Input type for tree set() - tree state */
267
- export type TreeSetInput<TRoot extends AnyTreeNodePrimitive> = TreeState<TRoot>;
268
-
269
- /** Input type for tree update() - same as set() for trees */
270
- export type TreeUpdateInput<TRoot extends AnyTreeNodePrimitive> = TreeState<TRoot>;
379
+ /** Input type for tree update() - same as set() for trees (nested format) */
380
+ export type TreeUpdateInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
271
381
 
272
382
  export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends boolean = false, THasDefault extends boolean = false>
273
383
  implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>, TRequired, THasDefault, TreeSetInput<TRoot>, TreeUpdateInput<TRoot>>
@@ -322,11 +432,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
322
432
  });
323
433
  }
324
434
 
325
- /** Set a default value for this tree */
326
- default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot, TRequired, true> {
435
+ /** Set a default value for this tree (nested format) */
436
+ default(defaultInput: TreeNodeSetInput<TRoot>): TreePrimitive<TRoot, TRequired, true> {
327
437
  return new TreePrimitive({
328
438
  ...this._schema,
329
- defaultValue,
439
+ defaultInput,
330
440
  });
331
441
  }
332
442
 
@@ -408,6 +518,78 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
408
518
  }
409
519
  }
410
520
 
521
+ /**
522
+ * Convert a nested TreeNodeSetInput to flat TreeState format.
523
+ * Recursively processes nodes, generating IDs and positions as needed.
524
+ *
525
+ * @param input - The root nested input to convert
526
+ * @param generateId - Optional ID generator (defaults to crypto.randomUUID)
527
+ * @returns Flat TreeState array
528
+ */
529
+ private _convertNestedToFlat(
530
+ input: TreeNodeSetInput<TRoot>,
531
+ generateId: () => string = () => crypto.randomUUID()
532
+ ): TreeState<TRoot> {
533
+ const result: TreeNodeState[] = [];
534
+ const seenIds = new Set<string>();
535
+
536
+ const processNode = (
537
+ nodeInput: InternalNodeInput,
538
+ parentId: string | null,
539
+ parentType: string | null,
540
+ leftPos: string | null,
541
+ rightPos: string | null
542
+ ): void => {
543
+ // Validate node type
544
+ this._validateChildType(parentType, nodeInput.type);
545
+
546
+ // Get the node primitive for this type
547
+ const nodePrimitive = this._getNodeTypePrimitive(nodeInput.type);
548
+
549
+ // Generate or use provided ID
550
+ const id = nodeInput.id ?? generateId();
551
+
552
+ // Check for duplicate IDs
553
+ if (seenIds.has(id)) {
554
+ throw new ValidationError(`Duplicate node ID: ${id}`);
555
+ }
556
+ seenIds.add(id);
557
+
558
+ // Generate position
559
+ const pos = generateTreePosBetween(leftPos, rightPos);
560
+
561
+ // Extract data fields (everything except type, id, and children)
562
+ const { type: _type, id: _id, children, ...dataFields } = nodeInput;
563
+
564
+ // Apply defaults to node data
565
+ const mergedData = applyDefaults(nodePrimitive.data as AnyPrimitive, dataFields);
566
+
567
+ // Add this node to result
568
+ result.push({
569
+ id,
570
+ type: nodeInput.type,
571
+ parentId,
572
+ pos,
573
+ data: mergedData,
574
+ });
575
+
576
+ // Process children recursively
577
+ let prevChildPos: string | null = null;
578
+ for (let i = 0; i < children.length; i++) {
579
+ const childInput = children[i]!;
580
+ // Each child gets a position after the previous child
581
+ processNode(childInput, id, nodeInput.type, prevChildPos, null);
582
+ // Update prevChildPos to the pos that was just assigned (it's the last item in result)
583
+ prevChildPos = result[result.length - 1]!.pos;
584
+ }
585
+ };
586
+
587
+ // Process root node (cast to InternalNodeInput for internal processing)
588
+ processNode(input as unknown as InternalNodeInput, null, null, null, null);
589
+
590
+ return result as TreeState<TRoot>;
591
+ }
592
+
411
593
  readonly _internal: PrimitiveInternal<TreeState<TRoot>, TreeProxy<TRoot>> = {
412
594
  createProxy: (
413
595
  env: ProxyEnvironment.ProxyEnvironment,
@@ -496,9 +678,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
496
678
  return getCurrentState();
497
679
  },
498
680
 
499
- set: (nodes: TreeState<TRoot>) => {
681
+ set: (input: TreeSetInput<TRoot>) => {
682
+ // Convert nested input to flat TreeState using env.generateId for IDs
683
+ const flatState = this._convertNestedToFlat(input, env.generateId);
500
684
  env.addOperation(
501
- Operation.fromDefinition(operationPath, this._opDefinitions.set, nodes)
685
+ Operation.fromDefinition(operationPath, this._opDefinitions.set, flatState)
502
686
  );
503
687
  },
504
688
 
@@ -1074,8 +1258,9 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
1074
1258
  },
1075
1259
 
1076
1260
  getInitialState: (): TreeState<TRoot> | undefined => {
1077
- if (this._schema.defaultValue !== undefined) {
1078
- return this._schema.defaultValue;
1261
+ if (this._schema.defaultInput !== undefined) {
1262
+ // Convert nested input to flat TreeState
1263
+ return this._convertNestedToFlat(this._schema.defaultInput);
1079
1264
  }
1080
1265
 
1081
1266
  // Automatically create a root node with default data
@@ -1093,6 +1278,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
1093
1278
  }] as TreeState<TRoot>;
1094
1279
  },
1095
1280
 
1281
+ convertSetInputToState: (input: unknown): TreeState<TRoot> => {
1282
+ // Convert nested input format to flat TreeState
1283
+ return this._convertNestedToFlat(input as TreeNodeSetInput<TRoot>);
1284
+ },
1285
+
1096
1286
  transformOperation: (
1097
1287
  clientOp: Operation.Operation<any, any, any>,
1098
1288
  serverOp: Operation.Operation<any, any, any>
@@ -1201,7 +1391,7 @@ export const Tree = <TRoot extends AnyTreeNodePrimitive>(
1201
1391
  ): TreePrimitive<TRoot, false, false> =>
1202
1392
  new TreePrimitive({
1203
1393
  required: false,
1204
- defaultValue: undefined,
1394
+ defaultInput: undefined,
1205
1395
  root: options.root,
1206
1396
  validators: [],
1207
1397
  });
@@ -38,10 +38,19 @@ export interface Primitive<TState, TProxy, TRequired extends boolean = false, TH
38
38
  readonly applyOperation: (state: TState | undefined, operation: Operation.Operation<any, any, any>) => TState;
39
39
  /** Returns the initial/default state for this primitive */
40
40
  readonly getInitialState: () => TState | undefined;
41
+ /**
42
+ * Converts a set input value to state format.
43
+ * For most primitives, this is a simple pass-through with defaults applied.
44
+ * For Tree primitives, this converts nested input to flat TreeState.
45
+ *
46
+ * @param input - The set input value
47
+ * @returns The corresponding state value
48
+ */
49
+ readonly convertSetInputToState?: (input: unknown) => TState;
41
50
  /**
42
51
  * Transforms a client operation against a server operation.
43
52
  * Used for operational transformation (OT) conflict resolution.
44
- *
53
+ *
45
54
  * @param clientOp - The client's operation to transform
46
55
  * @param serverOp - The server's operation that has already been applied
47
56
  * @returns TransformResult indicating how the client operation should be handled
@@ -60,8 +60,8 @@ export type SubmitResult =
60
60
  export interface ServerDocumentOptions<TSchema extends Primitive.AnyPrimitive> {
61
61
  /** The schema defining the document structure */
62
62
  readonly schema: TSchema;
63
- /** Initial state (optional, will use schema defaults if not provided) */
64
- readonly initialState?: Primitive.InferState<TSchema>;
63
+ /** Initial value (optional, will use schema defaults if not provided) - uses set input format */
64
+ readonly initialState?: Primitive.InferSetInput<TSchema>;
65
65
  /** Initial version number (optional, defaults to 0) */
66
66
  readonly initialVersion?: number;
67
67
  /** Called when a transaction is successfully applied and should be broadcast */
@@ -177,8 +177,9 @@ export const make = <TSchema extends Primitive.AnyPrimitive>(
177
177
  }
178
178
 
179
179
  // Create a temporary document with current state to test the operations
180
+ // Use initialState (not initial) since currentState is already in flat state format
180
181
  const currentState = _document.get();
181
- const tempDoc = Document.make(schema, { initial: currentState });
182
+ const tempDoc = Document.make(schema, { initialState: currentState });
182
183
 
183
184
  try {
184
185
  // Attempt to apply all operations