@voidhash/mimic 0.0.4 → 0.0.6

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 (53) 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/Presence.d.mts.map +1 -1
  10. package/dist/Primitive.d.cts +2 -2
  11. package/dist/Primitive.d.mts +2 -2
  12. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.cjs +15 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutProperties.mjs +15 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.cjs +14 -0
  15. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectWithoutPropertiesLoose.mjs +13 -0
  16. package/dist/client/ClientDocument.cjs +17 -12
  17. package/dist/client/ClientDocument.d.mts.map +1 -1
  18. package/dist/client/ClientDocument.mjs +17 -12
  19. package/dist/client/ClientDocument.mjs.map +1 -1
  20. package/dist/client/WebSocketTransport.cjs +6 -6
  21. package/dist/client/WebSocketTransport.mjs +6 -6
  22. package/dist/client/WebSocketTransport.mjs.map +1 -1
  23. package/dist/primitives/Tree.cjs +58 -8
  24. package/dist/primitives/Tree.d.cts +99 -10
  25. package/dist/primitives/Tree.d.cts.map +1 -1
  26. package/dist/primitives/Tree.d.mts +99 -10
  27. package/dist/primitives/Tree.d.mts.map +1 -1
  28. package/dist/primitives/Tree.mjs +58 -8
  29. package/dist/primitives/Tree.mjs.map +1 -1
  30. package/dist/primitives/shared.d.cts +9 -0
  31. package/dist/primitives/shared.d.cts.map +1 -1
  32. package/dist/primitives/shared.d.mts +9 -0
  33. package/dist/primitives/shared.d.mts.map +1 -1
  34. package/dist/primitives/shared.mjs.map +1 -1
  35. package/dist/server/ServerDocument.cjs +1 -1
  36. package/dist/server/ServerDocument.d.cts +3 -3
  37. package/dist/server/ServerDocument.d.cts.map +1 -1
  38. package/dist/server/ServerDocument.d.mts +3 -3
  39. package/dist/server/ServerDocument.d.mts.map +1 -1
  40. package/dist/server/ServerDocument.mjs +1 -1
  41. package/dist/server/ServerDocument.mjs.map +1 -1
  42. package/package.json +2 -2
  43. package/src/Document.ts +18 -5
  44. package/src/client/ClientDocument.ts +20 -21
  45. package/src/client/WebSocketTransport.ts +9 -9
  46. package/src/primitives/Tree.ts +213 -19
  47. package/src/primitives/shared.ts +10 -1
  48. package/src/server/ServerDocument.ts +4 -3
  49. package/tests/client/ClientDocument.test.ts +309 -2
  50. package/tests/client/WebSocketTransport.test.ts +228 -3
  51. package/tests/primitives/Tree.test.ts +296 -17
  52. package/tests/server/ServerDocument.test.ts +1 -1
  53. 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
  }
@@ -96,6 +96,8 @@ const generateTreePosBetween = (left: string | null, right: string | null): stri
96
96
  */
97
97
  export type TreeNodeSnapshot<TNode extends AnyTreeNodePrimitive> = {
98
98
  readonly id: string;
99
+ readonly pos: string;
100
+ readonly parentId: string | null;
99
101
  readonly type: InferTreeNodeType<TNode>;
100
102
  readonly children: TreeNodeSnapshot<InferTreeNodeChildren<TNode>>[];
101
103
  } & InferTreeNodeDataState<TNode>;
@@ -121,15 +123,128 @@ export type TreeNodeUpdateValue<TNode extends AnyTreeNodePrimitive> =
121
123
  * Uses StructSetInput directly so that:
122
124
  * - Fields that are required AND have no default must be provided
123
125
  * - Fields that are optional OR have defaults can be omitted
124
- *
126
+ *
125
127
  * This bypasses the struct-level NeedsValue wrapper since tree inserts
126
128
  * always require a data object (even if empty for all-optional fields).
127
129
  */
128
- export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
130
+ export type TreeNodeDataSetInput<TNode extends AnyTreeNodePrimitive> =
129
131
  TNode["data"] extends StructPrimitive<infer TFields, any, any>
130
132
  ? StructSetInput<TFields>
131
133
  : InferSetInput<TNode["data"]>;
132
134
 
135
+ // =============================================================================
136
+ // Nested Input Types for set() and default()
137
+ // =============================================================================
138
+
139
+ /**
140
+ * Type guard to check if a type is `any` or unknown (structural check).
141
+ * Returns true if T is `any`, false otherwise.
142
+ */
143
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
144
+
145
+ /**
146
+ * Get children types, with special handling for self-referential nodes.
147
+ * When InferTreeNodeChildren returns `any` (from TreeNodePrimitive<..., any>),
148
+ * we fall back to using TNode itself as its own child type.
149
+ * This handles the common case of self-referential nodes like:
150
+ * FolderNode = TreeNode("folder", { children: [TreeNodeSelf, FileNode] })
151
+ * Where FolderNode's TChildren is resolved to FolderNode | FileNode,
152
+ * but the self-referenced FolderNode has TChildren = any.
153
+ */
154
+ type ResolveChildrenForInput<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
155
+ IsAny<InferTreeNodeChildren<TNode>> extends true
156
+ ? TOriginalNode // Self-reference: use the original node's type
157
+ : InferTreeNodeChildren<TNode>;
158
+
159
+ /**
160
+ * Helper type that creates a properly typed node input for a specific node type.
161
+ * This is the "strict" version that enforces exact property matching.
162
+ *
163
+ * The TOriginalNode parameter is used to track the original root node type
164
+ * for proper self-reference resolution in deeply nested structures.
165
+ */
166
+ type TreeNodeSetInputStrictWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> = {
167
+ readonly type: InferTreeNodeType<TNode>;
168
+ readonly id?: string;
169
+ readonly children: TreeNodeSetInputUnionWithRoot<ResolveChildrenForInput<TNode, TOriginalNode>, TOriginalNode>[];
170
+ } & TreeNodeDataSetInput<TNode>;
171
+
172
+ /**
173
+ * Distributive conditional type that creates a union of TreeNodeSetInputStrict
174
+ * for each node type in the union. This ensures proper type discrimination.
175
+ *
176
+ * When TNode is a union (e.g., FolderNode | FileNode), this distributes to:
177
+ * TreeNodeSetInputStrict<FolderNode> | TreeNodeSetInputStrict<FileNode>
178
+ *
179
+ * This creates a proper discriminated union where excess property checking works.
180
+ */
181
+ type TreeNodeSetInputUnionWithRoot<TNode extends AnyTreeNodePrimitive, TOriginalNode extends AnyTreeNodePrimitive> =
182
+ TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrictWithRoot<TNode, TOriginalNode> : never;
183
+
184
+ /**
185
+ * Helper type for single-parameter usage - uses TNode as its own original.
186
+ */
187
+ type TreeNodeSetInputStrict<TNode extends AnyTreeNodePrimitive> =
188
+ TreeNodeSetInputStrictWithRoot<TNode, TNode>;
189
+
190
+ /**
191
+ * Distributive conditional for single-parameter usage.
192
+ */
193
+ export type TreeNodeSetInputUnion<TNode extends AnyTreeNodePrimitive> =
194
+ TNode extends AnyTreeNodePrimitive ? TreeNodeSetInputStrict<TNode> : never;
195
+
196
+ /**
197
+ * Input type for a single node in a nested tree set/default operation.
198
+ *
199
+ * - `type` is REQUIRED - explicit type discriminator for the node
200
+ * - `id` is optional - auto-generated if not provided
201
+ * - `children` is a typed array of allowed child node inputs
202
+ * - Data fields are spread at the node level (like TreeNodeSnapshot)
203
+ *
204
+ * When TNode is a union type (e.g., from InferTreeNodeChildren), this properly
205
+ * distributes to create a discriminated union where:
206
+ * - Each variant has its specific `type` literal
207
+ * - Each variant has its specific data fields
208
+ * - Excess property checking works correctly
209
+ *
210
+ * @example
211
+ * ```typescript
212
+ * const input: TreeNodeSetInput<BoardNode> = {
213
+ * type: "board",
214
+ * name: "My Board",
215
+ * children: [
216
+ * { type: "column", name: "Todo", children: [] }
217
+ * ]
218
+ * };
219
+ * ```
220
+ */
221
+ export type TreeNodeSetInput<TNode extends AnyTreeNodePrimitive> = TreeNodeSetInputUnion<TNode>;
222
+
223
+ /**
224
+ * Input type for tree set() and default() operations.
225
+ * Accepts a nested tree structure that will be converted to flat TreeState internally.
226
+ */
227
+ export type TreeSetInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
228
+
229
+ /**
230
+ * Infer the set input type for a tree primitive.
231
+ */
232
+ export type InferTreeSetInput<T extends TreePrimitive<any>> =
233
+ T extends TreePrimitive<infer TRoot> ? TreeSetInput<TRoot> : never;
234
+
235
+ /**
236
+ * Internal type for processing any node input during conversion.
237
+ * This is only used internally in _convertNestedToFlat and is not exported.
238
+ * Allows us to keep the public TreeNodeSetInput<TNode> fully type-safe while
239
+ * having a runtime-compatible type for internal processing.
240
+ */
241
+ type InternalNodeInput = {
242
+ readonly type: string;
243
+ readonly id?: string;
244
+ readonly children: InternalNodeInput[];
245
+ readonly [key: string]: unknown;
246
+ };
247
+
133
248
  /**
134
249
  * Typed proxy for a specific node type - provides type-safe data access
135
250
  */
@@ -172,9 +287,9 @@ export interface TreeNodeProxyBase<_TRoot extends AnyTreeNodePrimitive> {
172
287
  export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
173
288
  /** Gets the entire tree state (flat array of nodes) */
174
289
  get(): TreeState<TRoot>;
175
-
176
- /** Replaces the entire tree */
177
- set(nodes: TreeState<TRoot>): void;
290
+
291
+ /** Replaces the entire tree with a nested input structure */
292
+ set(input: TreeSetInput<TRoot>): void;
178
293
 
179
294
  /** Gets the root node state */
180
295
  root(): TypedTreeNodeState<TRoot> | undefined;
@@ -258,16 +373,13 @@ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
258
373
 
259
374
  interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
260
375
  readonly required: boolean;
261
- readonly defaultValue: TreeState<TRoot> | undefined;
376
+ readonly defaultInput: TreeNodeSetInput<TRoot> | undefined;
262
377
  readonly root: TRoot;
263
378
  readonly validators: readonly Validator<TreeState<TRoot>>[];
264
379
  }
265
380
 
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>;
381
+ /** Input type for tree update() - same as set() for trees (nested format) */
382
+ export type TreeUpdateInput<TRoot extends AnyTreeNodePrimitive> = TreeNodeSetInput<TRoot>;
271
383
 
272
384
  export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends boolean = false, THasDefault extends boolean = false>
273
385
  implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>, TRequired, THasDefault, TreeSetInput<TRoot>, TreeUpdateInput<TRoot>>
@@ -322,11 +434,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
322
434
  });
323
435
  }
324
436
 
325
- /** Set a default value for this tree */
326
- default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot, TRequired, true> {
437
+ /** Set a default value for this tree (nested format) */
438
+ default(defaultInput: TreeNodeSetInput<TRoot>): TreePrimitive<TRoot, TRequired, true> {
327
439
  return new TreePrimitive({
328
440
  ...this._schema,
329
- defaultValue,
441
+ defaultInput,
330
442
  });
331
443
  }
332
444
 
@@ -408,6 +520,78 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
408
520
  }
409
521
  }
410
522
 
523
+ /**
524
+ * Convert a nested TreeNodeSetInput to flat TreeState format.
525
+ * Recursively processes nodes, generating IDs and positions as needed.
526
+ *
527
+ * @param input - The root nested input to convert
528
+ * @param generateId - Optional ID generator (defaults to crypto.randomUUID)
529
+ * @returns Flat TreeState array
530
+ */
531
+ private _convertNestedToFlat(
532
+ input: TreeNodeSetInput<TRoot>,
533
+ generateId: () => string = () => crypto.randomUUID()
534
+ ): TreeState<TRoot> {
535
+ const result: TreeNodeState[] = [];
536
+ const seenIds = new Set<string>();
537
+
538
+ const processNode = (
539
+ nodeInput: InternalNodeInput,
540
+ parentId: string | null,
541
+ parentType: string | null,
542
+ leftPos: string | null,
543
+ rightPos: string | null
544
+ ): void => {
545
+ // Validate node type
546
+ this._validateChildType(parentType, nodeInput.type);
547
+
548
+ // Get the node primitive for this type
549
+ const nodePrimitive = this._getNodeTypePrimitive(nodeInput.type);
550
+
551
+ // Generate or use provided ID
552
+ const id = nodeInput.id ?? generateId();
553
+
554
+ // Check for duplicate IDs
555
+ if (seenIds.has(id)) {
556
+ throw new ValidationError(`Duplicate node ID: ${id}`);
557
+ }
558
+ seenIds.add(id);
559
+
560
+ // Generate position
561
+ const pos = generateTreePosBetween(leftPos, rightPos);
562
+
563
+ // Extract data fields (everything except type, id, and children)
564
+ const { type: _type, id: _id, children, ...dataFields } = nodeInput;
565
+
566
+ // Apply defaults to node data
567
+ const mergedData = applyDefaults(nodePrimitive.data as AnyPrimitive, dataFields);
568
+
569
+ // Add this node to result
570
+ result.push({
571
+ id,
572
+ type: nodeInput.type,
573
+ parentId,
574
+ pos,
575
+ data: mergedData,
576
+ });
577
+
578
+ // Process children recursively
579
+ let prevChildPos: string | null = null;
580
+ for (let i = 0; i < children.length; i++) {
581
+ const childInput = children[i]!;
582
+ // Each child gets a position after the previous child
583
+ processNode(childInput, id, nodeInput.type, prevChildPos, null);
584
+ // Update prevChildPos to the pos that was just assigned (it's the last item in result)
585
+ prevChildPos = result[result.length - 1]!.pos;
586
+ }
587
+ };
588
+
589
+ // Process root node (cast to InternalNodeInput for internal processing)
590
+ processNode(input as unknown as InternalNodeInput, null, null, null, null);
591
+
592
+ return result as TreeState<TRoot>;
593
+ }
594
+
411
595
  readonly _internal: PrimitiveInternal<TreeState<TRoot>, TreeProxy<TRoot>> = {
412
596
  createProxy: (
413
597
  env: ProxyEnvironment.ProxyEnvironment,
@@ -486,6 +670,8 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
486
670
  return {
487
671
  id: node.id,
488
672
  type: node.type,
673
+ pos: node.pos,
674
+ parentId: node.parentId,
489
675
  ...(node.data as object),
490
676
  children,
491
677
  } as unknown as TreeNodeSnapshot<TRoot>;
@@ -496,9 +682,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
496
682
  return getCurrentState();
497
683
  },
498
684
 
499
- set: (nodes: TreeState<TRoot>) => {
685
+ set: (input: TreeSetInput<TRoot>) => {
686
+ // Convert nested input to flat TreeState using env.generateId for IDs
687
+ const flatState = this._convertNestedToFlat(input, env.generateId);
500
688
  env.addOperation(
501
- Operation.fromDefinition(operationPath, this._opDefinitions.set, nodes)
689
+ Operation.fromDefinition(operationPath, this._opDefinitions.set, flatState)
502
690
  );
503
691
  },
504
692
 
@@ -1074,8 +1262,9 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
1074
1262
  },
1075
1263
 
1076
1264
  getInitialState: (): TreeState<TRoot> | undefined => {
1077
- if (this._schema.defaultValue !== undefined) {
1078
- return this._schema.defaultValue;
1265
+ if (this._schema.defaultInput !== undefined) {
1266
+ // Convert nested input to flat TreeState
1267
+ return this._convertNestedToFlat(this._schema.defaultInput);
1079
1268
  }
1080
1269
 
1081
1270
  // Automatically create a root node with default data
@@ -1093,6 +1282,11 @@ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive, TRequired extends
1093
1282
  }] as TreeState<TRoot>;
1094
1283
  },
1095
1284
 
1285
+ convertSetInputToState: (input: unknown): TreeState<TRoot> => {
1286
+ // Convert nested input format to flat TreeState
1287
+ return this._convertNestedToFlat(input as TreeNodeSetInput<TRoot>);
1288
+ },
1289
+
1096
1290
  transformOperation: (
1097
1291
  clientOp: Operation.Operation<any, any, any>,
1098
1292
  serverOp: Operation.Operation<any, any, any>
@@ -1201,7 +1395,7 @@ export const Tree = <TRoot extends AnyTreeNodePrimitive>(
1201
1395
  ): TreePrimitive<TRoot, false, false> =>
1202
1396
  new TreePrimitive({
1203
1397
  required: false,
1204
- defaultValue: undefined,
1398
+ defaultInput: undefined,
1205
1399
  root: options.root,
1206
1400
  validators: [],
1207
1401
  });
@@ -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