|
1 |
| -import { Loro, LoroList, LoroMovableList, LoroText } from "npm:[email protected]"; |
| 1 | +import { Cursor, Loro } from "npm:[email protected]"; |
2 | 2 | import { expect } from "npm:[email protected]";
|
3 | 3 |
|
4 | 4 | Deno.test("List", () => {
|
5 |
| - let list = new LoroList(); |
6 |
| - list.push(0); |
7 |
| - list.push("1"); |
8 |
| - const doc = new Loro(); |
9 |
| - const map = doc.getMap("root"); |
10 |
| - list = map.setContainer("list", list); |
11 |
| - expect(doc.toJson()).toStrictEqual({ root: { list: [0, "1"] } }); |
12 |
| - list.delete(0, 1); |
13 |
| - expect(doc.toJson()).toStrictEqual({ root: { list: ["1"] } }); |
| 5 | + const docA = new Loro(); |
| 6 | + docA.setPeerId("1"); |
| 7 | + const listA = docA.getList("list"); |
| 8 | + listA.push(0); |
| 9 | + listA.push(1); |
| 10 | + listA.push(2); |
| 11 | + const bytes: Uint8Array = docA.exportSnapshot(); |
| 12 | + |
| 13 | + const docB = Loro.fromSnapshot(bytes); |
| 14 | + docB.setPeerId("2"); |
| 15 | + const listB = docB.getList("list"); |
| 16 | + { |
| 17 | + // Concurrently docA and docB update element at index 2 |
| 18 | + // docA updates it to 8 |
| 19 | + // docB updates it to 9 |
| 20 | + // docA.toJson() should return { list: [0, 1, 8] } |
| 21 | + // docB.toJson() should return { list: [0, 1, 9] } |
| 22 | + |
| 23 | + listB.delete(2, 1); |
| 24 | + listB.insert(2, 9); |
| 25 | + expect(docB.toJson()).toStrictEqual({ list: [0, 1, 9] }); |
| 26 | + listA.delete(2, 1); |
| 27 | + listA.insert(2, 8); |
| 28 | + expect(docA.toJson()).toStrictEqual({ list: [0, 1, 8] }); |
| 29 | + } |
| 30 | + |
| 31 | + { |
| 32 | + // Merge docA and docB |
| 33 | + docA.import(docB.exportFrom(docA.version())); |
| 34 | + docB.import(docA.exportFrom(docB.version())); |
| 35 | + } |
| 36 | + |
| 37 | + expect(docA.toJson()).toStrictEqual({ list: [0, 1, 8, 9] }); |
| 38 | + expect(docB.toJson()).toStrictEqual({ list: [0, 1, 8, 9] }); |
14 | 39 | })
|
15 | 40 |
|
16 | 41 | Deno.test("MovableList", () => {
|
17 |
| - let list = new LoroMovableList(); |
18 |
| - list.push(0); |
19 |
| - list.push("1"); |
| 42 | + const docA = new Loro(); |
| 43 | + docA.setPeerId("1"); |
| 44 | + const listA = docA.getMovableList("list"); |
| 45 | + listA.push(0); |
| 46 | + listA.push(1); |
| 47 | + listA.push(2); |
| 48 | + const bytes: Uint8Array = docA.exportSnapshot(); |
| 49 | + |
| 50 | + const docB = Loro.fromSnapshot(bytes); |
| 51 | + docB.setPeerId("2"); |
| 52 | + const listB = docB.getMovableList("list"); |
| 53 | + { |
| 54 | + // Concurrently docA and docB update element at index 2 |
| 55 | + // docA updates it to 8 |
| 56 | + // docB updates it to 9 |
| 57 | + // docA.toJson() should return { list: [0, 1, 8] } |
| 58 | + // docB.toJson() should return { list: [0, 1, 9] } |
| 59 | + |
| 60 | + listA.set(2, 8); |
| 61 | + expect(docA.toJson()).toStrictEqual({ list: [0, 1, 8] }); |
| 62 | + listB.set(2, 9); |
| 63 | + expect(docB.toJson()).toStrictEqual({ list: [0, 1, 9] }); |
| 64 | + } |
| 65 | + |
| 66 | + { |
| 67 | + // Merge docA and docB |
| 68 | + docA.import(docB.exportFrom(docA.version())); |
| 69 | + docB.import(docA.exportFrom(docB.version())); |
| 70 | + } |
| 71 | + |
| 72 | + // Converge to [0, 1, 9] because docB has larger peerId thus larger logical time |
| 73 | + expect(docA.toJson()).toStrictEqual({ list: [0, 1, 9] }); |
| 74 | + expect(docB.toJson()).toStrictEqual({ list: [0, 1, 9] }); |
| 75 | + |
| 76 | + { |
| 77 | + // Concurrently docA and docB move element at index 0 |
| 78 | + // docA moves it to 2 |
| 79 | + // docB moves it to 1 |
| 80 | + // docA.toJson() should return { list: [1, 9, 0] } |
| 81 | + // docB.toJson() should return { list: [1, 0, 9] } |
| 82 | + |
| 83 | + listA.move(0, 2); |
| 84 | + listB.move(0, 1); |
| 85 | + expect(docA.toJson()).toStrictEqual({ list: [1, 9, 0] }); |
| 86 | + expect(docB.toJson()).toStrictEqual({ list: [1, 0, 9] }); |
| 87 | + } |
| 88 | + |
| 89 | + { |
| 90 | + // Merge docA and docB |
| 91 | + docA.import(docB.exportFrom(docA.version())); |
| 92 | + docB.import(docA.exportFrom(docB.version())); |
| 93 | + } |
| 94 | + |
| 95 | + // Converge to [1, 0, 9] because docB has larger peerId thus larger logical time |
| 96 | + expect(docA.toJson()).toStrictEqual({ list: [1, 0, 9] }); |
| 97 | + expect(docB.toJson()).toStrictEqual({ list: [1, 0, 9] }); |
| 98 | +}) |
| 99 | + |
| 100 | + |
| 101 | +Deno.test("List Cursors", () => { |
20 | 102 | const doc = new Loro();
|
21 |
| - const map = doc.getMap("root"); |
22 |
| - list = map.setContainer("list", list); |
23 |
| - expect(doc.toJson()).toStrictEqual({ root: { list: [0, "1"] } }); |
24 |
| - list.move(0, 1); |
25 |
| - expect(doc.toJson()).toStrictEqual({ root: { list: ["1", 0] } }); |
26 |
| - // Uint8Array is a special type in Loro |
27 |
| - list.set(1, new Uint8Array([1, 2, 3])); |
28 |
| - expect(doc.toJson()).toStrictEqual({ root: { list: ["1", new Uint8Array([1, 2, 3])] } }); |
29 |
| - const text = list.setContainer(0, new LoroText()); |
30 |
| - text.insert(0, "Hello") |
31 |
| - expect(doc.toJson()).toStrictEqual({ root: { list: ["Hello", new Uint8Array([1, 2, 3])] } }); |
| 103 | + doc.setPeerId("1"); |
| 104 | + const list = doc.getList("list"); |
| 105 | + list.push("Hello"); |
| 106 | + list.push("World"); |
| 107 | + const cursor = list.getCursor(1)!; |
| 108 | + expect(cursor.pos()).toStrictEqual({ peer: "1", counter: 1 }); |
| 109 | + |
| 110 | + const encodedCursor: Uint8Array = cursor.encode(); |
| 111 | + const exported: Uint8Array = doc.exportSnapshot(); |
| 112 | + |
| 113 | + // Sending the exported snapshot and the encoded cursor to peer 2 |
| 114 | + // Peer 2 will decode the cursor and get the position of the cursor in the list |
| 115 | + // Peer 2 will then insert "Hello" at the beginning of the list |
| 116 | + |
| 117 | + const docB = new Loro(); |
| 118 | + docB.setPeerId("2"); |
| 119 | + const listB = docB.getList("list"); |
| 120 | + docB.import(exported); |
| 121 | + listB.insert(0, "Foo"); |
| 122 | + expect(docB.toJson()).toStrictEqual({ list: ["Foo", "Hello", "World"] }); |
| 123 | + const cursorB = Cursor.decode(encodedCursor); |
| 124 | + { |
| 125 | + // The cursor position is shifted to the right by 1 |
| 126 | + const pos = docB.getCursorPos(cursorB); |
| 127 | + expect(pos.offset).toBe(2); |
| 128 | + } |
| 129 | + listB.insert(1, "Bar"); |
| 130 | + expect(docB.toJson()).toStrictEqual({ list: ["Foo", "Bar", "Hello", "World"] }); |
| 131 | + { |
| 132 | + // The cursor position is shifted to the right by 1 |
| 133 | + const pos = docB.getCursorPos(cursorB); |
| 134 | + expect(pos.offset).toBe(3); |
| 135 | + } |
| 136 | + listB.delete(3, 1); |
| 137 | + expect(docB.toJson()).toStrictEqual({ list: ["Foo", "Bar", "Hello"] }); |
| 138 | + { |
| 139 | + // The position cursor points to is now deleted, |
| 140 | + // but it should still get the position |
| 141 | + const pos = docB.getCursorPos(cursorB); |
| 142 | + expect(pos.offset).toBe(3); |
| 143 | + // It will also offer a update on the cursor position. |
| 144 | + // |
| 145 | + // It's because the old cursor position is deleted, `doc.getCursorPos()` will slow down over time. |
| 146 | + // Internally, it needs to traverse the related history to find the correct position for a deleted |
| 147 | + // cursor position. |
| 148 | + // |
| 149 | + // After refreshing the cursor, the performance of `doc.getCursorPos()` will improve. |
| 150 | + expect(pos.update).toBeDefined(); |
| 151 | + const newCursor: Cursor = pos.update!; |
| 152 | + // The new cursor position is undefined because the cursor is at the end of the list |
| 153 | + expect(newCursor.pos()).toBeUndefined(); |
| 154 | + // The side is 1 because the cursor is at the right end of the list |
| 155 | + expect(newCursor.side()).toBe(1); |
| 156 | + |
| 157 | + const newPos = docB.getCursorPos(newCursor); |
| 158 | + // The offset doesn't changed |
| 159 | + expect(newPos.offset).toBe(3); |
| 160 | + // The update is undefined because the cursor no longer needs to be updated |
| 161 | + expect(newPos.update).toBeUndefined(); |
| 162 | + } |
32 | 163 | })
|
33 | 164 |
|
0 commit comments