@vuer-ai/vuer-rtc 0.5.0 → 0.5.2
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/DELETE_COALESCENCE_SOLUTION.md +345 -0
- package/dist/client/coalesceTextOperations.d.ts.map +1 -1
- package/dist/client/coalesceTextOperations.js +41 -2
- package/dist/client/coalesceTextOperations.js.map +1 -1
- package/dist/client/coalescence/textDeletes.d.ts.map +1 -1
- package/dist/client/coalescence/textDeletes.js +7 -0
- package/dist/client/coalescence/textDeletes.js.map +1 -1
- package/dist/client/coalescence/utils.d.ts +29 -0
- package/dist/client/coalescence/utils.d.ts.map +1 -0
- package/dist/client/coalescence/utils.js +52 -0
- package/dist/client/coalescence/utils.js.map +1 -0
- package/dist/crdt/Rope.d.ts.map +1 -1
- package/dist/crdt/Rope.js +22 -1
- package/dist/crdt/Rope.js.map +1 -1
- package/package.json +1 -1
- package/src/client/coalesceTextOperations.ts +40 -2
- package/src/client/coalescence/textDeletes.ts +8 -0
- package/src/client/coalescence/utils.ts +60 -0
- package/src/crdt/Rope.ts +25 -1
- package/tests/client/coalesce-graph-operations.test.ts +321 -0
- package/tests/client/coalesce-text-operations.test.ts +1 -2
- package/tests/client/delete-coalescence-bug.test.ts +249 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Delete Coalescence Solution
|
|
2
|
+
|
|
3
|
+
## Root Cause Analysis
|
|
4
|
+
|
|
5
|
+
### The Problem
|
|
6
|
+
|
|
7
|
+
When users type text locally and then delete it, the deletion array contains many single-character entries instead of being optimized:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
delete
|
|
11
|
+
7 deletions [id:Alice:7 len:1, id:Alice:6 len:1, id:Alice:5 len:1, ...]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Expected:
|
|
15
|
+
```
|
|
16
|
+
delete
|
|
17
|
+
1 deletion [id:Alice:7 len:7] // or [id:Alice:1 len:7]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Why This Happens
|
|
21
|
+
|
|
22
|
+
1. **Local typing creates separate items:**
|
|
23
|
+
- User types "k" → `insert(rope, 0, 'k')` → Item with id "Alice:1", content "k"
|
|
24
|
+
- User types "k" → `insert(rope, 1, 'k')` → Item with id "Alice:2", content "k"
|
|
25
|
+
- ... (7 times total)
|
|
26
|
+
- Result: Rope has **7 separate items**, each with content "k"
|
|
27
|
+
|
|
28
|
+
2. **Coalescence happens at network layer only:**
|
|
29
|
+
- 7 InsertOp operations are merged into 1 operation with content "kkkkkkk"
|
|
30
|
+
- This merged operation is sent over the network
|
|
31
|
+
- **Local rope is NOT updated** with the coalesced operation
|
|
32
|
+
- Local rope still has 7 separate items
|
|
33
|
+
|
|
34
|
+
3. **Remote clients create efficient structure:**
|
|
35
|
+
- Remote client receives coalesced InsertOp: `{id: "Alice:1", content: "kkkkkkk", ...}`
|
|
36
|
+
- `apply()` creates **1 item** with content "kkkkkkk" and length 7
|
|
37
|
+
- Remote rope is more efficient than local!
|
|
38
|
+
|
|
39
|
+
4. **Deletion creates many entries:**
|
|
40
|
+
- User deletes 7 characters: `remove(rope, 0, 7)`
|
|
41
|
+
- Loop processes each of the 7 items individually
|
|
42
|
+
- Creates deletion array: `[{id: Alice:1, len:1}, {id: Alice:2, len:1}, ...]`
|
|
43
|
+
- 7 deletion entries instead of 1!
|
|
44
|
+
|
|
45
|
+
### Key Insight
|
|
46
|
+
|
|
47
|
+
**Local and remote ropes have different structures for the same text!**
|
|
48
|
+
- Local (who typed): 7 items with content "k" each
|
|
49
|
+
- Remote (who received): 1 item with content "kkkkkkk"
|
|
50
|
+
|
|
51
|
+
This asymmetry causes inefficient deletions on the local side.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Proposed Solutions
|
|
56
|
+
|
|
57
|
+
### Solution 1: Optimize remove() to merge consecutive deletions ⭐ RECOMMENDED FOR TEXT CRDT
|
|
58
|
+
|
|
59
|
+
**Approach:** Keep rope structure as-is (individual items), but make `remove()` smarter. When deleting consecutive items from the same agent with consecutive sequence numbers, merge them into a single deletion entry.
|
|
60
|
+
|
|
61
|
+
**Changes:** `src/crdt/Rope.ts` - `remove()` function
|
|
62
|
+
|
|
63
|
+
**Implementation:**
|
|
64
|
+
```typescript
|
|
65
|
+
export function remove(rope: TextRope, position: number, length: number): DeleteOp {
|
|
66
|
+
if (length === 0) return { deletions: [] };
|
|
67
|
+
|
|
68
|
+
const deletions: Array<{ id: ItemId; length: number }> = [];
|
|
69
|
+
let remaining = length;
|
|
70
|
+
|
|
71
|
+
while (remaining > 0) {
|
|
72
|
+
const result = findByVisiblePos(rope._tree, position);
|
|
73
|
+
if (!result) break;
|
|
74
|
+
|
|
75
|
+
let item = result.item as Item;
|
|
76
|
+
let ordinal = result.ordinal;
|
|
77
|
+
const charOffset = result.charOffset;
|
|
78
|
+
|
|
79
|
+
if (charOffset > 0) {
|
|
80
|
+
splitItem(rope, ordinal, charOffset);
|
|
81
|
+
ordinal++;
|
|
82
|
+
item = getAt(rope._tree, ordinal) as Item;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const toDelete = Math.min(remaining, item.content.length);
|
|
86
|
+
|
|
87
|
+
if (toDelete < item.content.length) {
|
|
88
|
+
splitItem(rope, ordinal, toDelete);
|
|
89
|
+
item = getAt(rope._tree, ordinal) as Item;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
item.isDeleted = true;
|
|
93
|
+
recalcCountsFor(rope._tree, item);
|
|
94
|
+
|
|
95
|
+
// NEW: Try to merge with previous deletion if consecutive
|
|
96
|
+
const lastDeletion = deletions[deletions.length - 1];
|
|
97
|
+
|
|
98
|
+
if (lastDeletion && canMergeDeletions(lastDeletion.id, lastDeletion.length, item.id)) {
|
|
99
|
+
// Merge: extend the previous deletion
|
|
100
|
+
lastDeletion.length += item.content.length;
|
|
101
|
+
} else {
|
|
102
|
+
// Cannot merge: add new deletion entry
|
|
103
|
+
deletions.push({ id: item.id, length: item.content.length });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
remaining -= toDelete;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { deletions };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function canMergeDeletions(lastId: ItemId, lastLength: number, currentId: ItemId): boolean {
|
|
113
|
+
const { agent: lastAgent, seq: lastSeq } = parseItemId(lastId);
|
|
114
|
+
const { agent: currentAgent, seq: currentSeq } = parseItemId(currentId);
|
|
115
|
+
|
|
116
|
+
// Can merge if: same agent AND last deletion ends where current starts
|
|
117
|
+
return lastAgent === currentAgent && lastSeq + lastLength === currentSeq;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Example:**
|
|
122
|
+
```
|
|
123
|
+
Before: deletions = [{id: Alice:1, len:1}, {id: Alice:2, len:1}, {id: Alice:3, len:1}]
|
|
124
|
+
After: deletions = [{id: Alice:1, len:3}]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Pros:**
|
|
128
|
+
- ✅ Simple, localized fix in one function
|
|
129
|
+
- ✅ No changes to rope structure or CRDT semantics
|
|
130
|
+
- ✅ Works for both local and remote deletions
|
|
131
|
+
- ✅ Safe - preserves YATA correctness
|
|
132
|
+
- ✅ Automatic - no changes needed elsewhere
|
|
133
|
+
|
|
134
|
+
**Cons:**
|
|
135
|
+
- ⚠️ Doesn't fix underlying asymmetry (local rope still has more items than remote)
|
|
136
|
+
- ⚠️ Only optimizes deletions, not the rope structure itself
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### Solution 2: Optimize deletions in coalescence functions ⭐ RECOMMENDED FOR GRAPH CRDT
|
|
141
|
+
|
|
142
|
+
**Approach:** After merging delete operations, optimize the deletions array by merging consecutive entries.
|
|
143
|
+
|
|
144
|
+
**Changes:**
|
|
145
|
+
- `src/client/coalesceTextOperations.ts` - add optimization step
|
|
146
|
+
- `src/client/coalescence/textDeletes.ts` - add optimization step
|
|
147
|
+
- `src/client/coalesceGraphOps.ts` - add optimization for text.delete operations
|
|
148
|
+
|
|
149
|
+
**Implementation:**
|
|
150
|
+
```typescript
|
|
151
|
+
// New helper function
|
|
152
|
+
function optimizeDeletions(
|
|
153
|
+
deletions: Array<{ id: ItemId; length: number }>
|
|
154
|
+
): Array<{ id: ItemId; length: number }> {
|
|
155
|
+
if (deletions.length === 0) return deletions;
|
|
156
|
+
|
|
157
|
+
const result: Array<{ id: ItemId; length: number }> = [];
|
|
158
|
+
let current = { ...deletions[0] };
|
|
159
|
+
|
|
160
|
+
for (let i = 1; i < deletions.length; i++) {
|
|
161
|
+
const deletion = deletions[i];
|
|
162
|
+
const { agent: currAgent, seq: currSeq } = parseItemId(current.id);
|
|
163
|
+
const { agent: delAgent, seq: delSeq } = parseItemId(deletion.id);
|
|
164
|
+
|
|
165
|
+
// Can merge if same agent and consecutive sequence numbers
|
|
166
|
+
if (currAgent === delAgent && currSeq + current.length === delSeq) {
|
|
167
|
+
current.length += deletion.length;
|
|
168
|
+
} else {
|
|
169
|
+
result.push(current);
|
|
170
|
+
current = { ...deletion };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result.push(current);
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// In coalesceTextDeletes:
|
|
179
|
+
export function coalesceTextDeletes(
|
|
180
|
+
ops: TextDeleteOp[],
|
|
181
|
+
options: CoalesceOptions = {}
|
|
182
|
+
): TextDeleteOp[] {
|
|
183
|
+
// ... existing code to merge operations ...
|
|
184
|
+
|
|
185
|
+
if (pending !== null) {
|
|
186
|
+
// NEW: Optimize deletions array before returning
|
|
187
|
+
pending.deletions = optimizeDeletions(pending.deletions);
|
|
188
|
+
result.push(pending);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Example:**
|
|
196
|
+
```
|
|
197
|
+
Before coalescence:
|
|
198
|
+
Op1: deletions = [{id: Alice:1, len:1}]
|
|
199
|
+
Op2: deletions = [{id: Alice:2, len:1}]
|
|
200
|
+
Op3: deletions = [{id: Alice:3, len:1}]
|
|
201
|
+
|
|
202
|
+
After merging ops:
|
|
203
|
+
Op: deletions = [{id: Alice:1, len:1}, {id: Alice:2, len:1}, {id: Alice:3, len:1}]
|
|
204
|
+
|
|
205
|
+
After optimizing deletions:
|
|
206
|
+
Op: deletions = [{id: Alice:1, len:3}]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Pros:**
|
|
210
|
+
- ✅ Optimizes deletions at the operation level
|
|
211
|
+
- ✅ Works with existing coalescence infrastructure
|
|
212
|
+
- ✅ Complements Solution 1 (can use both)
|
|
213
|
+
- ✅ Handles both text CRDT and graph CRDT
|
|
214
|
+
|
|
215
|
+
**Cons:**
|
|
216
|
+
- ⚠️ Need to call optimization in multiple places
|
|
217
|
+
- ⚠️ Still doesn't fix the asymmetry
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### Solution 3: Apply coalesced operations to local rope ⛔ NOT RECOMMENDED
|
|
222
|
+
|
|
223
|
+
**Approach:** After coalescence, apply the coalesced operation to the local rope, replacing the individual items.
|
|
224
|
+
|
|
225
|
+
**Why NOT recommended:**
|
|
226
|
+
- ❌ Very complex - need to track which operations have been applied
|
|
227
|
+
- ❌ Changes semantics of local edits (modifying history)
|
|
228
|
+
- ❌ Could break cursor tracking and undo/redo
|
|
229
|
+
- ❌ Risky - affects core CRDT operations
|
|
230
|
+
- ❌ May violate YATA ordering guarantees
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### Solution 4: Merge consecutive items in the rope ⛔ NOT RECOMMENDED
|
|
235
|
+
|
|
236
|
+
**Approach:** After applying operations, detect and merge consecutive items from the same agent.
|
|
237
|
+
|
|
238
|
+
**Why NOT recommended:**
|
|
239
|
+
- ❌ Very complex - need to update B-tree, indices, ordinals
|
|
240
|
+
- ❌ Could break YATA ordering if not done carefully
|
|
241
|
+
- ❌ Might interfere with concurrent operations
|
|
242
|
+
- ❌ Changes item IDs, which could break references
|
|
243
|
+
- ❌ High risk, low reward
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Recommended Approach: HYBRID (Solutions 1 + 2) ⭐
|
|
248
|
+
|
|
249
|
+
Implement **both** Solution 1 and Solution 2 for comprehensive optimization:
|
|
250
|
+
|
|
251
|
+
### Part A: Text CRDT Level (Solution 1)
|
|
252
|
+
- Optimize `remove()` in `src/crdt/Rope.ts`
|
|
253
|
+
- Merges consecutive deletions when deleting from the rope
|
|
254
|
+
- Benefits: All deletions from rope are automatically optimized
|
|
255
|
+
|
|
256
|
+
### Part B: Graph CRDT Level (Solution 2)
|
|
257
|
+
- Add `optimizeDeletions()` helper function
|
|
258
|
+
- Call it in `coalesceTextDeletes()` after merging operations
|
|
259
|
+
- Call it in `coalesceGraphOps()` for text.delete operations
|
|
260
|
+
- Benefits: Deletions are optimized even when merging operations
|
|
261
|
+
|
|
262
|
+
### Why Hybrid?
|
|
263
|
+
|
|
264
|
+
1. **Comprehensive:** Optimizes at all levels
|
|
265
|
+
- Rope level (Part A): Direct deletions always optimized
|
|
266
|
+
- Coalescence level (Part B): Merged operations also optimized
|
|
267
|
+
|
|
268
|
+
2. **Safe:** No changes to rope structure or CRDT semantics
|
|
269
|
+
- Items remain separate (preserves YATA correctness)
|
|
270
|
+
- Only optimization is in the deletions array
|
|
271
|
+
|
|
272
|
+
3. **Effective:** Reduces deletion arrays significantly
|
|
273
|
+
- O(n) deletions → O(agents) in best case
|
|
274
|
+
- Example: 7 single-char deletions → 1 deletion with len:7
|
|
275
|
+
|
|
276
|
+
4. **Maintainable:** Localized changes
|
|
277
|
+
- One helper function: `optimizeDeletions()`
|
|
278
|
+
- Three call sites: `remove()`, `coalesceTextDeletes()`, `coalesceGraphOps()`
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Implementation Plan
|
|
283
|
+
|
|
284
|
+
### Phase 1: Text CRDT (Rope.ts)
|
|
285
|
+
1. Add `canMergeDeletions()` helper function
|
|
286
|
+
2. Modify `remove()` to merge consecutive deletions
|
|
287
|
+
3. Add tests in `delete-coalescence-bug.test.ts`
|
|
288
|
+
4. Verify all existing tests still pass
|
|
289
|
+
|
|
290
|
+
### Phase 2: Graph CRDT (coalesceGraphOps.ts)
|
|
291
|
+
1. Add `optimizeDeletions()` helper function to `src/client/coalescence/utils.ts`
|
|
292
|
+
2. Import and use in `coalesceTextDeletes()` in `src/client/coalescence/textDeletes.ts`
|
|
293
|
+
3. Import and use in `coalesceGraphOps()` for text.delete operations
|
|
294
|
+
4. Add tests in `coalesce-graph-operations.test.ts`
|
|
295
|
+
5. Verify all existing tests still pass
|
|
296
|
+
|
|
297
|
+
### Phase 3: Testing & Validation
|
|
298
|
+
1. Run full test suite: `pnpm test`
|
|
299
|
+
2. Test in demo: verify deletion arrays are optimized
|
|
300
|
+
3. Export debug data and verify deletions have merged IDs
|
|
301
|
+
4. Performance test: ensure no regression
|
|
302
|
+
|
|
303
|
+
### Phase 4: Documentation & Release
|
|
304
|
+
1. Update release notes
|
|
305
|
+
2. Bump version to 0.5.2
|
|
306
|
+
3. Publish to npm
|
|
307
|
+
4. Deploy docs
|
|
308
|
+
5. Create release documentation
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Expected Results
|
|
313
|
+
|
|
314
|
+
### Before Fix
|
|
315
|
+
```
|
|
316
|
+
// User types "kkkkkkk" (7 chars) and deletes
|
|
317
|
+
delete
|
|
318
|
+
7 deletions [id:Alice:1 len:1, id:Alice:2 len:1, id:Alice:3 len:1,
|
|
319
|
+
id:Alice:4 len:1, id:Alice:5 len:1, id:Alice:6 len:1,
|
|
320
|
+
id:Alice:7 len:1]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### After Fix
|
|
324
|
+
```
|
|
325
|
+
// User types "kkkkkkk" (7 chars) and deletes
|
|
326
|
+
delete
|
|
327
|
+
1 deletion [id:Alice:1 len:7]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Network Savings
|
|
331
|
+
- **Before:** 7 deletion entries × ~30 bytes each = ~210 bytes
|
|
332
|
+
- **After:** 1 deletion entry × ~30 bytes = ~30 bytes
|
|
333
|
+
- **Reduction:** 85% smaller deletion array
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Conclusion
|
|
338
|
+
|
|
339
|
+
The **Hybrid approach (Solutions 1 + 2)** provides the best balance of:
|
|
340
|
+
- ✅ Effectiveness (optimizes deletions at multiple levels)
|
|
341
|
+
- ✅ Safety (no changes to CRDT semantics)
|
|
342
|
+
- ✅ Simplicity (localized changes, easy to understand)
|
|
343
|
+
- ✅ Maintainability (small helper functions, clear logic)
|
|
344
|
+
|
|
345
|
+
This approach fixes the immediate problem (inefficient deletions) without introducing the complexity and risk of restructuring the rope itself.
|
|
@@ -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,
|
|
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
|
-
//
|
|
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
|
|
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":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAO9C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CACpC,GAAoB,EACpB,UAA2B,EAAE;IAE7B,MAAM,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAEvC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEjC,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,IAAI,aAAa,GAA4C,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"coalesceTextOperations.js","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textDeletes.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/textDeletes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"textDeletes.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/textDeletes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAGpE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,aAAa,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,YAAY,CAOhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EAAE,EACnB,OAAO,GAAE,eAAoB,GAC5B,YAAY,EAAE,CAoDhB"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Merges consecutive text delete operations.
|
|
5
5
|
*/
|
|
6
|
+
import { optimizeDeletions } from './utils.js';
|
|
6
7
|
/**
|
|
7
8
|
* Check if an operation is a text delete with CRDT metadata
|
|
8
9
|
*/
|
|
@@ -61,8 +62,14 @@ export function coalesceTextDeletes(ops, options = {}) {
|
|
|
61
62
|
}
|
|
62
63
|
// Flush any remaining pending delete
|
|
63
64
|
if (pending !== null) {
|
|
65
|
+
// Optimize deletions array before pushing
|
|
66
|
+
pending.deletions = optimizeDeletions(pending.deletions);
|
|
64
67
|
result.push(pending);
|
|
65
68
|
}
|
|
69
|
+
// Also optimize deletions in all previously pushed operations
|
|
70
|
+
for (const op of result) {
|
|
71
|
+
op.deletions = optimizeDeletions(op.deletions);
|
|
72
|
+
}
|
|
66
73
|
return result;
|
|
67
74
|
}
|
|
68
75
|
//# sourceMappingURL=textDeletes.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textDeletes.js","sourceRoot":"","sources":["../../../src/client/coalescence/textDeletes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"textDeletes.js","sourceRoot":"","sources":["../../../src/client/coalescence/textDeletes.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAgB/C;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,EAAa;IAC1C,OAAO,CACL,EAAE,CAAC,KAAK,KAAK,aAAa;QAC1B,KAAK,CAAC,OAAO,CAAE,EAAU,CAAC,SAAS,CAAC;QACpC,OAAQ,EAAU,CAAC,GAAG,KAAK,QAAQ;QACnC,OAAQ,EAAU,CAAC,EAAE,KAAK,QAAQ,CACnC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAmB,EACnB,UAA2B,EAAE;IAE7B,MAAM,EAAE,WAAW,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAEvC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEjC,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,IAAI,OAAO,GAAwB,IAAI,CAAC;IAExC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,2BAA2B;YAC3B,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;QACpD,CAAC;aAAM,CAAC;YACN,yBAAyB;YACzB,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,CAAC;YAEtE,sEAAsE;YACtE,MAAM,UAAU,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;YAC/C,MAAM,eAAe,GAAG,UAAU,IAAI,WAAW,CAAC;YAElD,IAAI,UAAU,IAAI,eAAe,EAAE,CAAC;gBAClC,4CAA4C;gBAC5C,MAAM,MAAM,GAAiB;oBAC3B,KAAK,EAAE,aAAa;oBACpB,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,SAAS,EAAE,CAAC,GAAG,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC;oBAClD,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,qCAAqC;oBACzE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,+CAA+C;iBAChE,CAAC;gBACF,OAAO,GAAG,MAAM,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,4CAA4C;gBAC5C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;YACpD,CAAC;QACH,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,0CAA0C;QAC1C,OAAO,CAAC,SAAS,GAAG,iBAAiB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACzD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,8DAA8D;IAC9D,KAAK,MAAM,EAAE,IAAI,MAAM,EAAE,CAAC;QACxB,EAAE,CAAC,SAAS,GAAG,iBAAiB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for operation coalescence
|
|
3
|
+
*/
|
|
4
|
+
import type { ItemId } from '../../crdt/Rope.js';
|
|
5
|
+
/**
|
|
6
|
+
* Parse an ItemId into agent and sequence number.
|
|
7
|
+
* ItemId format: "agent:seq" (e.g., "alice:42")
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseItemId(id: ItemId): {
|
|
10
|
+
agent: string;
|
|
11
|
+
seq: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Optimize a deletions array by merging consecutive deletions from the same agent.
|
|
15
|
+
*
|
|
16
|
+
* Example:
|
|
17
|
+
* Input: [{id: "alice:1", length: 1}, {id: "alice:2", length: 1}, {id: "alice:3", length: 1}]
|
|
18
|
+
* Output: [{id: "alice:1", length: 3}]
|
|
19
|
+
*
|
|
20
|
+
* This reduces the size of deletion arrays when consecutive items are deleted.
|
|
21
|
+
*/
|
|
22
|
+
export declare function optimizeDeletions(deletions: Array<{
|
|
23
|
+
id: ItemId;
|
|
24
|
+
length: number;
|
|
25
|
+
}>): Array<{
|
|
26
|
+
id: ItemId;
|
|
27
|
+
length: number;
|
|
28
|
+
}>;
|
|
29
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAStE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAC/C,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA2BvC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for operation coalescence
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Parse an ItemId into agent and sequence number.
|
|
6
|
+
* ItemId format: "agent:seq" (e.g., "alice:42")
|
|
7
|
+
*/
|
|
8
|
+
export function parseItemId(id) {
|
|
9
|
+
const colonIndex = id.lastIndexOf(':');
|
|
10
|
+
if (colonIndex === -1) {
|
|
11
|
+
// Fallback for invalid format
|
|
12
|
+
return { agent: id, seq: 0 };
|
|
13
|
+
}
|
|
14
|
+
const agent = id.substring(0, colonIndex);
|
|
15
|
+
const seq = parseInt(id.substring(colonIndex + 1), 10);
|
|
16
|
+
return { agent, seq: isNaN(seq) ? 0 : seq };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Optimize a deletions array by merging consecutive deletions from the same agent.
|
|
20
|
+
*
|
|
21
|
+
* Example:
|
|
22
|
+
* Input: [{id: "alice:1", length: 1}, {id: "alice:2", length: 1}, {id: "alice:3", length: 1}]
|
|
23
|
+
* Output: [{id: "alice:1", length: 3}]
|
|
24
|
+
*
|
|
25
|
+
* This reduces the size of deletion arrays when consecutive items are deleted.
|
|
26
|
+
*/
|
|
27
|
+
export function optimizeDeletions(deletions) {
|
|
28
|
+
if (deletions.length === 0)
|
|
29
|
+
return deletions;
|
|
30
|
+
const result = [];
|
|
31
|
+
let current = { ...deletions[0] };
|
|
32
|
+
for (let i = 1; i < deletions.length; i++) {
|
|
33
|
+
const deletion = deletions[i];
|
|
34
|
+
const currParsed = parseItemId(current.id);
|
|
35
|
+
const delParsed = parseItemId(deletion.id);
|
|
36
|
+
// Can merge if: same agent AND current deletion ends where next starts
|
|
37
|
+
if (currParsed.agent === delParsed.agent &&
|
|
38
|
+
currParsed.seq + current.length === delParsed.seq) {
|
|
39
|
+
// Merge: extend current deletion
|
|
40
|
+
current.length += deletion.length;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Cannot merge: push current and start new
|
|
44
|
+
result.push(current);
|
|
45
|
+
current = { ...deletion };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Push final deletion
|
|
49
|
+
result.push(current);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/client/coalescence/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,UAAU,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;QACtB,8BAA8B;QAC9B,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IAC/B,CAAC;IACD,MAAM,KAAK,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,QAAQ,CAAC,EAAE,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,SAAgD;IAEhD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE7C,MAAM,MAAM,GAA0C,EAAE,CAAC;IACzD,IAAI,OAAO,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IAElC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAE3C,uEAAuE;QACvE,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS,CAAC,KAAK;YACpC,UAAU,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,GAAG,EAAE,CAAC;YACtD,iCAAiC;YACjC,OAAO,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC;QACpC,CAAC;aAAM,CAAC;YACN,2CAA2C;YAC3C,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,OAAO,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,sBAAsB;IACtB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAErB,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/crdt/Rope.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Rope.d.ts","sourceRoot":"","sources":["../../src/crdt/Rope.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAIL,KAAK,QAAQ,EACd,MAAM,YAAY,CAAC;AAMpB;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;;;;GAKG;AACH,qBAAa,QAAQ;IACnB,KAAK,EAAE,QAAQ,CAAC;IAChB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;gBAGb,IAAI,EAAE,QAAQ,EACd,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,EACpC,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;IAShB;;;OAGG;IACH,QAAQ,IAAI,MAAM;IAIlB;;;;;;OAMG;IACH,MAAM,IAAI,MAAM;CAGjB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB;AAMD;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAExE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAMtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/D;AAgED,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAEhD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAM9C;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEhD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,CAE/C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAWtE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB,CAYA;AAgND,wBAAgB,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAgBpG;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAmB7G;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,GAAG,IAAI,CAsBxD;
|
|
1
|
+
{"version":3,"file":"Rope.d.ts","sourceRoot":"","sources":["../../src/crdt/Rope.ts"],"names":[],"mappings":"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"}
|
package/dist/crdt/Rope.js
CHANGED
|
@@ -437,6 +437,18 @@ export function apply(rope, op) {
|
|
|
437
437
|
// ============================================
|
|
438
438
|
// Operations — Delete
|
|
439
439
|
// ============================================
|
|
440
|
+
/**
|
|
441
|
+
* Check if two deletions can be merged into one.
|
|
442
|
+
* Deletions can be merged if they are from the same agent and the first
|
|
443
|
+
* deletion ends exactly where the second one starts (consecutive sequence numbers).
|
|
444
|
+
*/
|
|
445
|
+
function canMergeDeletions(lastId, lastLength, currentId) {
|
|
446
|
+
const lastParsed = parseItemId(lastId);
|
|
447
|
+
const currentParsed = parseItemId(currentId);
|
|
448
|
+
// Can merge if: same agent AND last deletion ends where current starts
|
|
449
|
+
return lastParsed.agent === currentParsed.agent &&
|
|
450
|
+
lastParsed.seq + lastLength === currentParsed.seq;
|
|
451
|
+
}
|
|
440
452
|
export function remove(rope, position, length) {
|
|
441
453
|
if (length === 0)
|
|
442
454
|
return { deletions: [] };
|
|
@@ -464,7 +476,16 @@ export function remove(rope, position, length) {
|
|
|
464
476
|
// Delete the item
|
|
465
477
|
item.isDeleted = true;
|
|
466
478
|
recalcCountsFor(rope._tree, item);
|
|
467
|
-
|
|
479
|
+
// Try to merge with previous deletion if consecutive
|
|
480
|
+
const lastDeletion = deletions[deletions.length - 1];
|
|
481
|
+
if (lastDeletion && canMergeDeletions(lastDeletion.id, lastDeletion.length, item.id)) {
|
|
482
|
+
// Merge: extend the previous deletion
|
|
483
|
+
lastDeletion.length += item.content.length;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// Cannot merge: add new deletion entry
|
|
487
|
+
deletions.push({ id: item.id, length: item.content.length });
|
|
488
|
+
}
|
|
468
489
|
remaining -= toDelete;
|
|
469
490
|
}
|
|
470
491
|
return { deletions };
|