@vuer-ai/vuer-rtc 0.4.2 → 0.5.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.
Files changed (90) hide show
  1. package/CLAUDE.md +29 -0
  2. package/COALESCE_FIX_VERIFICATION.md +81 -0
  3. package/REFACTORING_NOTES.md +229 -0
  4. package/dist/client/EditBuffer.d.ts +13 -1
  5. package/dist/client/EditBuffer.d.ts.map +1 -1
  6. package/dist/client/EditBuffer.js +47 -3
  7. package/dist/client/EditBuffer.js.map +1 -1
  8. package/dist/client/actions.d.ts +5 -1
  9. package/dist/client/actions.d.ts.map +1 -1
  10. package/dist/client/actions.js +12 -9
  11. package/dist/client/actions.js.map +1 -1
  12. package/dist/client/coalesceGraphOps.d.ts +34 -0
  13. package/dist/client/coalesceGraphOps.d.ts.map +1 -0
  14. package/dist/client/coalesceGraphOps.js +35 -0
  15. package/dist/client/coalesceGraphOps.js.map +1 -0
  16. package/dist/client/coalesceTextOperations.d.ts +42 -0
  17. package/dist/client/coalesceTextOperations.d.ts.map +1 -0
  18. package/dist/client/coalesceTextOperations.js +158 -0
  19. package/dist/client/coalesceTextOperations.js.map +1 -0
  20. package/dist/client/coalescence/index.d.ts +9 -0
  21. package/dist/client/coalescence/index.d.ts.map +1 -0
  22. package/dist/client/coalescence/index.js +9 -0
  23. package/dist/client/coalescence/index.js.map +1 -0
  24. package/dist/client/coalescence/registry.d.ts +48 -0
  25. package/dist/client/coalescence/registry.d.ts.map +1 -0
  26. package/dist/client/coalescence/registry.js +95 -0
  27. package/dist/client/coalescence/registry.js.map +1 -0
  28. package/dist/client/coalescence/textDeletes.d.ts +38 -0
  29. package/dist/client/coalescence/textDeletes.d.ts.map +1 -0
  30. package/dist/client/coalescence/textDeletes.js +68 -0
  31. package/dist/client/coalescence/textDeletes.js.map +1 -0
  32. package/dist/client/coalescence/textInserts.d.ts +45 -0
  33. package/dist/client/coalescence/textInserts.d.ts.map +1 -0
  34. package/dist/client/coalescence/textInserts.js +96 -0
  35. package/dist/client/coalescence/textInserts.js.map +1 -0
  36. package/dist/client/createGraph.d.ts.map +1 -1
  37. package/dist/client/createGraph.js +9 -2
  38. package/dist/client/createGraph.js.map +1 -1
  39. package/dist/client/createTextDocument.d.ts.map +1 -1
  40. package/dist/client/createTextDocument.js +2 -1
  41. package/dist/client/createTextDocument.js.map +1 -1
  42. package/dist/client/index.d.ts +4 -0
  43. package/dist/client/index.d.ts.map +1 -1
  44. package/dist/client/index.js +4 -0
  45. package/dist/client/index.js.map +1 -1
  46. package/dist/client/textActions.d.ts +1 -1
  47. package/dist/client/textActions.d.ts.map +1 -1
  48. package/dist/client/textActions.js +7 -2
  49. package/dist/client/textActions.js.map +1 -1
  50. package/dist/client/types.d.ts +3 -0
  51. package/dist/client/types.d.ts.map +1 -1
  52. package/dist/crdt/GraphTextCRDT.d.ts +0 -4
  53. package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
  54. package/dist/crdt/GraphTextCRDT.js +3 -0
  55. package/dist/crdt/GraphTextCRDT.js.map +1 -1
  56. package/dist/crdt/Rope.d.ts +27 -6
  57. package/dist/crdt/Rope.d.ts.map +1 -1
  58. package/dist/crdt/Rope.js +137 -69
  59. package/dist/crdt/Rope.js.map +1 -1
  60. package/dist/operations/OperationTypes.d.ts +10 -26
  61. package/dist/operations/OperationTypes.d.ts.map +1 -1
  62. package/dist/operations/apply/text.d.ts.map +1 -1
  63. package/dist/operations/apply/text.js +8 -16
  64. package/dist/operations/apply/text.js.map +1 -1
  65. package/examples/05-coalescence-usage.ts +189 -0
  66. package/package.json +1 -1
  67. package/src/client/EditBuffer.ts +51 -3
  68. package/src/client/actions.ts +13 -9
  69. package/src/client/coalesceGraphOps.ts +40 -0
  70. package/src/client/coalesceTextOperations.ts +172 -0
  71. package/src/client/coalescence/index.ts +18 -0
  72. package/src/client/coalescence/registry.ts +137 -0
  73. package/src/client/coalescence/textDeletes.ts +94 -0
  74. package/src/client/coalescence/textInserts.ts +128 -0
  75. package/src/client/createGraph.ts +11 -2
  76. package/src/client/createTextDocument.ts +2 -1
  77. package/src/client/index.ts +14 -0
  78. package/src/client/textActions.ts +9 -2
  79. package/src/client/types.ts +4 -1
  80. package/src/crdt/GraphTextCRDT.ts +0 -5
  81. package/src/crdt/Rope.ts +155 -79
  82. package/src/operations/OperationTypes.ts +10 -8
  83. package/src/operations/apply/text.ts +8 -20
  84. package/test-coalescence.ts +201 -0
  85. package/tests/client/actions.test.ts +156 -0
  86. package/tests/client/coalesce-graph-operations.test.ts +321 -0
  87. package/tests/client/coalesce-text-operations.test.ts +326 -0
  88. package/tests/client/edit-buffer.test.ts +137 -1
  89. package/tests/crdt/graph-text-crdt.test.ts +29 -17
  90. package/tests/crdt/rope.test.ts +13 -11
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Tests for coalesceTextOperations - verifying the bug fix for multi-character coalescence
3
+ */
4
+
5
+ import { describe, it, expect } from '@jest/globals';
6
+ import { coalesceTextOperations } from '../../src/client/coalesceTextOperations.js';
7
+ import type { TextOperation } from '../../src/client/textTypes.js';
8
+ import type { InsertOp } from '../../src/crdt/Rope.js';
9
+
10
+ describe('coalesceTextOperations', () => {
11
+ describe('multi-character coalescence (bug fix verification)', () => {
12
+ it('should coalesce ALL consecutive characters within threshold, not just pairs', () => {
13
+ // Simulate typing "Hello" quickly (within 300ms)
14
+ const ops: TextOperation[] = [
15
+ {
16
+ type: 'insert',
17
+ op: {
18
+ id: 'alice:1',
19
+ content: 'H',
20
+ parentId: null,
21
+ seq: 1,
22
+ ts: 1000.0,
23
+ },
24
+ },
25
+ {
26
+ type: 'insert',
27
+ op: {
28
+ id: 'alice:2',
29
+ content: 'e',
30
+ parentId: 'alice:1', // Forms chain with previous
31
+ seq: 2,
32
+ ts: 1000.05, // 50ms later
33
+ },
34
+ },
35
+ {
36
+ type: 'insert',
37
+ op: {
38
+ id: 'alice:3',
39
+ content: 'l',
40
+ parentId: 'alice:2', // Forms chain with previous
41
+ seq: 3,
42
+ ts: 1000.1, // 100ms from start
43
+ },
44
+ },
45
+ {
46
+ type: 'insert',
47
+ op: {
48
+ id: 'alice:4',
49
+ content: 'l',
50
+ parentId: 'alice:3', // Forms chain with previous
51
+ seq: 4,
52
+ ts: 1000.15, // 150ms from start
53
+ },
54
+ },
55
+ {
56
+ type: 'insert',
57
+ op: {
58
+ id: 'alice:5',
59
+ content: 'o',
60
+ parentId: 'alice:4', // Forms chain with previous
61
+ seq: 5,
62
+ ts: 1000.2, // 200ms from start
63
+ },
64
+ },
65
+ ];
66
+
67
+ const result = coalesceTextOperations(ops, { thresholdMs: 300 });
68
+
69
+ // Should coalesce into ONE operation with content="Hello"
70
+ expect(result).toHaveLength(1);
71
+ expect(result[0].type).toBe('insert');
72
+ expect((result[0] as any).op.content).toBe('Hello');
73
+ expect((result[0] as any).op.id).toBe('alice:1'); // Keep first ID
74
+ expect((result[0] as any).op.parentId).toBe(null); // Keep first parentId
75
+ });
76
+
77
+ it('should respect time threshold and create separate operations for slow typing', () => {
78
+ // Simulate typing "He" quickly, then pause, then "llo" quickly
79
+ const ops: TextOperation[] = [
80
+ {
81
+ type: 'insert',
82
+ op: {
83
+ id: 'alice:1',
84
+ content: 'H',
85
+ parentId: null,
86
+ seq: 1,
87
+ ts: 1000.0,
88
+ },
89
+ },
90
+ {
91
+ type: 'insert',
92
+ op: {
93
+ id: 'alice:2',
94
+ content: 'e',
95
+ parentId: 'alice:1',
96
+ seq: 2,
97
+ ts: 1000.05, // 50ms later - within threshold
98
+ },
99
+ },
100
+ {
101
+ type: 'insert',
102
+ op: {
103
+ id: 'alice:3',
104
+ content: 'l',
105
+ parentId: 'alice:2',
106
+ seq: 3,
107
+ ts: 1000.5, // 450ms later - EXCEEDS threshold
108
+ },
109
+ },
110
+ {
111
+ type: 'insert',
112
+ op: {
113
+ id: 'alice:4',
114
+ content: 'l',
115
+ parentId: 'alice:3',
116
+ seq: 4,
117
+ ts: 1000.55, // 50ms after 'l'
118
+ },
119
+ },
120
+ {
121
+ type: 'insert',
122
+ op: {
123
+ id: 'alice:5',
124
+ content: 'o',
125
+ parentId: 'alice:4',
126
+ seq: 5,
127
+ ts: 1000.6, // 50ms after second 'l'
128
+ },
129
+ },
130
+ ];
131
+
132
+ const result = coalesceTextOperations(ops, { thresholdMs: 300 });
133
+
134
+ // Should create TWO operations: "He" and "llo"
135
+ expect(result).toHaveLength(2);
136
+ expect((result[0] as any).op.content).toBe('He');
137
+ expect((result[0] as any).op.id).toBe('alice:1');
138
+ expect((result[1] as any).op.content).toBe('llo');
139
+ expect((result[1] as any).op.id).toBe('alice:3');
140
+ });
141
+
142
+ it('should handle single character operations (no coalescence)', () => {
143
+ const ops: TextOperation[] = [
144
+ {
145
+ type: 'insert',
146
+ op: {
147
+ id: 'alice:1',
148
+ content: 'H',
149
+ parentId: null,
150
+ seq: 1,
151
+ ts: 1000.0,
152
+ },
153
+ },
154
+ {
155
+ type: 'insert',
156
+ op: {
157
+ id: 'alice:2',
158
+ content: 'e',
159
+ parentId: 'alice:1',
160
+ seq: 2,
161
+ ts: 1001.5, // 1500ms later - exceeds threshold
162
+ },
163
+ },
164
+ {
165
+ type: 'insert',
166
+ op: {
167
+ id: 'alice:3',
168
+ content: 'l',
169
+ parentId: 'alice:2',
170
+ seq: 3,
171
+ ts: 1003.0, // 1500ms later - exceeds threshold
172
+ },
173
+ },
174
+ ];
175
+
176
+ const result = coalesceTextOperations(ops, { thresholdMs: 300 });
177
+
178
+ // Should create THREE separate operations (no coalescence)
179
+ expect(result).toHaveLength(3);
180
+ expect((result[0] as any).op.content).toBe('H');
181
+ expect((result[1] as any).op.content).toBe('e');
182
+ expect((result[2] as any).op.content).toBe('l');
183
+ });
184
+
185
+ it('should handle non-sequential IDs (different agents)', () => {
186
+ const ops: TextOperation[] = [
187
+ {
188
+ type: 'insert',
189
+ op: {
190
+ id: 'alice:1',
191
+ content: 'H',
192
+ parentId: null,
193
+ seq: 1,
194
+ ts: 1000.0,
195
+ },
196
+ },
197
+ {
198
+ type: 'insert',
199
+ op: {
200
+ id: 'bob:1', // Different agent
201
+ content: 'e',
202
+ parentId: 'alice:1',
203
+ seq: 2,
204
+ ts: 1000.05,
205
+ },
206
+ },
207
+ ];
208
+
209
+ const result = coalesceTextOperations(ops, { thresholdMs: 300 });
210
+
211
+ // Should NOT coalesce (different agents)
212
+ expect(result).toHaveLength(2);
213
+ expect((result[0] as any).op.content).toBe('H');
214
+ expect((result[1] as any).op.content).toBe('e');
215
+ });
216
+
217
+ it('should handle delete operations (flush pending inserts)', () => {
218
+ const ops: TextOperation[] = [
219
+ {
220
+ type: 'insert',
221
+ op: {
222
+ id: 'alice:1',
223
+ content: 'H',
224
+ parentId: null,
225
+ seq: 1,
226
+ ts: 1000.0,
227
+ },
228
+ },
229
+ {
230
+ type: 'insert',
231
+ op: {
232
+ id: 'alice:2',
233
+ content: 'e',
234
+ parentId: 'alice:1',
235
+ seq: 2,
236
+ ts: 1000.05,
237
+ },
238
+ },
239
+ {
240
+ type: 'delete',
241
+ op: {
242
+ deletions: [{ id: 'alice:2', length: 1 }],
243
+ seq: 3,
244
+ ts: 1000.1,
245
+ },
246
+ } as any,
247
+ ];
248
+
249
+ const result = coalesceTextOperations(ops, { thresholdMs: 300 });
250
+
251
+ // Should flush "He" and then add the delete
252
+ expect(result).toHaveLength(2);
253
+ expect(result[0].type).toBe('insert');
254
+ expect((result[0] as any).op.content).toBe('He');
255
+ expect(result[1].type).toBe('delete');
256
+ });
257
+
258
+ it('should verify _lastCharId metadata is tracked correctly', () => {
259
+ const ops: TextOperation[] = [
260
+ {
261
+ type: 'insert',
262
+ op: {
263
+ id: 'alice:1',
264
+ content: 'a',
265
+ parentId: null,
266
+ seq: 1,
267
+ ts: 1000.0,
268
+ },
269
+ },
270
+ {
271
+ type: 'insert',
272
+ op: {
273
+ id: 'alice:2',
274
+ content: 'b',
275
+ parentId: 'alice:1',
276
+ seq: 2,
277
+ ts: 1000.05,
278
+ },
279
+ },
280
+ {
281
+ type: 'insert',
282
+ op: {
283
+ id: 'alice:3',
284
+ content: 'c',
285
+ parentId: 'alice:2', // This should match _lastCharId after first merge
286
+ seq: 3,
287
+ ts: 1000.1,
288
+ },
289
+ },
290
+ ];
291
+
292
+ const result = coalesceTextOperations(ops, { thresholdMs: 300 });
293
+
294
+ // Should coalesce all three
295
+ expect(result).toHaveLength(1);
296
+ expect((result[0] as any).op.content).toBe('abc');
297
+ expect((result[0] as any).op._lastCharId).toBe('alice:3');
298
+ });
299
+ });
300
+
301
+ describe('edge cases', () => {
302
+ it('should handle empty operations array', () => {
303
+ const result = coalesceTextOperations([]);
304
+ expect(result).toHaveLength(0);
305
+ });
306
+
307
+ it('should handle single operation', () => {
308
+ const ops: TextOperation[] = [
309
+ {
310
+ type: 'insert',
311
+ op: {
312
+ id: 'alice:1',
313
+ content: 'H',
314
+ parentId: null,
315
+ seq: 1,
316
+ ts: 1000.0,
317
+ },
318
+ },
319
+ ];
320
+
321
+ const result = coalesceTextOperations(ops);
322
+ expect(result).toHaveLength(1);
323
+ expect((result[0] as any).op.content).toBe('H');
324
+ });
325
+ });
326
+ });
@@ -6,7 +6,7 @@ import { describe, it, expect } from '@jest/globals';
6
6
  import { readFileSync } from 'fs';
7
7
  import { join, dirname } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
- import { isAdditiveOp, mergeValues, EditBufferImpl } from '../../src/client/EditBuffer.js';
9
+ import { isAdditiveOp, mergeValues, EditBufferImpl, coalesceTextOps } from '../../src/client/EditBuffer.js';
10
10
  import type { Operation } from '../../src/operations/OperationTypes.js';
11
11
 
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -114,4 +114,140 @@ describe('EditBuffer', () => {
114
114
  expect(buffer.getOps()).toHaveLength(0);
115
115
  });
116
116
  });
117
+
118
+ describe('coalesceTextOps', () => {
119
+ it('should NOT coalesce text inserts with CRDT metadata', () => {
120
+ const ops: Operation[] = [
121
+ {
122
+ otype: 'text.insert',
123
+ key: 'text-doc',
124
+ path: 'content',
125
+ position: 0,
126
+ value: 'a',
127
+ id: { agent: 'session-1', seq: 1 },
128
+ content: 'a',
129
+ parentId: null,
130
+ seq: 1,
131
+ } as any,
132
+ {
133
+ otype: 'text.insert',
134
+ key: 'text-doc',
135
+ path: 'content',
136
+ position: 1,
137
+ value: 'b',
138
+ id: { agent: 'session-1', seq: 2 },
139
+ content: 'b',
140
+ parentId: { agent: 'session-1', seq: 1 },
141
+ seq: 2,
142
+ } as any,
143
+ ];
144
+
145
+ const result = coalesceTextOps(ops);
146
+
147
+ // Should NOT coalesce - each operation keeps its CRDT metadata
148
+ expect(result).toHaveLength(2);
149
+ expect((result[0] as any).id).toEqual({ agent: 'session-1', seq: 1 });
150
+ expect((result[0] as any).value).toBe('a');
151
+ expect((result[1] as any).id).toEqual({ agent: 'session-1', seq: 2 });
152
+ expect((result[1] as any).value).toBe('b');
153
+ });
154
+
155
+ it('should coalesce position-based text inserts without CRDT metadata', () => {
156
+ const ops: Operation[] = [
157
+ {
158
+ otype: 'text.insert',
159
+ key: 'text-doc',
160
+ path: 'content',
161
+ position: 0,
162
+ value: 'a',
163
+ } as any,
164
+ {
165
+ otype: 'text.insert',
166
+ key: 'text-doc',
167
+ path: 'content',
168
+ position: 1,
169
+ value: 'b',
170
+ } as any,
171
+ ];
172
+
173
+ const result = coalesceTextOps(ops);
174
+
175
+ // Should coalesce position-based operations
176
+ expect(result).toHaveLength(1);
177
+ expect((result[0] as any).position).toBe(0);
178
+ expect((result[0] as any).value).toBe('ab');
179
+ expect((result[0] as any).id).toBeUndefined();
180
+ });
181
+
182
+ it('should NOT coalesce text deletes with CRDT metadata', () => {
183
+ const ops: Operation[] = [
184
+ {
185
+ otype: 'text.delete',
186
+ key: 'text-doc',
187
+ path: 'content',
188
+ position: 0,
189
+ length: 1,
190
+ deletions: [{ id: { agent: 'session-1', seq: 1 }, length: 1 }],
191
+ } as any,
192
+ {
193
+ otype: 'text.delete',
194
+ key: 'text-doc',
195
+ path: 'content',
196
+ position: 0,
197
+ length: 1,
198
+ deletions: [{ id: { agent: 'session-1', seq: 2 }, length: 1 }],
199
+ } as any,
200
+ ];
201
+
202
+ const result = coalesceTextOps(ops);
203
+
204
+ // Should NOT coalesce - each operation keeps its CRDT metadata
205
+ expect(result).toHaveLength(2);
206
+ expect((result[0] as any).deletions).toEqual([{ id: { agent: 'session-1', seq: 1 }, length: 1 }]);
207
+ expect((result[1] as any).deletions).toEqual([{ id: { agent: 'session-1', seq: 2 }, length: 1 }]);
208
+ });
209
+
210
+ it('should preserve CRDT metadata through mixed operations', () => {
211
+ const ops: Operation[] = [
212
+ {
213
+ otype: 'text.insert',
214
+ key: 'text-doc',
215
+ path: 'content',
216
+ position: 0,
217
+ value: 'hello',
218
+ } as any,
219
+ {
220
+ otype: 'text.insert',
221
+ key: 'text-doc',
222
+ path: 'content',
223
+ position: 5,
224
+ value: ' ',
225
+ id: { agent: 'session-1', seq: 1 },
226
+ content: ' ',
227
+ parentId: null,
228
+ seq: 1,
229
+ } as any,
230
+ {
231
+ otype: 'text.insert',
232
+ key: 'text-doc',
233
+ path: 'content',
234
+ position: 6,
235
+ value: 'world',
236
+ } as any,
237
+ ];
238
+
239
+ const result = coalesceTextOps(ops);
240
+
241
+ // First "hello" should be alone (position-based, coalesced)
242
+ // Second " " should be alone (has CRDT metadata)
243
+ // Third "world" should be alone (position-based but not adjacent to first)
244
+ expect(result).toHaveLength(3);
245
+ expect((result[0] as any).value).toBe('hello');
246
+ expect((result[0] as any).id).toBeUndefined();
247
+ expect((result[1] as any).value).toBe(' ');
248
+ expect((result[1] as any).id).toEqual({ agent: 'session-1', seq: 1 });
249
+ expect((result[2] as any).value).toBe('world');
250
+ expect((result[2] as any).id).toBeUndefined();
251
+ });
252
+ });
117
253
  });
@@ -96,10 +96,11 @@ describe('GraphTextCRDT', () => {
96
96
  it('should return InsertOp with CRDT metadata', () => {
97
97
  const op = crdt.insertLocal('node-1', 'content', 0, 'test');
98
98
  expect(op.id).toBeDefined();
99
- expect(op.id.agent).toBe('session-1');
100
- expect(op.id.seq).toBeDefined();
99
+ expect(typeof op.id).toBe('string');
100
+ expect(op.id).toMatch(/^session-1:/); // String format: "agent:seq"
101
101
  expect(op.content).toBe('test');
102
102
  expect(op.seq).toBeDefined();
103
+ expect(op.ts).toBeDefined();
103
104
  });
104
105
 
105
106
  it('should handle multiple sequential inserts', () => {
@@ -112,7 +113,7 @@ describe('GraphTextCRDT', () => {
112
113
  it('should auto-create rope if not initialized', () => {
113
114
  const op = crdt.insertLocal('node-2', 'title', 0, 'New Title');
114
115
  expect(crdt.getText('node-2', 'title')).toBe('New Title');
115
- expect(op.id.agent).toBe('session-1');
116
+ expect(op.id).toMatch(/^session-1:/);
116
117
  });
117
118
  });
118
119
 
@@ -216,10 +217,11 @@ describe('GraphTextCRDT', () => {
216
217
 
217
218
  it('should apply remote insert operation', () => {
218
219
  const op: InsertOp = {
219
- id: { agent: 'session-2', seq: 0 },
220
+ id: 'session-2:0',
220
221
  content: 'Hello',
221
222
  parentId: null,
222
223
  seq: 1,
224
+ ts: Date.now() / 1000,
223
225
  };
224
226
  crdt.applyInsert('node-1', 'content', op);
225
227
  expect(crdt.getText('node-1', 'content')).toBe('Hello');
@@ -227,18 +229,20 @@ describe('GraphTextCRDT', () => {
227
229
 
228
230
  it('should apply remote insert with parent', () => {
229
231
  const op1: InsertOp = {
230
- id: { agent: 'session-1', seq: 0 },
232
+ id: 'session-1:0',
231
233
  content: 'Hello',
232
234
  parentId: null,
233
235
  seq: 1,
236
+ ts: Date.now() / 1000,
234
237
  };
235
238
  crdt.applyInsert('node-1', 'content', op1);
236
239
 
237
240
  const op2: InsertOp = {
238
- id: { agent: 'session-2', seq: 0 },
241
+ id: 'session-2:0',
239
242
  content: ' World',
240
- parentId: { agent: 'session-1', seq: 4 },
243
+ parentId: 'session-1:4',
241
244
  seq: 2,
245
+ ts: Date.now() / 1000,
242
246
  };
243
247
  crdt.applyInsert('node-1', 'content', op2);
244
248
  expect(crdt.getText('node-1', 'content')).toBe('Hello World');
@@ -246,10 +250,11 @@ describe('GraphTextCRDT', () => {
246
250
 
247
251
  it('should auto-create rope if not initialized', () => {
248
252
  const op: InsertOp = {
249
- id: { agent: 'session-2', seq: 0 },
253
+ id: 'session-2:0',
250
254
  content: 'test',
251
255
  parentId: null,
252
256
  seq: 1,
257
+ ts: Date.now() / 1000,
253
258
  };
254
259
  crdt.applyInsert('node-2', 'title', op);
255
260
  expect(crdt.getText('node-2', 'title')).toBe('test');
@@ -259,10 +264,11 @@ describe('GraphTextCRDT', () => {
259
264
  const op1 = crdt.insertLocal('node-1', 'content', 0, 'A');
260
265
 
261
266
  const op2: InsertOp = {
262
- id: { agent: 'session-2', seq: 0 },
267
+ id: 'session-2:0',
263
268
  content: 'B',
264
269
  parentId: null,
265
270
  seq: 1,
271
+ ts: Date.now() / 1000,
266
272
  };
267
273
  crdt.applyInsert('node-1', 'content', op2);
268
274
 
@@ -301,7 +307,7 @@ describe('GraphTextCRDT', () => {
301
307
 
302
308
  it('should auto-create rope if not initialized', () => {
303
309
  const op: DeleteOp = {
304
- deletions: [{ id: { agent: 'session-1', seq: 0 }, length: 5 }],
310
+ deletions: [{ id: 'session-1:0', length: 5 }],
305
311
  };
306
312
  crdt.applyDelete('node-2', 'title', op);
307
313
  expect(crdt.getText('node-2', 'title')).toBe('');
@@ -311,7 +317,7 @@ describe('GraphTextCRDT', () => {
311
317
  const delOp = crdt.deleteLocal('node-1', 'content', 0, 5);
312
318
 
313
319
  const remoteOp: DeleteOp = {
314
- deletions: [{ id: { agent: 'session-1', seq: 5 }, length: 6 }],
320
+ deletions: [{ id: 'session-1:5', length: 6 }],
315
321
  };
316
322
  crdt.applyDelete('node-1', 'content', remoteOp);
317
323
 
@@ -336,10 +342,11 @@ describe('GraphTextCRDT', () => {
336
342
  deletions: [{ id: items[0].id, length: 5 }],
337
343
  },
338
344
  insert: {
339
- id: { agent: 'session-2', seq: 0 },
345
+ id: 'session-2:0',
340
346
  content: 'Hi',
341
347
  parentId: null,
342
348
  seq: 2,
349
+ ts: Date.now() / 1000,
343
350
  },
344
351
  };
345
352
  crdt.applyReplace('node-1', 'content', op);
@@ -355,10 +362,11 @@ describe('GraphTextCRDT', () => {
355
362
  deletions: [{ id: items[0].id, length: 6 }],
356
363
  },
357
364
  insert: {
358
- id: { agent: 'session-2', seq: 0 },
365
+ id: 'session-2:0',
359
366
  content: '',
360
367
  parentId: null,
361
368
  seq: 2,
369
+ ts: Date.now() / 1000,
362
370
  },
363
371
  };
364
372
  crdt.applyReplace('node-1', 'content', op);
@@ -369,10 +377,11 @@ describe('GraphTextCRDT', () => {
369
377
  const op: ReplaceOp = {
370
378
  delete: { deletions: [] },
371
379
  insert: {
372
- id: { agent: 'session-2', seq: 0 },
380
+ id: 'session-2:0',
373
381
  content: 'new',
374
382
  parentId: null,
375
383
  seq: 1,
384
+ ts: Date.now() / 1000,
376
385
  },
377
386
  };
378
387
  crdt.applyReplace('node-2', 'title', op);
@@ -562,10 +571,11 @@ describe('GraphTextCRDT', () => {
562
571
 
563
572
  // Initialize both from the same initial state
564
573
  const initOp: InsertOp = {
565
- id: { agent: 'init', seq: 0 },
574
+ id: 'init:0',
566
575
  content: 'Hello',
567
576
  parentId: null,
568
577
  seq: 1,
578
+ ts: Date.now() / 1000,
569
579
  };
570
580
  crdt1.applyInsert('doc-1', 'content', initOp);
571
581
  crdt2.applyInsert('doc-1', 'content', initOp);
@@ -590,10 +600,11 @@ describe('GraphTextCRDT', () => {
590
600
 
591
601
  // Initialize from same initial state
592
602
  const initOp: InsertOp = {
593
- id: { agent: 'init', seq: 0 },
603
+ id: 'init:0',
594
604
  content: 'ABCDEF',
595
605
  parentId: null,
596
606
  seq: 1,
607
+ ts: Date.now() / 1000,
597
608
  };
598
609
  crdt1.applyInsert('doc-1', 'content', initOp);
599
610
  crdt2.applyInsert('doc-1', 'content', initOp);
@@ -615,10 +626,11 @@ describe('GraphTextCRDT', () => {
615
626
 
616
627
  // Initialize from same initial state
617
628
  const initOp: InsertOp = {
618
- id: { agent: 'init', seq: 0 },
629
+ id: 'init:0',
619
630
  content: 'test',
620
631
  parentId: null,
621
632
  seq: 1,
633
+ ts: Date.now() / 1000,
622
634
  };
623
635
  crdt1.applyInsert('doc-1', 'content', initOp);
624
636
  crdt2.applyInsert('doc-1', 'content', initOp);