@voidhash/mimic 0.0.1-alpha.1

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 (57) hide show
  1. package/README.md +17 -0
  2. package/package.json +33 -0
  3. package/src/Document.ts +256 -0
  4. package/src/FractionalIndex.ts +1249 -0
  5. package/src/Operation.ts +59 -0
  6. package/src/OperationDefinition.ts +23 -0
  7. package/src/OperationPath.ts +197 -0
  8. package/src/Presence.ts +142 -0
  9. package/src/Primitive.ts +32 -0
  10. package/src/Proxy.ts +8 -0
  11. package/src/ProxyEnvironment.ts +52 -0
  12. package/src/Transaction.ts +72 -0
  13. package/src/Transform.ts +13 -0
  14. package/src/client/ClientDocument.ts +1163 -0
  15. package/src/client/Rebase.ts +309 -0
  16. package/src/client/StateMonitor.ts +307 -0
  17. package/src/client/Transport.ts +318 -0
  18. package/src/client/WebSocketTransport.ts +572 -0
  19. package/src/client/errors.ts +145 -0
  20. package/src/client/index.ts +61 -0
  21. package/src/index.ts +12 -0
  22. package/src/primitives/Array.ts +457 -0
  23. package/src/primitives/Boolean.ts +128 -0
  24. package/src/primitives/Lazy.ts +89 -0
  25. package/src/primitives/Literal.ts +128 -0
  26. package/src/primitives/Number.ts +169 -0
  27. package/src/primitives/String.ts +189 -0
  28. package/src/primitives/Struct.ts +348 -0
  29. package/src/primitives/Tree.ts +1120 -0
  30. package/src/primitives/TreeNode.ts +113 -0
  31. package/src/primitives/Union.ts +329 -0
  32. package/src/primitives/shared.ts +122 -0
  33. package/src/server/ServerDocument.ts +267 -0
  34. package/src/server/errors.ts +90 -0
  35. package/src/server/index.ts +40 -0
  36. package/tests/Document.test.ts +556 -0
  37. package/tests/FractionalIndex.test.ts +377 -0
  38. package/tests/OperationPath.test.ts +151 -0
  39. package/tests/Presence.test.ts +321 -0
  40. package/tests/Primitive.test.ts +381 -0
  41. package/tests/client/ClientDocument.test.ts +1398 -0
  42. package/tests/client/WebSocketTransport.test.ts +992 -0
  43. package/tests/primitives/Array.test.ts +418 -0
  44. package/tests/primitives/Boolean.test.ts +126 -0
  45. package/tests/primitives/Lazy.test.ts +143 -0
  46. package/tests/primitives/Literal.test.ts +122 -0
  47. package/tests/primitives/Number.test.ts +133 -0
  48. package/tests/primitives/String.test.ts +128 -0
  49. package/tests/primitives/Struct.test.ts +311 -0
  50. package/tests/primitives/Tree.test.ts +467 -0
  51. package/tests/primitives/TreeNode.test.ts +50 -0
  52. package/tests/primitives/Union.test.ts +210 -0
  53. package/tests/server/ServerDocument.test.ts +528 -0
  54. package/tsconfig.build.json +24 -0
  55. package/tsconfig.json +8 -0
  56. package/tsdown.config.ts +18 -0
  57. package/vitest.mts +11 -0
@@ -0,0 +1,1120 @@
1
+ import { Effect, Schema } from "effect";
2
+ import * as OperationDefinition from "../OperationDefinition";
3
+ import * as Operation from "../Operation";
4
+ import * as OperationPath from "../OperationPath";
5
+ import * as ProxyEnvironment from "../ProxyEnvironment";
6
+ import * as Transform from "../Transform";
7
+ import * as FractionalIndex from "../FractionalIndex";
8
+ import type { Primitive, PrimitiveInternal, Validator, InferProxy } from "./shared";
9
+ import { ValidationError } from "./shared";
10
+ import { runValidators } from "./shared";
11
+ import type { AnyTreeNodePrimitive, InferTreeNodeType, InferTreeNodeDataState, InferTreeNodeChildren } from "./TreeNode";
12
+ import { InferStructState } from "./Struct";
13
+
14
+
15
+ /**
16
+ * A node in the tree state (flat storage format)
17
+ */
18
+ export interface TreeNodeState {
19
+ readonly id: string; // Unique node identifier (UUID)
20
+ readonly type: string; // Node type discriminator
21
+ readonly parentId: string | null; // Parent node ID (null for root)
22
+ readonly pos: string; // Fractional index for sibling ordering
23
+ readonly data: unknown; // Node-specific data
24
+ }
25
+
26
+ /**
27
+ * Typed node state for a specific node type
28
+ */
29
+ export interface TypedTreeNodeState<TNode extends AnyTreeNodePrimitive> {
30
+ readonly id: string;
31
+ readonly type: InferTreeNodeType<TNode>;
32
+ readonly parentId: string | null;
33
+ readonly pos: string;
34
+ readonly data: InferTreeNodeDataState<TNode>;
35
+ }
36
+
37
+ /**
38
+ * The state type for trees - a flat array of nodes
39
+ */
40
+ export type TreeState<TRoot extends AnyTreeNodePrimitive> = readonly TreeNodeState[];
41
+
42
+ /**
43
+ * Helper to get children sorted by position
44
+ */
45
+ const getOrderedChildren = (
46
+ nodes: readonly TreeNodeState[],
47
+ parentId: string | null
48
+ ): TreeNodeState[] => {
49
+ return [...nodes]
50
+ .filter(n => n.parentId === parentId)
51
+ .sort((a, b) => a.pos < b.pos ? -1 : a.pos > b.pos ? 1 : 0);
52
+ };
53
+
54
+ /**
55
+ * Get all descendant IDs of a node (recursive)
56
+ */
57
+ const getDescendantIds = (
58
+ nodes: readonly TreeNodeState[],
59
+ nodeId: string
60
+ ): string[] => {
61
+ const children = nodes.filter(n => n.parentId === nodeId);
62
+ const descendantIds: string[] = [];
63
+ for (const child of children) {
64
+ descendantIds.push(child.id);
65
+ descendantIds.push(...getDescendantIds(nodes, child.id));
66
+ }
67
+ return descendantIds;
68
+ };
69
+
70
+ /**
71
+ * Check if moving a node to a new parent would create a cycle
72
+ */
73
+ const wouldCreateCycle = (
74
+ nodes: readonly TreeNodeState[],
75
+ nodeId: string,
76
+ newParentId: string | null
77
+ ): boolean => {
78
+ if (newParentId === null) return false;
79
+ if (newParentId === nodeId) return true;
80
+
81
+ const descendants = getDescendantIds(nodes, nodeId);
82
+ return descendants.includes(newParentId);
83
+ };
84
+
85
+ /**
86
+ * Generate a fractional position between two positions
87
+ */
88
+ const generateTreePosBetween = (left: string | null, right: string | null): string => {
89
+ const charSet = FractionalIndex.base62CharSet();
90
+ return Effect.runSync(FractionalIndex.generateKeyBetween(left, right, charSet));
91
+ };
92
+
93
+ /**
94
+ * Snapshot of a single node for UI rendering (data properties spread at node level)
95
+ */
96
+ export type TreeNodeSnapshot<TNode extends AnyTreeNodePrimitive> = {
97
+ readonly id: string;
98
+ readonly type: InferTreeNodeType<TNode>;
99
+ readonly children: TreeNodeSnapshot<InferTreeNodeChildren<TNode>>[];
100
+ } & InferTreeNodeDataState<TNode>;
101
+
102
+ /**
103
+ * Infer the snapshot type for a tree (recursive tree structure for UI)
104
+ */
105
+ export type InferTreeSnapshot<T extends TreePrimitive<any>> =
106
+ T extends TreePrimitive<infer TRoot> ? TreeNodeSnapshot<TRoot> : never;
107
+
108
+ /**
109
+ * Typed proxy for a specific node type - provides type-safe data access
110
+ */
111
+ export interface TypedNodeProxy<TNode extends AnyTreeNodePrimitive> {
112
+ /** The node ID */
113
+ readonly id: string;
114
+ /** The node type */
115
+ readonly type: InferTreeNodeType<TNode>;
116
+ /** Access the node's data proxy */
117
+ readonly data: InferProxy<TNode["data"]>;
118
+ /** Get the raw node state */
119
+ get(): TypedTreeNodeState<TNode>;
120
+ }
121
+
122
+ /**
123
+ * Node proxy with type narrowing capabilities
124
+ */
125
+ export interface TreeNodeProxyBase<TRoot extends AnyTreeNodePrimitive> {
126
+ /** The node ID */
127
+ readonly id: string;
128
+ /** The node type (string) */
129
+ readonly type: string;
130
+ /** Type guard - narrows the proxy to a specific node type */
131
+ is<TNode extends AnyTreeNodePrimitive>(
132
+ nodeType: TNode
133
+ ): this is TypedNodeProxy<TNode>;
134
+ /** Type assertion - returns typed proxy (throws if wrong type) */
135
+ as<TNode extends AnyTreeNodePrimitive>(
136
+ nodeType: TNode
137
+ ): TypedNodeProxy<TNode>;
138
+ /** Get the raw node state */
139
+ get(): TreeNodeState;
140
+ }
141
+
142
+ /**
143
+ * Proxy for accessing and modifying tree nodes
144
+ */
145
+ export interface TreeProxy<TRoot extends AnyTreeNodePrimitive> {
146
+ /** Gets the entire tree state (flat array of nodes) */
147
+ get(): TreeState<TRoot>;
148
+
149
+ /** Replaces the entire tree */
150
+ set(nodes: TreeState<TRoot>): void;
151
+
152
+ /** Gets the root node state */
153
+ root(): TypedTreeNodeState<TRoot> | undefined;
154
+
155
+ /** Gets ordered children states of a parent (null for root's children) */
156
+ children(parentId: string | null): TreeNodeState[];
157
+
158
+ /** Gets a node proxy by ID with type narrowing capabilities */
159
+ node(id: string): TreeNodeProxyBase<TRoot> | undefined;
160
+
161
+ /** Insert a new node as the first child */
162
+ insertFirst<TNode extends AnyTreeNodePrimitive>(
163
+ parentId: string | null,
164
+ nodeType: TNode,
165
+ data: InferTreeNodeDataState<TNode>
166
+ ): string;
167
+
168
+ /** Insert a new node as the last child */
169
+ insertLast<TNode extends AnyTreeNodePrimitive>(
170
+ parentId: string | null,
171
+ nodeType: TNode,
172
+ data: InferTreeNodeDataState<TNode>
173
+ ): string;
174
+
175
+ /** Insert a new node at a specific index among siblings */
176
+ insertAt<TNode extends AnyTreeNodePrimitive>(
177
+ parentId: string | null,
178
+ index: number,
179
+ nodeType: TNode,
180
+ data: InferTreeNodeDataState<TNode>
181
+ ): string;
182
+
183
+ /** Insert a new node after a sibling */
184
+ insertAfter<TNode extends AnyTreeNodePrimitive>(
185
+ siblingId: string,
186
+ nodeType: TNode,
187
+ data: InferTreeNodeDataState<TNode>
188
+ ): string;
189
+
190
+ /** Insert a new node before a sibling */
191
+ insertBefore<TNode extends AnyTreeNodePrimitive>(
192
+ siblingId: string,
193
+ nodeType: TNode,
194
+ data: InferTreeNodeDataState<TNode>
195
+ ): string;
196
+
197
+ /** Remove a node and all its descendants */
198
+ remove(id: string): void;
199
+
200
+ /** Move a node to a new parent at a specific index */
201
+ move(nodeId: string, newParentId: string | null, toIndex: number): void;
202
+
203
+ /** Move a node after a sibling */
204
+ moveAfter(nodeId: string, siblingId: string): void;
205
+
206
+ /** Move a node before a sibling */
207
+ moveBefore(nodeId: string, siblingId: string): void;
208
+
209
+ /** Move a node to be the first child of a parent */
210
+ moveToFirst(nodeId: string, newParentId: string | null): void;
211
+
212
+ /** Move a node to be the last child of a parent */
213
+ moveToLast(nodeId: string, newParentId: string | null): void;
214
+
215
+ /** Returns a typed proxy for a specific node's data */
216
+ at<TNode extends AnyTreeNodePrimitive>(
217
+ id: string,
218
+ nodeType: TNode
219
+ ): InferProxy<TNode["data"]>;
220
+
221
+ /** Convert tree to a nested snapshot for UI rendering */
222
+ toSnapshot(): TreeNodeSnapshot<TRoot> | undefined;
223
+ }
224
+
225
+ interface TreePrimitiveSchema<TRoot extends AnyTreeNodePrimitive> {
226
+ readonly required: boolean;
227
+ readonly defaultValue: TreeState<TRoot> | undefined;
228
+ readonly root: TRoot;
229
+ readonly validators: readonly Validator<TreeState<TRoot>>[];
230
+ }
231
+
232
+ export class TreePrimitive<TRoot extends AnyTreeNodePrimitive>
233
+ implements Primitive<TreeState<TRoot>, TreeProxy<TRoot>>
234
+ {
235
+ readonly _tag = "TreePrimitive" as const;
236
+ readonly _State!: TreeState<TRoot>;
237
+ readonly _Proxy!: TreeProxy<TRoot>;
238
+
239
+ private readonly _schema: TreePrimitiveSchema<TRoot>;
240
+ private _nodeTypeRegistry: Map<string, AnyTreeNodePrimitive> | undefined;
241
+
242
+ private readonly _opDefinitions = {
243
+ set: OperationDefinition.make({
244
+ kind: "tree.set" as const,
245
+ payload: Schema.Unknown,
246
+ target: Schema.Unknown,
247
+ apply: (payload) => payload,
248
+ }),
249
+ insert: OperationDefinition.make({
250
+ kind: "tree.insert" as const,
251
+ payload: Schema.Unknown,
252
+ target: Schema.Unknown,
253
+ apply: (payload) => payload,
254
+ }),
255
+ remove: OperationDefinition.make({
256
+ kind: "tree.remove" as const,
257
+ payload: Schema.Unknown,
258
+ target: Schema.Unknown,
259
+ apply: (payload) => payload,
260
+ }),
261
+ move: OperationDefinition.make({
262
+ kind: "tree.move" as const,
263
+ payload: Schema.Unknown,
264
+ target: Schema.Unknown,
265
+ apply: (payload) => payload,
266
+ }),
267
+ };
268
+
269
+ constructor(schema: TreePrimitiveSchema<TRoot>) {
270
+ this._schema = schema;
271
+ }
272
+
273
+ /** Mark this tree as required */
274
+ required(): TreePrimitive<TRoot> {
275
+ return new TreePrimitive({
276
+ ...this._schema,
277
+ required: true,
278
+ });
279
+ }
280
+
281
+ /** Set a default value for this tree */
282
+ default(defaultValue: TreeState<TRoot>): TreePrimitive<TRoot> {
283
+ return new TreePrimitive({
284
+ ...this._schema,
285
+ defaultValue,
286
+ });
287
+ }
288
+
289
+ /** Get the root node type */
290
+ get root(): TRoot {
291
+ return this._schema.root;
292
+ }
293
+
294
+ /** Add a custom validation rule */
295
+ refine(fn: (value: TreeState<TRoot>) => boolean, message: string): TreePrimitive<TRoot> {
296
+ return new TreePrimitive({
297
+ ...this._schema,
298
+ validators: [...this._schema.validators, { validate: fn, message }],
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Build a registry of all node types reachable from root
304
+ */
305
+ private _buildNodeTypeRegistry(): Map<string, AnyTreeNodePrimitive> {
306
+ if (this._nodeTypeRegistry !== undefined) {
307
+ return this._nodeTypeRegistry;
308
+ }
309
+
310
+ const registry = new Map<string, AnyTreeNodePrimitive>();
311
+ const visited = new Set<string>();
312
+
313
+ const visit = (node: AnyTreeNodePrimitive) => {
314
+ if (visited.has(node.type)) return;
315
+ visited.add(node.type);
316
+ registry.set(node.type, node);
317
+
318
+ for (const child of node.children) {
319
+ visit(child);
320
+ }
321
+ };
322
+
323
+ visit(this._schema.root);
324
+ this._nodeTypeRegistry = registry;
325
+ return registry;
326
+ }
327
+
328
+ /**
329
+ * Get a node type primitive by its type string
330
+ */
331
+ private _getNodeTypePrimitive(type: string): AnyTreeNodePrimitive {
332
+ const registry = this._buildNodeTypeRegistry();
333
+ const nodeType = registry.get(type);
334
+ if (!nodeType) {
335
+ throw new ValidationError(`Unknown node type: ${type}`);
336
+ }
337
+ return nodeType;
338
+ }
339
+
340
+ /**
341
+ * Validate that a node type can be a child of a parent node type
342
+ */
343
+ private _validateChildType(
344
+ parentType: string | null,
345
+ childType: string
346
+ ): void {
347
+ if (parentType === null) {
348
+ // Root level - child must be the root type
349
+ if (childType !== this._schema.root.type) {
350
+ throw new ValidationError(
351
+ `Root node must be of type "${this._schema.root.type}", got "${childType}"`
352
+ );
353
+ }
354
+ return;
355
+ }
356
+
357
+ const parentNodePrimitive = this._getNodeTypePrimitive(parentType);
358
+ if (!parentNodePrimitive.isChildAllowed(childType)) {
359
+ const allowedTypes = parentNodePrimitive.children.map(c => c.type).join(", ");
360
+ throw new ValidationError(
361
+ `Node type "${childType}" is not allowed as a child of "${parentType}". ` +
362
+ `Allowed types: ${allowedTypes || "none"}`
363
+ );
364
+ }
365
+ }
366
+
367
+ readonly _internal: PrimitiveInternal<TreeState<TRoot>, TreeProxy<TRoot>> = {
368
+ createProxy: (
369
+ env: ProxyEnvironment.ProxyEnvironment,
370
+ operationPath: OperationPath.OperationPath
371
+ ): TreeProxy<TRoot> => {
372
+ // Helper to get current state
373
+ const getCurrentState = (): TreeState<TRoot> => {
374
+ const state = env.getState(operationPath) as TreeState<TRoot> | undefined;
375
+ return state ?? [];
376
+ };
377
+
378
+ // Helper to get parent type from state
379
+ const getParentType = (parentId: string | null): string | null => {
380
+ if (parentId === null) return null;
381
+ const state = getCurrentState();
382
+ const parent = state.find(n => n.id === parentId);
383
+ return parent?.type ?? null;
384
+ };
385
+
386
+ // Helper to create a node proxy with type narrowing
387
+ const createNodeProxy = (nodeState: TreeNodeState): TreeNodeProxyBase<TRoot> => {
388
+ return {
389
+ id: nodeState.id,
390
+ type: nodeState.type,
391
+
392
+ is: <TNode extends AnyTreeNodePrimitive>(
393
+ nodeType: TNode
394
+ ): boolean => {
395
+ return nodeState.type === nodeType.type;
396
+ },
397
+
398
+ as: <TNode extends AnyTreeNodePrimitive>(
399
+ nodeType: TNode
400
+ ): TypedNodeProxy<TNode> => {
401
+ if (nodeState.type !== nodeType.type) {
402
+ throw new ValidationError(
403
+ `Node is of type "${nodeState.type}", not "${nodeType.type}"`
404
+ );
405
+ }
406
+ const nodePath = operationPath.append(nodeState.id);
407
+ return {
408
+ id: nodeState.id,
409
+ type: nodeType.type as InferTreeNodeType<TNode>,
410
+ data: nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>,
411
+ get: () => nodeState as TypedTreeNodeState<TNode>,
412
+ };
413
+ },
414
+
415
+ get: () => nodeState,
416
+ } as TreeNodeProxyBase<TRoot>;
417
+ };
418
+
419
+ // Helper to build recursive snapshot
420
+ const buildSnapshot = (
421
+ nodeId: string,
422
+ nodes: readonly TreeNodeState[]
423
+ ): TreeNodeSnapshot<TRoot> | undefined => {
424
+ const node = nodes.find(n => n.id === nodeId);
425
+ if (!node) return undefined;
426
+
427
+ const childNodes = getOrderedChildren(nodes, nodeId);
428
+ const children: TreeNodeSnapshot<any>[] = [];
429
+ for (const child of childNodes) {
430
+ const childSnapshot = buildSnapshot(child.id, nodes);
431
+ if (childSnapshot) {
432
+ children.push(childSnapshot);
433
+ }
434
+ }
435
+
436
+ // Spread data properties at node level
437
+ return {
438
+ id: node.id,
439
+ type: node.type,
440
+ ...(node.data as object),
441
+ children,
442
+ } as unknown as TreeNodeSnapshot<TRoot>;
443
+ };
444
+
445
+ return {
446
+ get: (): TreeState<TRoot> => {
447
+ return getCurrentState();
448
+ },
449
+
450
+ set: (nodes: TreeState<TRoot>) => {
451
+ env.addOperation(
452
+ Operation.fromDefinition(operationPath, this._opDefinitions.set, nodes)
453
+ );
454
+ },
455
+
456
+ root: (): TypedTreeNodeState<TRoot> | undefined => {
457
+ const state = getCurrentState();
458
+ const rootNode = state.find(n => n.parentId === null);
459
+ return rootNode as TypedTreeNodeState<TRoot> | undefined;
460
+ },
461
+
462
+ children: (parentId: string | null): TreeNodeState[] => {
463
+ const state = getCurrentState();
464
+ return getOrderedChildren(state, parentId);
465
+ },
466
+
467
+ node: (id: string): TreeNodeProxyBase<TRoot> | undefined => {
468
+ const state = getCurrentState();
469
+ const nodeState = state.find(n => n.id === id);
470
+ if (!nodeState) return undefined;
471
+ return createNodeProxy(nodeState);
472
+ },
473
+
474
+ insertFirst: <TNode extends AnyTreeNodePrimitive>(
475
+ parentId: string | null,
476
+ nodeType: TNode,
477
+ data: InferTreeNodeDataState<TNode>
478
+ ): string => {
479
+ const state = getCurrentState();
480
+ const siblings = getOrderedChildren(state, parentId);
481
+ const firstPos = siblings.length > 0 ? siblings[0]!.pos : null;
482
+ const pos = generateTreePosBetween(null, firstPos);
483
+ const id = env.generateId();
484
+
485
+ // Validate parent exists (if not root)
486
+ if (parentId !== null && !state.find(n => n.id === parentId)) {
487
+ throw new ValidationError(`Parent node not found: ${parentId}`);
488
+ }
489
+
490
+ // Validate child type is allowed
491
+ const parentType = getParentType(parentId);
492
+ this._validateChildType(parentType, nodeType.type);
493
+
494
+ // Validate single root
495
+ if (parentId === null && state.some(n => n.parentId === null)) {
496
+ throw new ValidationError("Tree already has a root node");
497
+ }
498
+
499
+ env.addOperation(
500
+ Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
501
+ id,
502
+ type: nodeType.type,
503
+ parentId,
504
+ pos,
505
+ data,
506
+ })
507
+ );
508
+
509
+ return id;
510
+ },
511
+
512
+ insertLast: <TNode extends AnyTreeNodePrimitive>(
513
+ parentId: string | null,
514
+ nodeType: TNode,
515
+ data: InferTreeNodeDataState<TNode>
516
+ ): string => {
517
+ const state = getCurrentState();
518
+ const siblings = getOrderedChildren(state, parentId);
519
+ const lastPos = siblings.length > 0 ? siblings[siblings.length - 1]!.pos : null;
520
+ const pos = generateTreePosBetween(lastPos, null);
521
+ const id = env.generateId();
522
+
523
+ // Validate parent exists (if not root)
524
+ if (parentId !== null && !state.find(n => n.id === parentId)) {
525
+ throw new ValidationError(`Parent node not found: ${parentId}`);
526
+ }
527
+
528
+ // Validate child type is allowed
529
+ const parentType = getParentType(parentId);
530
+ this._validateChildType(parentType, nodeType.type);
531
+
532
+ // Validate single root
533
+ if (parentId === null && state.some(n => n.parentId === null)) {
534
+ throw new ValidationError("Tree already has a root node");
535
+ }
536
+
537
+ env.addOperation(
538
+ Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
539
+ id,
540
+ type: nodeType.type,
541
+ parentId,
542
+ pos,
543
+ data,
544
+ })
545
+ );
546
+
547
+ return id;
548
+ },
549
+
550
+ insertAt: <TNode extends AnyTreeNodePrimitive>(
551
+ parentId: string | null,
552
+ index: number,
553
+ nodeType: TNode,
554
+ data: InferTreeNodeDataState<TNode>
555
+ ): string => {
556
+ const state = getCurrentState();
557
+ const siblings = getOrderedChildren(state, parentId);
558
+ const clampedIndex = Math.max(0, Math.min(index, siblings.length));
559
+ const leftPos = clampedIndex > 0 && siblings[clampedIndex - 1] ? siblings[clampedIndex - 1]!.pos : null;
560
+ const rightPos = clampedIndex < siblings.length && siblings[clampedIndex] ? siblings[clampedIndex]!.pos : null;
561
+ const pos = generateTreePosBetween(leftPos, rightPos);
562
+ const id = env.generateId();
563
+
564
+ // Validate parent exists (if not root)
565
+ if (parentId !== null && !state.find(n => n.id === parentId)) {
566
+ throw new ValidationError(`Parent node not found: ${parentId}`);
567
+ }
568
+
569
+ // Validate child type is allowed
570
+ const parentType = getParentType(parentId);
571
+ this._validateChildType(parentType, nodeType.type);
572
+
573
+ // Validate single root
574
+ if (parentId === null && state.some(n => n.parentId === null)) {
575
+ throw new ValidationError("Tree already has a root node");
576
+ }
577
+
578
+ env.addOperation(
579
+ Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
580
+ id,
581
+ type: nodeType.type,
582
+ parentId,
583
+ pos,
584
+ data,
585
+ })
586
+ );
587
+
588
+ return id;
589
+ },
590
+
591
+ insertAfter: <TNode extends AnyTreeNodePrimitive>(
592
+ siblingId: string,
593
+ nodeType: TNode,
594
+ data: InferTreeNodeDataState<TNode>
595
+ ): string => {
596
+ const state = getCurrentState();
597
+ const sibling = state.find(n => n.id === siblingId);
598
+ if (!sibling) {
599
+ throw new ValidationError(`Sibling node not found: ${siblingId}`);
600
+ }
601
+
602
+ const parentId = sibling.parentId;
603
+ const siblings = getOrderedChildren(state, parentId);
604
+ const siblingIndex = siblings.findIndex(n => n.id === siblingId);
605
+ const nextSibling = siblings[siblingIndex + 1];
606
+ const pos = generateTreePosBetween(sibling.pos, nextSibling?.pos ?? null);
607
+ const id = env.generateId();
608
+
609
+ // Validate child type is allowed
610
+ const parentType = getParentType(parentId);
611
+ this._validateChildType(parentType, nodeType.type);
612
+
613
+ env.addOperation(
614
+ Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
615
+ id,
616
+ type: nodeType.type,
617
+ parentId,
618
+ pos,
619
+ data,
620
+ })
621
+ );
622
+
623
+ return id;
624
+ },
625
+
626
+ insertBefore: <TNode extends AnyTreeNodePrimitive>(
627
+ siblingId: string,
628
+ nodeType: TNode,
629
+ data: InferTreeNodeDataState<TNode>
630
+ ): string => {
631
+ const state = getCurrentState();
632
+ const sibling = state.find(n => n.id === siblingId);
633
+ if (!sibling) {
634
+ throw new ValidationError(`Sibling node not found: ${siblingId}`);
635
+ }
636
+
637
+ const parentId = sibling.parentId;
638
+ const siblings = getOrderedChildren(state, parentId);
639
+ const siblingIndex = siblings.findIndex(n => n.id === siblingId);
640
+ const prevSibling = siblings[siblingIndex - 1];
641
+ const pos = generateTreePosBetween(prevSibling?.pos ?? null, sibling.pos);
642
+ const id = env.generateId();
643
+
644
+ // Validate child type is allowed
645
+ const parentType = getParentType(parentId);
646
+ this._validateChildType(parentType, nodeType.type);
647
+
648
+ env.addOperation(
649
+ Operation.fromDefinition(operationPath, this._opDefinitions.insert, {
650
+ id,
651
+ type: nodeType.type,
652
+ parentId,
653
+ pos,
654
+ data,
655
+ })
656
+ );
657
+
658
+ return id;
659
+ },
660
+
661
+ remove: (id: string) => {
662
+ env.addOperation(
663
+ Operation.fromDefinition(operationPath, this._opDefinitions.remove, { id })
664
+ );
665
+ },
666
+
667
+ move: (nodeId: string, newParentId: string | null, toIndex: number) => {
668
+ const state = getCurrentState();
669
+ const node = state.find(n => n.id === nodeId);
670
+ if (!node) {
671
+ throw new ValidationError(`Node not found: ${nodeId}`);
672
+ }
673
+
674
+ // Validate parent exists (if not moving to root)
675
+ if (newParentId !== null && !state.find(n => n.id === newParentId)) {
676
+ throw new ValidationError(`Parent node not found: ${newParentId}`);
677
+ }
678
+
679
+ // Validate no cycle
680
+ if (wouldCreateCycle(state, nodeId, newParentId)) {
681
+ throw new ValidationError("Move would create a cycle in the tree");
682
+ }
683
+
684
+ // Validate child type is allowed in new parent
685
+ const newParentType = newParentId === null ? null : state.find(n => n.id === newParentId)?.type ?? null;
686
+ this._validateChildType(newParentType, node.type);
687
+
688
+ // Validate not moving root to a parent
689
+ if (node.parentId === null && newParentId !== null) {
690
+ throw new ValidationError("Cannot move root node to have a parent");
691
+ }
692
+
693
+ // Calculate new position among new siblings (excluding self)
694
+ const siblings = getOrderedChildren(state, newParentId).filter(n => n.id !== nodeId);
695
+ const clampedIndex = Math.max(0, Math.min(toIndex, siblings.length));
696
+ const leftPos = clampedIndex > 0 && siblings[clampedIndex - 1] ? siblings[clampedIndex - 1]!.pos : null;
697
+ const rightPos = clampedIndex < siblings.length && siblings[clampedIndex] ? siblings[clampedIndex]!.pos : null;
698
+ const pos = generateTreePosBetween(leftPos, rightPos);
699
+
700
+ env.addOperation(
701
+ Operation.fromDefinition(operationPath, this._opDefinitions.move, {
702
+ id: nodeId,
703
+ parentId: newParentId,
704
+ pos,
705
+ })
706
+ );
707
+ },
708
+
709
+ moveAfter: (nodeId: string, siblingId: string) => {
710
+ const state = getCurrentState();
711
+ const node = state.find(n => n.id === nodeId);
712
+ const sibling = state.find(n => n.id === siblingId);
713
+
714
+ if (!node) {
715
+ throw new ValidationError(`Node not found: ${nodeId}`);
716
+ }
717
+ if (!sibling) {
718
+ throw new ValidationError(`Sibling node not found: ${siblingId}`);
719
+ }
720
+
721
+ const newParentId = sibling.parentId;
722
+
723
+ // Validate no cycle
724
+ if (wouldCreateCycle(state, nodeId, newParentId)) {
725
+ throw new ValidationError("Move would create a cycle in the tree");
726
+ }
727
+
728
+ // Validate child type is allowed in new parent
729
+ const newParentType = newParentId === null ? null : state.find(n => n.id === newParentId)?.type ?? null;
730
+ this._validateChildType(newParentType, node.type);
731
+
732
+ // Validate not moving root to a parent
733
+ if (node.parentId === null && newParentId !== null) {
734
+ throw new ValidationError("Cannot move root node to have a parent");
735
+ }
736
+
737
+ const siblings = getOrderedChildren(state, newParentId).filter(n => n.id !== nodeId);
738
+ const siblingIndex = siblings.findIndex(n => n.id === siblingId);
739
+ const nextSibling = siblings[siblingIndex + 1];
740
+ const pos = generateTreePosBetween(sibling.pos, nextSibling?.pos ?? null);
741
+
742
+ env.addOperation(
743
+ Operation.fromDefinition(operationPath, this._opDefinitions.move, {
744
+ id: nodeId,
745
+ parentId: newParentId,
746
+ pos,
747
+ })
748
+ );
749
+ },
750
+
751
+ moveBefore: (nodeId: string, siblingId: string) => {
752
+ const state = getCurrentState();
753
+ const node = state.find(n => n.id === nodeId);
754
+ const sibling = state.find(n => n.id === siblingId);
755
+
756
+ if (!node) {
757
+ throw new ValidationError(`Node not found: ${nodeId}`);
758
+ }
759
+ if (!sibling) {
760
+ throw new ValidationError(`Sibling node not found: ${siblingId}`);
761
+ }
762
+
763
+ const newParentId = sibling.parentId;
764
+
765
+ // Validate no cycle
766
+ if (wouldCreateCycle(state, nodeId, newParentId)) {
767
+ throw new ValidationError("Move would create a cycle in the tree");
768
+ }
769
+
770
+ // Validate child type is allowed in new parent
771
+ const newParentType = newParentId === null ? null : state.find(n => n.id === newParentId)?.type ?? null;
772
+ this._validateChildType(newParentType, node.type);
773
+
774
+ // Validate not moving root to a parent
775
+ if (node.parentId === null && newParentId !== null) {
776
+ throw new ValidationError("Cannot move root node to have a parent");
777
+ }
778
+
779
+ const siblings = getOrderedChildren(state, newParentId).filter(n => n.id !== nodeId);
780
+ const siblingIndex = siblings.findIndex(n => n.id === siblingId);
781
+ const prevSibling = siblings[siblingIndex - 1];
782
+ const pos = generateTreePosBetween(prevSibling?.pos ?? null, sibling.pos);
783
+
784
+ env.addOperation(
785
+ Operation.fromDefinition(operationPath, this._opDefinitions.move, {
786
+ id: nodeId,
787
+ parentId: newParentId,
788
+ pos,
789
+ })
790
+ );
791
+ },
792
+
793
+ moveToFirst: (nodeId: string, newParentId: string | null) => {
794
+ const state = getCurrentState();
795
+ const node = state.find(n => n.id === nodeId);
796
+
797
+ if (!node) {
798
+ throw new ValidationError(`Node not found: ${nodeId}`);
799
+ }
800
+
801
+ // Validate parent exists (if not moving to root)
802
+ if (newParentId !== null && !state.find(n => n.id === newParentId)) {
803
+ throw new ValidationError(`Parent node not found: ${newParentId}`);
804
+ }
805
+
806
+ // Validate no cycle
807
+ if (wouldCreateCycle(state, nodeId, newParentId)) {
808
+ throw new ValidationError("Move would create a cycle in the tree");
809
+ }
810
+
811
+ // Validate child type is allowed in new parent
812
+ const newParentType = newParentId === null ? null : state.find(n => n.id === newParentId)?.type ?? null;
813
+ this._validateChildType(newParentType, node.type);
814
+
815
+ // Validate not moving root to a parent
816
+ if (node.parentId === null && newParentId !== null) {
817
+ throw new ValidationError("Cannot move root node to have a parent");
818
+ }
819
+
820
+ const siblings = getOrderedChildren(state, newParentId).filter(n => n.id !== nodeId);
821
+ const firstPos = siblings.length > 0 ? siblings[0]!.pos : null;
822
+ const pos = generateTreePosBetween(null, firstPos);
823
+
824
+ env.addOperation(
825
+ Operation.fromDefinition(operationPath, this._opDefinitions.move, {
826
+ id: nodeId,
827
+ parentId: newParentId,
828
+ pos,
829
+ })
830
+ );
831
+ },
832
+
833
+ moveToLast: (nodeId: string, newParentId: string | null) => {
834
+ const state = getCurrentState();
835
+ const node = state.find(n => n.id === nodeId);
836
+
837
+ if (!node) {
838
+ throw new ValidationError(`Node not found: ${nodeId}`);
839
+ }
840
+
841
+ // Validate parent exists (if not moving to root)
842
+ if (newParentId !== null && !state.find(n => n.id === newParentId)) {
843
+ throw new ValidationError(`Parent node not found: ${newParentId}`);
844
+ }
845
+
846
+ // Validate no cycle
847
+ if (wouldCreateCycle(state, nodeId, newParentId)) {
848
+ throw new ValidationError("Move would create a cycle in the tree");
849
+ }
850
+
851
+ // Validate child type is allowed in new parent
852
+ const newParentType = newParentId === null ? null : state.find(n => n.id === newParentId)?.type ?? null;
853
+ this._validateChildType(newParentType, node.type);
854
+
855
+ // Validate not moving root to a parent
856
+ if (node.parentId === null && newParentId !== null) {
857
+ throw new ValidationError("Cannot move root node to have a parent");
858
+ }
859
+
860
+ const siblings = getOrderedChildren(state, newParentId).filter(n => n.id !== nodeId);
861
+ const lastPos = siblings.length > 0 ? siblings[siblings.length - 1]!.pos : null;
862
+ const pos = generateTreePosBetween(lastPos, null);
863
+
864
+ env.addOperation(
865
+ Operation.fromDefinition(operationPath, this._opDefinitions.move, {
866
+ id: nodeId,
867
+ parentId: newParentId,
868
+ pos,
869
+ })
870
+ );
871
+ },
872
+
873
+ at: <TNode extends AnyTreeNodePrimitive>(
874
+ id: string,
875
+ nodeType: TNode
876
+ ): InferProxy<TNode["data"]> => {
877
+ // Get the node to verify its type
878
+ const state = getCurrentState();
879
+ const node = state.find(n => n.id === id);
880
+ if (!node) {
881
+ throw new ValidationError(`Node not found: ${id}`);
882
+ }
883
+ if (node.type !== nodeType.type) {
884
+ throw new ValidationError(
885
+ `Node is of type "${node.type}", not "${nodeType.type}"`
886
+ );
887
+ }
888
+
889
+ const nodePath = operationPath.append(id);
890
+ return nodeType.data._internal.createProxy(env, nodePath) as InferProxy<TNode["data"]>;
891
+ },
892
+
893
+ toSnapshot: (): TreeNodeSnapshot<TRoot> | undefined => {
894
+ const state = getCurrentState();
895
+ const rootNode = state.find(n => n.parentId === null);
896
+ if (!rootNode) return undefined;
897
+ return buildSnapshot(rootNode.id, state);
898
+ },
899
+ };
900
+ },
901
+
902
+ applyOperation: (
903
+ state: TreeState<TRoot> | undefined,
904
+ operation: Operation.Operation<any, any, any>
905
+ ): TreeState<TRoot> => {
906
+ const path = operation.path;
907
+ const tokens = path.toTokens().filter((t: string) => t !== "");
908
+ const currentState = state ?? [];
909
+
910
+ let newState: TreeState<TRoot>;
911
+
912
+ // If path is empty, this is a tree-level operation
913
+ if (tokens.length === 0) {
914
+ switch (operation.kind) {
915
+ case "tree.set": {
916
+ const payload = operation.payload;
917
+ if (!globalThis.Array.isArray(payload)) {
918
+ throw new ValidationError(`TreePrimitive.set requires an array payload`);
919
+ }
920
+ newState = payload as TreeState<TRoot>;
921
+ break;
922
+ }
923
+ case "tree.insert": {
924
+ const { id, type, parentId, pos, data } = operation.payload as {
925
+ id: string;
926
+ type: string;
927
+ parentId: string | null;
928
+ pos: string;
929
+ data: unknown;
930
+ };
931
+ newState = [...currentState, { id, type, parentId, pos, data }] as TreeState<TRoot>;
932
+ break;
933
+ }
934
+ case "tree.remove": {
935
+ const { id } = operation.payload as { id: string };
936
+ // Get all descendants to remove
937
+ const descendantIds = getDescendantIds(currentState, id);
938
+ const idsToRemove = new Set([id, ...descendantIds]);
939
+ newState = currentState.filter(node => !idsToRemove.has(node.id));
940
+ break;
941
+ }
942
+ case "tree.move": {
943
+ const { id, parentId, pos } = operation.payload as {
944
+ id: string;
945
+ parentId: string | null;
946
+ pos: string;
947
+ };
948
+ newState = currentState.map(node =>
949
+ node.id === id ? { ...node, parentId, pos } : node
950
+ ) as TreeState<TRoot>;
951
+ break;
952
+ }
953
+ default:
954
+ throw new ValidationError(`TreePrimitive cannot apply operation of kind: ${operation.kind}`);
955
+ }
956
+ } else {
957
+ // Otherwise, delegate to the node's data primitive
958
+ const nodeId = tokens[0]!;
959
+ const nodeIndex = currentState.findIndex(node => node.id === nodeId);
960
+
961
+ if (nodeIndex === -1) {
962
+ throw new ValidationError(`Tree node not found with ID: ${nodeId}`);
963
+ }
964
+
965
+ const node = currentState[nodeIndex]!;
966
+ const nodeTypePrimitive = this._getNodeTypePrimitive(node.type);
967
+ const remainingPath = path.shift();
968
+ const nodeOperation = {
969
+ ...operation,
970
+ path: remainingPath,
971
+ };
972
+
973
+ const newData = nodeTypePrimitive.data._internal.applyOperation(
974
+ node.data as InferStructState<any> | undefined,
975
+ nodeOperation
976
+ );
977
+
978
+ const mutableState = [...currentState];
979
+ mutableState[nodeIndex] = { ...node, data: newData };
980
+ newState = mutableState as TreeState<TRoot>;
981
+ }
982
+
983
+ // Run validators on the new state
984
+ runValidators(newState, this._schema.validators);
985
+
986
+ return newState;
987
+ },
988
+
989
+ getInitialState: (): TreeState<TRoot> | undefined => {
990
+ if (this._schema.defaultValue !== undefined) {
991
+ return this._schema.defaultValue;
992
+ }
993
+
994
+ // Automatically create a root node with default data
995
+ const rootNodeType = this._schema.root;
996
+ const rootData = rootNodeType.data._internal.getInitialState() ?? {};
997
+ const rootId = crypto.randomUUID();
998
+ const rootPos = generateTreePosBetween(null, null);
999
+
1000
+ return [{
1001
+ id: rootId,
1002
+ type: rootNodeType.type,
1003
+ parentId: null,
1004
+ pos: rootPos,
1005
+ data: rootData,
1006
+ }] as TreeState<TRoot>;
1007
+ },
1008
+
1009
+ transformOperation: (
1010
+ clientOp: Operation.Operation<any, any, any>,
1011
+ serverOp: Operation.Operation<any, any, any>
1012
+ ): Transform.TransformResult => {
1013
+ const clientPath = clientOp.path;
1014
+ const serverPath = serverOp.path;
1015
+
1016
+ // If paths don't overlap at all, no transformation needed
1017
+ if (!OperationPath.pathsOverlap(clientPath, serverPath)) {
1018
+ return { type: "transformed", operation: clientOp };
1019
+ }
1020
+
1021
+ // Handle tree.remove from server - check if client is operating on removed node or descendants
1022
+ if (serverOp.kind === "tree.remove") {
1023
+ const removedId = (serverOp.payload as { id: string }).id;
1024
+ const clientTokens = clientPath.toTokens().filter((t: string) => t !== "");
1025
+ const serverTokens = serverPath.toTokens().filter((t: string) => t !== "");
1026
+
1027
+ // Check if client operation targets the removed node or uses it
1028
+ if (clientOp.kind === "tree.move") {
1029
+ const movePayload = clientOp.payload as { id: string; parentId: string | null };
1030
+ // If moving the removed node or moving to a removed parent
1031
+ if (movePayload.id === removedId || movePayload.parentId === removedId) {
1032
+ return { type: "noop" };
1033
+ }
1034
+ }
1035
+
1036
+ if (clientOp.kind === "tree.insert") {
1037
+ const insertPayload = clientOp.payload as { parentId: string | null };
1038
+ // If inserting into a removed parent
1039
+ if (insertPayload.parentId === removedId) {
1040
+ return { type: "noop" };
1041
+ }
1042
+ }
1043
+
1044
+ // Check if client is operating on a node that was removed
1045
+ if (clientTokens.length > serverTokens.length) {
1046
+ const nodeId = clientTokens[serverTokens.length];
1047
+ if (nodeId === removedId) {
1048
+ return { type: "noop" };
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ // Both inserting - no conflict (fractional indexing handles order)
1054
+ if (serverOp.kind === "tree.insert" && clientOp.kind === "tree.insert") {
1055
+ return { type: "transformed", operation: clientOp };
1056
+ }
1057
+
1058
+ // Both moving same node - client wins
1059
+ if (serverOp.kind === "tree.move" && clientOp.kind === "tree.move") {
1060
+ const serverMoveId = (serverOp.payload as { id: string }).id;
1061
+ const clientMoveId = (clientOp.payload as { id: string }).id;
1062
+
1063
+ if (serverMoveId === clientMoveId) {
1064
+ return { type: "transformed", operation: clientOp };
1065
+ }
1066
+ // Different nodes - no conflict
1067
+ return { type: "transformed", operation: clientOp };
1068
+ }
1069
+
1070
+ // For same exact path: client wins (last-write-wins)
1071
+ if (OperationPath.pathsEqual(clientPath, serverPath)) {
1072
+ return { type: "transformed", operation: clientOp };
1073
+ }
1074
+
1075
+ // If server set entire tree and client is operating on a node
1076
+ if (serverOp.kind === "tree.set" && OperationPath.isPrefix(serverPath, clientPath)) {
1077
+ return { type: "transformed", operation: clientOp };
1078
+ }
1079
+
1080
+ // Delegate to node data primitive for nested operations
1081
+ const clientTokens = clientPath.toTokens().filter((t: string) => t !== "");
1082
+ const serverTokens = serverPath.toTokens().filter((t: string) => t !== "");
1083
+
1084
+ // Both operations target children of this tree
1085
+ if (clientTokens.length > 0 && serverTokens.length > 0) {
1086
+ const clientNodeId = clientTokens[0];
1087
+ const serverNodeId = serverTokens[0];
1088
+
1089
+ // If operating on different nodes, no conflict
1090
+ if (clientNodeId !== serverNodeId) {
1091
+ return { type: "transformed", operation: clientOp };
1092
+ }
1093
+
1094
+ // Same node - would need to delegate to node's data primitive
1095
+ // For simplicity, let client win
1096
+ return { type: "transformed", operation: clientOp };
1097
+ }
1098
+
1099
+ // Default: no transformation needed
1100
+ return { type: "transformed", operation: clientOp };
1101
+ },
1102
+ };
1103
+ }
1104
+
1105
+ /** Options for creating a Tree primitive */
1106
+ export interface TreeOptions<TRoot extends AnyTreeNodePrimitive> {
1107
+ /** The root node type */
1108
+ readonly root: TRoot;
1109
+ }
1110
+
1111
+ /** Creates a new TreePrimitive with the given root node type */
1112
+ export const Tree = <TRoot extends AnyTreeNodePrimitive>(
1113
+ options: TreeOptions<TRoot>
1114
+ ): TreePrimitive<TRoot> =>
1115
+ new TreePrimitive({
1116
+ required: false,
1117
+ defaultValue: undefined,
1118
+ root: options.root,
1119
+ validators: [],
1120
+ });