You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: packages/site/docs/guide/lesson-019.md
+161-22
Original file line number
Diff line number
Diff line change
@@ -4,13 +4,170 @@ publish: false
4
4
---
5
5
6
6
<scriptsetup>
7
+
importHistoryfrom'../components/History.vue';
7
8
importLoroCRDTfrom'../components/LoroCRDT.vue';
8
9
</script>
9
10
10
11
# Lesson 19 - History and collaboration
11
12
12
13
In this lesson, we'll explore how to implement multi-user collaborative editing functionality. We'll introduce several core concepts and technologies, including history records, Local-first, and CRDT.
13
14
15
+
## History {#history}
16
+
17
+
<History />
18
+
19
+
Whether you are a text or graphical editor, the history and undo redo functionality is a must. As implemented in [JavaScript-Undo-Manager], we can use an undoStack to save each operation and its inverse:
20
+
21
+
```ts
22
+
function createPerson(id, name) {
23
+
// first creation
24
+
addPerson(id, name);
25
+
26
+
// make undoable
27
+
undoManager.add({
28
+
undo: () =>removePerson(id),
29
+
redo: () =>addPerson(id, name),
30
+
});
31
+
}
32
+
```
33
+
34
+
We can also use two stacks to manage undo and redo operations separately, see: [UI Algorithms: A Tiny Undo Stack]. The last section of [How Figma's multiplayer technology works] introduces Figma's implementation approach:
35
+
36
+
> This is why in Figma an undo operation modifies redo history at the time of the undo, and likewise a redo operation modifies undo history at the time of the redo.
37
+
38
+
Referring to [Excalidraw HistoryEntry], we add a History class to manage undo and redo operations.
39
+
40
+
```ts
41
+
exportclassHistory {
42
+
#undoStack:HistoryStack= [];
43
+
#redoStack:HistoryStack= [];
44
+
45
+
clear() {
46
+
this.#undoStack.length=0;
47
+
this.#redoStack.length=0;
48
+
}
49
+
}
50
+
```
51
+
52
+
Each entry in the history stack contains two types of modifications to the system state, which we describe below:
53
+
54
+
```ts
55
+
typeHistoryStack=HistoryEntry[];
56
+
exportclassHistoryEntry {
57
+
privateconstructor(
58
+
publicreadonlyappStateChange:AppStateChange,
59
+
publicreadonlyelementsChange:ElementsChange,
60
+
) {}
61
+
}
62
+
```
63
+
64
+
### Desgin states {#design-states}
65
+
66
+
Referring to Excalidraw, we split the system state into `AppState` and `Elements`. The former includes the state of the canvas as well as the UI components, such as the current theme, the camera zoom level, the toolbar configurations and selections, etc.
67
+
68
+
```ts
69
+
exportinterfaceAppState {
70
+
theme:Theme;
71
+
checkboardStyle:CheckboardStyle;
72
+
cameraZoom:number;
73
+
penbarAll:Pen[];
74
+
penbarSelected:Pen[];
75
+
taskbarAll:Task[];
76
+
taskbarSelected:Task[];
77
+
layersSelected:SerializedNode['id'][];
78
+
propertiesOpened:SerializedNode['id'][];
79
+
}
80
+
```
81
+
82
+
As you can see, we prefer to use a flat data structure rather than a nested object structure like `{ penbar: { all: [], selected: [] } }`, in order to allow for quicker and easier state diff considerations that don't require recursion, see: [distinctKeysIterator].
83
+
84
+
The latter is the array of shapes in the canvas, which we previously covered in [Lesson 10] in the context of serializing shapes. Here we use a flat array instead of a tree structure, move the attributes in the `attributes` object up to the top level, and represent the parent-child relationship slightly differently, using `parentId` to associate the parent node with the `id`. However, we can't just traverse the tree structure and render it, we need to sort the graph array according to some rules, which we'll cover later:
85
+
86
+
```ts
87
+
// before
88
+
interfaceSerializedNode {
89
+
id:string;
90
+
children: [];
91
+
attributes: {
92
+
fill:string;
93
+
stroke:string;
94
+
};
95
+
}
96
+
97
+
// after
98
+
interfaceSerializedNode {
99
+
id:string;
100
+
parentId?:string;
101
+
fill:string;
102
+
stroke:string;
103
+
}
104
+
```
105
+
106
+
Consider collaboration we'll add more attributes like `version` later on. Let's see how to add a history entry.
107
+
108
+
### Record a history entry {#record-history-entry}
109
+
110
+
In the example at the top of this section, we used the API to insert two histories for updating the fill color of the rectangle, which you can do using the undo and redo operations in the top toolbar:
111
+
112
+
```ts
113
+
api.updateNode(node, {
114
+
fill: 'red',
115
+
});
116
+
api.updateNode(node, {
117
+
fill: 'blue',
118
+
});
119
+
```
120
+
121
+
Each call to `api.updateNode` adds a history record because the state of the graph did change, but it is important to note that only AppState changes should not reset the redoStack. when a change occurs we add the inverse operation of the change to the undoStack:
Now we can look at how to design the `AppStateChange` and `ElementsChange` data structures for `Change`, allowing us to use a generic `entry.inverse()` instead of describing each changeable attribute with `add/removeFill``add/removeStroke` and so on.
What is CRDT? The following introduction comes from [What are CRDTs]. The collaborative features of Google Docs / Figma / Tiptap are all implemented based on it. This article also compares the characteristics of CRDT and OT in detail:
@@ -143,28 +300,6 @@ Referring to [Excalidraw updateScene], we can also provide an `updateScene` meth
143
300
canvas.updateScene({ elements });
144
301
```
145
302
146
-
## History {#history}
147
-
148
-
Referring to [Excalidraw HistoryEntry], we add a History class to manage undo and redo operations.
149
-
150
-
```ts
151
-
exportclassHistory {
152
-
#undoStack:HistoryStack= [];
153
-
#redoStack:HistoryStack= [];
154
-
155
-
clear() {
156
-
this.#undoStack.length=0;
157
-
this.#redoStack.length=0;
158
-
}
159
-
}
160
-
```
161
-
162
-
### Undo and Redo {#undo-and-redo}
163
-
164
-
The last section of [How Figma's multiplayer technology works] introduces Figma's implementation approach:
165
-
166
-
> This is why in Figma an undo operation modifies redo history at the time of the undo, and likewise a redo operation modifies undo history at the time of the redo.
167
-
168
303
## Extended Reading {#extended-reading}
169
304
170
305
-[How Figma's multiplayer technology works]
@@ -198,3 +333,7 @@ The last section of [How Figma's multiplayer technology works] introduces Figma'
0 commit comments