@loro-extended/change 5.0.0 → 5.2.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
@@ -216,6 +216,100 @@ console.log(doc.toJSON()); // Updated document state
216
216
  | Atomic undo/redo | Batched: `change(doc, d => { ... })` |
217
217
  | Performance-critical bulk updates | Batched: `change(doc, d => { ... })` |
218
218
  | Simple reads + writes | Direct: `doc.users.set(...)` |
219
+ | Encapsulated ref operations | Ref-level: `change(ref, d => {...})` |
220
+
221
+ ### Ref-Level `change()` for Encapsulation
222
+
223
+ The `change()` function also works on individual refs (ListRef, TextRef, TreeRef, etc.), enabling better encapsulation when you want to pass refs around without exposing the entire document:
224
+
225
+ ```typescript
226
+ import { change } from "@loro-extended/change";
227
+
228
+ // Library code - expose only the ref, not the doc
229
+ class StateMachine {
230
+ private doc: TypedDoc<...>;
231
+
232
+ get states(): TreeRef<StateNodeShape> {
233
+ return this.doc.states;
234
+ }
235
+ }
236
+
237
+ // User code - works with just the ref
238
+ function addStates(states: TreeRef<StateNodeShape>) {
239
+ change(states, draft => {
240
+ const idle = draft.createNode();
241
+ idle.data.name.insert(0, "idle");
242
+
243
+ const running = draft.createNode();
244
+ running.data.name.insert(0, "running");
245
+ });
246
+ }
247
+
248
+ // Usage
249
+ const machine = new StateMachine();
250
+ addStates(machine.states); // No access to the underlying doc needed!
251
+ ```
252
+
253
+ This pattern is useful for:
254
+ - **Library APIs**: Expose typed refs without leaking document structure
255
+ - **Component isolation**: Pass refs to components that only need partial access
256
+ - **Testing**: Mock or stub individual refs without full document setup
257
+
258
+ All ref types support `change()`:
259
+
260
+ ```typescript
261
+ // ListRef
262
+ change(doc.items, draft => {
263
+ draft.push("item1");
264
+ draft.push("item2");
265
+ });
266
+
267
+ // TextRef
268
+ change(doc.title, draft => {
269
+ draft.insert(0, "Hello ");
270
+ draft.insert(6, "World");
271
+ });
272
+
273
+ // CounterRef
274
+ change(doc.count, draft => {
275
+ draft.increment(5);
276
+ draft.decrement(2);
277
+ });
278
+
279
+ // StructRef
280
+ change(doc.profile, draft => {
281
+ draft.bio.insert(0, "Hello");
282
+ draft.age.increment(1);
283
+ });
284
+
285
+ // RecordRef
286
+ change(doc.users, draft => {
287
+ draft.set("alice", { name: "Alice" });
288
+ draft.set("bob", { name: "Bob" });
289
+ });
290
+
291
+ // TreeRef
292
+ change(doc.tree, draft => {
293
+ const node = draft.createNode();
294
+ node.data.name.insert(0, "root");
295
+ });
296
+ ```
297
+
298
+ Nested `change()` calls are safe - Loro's commit is idempotent:
299
+
300
+ ```typescript
301
+ change(doc.items, outer => {
302
+ outer.push("from outer");
303
+
304
+ // Nested change on a different ref - works correctly
305
+ change(doc.count, inner => {
306
+ inner.increment(10);
307
+ });
308
+
309
+ outer.push("still in outer");
310
+ });
311
+ // All mutations are committed
312
+ ```
219
313
 
220
314
  ## Advanced Usage
221
315
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { LoroDoc, Container, LoroTreeNode, TreeID, Subscription, LoroText, LoroList, LoroMovableList, LoroMap, LoroCounter, LoroTree, Value } from 'loro-crdt';
1
+ import { PeerID, LoroDoc, Container, LoroTreeNode, TreeID, Subscription, LoroText, LoroList, LoroMovableList, LoroMap, LoroTree, LoroCounter, Value } from 'loro-crdt';
2
2
 
3
3
  type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
4
4
  /**
@@ -135,6 +135,14 @@ type JsonPatch = JsonPatchOperation[];
135
135
  * loro(doc).subscribe(callback);
136
136
  * ```
137
137
  */
138
+ /**
139
+ * Frontiers represent a specific version in the document's history.
140
+ * Each frontier is an operation ID consisting of a peer ID and counter.
141
+ */
142
+ type Frontiers = {
143
+ peer: PeerID;
144
+ counter: number;
145
+ }[];
138
146
  type TypedDoc<Shape extends DocShape> = Mutable<Shape> & {
139
147
  /**
140
148
  * The primary method of mutating typed documents.
@@ -168,6 +176,32 @@ type TypedDoc<Shape extends DocShape> = Mutable<Shape> & {
168
176
  * ```
169
177
  */
170
178
  toJSON(): Infer<Shape>;
179
+ /**
180
+ * Creates a new TypedDoc at a specified version (frontiers).
181
+ * The forked doc will only contain history before the specified frontiers.
182
+ * The forked doc has a different PeerID from the original.
183
+ *
184
+ * For raw LoroDoc access, use: `loro(doc).doc.forkAt(frontiers)`
185
+ *
186
+ * @param frontiers - The version to fork at (obtained from `loro(doc).doc.frontiers()`)
187
+ * @returns A new TypedDoc with the same schema at the specified version
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * import { loro } from "@loro-extended/change";
192
+ *
193
+ * const doc = createTypedDoc(schema);
194
+ * doc.title.update("Hello");
195
+ * const frontiers = loro(doc).doc.frontiers();
196
+ * doc.title.update("World");
197
+ *
198
+ * // Fork at the earlier version
199
+ * const forkedDoc = doc.forkAt(frontiers);
200
+ * console.log(forkedDoc.title.toString()); // "Hello"
201
+ * console.log(doc.title.toString()); // "World"
202
+ * ```
203
+ */
204
+ forkAt(frontiers: Frontiers): TypedDoc<Shape>;
171
205
  };
172
206
  /**
173
207
  * Creates a new TypedDoc with the given schema.
@@ -207,6 +241,133 @@ type TypedDoc<Shape extends DocShape> = Mutable<Shape> & {
207
241
  */
208
242
  declare function createTypedDoc<Shape extends DocShape>(shape: Shape, existingDoc?: LoroDoc): TypedDoc<Shape>;
209
243
 
244
+ /**
245
+ * Symbol for internal methods that should not be enumerable or accessible to users.
246
+ * Used to hide implementation details like absorbPlainValues(), getTypedRefParams(), etc.
247
+ *
248
+ * This achieves Success Criteria #7 from loro-api-refactor.md:
249
+ * "Internal methods hidden - Via Symbol, not enumerable"
250
+ */
251
+ declare const INTERNAL_SYMBOL: unique symbol;
252
+ /**
253
+ * Minimal interface for refs that only need absorbPlainValues.
254
+ * Used by TreeNodeRef which doesn't extend TypedRef.
255
+ */
256
+ interface RefInternalsBase {
257
+ /** Absorb mutated plain values back into Loro containers */
258
+ absorbPlainValues(): void;
259
+ }
260
+ type TypedRefParams<Shape extends DocShape | ContainerShape> = {
261
+ shape: Shape;
262
+ placeholder?: Infer<Shape>;
263
+ getContainer: () => ShapeToContainer<Shape>;
264
+ autoCommit?: boolean;
265
+ batchedMutation?: boolean;
266
+ getDoc: () => LoroDoc;
267
+ };
268
+ /**
269
+ * Abstract base class for all ref internal implementations.
270
+ * Contains shared logic that was previously in TypedRef.createBaseInternals().
271
+ *
272
+ * Subclasses implement specific behavior for each ref type.
273
+ */
274
+ declare abstract class BaseRefInternals<Shape extends DocShape | ContainerShape> implements RefInternalsBase {
275
+ protected readonly params: TypedRefParams<Shape>;
276
+ protected cachedContainer: ShapeToContainer<Shape> | undefined;
277
+ protected loroNamespace: LoroRefBase | undefined;
278
+ constructor(params: TypedRefParams<Shape>);
279
+ /** Get the underlying Loro container (cached) */
280
+ getContainer(): ShapeToContainer<Shape>;
281
+ /** Commit changes if autoCommit is enabled */
282
+ commitIfAuto(): void;
283
+ /** Get the shape for this ref */
284
+ getShape(): Shape;
285
+ /** Get the placeholder value */
286
+ getPlaceholder(): Infer<Shape> | undefined;
287
+ /** Check if autoCommit is enabled */
288
+ getAutoCommit(): boolean;
289
+ /** Check if in batched mutation mode */
290
+ getBatchedMutation(): boolean;
291
+ /** Get the LoroDoc */
292
+ getDoc(): LoroDoc;
293
+ /**
294
+ * Get the TypedRefParams needed to recreate this ref.
295
+ * Used by change() to create draft refs with modified params.
296
+ *
297
+ * Returns a new params object with the same shape, placeholder, getContainer, and getDoc,
298
+ * but allows overriding autoCommit and batchedMutation for draft creation.
299
+ */
300
+ getTypedRefParams(): TypedRefParams<Shape>;
301
+ /** Get the loro namespace (cached) */
302
+ getLoroNamespace(): LoroRefBase;
303
+ /** Absorb mutated plain values back into Loro containers - subclasses override */
304
+ abstract absorbPlainValues(): void;
305
+ /** Create the loro() namespace object - subclasses override for specific types */
306
+ protected createLoroNamespace(): LoroRefBase;
307
+ }
308
+ /**
309
+ * Base class for all typed refs.
310
+ *
311
+ * All internal methods are accessed via [INTERNAL_SYMBOL] to prevent
312
+ * namespace collisions with user data properties.
313
+ *
314
+ * Uses the Facade + Implementation pattern:
315
+ * - TypedRef is the thin public facade
316
+ * - BaseRefInternals subclasses contain all implementation logic
317
+ */
318
+ declare abstract class TypedRef<Shape extends DocShape | ContainerShape> {
319
+ /**
320
+ * Internal implementation accessed via Symbol.
321
+ * Subclasses must set this to their specific internals class instance.
322
+ */
323
+ abstract [INTERNAL_SYMBOL]: BaseRefInternals<Shape>;
324
+ /**
325
+ * Serializes the ref to a plain JSON-compatible value.
326
+ * Returns the plain type inferred from the shape.
327
+ */
328
+ abstract toJSON(): Infer<Shape>;
329
+ /**
330
+ * Access the loro() namespace via the well-known symbol.
331
+ * This is used by the loro() function to access CRDT internals.
332
+ */
333
+ get [LORO_SYMBOL](): LoroRefBase;
334
+ }
335
+
336
+ /**
337
+ * Internal implementation for CounterRef.
338
+ * Contains all logic, state, and implementation details.
339
+ */
340
+ declare class CounterRefInternals extends BaseRefInternals<CounterContainerShape> {
341
+ private materialized;
342
+ /** Increment the counter value */
343
+ increment(value?: number): void;
344
+ /** Decrement the counter value */
345
+ decrement(value?: number): void;
346
+ /** Get the current counter value */
347
+ getValue(): number;
348
+ /** No plain values in counter */
349
+ absorbPlainValues(): void;
350
+ /** Create the loro namespace for counter */
351
+ protected createLoroNamespace(): LoroCounterRef;
352
+ }
353
+
354
+ /**
355
+ * Counter typed ref - thin facade that delegates to CounterRefInternals.
356
+ */
357
+ declare class CounterRef extends TypedRef<CounterContainerShape> {
358
+ [INTERNAL_SYMBOL]: CounterRefInternals;
359
+ constructor(params: TypedRefParams<CounterContainerShape>);
360
+ /** Increment the counter by the given value (default 1) */
361
+ increment(value?: number): void;
362
+ /** Decrement the counter by the given value (default 1) */
363
+ decrement(value?: number): void;
364
+ /** Get the current counter value */
365
+ get value(): number;
366
+ valueOf(): number;
367
+ toJSON(): number;
368
+ [Symbol.toPrimitive](hint: string): number | string;
369
+ }
370
+
210
371
  /**
211
372
  * Internal implementation for ListRefBase.
212
373
  * Contains all logic, state, and implementation details for list operations.
@@ -214,7 +375,7 @@ declare function createTypedDoc<Shape extends DocShape>(shape: Shape, existingDo
214
375
  declare class ListRefBaseInternals<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"], MutableItem = NestedShape["_mutable"]> extends BaseRefInternals<any> {
215
376
  private itemCache;
216
377
  /** Get typed ref params for creating child refs at an index */
217
- getTypedRefParams(index: number, shape: ContainerShape): TypedRefParams<ContainerShape>;
378
+ getChildTypedRefParams(index: number, shape: ContainerShape): TypedRefParams<ContainerShape>;
218
379
  /** Get item for predicate functions (returns plain value) */
219
380
  getPredicateItem(index: number): Item | undefined;
220
381
  /** Get mutable item for return values (returns ref or cached value) */
@@ -310,7 +471,7 @@ declare class MovableListRef<NestedShape extends ContainerOrValueShape, Item = N
310
471
  declare class RecordRefInternals<NestedShape extends ContainerOrValueShape> extends BaseRefInternals<any> {
311
472
  private refCache;
312
473
  /** Get typed ref params for creating child refs at a key */
313
- getTypedRefParams(key: string, shape: ContainerShape): TypedRefParams<ContainerShape>;
474
+ getChildTypedRefParams(key: string, shape: ContainerShape): TypedRefParams<ContainerShape>;
314
475
  /** Get a ref for a key without creating (returns undefined for non-existent container keys) */
315
476
  getRef(key: string): unknown;
316
477
  /** Get or create a ref for a key (always creates for container shapes) */
@@ -398,6 +559,11 @@ type StructRef<NestedShapes extends Record<string, ContainerOrValueShape>> = {
398
559
  * @internal
399
560
  */
400
561
  [INTERNAL_SYMBOL]: RefInternalsBase;
562
+ /**
563
+ * Access CRDT internals via the well-known symbol.
564
+ * Used by the loro() function.
565
+ */
566
+ [LORO_SYMBOL]: LoroMapRef;
401
567
  };
402
568
 
403
569
  /**
@@ -481,6 +647,7 @@ interface TreeRefLike<DataShape extends StructContainerShape> {
481
647
  declare class TreeNodeRefInternals<DataShape extends StructContainerShape> implements RefInternalsBase {
482
648
  private readonly params;
483
649
  private dataRef;
650
+ private loroNamespace;
484
651
  constructor(params: TreeNodeRefParams<DataShape>);
485
652
  /** Get the underlying LoroTreeNode */
486
653
  getNode(): LoroTreeNode;
@@ -500,6 +667,10 @@ declare class TreeNodeRefInternals<DataShape extends StructContainerShape> imple
500
667
  getOrCreateDataRef(): StructRef<DataShape["shapes"]>;
501
668
  /** Absorb mutated plain values back into Loro containers */
502
669
  absorbPlainValues(): void;
670
+ /** Get the loro namespace (cached) */
671
+ getLoroNamespace(): LoroTreeNodeRef;
672
+ /** Create the loro namespace for tree node */
673
+ protected createLoroNamespace(): LoroTreeNodeRef;
503
674
  }
504
675
 
505
676
  interface TreeNodeRefParams<DataShape extends StructContainerShape> {
@@ -529,6 +700,10 @@ interface TreeNodeRefParams<DataShape extends StructContainerShape> {
529
700
  declare class TreeNodeRef<DataShape extends StructContainerShape> {
530
701
  [INTERNAL_SYMBOL]: TreeNodeRefInternals<DataShape>;
531
702
  constructor(params: TreeNodeRefParams<DataShape>);
703
+ /**
704
+ * Access the loro() namespace via the well-known symbol.
705
+ */
706
+ get [LORO_SYMBOL](): LoroTreeNodeRef;
532
707
  /**
533
708
  * The unique TreeID of this node.
534
709
  */
@@ -675,9 +850,14 @@ declare class TreeRef<DataShape extends StructContainerShape> extends TypedRef<T
675
850
  roots(): TreeNodeRef<DataShape>[];
676
851
  /**
677
852
  * Get all nodes in the tree (unordered).
678
- * Includes all nodes, not just roots.
853
+ * By default, excludes deleted nodes (tombstones).
854
+ *
855
+ * @param options.includeDeleted - If true, includes deleted nodes. Default: false.
856
+ * @returns Array of TreeNodeRef for matching nodes
679
857
  */
680
- nodes(): TreeNodeRef<DataShape>[];
858
+ nodes(options?: {
859
+ includeDeleted?: boolean;
860
+ }): TreeNodeRef<DataShape>[];
681
861
  /**
682
862
  * Check if a node with the given ID exists in the tree.
683
863
  */
@@ -804,6 +984,13 @@ interface LoroTreeRef extends LoroRefBase {
804
984
  /** The underlying LoroTree */
805
985
  readonly container: LoroTree;
806
986
  }
987
+ /**
988
+ * loro() return type for TreeNodeRef.
989
+ */
990
+ interface LoroTreeNodeRef extends LoroRefBase {
991
+ /** The underlying LoroTreeNode */
992
+ readonly container: LoroTreeNode;
993
+ }
807
994
  /**
808
995
  * loro() return type for TypedDoc.
809
996
  * Provides access to doc-level operations.
@@ -849,7 +1036,11 @@ declare function loro(ref: CounterRef): LoroCounterRef;
849
1036
  /**
850
1037
  * Access CRDT internals for a TreeRef.
851
1038
  */
852
- declare function loro<DataShape extends StructContainerShape>(ref: TreeRef<DataShape>): LoroTreeRef;
1039
+ declare function loro<DataShape extends StructContainerShape>(ref: TreeRef<DataShape> | TreeRefInterface<DataShape>): LoroTreeRef;
1040
+ /**
1041
+ * Access CRDT internals for a TreeNodeRef.
1042
+ */
1043
+ declare function loro<DataShape extends StructContainerShape>(ref: TreeNodeRef<DataShape>): LoroTreeNodeRef;
853
1044
  /**
854
1045
  * Access CRDT internals for a TypedDoc.
855
1046
  */
@@ -859,125 +1050,6 @@ declare function loro<Shape extends DocShape>(doc: TypedDoc<Shape>): LoroTypedDo
859
1050
  */
860
1051
  declare function loro<Shape extends ContainerShape>(ref: TypedRef<Shape>): LoroRefBase;
861
1052
 
862
- /**
863
- * Symbol for internal methods that should not be enumerable or accessible to users.
864
- * Used to hide implementation details like absorbPlainValues(), getTypedRefParams(), etc.
865
- *
866
- * This achieves Success Criteria #7 from loro-api-refactor.md:
867
- * "Internal methods hidden - Via Symbol, not enumerable"
868
- */
869
- declare const INTERNAL_SYMBOL: unique symbol;
870
- /**
871
- * Minimal interface for refs that only need absorbPlainValues.
872
- * Used by TreeNodeRef which doesn't extend TypedRef.
873
- */
874
- interface RefInternalsBase {
875
- /** Absorb mutated plain values back into Loro containers */
876
- absorbPlainValues(): void;
877
- }
878
- type TypedRefParams<Shape extends DocShape | ContainerShape> = {
879
- shape: Shape;
880
- placeholder?: Infer<Shape>;
881
- getContainer: () => ShapeToContainer<Shape>;
882
- autoCommit?: boolean;
883
- batchedMutation?: boolean;
884
- getDoc: () => LoroDoc;
885
- };
886
- /**
887
- * Abstract base class for all ref internal implementations.
888
- * Contains shared logic that was previously in TypedRef.createBaseInternals().
889
- *
890
- * Subclasses implement specific behavior for each ref type.
891
- */
892
- declare abstract class BaseRefInternals<Shape extends DocShape | ContainerShape> implements RefInternalsBase {
893
- protected readonly params: TypedRefParams<Shape>;
894
- protected cachedContainer: ShapeToContainer<Shape> | undefined;
895
- protected loroNamespace: LoroRefBase | undefined;
896
- constructor(params: TypedRefParams<Shape>);
897
- /** Get the underlying Loro container (cached) */
898
- getContainer(): ShapeToContainer<Shape>;
899
- /** Commit changes if autoCommit is enabled */
900
- commitIfAuto(): void;
901
- /** Get the shape for this ref */
902
- getShape(): Shape;
903
- /** Get the placeholder value */
904
- getPlaceholder(): Infer<Shape> | undefined;
905
- /** Check if autoCommit is enabled */
906
- getAutoCommit(): boolean;
907
- /** Check if in batched mutation mode */
908
- getBatchedMutation(): boolean;
909
- /** Get the LoroDoc */
910
- getDoc(): LoroDoc;
911
- /** Get the loro namespace (cached) */
912
- getLoroNamespace(): LoroRefBase;
913
- /** Absorb mutated plain values back into Loro containers - subclasses override */
914
- abstract absorbPlainValues(): void;
915
- /** Create the loro() namespace object - subclasses override for specific types */
916
- protected createLoroNamespace(): LoroRefBase;
917
- }
918
- /**
919
- * Base class for all typed refs.
920
- *
921
- * All internal methods are accessed via [INTERNAL_SYMBOL] to prevent
922
- * namespace collisions with user data properties.
923
- *
924
- * Uses the Facade + Implementation pattern:
925
- * - TypedRef is the thin public facade
926
- * - BaseRefInternals subclasses contain all implementation logic
927
- */
928
- declare abstract class TypedRef<Shape extends DocShape | ContainerShape> {
929
- /**
930
- * Internal implementation accessed via Symbol.
931
- * Subclasses must set this to their specific internals class instance.
932
- */
933
- abstract [INTERNAL_SYMBOL]: BaseRefInternals<Shape>;
934
- /**
935
- * Serializes the ref to a plain JSON-compatible value.
936
- * Returns the plain type inferred from the shape.
937
- */
938
- abstract toJSON(): Infer<Shape>;
939
- /**
940
- * Access the loro() namespace via the well-known symbol.
941
- * This is used by the loro() function to access CRDT internals.
942
- */
943
- get [LORO_SYMBOL](): LoroRefBase;
944
- }
945
-
946
- /**
947
- * Internal implementation for CounterRef.
948
- * Contains all logic, state, and implementation details.
949
- */
950
- declare class CounterRefInternals extends BaseRefInternals<CounterContainerShape> {
951
- private materialized;
952
- /** Increment the counter value */
953
- increment(value?: number): void;
954
- /** Decrement the counter value */
955
- decrement(value?: number): void;
956
- /** Get the current counter value */
957
- getValue(): number;
958
- /** No plain values in counter */
959
- absorbPlainValues(): void;
960
- /** Create the loro namespace for counter */
961
- protected createLoroNamespace(): LoroCounterRef;
962
- }
963
-
964
- /**
965
- * Counter typed ref - thin facade that delegates to CounterRefInternals.
966
- */
967
- declare class CounterRef extends TypedRef<CounterContainerShape> {
968
- [INTERNAL_SYMBOL]: CounterRefInternals;
969
- constructor(params: TypedRefParams<CounterContainerShape>);
970
- /** Increment the counter by the given value (default 1) */
971
- increment(value?: number): void;
972
- /** Decrement the counter by the given value (default 1) */
973
- decrement(value?: number): void;
974
- /** Get the current counter value */
975
- get value(): number;
976
- valueOf(): number;
977
- toJSON(): number;
978
- [Symbol.toPrimitive](hint: string): number | string;
979
- }
980
-
981
1053
  type WithPlaceholder<S extends Shape<any, any, any>> = S & {
982
1054
  placeholder(value: S["_placeholder"]): S;
983
1055
  };
@@ -1034,8 +1106,10 @@ interface TreeRefInterface<DataShape extends StructContainerShape> {
1034
1106
  createNode(initialData?: Partial<DataShape["_plain"]>): TreeNodeRef<DataShape>;
1035
1107
  /** Get all root nodes (nodes without parents) */
1036
1108
  roots(): TreeNodeRef<DataShape>[];
1037
- /** Get all nodes in the tree (unordered) */
1038
- nodes(): TreeNodeRef<DataShape>[];
1109
+ /** Get all nodes in the tree (unordered). By default excludes deleted nodes. */
1110
+ nodes(options?: {
1111
+ includeDeleted?: boolean;
1112
+ }): TreeNodeRef<DataShape>[];
1039
1113
  /** Check if a node with the given ID exists in the tree */
1040
1114
  has(id: TreeID): boolean;
1041
1115
  /** Enable fractional index generation for ordering */
@@ -1048,6 +1122,10 @@ interface TreeRefInterface<DataShape extends StructContainerShape> {
1048
1122
  fractionalIndex: string;
1049
1123
  data: DataShape["_plain"];
1050
1124
  }>;
1125
+ /**
1126
+ * Access CRDT internals via the well-known symbol.
1127
+ */
1128
+ readonly [LORO_SYMBOL]: LoroTreeRef;
1051
1129
  }
1052
1130
  /**
1053
1131
  * Container shape for tree (forest) structures.
@@ -1092,9 +1170,7 @@ type MapContainerShape<NestedShapes extends Record<string, ContainerOrValueShape
1092
1170
  */
1093
1171
  interface StructContainerShape<NestedShapes extends Record<string, ContainerOrValueShape> = Record<string, ContainerOrValueShape>> extends Shape<{
1094
1172
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
1095
- }, StructRef<NestedShapes> & {
1096
- [K in keyof NestedShapes]: NestedShapes[K]["_mutable"];
1097
- }, {
1173
+ }, StructRef<NestedShapes>, {
1098
1174
  [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
1099
1175
  }> {
1100
1176
  readonly _type: "struct";
@@ -1394,7 +1470,7 @@ declare function derivePlaceholder<T extends DocShape>(schema: T): InferPlacehol
1394
1470
  declare function deriveShapePlaceholder(shape: ContainerOrValueShape): unknown;
1395
1471
 
1396
1472
  /**
1397
- * The primary method of mutating typed documents.
1473
+ * The primary method of mutating typed documents and refs.
1398
1474
  * Batches multiple mutations into a single transaction.
1399
1475
  * All changes commit together at the end.
1400
1476
  *
@@ -1403,26 +1479,46 @@ declare function deriveShapePlaceholder(shape: ContainerOrValueShape): unknown;
1403
1479
  * - Performance (fewer commits)
1404
1480
  * - Atomic undo (all changes = one undo step)
1405
1481
  *
1406
- * Returns the doc for chaining.
1482
+ * Returns the doc/ref for chaining.
1407
1483
  *
1408
- * @param doc - The TypedDoc to mutate
1484
+ * @param target - The TypedDoc or TypedRef to mutate
1409
1485
  * @param fn - Function that performs mutations on the draft
1410
- * @returns The same TypedDoc for chaining
1486
+ * @returns The same target for chaining
1411
1487
  *
1412
1488
  * @example
1413
1489
  * ```typescript
1414
1490
  * import { change } from "@loro-extended/change"
1415
1491
  *
1416
- * // Chainable API
1492
+ * // Document-level change (chainable)
1417
1493
  * change(doc, draft => {
1418
1494
  * draft.count.increment(10)
1419
1495
  * draft.title.update("Hello")
1420
1496
  * })
1421
1497
  * .count.increment(5) // Optional: continue mutating
1422
- * .toJSON() // Optional: get last item snapshot when needed
1498
+ * .toJSON() // Optional: get snapshot
1499
+ *
1500
+ * // Ref-level change - enables encapsulation
1501
+ * function addItems(list: ListRef<...>) {
1502
+ * change(list, draft => {
1503
+ * draft.push({ name: "item1" })
1504
+ * draft.push({ name: "item2" })
1505
+ * })
1506
+ * }
1507
+ *
1508
+ * // TreeRef example - pass around refs without exposing the doc
1509
+ * function addStates(states: TreeRef<StateShape>) {
1510
+ * change(states, draft => {
1511
+ * draft.createNode({ name: "idle" })
1512
+ * draft.createNode({ name: "running" })
1513
+ * })
1514
+ * }
1423
1515
  * ```
1424
1516
  */
1425
1517
  declare function change<Shape extends DocShape>(doc: TypedDoc<Shape>, fn: (draft: Mutable<Shape>) => void): TypedDoc<Shape>;
1518
+ declare function change<DataShape extends StructContainerShape>(ref: TreeRef<DataShape>, fn: (draft: TreeRef<DataShape>) => void): TreeRef<DataShape>;
1519
+ declare function change<DataShape extends StructContainerShape>(ref: TreeRefInterface<DataShape>, fn: (draft: TreeRefInterface<DataShape>) => void): TreeRefInterface<DataShape>;
1520
+ declare function change<NestedShapes extends Record<string, ContainerOrValueShape>>(ref: StructRef<NestedShapes>, fn: (draft: StructRef<NestedShapes>) => void): StructRef<NestedShapes>;
1521
+ declare function change<T extends TypedRef<ContainerShape>>(ref: T, fn: (draft: T) => void): T;
1426
1522
  /**
1427
1523
  * Access the underlying LoroDoc for advanced operations.
1428
1524
  * Works on both TypedDoc and any typed ref (TextRef, CounterRef, ListRef, etc.).
@@ -1448,6 +1544,7 @@ declare function change<Shape extends DocShape>(doc: TypedDoc<Shape>, fn: (draft
1448
1544
  declare function getLoroDoc<Shape extends DocShape>(doc: TypedDoc<Shape>): LoroDoc;
1449
1545
  declare function getLoroDoc<Shape extends ContainerShape>(ref: TypedRef<Shape>): LoroDoc;
1450
1546
  declare function getLoroDoc<DataShape extends StructContainerShape>(ref: TreeRef<DataShape>): LoroDoc;
1547
+ declare function getLoroDoc<DataShape extends StructContainerShape>(ref: TreeRefInterface<DataShape>): LoroDoc;
1451
1548
  /**
1452
1549
  * Access the underlying Loro container from a typed ref.
1453
1550
  * Returns the correctly-typed container based on the ref type.
@@ -1480,7 +1577,35 @@ declare function getLoroContainer(ref: TypedRef<RecordContainerShape>): LoroMap;
1480
1577
  declare function getLoroContainer(ref: TypedRef<StructContainerShape>): LoroMap;
1481
1578
  declare function getLoroContainer<NestedShapes extends Record<string, ContainerOrValueShape>>(ref: StructRef<NestedShapes>): LoroMap;
1482
1579
  declare function getLoroContainer<DataShape extends StructContainerShape>(ref: TreeRef<DataShape>): LoroTree;
1580
+ declare function getLoroContainer<DataShape extends StructContainerShape>(ref: TreeRefInterface<DataShape>): LoroTree;
1483
1581
  declare function getLoroContainer<Shape extends ContainerShape>(ref: TypedRef<Shape>): ShapeToContainer<Shape>;
1582
+ /**
1583
+ * Creates a new TypedDoc at a specified version (frontiers).
1584
+ * The forked doc will only contain history before the specified frontiers.
1585
+ * The forked doc has a different PeerID from the original.
1586
+ *
1587
+ * For raw LoroDoc access, use: `loro(doc).doc.forkAt(frontiers)`
1588
+ *
1589
+ * @param doc - The TypedDoc to fork
1590
+ * @param frontiers - The version to fork at (obtained from `loro(doc).doc.frontiers()`)
1591
+ * @returns A new TypedDoc with the same schema at the specified version
1592
+ *
1593
+ * @example
1594
+ * ```typescript
1595
+ * import { forkAt, loro } from "@loro-extended/change"
1596
+ *
1597
+ * const doc = createTypedDoc(schema);
1598
+ * doc.title.update("Hello");
1599
+ * const frontiers = loro(doc).doc.frontiers();
1600
+ * doc.title.update("World");
1601
+ *
1602
+ * // Fork at the earlier version
1603
+ * const forkedDoc = forkAt(doc, frontiers);
1604
+ * console.log(forkedDoc.title.toString()); // "Hello"
1605
+ * console.log(doc.title.toString()); // "World"
1606
+ * ```
1607
+ */
1608
+ declare function forkAt<Shape extends DocShape>(doc: TypedDoc<Shape>, frontiers: Frontiers): TypedDoc<Shape>;
1484
1609
 
1485
1610
  /**
1486
1611
  * Overlays CRDT state with placeholder defaults
@@ -1623,4 +1748,4 @@ declare function createPlaceholderProxy<T extends object>(target: T): T;
1623
1748
  */
1624
1749
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
1625
1750
 
1626
- export { type AnyContainerShape, type AnyValueShape, type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, CounterRef, type DiscriminatedUnionValueShape, type DocShape, type Infer, type InferMutableType, type InferPlaceholderType, type InferRaw, LORO_SYMBOL, type ListContainerShape, ListRef, type LoroCounterRef, type LoroListRef, type LoroMapRef, type LoroRefBase, type LoroTextRef, type LoroTreeRef, type LoroTypedDocRef, type MapContainerShape, type MovableListContainerShape, MovableListRef, type Mutable, type ObjectValueShape, type PathBuilder, type PathNode, type PathSegment, type PathSelector, type RecordContainerShape, RecordRef, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StructContainerShape, type StructRef, type StructValueShape, type TextContainerShape, TextRef, type TreeContainerShape, type TreeNodeJSON, TreeNodeRef, TreeRef, type TreeRefInterface, type TypedDoc, type UnionValueShape, type ValueShape, type WithNullable, type WithPlaceholder, change, compileToJsonPath, createPathBuilder, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, evaluatePath, evaluatePathOnValue, getLoroContainer, getLoroDoc, hasWildcard, loro, mergeValue, overlayPlaceholder, validatePlaceholder };
1751
+ export { type AnyContainerShape, type AnyValueShape, type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, CounterRef, type DiscriminatedUnionValueShape, type DocShape, type Frontiers, type Infer, type InferMutableType, type InferPlaceholderType, type InferRaw, LORO_SYMBOL, type ListContainerShape, ListRef, type LoroCounterRef, type LoroListRef, type LoroMapRef, type LoroRefBase, type LoroTextRef, type LoroTreeRef, type LoroTypedDocRef, type MapContainerShape, type MovableListContainerShape, MovableListRef, type Mutable, type ObjectValueShape, type PathBuilder, type PathNode, type PathSegment, type PathSelector, type RecordContainerShape, RecordRef, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StructContainerShape, type StructRef, type StructValueShape, type TextContainerShape, TextRef, type TreeContainerShape, type TreeNodeJSON, TreeNodeRef, TreeRef, type TreeRefInterface, type TypedDoc, type UnionValueShape, type ValueShape, type WithNullable, type WithPlaceholder, change, compileToJsonPath, createPathBuilder, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, evaluatePath, evaluatePathOnValue, forkAt, getLoroContainer, getLoroDoc, hasWildcard, loro, mergeValue, overlayPlaceholder, validatePlaceholder };