diff --git a/components/TwinEditors/EditorManager.ts b/components/TwinEditors/EditorManager.ts index d571770..1337e28 100644 --- a/components/TwinEditors/EditorManager.ts +++ b/components/TwinEditors/EditorManager.ts @@ -1,10 +1,9 @@ import { bytesToBase64DataUrl, dataUrlToBytes } from "@/lib/utils/data"; import { showVersion } from "@/lib/utils/loro"; -import { Change, Loro, LoroEvent, OpId, setDebug } from "loro-crdt"; +import { Change, LoroDoc, LoroEventBatch, OpId, Subscription } from "loro-crdt"; import Quill from "quill"; import { QuillBinding } from "./QuillBinding"; -setDebug("*"); type PeerProfile = { name: string; peerId: string }; type Frontiers = OpId[]; @@ -39,14 +38,14 @@ const QUILL_TOOLBAR_MAP: WeakMap = new WeakMap(); class EditorInstance { #index: number; #profile: PeerProfile; // Exportable (JSON) - #text: Loro; // Exportable (binary data) + #text: LoroDoc; // Exportable (binary data) #quill: Quill; #binding: QuillBinding; #connected: boolean = true; // Exportable (JSON) #manager: EditorManager; public async export(): Promise { - const rawData = this.#text.exportSnapshot(); + const rawData = this.#text.export({mode: "snapshot"}); return { profile: { name: this.#profile.name, @@ -86,7 +85,7 @@ class EditorInstance { this.#index = index; this.#profile = profile; this.#manager = manager; - this.#text = new Loro(); + this.#text = new LoroDoc(); this.#text.configTextStyle({ bold: { expand: "after" }, italic: { expand: "after" }, @@ -114,12 +113,12 @@ class EditorInstance { this.#text.subscribe((e) => { // Synchronize the change to the sum text. const sumVersion = this.#manager.sumText.version(); - const updateData = this.#text.exportFrom(sumVersion); + const updateData = this.#text.export({mode: "update", from: sumVersion}); this.#manager.sumText.import(updateData); // If this is a local change (i.e., not a change synchronized from // other peers), we synchronize to other connected peers if we're // connected. - if (e.local) { + if (e.by === "local") { this.synchronize(); } }); @@ -133,7 +132,7 @@ class EditorInstance { return this.#profile.name; } - public get text(): Loro { + public get text(): LoroDoc { return this.#text; } @@ -166,8 +165,8 @@ class EditorInstance { await Promise.resolve(); for (const that of this.#otherPeers()) { if (!that.connected) return; - this.#text.import(that.#text.exportFrom(this.#text.version())); - that.#text.import(this.#text.exportFrom(that.#text.version())); + this.#text.import(that.#text.export({mode: "update", from: this.#text.version()})); + that.#text.import(this.#text.export({mode: "update", from: that.#text.version()})); } } } @@ -220,10 +219,10 @@ export async function decodeExport( } export class EditorManager { - #sumText: Loro; + #sumText: LoroDoc; #numberOfOperations: number = 0; #peers: EditorInstance[]; - #subscriptions: Map = new Map(); + #subscriptions: Map = new Map(); static import( data: EditorManagerImportData, @@ -253,9 +252,9 @@ export class EditorManager { ) { throw new RangeError("insufficient HTML elements for creating editors"); } - this.#sumText = new Loro(); + this.#sumText = new LoroDoc(); this.#sumText.subscribe((e) => { - if (!e.fromCheckout) { + if (e.by === "local") { this.#numberOfOperations += 1; } }); @@ -277,7 +276,7 @@ export class EditorManager { }; } - public get sumText(): Loro { + public get sumText(): LoroDoc { return this.#sumText; } @@ -314,7 +313,7 @@ export class EditorManager { } public subscribeAll( - listener: (instance: EditorInstance, e: LoroEvent) => void + listener: (instance: EditorInstance, e: LoroEventBatch) => void ): number { this.#subscriptions.set( this.#subscriptions.size, @@ -329,7 +328,7 @@ export class EditorManager { if (!this.#subscriptions.has(subscription)) return; this.#subscriptions .get(subscription) - ?.forEach((n, index) => this.#peers[index].text.unsubscribe(n)); + ?.forEach((unsubscribe, index) => unsubscribe()); this.#subscriptions.set(subscription, []); } diff --git a/components/TwinEditors/QuillBinding.ts b/components/TwinEditors/QuillBinding.ts index 67bf79c..6782d27 100644 --- a/components/TwinEditors/QuillBinding.ts +++ b/components/TwinEditors/QuillBinding.ts @@ -1,9 +1,9 @@ -import { Delta, Loro, LoroText, setDebug } from "loro-crdt"; +import { Delta, LoroDoc, LoroText, PeerID } from "loro-crdt"; import Quill, { DeltaOperation, Sources } from "quill"; import isEqual from "is-equal"; import QuillDelta from "quill-delta"; -type Frontiers = { peer: string; counter: number }[]; +type Frontiers = { peer: PeerID; counter: number }[]; function showFrontiers(frontiers: Frontiers): string { return frontiers.map((x) => `${x.peer}@${x.counter}`).join(";"); @@ -11,52 +11,55 @@ function showFrontiers(frontiers: Frontiers): string { export class QuillBinding { private richtext: LoroText; - constructor(public doc: Loro, public quill: Quill) { + constructor(public doc: LoroDoc, public quill: Quill) { this.quill = quill; - this.richtext = doc.getText("text"); - this.richtext.subscribe(doc, (event) => { - Promise.resolve().then(() => { - if ((!event.local || event.fromCheckout) && event.diff.type == "text" && event.origin !== "ignore") { - const eventDelta = event.diff.diff; - const delta: Delta[] = []; - let index = 0; - for (let i = 0; i < eventDelta.length; i++) { - const d = eventDelta[i]; - const length = d.delete || d.retain || d.insert!.length; - // skip the last newline that quill automatically appends - if ( - d.insert && - d.insert === "\n" && - index === quill.getLength() - 1 && - i === eventDelta.length - 1 && - d.attributes != null && - Object.keys(d.attributes).length > 0 - ) { - delta.push({ - retain: 1, - attributes: d.attributes, - }); + const richtext = doc.getText("text"); + this.richtext = richtext; + richtext.subscribe(async (event) => { + if (event.by !== "local" && event.origin !== "ignore") { + for (const e of event.events) { + if (e.diff.type === "text") { + const eventDelta = e.diff.diff; + const delta: Delta[] = []; + let index = 0; + for (let i = 0; i < eventDelta.length; i++) { + const d = eventDelta[i]; + const length = d.delete || d.retain || d.insert!.length; + // skip the last newline that quill automatically appends + if ( + d.insert && + d.insert === "\n" && + index === quill.getLength() - 1 && + i === eventDelta.length - 1 && + d.attributes != null && + Object.keys(d.attributes).length > 0 + ) { + delta.push({ + retain: 1, + attributes: d.attributes, + }); + index += length; + continue; + } + + delta.push(d); index += length; - continue; } - delta.push(d); - index += length; - } - - quill.updateContents(new QuillDelta(delta), "this" as any); - const a = this.richtext.toDelta(); - const b = this.quill.getContents().ops; - if (!assertEqual(a, b as any, true)) { - console.log(this.doc.peerId, "COMPARE AFTER CRDT_EVENT", event.diff); - this.resetQuillContent(a) + quill.updateContents(new QuillDelta(delta), "this" as any); + const a = doc.getText("text").toDelta(); + const b = this.quill.getContents().ops; + if (!assertEqual(a, b as any, true)) { + console.log(this.doc.peerId, "COMPARE AFTER CRDT_EVENT", e.diff); + this.resetQuillContent(a) + } } } - }); + } }); quill.setContents( new QuillDelta( - this.richtext.toDelta().map((x) => ({ + richtext.toDelta().map((x) => ({ insert: x.insert, attributions: x.attributes, })) @@ -116,7 +119,7 @@ export class QuillBinding { if (origin !== ("this" as any)) { if (this.richtext.toString().slice(-1) !== '\n') { this.richtext.applyDelta([{ retain: this.richtext.length }, { insert: "\n" }]); - this.doc.commit("ignore"); + this.doc.commit({ origin: "ignore" }); } this.applyDelta(ops as DeltaOperation[]); const a = this.richtext.toDelta(); diff --git a/components/landing/Demonstration/index.tsx b/components/landing/Demonstration/index.tsx index d5b1420..4e8f584 100644 --- a/components/landing/Demonstration/index.tsx +++ b/components/landing/Demonstration/index.tsx @@ -116,7 +116,7 @@ export default function DemoSection() { const n = manager.sumText.subscribe((): void => { updateTimelineHistory(manager.sumText.getAllChanges()); }); - return manager.sumText.unsubscribe.bind(manager.sumText, n); + return n; }, [resetCounter, updateTimelineHistory]); useEffect(() => { diff --git a/components/landing/data/demo.json b/components/landing/data/demo.json index 98f11c8..5b679d9 100644 --- a/components/landing/data/demo.json +++ b/components/landing/data/demo.json @@ -1,21 +1,21 @@ { - "numberOfOperations": 58, - "peers": [ - { - "profile": { - "name": "Alice", - "peerId": "0" - }, - "encodedLoroText": "data:application/octet-stream;base64,bG9yb9bWgiV3xUZN3pQcD4Fn8FkAAgcFAjQAHQsACgAaEgAEAgQACTkAOBgVBAARDAYASwBMAEsAGAQABwIBAAIQAAMBAgQAAQEEAAECBgABARkBDgwAEQoACQ0ADgoACQQADwoJDQAYCQ0AHQsACgkYHCcEGgsHDxoHKy4EERMHDBQBBAwbAQQIBgkEAAsCAQIBAAIJDwoEAQYCDxIOEAXA4MnaDAAQBBYFEKaIHwwJBAABAgYAAwECBwEBAQECAQECEAADAgEAAwGIAQIBFAIAAAIAQgFCXgVIZWxsbwcgSmVubnkhBiBMYW5lIQkKCiMgVG9kYXkIJ3MgVGFza3MBCgEKhAkAAYQIAQEILSBUYXNrIDABCoQJAQGECAABAi0gBFRhc2sCIDGEAQAAAQqEAQAAAQpIEQIAAAAAAAAAAAAAAAAAAAABBgEEAQMABBIDBGJvbGQGaXRhbGljBHRleHQQAQIGCQABAAEABgkIAh4JDgoBAQcLYgcEHwYA", - "connected": true - }, - { - "profile": { - "name": "Bob", - "peerId": "1" - }, - "encodedLoroText": "data:application/octet-stream;base64,bG9yb9bWgiV3xUZN3pQcD4Fn8FkAAgcFAjQAHQsACgAaEgAEAgQACTkAOBgVBAARDAYASwBMAEsAGAQABwIBAAIQAAMBAgQAAQEEAAECBgABARkBDgwAEQoACQ0ADgoACQQADwoJDQAYCQ0AHQsACgkYHCcEGgsHDxoHKy4EERMHDBQBBAwbAQQIBgkEAAsCAQIBAAIJDwoEAQYCDxIOEAXA4MnaDAAQBBYFEKaIHwwJBAABAgYAAwECBwEBAQECAQECEAADAgEAAwGIAQIBFAIAAAIAQgFCXgVIZWxsbwcgSmVubnkhBiBMYW5lIQkKCiMgVG9kYXkIJ3MgVGFza3MBCgEKhAkAAYQIAQEILSBUYXNrIDABCoQJAQGECAABAi0gBFRhc2sCIDGEAQAAAQqEAQAAAQpIEQIAAAAAAAAAAAAAAAAAAAABBgEEAQMABBIDBGJvbGQGaXRhbGljBHRleHQQAQIGCQABAAEABgkIAh4JDgoBAQcLYgcEHwYA", - "connected": true - } - ] + "numberOfOperations": 58, + "peers": [ + { + "profile": { + "name": "Alice", + "peerId": "0" + }, + "encodedLoroText": "data:application/octet-stream;base64,bG9ybwAAAAAAAAAAAAAAAM00amcAA5MBAABMT1JPAAQiTRhgQIJYAQAAgwAxAEAEKgIAAQATAQgAQA0JCgEBAPOjAAYBBgEBCgKk0IABAAKmUUAGAQADAAgABgEEAQIABBEEYm9sZAZpdGFsaWMEdGV4dAA5AQQCIgATBAAdJBo9Nj4Ac3RzegB5enmAARIGBQMMAAQFEwwADAAFDAAMAAUMBQEMCQQBAQgUAQEHAEIBCgxIZWxsbyBKZW5ueSEJCgojIFRvZGF5hAYAAQgncyBUYXNrcwEKhAEBAIQCAAABLYQBAQGEAgABByBUYXNrIDIHBcMAZB0GKQMjAs8ABAIAMwYKA9EA46fMQAEMAaeABgEAAgAG0AApAgzLAPAGIQEEAhIAChEKLgQCBQI5UE8HCgUHuADxCggJBgMBBgkIAQAmBiBMYW5lIQMKLQoBIAaFAFExCQotIAoAgDCECAABhAkAZADwCmZyAQBgAAIAdnYCAGIBOgAAzwBmAW4BBAAAAAAAR9mA5QEAAAAFAAAADAAAAAAAAAAAAAAAAAABAgB2dmlBwfBwAQAAIQEAAExPUk8ABCJNGGBAgugAAADxJAIBAEBIZWxsbyBKZW5ueSEgTGFuZSEKCiMgVG9kYXkncyBUYXNrcwoKLSBUYXNrIDAKLRIAJCAxCQBDMgoCAAEAEwEIAPM+AwQXBQACAQQAAQIOAAMBAgoAAQEUAAUCAQIhHQIBGhYvIBIPEicqJwQaFwoCGygDARADAQoBAwUDCwkdGgsADAsUEyQGAAsDBAMACww9APADBg8MHx0YCwYBDQABEBEEAQIAWADyEQ8BAAIBAAIMDwoAAQQCBml0YWxpYwRib2xkBwMAAQGEBQAQAQoAYwEAhAMAAA0AgAABAYQAAAEAAAAAAOD6K7EBAAAABQAAAAYAggR0ZXh0AQYAggR0ZXh0sFTxRgABAAAAAAAA", + "connected": true + }, + { + "profile": { + "name": "Bob", + "peerId": "1" + }, + "encodedLoroText": "data:application/octet-stream;base64,bG9ybwAAAAAAAAAAAAAAANBwYasAA5UBAABMT1JPAAQiTRhgQIJaAQAAgwAxAEAEKgIAAQATAQgAQA0JCgEBAPGeAAYBBgEBCgKk0IABAAKmUUAGAQADAAgABgEEAQIABBEEYm9sZAZpdGFsaWMEdGV4dAA8AQQCJAAVBAABCgQaGT02PgBzdHN6AHl6eYABEggFAwwABAUTDAAMAAUMAAwABQ0HAQUHCQQBAQgUAQEHAEMBCgVIZWxsbwcgSmVubnkhCQoKIyBUb2RheYQGAAEIJ3MgVGFza3MBCoQBAQCEAgAAAS2EAQEBhAIAAQcbAEMgMgcFxwBiHQYpAyMCDAAGAgAzBgoD1QDjp8xAAQwBp4AGAQACAAbUACkCDM8A8AYhAQQCEgAKEQouBAIFAjlQTwcKBQe6APEKCAkGAwEGCQgBACYGIExhbmUhAwotCgEgBoUAUTEJCi0gCgCAMIQIAAGECQBkAPAKZnIBAGAAAgB2dgIAYgE6AADTAGoBcgEEAAAAAAAY3WWsAQAAAAUAAAAMAAAAAAAAAAAAAAAAAAECAHZ2aUHB8HIBAAAyAQAATE9STwACAQBASGVsbG8gSmVubnkhIExhbmUhCgojIFRvZGF5J3MgVGFza3MKCi0gVGFzayAwCi0gVGFzayAxCi0gVGFzayAyCgIAAAAAAAAAAAEAAAAAAAAAAwQVDAADAgEUAAECDgAFAQIBIAAFAgECJQwCAwsaEAIlBi8gEg8SJyonBBoXMgMBEAMBCgwCDQ0DBQMLCR0bDAADDAsQAAUUEyQGAA0DBAMACwwBIAAFBg8MJQECCAAFDAEJEAAjDg0AARARBAECAAEMCwACAQACDgABAwoAAQQCBml0YWxpYwRib2xkBwMAAQGEAwABAYQDAQEBhAMBAIQDAACEAwEBAYQDAAEBhAAAAQDoMqJAAQAAAAUAAAAGAIIEdGV4dAAGAIIEdGV4dGU0Pm8RAQAAAAAAAA==", + "connected": true + } + ] } diff --git a/deno.lock b/deno.lock index c7d55f3..ce46df5 100644 --- a/deno.lock +++ b/deno.lock @@ -41,16 +41,17 @@ "npm:highlight.js@^11.9.0": "11.10.0", "npm:is-equal@^1.7.0": "1.7.0", "npm:jotai@^2.6.0": "2.10.1_@types+react@18.0.26_react@18.3.1", - "npm:loro-crdt@0.10": "0.10.1", "npm:loro-crdt@0.15.0": "0.15.0", "npm:loro-crdt@0.16.3": "0.16.3", "npm:loro-crdt@1.0.0-beta.5": "1.0.0-beta.5", "npm:loro-crdt@1.2.4": "1.2.4", "npm:loro-crdt@1.2.5": "1.2.5", + "npm:loro-crdt@^1.5.2": "1.5.2", "npm:lucide-react@0.294": "0.294.0_react@18.3.1", "npm:marked@^14.1.3": "14.1.3", "npm:next-seo@^5.15.0": "5.15.0_next@13.5.7__react@18.3.1__react-dom@18.3.1___react@18.3.1_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:next-sitemap@^4.2.3": "4.2.3_next@13.5.7__react@18.3.1__react-dom@18.3.1___react@18.3.1_react@18.3.1_react-dom@18.3.1__react@18.3.1", + "npm:next@13.5.7": "13.5.7_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:next@^13.3.4": "13.5.7_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:nextra-theme-docs@^2.12.3": "2.13.4_next@13.5.7__react@18.3.1__react-dom@18.3.1___react@18.3.1_nextra@2.13.4__next@13.5.7___react@18.3.1___react-dom@18.3.1____react@18.3.1__react@18.3.1__react-dom@18.3.1___react@18.3.1__shiki@0.14.7_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:nextra@^2.13.2": "2.13.4_next@13.5.7__react@18.3.1__react-dom@18.3.1___react@18.3.1_react@18.3.1_react-dom@18.3.1__react@18.3.1_shiki@0.14.7", @@ -7208,12 +7209,6 @@ "js-tokens" ] }, - "loro-crdt@0.10.1": { - "integrity": "sha512-1PlaxcaJirQ7Q4SZ9SW0UfaxFezlXNE4Ccqgd7VdA/KFkRu0/AUQu6uKbR74njoWLzwcupBR7OclFMMEjjh5Eg==", - "dependencies": [ - "loro-wasm@0.10.1" - ] - }, "loro-crdt@0.15.0": { "integrity": "sha512-fRDXQYAtrGbRKuhHcLigZItkov5veGptbObE0XkD3cUinrBlIQ60ENO9g9yd9OhowaZ16+15wEseIEyTorjX6g==", "dependencies": [ @@ -7238,8 +7233,8 @@ "loro-crdt@1.2.5": { "integrity": "sha512-ep7XD7JRc6wuhGHP22LdSCBwHL9XdQf8mHA0xK5tKMhnsBhmKNT4+YkUVAmtE4icQN/o83uXKtYk3ZCTNc+EUA==" }, - "loro-wasm@0.10.1": { - "integrity": "sha512-H8NmkUcGyg0bnhTl87Xma7TBoJUtEuq9kKQA+KimHqVj5xi5r1raog8DN3XYVotUzKEC0owRGvt6orJSdYRCtw==" + "loro-crdt@1.5.2": { + "integrity": "sha512-MvSY5sf8+86d7NL8cesXvIgQc4XZmGyDhAySrcSDCz5ahyvedaICDWXZ7tMCB33TQtp+wqnuausPalFfP8G/eA==" }, "loro-wasm@0.15.0": { "integrity": "sha512-BYVs3z/zs7fW2BXTwHPbVxitJ2l/EdCheT4MhH2ZQGCmi7SKHUoUZ5svW0sksuVmS9potDRcXAgtVvNtfWvCSw==" @@ -11101,7 +11096,7 @@ "npm:highlight.js@^11.9.0", "npm:is-equal@^1.7.0", "npm:jotai@^2.6.0", - "npm:loro-crdt@0.10", + "npm:loro-crdt@^1.5.2", "npm:lucide-react@0.294", "npm:marked@^14.1.3", "npm:next-seo@^5.15.0", diff --git a/package.json b/package.json index 107f275..9378cf7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "highlight.js": "^11.9.0", "is-equal": "^1.7.0", "jotai": "^2.6.0", - "loro-crdt": "^0.10.0", + "loro-crdt": "^1.5.2", "lucide-react": "^0.294.0", "next": "^13.3.4", "next-seo": "^5.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf26e45..8acbd9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: ^2.6.0 version: 2.6.0(@types/react@https://registry.npmmirror.com/@types/react/-/react-18.0.26.tgz)(react@18.2.0) loro-crdt: - specifier: ^0.10.0 - version: 0.10.0 + specifier: ^1.5.2 + version: 1.5.2 lucide-react: specifier: ^0.294.0 version: 0.294.0(react@18.2.0) @@ -5171,11 +5171,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loro-crdt@0.10.0: - resolution: {integrity: sha512-B9rYksjiEyv4nRbkkcU+6FRsvl5WtK9a9s2XPUAQfrBbVXQxZFaSZQHfflHAcWnYeV0JVhFEHfYTYfi5pf35iQ==} - - loro-wasm@0.10.0: - resolution: {integrity: sha512-BjI2GJmSa92YOlo4NhZDGXKOz+edqOu98qmtiIaRIcGUe9wA/qCq889A23rt3w2SyaQ4HQmj8ODLpIBE1eQgGw==} + loro-crdt@1.5.2: + resolution: {integrity: sha512-MvSY5sf8+86d7NL8cesXvIgQc4XZmGyDhAySrcSDCz5ahyvedaICDWXZ7tMCB33TQtp+wqnuausPalFfP8G/eA==} lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -13596,11 +13593,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loro-crdt@0.10.0: - dependencies: - loro-wasm: 0.10.0 - - loro-wasm@0.10.0: {} + loro-crdt@1.5.2: {} lower-case@2.0.2: dependencies: