@loro-extended/change 3.0.0 → 4.0.0

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.
package/README.md CHANGED
@@ -570,7 +570,7 @@ const schema = Shape.doc({
570
570
  - `Shape.movableList(itemSchema)` - Collaborative reorderable lists
571
571
  - `Shape.struct(shape)` - Collaborative structs with fixed keys (uses LoroMap internally)
572
572
  - `Shape.record(valueSchema)` - Collaborative key-value maps with dynamic string keys
573
- - `Shape.tree(shape)` - Collaborative hierarchical tree structures (Note: incomplete implementation)
573
+ - `Shape.tree(dataShape)` - Collaborative hierarchical tree structures with typed node metadata
574
574
  - `Shape.any()` - Escape hatch for untyped containers (see [Untyped Integration](#untyped-integration-with-external-libraries))
575
575
 
576
576
  #### Value Types
@@ -788,6 +788,121 @@ draft.metadata.values();
788
788
  const value = draft.metadata.get("key");
789
789
  ```
790
790
 
791
+ ### Tree Operations
792
+
793
+ Trees are hierarchical structures where each node has typed metadata. Perfect for state machines, file systems, org charts, and nested data.
794
+
795
+ ```typescript
796
+ // Define node data shape
797
+ const StateNodeDataShape = Shape.struct({
798
+ name: Shape.text(),
799
+ facts: Shape.record(Shape.plain.any()),
800
+ rules: Shape.list(
801
+ Shape.plain.struct({
802
+ name: Shape.plain.string(),
803
+ rego: Shape.plain.string(),
804
+ description: Shape.plain.string().nullable(),
805
+ })
806
+ ),
807
+ });
808
+
809
+ const schema = Shape.doc({
810
+ states: Shape.tree(StateNodeDataShape),
811
+ });
812
+
813
+ const doc = createTypedDoc(schema);
814
+
815
+ change(doc, (draft) => {
816
+ // Create root nodes
817
+ const idle = draft.states.createNode();
818
+ idle.data.name.insert(0, "idle");
819
+
820
+ const running = draft.states.createNode();
821
+ running.data.name.insert(0, "running");
822
+
823
+ // Create child nodes
824
+ const processing = idle.createNode();
825
+ processing.data.name.insert(0, "processing");
826
+
827
+ // Access typed node data
828
+ processing.data.rules.push({
829
+ name: "validate",
830
+ rego: "package validate",
831
+ description: null,
832
+ });
833
+
834
+ // Navigate the tree
835
+ const parent = processing.parent(); // Returns idle node
836
+ const children = idle.children(); // Returns [processing]
837
+
838
+ // Move nodes between parents
839
+ processing.move(running); // Move to different parent
840
+ processing.move(); // Move to root (no parent)
841
+
842
+ // Query the tree
843
+ const roots = draft.states.roots(); // All root nodes
844
+ const allNodes = draft.states.nodes(); // All nodes (flat)
845
+ const node = draft.states.getNodeByID(idle.id); // Find by ID
846
+ const exists = draft.states.has(idle.id); // Check existence
847
+
848
+ // Delete nodes (and all descendants)
849
+ draft.states.delete(running);
850
+
851
+ // Enable fractional indexing for ordering
852
+ draft.states.enableFractionalIndex(8);
853
+ const index = idle.index(); // Position among siblings
854
+ const fractionalIndex = idle.fractionalIndex(); // Fractional index string
855
+ });
856
+
857
+ // Serialize to JSON (nested structure)
858
+ const json = doc.toJSON();
859
+ // {
860
+ // states: [{
861
+ // id: "0@123",
862
+ // parent: null,
863
+ // index: 0,
864
+ // fractionalIndex: "80",
865
+ // data: { name: "idle", facts: {}, rules: [] },
866
+ // children: [...]
867
+ // }]
868
+ // }
869
+
870
+ // Get flat array representation
871
+ change(doc, (draft) => {
872
+ const flatArray = draft.states.toArray();
873
+ // [{ id, parent, index, fractionalIndex, data }, ...]
874
+ });
875
+ ```
876
+
877
+ **Tree Node Properties:**
878
+
879
+ - `node.id` - Unique TreeID for the node
880
+ - `node.data` - Typed StructRef for node metadata (access like `node.data.name`)
881
+ - `node.parent()` - Get parent node (or undefined for roots)
882
+ - `node.children()` - Get child nodes in order
883
+ - `node.index()` - Position among siblings
884
+ - `node.fractionalIndex()` - Fractional index string for ordering
885
+ - `node.isDeleted()` - Check if node has been deleted
886
+
887
+ **Tree Node Methods:**
888
+
889
+ - `node.createNode(initialData?, index?)` - Create child node
890
+ - `node.move(newParent?, index?)` - Move to new parent (undefined = root)
891
+ - `node.moveAfter(sibling)` - Move after sibling
892
+ - `node.moveBefore(sibling)` - Move before sibling
893
+
894
+ **TreeRef Methods:**
895
+
896
+ - `tree.createNode(initialData?)` - Create root node
897
+ - `tree.roots()` - Get all root nodes
898
+ - `tree.nodes()` - Get all nodes (flat)
899
+ - `tree.getNodeByID(id)` - Find node by TreeID
900
+ - `tree.has(id)` - Check if node exists
901
+ - `tree.delete(target)` - Delete node and descendants
902
+ - `tree.enableFractionalIndex(jitter?)` - Enable ordering
903
+ - `tree.toJSON()` - Nested JSON structure
904
+ - `tree.toArray()` - Flat array representation
905
+
791
906
  ### JSON Serialization and Snapshots
792
907
 
793
908
  You can easily get a plain JavaScript object snapshot of any part of the document using `JSON.stringify()` or `.toJSON()`. This works for the entire document, nested containers, and even during loading states (placeholders).
package/dist/index.d.ts CHANGED
@@ -1,7 +1,24 @@
1
- import { LoroDoc, LoroCounter, LoroList, LoroMovableList, Container, LoroMap, Value, LoroText, LoroTree } from 'loro-crdt';
1
+ import { LoroDoc, LoroCounter, LoroList, LoroMovableList, Container, LoroMap, Value, LoroText, TreeID, LoroTree } from 'loro-crdt';
2
2
 
3
+ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
4
+ /**
5
+ * Expands a type to a reasonable depth for IDE display.
6
+ * Stops at depth 4 to avoid infinite recursion with self-referential types
7
+ * like tree nodes that have `children: TreeNodeJSON[]`.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Without expansion, hover shows: TreeNodeJSON<StructContainerShape<...>>
12
+ * // With expansion, hover shows the actual structure:
13
+ * // { id: string; parent: string | null; data: { name: string; ... }; children: ... }
14
+ * ```
15
+ */
16
+ type ExpandDeep<T, Depth extends number = 4> = Depth extends 0 ? T : T extends (...args: any[]) => any ? T : T extends object ? T extends infer O ? {
17
+ [K in keyof O]: ExpandDeep<O[K], Prev[Depth]>;
18
+ } : never : T;
3
19
  /**
4
20
  * Infers the plain (JSON-serializable) type from any Shape.
21
+ * The result is fully expanded for better IDE hover display.
5
22
  *
6
23
  * This is the recommended way to extract types from shapes.
7
24
  * Works with DocShape, ContainerShape, and ValueShape.
@@ -29,7 +46,13 @@ import { LoroDoc, LoroCounter, LoroList, LoroMovableList, Container, LoroMap, Va
29
46
  * // Result: { name: string; cursor: { x: number; y: number } }
30
47
  * ```
31
48
  */
32
- type Infer<T> = T extends Shape<infer P, any, any> ? P : never;
49
+ type Infer<T> = T extends Shape<infer P, any, any> ? ExpandDeep<P> : never;
50
+ /**
51
+ * Infers the plain (JSON-serializable) type from any Shape without expansion.
52
+ * Use this if you prefer to see type alias names (like TreeNodeJSON) in hover displays,
53
+ * or if you need slightly faster type checking on very large schemas.
54
+ */
55
+ type InferRaw<T> = T extends Shape<infer P, any, any> ? P : never;
33
56
  /**
34
57
  * Infers the mutable type from any Shape.
35
58
  * This is the type used within change() callbacks for mutation.
@@ -52,8 +75,8 @@ type TypedRefParams<Shape extends DocShape | ContainerShape> = {
52
75
  shape: Shape;
53
76
  placeholder?: Infer<Shape>;
54
77
  getContainer: () => ShapeToContainer<Shape>;
55
- readonly?: boolean;
56
78
  autoCommit?: boolean;
79
+ batchedMutation?: boolean;
57
80
  getDoc?: () => LoroDoc;
58
81
  };
59
82
  declare abstract class TypedRef<Shape extends DocShape | ContainerShape> {
@@ -68,20 +91,14 @@ declare abstract class TypedRef<Shape extends DocShape | ContainerShape> {
68
91
  abstract toJSON(): Infer<Shape>;
69
92
  protected get shape(): Shape;
70
93
  protected get placeholder(): Infer<Shape> | undefined;
71
- protected get readonly(): boolean;
72
94
  protected get autoCommit(): boolean;
95
+ protected get batchedMutation(): boolean;
73
96
  protected get doc(): LoroDoc | undefined;
74
97
  /**
75
98
  * Commits changes if autoCommit is enabled.
76
99
  * Call this after any mutation operation.
77
100
  */
78
101
  protected commitIfAuto(): void;
79
- /**
80
- * Throws an error if this ref is in readonly mode.
81
- * Call this at the start of any mutating method.
82
- * @deprecated Mutations are always allowed now; this will be removed.
83
- */
84
- protected assertMutable(): void;
85
102
  protected get container(): ShapeToContainer<Shape>;
86
103
  }
87
104
 
@@ -257,9 +274,45 @@ interface TextContainerShape extends Shape<string, TextRef, string> {
257
274
  interface CounterContainerShape extends Shape<number, CounterRef, number> {
258
275
  readonly _type: "counter";
259
276
  }
260
- interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any, never[]> {
277
+ /**
278
+ * JSON representation of a tree node with typed data.
279
+ * Used for serialization (toJSON) of tree structures.
280
+ */
281
+ type TreeNodeJSON<DataShape extends StructContainerShape> = {
282
+ id: TreeID;
283
+ parent: TreeID | null;
284
+ index: number;
285
+ fractionalIndex: string;
286
+ data: DataShape["_plain"];
287
+ children: TreeNodeJSON<DataShape>[];
288
+ };
289
+ /**
290
+ * Container shape for tree (forest) structures.
291
+ * Each node in the tree has typed metadata stored in a LoroMap.
292
+ *
293
+ * Note: The Mutable type (second generic parameter) is `any` here to avoid
294
+ * circular dependency with TreeRef. The actual type is resolved at runtime
295
+ * and through the InferMutableType helper.
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const StateNodeDataShape = Shape.struct({
300
+ * name: Shape.text(),
301
+ * facts: Shape.record(Shape.plain.any()),
302
+ * })
303
+ *
304
+ * const Schema = Shape.doc({
305
+ * states: Shape.tree(StateNodeDataShape),
306
+ * })
307
+ * ```
308
+ */
309
+ interface TreeContainerShape<DataShape extends StructContainerShape = StructContainerShape> extends Shape<TreeNodeJSON<DataShape>[], any, never[]> {
261
310
  readonly _type: "tree";
262
- readonly shape: NestedShape;
311
+ /**
312
+ * The shape of each node's data (metadata).
313
+ * This is a StructContainerShape that defines the typed properties on node.data.
314
+ */
315
+ readonly shape: DataShape;
263
316
  }
264
317
  interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListRef<NestedShape>, never[]> {
265
318
  readonly _type: "list";
@@ -460,7 +513,29 @@ declare const Shape: {
460
513
  record: <T extends ContainerOrValueShape>(shape: T) => RecordContainerShape<T>;
461
514
  movableList: <T extends ContainerOrValueShape>(shape: T) => MovableListContainerShape<T>;
462
515
  text: () => WithPlaceholder<TextContainerShape>;
463
- tree: <T extends MapContainerShape | StructContainerShape>(shape: T) => TreeContainerShape;
516
+ /**
517
+ * Creates a tree container shape for hierarchical data structures.
518
+ * Each node in the tree has typed metadata defined by the data shape.
519
+ *
520
+ * @example
521
+ * ```typescript
522
+ * const StateNodeDataShape = Shape.struct({
523
+ * name: Shape.text(),
524
+ * facts: Shape.record(Shape.plain.any()),
525
+ * })
526
+ *
527
+ * const Schema = Shape.doc({
528
+ * states: Shape.tree(StateNodeDataShape),
529
+ * })
530
+ *
531
+ * doc.$.change(draft => {
532
+ * const root = draft.states.createNode({ name: "idle", facts: {} })
533
+ * const child = root.createNode({ name: "running", facts: {} })
534
+ * child.data.name = "active"
535
+ * })
536
+ * ```
537
+ */
538
+ tree: <T extends StructContainerShape>(shape: T) => TreeContainerShape<T>;
464
539
  plain: {
465
540
  string: <T extends string = string>(...options: T[]) => WithPlaceholder<StringValueShape<T>> & WithNullable<StringValueShape<T>>;
466
541
  number: () => WithPlaceholder<NumberValueShape> & WithNullable<NumberValueShape>;
@@ -921,4 +996,4 @@ declare function createPlaceholderProxy<T extends object>(target: T): T;
921
996
  */
922
997
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
923
998
 
924
- export { type AnyContainerShape, type AnyValueShape, type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Infer, type InferMutableType, type InferPlaceholderType, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type Mutable, type ObjectValueShape, type PathBuilder, type PathNode, type PathSegment, type PathSelector, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StructContainerShape, type StructValueShape, type TextContainerShape, type TreeContainerShape, type TypedDoc, type UnionValueShape, type ValueShape, type WithNullable, type WithPlaceholder, change, compileToJsonPath, createPathBuilder, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, evaluatePath, evaluatePathOnValue, getLoroDoc, hasWildcard, mergeValue, overlayPlaceholder, validatePlaceholder };
999
+ export { type AnyContainerShape, type AnyValueShape, type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Infer, type InferMutableType, type InferPlaceholderType, type InferRaw, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type Mutable, type ObjectValueShape, type PathBuilder, type PathNode, type PathSegment, type PathSelector, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StructContainerShape, type StructValueShape, type TextContainerShape, type TreeContainerShape, type TypedDoc, type UnionValueShape, type ValueShape, type WithNullable, type WithPlaceholder, change, compileToJsonPath, createPathBuilder, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, evaluatePath, evaluatePathOnValue, getLoroDoc, hasWildcard, mergeValue, overlayPlaceholder, validatePlaceholder };