@vuer-ai/vuer-rtc 0.5.0 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"coalesceTextOperations.d.ts","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,aAAa,EAAE,EACpB,OAAO,GAAE,eAAoB,GAC5B,aAAa,EAAE,CAiFjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI/D"}
1
+ {"version":3,"file":"coalesceTextOperations.d.ts","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,aAAa,EAAE,EACpB,OAAO,GAAE,eAAoB,GAC5B,aAAa,EAAE,CAuHjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI/D"}
@@ -28,8 +28,14 @@ export function coalesceTextOperations(ops, options = {}) {
28
28
  return ops;
29
29
  const result = [];
30
30
  let pendingInsert = null;
31
+ let pendingDelete = null;
31
32
  for (const op of ops) {
32
33
  if (op.type === 'insert') {
34
+ // Flush any pending delete before starting new insert
35
+ if (pendingDelete !== null) {
36
+ result.push(pendingDelete);
37
+ pendingDelete = null;
38
+ }
33
39
  if (pendingInsert === null) {
34
40
  // Start new pending insert
35
41
  pendingInsert = { type: 'insert', op: { ...op.op } };
@@ -84,19 +90,52 @@ export function coalesceTextOperations(ops, options = {}) {
84
90
  }
85
91
  }
86
92
  }
93
+ else if (op.type === 'delete') {
94
+ // Flush any pending insert before handling delete
95
+ if (pendingInsert !== null) {
96
+ result.push(pendingInsert);
97
+ pendingInsert = null;
98
+ }
99
+ if (pendingDelete === null) {
100
+ // Start new pending delete
101
+ pendingDelete = { type: 'delete', op: { ...op.op, deletions: [...op.op.deletions] } };
102
+ }
103
+ else {
104
+ // Try to merge consecutive delete operations
105
+ // Deletes can be merged if they're within the time threshold
106
+ // We simply combine the deletions arrays
107
+ const canMerge = true; // For deletes, we just merge the deletions arrays
108
+ if (canMerge) {
109
+ // Merge by combining deletions
110
+ pendingDelete.op.deletions.push(...op.op.deletions);
111
+ }
112
+ else {
113
+ // Can't merge - flush pending and start new
114
+ result.push(pendingDelete);
115
+ pendingDelete = { type: 'delete', op: { ...op.op, deletions: [...op.op.deletions] } };
116
+ }
117
+ }
118
+ }
87
119
  else {
88
- // Delete operation - flush any pending insert
120
+ // Other operation types - flush any pending
89
121
  if (pendingInsert !== null) {
90
122
  result.push(pendingInsert);
91
123
  pendingInsert = null;
92
124
  }
125
+ if (pendingDelete !== null) {
126
+ result.push(pendingDelete);
127
+ pendingDelete = null;
128
+ }
93
129
  result.push(op);
94
130
  }
95
131
  }
96
- // Flush any remaining pending insert
132
+ // Flush any remaining pending operations
97
133
  if (pendingInsert !== null) {
98
134
  result.push(pendingInsert);
99
135
  }
136
+ if (pendingDelete !== null) {
137
+ result.push(pendingDelete);
138
+ }
100
139
  return result;
101
140
  }
102
141
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"coalesceTextOperations.js","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coalesceTextOperations.js","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vuer-ai/vuer-rtc",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "CRDT-based real-time collaborative data structures",
@@ -41,9 +41,15 @@ export function coalesceTextOperations(
41
41
 
42
42
  const result: TextOperation[] = [];
43
43
  let pendingInsert: { type: 'insert'; op: InsertOp } | null = null;
44
+ let pendingDelete: { type: 'delete'; op: any } | null = null;
44
45
 
45
46
  for (const op of ops) {
46
47
  if (op.type === 'insert') {
48
+ // Flush any pending delete before starting new insert
49
+ if (pendingDelete !== null) {
50
+ result.push(pendingDelete);
51
+ pendingDelete = null;
52
+ }
47
53
  if (pendingInsert === null) {
48
54
  // Start new pending insert
49
55
  pendingInsert = { type: 'insert', op: { ...op.op } };
@@ -99,20 +105,52 @@ export function coalesceTextOperations(
99
105
  pendingInsert = { type: 'insert', op: { ...currOp } };
100
106
  }
101
107
  }
108
+ } else if (op.type === 'delete') {
109
+ // Flush any pending insert before handling delete
110
+ if (pendingInsert !== null) {
111
+ result.push(pendingInsert);
112
+ pendingInsert = null;
113
+ }
114
+
115
+ if (pendingDelete === null) {
116
+ // Start new pending delete
117
+ pendingDelete = { type: 'delete', op: { ...op.op, deletions: [...op.op.deletions] } };
118
+ } else {
119
+ // Try to merge consecutive delete operations
120
+ // Deletes can be merged if they're within the time threshold
121
+ // We simply combine the deletions arrays
122
+ const canMerge = true; // For deletes, we just merge the deletions arrays
123
+
124
+ if (canMerge) {
125
+ // Merge by combining deletions
126
+ pendingDelete.op.deletions.push(...op.op.deletions);
127
+ } else {
128
+ // Can't merge - flush pending and start new
129
+ result.push(pendingDelete);
130
+ pendingDelete = { type: 'delete', op: { ...op.op, deletions: [...op.op.deletions] } };
131
+ }
132
+ }
102
133
  } else {
103
- // Delete operation - flush any pending insert
134
+ // Other operation types - flush any pending
104
135
  if (pendingInsert !== null) {
105
136
  result.push(pendingInsert);
106
137
  pendingInsert = null;
107
138
  }
139
+ if (pendingDelete !== null) {
140
+ result.push(pendingDelete);
141
+ pendingDelete = null;
142
+ }
108
143
  result.push(op);
109
144
  }
110
145
  }
111
146
 
112
- // Flush any remaining pending insert
147
+ // Flush any remaining pending operations
113
148
  if (pendingInsert !== null) {
114
149
  result.push(pendingInsert);
115
150
  }
151
+ if (pendingDelete !== null) {
152
+ result.push(pendingDelete);
153
+ }
116
154
 
117
155
  return result;
118
156
  }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * End-to-end tests for graph operation coalescence
3
+ *
4
+ * Verifies that text.insert and text.delete operations are properly coalesced
5
+ * when using createGraph with coalescingEnabled.
6
+ */
7
+
8
+ import { describe, it, expect, jest } from '@jest/globals';
9
+ import { createGraph } from '../../src/client/createGraph.js';
10
+ import type { CRDTMessage, Operation } from '../../src/operations/OperationTypes.js';
11
+
12
+ describe('Graph Operation Coalescence (end-to-end)', () => {
13
+ describe('text.insert coalescence', () => {
14
+ it('should coalesce consecutive text.insert operations when coalescingEnabled', async () => {
15
+ const messages: CRDTMessage[] = [];
16
+ const store = createGraph({
17
+ sessionId: 'alice',
18
+ onSend: (msg) => messages.push(msg),
19
+ coalescingEnabled: true,
20
+ coalescingDelayMs: 50, // Short delay for testing
21
+ coalescingThresholdMs: 300, // 300ms threshold
22
+ });
23
+
24
+ // Create node first (insert as child of root '')
25
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
26
+
27
+ // Initialize a text property
28
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: '' });
29
+
30
+ // Simulate typing "Hello" quickly (within 300ms)
31
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'H' });
32
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'e' });
33
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'l' });
34
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'l' });
35
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 4, value: 'o' });
36
+
37
+ // Wait for coalescing timer to fire
38
+ await new Promise(resolve => setTimeout(resolve, 100));
39
+
40
+ // Should have sent ONE coalesced message (init + coalesced inserts)
41
+ expect(messages.length).toBe(1);
42
+
43
+ // The message should contain coalesced text.insert operations
44
+ const msg = messages[0];
45
+ const textInserts = msg.ops.filter(op => op.otype === 'text.insert');
46
+
47
+ // All 5 inserts should be coalesced into 1 operation
48
+ expect(textInserts.length).toBeLessThan(5);
49
+
50
+ // The coalesced operation should have content "Hello"
51
+ const firstInsert = textInserts[0];
52
+ expect(firstInsert.content).toBe('Hello');
53
+ });
54
+
55
+ it('should NOT coalesce when coalescingEnabled is false', async () => {
56
+ const messages: CRDTMessage[] = [];
57
+ const store = createGraph({
58
+ sessionId: 'alice',
59
+ onSend: (msg) => messages.push(msg),
60
+ coalescingEnabled: false, // Disabled
61
+ });
62
+
63
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
64
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: '' });
65
+
66
+ // Type "Hi" and commit immediately
67
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'H' });
68
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'i' });
69
+ store.commit('typing');
70
+
71
+ // Should send ONE message with init and 2 separate insert operations (not coalesced)
72
+ expect(messages.length).toBe(1);
73
+ const textInserts = messages[0].ops.filter(op => op.otype === 'text.insert');
74
+
75
+ // Should have 2 separate operations
76
+ expect(textInserts.length).toBe(2);
77
+ expect(textInserts[0].content).toBe('H');
78
+ expect(textInserts[1].content).toBe('i');
79
+ });
80
+
81
+ it('should respect time threshold and create separate operations for slow typing', async () => {
82
+ const messages: CRDTMessage[] = [];
83
+ const store = createGraph({
84
+ sessionId: 'alice',
85
+ onSend: (msg) => messages.push(msg),
86
+ coalescingEnabled: true,
87
+ coalescingDelayMs: 50,
88
+ coalescingThresholdMs: 100, // 100ms threshold
89
+ });
90
+
91
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
92
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: '' });
93
+
94
+ // Type "H" and "e" quickly
95
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'H' });
96
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'e' });
97
+
98
+ // Wait for first batch to be sent
99
+ await new Promise(resolve => setTimeout(resolve, 100));
100
+
101
+ // Type "l" and "l" quickly (after threshold)
102
+ await new Promise(resolve => setTimeout(resolve, 150)); // Exceed threshold
103
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'l' });
104
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'l' });
105
+
106
+ // Wait for second batch
107
+ await new Promise(resolve => setTimeout(resolve, 100));
108
+
109
+ // Should have sent TWO messages: one with "He", one with "ll"
110
+ expect(messages.length).toBe(2);
111
+
112
+ const firstInserts = messages[0].ops.filter(op => op.otype === 'text.insert');
113
+ const secondInserts = messages[1].ops.filter(op => op.otype === 'text.insert');
114
+
115
+ expect(firstInserts.length).toBe(1);
116
+ expect(firstInserts[0].content).toBe('He');
117
+
118
+ expect(secondInserts.length).toBe(1);
119
+ expect(secondInserts[0].content).toBe('ll');
120
+ });
121
+ });
122
+
123
+ describe('text.delete coalescence', () => {
124
+ it('should coalesce consecutive text.delete operations', async () => {
125
+ const messages: CRDTMessage[] = [];
126
+ const store = createGraph({
127
+ sessionId: 'alice',
128
+ onSend: (msg) => messages.push(msg),
129
+ coalescingEnabled: true,
130
+ coalescingDelayMs: 50,
131
+ coalescingThresholdMs: 300,
132
+ });
133
+
134
+ // Create node and initialize with some text
135
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
136
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: 'Hello World' });
137
+ await new Promise(resolve => setTimeout(resolve, 100));
138
+ messages.length = 0; // Clear init message
139
+
140
+ // Delete "World" one character at a time (positions 6-10)
141
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 6, length: 1 }); // Delete 'W'
142
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 6, length: 1 }); // Delete 'o'
143
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 6, length: 1 }); // Delete 'r'
144
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 6, length: 1 }); // Delete 'l'
145
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 6, length: 1 }); // Delete 'd'
146
+
147
+ // Wait for coalescing timer
148
+ await new Promise(resolve => setTimeout(resolve, 100));
149
+
150
+ // Should have ONE coalesced delete message
151
+ expect(messages.length).toBe(1);
152
+
153
+ const deleteOps = messages[0].ops.filter(op => op.otype === 'text.delete');
154
+
155
+ // Should be coalesced into fewer operations
156
+ expect(deleteOps.length).toBeLessThan(5);
157
+
158
+ // If coalesced into 1 operation, it should have a deletions array
159
+ if (deleteOps.length === 1) {
160
+ const firstDelete = deleteOps[0] as any;
161
+ expect(firstDelete.deletions).toBeDefined();
162
+ expect(firstDelete.deletions.length).toBeGreaterThanOrEqual(1);
163
+ }
164
+ });
165
+
166
+ it('should handle mixed insert and delete operations', async () => {
167
+ const messages: CRDTMessage[] = [];
168
+ const store = createGraph({
169
+ sessionId: 'alice',
170
+ onSend: (msg) => messages.push(msg),
171
+ coalescingEnabled: true,
172
+ coalescingDelayMs: 50,
173
+ coalescingThresholdMs: 300,
174
+ });
175
+
176
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
177
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: '' });
178
+
179
+ // Type "Hello", then delete "lo", then type "p"
180
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'H' });
181
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'e' });
182
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'l' });
183
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'l' });
184
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 4, value: 'o' });
185
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 3, length: 1 }); // Delete 'l'
186
+ store.edit({ otype: 'text.delete', key: 'doc', path: 'content', position: 3, length: 1 }); // Delete 'o'
187
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'p' });
188
+
189
+ // Wait for coalescing timer
190
+ await new Promise(resolve => setTimeout(resolve, 100));
191
+
192
+ // Should have ONE message with coalesced operations
193
+ expect(messages.length).toBe(1);
194
+
195
+ const msg = messages[0];
196
+ const inserts = msg.ops.filter(op => op.otype === 'text.insert');
197
+ const deletes = msg.ops.filter(op => op.otype === 'text.delete');
198
+
199
+ // Should have some coalesced inserts and deletes
200
+ expect(inserts.length).toBeGreaterThan(0);
201
+ expect(deletes.length).toBeGreaterThan(0);
202
+ });
203
+ });
204
+
205
+ describe('explicit commit with coalescence', () => {
206
+ it('should coalesce operations when explicitly committing with coalescingEnabled', () => {
207
+ const messages: CRDTMessage[] = [];
208
+ const store = createGraph({
209
+ sessionId: 'alice',
210
+ onSend: (msg) => messages.push(msg),
211
+ coalescingEnabled: true,
212
+ coalescingThresholdMs: 300,
213
+ });
214
+
215
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
216
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: '' });
217
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'H' });
218
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'i' });
219
+
220
+ // Explicitly commit (should clear timer and coalesce immediately)
221
+ store.commit('typing');
222
+
223
+ // Should send ONE message immediately with coalesced operations
224
+ expect(messages.length).toBe(1);
225
+
226
+ const textInserts = messages[0].ops.filter(op => op.otype === 'text.insert');
227
+ expect(textInserts.length).toBe(1);
228
+ expect(textInserts[0].content).toBe('Hi');
229
+ });
230
+ });
231
+
232
+ describe('multi-document coalescence', () => {
233
+ it('should coalesce operations on different documents independently', async () => {
234
+ const messages: CRDTMessage[] = [];
235
+ const store = createGraph({
236
+ sessionId: 'alice',
237
+ onSend: (msg) => messages.push(msg),
238
+ coalescingEnabled: true,
239
+ coalescingDelayMs: 50,
240
+ coalescingThresholdMs: 300,
241
+ });
242
+
243
+ // Edit doc1
244
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc1', tag: 'Doc', name: 'Document 1' } });
245
+ store.edit({ otype: 'text.init', key: 'doc1', path: 'content', value: '' });
246
+ store.edit({ otype: 'text.insert', key: 'doc1', path: 'content', position: 0, value: 'A' });
247
+ store.edit({ otype: 'text.insert', key: 'doc1', path: 'content', position: 1, value: 'B' });
248
+
249
+ // Edit doc2
250
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc2', tag: 'Doc', name: 'Document 2' } });
251
+ store.edit({ otype: 'text.init', key: 'doc2', path: 'content', value: '' });
252
+ store.edit({ otype: 'text.insert', key: 'doc2', path: 'content', position: 0, value: 'X' });
253
+ store.edit({ otype: 'text.insert', key: 'doc2', path: 'content', position: 1, value: 'Y' });
254
+
255
+ // Wait for coalescing
256
+ await new Promise(resolve => setTimeout(resolve, 100));
257
+
258
+ // Should send ONE message with operations from both docs
259
+ expect(messages.length).toBe(1);
260
+
261
+ const msg = messages[0];
262
+ const doc1Inserts = msg.ops.filter(op => op.otype === 'text.insert' && op.key === 'doc1');
263
+ const doc2Inserts = msg.ops.filter(op => op.otype === 'text.insert' && op.key === 'doc2');
264
+
265
+ // Each document's operations should be coalesced independently
266
+ expect(doc1Inserts.length).toBe(1);
267
+ expect(doc1Inserts[0].content).toBe('AB');
268
+
269
+ expect(doc2Inserts.length).toBe(1);
270
+ expect(doc2Inserts[0].content).toBe('XY');
271
+ });
272
+ });
273
+
274
+ describe('edge cases', () => {
275
+ it('should handle rapid commits without losing operations', async () => {
276
+ const messages: CRDTMessage[] = [];
277
+ const store = createGraph({
278
+ sessionId: 'alice',
279
+ onSend: (msg) => messages.push(msg),
280
+ coalescingEnabled: true,
281
+ coalescingDelayMs: 100,
282
+ coalescingThresholdMs: 300,
283
+ });
284
+
285
+ store.edit({ otype: 'node.insert', key: '', path: 'children', value: { key: 'doc', tag: 'Doc', name: 'Document' } });
286
+ store.edit({ otype: 'text.init', key: 'doc', path: 'content', value: '' });
287
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'A' });
288
+ store.commit('first');
289
+
290
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'B' });
291
+ store.commit('second');
292
+
293
+ store.edit({ otype: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'C' });
294
+ store.commit('third');
295
+
296
+ // Should have 3 messages (one per commit)
297
+ expect(messages.length).toBe(3);
298
+
299
+ // Each message should contain the expected operation
300
+ expect(messages[0].ops.some(op => op.otype === 'text.insert' && op.content === 'A')).toBe(true);
301
+ expect(messages[1].ops.some(op => op.otype === 'text.insert' && op.content === 'B')).toBe(true);
302
+ expect(messages[2].ops.some(op => op.otype === 'text.insert' && op.content === 'C')).toBe(true);
303
+ });
304
+
305
+ it('should handle empty edits gracefully', async () => {
306
+ const messages: CRDTMessage[] = [];
307
+ const store = createGraph({
308
+ sessionId: 'alice',
309
+ onSend: (msg) => messages.push(msg),
310
+ coalescingEnabled: true,
311
+ coalescingDelayMs: 50,
312
+ });
313
+
314
+ // Wait for timer without any edits
315
+ await new Promise(resolve => setTimeout(resolve, 100));
316
+
317
+ // Should not send any messages
318
+ expect(messages.length).toBe(0);
319
+ });
320
+ });
321
+ });
@@ -239,8 +239,7 @@ describe('coalesceTextOperations', () => {
239
239
  {
240
240
  type: 'delete',
241
241
  op: {
242
- id: 'alice:3',
243
- length: 1,
242
+ deletions: [{ id: 'alice:2', length: 1 }],
244
243
  seq: 3,
245
244
  ts: 1000.1,
246
245
  },