@cr_docs_t/dts 0.34.0 → 0.35.1
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/FugueTree/FTree.d.ts +0 -1
- package/dist/dts/FugueTree/FTree.d.ts.map +1 -1
- package/dist/dts/FugueTree/FTree.js +3 -3
- package/dist/dts/FugueTree/FugueTree.d.ts +4 -1
- package/dist/dts/FugueTree/FugueTree.d.ts.map +1 -1
- package/dist/dts/FugueTree/FugueTree.js +9 -5
- package/dist/tests/unit/AST.test.d.ts +2 -0
- package/dist/tests/unit/AST.test.d.ts.map +1 -0
- package/dist/tests/unit/AST.test.js +111 -0
- package/dist/tests/unit/FTree.test.d.ts +2 -0
- package/dist/tests/unit/FTree.test.d.ts.map +1 -0
- package/dist/tests/unit/FTree.test.js +447 -0
- package/dist/tests/unit/FugueTree.test.d.ts.map +1 -0
- package/dist/tests/unit/FugueTree.test.js +416 -0
- package/dist/tests/unit/mocks/BragiAST-mocks.d.ts +2 -0
- package/dist/tests/unit/mocks/BragiAST-mocks.d.ts.map +1 -0
- package/dist/tests/unit/mocks/BragiAST-mocks.js +1 -0
- package/dist/tests/unit/mocks/FTree-mocks.d.ts +20 -0
- package/dist/tests/unit/mocks/FTree-mocks.d.ts.map +1 -0
- package/dist/tests/unit/mocks/FTree-mocks.js +36 -0
- package/dist/tests/unit/mocks/FugueTree-mocks.d.ts +24 -0
- package/dist/tests/unit/mocks/FugueTree-mocks.d.ts.map +1 -0
- package/dist/tests/unit/mocks/FugueTree-mocks.js +52 -0
- package/dist/tests/utils.d.ts +9 -0
- package/dist/tests/utils.d.ts.map +1 -0
- package/dist/tests/utils.js +47 -0
- package/dist/treesitter/index.d.ts +1 -0
- package/dist/treesitter/index.d.ts.map +1 -1
- package/dist/treesitter/index.js +1 -0
- package/dist/treesitter/utils.d.ts +9 -0
- package/dist/treesitter/utils.d.ts.map +1 -0
- package/dist/treesitter/utils.js +84 -0
- package/dist/types/Models/Schema.d.ts +1 -1
- package/dist/types/Models/Schema.js +1 -1
- package/dist/wasm/tree-sitter-latex.wasm +0 -0
- package/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +16 -4
- package/dist/tests/FugueList.test.d.ts +0 -2
- package/dist/tests/FugueList.test.d.ts.map +0 -1
- package/dist/tests/FugueList.test.js +0 -8
- package/dist/tests/FugueTree.test.d.ts.map +0 -1
- package/dist/tests/FugueTree.test.js +0 -8
- package/dist/tests/mocks.d.ts +0 -4
- package/dist/tests/mocks.d.ts.map +0 -1
- package/dist/tests/mocks.js +0 -4
- /package/dist/tests/{FugueTree.test.d.ts → unit/FugueTree.test.d.ts} +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { describe, test, expect } from "@jest/globals";
|
|
2
|
+
import { Operation, MessageType } from "../../types/index.js";
|
|
3
|
+
import { makeFugueTree, treeWithText, makeForeignInsertMsg, makeForeignDeleteMsg, DOC_ID, USER_ID, } from "./mocks/FugueTree-mocks.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// observe / length
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
describe("FugueTree", () => {
|
|
8
|
+
describe("observe", () => {
|
|
9
|
+
test("empty tree has an empty string", () => {
|
|
10
|
+
expect(makeFugueTree().observe()).toBe("");
|
|
11
|
+
});
|
|
12
|
+
test("observe returns the full inserted text in order", () => {
|
|
13
|
+
const tree = treeWithText("hello");
|
|
14
|
+
expect(tree.observe()).toBe("hello");
|
|
15
|
+
});
|
|
16
|
+
test("observe reflects characters deleted from the middle", () => {
|
|
17
|
+
const tree = treeWithText("hello");
|
|
18
|
+
tree.delete(2); // remove 'l' at index 2
|
|
19
|
+
expect(tree.observe()).toBe("helo");
|
|
20
|
+
});
|
|
21
|
+
test("observe is empty after deleting all characters", () => {
|
|
22
|
+
const tree = treeWithText("hi");
|
|
23
|
+
tree.delete(0);
|
|
24
|
+
tree.delete(0);
|
|
25
|
+
expect(tree.observe()).toBe("");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
// length
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
describe("length", () => {
|
|
32
|
+
test("empty tree has length 0", () => {
|
|
33
|
+
expect(makeFugueTree().length()).toBe(0);
|
|
34
|
+
});
|
|
35
|
+
test("length matches number of inserted characters", () => {
|
|
36
|
+
const tree = treeWithText("abc");
|
|
37
|
+
expect(tree.length()).toBe(3);
|
|
38
|
+
});
|
|
39
|
+
test("length decrements after a delete", () => {
|
|
40
|
+
const tree = treeWithText("abc");
|
|
41
|
+
tree.delete(1);
|
|
42
|
+
expect(tree.length()).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// -----------------------------------------------------------------------
|
|
46
|
+
// get
|
|
47
|
+
// -----------------------------------------------------------------------
|
|
48
|
+
describe("get", () => {
|
|
49
|
+
test("returns the character at the given visible index", () => {
|
|
50
|
+
const tree = treeWithText("abc");
|
|
51
|
+
expect(tree.get(0)).toBe("a");
|
|
52
|
+
expect(tree.get(1)).toBe("b");
|
|
53
|
+
expect(tree.get(2)).toBe("c");
|
|
54
|
+
});
|
|
55
|
+
test("throws when index is negative", () => {
|
|
56
|
+
const tree = treeWithText("abc");
|
|
57
|
+
expect(() => tree.get(-1)).toThrow();
|
|
58
|
+
});
|
|
59
|
+
test("throws when index equals length", () => {
|
|
60
|
+
const tree = treeWithText("abc");
|
|
61
|
+
expect(() => tree.get(3)).toThrow();
|
|
62
|
+
});
|
|
63
|
+
test("returns correct character after a deletion shifts visible indices", () => {
|
|
64
|
+
const tree = treeWithText("abc");
|
|
65
|
+
tree.delete(0); // removes 'a'; visible string is now "bc"
|
|
66
|
+
expect(tree.get(0)).toBe("b");
|
|
67
|
+
expect(tree.get(1)).toBe("c");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// -----------------------------------------------------------------------
|
|
71
|
+
// insert (single character)
|
|
72
|
+
// -----------------------------------------------------------------------
|
|
73
|
+
describe("insert", () => {
|
|
74
|
+
test("returns a FugueMessage with INSERT operation", () => {
|
|
75
|
+
const tree = makeFugueTree();
|
|
76
|
+
const msg = tree.insert(0, "x");
|
|
77
|
+
expect(msg.operation).toBe(Operation.INSERT);
|
|
78
|
+
expect(msg.msgType).toBe(MessageType.FUGUE);
|
|
79
|
+
});
|
|
80
|
+
test("returned message carries the inserted character", () => {
|
|
81
|
+
const tree = makeFugueTree();
|
|
82
|
+
const msg = tree.insert(0, "z");
|
|
83
|
+
expect(msg.data).toBe("z");
|
|
84
|
+
});
|
|
85
|
+
test("returned message contains the tree's replicaId", () => {
|
|
86
|
+
const tree = makeFugueTree();
|
|
87
|
+
const msg = tree.insert(0, "a");
|
|
88
|
+
expect(msg.replicaId).toBe(tree.replicaId());
|
|
89
|
+
});
|
|
90
|
+
test("returned message contains the correct documentID and userIdentity", () => {
|
|
91
|
+
const tree = makeFugueTree();
|
|
92
|
+
const msg = tree.insert(0, "a");
|
|
93
|
+
expect(msg.documentID).toBe(DOC_ID);
|
|
94
|
+
expect(msg.userIdentity).toBe(USER_ID);
|
|
95
|
+
});
|
|
96
|
+
test("inserting at index 0 prepends the character", () => {
|
|
97
|
+
const tree = treeWithText("bc");
|
|
98
|
+
tree.insert(0, "a");
|
|
99
|
+
expect(tree.observe()).toBe("abc");
|
|
100
|
+
});
|
|
101
|
+
test("inserting at the end appends the character", () => {
|
|
102
|
+
const tree = treeWithText("ab");
|
|
103
|
+
tree.insert(2, "c");
|
|
104
|
+
expect(tree.observe()).toBe("abc");
|
|
105
|
+
});
|
|
106
|
+
test("inserting in the middle places the character correctly", () => {
|
|
107
|
+
const tree = treeWithText("ac");
|
|
108
|
+
tree.insert(1, "b");
|
|
109
|
+
expect(tree.observe()).toBe("abc");
|
|
110
|
+
});
|
|
111
|
+
test("counter increments with each insert so IDs are unique", () => {
|
|
112
|
+
const tree = makeFugueTree();
|
|
113
|
+
const msg1 = tree.insert(0, "a");
|
|
114
|
+
const msg2 = tree.insert(1, "b");
|
|
115
|
+
expect(msg2.id.counter).toBeGreaterThan(msg1.id.counter);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// -----------------------------------------------------------------------
|
|
119
|
+
// insertMultiple
|
|
120
|
+
// -----------------------------------------------------------------------
|
|
121
|
+
describe("insertMultiple", () => {
|
|
122
|
+
test("inserts each character and returns one message per character", () => {
|
|
123
|
+
const tree = makeFugueTree();
|
|
124
|
+
const msgs = tree.insertMultiple(0, "hello");
|
|
125
|
+
expect(msgs).toHaveLength(5);
|
|
126
|
+
expect(tree.observe()).toBe("hello");
|
|
127
|
+
});
|
|
128
|
+
test("each returned message has a unique id counter", () => {
|
|
129
|
+
const tree = makeFugueTree();
|
|
130
|
+
const msgs = tree.insertMultiple(0, "abc");
|
|
131
|
+
const counters = msgs.map((m) => m.id.counter);
|
|
132
|
+
expect(new Set(counters).size).toBe(3);
|
|
133
|
+
});
|
|
134
|
+
test("returns an empty array when given an empty string", () => {
|
|
135
|
+
const tree = makeFugueTree();
|
|
136
|
+
const msgs = tree.insertMultiple(0, "");
|
|
137
|
+
expect(msgs).toHaveLength(0);
|
|
138
|
+
expect(tree.observe()).toBe("");
|
|
139
|
+
});
|
|
140
|
+
test("inserting multiple characters in the middle produces correct order", () => {
|
|
141
|
+
const tree = treeWithText("ad");
|
|
142
|
+
tree.insertMultiple(1, "bc");
|
|
143
|
+
expect(tree.observe()).toBe("abcd");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// -----------------------------------------------------------------------
|
|
147
|
+
// delete (single character)
|
|
148
|
+
// -----------------------------------------------------------------------
|
|
149
|
+
describe("delete", () => {
|
|
150
|
+
test("removing the only character results in an empty document", () => {
|
|
151
|
+
const tree = treeWithText("x");
|
|
152
|
+
tree.delete(0);
|
|
153
|
+
expect(tree.observe()).toBe("");
|
|
154
|
+
expect(tree.length()).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
test("deleting the first character of several is correct", () => {
|
|
157
|
+
const tree = treeWithText("abc");
|
|
158
|
+
tree.delete(0);
|
|
159
|
+
expect(tree.observe()).toBe("bc");
|
|
160
|
+
});
|
|
161
|
+
test("deleting the last character is correct", () => {
|
|
162
|
+
const tree = treeWithText("abc");
|
|
163
|
+
tree.delete(2);
|
|
164
|
+
expect(tree.observe()).toBe("ab");
|
|
165
|
+
});
|
|
166
|
+
test("deleting the same index twice reflects the shifted visible string", () => {
|
|
167
|
+
const tree = treeWithText("abc");
|
|
168
|
+
tree.delete(0); // removes 'a'
|
|
169
|
+
tree.delete(0); // removes 'b' (now at visible index 0)
|
|
170
|
+
expect(tree.observe()).toBe("c");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// -----------------------------------------------------------------------
|
|
174
|
+
// deleteMultiple
|
|
175
|
+
// -----------------------------------------------------------------------
|
|
176
|
+
describe("deleteMultiple", () => {
|
|
177
|
+
test("deletes the specified number of characters and returns one message per deletion", () => {
|
|
178
|
+
const tree = treeWithText("hello");
|
|
179
|
+
const msgs = tree.deleteMultiple(1, 3);
|
|
180
|
+
expect(msgs).toHaveLength(3);
|
|
181
|
+
expect(tree.observe()).toBe("ho");
|
|
182
|
+
});
|
|
183
|
+
test("deleting length 0 returns an empty array and leaves the document unchanged", () => {
|
|
184
|
+
const tree = treeWithText("abc");
|
|
185
|
+
const msgs = tree.deleteMultiple(0, 0);
|
|
186
|
+
expect(msgs).toHaveLength(0);
|
|
187
|
+
expect(tree.observe()).toBe("abc");
|
|
188
|
+
});
|
|
189
|
+
test("all returned messages have DELETE operation", () => {
|
|
190
|
+
const tree = treeWithText("abc");
|
|
191
|
+
const msgs = tree.deleteMultiple(0, 2);
|
|
192
|
+
msgs.forEach((msg) => expect(msg.operation).toBe(Operation.DELETE));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// -----------------------------------------------------------------------
|
|
196
|
+
// effect — applying remote messages
|
|
197
|
+
// -----------------------------------------------------------------------
|
|
198
|
+
describe("effect", () => {
|
|
199
|
+
test("applying a remote INSERT from a different replica updates the document", () => {
|
|
200
|
+
const tree = makeFugueTree();
|
|
201
|
+
const foreignId = "foreign-replica";
|
|
202
|
+
const msg = makeForeignInsertMsg(foreignId, 0, "x");
|
|
203
|
+
const applied = tree.effect(msg);
|
|
204
|
+
expect(applied).toHaveLength(1);
|
|
205
|
+
expect(tree.observe()).toBe("x");
|
|
206
|
+
});
|
|
207
|
+
test("messages from this replica are skipped", () => {
|
|
208
|
+
const tree = makeFugueTree();
|
|
209
|
+
// Build a message that looks like it came from the same replicaId
|
|
210
|
+
const selfMsg = makeForeignInsertMsg(tree.replicaId(), 0, "x");
|
|
211
|
+
const applied = tree.effect(selfMsg);
|
|
212
|
+
expect(applied).toHaveLength(0);
|
|
213
|
+
expect(tree.observe()).toBe("");
|
|
214
|
+
});
|
|
215
|
+
test("accepts an array of messages and returns all successfully applied ones", () => {
|
|
216
|
+
const tree = makeFugueTree();
|
|
217
|
+
const msgs = [makeForeignInsertMsg("replica-b", 0, "a"), makeForeignInsertMsg("replica-b", 1, "b")];
|
|
218
|
+
// counter 1 depends on counter 0 being the parent — both are right children of root, so both apply
|
|
219
|
+
const applied = tree.effect(msgs);
|
|
220
|
+
expect(applied.length).toBeGreaterThanOrEqual(1);
|
|
221
|
+
});
|
|
222
|
+
test("duplicate pending messages are not stored twice", () => {
|
|
223
|
+
const tree = makeFugueTree();
|
|
224
|
+
// A message that references a non-existent parent will be deferred
|
|
225
|
+
const orphanMsg = {
|
|
226
|
+
...makeForeignInsertMsg("rep-x", 0, "z"),
|
|
227
|
+
parent: { sender: "ghost", counter: 99 },
|
|
228
|
+
};
|
|
229
|
+
tree.effect(orphanMsg);
|
|
230
|
+
tree.effect(orphanMsg); // second call with same key
|
|
231
|
+
// pendingMsgs should have exactly one entry
|
|
232
|
+
expect(tree.pendingMsgs.size).toBe(1);
|
|
233
|
+
});
|
|
234
|
+
test("pending messages are applied once their dependency arrives", () => {
|
|
235
|
+
const tree = makeFugueTree();
|
|
236
|
+
// msg2 references msg1 as parent, so msg1 must arrive first
|
|
237
|
+
const msg1 = makeForeignInsertMsg("rep-a", 0, "A");
|
|
238
|
+
const msg2 = {
|
|
239
|
+
...makeForeignInsertMsg("rep-a", 1, "B"),
|
|
240
|
+
parent: { sender: "rep-a", counter: 0 },
|
|
241
|
+
side: "R",
|
|
242
|
+
};
|
|
243
|
+
// Send msg2 first — it can't be applied yet
|
|
244
|
+
tree.effect(msg2);
|
|
245
|
+
expect(tree.pendingMsgs.size).toBe(1);
|
|
246
|
+
expect(tree.observe()).toBe("");
|
|
247
|
+
// Now send msg1 — msg2 should be resolved automatically
|
|
248
|
+
tree.effect(msg1);
|
|
249
|
+
expect(tree.pendingMsgs.size).toBe(0);
|
|
250
|
+
expect(tree.observe()).toContain("A");
|
|
251
|
+
});
|
|
252
|
+
test("applying a remote DELETE removes the character from the visible string", () => {
|
|
253
|
+
// Insert locally first, then simulate a remote delete of the same node
|
|
254
|
+
const tree = makeFugueTree();
|
|
255
|
+
const insertMsg = tree.insert(0, "x");
|
|
256
|
+
// Build a foreign delete targeting the node just inserted
|
|
257
|
+
const delMsg = makeForeignDeleteMsg("other-replica", insertMsg.id);
|
|
258
|
+
// Override replicaId to something different from the tree's own ID
|
|
259
|
+
const foreignDel = { ...delMsg, replicaId: "other-replica" };
|
|
260
|
+
tree.effect(foreignDel);
|
|
261
|
+
expect(tree.observe()).toBe("");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// -----------------------------------------------------------------------
|
|
265
|
+
// getById / getVisibleIndex
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
describe("getById", () => {
|
|
268
|
+
test("returns the node corresponding to an inserted character's ID", () => {
|
|
269
|
+
const tree = makeFugueTree();
|
|
270
|
+
const msg = tree.insert(0, "q");
|
|
271
|
+
const node = tree.getById(msg.id);
|
|
272
|
+
expect(node.value).toBe("q");
|
|
273
|
+
});
|
|
274
|
+
test("throws for an unknown ID", () => {
|
|
275
|
+
const tree = makeFugueTree();
|
|
276
|
+
expect(() => tree.getById({ sender: "nobody", counter: 99 })).toThrow();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe("getVisibleIndex", () => {
|
|
280
|
+
test("returns the correct index of an inserted node", () => {
|
|
281
|
+
const tree = treeWithText("abc");
|
|
282
|
+
const msgC = tree.insert(3, "d"); // 'd' is now at visible index 3
|
|
283
|
+
const node = tree.getById(msgC.id);
|
|
284
|
+
expect(tree.getVisibleIndex(node)).toBe(3);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
// -----------------------------------------------------------------------
|
|
288
|
+
// replicaId
|
|
289
|
+
// -----------------------------------------------------------------------
|
|
290
|
+
describe("replicaId", () => {
|
|
291
|
+
test("replicaId returns a non-empty string", () => {
|
|
292
|
+
const tree = makeFugueTree();
|
|
293
|
+
expect(typeof tree.replicaId()).toBe("string");
|
|
294
|
+
expect(tree.replicaId().length).toBeGreaterThan(0);
|
|
295
|
+
});
|
|
296
|
+
test("two different FugueTree instances have different replicaIds", () => {
|
|
297
|
+
const treeA = makeFugueTree();
|
|
298
|
+
const treeB = makeFugueTree();
|
|
299
|
+
expect(treeA.replicaId()).not.toBe(treeB.replicaId());
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// -----------------------------------------------------------------------
|
|
303
|
+
// save / load round-trip
|
|
304
|
+
// -----------------------------------------------------------------------
|
|
305
|
+
describe("save / load", () => {
|
|
306
|
+
test("save returns a non-empty Uint8Array", () => {
|
|
307
|
+
const tree = treeWithText("hello");
|
|
308
|
+
const bytes = tree.save();
|
|
309
|
+
expect(bytes).toBeInstanceOf(Uint8Array);
|
|
310
|
+
expect(bytes.length).toBeGreaterThan(0);
|
|
311
|
+
});
|
|
312
|
+
test("loading saved state into a new tree reproduces the same document", () => {
|
|
313
|
+
const original = treeWithText("hello world");
|
|
314
|
+
const bytes = original.save();
|
|
315
|
+
const restored = makeFugueTree();
|
|
316
|
+
restored.load(bytes);
|
|
317
|
+
expect(restored.observe()).toBe("hello world");
|
|
318
|
+
});
|
|
319
|
+
test("loading null is a no-op and leaves the tree unchanged", () => {
|
|
320
|
+
const tree = treeWithText("abc");
|
|
321
|
+
tree.load(null);
|
|
322
|
+
expect(tree.observe()).toBe("abc");
|
|
323
|
+
});
|
|
324
|
+
test("save / load round-trip preserves deletions", () => {
|
|
325
|
+
const original = treeWithText("hello");
|
|
326
|
+
original.delete(0); // remove 'h'
|
|
327
|
+
const bytes = original.save();
|
|
328
|
+
const restored = makeFugueTree();
|
|
329
|
+
restored.load(bytes);
|
|
330
|
+
expect(restored.observe()).toBe("ello");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
// -----------------------------------------------------------------------
|
|
334
|
+
// traverse
|
|
335
|
+
// -----------------------------------------------------------------------
|
|
336
|
+
describe("traverse", () => {
|
|
337
|
+
test("iterating traverse yields the visible characters in document order", () => {
|
|
338
|
+
const tree = treeWithText("abc");
|
|
339
|
+
expect([...tree.traverse()]).toEqual(["a", "b", "c"]);
|
|
340
|
+
});
|
|
341
|
+
test("deleted characters are excluded from traverse", () => {
|
|
342
|
+
const tree = treeWithText("abc");
|
|
343
|
+
tree.delete(1); // remove 'b'
|
|
344
|
+
expect([...tree.traverse()]).toEqual(["a", "c"]);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
// -----------------------------------------------------------------------
|
|
348
|
+
// clear
|
|
349
|
+
// -----------------------------------------------------------------------
|
|
350
|
+
describe("clear", () => {
|
|
351
|
+
test("clear resets the document to an empty string", () => {
|
|
352
|
+
const tree = treeWithText("hello");
|
|
353
|
+
tree.clear();
|
|
354
|
+
expect(tree.observe()).toBe("");
|
|
355
|
+
expect(tree.length()).toBe(0);
|
|
356
|
+
});
|
|
357
|
+
test("insertions after clear work correctly", () => {
|
|
358
|
+
const tree = treeWithText("old");
|
|
359
|
+
tree.clear();
|
|
360
|
+
tree.insertMultiple(0, "new");
|
|
361
|
+
expect(tree.observe()).toBe("new");
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
// -----------------------------------------------------------------------
|
|
365
|
+
// nextNonDescendant (public proxy)
|
|
366
|
+
// -----------------------------------------------------------------------
|
|
367
|
+
describe("nextNonDescendant", () => {
|
|
368
|
+
test("returns null for the last character in the document", () => {
|
|
369
|
+
const tree = treeWithText("a");
|
|
370
|
+
const msg = tree.insert(0, "b"); // 'b' is now at index 1 (last)
|
|
371
|
+
const node = tree.getById(msg.id);
|
|
372
|
+
// The last node in the tree has no next non-descendant
|
|
373
|
+
const next = tree.nextNonDescendant(node);
|
|
374
|
+
// It may or may not be null depending on tree structure, but must not throw
|
|
375
|
+
expect(next === null || next !== undefined).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
// -----------------------------------------------------------------------
|
|
379
|
+
// Ordering / CRDT convergence
|
|
380
|
+
// -----------------------------------------------------------------------
|
|
381
|
+
describe("CRDT ordering guarantees", () => {
|
|
382
|
+
test("concurrent inserts at the same index from two replicas both survive", () => {
|
|
383
|
+
const replicaA = makeFugueTree();
|
|
384
|
+
const replicaB = makeFugueTree();
|
|
385
|
+
const msgA = replicaA.insert(0, "A");
|
|
386
|
+
const msgB = replicaB.insert(0, "B");
|
|
387
|
+
// Cross-apply
|
|
388
|
+
replicaA.effect({ ...msgB, replicaId: replicaB.replicaId() });
|
|
389
|
+
replicaB.effect({ ...msgA, replicaId: replicaA.replicaId() });
|
|
390
|
+
// Both replicas must have 2 characters
|
|
391
|
+
expect(replicaA.length()).toBe(2);
|
|
392
|
+
expect(replicaB.length()).toBe(2);
|
|
393
|
+
});
|
|
394
|
+
test("two replicas converge to the same string after exchanging all messages", () => {
|
|
395
|
+
const replicaA = makeFugueTree();
|
|
396
|
+
const replicaB = makeFugueTree();
|
|
397
|
+
const msgsA = replicaA.insertMultiple(0, "hello");
|
|
398
|
+
const msgsB = replicaB.insertMultiple(0, "world");
|
|
399
|
+
// Apply A's messages to B and vice-versa
|
|
400
|
+
replicaA.effect(msgsB.map((m) => ({ ...m, replicaId: replicaB.replicaId() })));
|
|
401
|
+
replicaB.effect(msgsA.map((m) => ({ ...m, replicaId: replicaA.replicaId() })));
|
|
402
|
+
expect(replicaA.observe()).toBe(replicaB.observe());
|
|
403
|
+
});
|
|
404
|
+
test("insert followed by delete yields the same result regardless of message delivery order", () => {
|
|
405
|
+
const replicaA = makeFugueTree();
|
|
406
|
+
const replicaB = makeFugueTree();
|
|
407
|
+
// A inserts 'x', then deletes it
|
|
408
|
+
const insertMsg = replicaA.insert(0, "x");
|
|
409
|
+
const deleteMsg = replicaA.deleteMultiple(0, 1)[0];
|
|
410
|
+
// B receives delete before insert (out-of-order)
|
|
411
|
+
replicaB.effect({ ...deleteMsg, replicaId: replicaA.replicaId() });
|
|
412
|
+
replicaB.effect({ ...insertMsg, replicaId: replicaA.replicaId() });
|
|
413
|
+
expect(replicaB.observe()).toBe("");
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BragiAST-mocks.d.ts","sourceRoot":"","sources":["../../../../src/tests/unit/mocks/BragiAST-mocks.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../utils.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FNode, ID, FTree } from "../../../dts/index.js";
|
|
2
|
+
export declare const ROOT_ID: ID;
|
|
3
|
+
/**
|
|
4
|
+
* Builds a minimal FNode without inserting it into any tree.
|
|
5
|
+
* Useful for constructing expected-value objects in assertions.
|
|
6
|
+
*/
|
|
7
|
+
export declare function makeNode(sender: string, counter: number, value: string | null, parent: FNode | null, side: "L" | "R", isDeleted?: boolean): FNode;
|
|
8
|
+
export declare const ID_A0: ID;
|
|
9
|
+
export declare const ID_A1: ID;
|
|
10
|
+
export declare const ID_A2: ID;
|
|
11
|
+
export declare const ID_B0: ID;
|
|
12
|
+
export declare const ID_B1: ID;
|
|
13
|
+
export declare const ID_C0: ID;
|
|
14
|
+
/**
|
|
15
|
+
* Inserts the string "hello" into a fresh FTree one character at a time
|
|
16
|
+
* under the root, right-side, no rightOrigin. Returns the tree and the
|
|
17
|
+
* five inserted nodes in insertion order.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildHelloTree(tree: FTree): ID[];
|
|
20
|
+
//# sourceMappingURL=FTree-mocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FTree-mocks.d.ts","sourceRoot":"","sources":["../../../../src/tests/unit/mocks/FTree-mocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAE9D,eAAO,MAAM,OAAO,EAAE,EAA+B,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,QAAQ,CACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,MAAM,EAAE,KAAK,GAAG,IAAI,EACpB,IAAI,EAAE,GAAG,GAAG,GAAG,EACf,SAAS,UAAQ,GAClB,KAAK,CAWP;AAED,eAAO,MAAM,KAAK,EAAE,EAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,EAAE,EAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,EAAE,EAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,EAAE,EAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,EAAE,EAAgC,CAAC;AACrD,eAAO,MAAM,KAAK,EAAE,EAAgC,CAAC;AAErD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,KAAK,QAOzC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const ROOT_ID = { sender: "", counter: 0 };
|
|
2
|
+
/**
|
|
3
|
+
* Builds a minimal FNode without inserting it into any tree.
|
|
4
|
+
* Useful for constructing expected-value objects in assertions.
|
|
5
|
+
*/
|
|
6
|
+
export function makeNode(sender, counter, value, parent, side, isDeleted = false) {
|
|
7
|
+
return {
|
|
8
|
+
id: { sender, counter },
|
|
9
|
+
value,
|
|
10
|
+
isDeleted,
|
|
11
|
+
parent,
|
|
12
|
+
side,
|
|
13
|
+
leftChildren: [],
|
|
14
|
+
rightChildren: [],
|
|
15
|
+
size: 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export const ID_A0 = { sender: "A", counter: 0 };
|
|
19
|
+
export const ID_A1 = { sender: "A", counter: 1 };
|
|
20
|
+
export const ID_A2 = { sender: "A", counter: 2 };
|
|
21
|
+
export const ID_B0 = { sender: "B", counter: 0 };
|
|
22
|
+
export const ID_B1 = { sender: "B", counter: 1 };
|
|
23
|
+
export const ID_C0 = { sender: "C", counter: 0 };
|
|
24
|
+
/**
|
|
25
|
+
* Inserts the string "hello" into a fresh FTree one character at a time
|
|
26
|
+
* under the root, right-side, no rightOrigin. Returns the tree and the
|
|
27
|
+
* five inserted nodes in insertion order.
|
|
28
|
+
*/
|
|
29
|
+
export function buildHelloTree(tree) {
|
|
30
|
+
const chars = ["h", "e", "l", "l", "o"];
|
|
31
|
+
const ids = chars.map((_, i) => ({ sender: "A", counter: i }));
|
|
32
|
+
for (let i = 0; i < chars.length; i++) {
|
|
33
|
+
tree.addNode(ids[i], chars[i], tree.root, "R");
|
|
34
|
+
}
|
|
35
|
+
return ids;
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FugueTree } from "../../../dts/index.js";
|
|
2
|
+
import { FugueMessage } from "../../../types/index.js";
|
|
3
|
+
export declare const DOC_ID = "test-doc";
|
|
4
|
+
export declare const USER_ID = "test-user";
|
|
5
|
+
/**
|
|
6
|
+
* A fresh FugueTree with no WebSocket and stable IDs for deterministic tests.
|
|
7
|
+
*/
|
|
8
|
+
export declare function makeFugueTree(docId?: string, userId?: string): FugueTree;
|
|
9
|
+
export declare const emptyFugueTree: FugueTree;
|
|
10
|
+
/**
|
|
11
|
+
* Build a FugueTree pre-populated with the given string via insertMultiple.
|
|
12
|
+
*/
|
|
13
|
+
export declare function treeWithText(text: string): FugueTree;
|
|
14
|
+
/**
|
|
15
|
+
* Craft a minimal FugueMessage from one replica so that effect() can be
|
|
16
|
+
* called on a different replica. The parent ID must match the root node
|
|
17
|
+
* ({sender:"", counter:0}).
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeForeignInsertMsg(replicaId: string, counter: number, data: string, documentID?: string): FugueMessage;
|
|
20
|
+
export declare function makeForeignDeleteMsg(replicaId: string, targetId: {
|
|
21
|
+
sender: string;
|
|
22
|
+
counter: number;
|
|
23
|
+
}, documentID?: string): FugueMessage;
|
|
24
|
+
//# sourceMappingURL=FugueTree-mocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FugueTree-mocks.d.ts","sourceRoot":"","sources":["../../../../src/tests/unit/mocks/FugueTree-mocks.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,SAAS,EAAoB,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAA0B,MAAM,yBAAyB,CAAC;AAE/E,eAAO,MAAM,MAAM,aAAa,CAAC;AACjC,eAAO,MAAM,OAAO,cAAc,CAAC;AAEnC;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAExE;AAED,eAAO,MAAM,cAAc,WAAkB,CAAC;AAE9C;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAOpD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAAe,GAC5B,YAAY,CAYd;AAED,wBAAgB,oBAAoB,CAChC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC7C,UAAU,GAAE,MAAe,GAC5B,YAAY,CAWd"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FugueTree } from "../../../dts/index.js";
|
|
2
|
+
import { Operation, MessageType } from "../../../types/index.js";
|
|
3
|
+
export const DOC_ID = "test-doc";
|
|
4
|
+
export const USER_ID = "test-user";
|
|
5
|
+
/**
|
|
6
|
+
* A fresh FugueTree with no WebSocket and stable IDs for deterministic tests.
|
|
7
|
+
*/
|
|
8
|
+
export function makeFugueTree(docId, userId) {
|
|
9
|
+
return new FugueTree(null, docId ? docId : DOC_ID, userId ? userId : USER_ID);
|
|
10
|
+
}
|
|
11
|
+
export const emptyFugueTree = makeFugueTree();
|
|
12
|
+
/**
|
|
13
|
+
* Build a FugueTree pre-populated with the given string via insertMultiple.
|
|
14
|
+
*/
|
|
15
|
+
export function treeWithText(text) {
|
|
16
|
+
const tree = makeFugueTree();
|
|
17
|
+
const msgs = tree.insertMultiple(0, text);
|
|
18
|
+
msgs.forEach((msg) => {
|
|
19
|
+
/* already applied inside insertMultiple */
|
|
20
|
+
});
|
|
21
|
+
return tree;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Craft a minimal FugueMessage from one replica so that effect() can be
|
|
25
|
+
* called on a different replica. The parent ID must match the root node
|
|
26
|
+
* ({sender:"", counter:0}).
|
|
27
|
+
*/
|
|
28
|
+
export function makeForeignInsertMsg(replicaId, counter, data, documentID = DOC_ID) {
|
|
29
|
+
return {
|
|
30
|
+
msgType: MessageType.FUGUE,
|
|
31
|
+
operation: Operation.INSERT,
|
|
32
|
+
replicaId,
|
|
33
|
+
userIdentity: "foreign-user",
|
|
34
|
+
documentID,
|
|
35
|
+
id: { sender: replicaId, counter },
|
|
36
|
+
data,
|
|
37
|
+
parent: { sender: "", counter: 0 },
|
|
38
|
+
side: "R",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function makeForeignDeleteMsg(replicaId, targetId, documentID = DOC_ID) {
|
|
42
|
+
return {
|
|
43
|
+
msgType: MessageType.FUGUE,
|
|
44
|
+
operation: Operation.DELETE,
|
|
45
|
+
replicaId,
|
|
46
|
+
userIdentity: "foreign-user",
|
|
47
|
+
documentID,
|
|
48
|
+
id: targetId,
|
|
49
|
+
data: null,
|
|
50
|
+
side: "R",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BragiAST } from "../treesitter/types/AST.js";
|
|
2
|
+
import { Parser } from "web-tree-sitter";
|
|
3
|
+
export declare const getParser: () => Promise<Parser>;
|
|
4
|
+
/**
|
|
5
|
+
* Safely parses source code into a JS AST and instantly destroys the WASM
|
|
6
|
+
* memory. Use this in your tests instead of calling getParser() directly!
|
|
7
|
+
*/
|
|
8
|
+
export declare const getSafeAst: (sourceCode: string) => Promise<BragiAST>;
|
|
9
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/tests/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAGhE,OAAO,EAAY,MAAM,EAAE,MAAM,iBAAiB,CAAC;AA4BnD,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAOhD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,QAAQ,CAcrE,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseCST } from "../treesitter/types/AST.js";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __cwd = process.cwd();
|
|
7
|
+
let initPromise = null;
|
|
8
|
+
let latexLanguage = null;
|
|
9
|
+
const ensureInit = async () => {
|
|
10
|
+
if (initPromise)
|
|
11
|
+
return initPromise;
|
|
12
|
+
const treeSitterWasm = join(__cwd, "src", "wasm", "web-tree-sitter.wasm");
|
|
13
|
+
const latexWasm = join(__cwd, "src", "wasm", "tree-sitter-latex.wasm");
|
|
14
|
+
initPromise = (async () => {
|
|
15
|
+
await Parser.init({
|
|
16
|
+
locateFile: (name) => {
|
|
17
|
+
// if (name === "tree-sitter.wasm") return resolve(treeSitterWasm);
|
|
18
|
+
return resolve(treeSitterWasm);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
// Language.load is also a WASM allocation — cache it for the same reason.
|
|
22
|
+
latexLanguage = await Language.load(resolve(latexWasm));
|
|
23
|
+
})();
|
|
24
|
+
return initPromise;
|
|
25
|
+
};
|
|
26
|
+
export const getParser = async () => {
|
|
27
|
+
await ensureInit();
|
|
28
|
+
const parser = new Parser();
|
|
29
|
+
// latexLanguage is guaranteed non-null after ensureInit resolves
|
|
30
|
+
parser.setLanguage(latexLanguage);
|
|
31
|
+
return parser;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Safely parses source code into a JS AST and instantly destroys the WASM
|
|
35
|
+
* memory. Use this in your tests instead of calling getParser() directly!
|
|
36
|
+
*/
|
|
37
|
+
export const getSafeAst = async (sourceCode) => {
|
|
38
|
+
const parser = await getParser();
|
|
39
|
+
const tree = parser.parse(sourceCode);
|
|
40
|
+
if (!tree || !tree.rootNode) {
|
|
41
|
+
throw new Error("Parse failed");
|
|
42
|
+
}
|
|
43
|
+
const ast = parseCST(tree.rootNode);
|
|
44
|
+
tree.delete();
|
|
45
|
+
parser.delete();
|
|
46
|
+
return ast;
|
|
47
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/treesitter/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/treesitter/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC"}
|
package/dist/treesitter/index.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AstNode, BragiAST } from "./types/index.js";
|
|
2
|
+
export declare const preoderAstTraversalFunc: (ast: BragiAST, callback: (node: AstNode) => void) => void;
|
|
3
|
+
export declare const postorderAstTraversalFunc: (ast: BragiAST, callback: (node: AstNode) => void) => void;
|
|
4
|
+
export declare const breadthFirstAstTraversalFunc: (ast: BragiAST, callback: (node: AstNode) => void) => void;
|
|
5
|
+
export declare const preoderAstTraversal: (ast: BragiAST) => AstNode[];
|
|
6
|
+
export declare const postorderAstTraversal: (ast: BragiAST) => AstNode[];
|
|
7
|
+
export declare const breadthFirstAstTraversal: (ast: BragiAST) => AstNode[];
|
|
8
|
+
export declare const preorderAstTraversalIterator: (ast: BragiAST) => IterableIterator<AstNode>;
|
|
9
|
+
//# sourceMappingURL=utils.d.ts.map
|