├── .github └── workflows │ └── deno_test.yml ├── .vscode └── settings.json ├── 01_basic.test.ts ├── 02_text.test.ts ├── 03_version.test.ts ├── 04_time_travel.test.ts ├── 05_save_and_load.test.ts ├── 06_event.test.ts ├── 07_list.test.ts ├── 08_map.test.ts ├── 09_composition.test.ts ├── 10_op_and_change.test.ts ├── 11_tree_move.test.ts ├── 12_shallow_snapshot.test.ts ├── README.md ├── benches └── text.bench.ts ├── deno.json └── deno.lock /.github/workflows/deno_test.yml: -------------------------------------------------------------------------------- 1 | name: Deno tests 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: v1.x 21 | - name: Run deno tests 22 | run: deno task test 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } 6 | -------------------------------------------------------------------------------- /01_basic.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc, LoroList, LoroMap, LoroText } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Basic usage", () => { 5 | /** 6 | * LoroDoc is the entry point for using Loro. 7 | * You must create a Doc to use Map, List, Text, and other CRDT types. 8 | */ 9 | const doc = new LoroDoc(); 10 | const list: LoroList = doc.getList("list"); 11 | list.insert(0, "A"); 12 | list.insert(1, "B"); 13 | list.insert(2, "C"); // ["A", "B", "C"] 14 | 15 | const map: LoroMap = doc.getMap("map"); 16 | // map can only has string key 17 | map.set("key", "value"); 18 | expect(doc.toJSON()).toStrictEqual({ 19 | list: ["A", "B", "C"], 20 | map: { key: "value" }, 21 | }); 22 | 23 | // delete 2 element at index 0 24 | list.delete(0, 2); 25 | expect(doc.toJSON()).toStrictEqual({ 26 | list: ["C"], 27 | map: { key: "value" }, 28 | }); 29 | }); 30 | 31 | Deno.test("Sub containers", () => { 32 | /** 33 | * You can create sub CRDT containers in List and Map. 34 | */ 35 | const doc = new LoroDoc(); 36 | const list: LoroList = doc.getList("list"); 37 | const map: LoroMap = doc.getMap("list"); 38 | // insert a List container at index 0, and get the handler to that list 39 | const subList = list.insertContainer(0, new LoroList()); 40 | subList.insert(0, "A"); 41 | expect(list.toJSON()).toStrictEqual([["A"]]); 42 | // create a Text container inside the Map container 43 | const subtext = map.setContainer("text", new LoroText()); 44 | subtext.insert(0, "Hi"); 45 | expect(map.toJSON()).toStrictEqual({ text: "Hi" }); 46 | }); 47 | 48 | Deno.test("Sync", () => { 49 | /** 50 | * Two documents can complete synchronization with two rounds of exchanges. 51 | */ 52 | const docA = new LoroDoc(); 53 | const docB = new LoroDoc(); 54 | const listA: LoroList = docA.getList("list"); 55 | listA.insert(0, "A"); 56 | listA.insert(1, "B"); 57 | listA.insert(2, "C"); 58 | // B import the ops from A 59 | docB.import(docA.export({ mode: "update" })); 60 | expect(docB.toJSON()).toStrictEqual({ 61 | list: ["A", "B", "C"], 62 | }); 63 | 64 | const listB: LoroList = docB.getList("list"); 65 | // delete 1 element at index 1 66 | listB.delete(1, 1); 67 | // A import the missing ops from B 68 | docA.import(docB.export({ mode: "update", from: docA.version() })); 69 | // list at A is now ["A", "C"], with the same state as B 70 | expect(docA.toJSON()).toStrictEqual({ 71 | list: ["A", "C"], 72 | }); 73 | expect(docA.toJSON()).toStrictEqual(docB.toJSON()); 74 | }); 75 | -------------------------------------------------------------------------------- /02_text.test.ts: -------------------------------------------------------------------------------- 1 | import { Delta, LoroDoc } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Long text", () => { 5 | /** 6 | * Loro supports text manipulation. 7 | */ 8 | const doc = new LoroDoc(); 9 | const text = doc.getText("text"); 10 | for (let i = 0; i < 1_000_000; i += 1) { 11 | text.insert(i, i.toString()); 12 | } 13 | doc.export({ mode: "update" }); 14 | doc.export({ mode: "snapshot" }); 15 | }); 16 | 17 | Deno.test("Text", () => { 18 | /** 19 | * Loro supports text manipulation. 20 | */ 21 | const doc = new LoroDoc(); 22 | const text = doc.getText("text"); 23 | text.insert(0, "Hello world!"); 24 | text.delete(0, 6); 25 | expect(text.toString()).toBe("world!"); 26 | }); 27 | 28 | Deno.test("Rich text", () => { 29 | /** 30 | * Loro supports rich text CRDTs 31 | */ 32 | const doc = new LoroDoc(); 33 | const text = doc.getText("text"); 34 | text.insert(0, "Hello world!"); 35 | text.mark({ start: 0, end: 5 }, "bold", true); 36 | expect(text.toDelta()).toStrictEqual([ 37 | { 38 | insert: "Hello", 39 | attributes: { bold: true }, 40 | }, 41 | { 42 | insert: " world!", 43 | }, 44 | ] as Delta[]); 45 | }); 46 | 47 | Deno.test("Rich text custom expand behavior - Bold", () => { 48 | /** 49 | * For different styles on rich text, you may want different expand behavior 50 | * when users inserting new text at the boundary of the style. 51 | * 52 | * - Bold: expand the style to cover the new text inserted after the boundary. 53 | * - Link: will not expand the style when inserting new text at the boundary. 54 | */ 55 | const doc = new LoroDoc(); 56 | doc.configTextStyle({ bold: { expand: "after" } }); 57 | const text = doc.getText("text"); 58 | text.insert(0, "Hello world!"); 59 | text.mark({ start: 0, end: 5 }, "bold", true); 60 | text.insert(5, "!"); 61 | expect(text.toDelta()).toStrictEqual([ 62 | { 63 | insert: "Hello!", 64 | attributes: { bold: true }, 65 | }, 66 | { 67 | insert: " world!", 68 | }, 69 | ] as Delta[]); 70 | }); 71 | 72 | Deno.test("Rich text custom expand behavior - Link", () => { 73 | /** 74 | * For different styles on rich text, you may want different expand behavior 75 | * when users inserting new text at the boundary of the style. 76 | * 77 | * - Bold: expand the style to cover the new text inserted after the boundary. 78 | * - Link: will not expand the style when inserting new text at the boundary. 79 | */ 80 | const doc = new LoroDoc(); 81 | doc.configTextStyle({ 82 | link: { expand: "none" }, 83 | }); 84 | const text = doc.getText("text"); 85 | text.insert(0, "Hello world!"); 86 | text.mark({ start: 0, end: 5 }, "link", true); 87 | text.insert(5, "!"); 88 | expect(text.toDelta()).toStrictEqual([ 89 | { 90 | insert: "Hello", 91 | attributes: { link: true }, 92 | }, 93 | { 94 | insert: "! world!", 95 | }, 96 | ] as Delta[]); 97 | }); 98 | 99 | Deno.test("Rich text event", async () => { 100 | /** 101 | * Loro text will receive rich text event in Quill Delta format 102 | */ 103 | const doc = new LoroDoc(); 104 | const text = doc.getText("text"); 105 | text.insert(0, "Hello world!"); 106 | doc.commit(); 107 | let ran = false; 108 | text.subscribe((events) => { 109 | for (const event of events.events) { 110 | if (event.diff.type === "text") { 111 | expect(event.diff.diff).toStrictEqual([ 112 | { 113 | retain: 5, 114 | attributes: { bold: true }, 115 | }, 116 | ]); 117 | ran = true; 118 | } 119 | } 120 | }); 121 | text.mark({ start: 0, end: 5 }, "bold", true); 122 | doc.commit(); 123 | await new Promise((resolve) => setTimeout(resolve, 0)); 124 | expect(ran).toBeTruthy(); 125 | }); 126 | -------------------------------------------------------------------------------- /03_version.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc, OpId } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Frontiers & Version Vector Conversion", () => { 5 | const doc0 = new LoroDoc(); 6 | doc0.setPeerId(0n); 7 | doc0.getText("text").insert(0, "1"); 8 | doc0.commit(); 9 | const doc1 = new LoroDoc(); 10 | doc1.setPeerId(1n); 11 | doc1.getText("text").insert(0, "1"); 12 | doc1.commit(); 13 | doc1.import(doc0.export({ mode: "update" })); 14 | doc1.getText("text").insert(0, "1"); 15 | doc1.commit(); 16 | 17 | const frontiers = doc1.frontiers(); 18 | expect(frontiers).toStrictEqual([{ peer: "1", counter: 1 } as OpId]); 19 | const vv = doc1.frontiersToVV(frontiers); 20 | expect(vv.toJSON()).toStrictEqual( 21 | new Map([ 22 | ["0", 1], 23 | ["1", 2], 24 | ]), 25 | ); 26 | expect(doc1.vvToFrontiers(vv)).toStrictEqual(frontiers); 27 | }); 28 | 29 | Deno.test("Event", () => { 30 | const doc1 = new LoroDoc(); 31 | doc1.setPeerId(1); 32 | const doc2 = new LoroDoc(); 33 | doc2.setPeerId(2); 34 | 35 | // Some ops on doc1 and doc2 36 | doc1.getText("text").insert(0, "Alice"); 37 | doc2.getText("text").insert(0, "Hello, Loro!"); 38 | console.log(doc2.version().toJSON()); // Map(0) {} 39 | const updates = doc1.export({ mode: "update" }); 40 | doc2.import(updates); // This first commits any pending operations in doc2 41 | console.log(doc2.version().toJSON()); // Map(2) { "1" => 5, "2" => 12 } 42 | }); 43 | -------------------------------------------------------------------------------- /04_time_travel.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "npm:expect@29.7.0"; 2 | import { LoroDoc } from "npm:loro-crdt@1.0.8"; 3 | 4 | Deno.test("Time Travel", () => { 5 | /** 6 | * Time travel example of Loro 7 | */ 8 | const doc = new LoroDoc(); 9 | doc.setPeerId(0n); 10 | const text = doc.getText("text"); 11 | text.insert(0, "Hello"); 12 | doc.commit(); 13 | text.insert(5, " world"); 14 | expect(doc.toJSON()).toStrictEqual({ 15 | text: "Hello world", 16 | }); 17 | 18 | // Every unicode char insertion is a single operation for Text container 19 | doc.checkout([{ peer: "0", counter: 0 }]); 20 | expect(doc.toJSON()).toStrictEqual({ 21 | text: "H", 22 | }); 23 | 24 | doc.checkout([{ peer: "0", counter: 4 }]); 25 | expect(doc.toJSON()).toStrictEqual({ 26 | text: "Hello", 27 | }); 28 | 29 | // Returns to the latest version 30 | doc.attach(); 31 | expect(doc.toJSON()).toStrictEqual({ 32 | text: "Hello world", 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /05_save_and_load.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Save and load", () => { 5 | // To save the document, you can call `doc.exportSnapshot()` to get the binary data of the whole document. 6 | // When you want to load the document, you can call `doc.import(data)` to load the binary data. 7 | const doc = new LoroDoc(); 8 | doc.getText("text").insert(0, "Hello world!"); 9 | const data = doc.export({ mode: "snapshot" }); 10 | 11 | const newDoc = new LoroDoc(); 12 | newDoc.import(data); 13 | expect(newDoc.toJSON()).toStrictEqual({ 14 | text: "Hello world!", 15 | }); 16 | }); 17 | 18 | Deno.test("Save and load incrementally", () => { 19 | // It's costly to export the whole document on every keypress. 20 | // So you can call `doc.export({ mode: "update" })()` to get the binary data of the operations since last export. 21 | const doc = new LoroDoc(); 22 | doc.getText("text").insert(0, "Hello world!"); 23 | const data = doc.export({ mode: "snapshot" }); 24 | let lastSavedVersion = doc.version(); 25 | doc.getText("text").insert(0, "✨"); 26 | const update0 = doc.export({ mode: "update", from: lastSavedVersion }); 27 | lastSavedVersion = doc.version(); 28 | doc.getText("text").insert(0, "😶‍🌫️"); 29 | const update1 = doc.export({ mode: "update", from: lastSavedVersion }); 30 | 31 | { 32 | /** 33 | * You can import the snapshot and the updates to get the latest version of the document. 34 | */ 35 | 36 | // import the snapshot 37 | const newDoc = new LoroDoc(); 38 | newDoc.import(data); 39 | expect(newDoc.toJSON()).toStrictEqual({ 40 | text: "Hello world!", 41 | }); 42 | 43 | // import update0 44 | newDoc.import(update0); 45 | expect(newDoc.toJSON()).toStrictEqual({ 46 | text: "✨Hello world!", 47 | }); 48 | 49 | // import update1 50 | newDoc.import(update1); 51 | expect(newDoc.toJSON()).toStrictEqual({ 52 | text: "😶‍🌫️✨Hello world!", 53 | }); 54 | } 55 | 56 | { 57 | /** 58 | * You may also import them in a batch 59 | */ 60 | const newDoc = new LoroDoc(); 61 | newDoc.importUpdateBatch([update1, update0, data]); 62 | expect(newDoc.toJSON()).toStrictEqual({ 63 | text: "😶‍🌫️✨Hello world!", 64 | }); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /06_event.test.ts: -------------------------------------------------------------------------------- 1 | import { getType, LoroDoc, LoroMap, LoroText } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Event have delta that contains Container", async () => { 5 | const doc = new LoroDoc(); 6 | const list = doc.getList("list"); 7 | let ran = false; 8 | doc.subscribe((events) => { 9 | for (const event of events.events) { 10 | if (event.diff.type === "list") { 11 | for (const item of event.diff.diff) { 12 | expect(item.insert?.length).toBe(2); 13 | expect(getType(item.insert![0])).toBe("Text"); 14 | expect(getType(item.insert![1])).toBe("Map"); 15 | const t = item.insert![0] as LoroText; 16 | expect(t.toString()).toBe("Hello"); 17 | } 18 | ran = true; 19 | } 20 | } 21 | }); 22 | 23 | list.insertContainer(0, new LoroMap()); 24 | const t = list.insertContainer(0, new LoroText()); 25 | t.insert(0, "He"); 26 | t.insert(2, "llo"); 27 | doc.commit(); 28 | await new Promise((resolve) => setTimeout(resolve, 1)); 29 | expect(ran).toBeTruthy(); 30 | }); 31 | -------------------------------------------------------------------------------- /07_list.test.ts: -------------------------------------------------------------------------------- 1 | import { Cursor, LoroDoc } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("List", () => { 5 | const docA = new LoroDoc(); 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.export({ mode: "snapshot" }); 12 | 13 | const docB = LoroDoc.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.export({ mode: "update", from: docA.version() })); 34 | docB.import(docA.export({ mode: "update", from: docB.version() })); 35 | } 36 | 37 | expect(docA.toJSON()).toStrictEqual({ list: [0, 1, 8, 9] }); 38 | expect(docB.toJSON()).toStrictEqual({ list: [0, 1, 8, 9] }); 39 | }); 40 | 41 | Deno.test("MovableList", () => { 42 | const docA = new LoroDoc(); 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.export({ mode: "snapshot" }); 49 | 50 | const docB = LoroDoc.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.export({ mode: "update", from: docA.version() })); 69 | docB.import(docA.export({ mode: "update", from: 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.export({ mode: "update", from: docA.version() })); 92 | docB.import(docA.export({ mode: "update", from: 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 | Deno.test("List Cursors", () => { 101 | const doc = new LoroDoc(); 102 | doc.setPeerId("1"); 103 | const list = doc.getList("list"); 104 | list.push("Hello"); 105 | list.push("World"); 106 | const cursor = list.getCursor(1)!; 107 | expect(cursor.pos()).toStrictEqual({ peer: "1", counter: 1 }); 108 | 109 | const encodedCursor: Uint8Array = cursor.encode(); 110 | const exported: Uint8Array = doc.export({ mode: "snapshot" }); 111 | 112 | // Sending the exported snapshot and the encoded cursor to peer 2 113 | // Peer 2 will decode the cursor and get the position of the cursor in the list 114 | // Peer 2 will then insert "Hello" at the beginning of the list 115 | 116 | const docB = new LoroDoc(); 117 | docB.setPeerId("2"); 118 | const listB = docB.getList("list"); 119 | docB.import(exported); 120 | listB.insert(0, "Foo"); 121 | expect(docB.toJSON()).toStrictEqual({ list: ["Foo", "Hello", "World"] }); 122 | const cursorB = Cursor.decode(encodedCursor); 123 | { 124 | // The cursor position is shifted to the right by 1 125 | const pos = docB.getCursorPos(cursorB); 126 | expect(pos.offset).toBe(2); 127 | } 128 | listB.insert(1, "Bar"); 129 | expect(docB.toJSON()).toStrictEqual({ 130 | list: ["Foo", "Bar", "Hello", "World"], 131 | }); 132 | { 133 | // The cursor position is shifted to the right by 1 134 | const pos = docB.getCursorPos(cursorB); 135 | expect(pos.offset).toBe(3); 136 | } 137 | listB.delete(3, 1); 138 | expect(docB.toJSON()).toStrictEqual({ list: ["Foo", "Bar", "Hello"] }); 139 | { 140 | // The position cursor points to is now deleted, 141 | // but it should still get the position 142 | const pos = docB.getCursorPos(cursorB); 143 | expect(pos.offset).toBe(3); 144 | // It will also offer a update on the cursor position. 145 | // 146 | // It's because the old cursor position is deleted, `doc.getCursorPos()` will slow down over time. 147 | // Internally, it needs to traverse the related history to find the correct position for a deleted 148 | // cursor position. 149 | // 150 | // After refreshing the cursor, the performance of `doc.getCursorPos()` will improve. 151 | expect(pos.update).toBeDefined(); 152 | const newCursor: Cursor = pos.update!; 153 | // The new cursor position is undefined because the cursor is at the end of the list 154 | expect(newCursor.pos()).toBeUndefined(); 155 | // The side is 1 because the cursor is at the right end of the list 156 | expect(newCursor.side()).toBe(1); 157 | 158 | const newPos = docB.getCursorPos(newCursor); 159 | // The offset doesn't changed 160 | expect(newPos.offset).toBe(3); 161 | // The update is undefined because the cursor no longer needs to be updated 162 | expect(newPos.update).toBeUndefined(); 163 | } 164 | }); 165 | -------------------------------------------------------------------------------- /08_map.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc, LoroText } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("LoroMap", () => { 5 | const docA = new LoroDoc(); 6 | docA.setPeerId("0"); 7 | const docB = new LoroDoc(); 8 | docB.setPeerId("1"); 9 | 10 | const mapA = docA.getMap("map"); 11 | const mapB = docB.getMap("map"); 12 | 13 | mapA.set("a", 1); 14 | const textB = mapB.setContainer("a", new LoroText()); 15 | textB.insert(0, "Hi"); 16 | 17 | expect(docA.toJSON()).toStrictEqual({ map: { a: 1 } }); 18 | expect(docB.toJSON()).toStrictEqual({ map: { a: "Hi" } }); 19 | 20 | docA.import(docB.export({ mode: "update" })); 21 | docB.import(docA.export({ mode: "update" })); 22 | 23 | expect(docA.toJSON()).toStrictEqual({ map: { a: "Hi" } }); 24 | expect(docB.toJSON()).toStrictEqual({ map: { a: "Hi" } }); 25 | }); 26 | -------------------------------------------------------------------------------- /09_composition.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc, LoroList, LoroText } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Composition", async () => { 5 | const doc = new LoroDoc(); 6 | const map = doc.getMap("map"); 7 | let callTimes = 0; 8 | map.subscribe((_event) => { 9 | callTimes++; 10 | }); 11 | 12 | // Create a sub container for map 13 | // { map: { list: [] } } 14 | const list = map.setContainer("list", new LoroList()); 15 | list.push(0); 16 | list.push(1); 17 | 18 | // Create a sub container for list 19 | // { map: { list: [0, 1, LoroText] } } 20 | const text = list.insertContainer(2, new LoroText()); 21 | expect(doc.toJSON()).toStrictEqual({ map: { list: [0, 1, ""] } }); 22 | { 23 | // Commit will trigger the event, because list is a sub container of map 24 | doc.commit(); 25 | await new Promise((resolve) => setTimeout(resolve, 1)); 26 | expect(callTimes).toBe(1); 27 | } 28 | 29 | text.insert(0, "Hello, "); 30 | text.insert(7, "World!"); 31 | expect(doc.toJSON()).toStrictEqual({ 32 | map: { list: [0, 1, "Hello, World!"] }, 33 | }); 34 | { 35 | // Commit will trigger the event, because text is a descendant of map 36 | doc.commit(); 37 | await new Promise((resolve) => setTimeout(resolve, 1)); 38 | expect(callTimes).toBe(2); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /10_op_and_change.test.ts: -------------------------------------------------------------------------------- 1 | import { Change, LoroDoc } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("op and change", () => { 5 | const docA = new LoroDoc(); 6 | docA.setPeerId("0"); 7 | const textA = docA.getText("text"); 8 | // This create 3 operations 9 | textA.insert(0, "123"); 10 | // This create a new Change 11 | docA.commit(); 12 | // This create 2 operations 13 | textA.insert(0, "ab"); 14 | // This will NOT create a new Change 15 | docA.commit(); 16 | 17 | { 18 | const changeMap: Map<`${number}`, Change[]> = docA.getAllChanges(); 19 | expect(changeMap.size).toBe(1); 20 | expect(changeMap.get("0")).toStrictEqual([ 21 | { 22 | lamport: 0, 23 | length: 5, 24 | peer: "0", 25 | counter: 0, 26 | deps: [], 27 | timestamp: 0, 28 | message: undefined, 29 | }, 30 | ]); 31 | } 32 | 33 | // Create docB from doc 34 | const docB = LoroDoc.fromSnapshot(docA.export({ mode: "snapshot" })); 35 | docB.setPeerId("1"); 36 | const textB = docB.getText("text"); 37 | // This create 2 operations 38 | textB.insert(0, "cd"); 39 | 40 | // Import the Change from docB to doc 41 | const bytes = docB.export({ mode: "update" }); // Exporting has implicit commit 42 | docA.import(bytes); 43 | 44 | // This create 1 operations 45 | textA.insert(0, "1"); 46 | // Because doc import a Change from docB, it will create a new Change for 47 | // new commit to record this causal order 48 | docA.commit(); 49 | { 50 | const changeMap: Map<`${number}`, Change[]> = docA.getAllChanges(); 51 | expect(changeMap.size).toBe(2); 52 | expect(changeMap.get("0")).toStrictEqual([ 53 | { 54 | lamport: 0, 55 | length: 5, 56 | peer: "0", 57 | counter: 0, 58 | deps: [], 59 | timestamp: 0, 60 | message: undefined, 61 | }, 62 | { 63 | lamport: 7, 64 | length: 1, 65 | peer: "0", 66 | counter: 5, 67 | deps: [{ peer: "1", counter: 1 }], 68 | timestamp: 0, 69 | message: undefined, 70 | }, 71 | ]); 72 | expect(changeMap.get("1")).toStrictEqual([ 73 | { 74 | lamport: 5, 75 | length: 2, 76 | peer: "1", 77 | counter: 0, 78 | deps: [{ peer: "0", counter: 4 }], 79 | timestamp: 0, 80 | message: undefined, 81 | }, 82 | ]); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /11_tree_move.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Tree move", () => { 5 | const docA = new LoroDoc(); 6 | 7 | const treeA = docA.getTree("tree"); 8 | treeA.enableFractionalIndex(0); 9 | const node0 = treeA.createNode(); 10 | const node1 = treeA.createNode(node0.id, 0); 11 | const node2 = treeA.createNode(node0.id, 1); 12 | node2.moveBefore(node1); 13 | expect(node2.index()).toBe(0); 14 | expect(node1.index()).toBe(1); 15 | }); 16 | -------------------------------------------------------------------------------- /12_shallow_snapshot.test.ts: -------------------------------------------------------------------------------- 1 | import { LoroDoc } from "npm:loro-crdt@1.0.8"; 2 | import { expect } from "npm:expect@29.7.0"; 3 | 4 | Deno.test("Shallow snapshot", () => { 5 | const rootDoc = new LoroDoc(); 6 | rootDoc.setPeerId("0"); 7 | rootDoc.getText("text").insert(0, "Hello world!"); 8 | const snapshot = rootDoc.export({ 9 | mode: "shallow-snapshot", 10 | frontiers: [{ peer: "0", counter: 5 }], 11 | }); 12 | 13 | const shallowDoc = new LoroDoc(); 14 | shallowDoc.import(snapshot); 15 | expect(shallowDoc.getText("text").toString()).toBe("Hello world!"); 16 | expect(shallowDoc.isShallow()).toBe(true); 17 | }); 18 | 19 | Deno.test("Shallow snapshot - should throw if there is old update before the shallow root", () => { 20 | const rootDoc = new LoroDoc(); 21 | rootDoc.setPeerId("0"); 22 | rootDoc.getText("text").insert(0, "He"); 23 | const oldDoc = new LoroDoc(); 24 | oldDoc.import(rootDoc.export({ mode: "update" })); 25 | 26 | rootDoc.getText("text").insert(2, "llo world!"); 27 | const snapshot = rootDoc.export({ 28 | mode: "shallow-snapshot", 29 | frontiers: [{ peer: "0", counter: 5 }], 30 | }); 31 | 32 | // 33 | // Shallow Snapshot 34 | // ╔ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╗ 35 | // 36 | // ┌────┐ ┌─────┐ ║ ┌─────────┐ ║ 37 | // │ He │◀─┬──│ llo │◀────│ world! │ 38 | // └────┘ │ └─────┘ ║ └─────────┘ ║ 39 | // │ 40 | // │ ┌────┐ ║ ║ 41 | // └───│ e! │ 42 | // └────┘ ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝ 43 | // old 44 | // update 45 | // 46 | oldDoc.getText("text").insert(0, "e!"); 47 | const shallowDoc = new LoroDoc(); 48 | shallowDoc.import(snapshot); 49 | const update = oldDoc.export({ mode: "update", from: rootDoc.version() }); 50 | expect(() => shallowDoc.import(update)) 51 | .toThrow(); 52 | }); 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loro Examples 2 | 3 | This repo contains examples of how to use [Loro](https://loro.dev). 4 | 5 | This repo uses deno because it's easier and faster to setup. 6 | 7 | All the examples are tests in this folder. 8 | 9 | - [01_basic.test.ts](./01_basic.test.ts) 10 | - [02_text.test.ts](./02_text.test.ts) 11 | 12 | ## Running the examples 13 | 14 | ```bash 15 | deno task test 16 | ``` 17 | 18 | 19 | -------------------------------------------------------------------------------- /benches/text.bench.ts: -------------------------------------------------------------------------------- 1 | import { Loro } from "npm:loro-crdt@1.0.8"; 2 | 3 | /** 4 | cpu: Apple M1 5 | runtime: deno 1.38.0 (aarch64-apple-darwin) 6 | 7 | file:///Users/zxch3n/Code/loro-deno-examples/benches/text.bench.ts 8 | benchmark time (avg) iter/s (min … max) p75 p99 p995 9 | --------------------------------------------------------------------------- ----------------------------- 10 | Text 30.38 ms/iter 32.9 (27.97 ms … 42.13 ms) 30.37 ms 42.13 ms 42.13 ms 11 | Text in raw JS string 102.6 ms/iter 9.7 (96.78 ms … 119.13 ms) 104.68 ms 119.13 ms 119.13 ms 12 | */ 13 | 14 | Deno.bench("Text", () => { 15 | const doc = new Loro(); 16 | const text = doc.getText("text"); 17 | for (let i = 0; i < 30000; i++) { 18 | text.insert(i, i.toString()); 19 | } 20 | }); 21 | 22 | Deno.bench("Text in raw JS string", () => { 23 | let text = ""; 24 | for (let i = 0; i < 30000; i++) { 25 | text = text.slice(0, i) + i.toString() + text.slice(i); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "test": "deno test --allow-read", 4 | "bench": "deno bench --allow-read" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "npm:expect@29.7.0": "29.7.0", 5 | "npm:loro-crdt@1.0.0-beta.5": "1.0.0-beta.5", 6 | "npm:loro-crdt@1.0.8": "1.0.8" 7 | }, 8 | "npm": { 9 | "@babel/code-frame@7.25.9": { 10 | "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", 11 | "dependencies": [ 12 | "@babel/highlight", 13 | "picocolors" 14 | ] 15 | }, 16 | "@babel/helper-validator-identifier@7.25.9": { 17 | "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" 18 | }, 19 | "@babel/highlight@7.25.9": { 20 | "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", 21 | "dependencies": [ 22 | "@babel/helper-validator-identifier", 23 | "chalk@2.4.2", 24 | "js-tokens", 25 | "picocolors" 26 | ] 27 | }, 28 | "@jest/expect-utils@29.7.0": { 29 | "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", 30 | "dependencies": [ 31 | "jest-get-type" 32 | ] 33 | }, 34 | "@jest/schemas@29.6.3": { 35 | "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", 36 | "dependencies": [ 37 | "@sinclair/typebox" 38 | ] 39 | }, 40 | "@jest/types@29.6.3": { 41 | "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", 42 | "dependencies": [ 43 | "@jest/schemas", 44 | "@types/istanbul-lib-coverage", 45 | "@types/istanbul-reports", 46 | "@types/node", 47 | "@types/yargs", 48 | "chalk@4.1.2" 49 | ] 50 | }, 51 | "@sinclair/typebox@0.27.8": { 52 | "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" 53 | }, 54 | "@types/istanbul-lib-coverage@2.0.6": { 55 | "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" 56 | }, 57 | "@types/istanbul-lib-report@3.0.3": { 58 | "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", 59 | "dependencies": [ 60 | "@types/istanbul-lib-coverage" 61 | ] 62 | }, 63 | "@types/istanbul-reports@3.0.4": { 64 | "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", 65 | "dependencies": [ 66 | "@types/istanbul-lib-report" 67 | ] 68 | }, 69 | "@types/node@22.5.4": { 70 | "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", 71 | "dependencies": [ 72 | "undici-types" 73 | ] 74 | }, 75 | "@types/stack-utils@2.0.3": { 76 | "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" 77 | }, 78 | "@types/yargs-parser@21.0.3": { 79 | "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" 80 | }, 81 | "@types/yargs@17.0.33": { 82 | "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", 83 | "dependencies": [ 84 | "@types/yargs-parser" 85 | ] 86 | }, 87 | "ansi-styles@3.2.1": { 88 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 89 | "dependencies": [ 90 | "color-convert@1.9.3" 91 | ] 92 | }, 93 | "ansi-styles@4.3.0": { 94 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 95 | "dependencies": [ 96 | "color-convert@2.0.1" 97 | ] 98 | }, 99 | "ansi-styles@5.2.0": { 100 | "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" 101 | }, 102 | "braces@3.0.3": { 103 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 104 | "dependencies": [ 105 | "fill-range" 106 | ] 107 | }, 108 | "chalk@2.4.2": { 109 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 110 | "dependencies": [ 111 | "ansi-styles@3.2.1", 112 | "escape-string-regexp@1.0.5", 113 | "supports-color@5.5.0" 114 | ] 115 | }, 116 | "chalk@4.1.2": { 117 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 118 | "dependencies": [ 119 | "ansi-styles@4.3.0", 120 | "supports-color@7.2.0" 121 | ] 122 | }, 123 | "ci-info@3.9.0": { 124 | "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" 125 | }, 126 | "color-convert@1.9.3": { 127 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 128 | "dependencies": [ 129 | "color-name@1.1.3" 130 | ] 131 | }, 132 | "color-convert@2.0.1": { 133 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 134 | "dependencies": [ 135 | "color-name@1.1.4" 136 | ] 137 | }, 138 | "color-name@1.1.3": { 139 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 140 | }, 141 | "color-name@1.1.4": { 142 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 143 | }, 144 | "diff-sequences@29.6.3": { 145 | "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==" 146 | }, 147 | "escape-string-regexp@1.0.5": { 148 | "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" 149 | }, 150 | "escape-string-regexp@2.0.0": { 151 | "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" 152 | }, 153 | "expect@29.7.0": { 154 | "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", 155 | "dependencies": [ 156 | "@jest/expect-utils", 157 | "jest-get-type", 158 | "jest-matcher-utils", 159 | "jest-message-util", 160 | "jest-util" 161 | ] 162 | }, 163 | "fill-range@7.1.1": { 164 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 165 | "dependencies": [ 166 | "to-regex-range" 167 | ] 168 | }, 169 | "graceful-fs@4.2.11": { 170 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 171 | }, 172 | "has-flag@3.0.0": { 173 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" 174 | }, 175 | "has-flag@4.0.0": { 176 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 177 | }, 178 | "is-number@7.0.0": { 179 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" 180 | }, 181 | "jest-diff@29.7.0": { 182 | "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", 183 | "dependencies": [ 184 | "chalk@4.1.2", 185 | "diff-sequences", 186 | "jest-get-type", 187 | "pretty-format" 188 | ] 189 | }, 190 | "jest-get-type@29.6.3": { 191 | "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" 192 | }, 193 | "jest-matcher-utils@29.7.0": { 194 | "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", 195 | "dependencies": [ 196 | "chalk@4.1.2", 197 | "jest-diff", 198 | "jest-get-type", 199 | "pretty-format" 200 | ] 201 | }, 202 | "jest-message-util@29.7.0": { 203 | "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", 204 | "dependencies": [ 205 | "@babel/code-frame", 206 | "@jest/types", 207 | "@types/stack-utils", 208 | "chalk@4.1.2", 209 | "graceful-fs", 210 | "micromatch", 211 | "pretty-format", 212 | "slash", 213 | "stack-utils" 214 | ] 215 | }, 216 | "jest-util@29.7.0": { 217 | "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", 218 | "dependencies": [ 219 | "@jest/types", 220 | "@types/node", 221 | "chalk@4.1.2", 222 | "ci-info", 223 | "graceful-fs", 224 | "picomatch" 225 | ] 226 | }, 227 | "js-tokens@4.0.0": { 228 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 229 | }, 230 | "loro-crdt@1.0.0-beta.5": { 231 | "integrity": "sha512-vSs4YDabF4iuwNX15djXQNoV6+sORNzqC6vrurOGN1VFlJ15TlBKkR5Y0WF30UBx1RTuI3tUBtcEm+vqiJKsgA==", 232 | "dependencies": [ 233 | "loro-wasm" 234 | ] 235 | }, 236 | "loro-crdt@1.0.8": { 237 | "integrity": "sha512-8Qbt7klEs4CslDZ4/GsEhq6kVpHQcaz1kNUXdx/i4MP9y1M9/Qnzfamu5fHWmSe4qHOk3dzOhLYY1/Wt8UsaMA==" 238 | }, 239 | "loro-wasm@1.0.0-beta.5": { 240 | "integrity": "sha512-UR8dOyNKmRR6gABr7KfjKCGgEWDR6yR2AuN59Kbz0QkNIOztMYdQnSlSl4x7vKpkK+rz2QmhkNUD3KdzkANVHw==" 241 | }, 242 | "micromatch@4.0.8": { 243 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 244 | "dependencies": [ 245 | "braces", 246 | "picomatch" 247 | ] 248 | }, 249 | "picocolors@1.1.1": { 250 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 251 | }, 252 | "picomatch@2.3.1": { 253 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" 254 | }, 255 | "pretty-format@29.7.0": { 256 | "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", 257 | "dependencies": [ 258 | "@jest/schemas", 259 | "ansi-styles@5.2.0", 260 | "react-is" 261 | ] 262 | }, 263 | "react-is@18.3.1": { 264 | "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" 265 | }, 266 | "slash@3.0.0": { 267 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" 268 | }, 269 | "stack-utils@2.0.6": { 270 | "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", 271 | "dependencies": [ 272 | "escape-string-regexp@2.0.0" 273 | ] 274 | }, 275 | "supports-color@5.5.0": { 276 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 277 | "dependencies": [ 278 | "has-flag@3.0.0" 279 | ] 280 | }, 281 | "supports-color@7.2.0": { 282 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 283 | "dependencies": [ 284 | "has-flag@4.0.0" 285 | ] 286 | }, 287 | "to-regex-range@5.0.1": { 288 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 289 | "dependencies": [ 290 | "is-number" 291 | ] 292 | }, 293 | "undici-types@6.19.8": { 294 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" 295 | } 296 | } 297 | } 298 | --------------------------------------------------------------------------------