@vuer-ai/vuer-rtc 0.5.3 → 0.5.4
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/client/coalesceTextOperations.d.ts.map +1 -1
- package/dist/client/coalesceTextOperations.js +25 -0
- package/dist/client/coalesceTextOperations.js.map +1 -1
- package/dist/client/coalescence/textDeletes.d.ts.map +1 -1
- package/dist/client/coalescence/textDeletes.js +23 -5
- package/dist/client/coalescence/textDeletes.js.map +1 -1
- package/package.json +1 -1
- package/src/client/coalesceTextOperations.ts +27 -0
- package/src/client/coalescence/textDeletes.ts +25 -5
- package/tests/client/coalesce-text-operations.test.ts +99 -0
- package/tests/client/delete-coalescence-bug.test.ts +7 -7
|
@@ -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;
|
|
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;AAIhD,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAsBD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,aAAa,EAAE,EACpB,OAAO,GAAE,eAAoB,GAC5B,aAAa,EAAE,CA6HjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI/D"}
|
|
@@ -11,6 +11,25 @@
|
|
|
11
11
|
* 4. Compatible YATA structure (forms a chain)
|
|
12
12
|
*/
|
|
13
13
|
import { parseItemId } from '../crdt/Rope.js';
|
|
14
|
+
import { optimizeDeletions, parseItemId as parseId } from './coalescence/utils.js';
|
|
15
|
+
/**
|
|
16
|
+
* Sort and optimize deletions array.
|
|
17
|
+
* Deletions must be sorted in ascending order for optimizeDeletions to work correctly.
|
|
18
|
+
*/
|
|
19
|
+
function sortAndOptimizeDeletions(deletions) {
|
|
20
|
+
if (deletions.length === 0)
|
|
21
|
+
return deletions;
|
|
22
|
+
// Sort deletions by agent, then by sequence number (ascending)
|
|
23
|
+
const sorted = [...deletions].sort((a, b) => {
|
|
24
|
+
const aId = parseId(a.id);
|
|
25
|
+
const bId = parseId(b.id);
|
|
26
|
+
if (aId.agent !== bId.agent) {
|
|
27
|
+
return aId.agent.localeCompare(bId.agent);
|
|
28
|
+
}
|
|
29
|
+
return aId.seq - bId.seq;
|
|
30
|
+
});
|
|
31
|
+
return optimizeDeletions(sorted);
|
|
32
|
+
}
|
|
14
33
|
/**
|
|
15
34
|
* Coalesce consecutive text insert operations.
|
|
16
35
|
*
|
|
@@ -33,6 +52,8 @@ export function coalesceTextOperations(ops, options = {}) {
|
|
|
33
52
|
if (op.type === 'insert') {
|
|
34
53
|
// Flush any pending delete before starting new insert
|
|
35
54
|
if (pendingDelete !== null) {
|
|
55
|
+
// Sort and optimize deletions array before flushing
|
|
56
|
+
pendingDelete.op.deletions = sortAndOptimizeDeletions(pendingDelete.op.deletions);
|
|
36
57
|
result.push(pendingDelete);
|
|
37
58
|
pendingDelete = null;
|
|
38
59
|
}
|
|
@@ -123,6 +144,8 @@ export function coalesceTextOperations(ops, options = {}) {
|
|
|
123
144
|
pendingInsert = null;
|
|
124
145
|
}
|
|
125
146
|
if (pendingDelete !== null) {
|
|
147
|
+
// Sort and optimize deletions array before flushing
|
|
148
|
+
pendingDelete.op.deletions = sortAndOptimizeDeletions(pendingDelete.op.deletions);
|
|
126
149
|
result.push(pendingDelete);
|
|
127
150
|
pendingDelete = null;
|
|
128
151
|
}
|
|
@@ -134,6 +157,8 @@ export function coalesceTextOperations(ops, options = {}) {
|
|
|
134
157
|
result.push(pendingInsert);
|
|
135
158
|
}
|
|
136
159
|
if (pendingDelete !== null) {
|
|
160
|
+
// Sort and optimize deletions array before flushing
|
|
161
|
+
pendingDelete.op.deletions = sortAndOptimizeDeletions(pendingDelete.op.deletions);
|
|
137
162
|
result.push(pendingDelete);
|
|
138
163
|
}
|
|
139
164
|
return result;
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"coalesceTextOperations.js","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,WAAW,IAAI,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAOnF;;;GAGG;AACH,SAAS,wBAAwB,CAAC,SAAgD;IAChF,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE7C,+DAA+D;IAC/D,MAAM,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1B,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1B,IAAI,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,CAAC;YAC5B,OAAO,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;;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;IAClE,IAAI,aAAa,GAAuC,IAAI,CAAC;IAE7D,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,EAAE,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACzB,sDAAsD;YACtD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;gBAC3B,oDAAoD;gBACpD,aAAa,CAAC,EAAE,CAAC,SAAS,GAAG,wBAAwB,CAAC,aAAa,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;gBAClF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC3B,aAAa,GAAG,IAAI,CAAC;YACvB,CAAC;YACD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;gBAC3B,2BAA2B;gBAC3B,aAAa,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC;YACvD,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,aAAa,CAAC,EAAE,CAAC;gBAChC,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC;gBAErB,wDAAwD;gBACxD,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACtC,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBAEtC,yBAAyB;gBACzB,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,CAAC;gBAEhD,kEAAkE;gBAClE,kEAAkE;gBAClE,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;gBAExE,sEAAsE;gBACtE,MAAM,UAAU,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;gBAClD,MAAM,eAAe,GAAG,UAAU,IAAI,WAAW,CAAC;gBAElD,yDAAyD;gBACzD,4EAA4E;gBAC5E,qEAAqE;gBACrE,mEAAmE;gBACnE,MAAM,UAAU,GAAI,MAAc,CAAC,WAAW,IAAI,MAAM,CAAC,EAAE,CAAC;gBAC5D,MAAM,UAAU,GACd,MAAM,CAAC,QAAQ,KAAK,UAAU;oBAC9B,CAAC,MAAM,CAAC,QAAQ,KAAK,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;gBAEpE,IAAI,SAAS,IAAI,aAAa,IAAI,eAAe,IAAI,UAAU,EAAE,CAAC;oBAChE,mBAAmB;oBACnB,MAAM,QAAQ,GAAa;wBACzB,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,+BAA+B;wBAC9C,OAAO,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,sBAAsB;wBAChE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,sBAAsB;wBACjD,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,qCAAqC;wBAC5E,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,+CAA+C;wBAC9D,iEAAiE;wBACjE,WAAW,EAAE,MAAM,CAAC,EAAE;qBAChB,CAAC;oBACT,aAAa,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC;gBACnD,CAAC;qBAAM,CAAC;oBACN,8BAA8B;oBAC9B,IAAI,CAAC,SAAS;wBAAE,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;oBAC9F,IAAI,CAAC,aAAa;wBAAE,OAAO,CAAC,GAAG,CAAC,gCAAgC,EAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;oBACzH,IAAI,CAAC,eAAe;wBAAE,OAAO,CAAC,GAAG,CAAC,qCAAqC,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,CAAC;oBAChH,IAAI,CAAC,UAAU;wBAAE,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;oBAEjK,4CAA4C;oBAC5C,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;oBAC3B,aAAa,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,CAAC;gBACxD,CAAC;YACH,CAAC;QACH,CAAC;aAAM,IAAI,EAAE,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChC,kDAAkD;YAClD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC3B,aAAa,GAAG,IAAI,CAAC;YACvB,CAAC;YAED,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;gBAC3B,2BAA2B;gBAC3B,aAAa,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;YACxF,CAAC;iBAAM,CAAC;gBACN,6CAA6C;gBAC7C,6DAA6D;gBAC7D,yCAAyC;gBACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,kDAAkD;gBAEzE,IAAI,QAAQ,EAAE,CAAC;oBACb,+BAA+B;oBAC/B,aAAa,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;gBACtD,CAAC;qBAAM,CAAC;oBACN,4CAA4C;oBAC5C,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;oBAC3B,aAAa,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC;gBACxF,CAAC;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,4CAA4C;YAC5C,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC3B,aAAa,GAAG,IAAI,CAAC;YACvB,CAAC;YACD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;gBAC3B,oDAAoD;gBACpD,aAAa,CAAC,EAAE,CAAC,SAAS,GAAG,wBAAwB,CAAC,aAAa,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;gBAClF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC3B,aAAa,GAAG,IAAI,CAAC;YACvB,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,yCAAyC;IACzC,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,oDAAoD;QACpD,aAAa,CAAC,EAAE,CAAC,SAAS,GAAG,wBAAwB,CAAC,aAAa,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;QAClF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7B,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,CAAW,EAAE,CAAW;IACrD,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG;QAAE,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,gBAAgB;IAC3D,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE;QAAE,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,kBAAkB;IACzD,OAAO,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,iBAAiB;AACpD,CAAC"}
|
|
@@ -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;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;CAGlD;AAED,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;
|
|
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;CAGlD;AAED,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAsBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,YAAY,CAMhE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EAAE,EACnB,OAAO,GAAE,eAAoB,GAC5B,YAAY,EAAE,CAgDhB"}
|
|
@@ -3,7 +3,25 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Merges consecutive text delete operations.
|
|
5
5
|
*/
|
|
6
|
-
import { optimizeDeletions } from './utils.js';
|
|
6
|
+
import { optimizeDeletions, parseItemId } from './utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Sort and optimize deletions array.
|
|
9
|
+
* Deletions must be sorted in ascending order for optimizeDeletions to work correctly.
|
|
10
|
+
*/
|
|
11
|
+
function sortAndOptimizeDeletions(deletions) {
|
|
12
|
+
if (deletions.length === 0)
|
|
13
|
+
return deletions;
|
|
14
|
+
// Sort deletions by agent, then by sequence number (ascending)
|
|
15
|
+
const sorted = [...deletions].sort((a, b) => {
|
|
16
|
+
const aId = parseItemId(a.id);
|
|
17
|
+
const bId = parseItemId(b.id);
|
|
18
|
+
if (aId.agent !== bId.agent) {
|
|
19
|
+
return aId.agent.localeCompare(bId.agent);
|
|
20
|
+
}
|
|
21
|
+
return aId.seq - bId.seq;
|
|
22
|
+
});
|
|
23
|
+
return optimizeDeletions(sorted);
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
26
|
* Check if an operation is a text delete with CRDT metadata
|
|
9
27
|
*/
|
|
@@ -58,13 +76,13 @@ export function coalesceTextDeletes(ops, options = {}) {
|
|
|
58
76
|
}
|
|
59
77
|
// Flush any remaining pending delete
|
|
60
78
|
if (pending !== null) {
|
|
61
|
-
//
|
|
62
|
-
pending.deletions =
|
|
79
|
+
// Sort and optimize deletions array before pushing
|
|
80
|
+
pending.deletions = sortAndOptimizeDeletions(pending.deletions);
|
|
63
81
|
result.push(pending);
|
|
64
82
|
}
|
|
65
|
-
// Also optimize deletions in all previously pushed operations
|
|
83
|
+
// Also sort and optimize deletions in all previously pushed operations
|
|
66
84
|
for (const op of result) {
|
|
67
|
-
op.deletions =
|
|
85
|
+
op.deletions = sortAndOptimizeDeletions(op.deletions);
|
|
68
86
|
}
|
|
69
87
|
return result;
|
|
70
88
|
}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"textDeletes.js","sourceRoot":"","sources":["../../../src/client/coalescence/textDeletes.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -14,12 +14,33 @@
|
|
|
14
14
|
import type { TextOperation } from './textTypes.js';
|
|
15
15
|
import type { InsertOp } from '../crdt/Rope.js';
|
|
16
16
|
import { parseItemId } from '../crdt/Rope.js';
|
|
17
|
+
import { optimizeDeletions, parseItemId as parseId } from './coalescence/utils.js';
|
|
17
18
|
|
|
18
19
|
export interface CoalesceOptions {
|
|
19
20
|
/** Time threshold in milliseconds (default: 1000ms = 1 second) */
|
|
20
21
|
thresholdMs?: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Sort and optimize deletions array.
|
|
26
|
+
* Deletions must be sorted in ascending order for optimizeDeletions to work correctly.
|
|
27
|
+
*/
|
|
28
|
+
function sortAndOptimizeDeletions(deletions: Array<{ id: string; length: number }>): Array<{ id: string; length: number }> {
|
|
29
|
+
if (deletions.length === 0) return deletions;
|
|
30
|
+
|
|
31
|
+
// Sort deletions by agent, then by sequence number (ascending)
|
|
32
|
+
const sorted = [...deletions].sort((a, b) => {
|
|
33
|
+
const aId = parseId(a.id);
|
|
34
|
+
const bId = parseId(b.id);
|
|
35
|
+
if (aId.agent !== bId.agent) {
|
|
36
|
+
return aId.agent.localeCompare(bId.agent);
|
|
37
|
+
}
|
|
38
|
+
return aId.seq - bId.seq;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return optimizeDeletions(sorted);
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
/**
|
|
24
45
|
* Coalesce consecutive text insert operations.
|
|
25
46
|
*
|
|
@@ -47,6 +68,8 @@ export function coalesceTextOperations(
|
|
|
47
68
|
if (op.type === 'insert') {
|
|
48
69
|
// Flush any pending delete before starting new insert
|
|
49
70
|
if (pendingDelete !== null) {
|
|
71
|
+
// Sort and optimize deletions array before flushing
|
|
72
|
+
pendingDelete.op.deletions = sortAndOptimizeDeletions(pendingDelete.op.deletions);
|
|
50
73
|
result.push(pendingDelete);
|
|
51
74
|
pendingDelete = null;
|
|
52
75
|
}
|
|
@@ -137,6 +160,8 @@ export function coalesceTextOperations(
|
|
|
137
160
|
pendingInsert = null;
|
|
138
161
|
}
|
|
139
162
|
if (pendingDelete !== null) {
|
|
163
|
+
// Sort and optimize deletions array before flushing
|
|
164
|
+
pendingDelete.op.deletions = sortAndOptimizeDeletions(pendingDelete.op.deletions);
|
|
140
165
|
result.push(pendingDelete);
|
|
141
166
|
pendingDelete = null;
|
|
142
167
|
}
|
|
@@ -149,6 +174,8 @@ export function coalesceTextOperations(
|
|
|
149
174
|
result.push(pendingInsert);
|
|
150
175
|
}
|
|
151
176
|
if (pendingDelete !== null) {
|
|
177
|
+
// Sort and optimize deletions array before flushing
|
|
178
|
+
pendingDelete.op.deletions = sortAndOptimizeDeletions(pendingDelete.op.deletions);
|
|
152
179
|
result.push(pendingDelete);
|
|
153
180
|
}
|
|
154
181
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Operation } from '../../operations/OperationTypes.js';
|
|
8
|
-
import { optimizeDeletions } from './utils.js';
|
|
8
|
+
import { optimizeDeletions, parseItemId } from './utils.js';
|
|
9
9
|
|
|
10
10
|
export interface TextDeleteOp {
|
|
11
11
|
otype: 'text.delete';
|
|
@@ -21,6 +21,26 @@ export interface CoalesceOptions {
|
|
|
21
21
|
thresholdMs?: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Sort and optimize deletions array.
|
|
26
|
+
* Deletions must be sorted in ascending order for optimizeDeletions to work correctly.
|
|
27
|
+
*/
|
|
28
|
+
function sortAndOptimizeDeletions(deletions: Array<{ id: string; length: number }>): Array<{ id: string; length: number }> {
|
|
29
|
+
if (deletions.length === 0) return deletions;
|
|
30
|
+
|
|
31
|
+
// Sort deletions by agent, then by sequence number (ascending)
|
|
32
|
+
const sorted = [...deletions].sort((a, b) => {
|
|
33
|
+
const aId = parseItemId(a.id);
|
|
34
|
+
const bId = parseItemId(b.id);
|
|
35
|
+
if (aId.agent !== bId.agent) {
|
|
36
|
+
return aId.agent.localeCompare(bId.agent);
|
|
37
|
+
}
|
|
38
|
+
return aId.seq - bId.seq;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return optimizeDeletions(sorted);
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
/**
|
|
25
45
|
* Check if an operation is a text delete with CRDT metadata
|
|
26
46
|
*/
|
|
@@ -83,14 +103,14 @@ export function coalesceTextDeletes(
|
|
|
83
103
|
|
|
84
104
|
// Flush any remaining pending delete
|
|
85
105
|
if (pending !== null) {
|
|
86
|
-
//
|
|
87
|
-
pending.deletions =
|
|
106
|
+
// Sort and optimize deletions array before pushing
|
|
107
|
+
pending.deletions = sortAndOptimizeDeletions(pending.deletions);
|
|
88
108
|
result.push(pending);
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
// Also optimize deletions in all previously pushed operations
|
|
111
|
+
// Also sort and optimize deletions in all previously pushed operations
|
|
92
112
|
for (const op of result) {
|
|
93
|
-
op.deletions =
|
|
113
|
+
op.deletions = sortAndOptimizeDeletions(op.deletions);
|
|
94
114
|
}
|
|
95
115
|
|
|
96
116
|
return result;
|
|
@@ -298,6 +298,105 @@ describe('coalesceTextOperations', () => {
|
|
|
298
298
|
});
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
+
describe('delete operations coalescence', () => {
|
|
302
|
+
it('should merge consecutive delete operations and optimize deletions array', () => {
|
|
303
|
+
// Simulate pressing backspace 7 times (7 separate delete operations)
|
|
304
|
+
const ops: TextOperation[] = [
|
|
305
|
+
{
|
|
306
|
+
type: 'delete',
|
|
307
|
+
op: {
|
|
308
|
+
deletions: [{ id: 'alice:7', length: 1 }],
|
|
309
|
+
},
|
|
310
|
+
} as any,
|
|
311
|
+
{
|
|
312
|
+
type: 'delete',
|
|
313
|
+
op: {
|
|
314
|
+
deletions: [{ id: 'alice:6', length: 1 }],
|
|
315
|
+
},
|
|
316
|
+
} as any,
|
|
317
|
+
{
|
|
318
|
+
type: 'delete',
|
|
319
|
+
op: {
|
|
320
|
+
deletions: [{ id: 'alice:5', length: 1 }],
|
|
321
|
+
},
|
|
322
|
+
} as any,
|
|
323
|
+
{
|
|
324
|
+
type: 'delete',
|
|
325
|
+
op: {
|
|
326
|
+
deletions: [{ id: 'alice:4', length: 1 }],
|
|
327
|
+
},
|
|
328
|
+
} as any,
|
|
329
|
+
{
|
|
330
|
+
type: 'delete',
|
|
331
|
+
op: {
|
|
332
|
+
deletions: [{ id: 'alice:3', length: 1 }],
|
|
333
|
+
},
|
|
334
|
+
} as any,
|
|
335
|
+
{
|
|
336
|
+
type: 'delete',
|
|
337
|
+
op: {
|
|
338
|
+
deletions: [{ id: 'alice:2', length: 1 }],
|
|
339
|
+
},
|
|
340
|
+
} as any,
|
|
341
|
+
{
|
|
342
|
+
type: 'delete',
|
|
343
|
+
op: {
|
|
344
|
+
deletions: [{ id: 'alice:1', length: 1 }],
|
|
345
|
+
},
|
|
346
|
+
} as any,
|
|
347
|
+
];
|
|
348
|
+
|
|
349
|
+
const result = coalesceTextOperations(ops, { thresholdMs: 1000 });
|
|
350
|
+
|
|
351
|
+
// Should merge into ONE delete operation
|
|
352
|
+
expect(result).toHaveLength(1);
|
|
353
|
+
expect(result[0].type).toBe('delete');
|
|
354
|
+
|
|
355
|
+
// CRITICAL: Deletions array should be optimized (7 entries → 1 entry)
|
|
356
|
+
const deleteOp = result[0] as any;
|
|
357
|
+
expect(deleteOp.op.deletions).toBeDefined();
|
|
358
|
+
expect(deleteOp.op.deletions).toHaveLength(1);
|
|
359
|
+
expect(deleteOp.op.deletions[0]).toEqual({ id: 'alice:1', length: 7 });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should optimize deletions with gaps', () => {
|
|
363
|
+
// Deletions with non-consecutive IDs (can't be fully merged)
|
|
364
|
+
const ops: TextOperation[] = [
|
|
365
|
+
{
|
|
366
|
+
type: 'delete',
|
|
367
|
+
op: {
|
|
368
|
+
deletions: [
|
|
369
|
+
{ id: 'alice:5', length: 1 },
|
|
370
|
+
{ id: 'alice:4', length: 1 },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
} as any,
|
|
374
|
+
{
|
|
375
|
+
type: 'delete',
|
|
376
|
+
op: {
|
|
377
|
+
deletions: [
|
|
378
|
+
{ id: 'alice:2', length: 1 }, // Gap! alice:3 is missing
|
|
379
|
+
{ id: 'alice:1', length: 1 },
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
} as any,
|
|
383
|
+
];
|
|
384
|
+
|
|
385
|
+
const result = coalesceTextOperations(ops, { thresholdMs: 1000 });
|
|
386
|
+
|
|
387
|
+
// Should merge into ONE delete operation
|
|
388
|
+
expect(result).toHaveLength(1);
|
|
389
|
+
expect(result[0].type).toBe('delete');
|
|
390
|
+
|
|
391
|
+
// Deletions should be optimized: [alice:4-5 (len 2), alice:1-2 (len 2)]
|
|
392
|
+
const deleteOp = result[0] as any;
|
|
393
|
+
expect(deleteOp.op.deletions).toBeDefined();
|
|
394
|
+
expect(deleteOp.op.deletions).toHaveLength(2); // Two ranges due to gap
|
|
395
|
+
expect(deleteOp.op.deletions).toContainEqual({ id: 'alice:4', length: 2 });
|
|
396
|
+
expect(deleteOp.op.deletions).toContainEqual({ id: 'alice:1', length: 2 });
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
301
400
|
describe('edge cases', () => {
|
|
302
401
|
it('should handle empty operations array', () => {
|
|
303
402
|
const result = coalesceTextOperations([]);
|
|
@@ -235,15 +235,15 @@ describe('Delete Coalescence Bug', () => {
|
|
|
235
235
|
expect(coalesced).toHaveLength(1);
|
|
236
236
|
expect(coalesced[0].type).toBe('delete');
|
|
237
237
|
|
|
238
|
-
//
|
|
238
|
+
// FIXED in v0.5.4: coalesceTextOperations now sorts and optimizes deletions!
|
|
239
239
|
// Each individual remove() creates 1 deletion (only deleting 1 char)
|
|
240
|
-
// When coalesceTextOperations merges the 7 ops, it
|
|
240
|
+
// When coalesceTextOperations merges the 7 ops, it:
|
|
241
|
+
// 1. Combines the deletions arrays: [{id:Alice:6,len:1}, ..., {id:Alice:0,len:1}]
|
|
242
|
+
// 2. Sorts them: [{id:Alice:0,len:1}, ..., {id:Alice:6,len:1}]
|
|
243
|
+
// 3. Optimizes them: [{id:Alice:0,len:7}]
|
|
241
244
|
const coalescedOp = (coalesced[0] as any).op;
|
|
242
|
-
expect(coalescedOp.deletions).toHaveLength(
|
|
243
|
-
|
|
244
|
-
// Note: These 7 deletions are NOT consecutive because we deleted backwards (6→0)
|
|
245
|
-
// So they cannot be merged. If we add optimizeDeletions() to coalesceTextOperations,
|
|
246
|
-
// it would merge them if they were consecutive.
|
|
245
|
+
expect(coalescedOp.deletions).toHaveLength(1);
|
|
246
|
+
expect(coalescedOp.deletions[0]).toEqual({ id: 'Alice:0', length: 7 });
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
249
|
});
|