@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.
- package/CLAUDE.md +29 -0
- package/COALESCE_FIX_VERIFICATION.md +81 -0
- package/REFACTORING_NOTES.md +229 -0
- package/dist/client/EditBuffer.d.ts +13 -1
- package/dist/client/EditBuffer.d.ts.map +1 -1
- package/dist/client/EditBuffer.js +47 -3
- package/dist/client/EditBuffer.js.map +1 -1
- package/dist/client/actions.d.ts +5 -1
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +12 -9
- package/dist/client/actions.js.map +1 -1
- package/dist/client/coalesceGraphOps.d.ts +34 -0
- package/dist/client/coalesceGraphOps.d.ts.map +1 -0
- package/dist/client/coalesceGraphOps.js +35 -0
- package/dist/client/coalesceGraphOps.js.map +1 -0
- package/dist/client/coalesceTextOperations.d.ts +42 -0
- package/dist/client/coalesceTextOperations.d.ts.map +1 -0
- package/dist/client/coalesceTextOperations.js +119 -0
- package/dist/client/coalesceTextOperations.js.map +1 -0
- package/dist/client/coalescence/index.d.ts +9 -0
- package/dist/client/coalescence/index.d.ts.map +1 -0
- package/dist/client/coalescence/index.js +9 -0
- package/dist/client/coalescence/index.js.map +1 -0
- package/dist/client/coalescence/registry.d.ts +48 -0
- package/dist/client/coalescence/registry.d.ts.map +1 -0
- package/dist/client/coalescence/registry.js +95 -0
- package/dist/client/coalescence/registry.js.map +1 -0
- package/dist/client/coalescence/textDeletes.d.ts +38 -0
- package/dist/client/coalescence/textDeletes.d.ts.map +1 -0
- package/dist/client/coalescence/textDeletes.js +68 -0
- package/dist/client/coalescence/textDeletes.js.map +1 -0
- package/dist/client/coalescence/textInserts.d.ts +45 -0
- package/dist/client/coalescence/textInserts.d.ts.map +1 -0
- package/dist/client/coalescence/textInserts.js +96 -0
- package/dist/client/coalescence/textInserts.js.map +1 -0
- package/dist/client/createGraph.d.ts.map +1 -1
- package/dist/client/createGraph.js +9 -2
- package/dist/client/createGraph.js.map +1 -1
- package/dist/client/createTextDocument.d.ts.map +1 -1
- package/dist/client/createTextDocument.js +2 -1
- package/dist/client/createTextDocument.js.map +1 -1
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/textActions.d.ts +1 -1
- package/dist/client/textActions.d.ts.map +1 -1
- package/dist/client/textActions.js +7 -2
- package/dist/client/textActions.js.map +1 -1
- package/dist/client/types.d.ts +3 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/crdt/GraphTextCRDT.d.ts +0 -4
- package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
- package/dist/crdt/GraphTextCRDT.js +3 -0
- package/dist/crdt/GraphTextCRDT.js.map +1 -1
- package/dist/crdt/Rope.d.ts +27 -6
- package/dist/crdt/Rope.d.ts.map +1 -1
- package/dist/crdt/Rope.js +137 -69
- package/dist/crdt/Rope.js.map +1 -1
- package/dist/operations/OperationTypes.d.ts +10 -26
- package/dist/operations/OperationTypes.d.ts.map +1 -1
- package/dist/operations/apply/text.d.ts.map +1 -1
- package/dist/operations/apply/text.js +8 -16
- package/dist/operations/apply/text.js.map +1 -1
- package/examples/05-coalescence-usage.ts +189 -0
- package/package.json +1 -1
- package/src/client/EditBuffer.ts +51 -3
- package/src/client/actions.ts +13 -9
- package/src/client/coalesceGraphOps.ts +40 -0
- package/src/client/coalesceTextOperations.ts +134 -0
- package/src/client/coalescence/index.ts +18 -0
- package/src/client/coalescence/registry.ts +137 -0
- package/src/client/coalescence/textDeletes.ts +94 -0
- package/src/client/coalescence/textInserts.ts +128 -0
- package/src/client/createGraph.ts +11 -2
- package/src/client/createTextDocument.ts +2 -1
- package/src/client/index.ts +14 -0
- package/src/client/textActions.ts +9 -2
- package/src/client/types.ts +4 -1
- package/src/crdt/GraphTextCRDT.ts +0 -5
- package/src/crdt/Rope.ts +155 -79
- package/src/operations/OperationTypes.ts +10 -8
- package/src/operations/apply/text.ts +8 -20
- package/test-coalescence.ts +201 -0
- package/tests/client/actions.test.ts +156 -0
- package/tests/client/coalesce-text-operations.test.ts +327 -0
- package/tests/client/edit-buffer.test.ts +137 -1
- package/tests/crdt/graph-text-crdt.test.ts +29 -17
- package/tests/crdt/rope.test.ts +13 -11
package/dist/crdt/Rope.d.ts
CHANGED
|
@@ -6,16 +6,18 @@
|
|
|
6
6
|
* ID lookups without invalidation.
|
|
7
7
|
*/
|
|
8
8
|
import { type ItemTree } from './BTree.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
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;
|
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,MAAM,
|
|
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
|
-
|
|
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
|
|
60
|
+
if (a < b)
|
|
58
61
|
return -1;
|
|
59
|
-
if (a
|
|
62
|
+
if (a > b)
|
|
60
63
|
return 1;
|
|
61
|
-
return
|
|
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:
|
|
83
|
-
endSeq:
|
|
102
|
+
startSeq: parsed.seq,
|
|
103
|
+
endSeq: parsed.seq + item.content.length,
|
|
84
104
|
item,
|
|
85
105
|
};
|
|
86
|
-
let arr = index.get(
|
|
106
|
+
let arr = index.get(parsed.agent);
|
|
87
107
|
if (!arr) {
|
|
88
108
|
arr = [];
|
|
89
|
-
index.set(
|
|
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
|
|
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,
|
|
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
|
-
|
|
134
|
+
const parsed = parseItemId(item.id);
|
|
135
|
+
let arr = index.get(parsed.agent);
|
|
114
136
|
if (!arr) {
|
|
115
137
|
arr = [];
|
|
116
|
-
index.set(
|
|
138
|
+
index.set(parsed.agent, arr);
|
|
117
139
|
}
|
|
118
|
-
arr.push({ startSeq:
|
|
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
|
|
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,
|
|
175
|
+
const ei = aiFind(entries, parsed.seq);
|
|
153
176
|
if (ei === -1)
|
|
154
177
|
return -1;
|
|
155
178
|
const item = entries[ei].item;
|
|
156
|
-
|
|
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:
|
|
209
|
+
id: createItemId(itemParsed.agent, itemParsed.seq),
|
|
185
210
|
content: item.content.slice(0, offset),
|
|
186
211
|
isDeleted: item.isDeleted,
|
|
187
|
-
parentId: item.parentId
|
|
212
|
+
parentId: item.parentId,
|
|
188
213
|
seq: item.seq,
|
|
214
|
+
ts: item.ts,
|
|
189
215
|
};
|
|
190
216
|
const after = {
|
|
191
|
-
id:
|
|
217
|
+
id: createItemId(itemParsed.agent, itemParsed.seq + offset),
|
|
192
218
|
content: item.content.slice(offset),
|
|
193
219
|
isDeleted: item.isDeleted,
|
|
194
|
-
parentId:
|
|
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 (
|
|
202
|
-
const maxSeqInItem =
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
286
|
-
|
|
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:
|
|
347
|
+
id: op.id,
|
|
302
348
|
content: op.content,
|
|
303
349
|
isDeleted: false,
|
|
304
|
-
parentId: op.parentId
|
|
350
|
+
parentId: op.parentId,
|
|
305
351
|
seq: op.seq,
|
|
352
|
+
ts: op.ts,
|
|
306
353
|
};
|
|
307
354
|
integrate(rope, newItem, parentOrdinal);
|
|
308
|
-
|
|
309
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
422
|
+
id: op.id,
|
|
373
423
|
content: op.content,
|
|
374
424
|
isDeleted: false,
|
|
375
|
-
parentId: op.parentId
|
|
425
|
+
parentId: op.parentId,
|
|
376
426
|
seq: op.seq,
|
|
427
|
+
ts: op.ts,
|
|
377
428
|
};
|
|
378
429
|
integrate(rope, newItem, parentOrdinal);
|
|
379
|
-
|
|
380
|
-
|
|
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:
|
|
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
|
-
|
|
478
|
+
const parsed = parseItemId(del.id);
|
|
479
|
+
let ranges = byAgent.get(parsed.agent);
|
|
427
480
|
if (!ranges) {
|
|
428
481
|
ranges = [];
|
|
429
|
-
byAgent.set(
|
|
482
|
+
byAgent.set(parsed.agent, ranges);
|
|
430
483
|
}
|
|
431
|
-
ranges.push({ start:
|
|
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
|
|
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:
|
|
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
|
|
588
|
-
|
|
589
|
-
|
|
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
|
|
603
|
-
item.id.seq,
|
|
667
|
+
item.id,
|
|
604
668
|
item.content,
|
|
605
|
-
item.parentId
|
|
606
|
-
item.
|
|
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(([
|
|
613
|
-
id
|
|
677
|
+
const items = rawItems.map(([id, content, parentId, seq, ts]) => ({
|
|
678
|
+
id,
|
|
614
679
|
content,
|
|
615
680
|
isDeleted: false,
|
|
616
|
-
parentId
|
|
617
|
-
seq
|
|
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:
|
|
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
|
|
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') {
|