@vuer-ai/vuer-rtc 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CLAUDE.md +29 -0
  2. package/COALESCE_FIX_VERIFICATION.md +81 -0
  3. package/REFACTORING_NOTES.md +229 -0
  4. package/dist/client/EditBuffer.d.ts +13 -1
  5. package/dist/client/EditBuffer.d.ts.map +1 -1
  6. package/dist/client/EditBuffer.js +47 -3
  7. package/dist/client/EditBuffer.js.map +1 -1
  8. package/dist/client/actions.d.ts +5 -1
  9. package/dist/client/actions.d.ts.map +1 -1
  10. package/dist/client/actions.js +12 -9
  11. package/dist/client/actions.js.map +1 -1
  12. package/dist/client/coalesceGraphOps.d.ts +34 -0
  13. package/dist/client/coalesceGraphOps.d.ts.map +1 -0
  14. package/dist/client/coalesceGraphOps.js +35 -0
  15. package/dist/client/coalesceGraphOps.js.map +1 -0
  16. package/dist/client/coalesceTextOperations.d.ts +42 -0
  17. package/dist/client/coalesceTextOperations.d.ts.map +1 -0
  18. package/dist/client/coalesceTextOperations.js +119 -0
  19. package/dist/client/coalesceTextOperations.js.map +1 -0
  20. package/dist/client/coalescence/index.d.ts +9 -0
  21. package/dist/client/coalescence/index.d.ts.map +1 -0
  22. package/dist/client/coalescence/index.js +9 -0
  23. package/dist/client/coalescence/index.js.map +1 -0
  24. package/dist/client/coalescence/registry.d.ts +48 -0
  25. package/dist/client/coalescence/registry.d.ts.map +1 -0
  26. package/dist/client/coalescence/registry.js +95 -0
  27. package/dist/client/coalescence/registry.js.map +1 -0
  28. package/dist/client/coalescence/textDeletes.d.ts +38 -0
  29. package/dist/client/coalescence/textDeletes.d.ts.map +1 -0
  30. package/dist/client/coalescence/textDeletes.js +68 -0
  31. package/dist/client/coalescence/textDeletes.js.map +1 -0
  32. package/dist/client/coalescence/textInserts.d.ts +45 -0
  33. package/dist/client/coalescence/textInserts.d.ts.map +1 -0
  34. package/dist/client/coalescence/textInserts.js +96 -0
  35. package/dist/client/coalescence/textInserts.js.map +1 -0
  36. package/dist/client/createGraph.d.ts.map +1 -1
  37. package/dist/client/createGraph.js +9 -2
  38. package/dist/client/createGraph.js.map +1 -1
  39. package/dist/client/createTextDocument.d.ts.map +1 -1
  40. package/dist/client/createTextDocument.js +2 -1
  41. package/dist/client/createTextDocument.js.map +1 -1
  42. package/dist/client/index.d.ts +4 -0
  43. package/dist/client/index.d.ts.map +1 -1
  44. package/dist/client/index.js +4 -0
  45. package/dist/client/index.js.map +1 -1
  46. package/dist/client/textActions.d.ts +1 -1
  47. package/dist/client/textActions.d.ts.map +1 -1
  48. package/dist/client/textActions.js +7 -2
  49. package/dist/client/textActions.js.map +1 -1
  50. package/dist/client/types.d.ts +3 -0
  51. package/dist/client/types.d.ts.map +1 -1
  52. package/dist/crdt/GraphTextCRDT.d.ts +0 -4
  53. package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
  54. package/dist/crdt/GraphTextCRDT.js +3 -0
  55. package/dist/crdt/GraphTextCRDT.js.map +1 -1
  56. package/dist/crdt/Rope.d.ts +27 -6
  57. package/dist/crdt/Rope.d.ts.map +1 -1
  58. package/dist/crdt/Rope.js +137 -69
  59. package/dist/crdt/Rope.js.map +1 -1
  60. package/dist/operations/OperationTypes.d.ts +10 -26
  61. package/dist/operations/OperationTypes.d.ts.map +1 -1
  62. package/dist/operations/apply/text.d.ts.map +1 -1
  63. package/dist/operations/apply/text.js +8 -16
  64. package/dist/operations/apply/text.js.map +1 -1
  65. package/examples/05-coalescence-usage.ts +189 -0
  66. package/package.json +1 -1
  67. package/src/client/EditBuffer.ts +51 -3
  68. package/src/client/actions.ts +13 -9
  69. package/src/client/coalesceGraphOps.ts +40 -0
  70. package/src/client/coalesceTextOperations.ts +134 -0
  71. package/src/client/coalescence/index.ts +18 -0
  72. package/src/client/coalescence/registry.ts +137 -0
  73. package/src/client/coalescence/textDeletes.ts +94 -0
  74. package/src/client/coalescence/textInserts.ts +128 -0
  75. package/src/client/createGraph.ts +11 -2
  76. package/src/client/createTextDocument.ts +2 -1
  77. package/src/client/index.ts +14 -0
  78. package/src/client/textActions.ts +9 -2
  79. package/src/client/types.ts +4 -1
  80. package/src/crdt/GraphTextCRDT.ts +0 -5
  81. package/src/crdt/Rope.ts +155 -79
  82. package/src/operations/OperationTypes.ts +10 -8
  83. package/src/operations/apply/text.ts +8 -20
  84. package/test-coalescence.ts +201 -0
  85. package/tests/client/actions.test.ts +156 -0
  86. package/tests/client/coalesce-text-operations.test.ts +327 -0
  87. package/tests/client/edit-buffer.test.ts +137 -1
  88. package/tests/crdt/graph-text-crdt.test.ts +29 -17
  89. package/tests/crdt/rope.test.ts +13 -11
@@ -6,16 +6,18 @@
6
6
  * ID lookups without invalidation.
7
7
  */
8
8
  import { type ItemTree } from './BTree.js';
9
- export interface ItemId {
10
- agent: string;
11
- seq: number;
12
- }
9
+ /**
10
+ * ItemId - String format for efficient comparison
11
+ * Format: "agent:seq" (e.g., "agent-1:42")
12
+ */
13
+ export type ItemId = string;
13
14
  export interface Item {
14
15
  id: ItemId;
15
16
  content: string;
16
17
  isDeleted: boolean;
17
18
  parentId: ItemId | null;
18
19
  seq: number;
20
+ ts: number;
19
21
  }
20
22
  interface SpanEntry {
21
23
  startSeq: number;
@@ -54,6 +56,7 @@ export interface InsertOp {
54
56
  content: string;
55
57
  parentId: ItemId | null;
56
58
  seq: number;
59
+ ts: number;
57
60
  }
58
61
  export interface DeleteOp {
59
62
  deletions: Array<{
@@ -69,8 +72,26 @@ export interface ReplaceOp {
69
72
  delete: DeleteOp;
70
73
  insert: InsertOp;
71
74
  }
75
+ /**
76
+ * Compare two ItemIds for equality
77
+ */
72
78
  export declare function itemIdEquals(a: ItemId | null, b: ItemId | null): boolean;
79
+ /**
80
+ * Compare two ItemIds for ordering (primarily for agent ordering)
81
+ * For tie-breaking by ts, use seq + ts + id comparison
82
+ */
73
83
  export declare function itemIdCompare(a: ItemId, b: ItemId): number;
84
+ /**
85
+ * Parse an ItemId string into its components
86
+ */
87
+ export declare function parseItemId(id: ItemId): {
88
+ agent: string;
89
+ seq: number;
90
+ };
91
+ /**
92
+ * Create an ItemId from agent and seq
93
+ */
94
+ export declare function createItemId(agent: string, seq: number): ItemId;
74
95
  export declare function create(agentId: string): TextRope;
75
96
  export declare function getText(rope: TextRope): string;
76
97
  export declare function getLength(rope: TextRope): number;
@@ -89,12 +110,12 @@ export declare function remove(rope: TextRope, position: number, length: number)
89
110
  export declare function applyDelete(rope: TextRope, op: DeleteOp): void;
90
111
  export declare function replace(rope: TextRope, position: number, deleteLength: number, insertText: string, agentId?: string): ReplaceOp;
91
112
  export declare function applyReplace(rope: TextRope, op: ReplaceOp): void;
92
- export declare function move(rope: TextRope, fromPosition: number, length: number, toPosition: number): MoveOp;
113
+ export declare function move(rope: TextRope, fromPosition: number, length: number, toPosition: number, agentId?: string): MoveOp;
93
114
  export declare function applyMove(rope: TextRope, op: MoveOp): void;
94
115
  export declare function merge(rope: TextRope, other: TextRope): void;
95
116
  export declare function snapshot(rope: TextRope): TextRope;
96
117
  export declare function compact(rope: TextRope): TextRope;
97
- export type RawTextRope = [string, number, number, Array<[string, number, string, string | null, number | null]>];
118
+ export type RawTextRope = [string, number, number, Array<[string, string, string | null, number, number]>];
98
119
  export declare function toRaw(rope: TextRope): RawTextRope;
99
120
  export declare function fromRaw(raw: RawTextRope, newAgentId?: string): TextRope;
100
121
  export declare function fromSnapshot(snap: TextRope, newAgentId?: string): TextRope;
@@ -1 +1 @@
1
- {"version":3,"file":"Rope.d.ts","sourceRoot":"","sources":["../../src/crdt/Rope.ts"],"names":[],"mappings":"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"}
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
@@ -46,19 +46,38 @@ export class TextRope {
46
46
  // ============================================
47
47
  // Utilities
48
48
  // ============================================
49
+ /**
50
+ * Compare two ItemIds for equality
51
+ */
49
52
  export function itemIdEquals(a, b) {
50
- if (a === null && b === null)
51
- return true;
52
- if (a === null || b === null)
53
- return false;
54
- return a.agent === b.agent && a.seq === b.seq;
53
+ return a === b;
55
54
  }
55
+ /**
56
+ * Compare two ItemIds for ordering (primarily for agent ordering)
57
+ * For tie-breaking by ts, use seq + ts + id comparison
58
+ */
56
59
  export function itemIdCompare(a, b) {
57
- if (a.agent < b.agent)
60
+ if (a < b)
58
61
  return -1;
59
- if (a.agent > b.agent)
62
+ if (a > b)
60
63
  return 1;
61
- return a.seq - b.seq;
64
+ return 0;
65
+ }
66
+ /**
67
+ * Parse an ItemId string into its components
68
+ */
69
+ export function parseItemId(id) {
70
+ const colonIdx = id.lastIndexOf(':');
71
+ return {
72
+ agent: id.slice(0, colonIdx),
73
+ seq: parseInt(id.slice(colonIdx + 1), 10),
74
+ };
75
+ }
76
+ /**
77
+ * Create an ItemId from agent and seq
78
+ */
79
+ export function createItemId(agent, seq) {
80
+ return `${agent}:${seq}`;
62
81
  }
63
82
  // ============================================
64
83
  // Agent Index helpers
@@ -78,15 +97,16 @@ function aiFind(entries, seq) {
78
97
  return -1;
79
98
  }
80
99
  function aiAdd(index, item) {
100
+ const parsed = parseItemId(item.id);
81
101
  const entry = {
82
- startSeq: item.id.seq,
83
- endSeq: item.id.seq + item.content.length,
102
+ startSeq: parsed.seq,
103
+ endSeq: parsed.seq + item.content.length,
84
104
  item,
85
105
  };
86
- let arr = index.get(item.id.agent);
106
+ let arr = index.get(parsed.agent);
87
107
  if (!arr) {
88
108
  arr = [];
89
- index.set(item.id.agent, arr);
109
+ index.set(parsed.agent, arr);
90
110
  }
91
111
  let lo = 0, hi = arr.length;
92
112
  while (lo < hi) {
@@ -99,10 +119,11 @@ function aiAdd(index, item) {
99
119
  arr.splice(lo, 0, entry);
100
120
  }
101
121
  function aiRemove(index, item) {
102
- const arr = index.get(item.id.agent);
122
+ const parsed = parseItemId(item.id);
123
+ const arr = index.get(parsed.agent);
103
124
  if (!arr)
104
125
  return;
105
- const ei = aiFind(arr, item.id.seq);
126
+ const ei = aiFind(arr, parsed.seq);
106
127
  if (ei !== -1 && arr[ei].item === item) {
107
128
  arr.splice(ei, 1);
108
129
  }
@@ -110,12 +131,13 @@ function aiRemove(index, item) {
110
131
  function aiBuild(items) {
111
132
  const index = new Map();
112
133
  for (const item of items) {
113
- let arr = index.get(item.id.agent);
134
+ const parsed = parseItemId(item.id);
135
+ let arr = index.get(parsed.agent);
114
136
  if (!arr) {
115
137
  arr = [];
116
- index.set(item.id.agent, arr);
138
+ index.set(parsed.agent, arr);
117
139
  }
118
- arr.push({ startSeq: item.id.seq, endSeq: item.id.seq + item.content.length, item });
140
+ arr.push({ startSeq: parsed.seq, endSeq: parsed.seq + item.content.length, item });
119
141
  }
120
142
  for (const arr of index.values()) {
121
143
  if (arr.length > 1)
@@ -146,14 +168,16 @@ export function getItems(rope) {
146
168
  export function findItemById(rope, id) {
147
169
  if (id === null)
148
170
  return -1;
149
- const entries = rope._agentIndex.get(id.agent);
171
+ const parsed = parseItemId(id);
172
+ const entries = rope._agentIndex.get(parsed.agent);
150
173
  if (!entries)
151
174
  return -1;
152
- const ei = aiFind(entries, id.seq);
175
+ const ei = aiFind(entries, parsed.seq);
153
176
  if (ei === -1)
154
177
  return -1;
155
178
  const item = entries[ei].item;
156
- if (item.id.seq !== id.seq)
179
+ const itemParsed = parseItemId(item.id);
180
+ if (itemParsed.seq !== parsed.seq)
157
181
  return -1;
158
182
  return computeOrdinal(rope._tree, item);
159
183
  }
@@ -180,44 +204,49 @@ export function getStats(rope) {
180
204
  // ============================================
181
205
  function splitItem(rope, ordinal, offset) {
182
206
  const item = getAt(rope._tree, ordinal);
207
+ const itemParsed = parseItemId(item.id);
183
208
  const before = {
184
- id: { agent: item.id.agent, seq: item.id.seq },
209
+ id: createItemId(itemParsed.agent, itemParsed.seq),
185
210
  content: item.content.slice(0, offset),
186
211
  isDeleted: item.isDeleted,
187
- parentId: item.parentId ? { agent: item.parentId.agent, seq: item.parentId.seq } : null,
212
+ parentId: item.parentId,
188
213
  seq: item.seq,
214
+ ts: item.ts,
189
215
  };
190
216
  const after = {
191
- id: { agent: item.id.agent, seq: item.id.seq + offset },
217
+ id: createItemId(itemParsed.agent, itemParsed.seq + offset),
192
218
  content: item.content.slice(offset),
193
219
  isDeleted: item.isDeleted,
194
- parentId: { agent: item.id.agent, seq: item.id.seq + offset - 1 },
220
+ parentId: createItemId(itemParsed.agent, itemParsed.seq + offset - 1),
195
221
  seq: item.seq,
222
+ ts: item.ts,
196
223
  };
197
224
  splitAtOrdinal(rope._tree, ordinal, before, after);
198
225
  aiRemove(rope._agentIndex, item);
199
226
  aiAdd(rope._agentIndex, before);
200
227
  aiAdd(rope._agentIndex, after);
201
- if (item.id.agent === rope.agentId) {
202
- const maxSeqInItem = item.id.seq + item.content.length - 1;
228
+ if (itemParsed.agent === rope.agentId) {
229
+ const maxSeqInItem = itemParsed.seq + item.content.length - 1;
203
230
  if (maxSeqInItem >= rope.clock)
204
231
  rope.clock = maxSeqInItem + 1;
205
232
  }
206
233
  return before;
207
234
  }
208
235
  function containsItemId(rope, id) {
209
- const entries = rope._agentIndex.get(id.agent);
236
+ const parsed = parseItemId(id);
237
+ const entries = rope._agentIndex.get(parsed.agent);
210
238
  if (!entries)
211
239
  return false;
212
- return aiFind(entries, id.seq) !== -1;
240
+ return aiFind(entries, parsed.seq) !== -1;
213
241
  }
214
242
  function findParentOrdinal(rope, parentId) {
215
243
  if (parentId === null)
216
244
  return -1;
217
- const entries = rope._agentIndex.get(parentId.agent);
245
+ const parsed = parseItemId(parentId);
246
+ const entries = rope._agentIndex.get(parsed.agent);
218
247
  if (!entries)
219
248
  return -1;
220
- const ei = aiFind(entries, parentId.seq);
249
+ const ei = aiFind(entries, parsed.seq);
221
250
  if (ei === -1)
222
251
  return -1;
223
252
  return computeOrdinal(rope._tree, entries[ei].item);
@@ -225,15 +254,17 @@ function findParentOrdinal(rope, parentId) {
225
254
  function splitForParent(rope, parentId) {
226
255
  if (parentId === null)
227
256
  return -1;
228
- const entries = rope._agentIndex.get(parentId.agent);
257
+ const parsedParent = parseItemId(parentId);
258
+ const entries = rope._agentIndex.get(parsedParent.agent);
229
259
  if (!entries)
230
260
  return -1;
231
- const ei = aiFind(entries, parentId.seq);
261
+ const ei = aiFind(entries, parsedParent.seq);
232
262
  if (ei === -1)
233
263
  return -1;
234
264
  const item = entries[ei].item;
235
265
  const ordinal = computeOrdinal(rope._tree, item);
236
- const splitOffset = parentId.seq - item.id.seq + 1;
266
+ const itemParsed = parseItemId(item.id);
267
+ const splitOffset = parsedParent.seq - itemParsed.seq + 1;
237
268
  if (splitOffset < item.content.length) {
238
269
  splitItem(rope, ordinal, splitOffset);
239
270
  }
@@ -252,7 +283,8 @@ function findInsertPosition(rope, position) {
252
283
  return null;
253
284
  const item = result.item;
254
285
  const charOffset = Math.min(result.charOffset, item.content.length - 1);
255
- return { agent: item.id.agent, seq: item.id.seq + charOffset };
286
+ const itemParsed = parseItemId(item.id);
287
+ return createItemId(itemParsed.agent, itemParsed.seq + charOffset);
256
288
  }
257
289
  function findInsertPositionWithSplit(rope, position) {
258
290
  if (position === 0 || totalItems(rope._tree) === 0) {
@@ -266,7 +298,8 @@ function findInsertPositionWithSplit(rope, position) {
266
298
  }
267
299
  const item = result.item;
268
300
  const charOffset = Math.min(result.charOffset, item.content.length - 1);
269
- const parentId = { agent: item.id.agent, seq: item.id.seq + charOffset };
301
+ const itemParsed = parseItemId(item.id);
302
+ const parentId = createItemId(itemParsed.agent, itemParsed.seq + charOffset);
270
303
  const needsSplit = charOffset + 1 < item.content.length;
271
304
  return { parentId, parentOrdinal: result.ordinal, needsSplit, splitOffset: charOffset + 1 };
272
305
  }
@@ -279,15 +312,28 @@ function integrate(rope, newItem, parentOrdinal) {
279
312
  if (insertOrdinal < total) {
280
313
  for (const existing of iterateFrom(rope._tree, insertOrdinal)) {
281
314
  const existingItem = existing;
315
+ // First, compare by Lamport timestamp (seq) - higher seq means happened later
282
316
  if (newItem.seq > existingItem.seq)
283
317
  break;
284
318
  const existingParentOrdinal = findParentOrdinal(rope, existingItem.parentId);
285
- if (existingParentOrdinal < parentOrdinal ||
286
- (existingParentOrdinal === parentOrdinal &&
287
- newItem.seq === existingItem.seq &&
288
- itemIdCompare(newItem.id, existingItem.id) < 0)) {
319
+ // If existing item's parent is before our parent in the document, insert here
320
+ if (existingParentOrdinal < parentOrdinal) {
289
321
  break;
290
322
  }
323
+ // If same parent and same Lamport seq, use wall-clock time (ts) for tie-breaking
324
+ if (existingParentOrdinal === parentOrdinal && newItem.seq === existingItem.seq) {
325
+ // Use ts for tie-breaking (earlier ts wins - inserts first)
326
+ if (newItem.ts < existingItem.ts) {
327
+ break;
328
+ }
329
+ // If ts also equal (rare), fall back to ID comparison for determinism
330
+ if (newItem.ts === existingItem.ts && itemIdCompare(newItem.id, existingItem.id) < 0) {
331
+ break;
332
+ }
333
+ }
334
+ // If same parent but different seq, the YATA algorithm says:
335
+ // Insert before items with lower seq (that happened earlier)
336
+ // This is already handled by the first check above
291
337
  insertOrdinal++;
292
338
  }
293
339
  }
@@ -298,15 +344,17 @@ function _applyLocal(rope, op, parentOrdinal) {
298
344
  if (op.seq > rope.maxSeq)
299
345
  rope.maxSeq = op.seq;
300
346
  const newItem = {
301
- id: { agent: op.id.agent, seq: op.id.seq },
347
+ id: op.id,
302
348
  content: op.content,
303
349
  isDeleted: false,
304
- parentId: op.parentId ? { agent: op.parentId.agent, seq: op.parentId.seq } : null,
350
+ parentId: op.parentId,
305
351
  seq: op.seq,
352
+ ts: op.ts,
306
353
  };
307
354
  integrate(rope, newItem, parentOrdinal);
308
- if (op.id.agent === rope.agentId) {
309
- const maxSeqInItem = op.id.seq + op.content.length - 1;
355
+ const idParsed = parseItemId(op.id);
356
+ if (idParsed.agent === rope.agentId) {
357
+ const maxSeqInItem = idParsed.seq + op.content.length - 1;
310
358
  if (maxSeqInItem >= rope.clock)
311
359
  rope.clock = maxSeqInItem + 1;
312
360
  }
@@ -337,10 +385,11 @@ export function insert(rope, position, content, agentId) {
337
385
  switchAgent(rope, agentId);
338
386
  const parentId = findInsertPosition(rope, position);
339
387
  const op = {
340
- id: { agent: rope.agentId, seq: rope.clock },
388
+ id: createItemId(rope.agentId, rope.clock),
341
389
  content,
342
390
  parentId,
343
391
  seq: rope.maxSeq + 1,
392
+ ts: Date.now() / 1000,
344
393
  };
345
394
  const parentOrdinal = splitForParent(rope, op.parentId);
346
395
  _applyLocal(rope, op, parentOrdinal);
@@ -354,10 +403,11 @@ export function insertWithSplit(rope, position, content, agentId) {
354
403
  splitItem(rope, parentOrdinal, splitOffset);
355
404
  }
356
405
  const op = {
357
- id: { agent: rope.agentId, seq: rope.clock },
406
+ id: createItemId(rope.agentId, rope.clock),
358
407
  content,
359
408
  parentId,
360
409
  seq: rope.maxSeq + 1,
410
+ ts: Date.now() / 1000,
361
411
  };
362
412
  _applyLocal(rope, op, parentOrdinal);
363
413
  return op;
@@ -369,15 +419,17 @@ export function apply(rope, op) {
369
419
  return;
370
420
  const parentOrdinal = splitForParent(rope, op.parentId);
371
421
  const newItem = {
372
- id: { agent: op.id.agent, seq: op.id.seq },
422
+ id: op.id,
373
423
  content: op.content,
374
424
  isDeleted: false,
375
- parentId: op.parentId ? { agent: op.parentId.agent, seq: op.parentId.seq } : null,
425
+ parentId: op.parentId,
376
426
  seq: op.seq,
427
+ ts: op.ts,
377
428
  };
378
429
  integrate(rope, newItem, parentOrdinal);
379
- if (op.id.agent === rope.agentId) {
380
- const maxSeqInItem = op.id.seq + op.content.length - 1;
430
+ const idParsed = parseItemId(op.id);
431
+ if (idParsed.agent === rope.agentId) {
432
+ const maxSeqInItem = idParsed.seq + op.content.length - 1;
381
433
  if (maxSeqInItem >= rope.clock)
382
434
  rope.clock = maxSeqInItem + 1;
383
435
  }
@@ -412,7 +464,7 @@ export function remove(rope, position, length) {
412
464
  // Delete the item
413
465
  item.isDeleted = true;
414
466
  recalcCountsFor(rope._tree, item);
415
- deletions.push({ id: { ...item.id }, length: item.content.length });
467
+ deletions.push({ id: item.id, length: item.content.length });
416
468
  remaining -= toDelete;
417
469
  }
418
470
  return { deletions };
@@ -423,12 +475,13 @@ export function applyDelete(rope, op) {
423
475
  // Group and merge deletion ranges by agent
424
476
  const byAgent = new Map();
425
477
  for (const del of op.deletions) {
426
- let ranges = byAgent.get(del.id.agent);
478
+ const parsed = parseItemId(del.id);
479
+ let ranges = byAgent.get(parsed.agent);
427
480
  if (!ranges) {
428
481
  ranges = [];
429
- byAgent.set(del.id.agent, ranges);
482
+ byAgent.set(parsed.agent, ranges);
430
483
  }
431
- ranges.push({ start: del.id.seq, end: del.id.seq + del.length });
484
+ ranges.push({ start: parsed.seq, end: parsed.seq + del.length });
432
485
  }
433
486
  for (const [, ranges] of byAgent) {
434
487
  if (ranges.length > 1) {
@@ -473,7 +526,8 @@ function applyDeleteRange(rope, agent, start, end) {
473
526
  continue;
474
527
  }
475
528
  const item = entries[ei].item;
476
- const itemStart = item.id.seq;
529
+ const itemParsed = parseItemId(item.id);
530
+ const itemStart = itemParsed.seq;
477
531
  const itemEnd = itemStart + item.content.length;
478
532
  const overlapStart = Math.max(itemStart, start);
479
533
  const overlapEnd = Math.min(itemEnd, end);
@@ -513,7 +567,7 @@ export function replace(rope, position, deleteLength, insertText, agentId) {
513
567
  const deleteOp = deleteLength > 0 ? remove(rope, position, deleteLength) : { deletions: [] };
514
568
  const insertOp = insertText.length > 0
515
569
  ? insertWithSplit(rope, position, insertText)
516
- : { id: { agent: rope.agentId, seq: rope.clock }, content: '', parentId: null, seq: rope.maxSeq };
570
+ : { id: createItemId(rope.agentId, rope.clock), content: '', parentId: null, seq: rope.maxSeq, ts: Date.now() / 1000 };
517
571
  return { delete: deleteOp, insert: insertOp };
518
572
  }
519
573
  export function applyReplace(rope, op) {
@@ -522,7 +576,9 @@ export function applyReplace(rope, op) {
522
576
  apply(rope, op.insert);
523
577
  }
524
578
  }
525
- export function move(rope, fromPosition, length, toPosition) {
579
+ export function move(rope, fromPosition, length, toPosition, agentId) {
580
+ if (agentId !== undefined)
581
+ switchAgent(rope, agentId);
526
582
  // Extract content before deleting
527
583
  let content = '';
528
584
  let remaining = length;
@@ -566,6 +622,7 @@ export function merge(rope, other) {
566
622
  content: item.content,
567
623
  parentId: item.parentId,
568
624
  seq: item.seq,
625
+ ts: item.ts,
569
626
  });
570
627
  if (item.isDeleted) {
571
628
  applyDelete(rope, { deletions: [{ id: item.id, length: item.content.length }] });
@@ -584,9 +641,17 @@ export function compact(rope) {
584
641
  if (item.isDeleted)
585
642
  continue;
586
643
  const newItem = { ...item };
587
- if (prevItem && prevItem.id.agent === newItem.id.agent &&
588
- prevItem.id.seq + prevItem.content.length === newItem.id.seq) {
589
- prevItem.content += newItem.content;
644
+ if (prevItem) {
645
+ const prevParsed = parseItemId(prevItem.id);
646
+ const newParsed = parseItemId(newItem.id);
647
+ if (prevParsed.agent === newParsed.agent &&
648
+ prevParsed.seq + prevItem.content.length === newParsed.seq) {
649
+ prevItem.content += newItem.content;
650
+ }
651
+ else {
652
+ visibleItems.push(newItem);
653
+ prevItem = newItem;
654
+ }
590
655
  }
591
656
  else {
592
657
  visibleItems.push(newItem);
@@ -599,22 +664,23 @@ export function toRaw(rope) {
599
664
  const compacted = compact(rope);
600
665
  const items = flattenItems(compacted._tree);
601
666
  const rawItems = items.map(item => [
602
- item.id.agent,
603
- item.id.seq,
667
+ item.id,
604
668
  item.content,
605
- item.parentId?.agent ?? null,
606
- item.parentId?.seq ?? null,
669
+ item.parentId,
670
+ item.seq,
671
+ item.ts,
607
672
  ]);
608
673
  return [rope.agentId, rope.clock, rope.maxSeq, rawItems];
609
674
  }
610
675
  export function fromRaw(raw, newAgentId) {
611
676
  const [agentId, clock, maxSeq, rawItems] = raw;
612
- const items = rawItems.map(([agent, seq, content, parentAgent, parentSeq]) => ({
613
- id: { agent, seq },
677
+ const items = rawItems.map(([id, content, parentId, seq, ts]) => ({
678
+ id,
614
679
  content,
615
680
  isDeleted: false,
616
- parentId: parentAgent !== null ? { agent: parentAgent, seq: parentSeq } : null,
617
- seq: maxSeq,
681
+ parentId,
682
+ seq,
683
+ ts,
618
684
  }));
619
685
  return new TextRope(bulkLoad(items), aiBuild(items), newAgentId ?? agentId, newAgentId ? 0 : clock, maxSeq);
620
686
  }
@@ -662,11 +728,13 @@ function extractItemsFromSerializedTree(node) {
662
728
  if (!Array.isArray(items))
663
729
  return [];
664
730
  return items.map((item) => ({
665
- id: { agent: item.id.agent, seq: item.id.seq },
731
+ id: typeof item.id === 'string' ? item.id : createItemId(item.id.agent, item.id.seq),
666
732
  content: item.content,
667
733
  isDeleted: item.isDeleted ?? false,
668
- parentId: item.parentId ? { agent: item.parentId.agent, seq: item.parentId.seq } : null,
734
+ parentId: item.parentId === null ? null :
735
+ (typeof item.parentId === 'string' ? item.parentId : createItemId(item.parentId.agent, item.parentId.seq)),
669
736
  seq: item.seq,
737
+ ts: item.ts ?? Date.now() / 1000,
670
738
  }));
671
739
  }
672
740
  else if (n.type === 'internal') {