@loro-extended/change 5.3.0 → 5.4.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
@@ -605,15 +605,18 @@ loro(doc).rawValue; // Unmerged CRDT value
605
605
 
606
606
  **RecordRef** (Map-like interface)
607
607
 
608
- | Direct Access | Only via `loro()` |
609
- | -------------------- | ------------------------------ |
610
- | `get(key)` | `setContainer(key, container)` |
611
- | `set(key, value)` | `subscribe(callback)` |
612
- | `delete(key)` | `doc` |
613
- | `has(key)` | `container` |
614
- | `keys()`, `values()` | |
615
- | `size` | |
616
- | `toJSON()` | |
608
+ | Direct Access | Only via `loro()` |
609
+ | -------------------------------- | ------------------------------ |
610
+ | `get(key)` | `setContainer(key, container)` |
611
+ | `set(key, value)` | `subscribe(callback)` |
612
+ | `delete(key)` | `doc` |
613
+ | `has(key)` | `container` |
614
+ | `keys()`, `values()`, `entries()`| |
615
+ | `size` | |
616
+ | `replace(values)` | |
617
+ | `merge(values)` | |
618
+ | `clear()` | |
619
+ | `toJSON()` | |
617
620
 
618
621
  **TextRef**
619
622
 
@@ -906,6 +909,40 @@ draft.metadata.values();
906
909
  const value = draft.metadata.get("key");
907
910
  ```
908
911
 
912
+ ### Record Bulk Update Operations
913
+
914
+ Records support bulk update methods for efficient batch operations:
915
+
916
+ ```typescript
917
+ // Replace entire contents - keys not in the new object are removed
918
+ draft.players.replace({
919
+ alice: { name: "Alice", score: 100 },
920
+ bob: { name: "Bob", score: 50 },
921
+ });
922
+ // Result: only alice and bob exist, any previous entries are removed
923
+
924
+ // Merge values - existing keys not in the new object are kept
925
+ draft.scores.merge({
926
+ alice: 150, // updates alice
927
+ charlie: 25, // adds charlie
928
+ });
929
+ // Result: alice=150, bob=50 (unchanged), charlie=25
930
+
931
+ // Clear all entries
932
+ draft.history.clear();
933
+ // Result: empty record
934
+ ```
935
+
936
+ **Method semantics:**
937
+
938
+ | Method | Adds new | Updates existing | Removes absent |
939
+ | ------------------ | -------- | ---------------- | -------------- |
940
+ | `replace(values)` | ✅ | ✅ | ✅ |
941
+ | `merge(values)` | ✅ | ✅ | ❌ |
942
+ | `clear()` | ❌ | ❌ | ✅ (all) |
943
+
944
+ These methods batch all operations into a single commit, avoiding multiple subscription notifications.
945
+
909
946
  ### Tree Operations
910
947
 
911
948
  Trees are hierarchical structures where each node has typed metadata. Perfect for state machines, file systems, org charts, and nested data.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PeerID, LoroDoc, Container, LoroTreeNode, TreeID, Subscription, LoroText, LoroList, LoroMovableList, LoroMap, LoroTree, LoroCounter, Value } from 'loro-crdt';
1
+ import { PeerID, LoroDoc, Container, LoroTreeNode, TreeID, LoroEventBatch, Subscription, LoroText, LoroList, LoroMovableList, LoroMap, LoroTree, LoroCounter, Value, ContainerID, Diff } from 'loro-crdt';
2
2
 
3
3
  type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
4
4
  /**
@@ -256,6 +256,8 @@ declare const INTERNAL_SYMBOL: unique symbol;
256
256
  interface RefInternalsBase {
257
257
  /** Absorb mutated plain values back into Loro containers */
258
258
  absorbPlainValues(): void;
259
+ /** Force materialization of the container and its nested containers */
260
+ materialize(): void;
259
261
  }
260
262
  type TypedRefParams<Shape extends DocShape | ContainerShape> = {
261
263
  shape: Shape;
@@ -275,11 +277,19 @@ declare abstract class BaseRefInternals<Shape extends DocShape | ContainerShape>
275
277
  protected readonly params: TypedRefParams<Shape>;
276
278
  protected cachedContainer: ShapeToContainer<Shape> | undefined;
277
279
  protected loroNamespace: LoroRefBase | undefined;
280
+ private _suppressAutoCommit;
278
281
  constructor(params: TypedRefParams<Shape>);
279
282
  /** Get the underlying Loro container (cached) */
280
283
  getContainer(): ShapeToContainer<Shape>;
281
- /** Commit changes if autoCommit is enabled */
284
+ /** Commit changes if autoCommit is enabled and not suppressed */
282
285
  commitIfAuto(): void;
286
+ /**
287
+ * Temporarily suppress auto-commit during batch operations.
288
+ * Used by assignPlainValueToTypedRef() to batch multiple property assignments.
289
+ */
290
+ setSuppressAutoCommit(suppress: boolean): void;
291
+ /** Check if auto-commit is currently suppressed */
292
+ isSuppressAutoCommit(): boolean;
283
293
  /** Get the shape for this ref */
284
294
  getShape(): Shape;
285
295
  /** Get the placeholder value */
@@ -302,6 +312,8 @@ declare abstract class BaseRefInternals<Shape extends DocShape | ContainerShape>
302
312
  getLoroNamespace(): LoroRefBase;
303
313
  /** Absorb mutated plain values back into Loro containers - subclasses override */
304
314
  abstract absorbPlainValues(): void;
315
+ /** Force materialization of the container and its nested containers */
316
+ materialize(): void;
305
317
  /** Create the loro() namespace object - subclasses override for specific types */
306
318
  protected createLoroNamespace(): LoroRefBase;
307
319
  }
@@ -480,6 +492,20 @@ declare class RecordRefInternals<NestedShape extends ContainerOrValueShape> exte
480
492
  set(key: string, value: any): void;
481
493
  /** Delete a key */
482
494
  delete(key: string): void;
495
+ /**
496
+ * Replace entire contents with new values.
497
+ * Keys not in `values` are removed.
498
+ */
499
+ replace(values: Record<string, any>): void;
500
+ /**
501
+ * Merge values into record.
502
+ * Existing keys not in `values` are kept.
503
+ */
504
+ merge(values: Record<string, any>): void;
505
+ /**
506
+ * Remove all entries from the record.
507
+ */
508
+ clear(): void;
483
509
  /** Absorb mutated plain values back into Loro containers */
484
510
  absorbPlainValues(): void;
485
511
  /** Create the loro namespace for record */
@@ -501,8 +527,59 @@ declare class RecordRef<NestedShape extends ContainerOrValueShape> extends Typed
501
527
  setContainer<C extends Container>(key: string, container: C): C;
502
528
  has(key: string): boolean;
503
529
  keys(): string[];
504
- values(): any[];
530
+ /**
531
+ * Returns an array of all values in the record.
532
+ * For container-valued records, returns properly typed refs.
533
+ */
534
+ values(): InferMutableType<NestedShape>[];
535
+ /**
536
+ * Returns an array of [key, value] pairs.
537
+ * For container-valued records, values are properly typed refs.
538
+ */
539
+ entries(): [string, InferMutableType<NestedShape>][];
505
540
  get size(): number;
541
+ /**
542
+ * Replace entire contents with new values.
543
+ * Keys not in `values` are removed.
544
+ *
545
+ * @example
546
+ * ```typescript
547
+ * doc.change(draft => {
548
+ * draft.players.replace({
549
+ * alice: { score: 100 },
550
+ * bob: { score: 50 }
551
+ * })
552
+ * })
553
+ * ```
554
+ */
555
+ replace(values: Record<string, Infer<NestedShape>>): void;
556
+ /**
557
+ * Merge values into record.
558
+ * Existing keys not in `values` are kept.
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * doc.change(draft => {
563
+ * // Adds charlie, updates alice, keeps bob unchanged
564
+ * draft.players.merge({
565
+ * alice: { score: 150 },
566
+ * charlie: { score: 25 }
567
+ * })
568
+ * })
569
+ * ```
570
+ */
571
+ merge(values: Record<string, Infer<NestedShape>>): void;
572
+ /**
573
+ * Remove all entries from the record.
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * doc.change(draft => {
578
+ * draft.players.clear()
579
+ * })
580
+ * ```
581
+ */
582
+ clear(): void;
506
583
  toJSON(): Record<string, Infer<NestedShape>>;
507
584
  }
508
585
 
@@ -667,6 +744,8 @@ declare class TreeNodeRefInternals<DataShape extends StructContainerShape> imple
667
744
  getOrCreateDataRef(): StructRef<DataShape["shapes"]>;
668
745
  /** Absorb mutated plain values back into Loro containers */
669
746
  absorbPlainValues(): void;
747
+ /** Force materialization of the container and its nested containers */
748
+ materialize(): void;
670
749
  /** Get the loro namespace (cached) */
671
750
  getLoroNamespace(): LoroTreeNodeRef;
672
751
  /** Create the loro namespace for tree node */
@@ -930,7 +1009,7 @@ interface LoroRefBase {
930
1009
  * @param callback - Function called when the container changes
931
1010
  * @returns Subscription that can be used to unsubscribe
932
1011
  */
933
- subscribe(callback: (event: unknown) => void): Subscription;
1012
+ subscribe(callback: (event: LoroEventBatch) => void): Subscription;
934
1013
  }
935
1014
  /**
936
1015
  * loro() return type for ListRef and MovableListRef.
@@ -1159,10 +1238,6 @@ interface MovableListContainerShape<NestedShape extends ContainerOrValueShape =
1159
1238
  readonly _type: "movableList";
1160
1239
  readonly shape: NestedShape;
1161
1240
  }
1162
- /**
1163
- * @deprecated Use StructContainerShape instead. MapContainerShape is an alias for backward compatibility.
1164
- */
1165
- type MapContainerShape<NestedShapes extends Record<string, ContainerOrValueShape> = Record<string, ContainerOrValueShape>> = StructContainerShape<NestedShapes>;
1166
1241
  /**
1167
1242
  * Container shape for objects with fixed keys (structs).
1168
1243
  * This is the preferred way to define fixed-key objects.
@@ -1243,10 +1318,6 @@ interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array, Uint8Array>
1243
1318
  readonly _type: "value";
1244
1319
  readonly valueType: "uint8array";
1245
1320
  }
1246
- /**
1247
- * @deprecated Use StructValueShape instead. ObjectValueShape is an alias for backward compatibility.
1248
- */
1249
- type ObjectValueShape<T extends Record<string, ValueShape> = Record<string, ValueShape>> = StructValueShape<T>;
1250
1321
  /**
1251
1322
  * Value shape for objects with fixed keys (structs).
1252
1323
  * This is the preferred way to define fixed-key plain value objects.
@@ -1600,6 +1671,39 @@ declare function getLoroContainer<NestedShapes extends Record<string, ContainerO
1600
1671
  declare function getLoroContainer<DataShape extends StructContainerShape>(ref: TreeRef<DataShape>): LoroTree;
1601
1672
  declare function getLoroContainer<DataShape extends StructContainerShape>(ref: TreeRefInterface<DataShape>): LoroTree;
1602
1673
  declare function getLoroContainer<Shape extends ContainerShape>(ref: TypedRef<Shape>): ShapeToContainer<Shape>;
1674
+ /**
1675
+ * Creates a new TypedDoc as a fork of the current document.
1676
+ * The forked doc contains all history up to the current version.
1677
+ * The forked doc has a different PeerID from the original by default.
1678
+ *
1679
+ * For raw LoroDoc access, use: `loro(doc).doc.fork()`
1680
+ *
1681
+ * @param doc - The TypedDoc to fork
1682
+ * @param options - Optional settings
1683
+ * @param options.preservePeerId - If true, copies the original doc's peer ID to the fork
1684
+ * @returns A new TypedDoc with the same schema at the current version
1685
+ *
1686
+ * @example
1687
+ * ```typescript
1688
+ * import { fork, loro } from "@loro-extended/change"
1689
+ *
1690
+ * const doc = createTypedDoc(schema);
1691
+ * doc.title.update("Hello");
1692
+ *
1693
+ * // Fork the document
1694
+ * const forkedDoc = fork(doc);
1695
+ * forkedDoc.title.update("World");
1696
+ *
1697
+ * console.log(doc.title.toString()); // "Hello"
1698
+ * console.log(forkedDoc.title.toString()); // "World"
1699
+ *
1700
+ * // Fork with same peer ID (for World/Worldview pattern)
1701
+ * const worldview = fork(world, { preservePeerId: true });
1702
+ * ```
1703
+ */
1704
+ declare function fork<Shape extends DocShape>(doc: TypedDoc<Shape>, options?: {
1705
+ preservePeerId?: boolean;
1706
+ }): TypedDoc<Shape>;
1603
1707
  /**
1604
1708
  * Creates a new TypedDoc at a specified version (frontiers).
1605
1709
  * The forked doc will only contain history before the specified frontiers.
@@ -1627,6 +1731,52 @@ declare function getLoroContainer<Shape extends ContainerShape>(ref: TypedRef<Sh
1627
1731
  * ```
1628
1732
  */
1629
1733
  declare function forkAt<Shape extends DocShape>(doc: TypedDoc<Shape>, frontiers: Frontiers): TypedDoc<Shape>;
1734
+ /**
1735
+ * Creates a new TypedDoc at a specified version using a shallow snapshot.
1736
+ * Unlike `forkAt`, this creates a "garbage-collected" snapshot that only
1737
+ * contains the current state and history since the specified frontiers.
1738
+ *
1739
+ * This is more memory-efficient than `forkAt` for documents with long history,
1740
+ * especially useful for the fork-and-merge pattern in LEA where we only need:
1741
+ * 1. Read current state
1742
+ * 2. Apply changes
1743
+ * 3. Export delta and merge back
1744
+ *
1745
+ * The shallow fork has a different PeerID from the original by default.
1746
+ * Use `preservePeerId: true` to copy the original's peer ID (useful for
1747
+ * fork-and-merge patterns where you want consistent frontier progression).
1748
+ *
1749
+ * @param doc - The TypedDoc to fork
1750
+ * @param frontiers - The version to fork at (obtained from `loro(doc).doc.frontiers()`)
1751
+ * @param options - Optional settings
1752
+ * @param options.preservePeerId - If true, copies the original doc's peer ID to the fork
1753
+ * @returns A new TypedDoc with the same schema at the specified version (shallow)
1754
+ *
1755
+ * @example
1756
+ * ```typescript
1757
+ * import { shallowForkAt, loro } from "@loro-extended/change"
1758
+ *
1759
+ * const doc = createTypedDoc(schema);
1760
+ * doc.title.update("Hello");
1761
+ * const frontiers = loro(doc).doc.frontiers();
1762
+ *
1763
+ * // Create a shallow fork (memory-efficient)
1764
+ * const shallowDoc = shallowForkAt(doc, frontiers, { preservePeerId: true });
1765
+ *
1766
+ * // Modify the shallow doc
1767
+ * shallowDoc.title.update("World");
1768
+ *
1769
+ * // Merge changes back
1770
+ * const update = loro(shallowDoc).doc.export({
1771
+ * mode: "update",
1772
+ * from: loro(doc).doc.version()
1773
+ * });
1774
+ * loro(doc).doc.import(update);
1775
+ * ```
1776
+ */
1777
+ declare function shallowForkAt<Shape extends DocShape>(doc: TypedDoc<Shape>, frontiers: Frontiers, options?: {
1778
+ preservePeerId?: boolean;
1779
+ }): TypedDoc<Shape>;
1630
1780
 
1631
1781
  /**
1632
1782
  * Overlays CRDT state with placeholder defaults
@@ -1763,10 +1913,21 @@ declare function evaluatePathOnValue(value: unknown, segments: PathSegment[]): u
1763
1913
  */
1764
1914
  declare function createPlaceholderProxy<T extends object>(target: T): T;
1765
1915
 
1916
+ /**
1917
+ * Replay a diff as local operations on a document.
1918
+ *
1919
+ * Unlike doc.import() which creates import events, this creates LOCAL events
1920
+ * that are captured by subscribeLocalUpdates() and UndoManager.
1921
+ *
1922
+ * @param doc - The target document to apply changes to
1923
+ * @param diff - The diff from doc.diff(from, to, false)
1924
+ */
1925
+ declare function replayDiff(doc: LoroDoc, diff: [ContainerID, Diff][]): void;
1926
+
1766
1927
  /**
1767
1928
  * Validates placeholder against schema structure without using Zod
1768
1929
  * Combines the functionality of createPlaceholderValidator and createValueValidator
1769
1930
  */
1770
1931
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
1771
1932
 
1772
- 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 };
1933
+ 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 MovableListContainerShape, MovableListRef, type Mutable, type NumberValueShape, type PathBuilder, type PathNode, type PathSegment, type PathSelector, type RecordContainerShape, RecordRef, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StringValueShape, 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, fork, forkAt, getLoroContainer, getLoroDoc, hasWildcard, loro, mergeValue, overlayPlaceholder, replayDiff, shallowForkAt, validatePlaceholder };