@loro-extended/change 2.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 +116 -1
- package/dist/index.d.ts +89 -14
- package/dist/index.js +480 -156
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/overlay.ts +62 -3
- package/src/shape.ts +69 -8
- package/src/typed-doc.ts +1 -0
- package/src/typed-refs/base.ts +7 -18
- package/src/typed-refs/counter.ts +0 -2
- package/src/typed-refs/doc.ts +3 -21
- package/src/typed-refs/list-base.ts +28 -29
- package/src/typed-refs/list-value-updates.test.ts +213 -0
- package/src/typed-refs/movable-list.ts +0 -2
- package/src/typed-refs/record-value-updates.test.ts +214 -0
- package/src/typed-refs/record.ts +48 -51
- package/src/typed-refs/struct-value-updates.test.ts +200 -0
- package/src/typed-refs/struct.ts +39 -44
- package/src/typed-refs/text.ts +0 -6
- package/src/typed-refs/tree-node-value-updates.test.ts +234 -0
- package/src/typed-refs/tree-node.ts +236 -0
- package/src/typed-refs/tree.test.ts +384 -0
- package/src/typed-refs/tree.ts +252 -24
- package/src/typed-refs/utils.ts +30 -7
- package/src/types.ts +36 -1
- package/src/utils/type-guards.ts +1 -0
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|