loro-crdt 1.0.9 β†’ 1.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 9abeb81: Add methods to modify VV
8
+ - ee26952: Add isDeleted() method to each container
9
+
10
+ ## 1.1.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 6e878d2: Feat add API to query creators, the last editors/movers
15
+ - 778ca54: Feat: allow users to query the changed containers in the target id range
16
+
17
+ ### Patch Changes
18
+
19
+ - 6616101: Perf: optimize importBatch
20
+
21
+ When using importBatch to import a series of snapshots and updates, we should import the snapshot with the greatest version first.
22
+
23
+ - 6e878d2: Feat: getLastEditor on LoroMap
24
+ - 8486234: Fix get encoded blob meta
25
+
3
26
  ## 1.0.9
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -9,8 +9,7 @@
9
9
  <a href="https://loro.dev" alt="loro-site">Loro</a>
10
10
  </h1>
11
11
  <p align="center">
12
- <b>Reimagine state management with CRDTs 🦜</b><br/>
13
- Make your app state synchronized and collaborative effortlessly.
12
+ <b>Make your JSON data collaborative and version-controlled 🦜</b>
14
13
  </p>
15
14
  <p align="center">
16
15
  <a href="https://trendshift.io/repositories/4964" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4964" alt="loro-dev%2Floro | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
@@ -42,11 +41,11 @@
42
41
  ✨ Loro 1.0 is out! Read the <a href="https://loro.dev/blog/v1.0">announcement</a>.
43
42
  </h4>
44
43
 
45
- Loro is a [CRDTs(Conflict-free Replicated Data Types)](https://crdt.tech/) library that makes building [local-first apps][local-first] easier. It is currently available for JavaScript (via WASM) and Rust developers.
44
+ Loro is a [CRDTs(Conflict-free Replicated Data Types)](https://crdt.tech/) library that makes building [local-first][local-first] and collaborative apps easier. You can now use it in Rust, JS (via WASM), and Swift.
46
45
 
47
46
  # Features
48
47
 
49
- **Basic Features Provided by CRDTs**
48
+ **Features Provided by CRDTs**
50
49
 
51
50
  - P2P Synchronization
52
51
  - Automatic Merging
@@ -61,10 +60,10 @@ Loro is a [CRDTs(Conflict-free Replicated Data Types)](https://crdt.tech/) libra
61
60
  - 🌲 [Moveable Tree](https://loro.dev/docs/tutorial/tree)
62
61
  - πŸš— [Moveable List](https://loro.dev/docs/tutorial/list)
63
62
  - πŸ—ΊοΈ [Last-Write-Wins Map](https://loro.dev/docs/tutorial/map)
64
- - πŸ”„ [Replayable Event Graph](https://loro.dev/docs/advanced/replayable_event_graph)
65
63
 
66
64
  **Advanced Features in Loro**
67
65
 
66
+ - πŸš€ [Fast Document Loading](https://loro.dev/blog/v1.0)
68
67
  - ⏱️ Fast [Time Travel](https://loro.dev/docs/tutorial/time_travel) Through History
69
68
  - πŸ›οΈ [Version Control with Real-Time Collaboration](https://loro.dev/blog/v1.0#version-control)
70
69
  - πŸ“¦ [Shallow Snapshot](https://loro.dev/docs/advanced/shallow_snapshot) that Works like Git Shallow Clone
@@ -82,9 +81,8 @@ import { expect, test } from 'vitest';
82
81
  import { LoroDoc, LoroList } from 'loro-crdt';
83
82
 
84
83
  test('sync example', () => {
85
- /**
86
- * Demonstrates synchronization of two documents with two rounds of exchanges.
87
- */
84
+ // Sync two docs with two rounds of exchanges
85
+
88
86
  // Initialize document A
89
87
  const docA = new LoroDoc();
90
88
  const listA: LoroList = docA.getList('list');
@@ -92,34 +90,36 @@ test('sync example', () => {
92
90
  listA.insert(1, 'B');
93
91
  listA.insert(2, 'C');
94
92
 
95
- // Export the state of document A as a byte array
93
+ // Export all updates from docA
96
94
  const bytes: Uint8Array = docA.export({ mode: 'update' });
97
95
 
98
96
  // Simulate sending `bytes` across the network to another peer, B
97
+
99
98
  const docB = new LoroDoc();
100
99
  // Peer B imports the updates from A
101
100
  docB.import(bytes);
102
101
 
103
- // Verify that B's state matches A's state
102
+ // B's state matches A's state
104
103
  expect(docB.toJSON()).toStrictEqual({
105
104
  list: ['A', 'B', 'C'],
106
105
  });
107
106
 
108
- // Get the current operation log version of document B
107
+ // Get the current version of docB
109
108
  const version = docB.oplogVersion();
110
109
 
111
110
  // Simulate editing at B: delete item 'B'
112
111
  const listB: LoroList = docB.getList('list');
113
112
  listB.delete(1, 1);
114
113
 
115
- // Export the updates from B since the last synchronization point
114
+ // Export the updates from B since the last sync point
116
115
  const bytesB: Uint8Array = docB.export({ mode: 'update', from: version });
117
116
 
118
117
  // Simulate sending `bytesB` back across the network to A
118
+
119
119
  // A imports the updates from B
120
120
  docA.import(bytesB);
121
121
 
122
- // Verify that the list at A now matches the list at B after merging
122
+ // A has the same state as B
123
123
  expect(docA.toJSON()).toStrictEqual({
124
124
  list: ['A', 'C'],
125
125
  });
@@ -26,12 +26,13 @@ export function setDebug(): void;
26
26
  * - endVersionVector
27
27
  * - startTimestamp
28
28
  * - endTimestamp
29
- * - isSnapshot
29
+ * - mode
30
30
  * - changeNum
31
31
  * @param {Uint8Array} blob
32
+ * @param {boolean} check_checksum
32
33
  * @returns {ImportBlobMetadata}
33
34
  */
34
- export function decodeImportBlobMeta(blob: Uint8Array): ImportBlobMetadata;
35
+ export function decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata;
35
36
 
36
37
  /**
37
38
  * Container types supported by loro.
@@ -262,7 +263,7 @@ export interface ImportBlobMetadata {
262
263
  startFrontiers: OpId[],
263
264
  startTimestamp: number;
264
265
  endTimestamp: number;
265
- isSnapshot: boolean;
266
+ mode: "outdated-snapshot" | "outdated-update" | "snapshot" | "shallow-snapshot" | "update";
266
267
  changeNum: number;
267
268
  }
268
269
 
@@ -351,6 +352,11 @@ export type TreeNodeValue = {
351
352
  children: TreeNodeValue[],
352
353
  }
353
354
 
355
+ export type TreeNodeJSON<T> = Omit<TreeNodeValue, 'meta' | 'children'> & {
356
+ meta: T,
357
+ children: TreeNodeJSON<T>[],
358
+ }
359
+
354
360
  interface LoroTree{
355
361
  toArray(): TreeNodeValue[];
356
362
  getNodes(options?: { withDeleted: boolean = false }): LoroTreeNode[];
@@ -432,6 +438,11 @@ export type JsonChange = {
432
438
  ops: JsonOp[]
433
439
  }
434
440
 
441
+ export interface TextUpdateOptions {
442
+ timeoutMs?: number,
443
+ useRefinedDiff?: boolean,
444
+ }
445
+
435
446
  export type ExportMode = {
436
447
  mode: "update",
437
448
  from?: VersionVector,
@@ -1012,6 +1023,32 @@ interface LoroText {
1012
1023
  insert(pos: number, text: string): void;
1013
1024
  delete(pos: number, len: number): void;
1014
1025
  subscribe(listener: Listener): Subscription;
1026
+ /**
1027
+ * Update the current text to the target text.
1028
+ *
1029
+ * It will calculate the minimal difference and apply it to the current text.
1030
+ * It uses Myers' diff algorithm to compute the optimal difference.
1031
+ *
1032
+ * This could take a long time for large texts (e.g. > 50_000 characters).
1033
+ * In that case, you should use `updateByLine` instead.
1034
+ *
1035
+ * @example
1036
+ * ```ts
1037
+ * import { LoroDoc } from "loro-crdt";
1038
+ *
1039
+ * const doc = new LoroDoc();
1040
+ * const text = doc.getText("text");
1041
+ * text.insert(0, "Hello");
1042
+ * text.update("Hello World");
1043
+ * console.log(text.toString()); // "Hello World"
1044
+ * ```
1045
+ */
1046
+ update(text: string, options?: TextUpdateOptions): void;
1047
+ /**
1048
+ * Update the current text based on the provided text.
1049
+ * This update calculation is line-based, which will be more efficient but less precise.
1050
+ */
1051
+ updateByLine(text: string, options?: TextUpdateOptions): void;
1015
1052
  }
1016
1053
  interface LoroTree<T extends Record<string, unknown> = Record<string, unknown>> {
1017
1054
  new(): LoroTree<T>;
@@ -1080,6 +1117,7 @@ interface LoroTreeNode<T extends Record<string, unknown> = Record<string, unknow
1080
1117
  * the WASM boundary.
1081
1118
  */
1082
1119
  children(): Array<LoroTreeNode<T>> | undefined;
1120
+ toJSON(): TreeNodeJSON<T>;
1083
1121
  }
1084
1122
  interface AwarenessWasm<T extends Value = Value> {
1085
1123
  getState(peer: PeerID): T | undefined;
@@ -1532,9 +1570,9 @@ export class LoroDoc {
1532
1570
  * @param ids - the changes to visit
1533
1571
  * @param f - the callback function, return `true` to continue visiting, return `false` to stop
1534
1572
  * @param {({ peer: PeerID, counter: number })[]} ids
1535
- * @param {Function} f
1573
+ * @param {(change: ChangeMeta) => boolean} f
1536
1574
  */
1537
- travelChangeAncestors(ids: ({ peer: PeerID, counter: number })[], f: Function): void;
1575
+ travelChangeAncestors(ids: ({ peer: PeerID, counter: number })[], f: (change: ChangeMeta) => boolean): void;
1538
1576
  /**
1539
1577
  * Checkout the `DocState` to a specific version.
1540
1578
  *
@@ -1638,9 +1676,9 @@ export class LoroDoc {
1638
1676
  /**
1639
1677
  * Get the path from the root to the container
1640
1678
  * @param {ContainerID} id
1641
- * @returns {Array<any> | undefined}
1679
+ * @returns {(string|number)[] | undefined}
1642
1680
  */
1643
- getPathToContainer(id: ContainerID): Array<any> | undefined;
1681
+ getPathToContainer(id: ContainerID): (string|number)[] | undefined;
1644
1682
  /**
1645
1683
  * Evaluate JSONPath against a LoroDoc
1646
1684
  * @param {string} jsonpath
@@ -2010,6 +2048,23 @@ export class LoroDoc {
2010
2048
  */
2011
2049
  getCursorPos(cursor: Cursor): { update?: Cursor, offset: number, side: Side };
2012
2050
  /**
2051
+ * Gets container IDs modified in the given ID range.
2052
+ *
2053
+ * **NOTE:** This method will implicitly commit.
2054
+ *
2055
+ * This method identifies which containers were affected by changes in a given range of operations.
2056
+ * It can be used together with `doc.travelChangeAncestors()` to analyze the history of changes
2057
+ * and determine which containers were modified by each change.
2058
+ *
2059
+ * @param id - The starting ID of the change range
2060
+ * @param len - The length of the change range to check
2061
+ * @returns An array of container IDs that were modified in the given range
2062
+ * @param {{ peer: PeerID, counter: number }} id
2063
+ * @param {number} len
2064
+ * @returns {(ContainerID)[]}
2065
+ */
2066
+ getChangedContainersIn(id: { peer: PeerID, counter: number }, len: number): (ContainerID)[];
2067
+ /**
2013
2068
  * Peer ID of the current writer.
2014
2069
  */
2015
2070
  readonly peerId: bigint;
@@ -2105,6 +2160,16 @@ export class LoroList {
2105
2160
  */
2106
2161
  clear(): void;
2107
2162
  /**
2163
+ * @param {number} pos
2164
+ * @returns {{ peer: PeerID, counter: number } | undefined}
2165
+ */
2166
+ getIdAt(pos: number): { peer: PeerID, counter: number } | undefined;
2167
+ /**
2168
+ * Check if the container is deleted
2169
+ * @returns {boolean}
2170
+ */
2171
+ isDeleted(): boolean;
2172
+ /**
2108
2173
  * Get the id of this container.
2109
2174
  */
2110
2175
  readonly id: ContainerID;
@@ -2255,6 +2320,17 @@ export class LoroMap {
2255
2320
  */
2256
2321
  clear(): void;
2257
2322
  /**
2323
+ * Get the peer id of the last editor on the given entry
2324
+ * @param {string} key
2325
+ * @returns {PeerID | undefined}
2326
+ */
2327
+ getLastEditor(key: string): PeerID | undefined;
2328
+ /**
2329
+ * Check if the container is deleted
2330
+ * @returns {boolean}
2331
+ */
2332
+ isDeleted(): boolean;
2333
+ /**
2258
2334
  * The container id of this handler.
2259
2335
  */
2260
2336
  readonly id: ContainerID;
@@ -2375,6 +2451,29 @@ export class LoroMovableList {
2375
2451
  */
2376
2452
  clear(): void;
2377
2453
  /**
2454
+ * Get the creator of the list item at the given position.
2455
+ * @param {number} pos
2456
+ * @returns {PeerID | undefined}
2457
+ */
2458
+ getCreatorAt(pos: number): PeerID | undefined;
2459
+ /**
2460
+ * Get the last mover of the list item at the given position.
2461
+ * @param {number} pos
2462
+ * @returns {PeerID | undefined}
2463
+ */
2464
+ getLastMoverAt(pos: number): PeerID | undefined;
2465
+ /**
2466
+ * Get the last editor of the list item at the given position.
2467
+ * @param {number} pos
2468
+ * @returns {PeerID | undefined}
2469
+ */
2470
+ getLastEditorAt(pos: number): PeerID | undefined;
2471
+ /**
2472
+ * Check if the container is deleted
2473
+ * @returns {boolean}
2474
+ */
2475
+ isDeleted(): boolean;
2476
+ /**
2378
2477
  * Get the id of this container.
2379
2478
  */
2380
2479
  readonly id: ContainerID;
@@ -2415,11 +2514,14 @@ export class LoroText {
2415
2514
  */
2416
2515
  kind(): 'Text';
2417
2516
  /**
2418
- * Iterate each span(internal storage unit) of the text.
2517
+ * Iterate each text span(internal storage unit)
2419
2518
  *
2420
2519
  * The callback function will be called for each span in the text.
2421
2520
  * If the callback returns `false`, the iteration will stop.
2422
2521
  *
2522
+ * Limitation: you cannot access or alter the doc state when iterating (this is for performance consideration).
2523
+ * If you need to access or alter the doc state, please use `toString` instead.
2524
+ *
2423
2525
  * @example
2424
2526
  * ```ts
2425
2527
  * import { LoroDoc } from "loro-crdt";
@@ -2433,35 +2535,6 @@ export class LoroText {
2433
2535
  */
2434
2536
  iter(callback: Function): void;
2435
2537
  /**
2436
- * Update the current text to the target text.
2437
- *
2438
- * It will calculate the minimal difference and apply it to the current text.
2439
- * It uses Myers' diff algorithm to compute the optimal difference.
2440
- *
2441
- * This could take a long time for large texts (e.g. > 50_000 characters).
2442
- * In that case, you should use `updateByLine` instead.
2443
- *
2444
- * @example
2445
- * ```ts
2446
- * import { LoroDoc } from "loro-crdt";
2447
- *
2448
- * const doc = new LoroDoc();
2449
- * const text = doc.getText("text");
2450
- * text.insert(0, "Hello");
2451
- * text.update("Hello World");
2452
- * console.log(text.toString()); // "Hello World"
2453
- * ```
2454
- * @param {string} text
2455
- */
2456
- update(text: string): void;
2457
- /**
2458
- * Update the current text to the target text, the difference is calculated line by line.
2459
- *
2460
- * It uses Myers' diff algorithm to compute the optimal difference.
2461
- * @param {string} text
2462
- */
2463
- updateByLine(text: string): void;
2464
- /**
2465
2538
  * Insert the string at the given index (utf-16 index).
2466
2539
  *
2467
2540
  * @example
@@ -2705,6 +2778,17 @@ export class LoroText {
2705
2778
  */
2706
2779
  push(s: string): void;
2707
2780
  /**
2781
+ * Get the editor of the text at the given position.
2782
+ * @param {number} pos
2783
+ * @returns {PeerID | undefined}
2784
+ */
2785
+ getEditorOf(pos: number): PeerID | undefined;
2786
+ /**
2787
+ * Check if the container is deleted
2788
+ * @returns {boolean}
2789
+ */
2790
+ isDeleted(): boolean;
2791
+ /**
2708
2792
  * Get the container id of the text.
2709
2793
  */
2710
2794
  readonly id: ContainerID;
@@ -2869,6 +2953,11 @@ export class LoroTree {
2869
2953
  */
2870
2954
  isFractionalIndexEnabled(): boolean;
2871
2955
  /**
2956
+ * Check if the container is deleted
2957
+ * @returns {boolean}
2958
+ */
2959
+ isDeleted(): boolean;
2960
+ /**
2872
2961
  * Get the id of the container.
2873
2962
  */
2874
2963
  readonly id: ContainerID;
@@ -2974,6 +3063,21 @@ export class LoroTreeNode {
2974
3063
  */
2975
3064
  isDeleted(): boolean;
2976
3065
  /**
3066
+ * Get the last mover of this node.
3067
+ * @returns {{ peer: PeerID, counter: number } | undefined}
3068
+ */
3069
+ getLastMoveId(): { peer: PeerID, counter: number } | undefined;
3070
+ /**
3071
+ * Get the creation id of this node.
3072
+ * @returns {{ peer: PeerID, counter: number }}
3073
+ */
3074
+ creationId(): { peer: PeerID, counter: number };
3075
+ /**
3076
+ * Get the creator of this node.
3077
+ * @returns {PeerID}
3078
+ */
3079
+ creator(): PeerID;
3080
+ /**
2977
3081
  * The TreeID of the node.
2978
3082
  */
2979
3083
  readonly id: TreeID;
@@ -3118,4 +3222,22 @@ export class VersionVector {
3118
3222
  * @returns {number | undefined}
3119
3223
  */
3120
3224
  compare(other: VersionVector): number | undefined;
3225
+ /**
3226
+ * set the exclusive ending point. target id will NOT be included by self
3227
+ * @param {{ peer: PeerID, counter: number }} id
3228
+ */
3229
+ setEnd(id: { peer: PeerID, counter: number }): void;
3230
+ /**
3231
+ * set the inclusive ending point. target id will be included
3232
+ * @param {{ peer: PeerID, counter: number }} id
3233
+ */
3234
+ setLast(id: { peer: PeerID, counter: number }): void;
3235
+ /**
3236
+ * @param {PeerID} peer
3237
+ */
3238
+ remove(peer: PeerID): void;
3239
+ /**
3240
+ * @returns {number}
3241
+ */
3242
+ length(): number;
3121
3243
  }