@cr_docs_t/dts 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dts/Fugue/FNode.d.ts +2 -4
- package/dist/dts/Fugue/FNode.d.ts.map +1 -1
- package/dist/dts/Fugue/FNode.js +2 -2
- package/dist/dts/Fugue/FugueList.d.ts +1 -1
- package/dist/dts/Fugue/FugueList.d.ts.map +1 -1
- package/dist/dts/Fugue/FugueList.js +10 -15
- package/dist/dts/Fugue/index.d.ts +0 -1
- package/dist/dts/Fugue/index.d.ts.map +1 -1
- package/dist/dts/Fugue/index.js +1 -1
- package/dist/dts/FugueTree/FTree.d.ts +147 -0
- package/dist/dts/FugueTree/FTree.d.ts.map +1 -0
- package/dist/dts/FugueTree/FTree.js +467 -0
- package/dist/dts/FugueTree/FugueTree.d.ts +127 -0
- package/dist/dts/FugueTree/FugueTree.d.ts.map +1 -0
- package/dist/dts/FugueTree/FugueTree.js +351 -0
- package/dist/dts/FugueTree/index.d.ts +3 -0
- package/dist/dts/FugueTree/index.d.ts.map +1 -0
- package/dist/dts/FugueTree/index.js +2 -0
- package/dist/dts/Serailizers/Fugue/Message.d.ts +1 -1
- package/dist/dts/Serailizers/Fugue/Message.d.ts.map +1 -1
- package/dist/dts/Serailizers/Fugue/Message.js +12 -4
- package/dist/dts/Serailizers/Fugue/State.d.ts +1 -1
- package/dist/dts/Serailizers/Fugue/State.d.ts.map +1 -1
- package/dist/dts/Serailizers/FugueTree/Message.d.ts +13 -0
- package/dist/dts/Serailizers/FugueTree/Message.d.ts.map +1 -0
- package/dist/dts/Serailizers/FugueTree/Message.js +21 -0
- package/dist/dts/Serailizers/FugueTree/State.d.ts +9 -0
- package/dist/dts/Serailizers/FugueTree/State.d.ts.map +1 -0
- package/dist/dts/Serailizers/FugueTree/State.js +16 -0
- package/dist/dts/Serailizers/FugueTree/index.d.ts +3 -0
- package/dist/dts/Serailizers/FugueTree/index.d.ts.map +1 -0
- package/dist/dts/Serailizers/FugueTree/index.js +2 -0
- package/dist/dts/Serailizers/index.d.ts +1 -1
- package/dist/dts/Serailizers/index.d.ts.map +1 -1
- package/dist/dts/Serailizers/index.js +2 -1
- package/dist/dts/TotalOrder/StringTotalOrder.d.ts +2 -2
- package/dist/dts/TotalOrder/StringTotalOrder.d.ts.map +1 -1
- package/dist/dts/index.d.ts +2 -1
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/index.js +2 -1
- package/dist/tests/FugueTree.test.d.ts +2 -0
- package/dist/tests/FugueTree.test.d.ts.map +1 -0
- package/dist/tests/FugueTree.test.js +8 -0
- package/dist/tests/mocks.d.ts +2 -1
- package/dist/tests/mocks.d.ts.map +1 -1
- package/dist/tests/mocks.js +2 -1
- package/dist/types/Fugue/Fugue.d.ts +2 -2
- package/dist/types/Fugue/Fugue.d.ts.map +1 -1
- package/dist/types/FugueTree/Message.d.ts +42 -0
- package/dist/types/FugueTree/Message.d.ts.map +1 -0
- package/dist/types/FugueTree/Message.js +27 -0
- package/dist/types/FugueTree/index.d.ts +2 -0
- package/dist/types/FugueTree/index.d.ts.map +1 -0
- package/dist/types/FugueTree/index.js +1 -0
- package/dist/types/Models/Schema.d.ts +18 -0
- package/dist/types/Models/Schema.d.ts.map +1 -1
- package/dist/types/Models/Schema.js +7 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FTree is a CRDT for collaborative text editing. It represents the document as a tree of nodes,
|
|
3
|
+
* where each node corresponds to a character. The tree structure encodes the relative ordering
|
|
4
|
+
* of characters, and each node has a unique ID that allows it to be referenced by other nodes.
|
|
5
|
+
* The tree supports insertions and deletions of characters, and can be traversed in order to reconstruct the document text.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
export class FTree {
|
|
9
|
+
constructor() {
|
|
10
|
+
// Map from sender ID to an array of nodes created by that sender, indexed by counter. This allows for efficient lookup of nodes by ID.
|
|
11
|
+
this.nodes = new Map();
|
|
12
|
+
this.root = {
|
|
13
|
+
id: { sender: "", counter: 0 },
|
|
14
|
+
value: null,
|
|
15
|
+
isDeleted: true,
|
|
16
|
+
parent: null,
|
|
17
|
+
side: "R",
|
|
18
|
+
leftChildren: [],
|
|
19
|
+
rightChildren: [],
|
|
20
|
+
size: 0,
|
|
21
|
+
};
|
|
22
|
+
this.nodes.set("", [this.root]);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the node with the given ID. Throws an error if no such node exists.
|
|
26
|
+
* @param id - the ID of the node to retrieve
|
|
27
|
+
* @returns the node with the given ID
|
|
28
|
+
*/
|
|
29
|
+
getByID(id) {
|
|
30
|
+
const sender = this.nodes.get(id.sender);
|
|
31
|
+
if (sender !== undefined) {
|
|
32
|
+
const node = sender[id.counter];
|
|
33
|
+
if (node !== undefined)
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Unknown ID ${JSON.stringify(id)}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Add a node to the tree with the given ID, value, parent, side, and optional rightOrigin.
|
|
40
|
+
* The node is inserted among its siblings according to the order defined by rightOrigin and sender ID,
|
|
41
|
+
* and the size of all ancestors is updated accordingly.
|
|
42
|
+
* @param id - the ID of the new node
|
|
43
|
+
* @param value - the character value of the new node
|
|
44
|
+
* @param parent - the parent node of the new node
|
|
45
|
+
* @param side - the side (left or right) of the new node with respect to its parent
|
|
46
|
+
* @param rightOriginID - the ID of the node to the right of which the new node was inserted, or null if inserted at the end of its siblings (optional)
|
|
47
|
+
*/
|
|
48
|
+
addNode(id, value, parent, side, rightOriginID) {
|
|
49
|
+
const node = {
|
|
50
|
+
id,
|
|
51
|
+
value,
|
|
52
|
+
isDeleted: false,
|
|
53
|
+
parent,
|
|
54
|
+
side,
|
|
55
|
+
leftChildren: [],
|
|
56
|
+
rightChildren: [],
|
|
57
|
+
size: 0,
|
|
58
|
+
};
|
|
59
|
+
// If a rightOriginID is present we set the rightOrigin pointer.
|
|
60
|
+
// We require that rightOrigin is already in the tree, since it
|
|
61
|
+
// must be a sibling of the new node and thus must have been inserted before it.
|
|
62
|
+
if (rightOriginID !== undefined) {
|
|
63
|
+
node.rightOrigin = rightOriginID === null ? null : this.getByID(rightOriginID);
|
|
64
|
+
}
|
|
65
|
+
// Add node to nodesByID for lookup by ID.
|
|
66
|
+
let sender = this.nodes.get(id.sender);
|
|
67
|
+
if (!sender) {
|
|
68
|
+
sender = [];
|
|
69
|
+
this.nodes.set(id.sender, sender);
|
|
70
|
+
}
|
|
71
|
+
sender.push(node);
|
|
72
|
+
// Insert node into siblings and update sizes of ancestors.
|
|
73
|
+
this.insertIntoSiblings(node);
|
|
74
|
+
this.updateSize(node, 1);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* TODO: Maybe change the children types to linked list to make insertions more efficient, since we expect many insertions at the end of the siblings.
|
|
78
|
+
* Inserts a node into the correct position among its parent node's children arrays based on the
|
|
79
|
+
* node's rightOrigin and sender ID, and the existing order of its rightOrigin among its siblings.
|
|
80
|
+
* @param node - The node to insert into its siblings
|
|
81
|
+
*/
|
|
82
|
+
insertIntoSiblings(node) {
|
|
83
|
+
// Insert node among its same-side siblings.
|
|
84
|
+
const p = node.parent;
|
|
85
|
+
if (node.side === "R") {
|
|
86
|
+
const right = p.rightChildren;
|
|
87
|
+
// We want to insert node in the rightChildren array so that it is after all nodes that are less than
|
|
88
|
+
// node according to the following order:
|
|
89
|
+
// - If a and b have different rightOrigins, the one with the lesser rightOrigin goes first with null < any node.
|
|
90
|
+
// - If a ad b have the same rightOrigin, the one with the lesser sender ID goes first
|
|
91
|
+
let i = 0;
|
|
92
|
+
for (; i < right.length; i++) {
|
|
93
|
+
const sib = right[i];
|
|
94
|
+
const isLessThanSib = this.isLess(node.rightOrigin, sib.rightOrigin);
|
|
95
|
+
const isEqualRightOrigin = node.rightOrigin === sib.rightOrigin;
|
|
96
|
+
const isGreaterSender = node.id.sender > sib.id.sender;
|
|
97
|
+
if (!(isLessThanSib || (isEqualRightOrigin && isGreaterSender)))
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
right.splice(i, 0, node);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const left = p.leftChildren;
|
|
104
|
+
// We want to insert node in the leftChildren array so that it is after all nodes that are have a lexographically
|
|
105
|
+
// less sender ID, since all nodes in the leftChildren array have the same rightOrigin (the parent node).
|
|
106
|
+
let i = 0;
|
|
107
|
+
for (; i < left.length; i++) {
|
|
108
|
+
if (!(node.id.sender.localeCompare(left[i].id.sender) > 0))
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
left.splice(i, 0, node);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Determines whether node a should come before node b in the document order, based on their rightOrigin and sender ID.
|
|
116
|
+
* returns true if a should come before b, false otherwise. i.e. a < b
|
|
117
|
+
* @param a - the first node to compare
|
|
118
|
+
* @param b - the second node to compare
|
|
119
|
+
*/
|
|
120
|
+
isLess(a, b) {
|
|
121
|
+
if (a === b)
|
|
122
|
+
return false;
|
|
123
|
+
if (a === null)
|
|
124
|
+
return false;
|
|
125
|
+
if (b === null)
|
|
126
|
+
return true;
|
|
127
|
+
// Walk one node up the tree until they are both the same depth.
|
|
128
|
+
const aDepth = this.depth(a);
|
|
129
|
+
const bDepth = this.depth(b);
|
|
130
|
+
let aAn = a;
|
|
131
|
+
let bAn = b;
|
|
132
|
+
if (aDepth > bDepth) {
|
|
133
|
+
let lastSide;
|
|
134
|
+
for (let i = aDepth; i > bDepth; i--) {
|
|
135
|
+
lastSide = aAn.side;
|
|
136
|
+
aAn = aAn.parent;
|
|
137
|
+
}
|
|
138
|
+
if (aAn === b) {
|
|
139
|
+
// a is a descendant of b on lastSide.
|
|
140
|
+
return lastSide === "L";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (bDepth > aDepth) {
|
|
144
|
+
let lastSide;
|
|
145
|
+
for (let i = bDepth; i > aDepth; i--) {
|
|
146
|
+
lastSide = bAn.side;
|
|
147
|
+
bAn = bAn.parent;
|
|
148
|
+
}
|
|
149
|
+
if (bAn === a) {
|
|
150
|
+
// b is a descendant of a on lastSide.
|
|
151
|
+
return lastSide === "R";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Walk both nodes up the tree until we find a common ancestor.
|
|
155
|
+
while (aAn.parent !== bAn.parent) {
|
|
156
|
+
// If we reach the root, the loop will terminate, so both parents
|
|
157
|
+
// are non-null here.
|
|
158
|
+
aAn = aAn.parent;
|
|
159
|
+
bAn = bAn.parent;
|
|
160
|
+
}
|
|
161
|
+
// Now aAn and bAn are distinct siblings. See how they are sorted
|
|
162
|
+
// in their parent's child arrays.
|
|
163
|
+
if (aAn.side !== bAn.side)
|
|
164
|
+
return aAn.side === "L";
|
|
165
|
+
else {
|
|
166
|
+
const siblings = aAn.side === "L" ? aAn.parent.leftChildren : aAn.parent.rightChildren;
|
|
167
|
+
return siblings.indexOf(aAn) < siblings.indexOf(bAn);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Find the depth of a node in the tree, defined as the number of edges from the node to the root. The root has depth 0.
|
|
172
|
+
* @param node - the node whose depth to calculate
|
|
173
|
+
* @returns the depth of the node in the tree
|
|
174
|
+
*/
|
|
175
|
+
depth(node) {
|
|
176
|
+
let d = 0;
|
|
177
|
+
let n = node;
|
|
178
|
+
while (n.parent !== null) {
|
|
179
|
+
d++;
|
|
180
|
+
n = n.parent;
|
|
181
|
+
}
|
|
182
|
+
return d;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Update the size of a node and all its ancestors by adding delta.
|
|
186
|
+
* This should be called whenever a node is inserted or deleted to keep the size values accurate.
|
|
187
|
+
* @param node - the node whose size and ancestors' sizes to update
|
|
188
|
+
* @param delta - the amount to add to the size of the node and its ancestors (positive for insertions, negative for deletions)
|
|
189
|
+
*/
|
|
190
|
+
updateSize(node, delta) {
|
|
191
|
+
let an = node;
|
|
192
|
+
while (an !== null) {
|
|
193
|
+
an.size += delta;
|
|
194
|
+
an = an.parent;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get the node by index from a starting node, i.e. the node corresponding to the index-th non-deleted
|
|
199
|
+
* character in the subtree rooted at the starting node, where indices are 0-based.
|
|
200
|
+
* @param node - node to start the search from (e.g. the root for document-level indexing)
|
|
201
|
+
* @param index - the index of the node to retrieve among the non-deleted nodes in the subtree rooted at the starting node
|
|
202
|
+
* @returns the node corresponding to the index-th non-deleted character in the subtree rooted at the starting node
|
|
203
|
+
*/
|
|
204
|
+
getByIndex(node, index) {
|
|
205
|
+
if (index < 0 || index >= node.size) {
|
|
206
|
+
throw new Error(`Index out of bounds: ${index}`);
|
|
207
|
+
}
|
|
208
|
+
// Inorder traversal of the subtree, but using the size values to skip over deleted nodes and entire subtrees that are before the index.
|
|
209
|
+
let rem = index;
|
|
210
|
+
rec: while (true) {
|
|
211
|
+
for (const child of node.leftChildren) {
|
|
212
|
+
if (rem < child.size) {
|
|
213
|
+
node = child;
|
|
214
|
+
continue rec;
|
|
215
|
+
}
|
|
216
|
+
rem -= child.size;
|
|
217
|
+
}
|
|
218
|
+
if (!node.isDeleted) {
|
|
219
|
+
// If the current node is not deleted and rem is 0, we are at the node we want
|
|
220
|
+
// otherwise keep traversing, decrementing rem by 1 to account for the current node.
|
|
221
|
+
if (rem === 0)
|
|
222
|
+
return node;
|
|
223
|
+
rem--;
|
|
224
|
+
}
|
|
225
|
+
for (const child of node.rightChildren) {
|
|
226
|
+
if (rem < child.size) {
|
|
227
|
+
node = child;
|
|
228
|
+
continue rec;
|
|
229
|
+
}
|
|
230
|
+
rem -= child.size;
|
|
231
|
+
}
|
|
232
|
+
throw new Error("Index in range but not found");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get the index of a node disregarding the deleted nodes starting from the root
|
|
237
|
+
* @param node - the node whose index to calculate among the non-deleted nodes in the subtree rooted at the root
|
|
238
|
+
* @returns the index of the node among the non-deleted nodes in the subtree rooted at the root
|
|
239
|
+
*/
|
|
240
|
+
getVisibleIndex(node) {
|
|
241
|
+
let index = 0;
|
|
242
|
+
// Add the size of all visible nodes in our own left-side subtrees
|
|
243
|
+
// since they come before us in the document (inorder) order.
|
|
244
|
+
for (const left of node.leftChildren) {
|
|
245
|
+
index += left.size;
|
|
246
|
+
}
|
|
247
|
+
let curr = node;
|
|
248
|
+
while (curr.parent !== null) {
|
|
249
|
+
const parent = curr.parent;
|
|
250
|
+
if (curr.side === "R") {
|
|
251
|
+
// If we are on the right side of our parent:
|
|
252
|
+
// Everything in parent's leftChildren is before us
|
|
253
|
+
for (const left of parent.leftChildren)
|
|
254
|
+
index += left.size;
|
|
255
|
+
// The parent itself is before us (if not deleted)
|
|
256
|
+
if (!parent.isDeleted)
|
|
257
|
+
index += 1;
|
|
258
|
+
// Every right-sibling that is to our left in the array is before us
|
|
259
|
+
const sibIdx = parent.rightChildren.indexOf(curr);
|
|
260
|
+
for (let i = 0; i < sibIdx; i++) {
|
|
261
|
+
index += parent.rightChildren[i].size;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// If we are on the left side of our parent:
|
|
266
|
+
// Only siblings to our left in the leftChildren array are before us
|
|
267
|
+
const sibIdx = parent.leftChildren.indexOf(curr);
|
|
268
|
+
for (let i = 0; i < sibIdx; i++) {
|
|
269
|
+
index += parent.leftChildren[i].size;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
curr = parent;
|
|
273
|
+
}
|
|
274
|
+
return index;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Get the leftmost descendant of a node, i.e. the node corresponding to the first non-deleted character in the subtree
|
|
278
|
+
* rooted at the given node in document order.
|
|
279
|
+
* @param node - the node whose leftmost descendant to find
|
|
280
|
+
* @returns the leftmost descendant of the given node
|
|
281
|
+
*/
|
|
282
|
+
leftmostDescendant(node) {
|
|
283
|
+
let desc = node;
|
|
284
|
+
while (desc.leftChildren.length !== 0)
|
|
285
|
+
desc = desc.leftChildren[0];
|
|
286
|
+
return desc;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Get the next non-descendant of a node, i.e. the node corresponding to the next non-deleted character in document order
|
|
290
|
+
* that is not in the subtree rooted at the given node.
|
|
291
|
+
* @param node - the node whose next non-descendant to find
|
|
292
|
+
* @returns the next non-descendant of the given node, or null if there is no such node (i.e. the given node is the last non-deleted character in document order)
|
|
293
|
+
*/
|
|
294
|
+
nextNonDescendant(node) {
|
|
295
|
+
let current = node;
|
|
296
|
+
while (current.parent !== null) {
|
|
297
|
+
const siblings = current.side === "L" ? current.parent.leftChildren : current.parent.rightChildren;
|
|
298
|
+
const index = siblings.indexOf(current);
|
|
299
|
+
if (index < siblings.length - 1) {
|
|
300
|
+
// The next sibling's subtree immediately follows current's subtree.
|
|
301
|
+
// Find its leftmost element.
|
|
302
|
+
const nextSibling = siblings[index + 1];
|
|
303
|
+
return this.leftmostDescendant(nextSibling);
|
|
304
|
+
}
|
|
305
|
+
else if (current.side === "L") {
|
|
306
|
+
// The parent immediately follows current's subtree.
|
|
307
|
+
return current.parent;
|
|
308
|
+
}
|
|
309
|
+
current = current.parent;
|
|
310
|
+
}
|
|
311
|
+
// We've reached the root without finding any further-right subtrees.
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Traverse the subtree rooted at a node in document order, yielding the value of each non-deleted node.
|
|
316
|
+
* @param node - the node to traverse from
|
|
317
|
+
*/
|
|
318
|
+
*traverse(node) {
|
|
319
|
+
let current = node;
|
|
320
|
+
// Stack records the next child to visit for that node.
|
|
321
|
+
// We don't need to store node because we can infer it from the
|
|
322
|
+
// current node's parent etc.
|
|
323
|
+
const S = [{ side: "L", childIndex: 0 }];
|
|
324
|
+
while (true) {
|
|
325
|
+
const top = S[S.length - 1];
|
|
326
|
+
const children = top.side === "L" ? current.leftChildren : current.rightChildren;
|
|
327
|
+
if (top.childIndex === children.length) {
|
|
328
|
+
// We are done with the children on top.side.
|
|
329
|
+
if (top.side === "L") {
|
|
330
|
+
// Visit current, then move to right children.
|
|
331
|
+
if (!current.isDeleted)
|
|
332
|
+
yield current.value;
|
|
333
|
+
top.side = "R";
|
|
334
|
+
top.childIndex = 0;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
// Go to the parent.
|
|
338
|
+
if (current.parent === null)
|
|
339
|
+
return;
|
|
340
|
+
current = current.parent;
|
|
341
|
+
S.pop();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const child = children[top.childIndex];
|
|
346
|
+
// Save for later that we need to visit the next child.
|
|
347
|
+
top.childIndex++;
|
|
348
|
+
if (child.size > 0) {
|
|
349
|
+
// Traverse child.
|
|
350
|
+
current = child;
|
|
351
|
+
S.push({ side: "L", childIndex: 0 });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Serialize the tree into a Uint8Array. The tree is converted into a JSON object where each node is
|
|
358
|
+
* represented by its value, isDeleted flag, parent ID, side, size, and rightOrigin ID (if applicable).
|
|
359
|
+
* The JSON object is then converted to a string and encoded as a Uint8Array for storage or transmission.
|
|
360
|
+
* @returns a Uint8Array containing the serialized tree data
|
|
361
|
+
*/
|
|
362
|
+
save() {
|
|
363
|
+
// Convert nodesByID into JSON format, also converting each Node into a NodeSave.
|
|
364
|
+
const save = {};
|
|
365
|
+
for (const [sender, bySender] of this.nodes) {
|
|
366
|
+
save[sender] = bySender.map((node) => {
|
|
367
|
+
const nodeSave = {
|
|
368
|
+
value: node.value,
|
|
369
|
+
isDeleted: node.isDeleted,
|
|
370
|
+
parent: node.parent === null ? null : node.parent.id,
|
|
371
|
+
side: node.side,
|
|
372
|
+
size: node.size,
|
|
373
|
+
};
|
|
374
|
+
if (node.rightOrigin !== undefined) {
|
|
375
|
+
nodeSave.rightOrigin = node.rightOrigin === null ? null : node.rightOrigin.id;
|
|
376
|
+
}
|
|
377
|
+
return nodeSave;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return new Uint8Array(Buffer.from(JSON.stringify(save)));
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Load the tree from a Uint8Array containing serialized tree data in the format produced by the save() method.
|
|
384
|
+
* The data is parsed from JSON format, and the tree is reconstructed by first creating all nodes without setting their parent or rightOrigin pointers,
|
|
385
|
+
* then filling in the parent and rightOrigin pointers, and finally calling insertIntoSiblings on each node to reconstruct the children arrays.
|
|
386
|
+
* @param saveData - a Uint8Array containing the serialized tree data to load
|
|
387
|
+
*/
|
|
388
|
+
load(saveData) {
|
|
389
|
+
const save = JSON.parse(new TextDecoder().decode(saveData));
|
|
390
|
+
// First create all nodes without pointers to other nodes (parent, children,
|
|
391
|
+
// rightOrigin).
|
|
392
|
+
for (const [sender, bySenderSave] of Object.entries(save)) {
|
|
393
|
+
if (sender === "") {
|
|
394
|
+
// Root node. Just set its size.
|
|
395
|
+
this.root.size = bySenderSave[0].size;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
this.nodes.set(sender, bySenderSave.map((nodeSave, counter) => ({
|
|
399
|
+
id: { sender, counter },
|
|
400
|
+
parent: null,
|
|
401
|
+
value: nodeSave.value,
|
|
402
|
+
isDeleted: nodeSave.isDeleted,
|
|
403
|
+
side: nodeSave.side,
|
|
404
|
+
size: nodeSave.size,
|
|
405
|
+
leftChildren: [],
|
|
406
|
+
rightChildren: [],
|
|
407
|
+
})));
|
|
408
|
+
}
|
|
409
|
+
// Next, fill in the parent and rightOrigin pointers.
|
|
410
|
+
for (const [sender, bySender] of this.nodes) {
|
|
411
|
+
if (sender === "")
|
|
412
|
+
continue;
|
|
413
|
+
const bySenderSave = save[sender];
|
|
414
|
+
for (let i = 0; i < bySender.length; i++) {
|
|
415
|
+
const node = bySender[i];
|
|
416
|
+
const nodeSave = bySenderSave[i];
|
|
417
|
+
if (nodeSave.parent !== null) {
|
|
418
|
+
node.parent = this.getByID(nodeSave.parent);
|
|
419
|
+
}
|
|
420
|
+
if (nodeSave.rightOrigin !== undefined) {
|
|
421
|
+
node.rightOrigin = nodeSave.rightOrigin === null ? null : this.getByID(nodeSave.rightOrigin);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Finally, call insertIntoSiblings on each node to fill in the children
|
|
426
|
+
// arrays.
|
|
427
|
+
// We must be careful to wait until after doing so for node.rightOrigin
|
|
428
|
+
// and its ancestors, since insertIntoSiblings references the existing list order
|
|
429
|
+
// on node.rightOrigin.
|
|
430
|
+
// Nodes go from "pending" -> "ready" (rightOrigin valid) ->
|
|
431
|
+
// "valid" (insertIntoSiblings called).
|
|
432
|
+
// readyNodes is a stack; pendingNodes maps from a node to its dependencies.
|
|
433
|
+
const readyNodes = [];
|
|
434
|
+
const pendingNodes = new Map();
|
|
435
|
+
for (const [sender, bySender] of this.nodes) {
|
|
436
|
+
if (sender === "")
|
|
437
|
+
continue;
|
|
438
|
+
for (let i = 0; i < bySender.length; i++) {
|
|
439
|
+
const node = bySender[i];
|
|
440
|
+
if (node.rightOrigin === undefined || node.rightOrigin === null) {
|
|
441
|
+
// rightOrigin not used or is the root; node is ready.
|
|
442
|
+
readyNodes.push(node);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
let pendingArr = pendingNodes.get(node.rightOrigin);
|
|
446
|
+
if (pendingArr === undefined) {
|
|
447
|
+
pendingArr = [];
|
|
448
|
+
pendingNodes.set(node.rightOrigin, pendingArr);
|
|
449
|
+
}
|
|
450
|
+
pendingArr.push(node);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
while (readyNodes.length !== 0) {
|
|
455
|
+
const node = readyNodes.pop();
|
|
456
|
+
this.insertIntoSiblings(node);
|
|
457
|
+
// node's dependencies are now ready.
|
|
458
|
+
const deps = pendingNodes.get(node);
|
|
459
|
+
if (deps !== undefined)
|
|
460
|
+
readyNodes.push(...deps);
|
|
461
|
+
pendingNodes.delete(node);
|
|
462
|
+
}
|
|
463
|
+
if (pendingNodes.size !== 0) {
|
|
464
|
+
throw new Error("Failed to validate all nodes");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { FugueMessage } from "../../types/FugueTree/Message.js";
|
|
2
|
+
import { FNode, ID } from "./FTree.js";
|
|
3
|
+
/**
|
|
4
|
+
* A Fugue Tree CRDT, with insert and delete operations.
|
|
5
|
+
*/
|
|
6
|
+
export declare class FugueTree {
|
|
7
|
+
private counter;
|
|
8
|
+
private tree;
|
|
9
|
+
ws: WebSocket | null;
|
|
10
|
+
documentID: string;
|
|
11
|
+
readonly replicaID: string;
|
|
12
|
+
userIdentity: string;
|
|
13
|
+
pendingMsgs: Map<string, FugueMessage>;
|
|
14
|
+
readonly batchSize = 100;
|
|
15
|
+
constructor(ws: WebSocket | null, documentID: string, userIdentity: string);
|
|
16
|
+
/**
|
|
17
|
+
* Make msg key for pending messages map
|
|
18
|
+
* @param msg - the message to make key for
|
|
19
|
+
* @returns the key for the message in pending messages map
|
|
20
|
+
*/
|
|
21
|
+
private makeMsgKey;
|
|
22
|
+
/**
|
|
23
|
+
* Propagates message or messages to replicas
|
|
24
|
+
* @param msg - Message or messages to propagate to replicas
|
|
25
|
+
*/
|
|
26
|
+
private propagate;
|
|
27
|
+
/**
|
|
28
|
+
* Inserts a value at the given index and returns the corresponding FugueMessage.
|
|
29
|
+
* @param index - the index to insert the value at
|
|
30
|
+
* @param value - the value to insert
|
|
31
|
+
* @returns the FugueMessage representing the insert operation
|
|
32
|
+
*/
|
|
33
|
+
private insertImpl;
|
|
34
|
+
/**
|
|
35
|
+
* Inserts multiple values starting at the given index. This is optimized for batch inserts, such as pasting a large chunk of text.
|
|
36
|
+
* @param index - the index to start inserting values at
|
|
37
|
+
* @param values - the string of values to insert, where each character is inserted as a separate node in the tree
|
|
38
|
+
*/
|
|
39
|
+
insertMultiple(index: number, values: string): FugueMessage[];
|
|
40
|
+
/**
|
|
41
|
+
* Inserts a value at the given index and propagates the corresponding FugueMessage to replicas.
|
|
42
|
+
* @param index - the index to insert the value at
|
|
43
|
+
* @param value - the value to insert
|
|
44
|
+
*/
|
|
45
|
+
insert(index: number, value: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Deletes the value at the given index and returns the corresponding FugueMessage.
|
|
48
|
+
* @param index - the index to delete the value at
|
|
49
|
+
* @returns the FugueMessage representing the delete operation
|
|
50
|
+
*/
|
|
51
|
+
private deleteImpl;
|
|
52
|
+
/**
|
|
53
|
+
* Deletes multiple values starting at the given index. This is optimized for batch deletes, such as deleting a large chunk of text.
|
|
54
|
+
* @param index - the index to start deleting values at
|
|
55
|
+
* @param length - the number of characters to delete, starting from the index
|
|
56
|
+
*/
|
|
57
|
+
deleteMultiple(index: number, length: number): FugueMessage[];
|
|
58
|
+
/**
|
|
59
|
+
* Deletes the value at the given index and propagates the corresponding FugueMessage to replicas.
|
|
60
|
+
* @param index - the index to delete the value at
|
|
61
|
+
*/
|
|
62
|
+
delete(index: number): void;
|
|
63
|
+
/**
|
|
64
|
+
* Applies a FugueMessage to the tree. Returns true if the message was successfully applied,
|
|
65
|
+
* or false if it could not be applied due to missing dependencies (e.g. parent node for an insert).
|
|
66
|
+
* @param msg - the FugueMessage to apply to the tree
|
|
67
|
+
*/
|
|
68
|
+
private applyToTree;
|
|
69
|
+
/**
|
|
70
|
+
* Processes pending messages that may now be applicable after applying new messages.
|
|
71
|
+
* This is called after successfully applying new messages, to check if any pending messages can now be applied due to their dependencies being satisfied.
|
|
72
|
+
* @param applied - the list of messages that were just applied, which may have satisfied dependencies for pending messages
|
|
73
|
+
*/
|
|
74
|
+
private processPending;
|
|
75
|
+
/**
|
|
76
|
+
* Applies effect messages to the list
|
|
77
|
+
* @param msg - Message or messages to apply effect for, can be batched
|
|
78
|
+
* @returns the list of messages that were successfully applied
|
|
79
|
+
*/
|
|
80
|
+
effect(msg: FugueMessage | FugueMessage[]): FugueMessage[];
|
|
81
|
+
/**
|
|
82
|
+
* Gets the value at the given index in the visible string.
|
|
83
|
+
* @param index - the index to get the value at, where the index is based on the visible string
|
|
84
|
+
* @returns the value at the given index in the visible string
|
|
85
|
+
*/
|
|
86
|
+
get(index: number): string;
|
|
87
|
+
/**
|
|
88
|
+
* Gets the length of the visible string, which is the number of non-deleted nodes in the tree.
|
|
89
|
+
* @returns the length of the visible string
|
|
90
|
+
*/
|
|
91
|
+
length(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Returns the visible string by traversing the tree and concatenating the values of non-deleted nodes.
|
|
94
|
+
* @returns the visible string represented by the tree, which is the concatenation of values of non-deleted nodes in traversal order
|
|
95
|
+
*/
|
|
96
|
+
observe(): string;
|
|
97
|
+
/**
|
|
98
|
+
* Serializes the tree into a Uint8Array.
|
|
99
|
+
* @returns a Uint8Array representing the serialized tree.
|
|
100
|
+
*/
|
|
101
|
+
save(): Uint8Array;
|
|
102
|
+
/**
|
|
103
|
+
* Loads the tree from a Uint8Array. This replaces the current tree with the loaded tree.
|
|
104
|
+
* @param data - a Uint8Array representing the serialized tree to load.
|
|
105
|
+
*/
|
|
106
|
+
load(data: Uint8Array | null): void;
|
|
107
|
+
/**
|
|
108
|
+
* Gets the replica ID of this FugueTree instance, which is a unique identifier for this replica in the distributed system.
|
|
109
|
+
* The replica ID is used in messages to identify the source of operations and to ensure that operations from the same replica
|
|
110
|
+
* are applied in order.
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
replicaId(): string;
|
|
114
|
+
/**
|
|
115
|
+
* Gets the FNode corresponding to the given ID.
|
|
116
|
+
* @param id - the ID of the node to retrieve
|
|
117
|
+
* @returns the FNode corresponding to the given ID, or null if no such node exists in the tree
|
|
118
|
+
*/
|
|
119
|
+
getById(id: ID): FNode;
|
|
120
|
+
/**
|
|
121
|
+
* Gets the index of the given node in the visible string.
|
|
122
|
+
* @param node - the FNode to get the visible index of
|
|
123
|
+
* @returns the index of the given node in the visible string
|
|
124
|
+
*/
|
|
125
|
+
getVisibleIndex(node: FNode): number;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=FugueTree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FugueTree.d.ts","sourceRoot":"","sources":["../../../src/dts/FugueTree/FugueTree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAa,MAAM,kCAAkC,CAAC;AAG3E,OAAO,EAAE,KAAK,EAAS,EAAE,EAAE,MAAM,YAAY,CAAC;AAE9C;;GAEG;AACH,qBAAa,SAAS;IAClB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,IAAI,CAAQ;IACpB,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,SAAS,SAAmB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,4BAAmC;IAE9C,QAAQ,CAAC,SAAS,OAAO;gBAEb,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;IAO1E;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAIlB;;;OAGG;IACH,OAAO,CAAC,SAAS;IAQjB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IA+ClB;;;;OAIG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IA+B5C;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAOnC;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAsBlB;;;;OAIG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IA4B5C;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM;IAKpB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA8BnB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAkBtB;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,YAAY,GAAG,YAAY,EAAE,GAAG,YAAY,EAAE;IAuB1D;;;;OAIG;IACH,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAS1B;;;OAGG;IACH,MAAM,IAAI,MAAM;IAIhB;;;OAGG;IACH,OAAO,IAAI,MAAM;IAWjB;;;OAGG;IACH,IAAI,IAAI,UAAU;IAKlB;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAK5B;;;;;OAKG;IACH,SAAS,IAAI,MAAM;IAInB;;;;OAIG;IACH,OAAO,CAAC,EAAE,EAAE,EAAE;IAId;;;;OAIG;IACH,eAAe,CAAC,IAAI,EAAE,KAAK;CAG9B"}
|