@vuer-ai/vuer-rtc 0.7.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CLAUDE.md +3 -2
  2. package/dist/client/EditBuffer.d.ts +4 -4
  3. package/dist/client/EditBuffer.d.ts.map +1 -1
  4. package/dist/client/EditBuffer.js +26 -25
  5. package/dist/client/EditBuffer.js.map +1 -1
  6. package/dist/client/actions.d.ts +3 -3
  7. package/dist/client/actions.d.ts.map +1 -1
  8. package/dist/client/actions.js +71 -70
  9. package/dist/client/actions.js.map +1 -1
  10. package/dist/client/coalesceGraphOps.d.ts +4 -4
  11. package/dist/client/coalesceGraphOps.js +4 -4
  12. package/dist/client/coalesceTextOperations.d.ts.map +1 -1
  13. package/dist/client/coalesceTextOperations.js +23 -20
  14. package/dist/client/coalesceTextOperations.js.map +1 -1
  15. package/dist/client/coalescence/lwwOperations.js +3 -3
  16. package/dist/client/coalescence/lwwOperations.js.map +1 -1
  17. package/dist/client/coalescence/numberOperations.js +2 -2
  18. package/dist/client/coalescence/numberOperations.js.map +1 -1
  19. package/dist/client/coalescence/registry.d.ts +3 -3
  20. package/dist/client/coalescence/registry.d.ts.map +1 -1
  21. package/dist/client/coalescence/registry.js +11 -11
  22. package/dist/client/coalescence/registry.js.map +1 -1
  23. package/dist/client/coalescence/textDeletes.d.ts +8 -7
  24. package/dist/client/coalescence/textDeletes.d.ts.map +1 -1
  25. package/dist/client/coalescence/textDeletes.js +11 -11
  26. package/dist/client/coalescence/textDeletes.js.map +1 -1
  27. package/dist/client/coalescence/textInserts.d.ts +8 -5
  28. package/dist/client/coalescence/textInserts.d.ts.map +1 -1
  29. package/dist/client/coalescence/textInserts.js +32 -12
  30. package/dist/client/coalescence/textInserts.js.map +1 -1
  31. package/dist/client/coalescence/utils.d.ts +3 -9
  32. package/dist/client/coalescence/utils.d.ts.map +1 -1
  33. package/dist/client/coalescence/utils.js +10 -8
  34. package/dist/client/coalescence/utils.js.map +1 -1
  35. package/dist/client/coalescence/vector3Operations.js +2 -2
  36. package/dist/client/coalescence/vector3Operations.js.map +1 -1
  37. package/dist/client/createGraph.d.ts +2 -2
  38. package/dist/client/createGraph.js +4 -4
  39. package/dist/client/createGraph.js.map +1 -1
  40. package/dist/client/createTextDocument.d.ts +1 -1
  41. package/dist/client/createTextDocument.js +3 -3
  42. package/dist/client/createTextDocument.js.map +1 -1
  43. package/dist/client/hooks.d.ts +3 -3
  44. package/dist/client/hooks.d.ts.map +1 -1
  45. package/dist/client/hooks.js +4 -4
  46. package/dist/client/hooks.js.map +1 -1
  47. package/dist/client/textActions.d.ts +2 -2
  48. package/dist/client/textActions.d.ts.map +1 -1
  49. package/dist/client/textActions.js +47 -47
  50. package/dist/client/textActions.js.map +1 -1
  51. package/dist/client/textTypes.d.ts +8 -8
  52. package/dist/client/textTypes.d.ts.map +1 -1
  53. package/dist/client/types.d.ts +4 -4
  54. package/dist/client/types.d.ts.map +1 -1
  55. package/dist/crdt/GraphTextCRDT.d.ts +2 -2
  56. package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
  57. package/dist/crdt/GraphTextCRDT.js +6 -6
  58. package/dist/crdt/GraphTextCRDT.js.map +1 -1
  59. package/dist/crdt/Rope.d.ts +13 -14
  60. package/dist/crdt/Rope.d.ts.map +1 -1
  61. package/dist/crdt/Rope.js +130 -59
  62. package/dist/crdt/Rope.js.map +1 -1
  63. package/dist/crdt/index.d.ts +1 -1
  64. package/dist/crdt/index.d.ts.map +1 -1
  65. package/dist/crdt/index.js +1 -1
  66. package/dist/crdt/index.js.map +1 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/operations/OperationTypes.d.ts +45 -48
  72. package/dist/operations/OperationTypes.d.ts.map +1 -1
  73. package/dist/operations/OperationValidator.js +11 -11
  74. package/dist/operations/OperationValidator.js.map +1 -1
  75. package/dist/operations/apply/node.js +3 -3
  76. package/dist/operations/apply/node.js.map +1 -1
  77. package/dist/operations/apply/text.d.ts.map +1 -1
  78. package/dist/operations/apply/text.js +35 -32
  79. package/dist/operations/apply/text.js.map +1 -1
  80. package/dist/operations/apply/types.d.ts +4 -4
  81. package/dist/operations/apply/types.d.ts.map +1 -1
  82. package/dist/operations/apply/types.js +8 -8
  83. package/dist/operations/apply/types.js.map +1 -1
  84. package/dist/operations/dispatcher.d.ts.map +1 -1
  85. package/dist/operations/dispatcher.js +52 -13
  86. package/dist/operations/dispatcher.js.map +1 -1
  87. package/dist/serdes.d.ts +1 -1
  88. package/dist/serdes.d.ts.map +1 -1
  89. package/dist/state/ConflictResolver.d.ts +9 -9
  90. package/dist/state/ConflictResolver.d.ts.map +1 -1
  91. package/dist/state/ConflictResolver.js +20 -20
  92. package/dist/state/ConflictResolver.js.map +1 -1
  93. package/dist/state/DType.d.ts +2 -2
  94. package/dist/state/DType.d.ts.map +1 -1
  95. package/dist/state/DType.js +14 -14
  96. package/dist/state/DType.js.map +1 -1
  97. package/dist/state/VectorClock.d.ts +6 -6
  98. package/dist/state/VectorClock.d.ts.map +1 -1
  99. package/dist/state/VectorClock.js +14 -14
  100. package/dist/state/VectorClock.js.map +1 -1
  101. package/dist/state/index.d.ts +1 -1
  102. package/dist/state/index.js +1 -1
  103. package/examples/01-basic-usage.ts +16 -16
  104. package/examples/02-concurrent-edits.ts +29 -29
  105. package/examples/03-scene-building.ts +28 -28
  106. package/examples/04-conflict-resolution.ts +56 -56
  107. package/examples/05-coalescence-usage.ts +23 -23
  108. package/examples/README.md +12 -12
  109. package/package.json +1 -1
  110. package/src/client/EditBuffer.ts +28 -27
  111. package/src/client/TEXT_DOCUMENT_API.md +9 -9
  112. package/src/client/actions.ts +74 -70
  113. package/src/client/coalesceGraphOps.ts +4 -4
  114. package/src/client/coalesceTextOperations.ts +26 -22
  115. package/src/client/coalescence/lwwOperations.ts +3 -3
  116. package/src/client/coalescence/numberOperations.ts +2 -2
  117. package/src/client/coalescence/registry.ts +13 -12
  118. package/src/client/coalescence/textDeletes.ts +22 -18
  119. package/src/client/coalescence/textInserts.ts +49 -25
  120. package/src/client/coalescence/utils.ts +14 -11
  121. package/src/client/coalescence/vector3Operations.ts +2 -2
  122. package/src/client/createGraph.ts +4 -4
  123. package/src/client/createTextDocument.ts +3 -3
  124. package/src/client/hooks.tsx +5 -5
  125. package/src/client/textActions.ts +47 -47
  126. package/src/client/textTypes.ts +8 -8
  127. package/src/client/types.ts +4 -4
  128. package/src/crdt/GraphTextCRDT.ts +6 -6
  129. package/src/crdt/Rope.ts +156 -71
  130. package/src/crdt/index.ts +2 -0
  131. package/src/index.ts +2 -0
  132. package/src/operations/OperationTypes.ts +47 -47
  133. package/src/operations/OperationValidator.ts +11 -11
  134. package/src/operations/apply/node.ts +3 -3
  135. package/src/operations/apply/text.ts +38 -32
  136. package/src/operations/apply/types.ts +11 -11
  137. package/src/operations/dispatcher.ts +57 -13
  138. package/src/serdes.ts +1 -1
  139. package/src/state/ConflictResolver.ts +23 -23
  140. package/src/state/DType.ts +16 -16
  141. package/src/state/VectorClock.ts +14 -14
  142. package/src/state/index.ts +1 -1
  143. package/tests/client/actions.test.ts +76 -76
  144. package/tests/client/coalesce-graph-operations.test.ts +84 -84
  145. package/tests/client/coalesce-text-operations.test.ts +91 -114
  146. package/tests/client/compaction.test.ts +18 -18
  147. package/tests/client/delete-coalescence-bug.test.ts +34 -34
  148. package/tests/client/edit-buffer.test.ts +27 -30
  149. package/tests/client/graph-coalescence-phase1.test.ts +66 -66
  150. package/tests/client/graph-coalescence.test.ts +50 -50
  151. package/tests/client/journal-benchmark.test.ts +5 -5
  152. package/tests/crdt/graph-text-crdt.test.ts +60 -64
  153. package/tests/crdt/rope.test.ts +9 -8
  154. package/tests/crdt/text-operations.test.ts +28 -28
  155. package/tests/fixtures/array-ops.jsonl +6 -6
  156. package/tests/fixtures/boolean-ops.jsonl +6 -6
  157. package/tests/fixtures/color-ops.jsonl +4 -4
  158. package/tests/fixtures/edit-buffer.jsonl +3 -3
  159. package/tests/fixtures/node-ops.jsonl +6 -6
  160. package/tests/fixtures/number-ops.jsonl +7 -7
  161. package/tests/fixtures/object-ops.jsonl +4 -4
  162. package/tests/fixtures/operations.jsonl +7 -7
  163. package/tests/fixtures/string-ops.jsonl +4 -4
  164. package/tests/fixtures/undo-redo.jsonl +3 -3
  165. package/tests/fixtures/vector-ops.jsonl +17 -17
  166. package/tests/operations/collections.test.ts +4 -4
  167. package/tests/operations/nodes.test.ts +5 -5
  168. package/tests/operations/operation-ordering.test.ts +406 -0
  169. package/tests/operations/primitives.test.ts +4 -4
  170. package/tests/operations/unified-schema.test.ts +27 -27
  171. package/tests/operations/vectors.test.ts +4 -4
  172. package/tests/sync/digest.test.ts +5 -5
@@ -29,12 +29,12 @@ export function generateUUID(): string {
29
29
  /**
30
30
  * Create initial client state
31
31
  */
32
- export function createInitialState(sessionId: string, initialSnapshot?: Snapshot): ClientState {
32
+ export function createInitialState(client: string, initialSnapshot?: Snapshot): ClientState {
33
33
  const emptyGraph = createEmptyGraph();
34
34
  const snapshot = initialSnapshot ?? {
35
35
  graph: emptyGraph,
36
36
  vectorClock: {},
37
- lamportTime: 0,
37
+ lt: 0,
38
38
  journalIndex: 0,
39
39
  };
40
40
 
@@ -43,9 +43,9 @@ export function createInitialState(sessionId: string, initialSnapshot?: Snapshot
43
43
  journal: [],
44
44
  edits: { ops: [], baseGraph: null },
45
45
  snapshot,
46
- lamportTime: snapshot.lamportTime ?? 0,
47
- vectorClock: clockManager.create(sessionId),
48
- sessionId,
46
+ lt: snapshot.lt ?? 0,
47
+ vectorClock: clockManager.create(client),
48
+ client: client,
49
49
  };
50
50
  }
51
51
 
@@ -158,23 +158,26 @@ function reuseUnchangedNodes(oldGraph: SceneGraph, newGraph: SceneGraph): SceneG
158
158
  export function rebuildGraph(
159
159
  snapshot: Snapshot,
160
160
  journal: JournalEntry[],
161
- pendingOps: Operation[]
161
+ pendingOps: Operation[],
162
+ client: string,
163
+ vectorClock: VectorClock,
164
+ lt: number
162
165
  ): SceneGraph {
163
166
  let graph = cloneSnapshotGraph(snapshot.graph);
164
167
 
165
168
  // Sort journal by causal order so out-of-order deliveries
166
169
  // (e.g. via sync retransmission) are replayed correctly.
167
170
  const sorted = [...journal].sort((a, b) => {
168
- const dt = a.msg.lamportTime - b.msg.lamportTime;
171
+ const dt = a.msg.lt - b.msg.lt;
169
172
  if (dt !== 0) return dt;
170
- return a.msg.sessionId < b.msg.sessionId ? -1 : a.msg.sessionId > b.msg.sessionId ? 1 : 0;
173
+ return a.msg.client < b.msg.client ? -1 : a.msg.client > b.msg.client ? 1 : 0;
171
174
  });
172
175
 
173
176
  // Apply journal (skip deleted entries and meta ops)
174
177
  for (const entry of sorted) {
175
178
  if (entry.deletedAt) continue;
176
179
 
177
- const realOps = entry.msg.ops.filter(op => !op.otype.startsWith('meta.'));
180
+ const realOps = entry.msg.ops.filter(op => !op.ot.startsWith('meta.'));
178
181
  if (realOps.length > 0) {
179
182
  graph = applyMessage(graph, { ...entry.msg, ops: realOps });
180
183
  }
@@ -184,10 +187,10 @@ export function rebuildGraph(
184
187
  if (pendingOps.length > 0) {
185
188
  for (const op of pendingOps) {
186
189
  applyOperation(graph, op, {
187
- sessionId: 'local',
188
- clock: {},
189
- lamportTime: 0,
190
- timestamp: Date.now() / 1000,
190
+ client,
191
+ clock: vectorClock,
192
+ lt,
193
+ ts: Date.now() / 1000,
191
194
  });
192
195
  }
193
196
  }
@@ -208,8 +211,8 @@ export function onEdit(state: ClientState, op: Operation): ClientState {
208
211
 
209
212
  if (existingIdx >= 0) {
210
213
  const existing = ops[existingIdx];
211
- if (existing.otype === op.otype && isAdditiveOp(op.otype)) {
212
- const mergedValue = mergeValues(op.otype, (existing as any).value, (op as any).value);
214
+ if (existing.ot === op.ot && isAdditiveOp(op.ot)) {
215
+ const mergedValue = mergeValues(op.ot, (existing as any).value, (op as any).value);
213
216
  ops[existingIdx] = { ...existing, value: mergedValue } as Operation;
214
217
  } else {
215
218
  ops[existingIdx] = op;
@@ -226,12 +229,12 @@ export function onEdit(state: ClientState, op: Operation): ClientState {
226
229
  if (typeof op.key === 'string' && op.key in graph.nodes) {
227
230
  keysToClone.push(op.key);
228
231
  }
229
- if (op.otype === 'node.move') {
232
+ if (op.ot === 'node.move') {
230
233
  const { nodeKey, newParent } = (op as any).value;
231
234
  if (nodeKey && nodeKey in graph.nodes) keysToClone.push(nodeKey);
232
235
  if (newParent && newParent in graph.nodes) keysToClone.push(newParent);
233
236
  }
234
- if (op.otype === 'node.remove') {
237
+ if (op.ot === 'node.remove') {
235
238
  const nodeKey = (op as any).value;
236
239
  if (typeof nodeKey === 'string' && nodeKey in graph.nodes) keysToClone.push(nodeKey);
237
240
  }
@@ -247,10 +250,10 @@ export function onEdit(state: ClientState, op: Operation): ClientState {
247
250
  }
248
251
 
249
252
  applyOperation(graph, op, {
250
- sessionId: state.sessionId,
253
+ client: state.client,
251
254
  clock: state.vectorClock,
252
- lamportTime: state.lamportTime + 1,
253
- timestamp: Date.now() / 1000,
255
+ lt: state.lt + 1,
256
+ ts: Date.now() / 1000,
254
257
  });
255
258
 
256
259
  return {
@@ -276,8 +279,8 @@ export function commitEdits(
276
279
  return { state, msg: null };
277
280
  }
278
281
 
279
- const newClock = clockManager.increment(state.vectorClock, state.sessionId);
280
- const newLamport = state.lamportTime + 1;
282
+ const newClock = clockManager.increment(state.vectorClock, state.client);
283
+ const newLamport = state.lt + 1;
281
284
 
282
285
  // Coalesce CRDT operations before sending (if threshold specified)
283
286
  const coalescedOps = coalescingThresholdMs !== undefined
@@ -285,11 +288,11 @@ export function commitEdits(
285
288
  : state.edits.ops;
286
289
 
287
290
  const msg: CRDTMessage = {
288
- id: `${state.sessionId}:${newLamport}`,
289
- sessionId: state.sessionId,
291
+ id: `${state.client}:${newLamport}`,
292
+ client: state.client,
290
293
  clock: newClock,
291
- lamportTime: newLamport,
292
- timestamp: Date.now() / 1000,
294
+ lt: newLamport,
295
+ ts: Date.now() / 1000,
293
296
  ops: coalescedOps,
294
297
  };
295
298
 
@@ -298,7 +301,7 @@ export function commitEdits(
298
301
  ...state,
299
302
  journal: [...state.journal, { msg, ack: false }],
300
303
  edits: { ops: [], baseGraph: null },
301
- lamportTime: msg.lamportTime,
304
+ lt: msg.lt,
302
305
  vectorClock: newClock,
303
306
  },
304
307
  msg,
@@ -343,11 +346,11 @@ export function onRemoteMessage(state: ClientState, msg: CRDTMessage): ClientSta
343
346
 
344
347
  // Process meta ops (undo/redo) — clone modified entries
345
348
  for (const op of msg.ops) {
346
- if (op.otype === 'meta.undo') {
349
+ if (op.ot === 'meta.undo') {
347
350
  const targetMsgId = (op as any).targetMsgId;
348
351
  const idx = journal.findIndex(e => e.msg.id === targetMsgId);
349
- if (idx >= 0) journal[idx] = { ...journal[idx], deletedAt: msg.lamportTime };
350
- } else if (op.otype === 'meta.redo') {
352
+ if (idx >= 0) journal[idx] = { ...journal[idx], deletedAt: msg.lt };
353
+ } else if (op.ot === 'meta.redo') {
351
354
  const targetMsgId = (op as any).targetMsgId;
352
355
  const idx = journal.findIndex(e => e.msg.id === targetMsgId);
353
356
  if (idx >= 0) {
@@ -358,11 +361,11 @@ export function onRemoteMessage(state: ClientState, msg: CRDTMessage): ClientSta
358
361
  }
359
362
 
360
363
  const vectorClock = clockManager.merge(state.vectorClock, msg.clock);
361
- const lamportTime = Math.max(state.lamportTime, msg.lamportTime);
362
- const rebuilt = rebuildGraph(state.snapshot, journal, state.edits.ops);
364
+ const lamportTime = Math.max(state.lt, msg.lt);
365
+ const rebuilt = rebuildGraph(state.snapshot, journal, state.edits.ops, state.client, vectorClock, lamportTime);
363
366
  const graph = reuseUnchangedNodes(state.graph, rebuilt);
364
367
 
365
- return { ...state, journal, vectorClock, lamportTime, graph };
368
+ return { ...state, journal, vectorClock, lt: lamportTime, graph };
366
369
  }
367
370
 
368
371
  /**
@@ -382,8 +385,8 @@ export function undo(state: ClientState): { state: ClientState; msg: CRDTMessage
382
385
  const lastActive = [...currentState.journal]
383
386
  .reverse()
384
387
  .find(e => !e.deletedAt
385
- && e.msg.sessionId === currentState.sessionId
386
- && !e.msg.ops.some(op => op.otype.startsWith('meta.'))
388
+ && e.msg.client === currentState.client
389
+ && !e.msg.ops.some(op => op.ot.startsWith('meta.'))
387
390
  );
388
391
 
389
392
  if (!lastActive) return { state: currentState, msg: null };
@@ -391,15 +394,15 @@ export function undo(state: ClientState): { state: ClientState; msg: CRDTMessage
391
394
  }
392
395
 
393
396
  // Create undo message
394
- const newClock = clockManager.increment(currentState.vectorClock, currentState.sessionId);
395
- const newLamport = currentState.lamportTime + 1;
397
+ const newClock = clockManager.increment(currentState.vectorClock, currentState.client);
398
+ const newLamport = currentState.lt + 1;
396
399
  const undoMsg: CRDTMessage = {
397
- id: `${currentState.sessionId}:${newLamport}`,
398
- sessionId: currentState.sessionId,
400
+ id: `${currentState.client}:${newLamport}`,
401
+ client: currentState.client,
399
402
  clock: newClock,
400
- lamportTime: newLamport,
401
- timestamp: Date.now() / 1000,
402
- ops: [{ otype: 'meta.undo', key: '_meta', path: '_meta', targetMsgId }],
403
+ lt: newLamport,
404
+ ts: Date.now() / 1000,
405
+ ops: [{ ot: 'meta.undo', key: '_meta', path: '_meta', targetMsgId }],
403
406
  };
404
407
 
405
408
  // Build new journal with undo entry + target marked deleted
@@ -407,19 +410,19 @@ export function undo(state: ClientState): { state: ClientState; msg: CRDTMessage
407
410
  // can reliably pick the most recently undone entry.
408
411
  const journal: JournalEntry[] = currentState.journal.map(e => {
409
412
  if (e.msg.id === targetMsgId) {
410
- return { ...e, deletedAt: undoMsg.lamportTime, ack: false };
413
+ return { ...e, deletedAt: undoMsg.lt, ack: false };
411
414
  }
412
415
  return e;
413
416
  });
414
417
  journal.push({ msg: undoMsg, ack: false });
415
418
 
416
- const rebuilt = rebuildGraph(currentState.snapshot, journal, currentState.edits.ops);
419
+ const rebuilt = rebuildGraph(currentState.snapshot, journal, currentState.edits.ops, currentState.client, currentState.vectorClock, currentState.lt);
417
420
  const graph = reuseUnchangedNodes(currentState.graph, rebuilt);
418
421
 
419
422
  const newState: ClientState = {
420
423
  ...currentState,
421
424
  journal,
422
- lamportTime: undoMsg.lamportTime,
425
+ lt: undoMsg.lt,
423
426
  vectorClock: newClock,
424
427
  graph,
425
428
  };
@@ -437,8 +440,8 @@ export function redo(state: ClientState): { state: ClientState; msg: CRDTMessage
437
440
  for (const e of state.journal) {
438
441
  if (
439
442
  e.deletedAt &&
440
- e.msg.sessionId === state.sessionId &&
441
- !e.msg.ops.some(op => op.otype.startsWith('meta.'))
443
+ e.msg.client === state.client &&
444
+ !e.msg.ops.some(op => op.ot.startsWith('meta.'))
442
445
  ) {
443
446
  if (!lastDeleted || e.deletedAt > lastDeleted.deletedAt!) {
444
447
  lastDeleted = e;
@@ -449,15 +452,15 @@ export function redo(state: ClientState): { state: ClientState; msg: CRDTMessage
449
452
  if (!lastDeleted) return { state, msg: null };
450
453
 
451
454
  // Create redo message
452
- const newClock = clockManager.increment(state.vectorClock, state.sessionId);
453
- const newLamport = state.lamportTime + 1;
455
+ const newClock = clockManager.increment(state.vectorClock, state.client);
456
+ const newLamport = state.lt + 1;
454
457
  const redoMsg: CRDTMessage = {
455
- id: `${state.sessionId}:${newLamport}`,
456
- sessionId: state.sessionId,
458
+ id: `${state.client}:${newLamport}`,
459
+ client: state.client,
457
460
  clock: newClock,
458
- lamportTime: newLamport,
459
- timestamp: Date.now() / 1000,
460
- ops: [{ otype: 'meta.redo', key: '_meta', path: '_meta', targetMsgId: lastDeleted.msg.id }],
461
+ lt: newLamport,
462
+ ts: Date.now() / 1000,
463
+ ops: [{ ot: 'meta.redo', key: '_meta', path: '_meta', targetMsgId: lastDeleted.msg.id }],
461
464
  };
462
465
 
463
466
  // Build new journal with redo entry + target un-deleted
@@ -470,13 +473,13 @@ export function redo(state: ClientState): { state: ClientState; msg: CRDTMessage
470
473
  });
471
474
  journal.push({ msg: redoMsg, ack: false });
472
475
 
473
- const rebuilt = rebuildGraph(state.snapshot, journal, state.edits.ops);
476
+ const rebuilt = rebuildGraph(state.snapshot, journal, state.edits.ops, state.client, state.vectorClock, state.lt);
474
477
  const graph = reuseUnchangedNodes(state.graph, rebuilt);
475
478
 
476
479
  const newState: ClientState = {
477
480
  ...state,
478
481
  journal,
479
- lamportTime: redoMsg.lamportTime,
482
+ lt: redoMsg.lt,
480
483
  vectorClock: newClock,
481
484
  graph,
482
485
  };
@@ -543,7 +546,7 @@ export function compact(state: ClientState): ClientState {
543
546
  const entry = state.journal[i];
544
547
  if (entry.deletedAt) continue;
545
548
 
546
- const realOps = entry.msg.ops.filter(op => !op.otype.startsWith('meta.'));
549
+ const realOps = entry.msg.ops.filter(op => !op.ot.startsWith('meta.'));
547
550
  if (realOps.length > 0) {
548
551
  snapshotGraph = applyMessage(snapshotGraph, { ...entry.msg, ops: realOps });
549
552
  }
@@ -557,7 +560,7 @@ export function compact(state: ClientState): ClientState {
557
560
  snapshot: {
558
561
  graph: snapshotGraph,
559
562
  vectorClock: state.journal[lastAckedIdx].msg.clock,
560
- lamportTime: state.journal[lastAckedIdx].msg.lamportTime,
563
+ lt: state.journal[lastAckedIdx].msg.lt,
561
564
  journalIndex: state.snapshot.journalIndex + lastAckedIdx + 1,
562
565
  },
563
566
  journal: state.journal.slice(lastAckedIdx + 1),
@@ -601,7 +604,7 @@ export function compactToWatermark(state: ClientState, watermark: VectorClock):
601
604
  const entry = state.journal[i];
602
605
  if (entry.deletedAt) continue;
603
606
 
604
- const realOps = entry.msg.ops.filter(op => !op.otype.startsWith('meta.'));
607
+ const realOps = entry.msg.ops.filter(op => !op.ot.startsWith('meta.'));
605
608
  if (realOps.length > 0) {
606
609
  snapshotGraph = applyMessage(snapshotGraph, { ...entry.msg, ops: realOps });
607
610
  }
@@ -612,7 +615,7 @@ export function compactToWatermark(state: ClientState, watermark: VectorClock):
612
615
  snapshot: {
613
616
  graph: snapshotGraph,
614
617
  vectorClock: watermark,
615
- lamportTime: state.journal[lastIdx].msg.lamportTime,
618
+ lt: state.journal[lastIdx].msg.lt,
616
619
  journalIndex: state.snapshot.journalIndex + lastIdx + 1,
617
620
  },
618
621
  journal: state.journal.slice(lastIdx + 1),
@@ -646,13 +649,13 @@ export function getPendingCount(state: ClientState): number {
646
649
  * Initialize from server snapshot + journal
647
650
  */
648
651
  export function initFromServer(
649
- sessionId: string,
652
+ client: string,
650
653
  snapshot: Snapshot,
651
654
  journal: CRDTMessage[]
652
655
  ): ClientState {
653
656
  let graph = snapshot.graph;
654
657
  const journalEntries: JournalEntry[] = [];
655
- let maxLamport = snapshot.lamportTime ?? 0;
658
+ let maxLamport = snapshot.lt ?? 0;
656
659
  let mergedClock: VectorClock = snapshot.vectorClock;
657
660
 
658
661
  // Filter out journal entries already baked into the snapshot to
@@ -667,11 +670,11 @@ export function initFromServer(
667
670
  for (const msg of filteredJournal) {
668
671
  // Process meta ops
669
672
  for (const op of msg.ops) {
670
- if (op.otype === 'meta.undo') {
673
+ if (op.ot === 'meta.undo') {
671
674
  const targetMsgId = (op as any).targetMsgId;
672
675
  const target = journalEntries.find(e => e.msg.id === targetMsgId);
673
- if (target) target.deletedAt = msg.lamportTime;
674
- } else if (op.otype === 'meta.redo') {
676
+ if (target) target.deletedAt = msg.lt;
677
+ } else if (op.ot === 'meta.redo') {
675
678
  const targetMsgId = (op as any).targetMsgId;
676
679
  const target = journalEntries.find(e => e.msg.id === targetMsgId);
677
680
  if (target) delete target.deletedAt;
@@ -679,20 +682,21 @@ export function initFromServer(
679
682
  }
680
683
 
681
684
  journalEntries.push({ msg, ack: true });
682
- maxLamport = Math.max(maxLamport, msg.lamportTime);
685
+ maxLamport = Math.max(maxLamport, msg.lt);
683
686
  mergedClock = clockManager.merge(mergedClock, msg.clock);
684
687
  }
685
688
 
686
689
  // Rebuild graph
687
- graph = rebuildGraph(snapshot, journalEntries, []);
690
+ const finalClock = clockManager.increment(mergedClock, client);
691
+ graph = rebuildGraph(snapshot, journalEntries, [], client, finalClock, maxLamport);
688
692
 
689
693
  return {
690
694
  graph,
691
695
  journal: journalEntries,
692
696
  edits: { ops: [], baseGraph: null },
693
697
  snapshot,
694
- lamportTime: maxLamport,
695
- vectorClock: clockManager.increment(mergedClock, sessionId),
696
- sessionId,
698
+ lt: maxLamport,
699
+ vectorClock: finalClock,
700
+ client: client,
697
701
  };
698
702
  }
@@ -19,13 +19,13 @@ import { coalesceOperations, type CoalesceOptions } from './coalescence/index.js
19
19
  * Example:
20
20
  * ```typescript
21
21
  * const ops = [
22
- * { otype: 'text.insert', id: 'alice:1', content: 'h', ... },
23
- * { otype: 'text.insert', id: 'alice:2', content: 'e', ... },
24
- * { otype: 'text.insert', id: 'alice:3', content: 'l', ... },
22
+ * { ot: 'text.insert', id: 'alice:1', content: 'h', ... },
23
+ * { ot: 'text.insert', id: 'alice:2', content: 'e', ... },
24
+ * { ot: 'text.insert', id: 'alice:3', content: 'l', ... },
25
25
  * ];
26
26
  *
27
27
  * const coalesced = coalesceGraphOps(ops, { thresholdMs: 300 });
28
- * // Result: [{ otype: 'text.insert', id: 'alice:1', content: 'hel', ... }]
28
+ * // Result: [{ ot: 'text.insert', id: 'alice:1', content: 'hel', ... }]
29
29
  * ```
30
30
  *
31
31
  * @param ops - Array of graph operations
@@ -13,7 +13,7 @@
13
13
 
14
14
  import type { TextOperation } from './textTypes.js';
15
15
  import type { InsertOp, DeleteOp } from '../crdt/Rope.js';
16
- import { parseItemId } from '../crdt/Rope.js';
16
+ import { parseItemId, createItemId } from '../crdt/Rope.js';
17
17
  import { optimizeDeletions, parseItemId as parseId } from './coalescence/utils.js';
18
18
 
19
19
  export interface CoalesceOptions {
@@ -21,17 +21,18 @@ export interface CoalesceOptions {
21
21
  thresholdMs?: number;
22
22
  }
23
23
 
24
+
24
25
  /**
25
26
  * Sort and optimize deletions array.
26
27
  * Deletions must be sorted in ascending order for optimizeDeletions to work correctly.
27
28
  */
28
- function sortAndOptimizeDeletions(deletions: Array<{ id: string; length: number }>): Array<{ id: string; length: number }> {
29
+ function sortAndOptimizeDeletions(deletions: Array<[string, number]>): Array<[string, number]> {
29
30
  if (deletions.length === 0) return deletions;
30
31
 
31
32
  // Sort deletions by agent, then by sequence number (ascending)
32
33
  const sorted = [...deletions].sort((a, b) => {
33
- const aId = parseId(a.id);
34
- const bId = parseId(b.id);
34
+ const aId = parseId(a[0]); // id is at [0]
35
+ const bId = parseId(b[0]); // id is at [0]
35
36
  if (aId.agent !== bId.agent) {
36
37
  return aId.agent.localeCompare(bId.agent);
37
38
  }
@@ -65,11 +66,11 @@ export function coalesceTextOperations(
65
66
  let pendingDelete: DeleteOp | null = null;
66
67
 
67
68
  for (const op of ops) {
68
- if (op.otype === 'insert') {
69
+ if (op.ot === 'insert') {
69
70
  // Flush any pending delete before starting new insert
70
71
  if (pendingDelete !== null) {
71
72
  // Sort and optimize deletions array before flushing
72
- pendingDelete.deletions = sortAndOptimizeDeletions(pendingDelete.deletions);
73
+ pendingDelete.rm = sortAndOptimizeDeletions(pendingDelete.rm);
73
74
  result.push(pendingDelete);
74
75
  pendingDelete = null;
75
76
  }
@@ -87,9 +88,10 @@ export function coalesceTextOperations(
87
88
  // Check merge conditions
88
89
  const sameAgent = prevId.agent === currId.agent;
89
90
 
90
- // IDs must be sequential: next ID = prev ID + prev content length
91
- // Example: prev="alice:5" content="hel"(3 chars) → next="alice:8"
92
- const sequentialIds = currId.seq === prevId.seq + prevOp.content.length;
91
+ // IDs must be sequential: next ID = prev ID + prev value length
92
+ // Example: prev="alice:5" value="hel"(3 chars) → next="alice:8"
93
+ const prevContent = prevOp.value[1]; // content is at [1]
94
+ const sequentialIds = currId.seq === prevId.seq + prevContent.length;
93
95
 
94
96
  // Time threshold: operations must be close in time (ts is in seconds)
95
97
  const timeDiffMs = (currOp.ts - prevOp.ts) * 1000;
@@ -100,17 +102,19 @@ export function coalesceTextOperations(
100
102
  // (not the first ID, which is what prevOp.id contains after merging)
101
103
  // OR both should have the same parent (inserting at same position)
102
104
  const prevLastId = (prevOp as any)._lastCharId || prevOp.id;
105
+ const currOpAnchor = currOp.value[0]; // anchor is at [0] (can be null)
106
+ const prevOpAnchor = prevOp.value[0]; // anchor is at [0] (can be null)
103
107
  const formsChain =
104
- currOp.parentId === prevLastId ||
105
- (prevOp.parentId === currOp.parentId && prevOp.parentId !== null);
108
+ currOpAnchor === prevLastId ||
109
+ (prevOpAnchor === currOpAnchor && prevOpAnchor !== null);
106
110
 
107
111
  if (sameAgent && sequentialIds && withinThreshold && formsChain) {
108
112
  // Merge operations
113
+ const currContent = currOp.value[1]; // content is at [1]
109
114
  const mergedOp: InsertOp = {
110
- otype: 'insert',
115
+ ot: 'insert',
111
116
  id: prevOp.id, // Keep first ID (anchor point)
112
- content: prevOp.content + currOp.content, // Concatenate content
113
- parentId: prevOp.parentId, // Keep first parentId
117
+ value: [prevOpAnchor, prevContent + currContent], // [anchor, merged content]
114
118
  seq: Math.max(prevOp.seq, currOp.seq), // Use max Lamport clock for ordering
115
119
  ts: prevOp.ts, // Keep first timestamp (when sequence started)
116
120
  // Track the last character ID for chain validation in next merge
@@ -120,16 +124,16 @@ export function coalesceTextOperations(
120
124
  } else {
121
125
  // Debug: Log why merge failed
122
126
  if (!sameAgent) console.log('[coalesce] Different agents:', prevId.agent, 'vs', currId.agent);
123
- if (!sequentialIds) console.log('[coalesce] Non-sequential IDs:', currId.seq, '!==', prevId.seq + prevOp.content.length);
127
+ if (!sequentialIds) console.log('[coalesce] Non-sequential IDs:', currId.seq, '!==', prevId.seq + prevContent.length);
124
128
  if (!withinThreshold) console.log('[coalesce] Time threshold exceeded:', timeDiffMs, 'ms >', thresholdMs, 'ms');
125
- if (!formsChain) console.log('[coalesce] Not a YATA chain:', { currParent: currOp.parentId, prevId: prevOp.id, prevParent: prevOp.parentId, currOp: currOp.id });
129
+ if (!formsChain) console.log('[coalesce] Not a YATA chain:', { currAnchor: currOpAnchor, prevId: prevOp.id, prevAnchor: prevOpAnchor, currOp: currOp.id });
126
130
 
127
131
  // Can't merge - flush pending and start new
128
132
  result.push(pendingInsert);
129
133
  pendingInsert = { ...currOp };
130
134
  }
131
135
  }
132
- } else if (op.otype === 'delete') {
136
+ } else if (op.ot === 'delete') {
133
137
  // Flush any pending insert before handling delete
134
138
  if (pendingInsert !== null) {
135
139
  result.push(pendingInsert);
@@ -138,7 +142,7 @@ export function coalesceTextOperations(
138
142
 
139
143
  if (pendingDelete === null) {
140
144
  // Start new pending delete
141
- pendingDelete = { ...op, deletions: [...op.deletions] };
145
+ pendingDelete = { ...op, rm: [...op.rm] };
142
146
  } else {
143
147
  // Try to merge consecutive delete operations
144
148
  // Deletes can be merged if they're within the time threshold
@@ -147,11 +151,11 @@ export function coalesceTextOperations(
147
151
 
148
152
  if (canMerge) {
149
153
  // Merge by combining deletions
150
- pendingDelete.deletions.push(...op.deletions);
154
+ pendingDelete.rm.push(...op.rm);
151
155
  } else {
152
156
  // Can't merge - flush pending and start new
153
157
  result.push(pendingDelete);
154
- pendingDelete = { ...op, deletions: [...op.deletions] };
158
+ pendingDelete = { ...op, rm: [...op.rm] };
155
159
  }
156
160
  }
157
161
  } else {
@@ -162,7 +166,7 @@ export function coalesceTextOperations(
162
166
  }
163
167
  if (pendingDelete !== null) {
164
168
  // Sort and optimize deletions array before flushing
165
- pendingDelete.deletions = sortAndOptimizeDeletions(pendingDelete.deletions);
169
+ pendingDelete.rm = sortAndOptimizeDeletions(pendingDelete.rm);
166
170
  result.push(pendingDelete);
167
171
  pendingDelete = null;
168
172
  }
@@ -176,7 +180,7 @@ export function coalesceTextOperations(
176
180
  }
177
181
  if (pendingDelete !== null) {
178
182
  // Sort and optimize deletions array before flushing
179
- pendingDelete.deletions = sortAndOptimizeDeletions(pendingDelete.deletions);
183
+ pendingDelete.rm = sortAndOptimizeDeletions(pendingDelete.rm);
180
184
  result.push(pendingDelete);
181
185
  }
182
186
 
@@ -41,8 +41,8 @@ export type LWWSetOp =
41
41
  */
42
42
  export function isLWWSetOp(op: Operation): op is LWWSetOp {
43
43
  return (
44
- typeof op.otype === 'string' &&
45
- op.otype.endsWith('.set') &&
44
+ typeof op.ot === 'string' &&
45
+ op.ot.endsWith('.set') &&
46
46
  (op as any).value !== undefined
47
47
  );
48
48
  }
@@ -82,7 +82,7 @@ export function coalesceLWWSets(
82
82
  const sameTarget =
83
83
  pending.key === op.key &&
84
84
  pending.path === op.path &&
85
- pending.otype === op.otype;
85
+ pending.ot === op.ot;
86
86
 
87
87
  if (sameTarget) {
88
88
  // LWW: Replace with newer value
@@ -16,7 +16,7 @@ export type { NumberAddOp };
16
16
  */
17
17
  export function isNumberAddOp(op: Operation): op is NumberAddOp {
18
18
  return (
19
- op.otype === 'number.add' &&
19
+ op.ot === 'number.add' &&
20
20
  typeof (op as any).value === 'number'
21
21
  );
22
22
  }
@@ -58,7 +58,7 @@ export function coalesceNumberAdds(
58
58
  if (sameTarget) {
59
59
  // Merge operations by summing values
60
60
  pending = {
61
- otype: 'number.add',
61
+ ot: 'number.add',
62
62
  key: pending.key,
63
63
  path: pending.path,
64
64
  value: pending.value + op.value,
@@ -5,12 +5,13 @@
5
5
  * Allows extensible operation coalescence with a consistent interface.
6
6
  */
7
7
 
8
- import type { Operation } from '../../operations/OperationTypes.js';
8
+ import type { Operation, TextInsertOp as GraphTextInsert, TextDeleteOp as GraphTextDelete, TextReplaceOp } from '../../operations/OperationTypes.js';
9
9
  import { coalesceTextInserts, isTextInsertOp, type TextInsertOp } from './textInserts.js';
10
10
  import { coalesceTextDeletes, isTextDeleteOp, type TextDeleteOp } from './textDeletes.js';
11
11
  import { coalesceNumberAdds, isNumberAddOp } from './numberOperations.js';
12
12
  import { coalesceVector3Adds, isVector3AddOp } from './vector3Operations.js';
13
13
  import { coalesceLWWSets, isLWWSetOp } from './lwwOperations.js';
14
+ import { parseItemId } from '../../crdt/Rope.js';
14
15
 
15
16
  export interface CoalesceOptions {
16
17
  /** Time threshold in milliseconds (default: 1000ms = 1 second) */
@@ -47,11 +48,11 @@ const registry = new Map<string, RegistryEntry>();
47
48
  * Register a coalescence handler for an operation type
48
49
  */
49
50
  export function registerCoalescer<T extends Operation>(
50
- otype: string,
51
+ ot: string,
51
52
  guard: TypeGuard<T>,
52
53
  handler: CoalesceHandler<T>
53
54
  ): void {
54
- registry.set(otype, {
55
+ registry.set(ot, {
55
56
  guard: guard as unknown as TypeGuard,
56
57
  handler: handler as unknown as CoalesceHandler
57
58
  });
@@ -60,15 +61,15 @@ export function registerCoalescer<T extends Operation>(
60
61
  /**
61
62
  * Get coalescence handler for an operation type
62
63
  */
63
- export function getCoalescer(otype: string): RegistryEntry | undefined {
64
- return registry.get(otype);
64
+ export function getCoalescer(ot: string): RegistryEntry | undefined {
65
+ return registry.get(ot);
65
66
  }
66
67
 
67
68
  /**
68
69
  * Check if an operation type has a registered coalescer
69
70
  */
70
- export function hasCoalescer(otype: string): boolean {
71
- return registry.has(otype);
71
+ export function hasCoalescer(ot: string): boolean {
72
+ return registry.has(ot);
72
73
  }
73
74
 
74
75
  /**
@@ -89,11 +90,11 @@ export function coalesceOperations(
89
90
  let currentType: string | null = null;
90
91
 
91
92
  for (const op of ops) {
92
- const entry = registry.get(op.otype);
93
+ const entry = registry.get(op.ot);
93
94
 
94
95
  if (entry && entry.guard(op)) {
95
96
  // Operation has a coalescer
96
- if (currentType === op.otype) {
97
+ if (currentType === op.ot) {
97
98
  // Same type - add to pending group
98
99
  pendingGroup.push(op);
99
100
  } else {
@@ -105,7 +106,7 @@ export function coalesceOperations(
105
106
  result.push(...coalesced);
106
107
  }
107
108
  }
108
- currentType = op.otype;
109
+ currentType = op.ot;
109
110
  pendingGroup = [op];
110
111
  }
111
112
  } else {
@@ -154,6 +155,6 @@ const LWW_SET_OPERATIONS = [
154
155
  'object.set',
155
156
  ] as const;
156
157
 
157
- for (const otype of LWW_SET_OPERATIONS) {
158
- registerCoalescer(otype, isLWWSetOp as TypeGuard<any>, coalesceLWWSets as CoalesceHandler<any>);
158
+ for (const ot of LWW_SET_OPERATIONS) {
159
+ registerCoalescer(ot, isLWWSetOp as TypeGuard<any>, coalesceLWWSets as CoalesceHandler<any>);
159
160
  }