Skip to content

Commit c2ff743

Browse files
authored
refactor: use the latest loro-crdt as demo (#8)
1 parent 244108c commit c2ff743

File tree

7 files changed

+90
-100
lines changed

7 files changed

+90
-100
lines changed

components/TwinEditors/EditorManager.ts

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { bytesToBase64DataUrl, dataUrlToBytes } from "@/lib/utils/data";
22
import { showVersion } from "@/lib/utils/loro";
3-
import { Change, Loro, LoroEvent, OpId, setDebug } from "loro-crdt";
3+
import { Change, LoroDoc, LoroEventBatch, OpId, Subscription } from "loro-crdt";
44
import Quill from "quill";
55
import { QuillBinding } from "./QuillBinding";
66

7-
setDebug("*");
87
type PeerProfile = { name: string; peerId: string };
98

109
type Frontiers = OpId[];
@@ -39,14 +38,14 @@ const QUILL_TOOLBAR_MAP: WeakMap<HTMLElement, Quill> = new WeakMap();
3938
class EditorInstance {
4039
#index: number;
4140
#profile: PeerProfile; // Exportable (JSON)
42-
#text: Loro; // Exportable (binary data)
41+
#text: LoroDoc; // Exportable (binary data)
4342
#quill: Quill;
4443
#binding: QuillBinding;
4544
#connected: boolean = true; // Exportable (JSON)
4645
#manager: EditorManager;
4746

4847
public async export(): Promise<EditorInstanceExportData> {
49-
const rawData = this.#text.exportSnapshot();
48+
const rawData = this.#text.export({mode: "snapshot"});
5049
return {
5150
profile: {
5251
name: this.#profile.name,
@@ -86,7 +85,7 @@ class EditorInstance {
8685
this.#index = index;
8786
this.#profile = profile;
8887
this.#manager = manager;
89-
this.#text = new Loro();
88+
this.#text = new LoroDoc();
9089
this.#text.configTextStyle({
9190
bold: { expand: "after" },
9291
italic: { expand: "after" },
@@ -114,12 +113,12 @@ class EditorInstance {
114113
this.#text.subscribe((e) => {
115114
// Synchronize the change to the sum text.
116115
const sumVersion = this.#manager.sumText.version();
117-
const updateData = this.#text.exportFrom(sumVersion);
116+
const updateData = this.#text.export({mode: "update", from: sumVersion});
118117
this.#manager.sumText.import(updateData);
119118
// If this is a local change (i.e., not a change synchronized from
120119
// other peers), we synchronize to other connected peers if we're
121120
// connected.
122-
if (e.local) {
121+
if (e.by === "local") {
123122
this.synchronize();
124123
}
125124
});
@@ -133,7 +132,7 @@ class EditorInstance {
133132
return this.#profile.name;
134133
}
135134

136-
public get text(): Loro {
135+
public get text(): LoroDoc {
137136
return this.#text;
138137
}
139138

@@ -166,8 +165,8 @@ class EditorInstance {
166165
await Promise.resolve();
167166
for (const that of this.#otherPeers()) {
168167
if (!that.connected) return;
169-
this.#text.import(that.#text.exportFrom(this.#text.version()));
170-
that.#text.import(this.#text.exportFrom(that.#text.version()));
168+
this.#text.import(that.#text.export({mode: "update", from: this.#text.version()}));
169+
that.#text.import(this.#text.export({mode: "update", from: that.#text.version()}));
171170
}
172171
}
173172
}
@@ -220,10 +219,10 @@ export async function decodeExport(
220219
}
221220

222221
export class EditorManager {
223-
#sumText: Loro;
222+
#sumText: LoroDoc;
224223
#numberOfOperations: number = 0;
225224
#peers: EditorInstance[];
226-
#subscriptions: Map<number, number[]> = new Map();
225+
#subscriptions: Map<number, Subscription[]> = new Map();
227226

228227
static import(
229228
data: EditorManagerImportData,
@@ -253,9 +252,9 @@ export class EditorManager {
253252
) {
254253
throw new RangeError("insufficient HTML elements for creating editors");
255254
}
256-
this.#sumText = new Loro();
255+
this.#sumText = new LoroDoc();
257256
this.#sumText.subscribe((e) => {
258-
if (!e.fromCheckout) {
257+
if (e.by === "local") {
259258
this.#numberOfOperations += 1;
260259
}
261260
});
@@ -277,7 +276,7 @@ export class EditorManager {
277276
};
278277
}
279278

280-
public get sumText(): Loro {
279+
public get sumText(): LoroDoc {
281280
return this.#sumText;
282281
}
283282

@@ -314,7 +313,7 @@ export class EditorManager {
314313
}
315314

316315
public subscribeAll(
317-
listener: (instance: EditorInstance, e: LoroEvent) => void
316+
listener: (instance: EditorInstance, e: LoroEventBatch) => void
318317
): number {
319318
this.#subscriptions.set(
320319
this.#subscriptions.size,
@@ -329,7 +328,7 @@ export class EditorManager {
329328
if (!this.#subscriptions.has(subscription)) return;
330329
this.#subscriptions
331330
.get(subscription)
332-
?.forEach((n, index) => this.#peers[index].text.unsubscribe(n));
331+
?.forEach((unsubscribe, index) => unsubscribe());
333332
this.#subscriptions.set(subscription, []);
334333
}
335334

components/TwinEditors/QuillBinding.ts

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,65 @@
1-
import { Delta, Loro, LoroText, setDebug } from "loro-crdt";
1+
import { Delta, LoroDoc, LoroText, PeerID } from "loro-crdt";
22
import Quill, { DeltaOperation, Sources } from "quill";
33
import isEqual from "is-equal";
44
import QuillDelta from "quill-delta";
55

6-
type Frontiers = { peer: string; counter: number }[];
6+
type Frontiers = { peer: PeerID; counter: number }[];
77

88
function showFrontiers(frontiers: Frontiers): string {
99
return frontiers.map((x) => `${x.peer}@${x.counter}`).join(";");
1010
}
1111

1212
export class QuillBinding {
1313
private richtext: LoroText;
14-
constructor(public doc: Loro, public quill: Quill) {
14+
constructor(public doc: LoroDoc, public quill: Quill) {
1515
this.quill = quill;
16-
this.richtext = doc.getText("text");
17-
this.richtext.subscribe(doc, (event) => {
18-
Promise.resolve().then(() => {
19-
if ((!event.local || event.fromCheckout) && event.diff.type == "text" && event.origin !== "ignore") {
20-
const eventDelta = event.diff.diff;
21-
const delta: Delta<string>[] = [];
22-
let index = 0;
23-
for (let i = 0; i < eventDelta.length; i++) {
24-
const d = eventDelta[i];
25-
const length = d.delete || d.retain || d.insert!.length;
26-
// skip the last newline that quill automatically appends
27-
if (
28-
d.insert &&
29-
d.insert === "\n" &&
30-
index === quill.getLength() - 1 &&
31-
i === eventDelta.length - 1 &&
32-
d.attributes != null &&
33-
Object.keys(d.attributes).length > 0
34-
) {
35-
delta.push({
36-
retain: 1,
37-
attributes: d.attributes,
38-
});
16+
const richtext = doc.getText("text");
17+
this.richtext = richtext;
18+
richtext.subscribe(async (event) => {
19+
if (event.by !== "local" && event.origin !== "ignore") {
20+
for (const e of event.events) {
21+
if (e.diff.type === "text") {
22+
const eventDelta = e.diff.diff;
23+
const delta: Delta<string>[] = [];
24+
let index = 0;
25+
for (let i = 0; i < eventDelta.length; i++) {
26+
const d = eventDelta[i];
27+
const length = d.delete || d.retain || d.insert!.length;
28+
// skip the last newline that quill automatically appends
29+
if (
30+
d.insert &&
31+
d.insert === "\n" &&
32+
index === quill.getLength() - 1 &&
33+
i === eventDelta.length - 1 &&
34+
d.attributes != null &&
35+
Object.keys(d.attributes).length > 0
36+
) {
37+
delta.push({
38+
retain: 1,
39+
attributes: d.attributes,
40+
});
41+
index += length;
42+
continue;
43+
}
44+
45+
delta.push(d);
3946
index += length;
40-
continue;
4147
}
4248

43-
delta.push(d);
44-
index += length;
45-
}
46-
47-
quill.updateContents(new QuillDelta(delta), "this" as any);
48-
const a = this.richtext.toDelta();
49-
const b = this.quill.getContents().ops;
50-
if (!assertEqual(a, b as any, true)) {
51-
console.log(this.doc.peerId, "COMPARE AFTER CRDT_EVENT", event.diff);
52-
this.resetQuillContent(a)
49+
quill.updateContents(new QuillDelta(delta), "this" as any);
50+
const a = doc.getText("text").toDelta();
51+
const b = this.quill.getContents().ops;
52+
if (!assertEqual(a, b as any, true)) {
53+
console.log(this.doc.peerId, "COMPARE AFTER CRDT_EVENT", e.diff);
54+
this.resetQuillContent(a)
55+
}
5356
}
5457
}
55-
});
58+
}
5659
});
5760
quill.setContents(
5861
new QuillDelta(
59-
this.richtext.toDelta().map((x) => ({
62+
richtext.toDelta().map((x) => ({
6063
insert: x.insert,
6164
attributions: x.attributes,
6265
}))
@@ -116,7 +119,7 @@ export class QuillBinding {
116119
if (origin !== ("this" as any)) {
117120
if (this.richtext.toString().slice(-1) !== '\n') {
118121
this.richtext.applyDelta([{ retain: this.richtext.length }, { insert: "\n" }]);
119-
this.doc.commit("ignore");
122+
this.doc.commit({ origin: "ignore" });
120123
}
121124
this.applyDelta(ops as DeltaOperation[]);
122125
const a = this.richtext.toDelta();

components/landing/Demonstration/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export default function DemoSection() {
116116
const n = manager.sumText.subscribe((): void => {
117117
updateTimelineHistory(manager.sumText.getAllChanges());
118118
});
119-
return manager.sumText.unsubscribe.bind(manager.sumText, n);
119+
return n;
120120
}, [resetCounter, updateTimelineHistory]);
121121

122122
useEffect(() => {

components/landing/data/demo.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
{
2-
"numberOfOperations": 58,
3-
"peers": [
4-
{
5-
"profile": {
6-
"name": "Alice",
7-
"peerId": "0"
8-
},
9-
"encodedLoroText": "data:application/octet-stream;base64,bG9yb9bWgiV3xUZN3pQcD4Fn8FkAAgcFAjQAHQsACgAaEgAEAgQACTkAOBgVBAARDAYASwBMAEsAGAQABwIBAAIQAAMBAgQAAQEEAAECBgABARkBDgwAEQoACQ0ADgoACQQADwoJDQAYCQ0AHQsACgkYHCcEGgsHDxoHKy4EERMHDBQBBAwbAQQIBgkEAAsCAQIBAAIJDwoEAQYCDxIOEAXA4MnaDAAQBBYFEKaIHwwJBAABAgYAAwECBwEBAQECAQECEAADAgEAAwGIAQIBFAIAAAIAQgFCXgVIZWxsbwcgSmVubnkhBiBMYW5lIQkKCiMgVG9kYXkIJ3MgVGFza3MBCgEKhAkAAYQIAQEILSBUYXNrIDABCoQJAQGECAABAi0gBFRhc2sCIDGEAQAAAQqEAQAAAQpIEQIAAAAAAAAAAAAAAAAAAAABBgEEAQMABBIDBGJvbGQGaXRhbGljBHRleHQQAQIGCQABAAEABgkIAh4JDgoBAQcLYgcEHwYA",
10-
"connected": true
11-
},
12-
{
13-
"profile": {
14-
"name": "Bob",
15-
"peerId": "1"
16-
},
17-
"encodedLoroText": "data:application/octet-stream;base64,bG9yb9bWgiV3xUZN3pQcD4Fn8FkAAgcFAjQAHQsACgAaEgAEAgQACTkAOBgVBAARDAYASwBMAEsAGAQABwIBAAIQAAMBAgQAAQEEAAECBgABARkBDgwAEQoACQ0ADgoACQQADwoJDQAYCQ0AHQsACgkYHCcEGgsHDxoHKy4EERMHDBQBBAwbAQQIBgkEAAsCAQIBAAIJDwoEAQYCDxIOEAXA4MnaDAAQBBYFEKaIHwwJBAABAgYAAwECBwEBAQECAQECEAADAgEAAwGIAQIBFAIAAAIAQgFCXgVIZWxsbwcgSmVubnkhBiBMYW5lIQkKCiMgVG9kYXkIJ3MgVGFza3MBCgEKhAkAAYQIAQEILSBUYXNrIDABCoQJAQGECAABAi0gBFRhc2sCIDGEAQAAAQqEAQAAAQpIEQIAAAAAAAAAAAAAAAAAAAABBgEEAQMABBIDBGJvbGQGaXRhbGljBHRleHQQAQIGCQABAAEABgkIAh4JDgoBAQcLYgcEHwYA",
18-
"connected": true
19-
}
20-
]
2+
"numberOfOperations": 58,
3+
"peers": [
4+
{
5+
"profile": {
6+
"name": "Alice",
7+
"peerId": "0"
8+
},
9+
"encodedLoroText": "data:application/octet-stream;base64,bG9ybwAAAAAAAAAAAAAAAM00amcAA5MBAABMT1JPAAQiTRhgQIJYAQAAgwAxAEAEKgIAAQATAQgAQA0JCgEBAPOjAAYBBgEBCgKk0IABAAKmUUAGAQADAAgABgEEAQIABBEEYm9sZAZpdGFsaWMEdGV4dAA5AQQCIgATBAAdJBo9Nj4Ac3RzegB5enmAARIGBQMMAAQFEwwADAAFDAAMAAUMBQEMCQQBAQgUAQEHAEIBCgxIZWxsbyBKZW5ueSEJCgojIFRvZGF5hAYAAQgncyBUYXNrcwEKhAEBAIQCAAABLYQBAQGEAgABByBUYXNrIDIHBcMAZB0GKQMjAs8ABAIAMwYKA9EA46fMQAEMAaeABgEAAgAG0AApAgzLAPAGIQEEAhIAChEKLgQCBQI5UE8HCgUHuADxCggJBgMBBgkIAQAmBiBMYW5lIQMKLQoBIAaFAFExCQotIAoAgDCECAABhAkAZADwCmZyAQBgAAIAdnYCAGIBOgAAzwBmAW4BBAAAAAAAR9mA5QEAAAAFAAAADAAAAAAAAAAAAAAAAAABAgB2dmlBwfBwAQAAIQEAAExPUk8ABCJNGGBAgugAAADxJAIBAEBIZWxsbyBKZW5ueSEgTGFuZSEKCiMgVG9kYXkncyBUYXNrcwoKLSBUYXNrIDAKLRIAJCAxCQBDMgoCAAEAEwEIAPM+AwQXBQACAQQAAQIOAAMBAgoAAQEUAAUCAQIhHQIBGhYvIBIPEicqJwQaFwoCGygDARADAQoBAwUDCwkdGgsADAsUEyQGAAsDBAMACww9APADBg8MHx0YCwYBDQABEBEEAQIAWADyEQ8BAAIBAAIMDwoAAQQCBml0YWxpYwRib2xkBwMAAQGEBQAQAQoAYwEAhAMAAA0AgAABAYQAAAEAAAAAAOD6K7EBAAAABQAAAAYAggR0ZXh0AQYAggR0ZXh0sFTxRgABAAAAAAAA",
10+
"connected": true
11+
},
12+
{
13+
"profile": {
14+
"name": "Bob",
15+
"peerId": "1"
16+
},
17+
"encodedLoroText": "data:application/octet-stream;base64,bG9ybwAAAAAAAAAAAAAAANBwYasAA5UBAABMT1JPAAQiTRhgQIJaAQAAgwAxAEAEKgIAAQATAQgAQA0JCgEBAPGeAAYBBgEBCgKk0IABAAKmUUAGAQADAAgABgEEAQIABBEEYm9sZAZpdGFsaWMEdGV4dAA8AQQCJAAVBAABCgQaGT02PgBzdHN6AHl6eYABEggFAwwABAUTDAAMAAUMAAwABQ0HAQUHCQQBAQgUAQEHAEMBCgVIZWxsbwcgSmVubnkhCQoKIyBUb2RheYQGAAEIJ3MgVGFza3MBCoQBAQCEAgAAAS2EAQEBhAIAAQcbAEMgMgcFxwBiHQYpAyMCDAAGAgAzBgoD1QDjp8xAAQwBp4AGAQACAAbUACkCDM8A8AYhAQQCEgAKEQouBAIFAjlQTwcKBQe6APEKCAkGAwEGCQgBACYGIExhbmUhAwotCgEgBoUAUTEJCi0gCgCAMIQIAAGECQBkAPAKZnIBAGAAAgB2dgIAYgE6AADTAGoBcgEEAAAAAAAY3WWsAQAAAAUAAAAMAAAAAAAAAAAAAAAAAAECAHZ2aUHB8HIBAAAyAQAATE9STwACAQBASGVsbG8gSmVubnkhIExhbmUhCgojIFRvZGF5J3MgVGFza3MKCi0gVGFzayAwCi0gVGFzayAxCi0gVGFzayAyCgIAAAAAAAAAAAEAAAAAAAAAAwQVDAADAgEUAAECDgAFAQIBIAAFAgECJQwCAwsaEAIlBi8gEg8SJyonBBoXMgMBEAMBCgwCDQ0DBQMLCR0bDAADDAsQAAUUEyQGAA0DBAMACwwBIAAFBg8MJQECCAAFDAEJEAAjDg0AARARBAECAAEMCwACAQACDgABAwoAAQQCBml0YWxpYwRib2xkBwMAAQGEAwABAYQDAQEBhAMBAIQDAACEAwEBAYQDAAEBhAAAAQDoMqJAAQAAAAUAAAAGAIIEdGV4dAAGAIIEdGV4dGU0Pm8RAQAAAAAAAA==",
18+
"connected": true
19+
}
20+
]
2121
}

deno.lock

Lines changed: 5 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"highlight.js": "^11.9.0",
3939
"is-equal": "^1.7.0",
4040
"jotai": "^2.6.0",
41-
"loro-crdt": "^0.10.0",
41+
"loro-crdt": "^1.5.2",
4242
"lucide-react": "^0.294.0",
4343
"next": "^13.3.4",
4444
"next-seo": "^5.15.0",

pnpm-lock.yaml

Lines changed: 5 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)