@vuer-ai/vuer-rtc 0.5.2 → 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.
@@ -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,CAuHjB;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;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":"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"}
1
+ {"version":3,"file":"coalesceTextOperations.js","sourceRoot":"","sources":["../../src/client/coalesceTextOperations.ts"],"names":[],"mappings":"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"}
@@ -12,8 +12,6 @@ export interface TextDeleteOp {
12
12
  id: string;
13
13
  length: number;
14
14
  }>;
15
- seq: number;
16
- ts: number;
17
15
  }
18
16
  export interface CoalesceOptions {
19
17
  /** Time threshold in milliseconds (default: 1000ms = 1 second) */
@@ -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;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"}
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,15 +3,32 @@
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
  */
10
28
  export function isTextDeleteOp(op) {
11
29
  return (op.otype === 'text.delete' &&
12
30
  Array.isArray(op.deletions) &&
13
- typeof op.seq === 'number' &&
14
- typeof op.ts === 'number');
31
+ op.deletions.length > 0);
15
32
  }
16
33
  /**
17
34
  * Coalesce consecutive text delete operations.
@@ -38,18 +55,15 @@ export function coalesceTextDeletes(ops, options = {}) {
38
55
  else {
39
56
  // Check merge conditions
40
57
  const sameTarget = pending.key === op.key && pending.path === op.path;
41
- // Time threshold: operations must be close in time (ts is in seconds)
42
- const timeDiffMs = (op.ts - pending.ts) * 1000;
43
- const withinThreshold = timeDiffMs <= thresholdMs;
44
- if (sameTarget && withinThreshold) {
58
+ // NOTE: Time threshold is not available for TextDeleteOp (no ts field)
59
+ // We merge based on target (key + path) only
60
+ if (sameTarget) {
45
61
  // Merge operations - combine deletion lists
46
62
  const merged = {
47
63
  otype: 'text.delete',
48
64
  key: pending.key,
49
65
  path: pending.path,
50
66
  deletions: [...pending.deletions, ...op.deletions],
51
- seq: Math.max(pending.seq, op.seq), // Use max Lamport clock for ordering
52
- ts: pending.ts, // Keep first timestamp (when sequence started)
53
67
  };
54
68
  pending = merged;
55
69
  }
@@ -62,13 +76,13 @@ export function coalesceTextDeletes(ops, options = {}) {
62
76
  }
63
77
  // Flush any remaining pending delete
64
78
  if (pending !== null) {
65
- // Optimize deletions array before pushing
66
- pending.deletions = optimizeDeletions(pending.deletions);
79
+ // Sort and optimize deletions array before pushing
80
+ pending.deletions = sortAndOptimizeDeletions(pending.deletions);
67
81
  result.push(pending);
68
82
  }
69
- // Also optimize deletions in all previously pushed operations
83
+ // Also sort and optimize deletions in all previously pushed operations
70
84
  for (const op of result) {
71
- op.deletions = optimizeDeletions(op.deletions);
85
+ op.deletions = sortAndOptimizeDeletions(op.deletions);
72
86
  }
73
87
  return result;
74
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;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"}
1
+ {"version":3,"file":"textDeletes.js","sourceRoot":"","sources":["../../../src/client/coalescence/textDeletes.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.2",
3
+ "version": "0.5.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "CRDT-based real-time collaborative data structures",
@@ -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,15 +5,15 @@
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';
12
12
  key: string;
13
13
  path: string;
14
14
  deletions: Array<{ id: string; length: number }>; // Array of deletion ranges
15
- seq: number;
16
- ts: number;
15
+ // NOTE: TextDeleteOp does NOT have seq/ts fields (those are only on TextInsertOp)
16
+ // Time-based coalescence is not available for delete operations
17
17
  }
18
18
 
19
19
  export interface CoalesceOptions {
@@ -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
  */
@@ -28,8 +48,7 @@ export function isTextDeleteOp(op: Operation): op is TextDeleteOp {
28
48
  return (
29
49
  op.otype === 'text.delete' &&
30
50
  Array.isArray((op as any).deletions) &&
31
- typeof (op as any).seq === 'number' &&
32
- typeof (op as any).ts === 'number'
51
+ (op as any).deletions.length > 0
33
52
  );
34
53
  }
35
54
 
@@ -63,19 +82,15 @@ export function coalesceTextDeletes(
63
82
  // Check merge conditions
64
83
  const sameTarget = pending.key === op.key && pending.path === op.path;
65
84
 
66
- // Time threshold: operations must be close in time (ts is in seconds)
67
- const timeDiffMs = (op.ts - pending.ts) * 1000;
68
- const withinThreshold = timeDiffMs <= thresholdMs;
69
-
70
- if (sameTarget && withinThreshold) {
85
+ // NOTE: Time threshold is not available for TextDeleteOp (no ts field)
86
+ // We merge based on target (key + path) only
87
+ if (sameTarget) {
71
88
  // Merge operations - combine deletion lists
72
89
  const merged: TextDeleteOp = {
73
90
  otype: 'text.delete',
74
91
  key: pending.key,
75
92
  path: pending.path,
76
93
  deletions: [...pending.deletions, ...op.deletions],
77
- seq: Math.max(pending.seq, op.seq), // Use max Lamport clock for ordering
78
- ts: pending.ts, // Keep first timestamp (when sequence started)
79
94
  };
80
95
  pending = merged;
81
96
  } else {
@@ -88,14 +103,14 @@ export function coalesceTextDeletes(
88
103
 
89
104
  // Flush any remaining pending delete
90
105
  if (pending !== null) {
91
- // Optimize deletions array before pushing
92
- pending.deletions = optimizeDeletions(pending.deletions);
106
+ // Sort and optimize deletions array before pushing
107
+ pending.deletions = sortAndOptimizeDeletions(pending.deletions);
93
108
  result.push(pending);
94
109
  }
95
110
 
96
- // Also optimize deletions in all previously pushed operations
111
+ // Also sort and optimize deletions in all previously pushed operations
97
112
  for (const op of result) {
98
- op.deletions = optimizeDeletions(op.deletions);
113
+ op.deletions = sortAndOptimizeDeletions(op.deletions);
99
114
  }
100
115
 
101
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
- // The deletions array should already be efficient from remove() optimization
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 combines the deletions arrays
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(7);
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
  });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Integration Tests for Graph Operation Coalescence
3
+ *
4
+ * These tests verify that operation coalescence works end-to-end through
5
+ * the GraphStore commit pipeline, not just at the Rope level.
6
+ *
7
+ * CRITICAL: These tests catch bugs that unit tests miss - like the isTextDeleteOp
8
+ * type guard checking for fields that don't exist!
9
+ */
10
+
11
+ import { describe, it, expect } from '@jest/globals';
12
+ import { createGraph } from '../../src/client/createGraph.js';
13
+ import type { CRDTMessage } from '../../src/operations/OperationTypes.js';
14
+
15
+ describe('Graph Operation Coalescence (Integration)', () => {
16
+ describe('text.delete coalescence', () => {
17
+ it('should coalesce consecutive text.delete operations', () => {
18
+ const messages: CRDTMessage[] = [];
19
+
20
+ const store = createGraph({
21
+ sessionId: 'alice',
22
+ coalescingEnabled: true,
23
+ coalescingDelayMs: 0,
24
+ coalescingThresholdMs: 1000,
25
+ onSend: (msg) => messages.push(msg),
26
+ });
27
+
28
+ // Initialize scene and text node
29
+ store.edit({ otype: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
30
+ store.commit('init scene');
31
+
32
+ store.edit({ otype: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
33
+ store.commit('add text node');
34
+
35
+ store.edit({ otype: 'text.init', key: 'text-doc', path: 'content', value: '' } as any);
36
+ store.commit('init text');
37
+
38
+ // Type "Hello" (5 operations)
39
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 0, value: 'H' } as any);
40
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 1, value: 'e' } as any);
41
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 2, value: 'l' } as any);
42
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 3, value: 'l' } as any);
43
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 4, value: 'o' } as any);
44
+ store.commit('type Hello');
45
+
46
+ messages.length = 0; // Clear previous messages
47
+
48
+ // Delete all 5 characters (1 operation)
49
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 0, length: 5 } as any);
50
+ store.commit('delete all');
51
+
52
+ // Verify we sent exactly 1 message
53
+ expect(messages).toHaveLength(1);
54
+
55
+ const msg = messages[0];
56
+ expect(msg.ops).toHaveLength(1);
57
+ expect(msg.ops[0].otype).toBe('text.delete');
58
+
59
+ const deleteOp = msg.ops[0] as any;
60
+ expect(deleteOp.deletions).toBeDefined();
61
+
62
+ // CRITICAL: This verifies the coalescence optimization works!
63
+ // Without the fix, this would have many single-char deletions
64
+ expect(deleteOp.deletions).toHaveLength(1);
65
+ expect(deleteOp.deletions[0].length).toBe(5);
66
+ });
67
+
68
+ it('should coalesce multiple consecutive delete operations', () => {
69
+ const messages: CRDTMessage[] = [];
70
+
71
+ const store = createGraph({
72
+ sessionId: 'alice',
73
+ coalescingEnabled: true,
74
+ coalescingDelayMs: 0,
75
+ coalescingThresholdMs: 1000,
76
+ onSend: (msg) => messages.push(msg),
77
+ });
78
+
79
+ // Initialize
80
+ store.edit({ otype: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
81
+ store.commit('init');
82
+
83
+ store.edit({ otype: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
84
+ store.commit('add text');
85
+
86
+ store.edit({ otype: 'text.init', key: 'text-doc', path: 'content', value: 'Hello' } as any);
87
+ store.commit('init text with Hello');
88
+
89
+ messages.length = 0;
90
+
91
+ // Simulate rapid backspace (5 separate delete operations)
92
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 4, length: 1 } as any); // 'o'
93
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 3, length: 1 } as any); // 'l'
94
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 2, length: 1 } as any); // 'l'
95
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 1, length: 1 } as any); // 'e'
96
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 0, length: 1 } as any); // 'H'
97
+ store.commit('rapid backspace');
98
+
99
+ // Verify coalescence worked
100
+ expect(messages).toHaveLength(1);
101
+
102
+ const msg = messages[0];
103
+
104
+ // Find the delete operation(s)
105
+ const deleteOps = msg.ops.filter(op => op.otype === 'text.delete');
106
+ expect(deleteOps.length).toBeGreaterThan(0);
107
+
108
+ // CRITICAL TEST: Verify each operation has optimized deletions
109
+ // The key fix is that isTextDeleteOp now accepts operations with deletions arrays
110
+ for (const op of deleteOps) {
111
+ const deleteOp = op as any;
112
+ expect(deleteOp.deletions).toBeDefined();
113
+ expect(Array.isArray(deleteOp.deletions)).toBe(true);
114
+ expect(deleteOp.deletions.length).toBeGreaterThan(0);
115
+
116
+ // Each deletion should be valid
117
+ for (const del of deleteOp.deletions) {
118
+ expect(del.id).toBeDefined();
119
+ expect(typeof del.length).toBe('number');
120
+ expect(del.length).toBeGreaterThan(0);
121
+ }
122
+ }
123
+
124
+ // Verify we're not sending more operations than we created
125
+ // (coalescence may merge some, but shouldn't create more)
126
+ expect(msg.ops.length).toBeLessThanOrEqual(5);
127
+ });
128
+
129
+ it('should verify type guard accepts delete operations with deletions', () => {
130
+ const messages: CRDTMessage[] = [];
131
+
132
+ const store = createGraph({
133
+ sessionId: 'alice',
134
+ coalescingEnabled: true,
135
+ coalescingDelayMs: 0,
136
+ coalescingThresholdMs: 1000,
137
+ onSend: (msg) => messages.push(msg),
138
+ });
139
+
140
+ // Initialize
141
+ store.edit({ otype: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
142
+ store.commit('init');
143
+
144
+ store.edit({ otype: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
145
+ store.commit('add text');
146
+
147
+ store.edit({ otype: 'text.init', key: 'text-doc', path: 'content', value: 'Test' } as any);
148
+ store.commit('init text');
149
+
150
+ messages.length = 0;
151
+
152
+ // Single delete operation
153
+ store.edit({ otype: 'text.delete', key: 'text-doc', path: 'content', position: 0, length: 4 } as any);
154
+ store.commit('delete');
155
+
156
+ expect(messages).toHaveLength(1);
157
+
158
+ const deleteOp = messages[0].ops[0] as any;
159
+
160
+ // CRITICAL: Verify the operation has the deletions field
161
+ // This is what isTextDeleteOp checks for
162
+ expect(deleteOp.otype).toBe('text.delete');
163
+ expect(Array.isArray(deleteOp.deletions)).toBe(true);
164
+ expect(deleteOp.deletions.length).toBeGreaterThan(0);
165
+
166
+ // Verify it does NOT have seq/ts (those are on TextInsertOp only!)
167
+ expect(deleteOp.seq).toBeUndefined();
168
+ expect(deleteOp.ts).toBeUndefined();
169
+ });
170
+ });
171
+
172
+ describe('text.insert coalescence', () => {
173
+ it('should coalesce consecutive text.insert operations', () => {
174
+ const messages: CRDTMessage[] = [];
175
+
176
+ const store = createGraph({
177
+ sessionId: 'alice',
178
+ coalescingEnabled: true,
179
+ coalescingDelayMs: 0,
180
+ coalescingThresholdMs: 1000,
181
+ onSend: (msg) => messages.push(msg),
182
+ });
183
+
184
+ // Initialize
185
+ store.edit({ otype: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
186
+ store.commit('init');
187
+
188
+ store.edit({ otype: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
189
+ store.commit('add text');
190
+
191
+ store.edit({ otype: 'text.init', key: 'text-doc', path: 'content', value: '' } as any);
192
+ store.commit('init text');
193
+
194
+ messages.length = 0;
195
+
196
+ // Type "Hello" (5 operations)
197
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 0, value: 'H' } as any);
198
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 1, value: 'e' } as any);
199
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 2, value: 'l' } as any);
200
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 3, value: 'l' } as any);
201
+ store.edit({ otype: 'text.insert', key: 'text-doc', path: 'content', position: 4, value: 'o' } as any);
202
+ store.commit('type Hello');
203
+
204
+ expect(messages).toHaveLength(1);
205
+
206
+ const msg = messages[0];
207
+
208
+ // Verify inserts were coalesced
209
+ expect(msg.ops).toHaveLength(1);
210
+ expect(msg.ops[0].otype).toBe('text.insert');
211
+
212
+ const insertOp = msg.ops[0] as any;
213
+ expect(insertOp.content).toBe('Hello');
214
+ expect(insertOp.id).toBeDefined();
215
+ expect(insertOp.seq).toBeDefined();
216
+ expect(insertOp.ts).toBeDefined();
217
+ });
218
+ });
219
+ });