@vuer-ai/vuer-rtc-server 0.2.3 → 0.4.1

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 (104) hide show
  1. package/.env +1 -1
  2. package/README.md +56 -0
  3. package/dist/archive/ArchivalService.js +1 -1
  4. package/dist/archive/ArchivalService.js.map +1 -1
  5. package/dist/broker/InMemoryBroker.d.ts +2 -2
  6. package/dist/broker/InMemoryBroker.d.ts.map +1 -1
  7. package/dist/broker/InMemoryBroker.js +4 -4
  8. package/dist/broker/InMemoryBroker.js.map +1 -1
  9. package/dist/broker/types.d.ts +3 -3
  10. package/dist/broker/types.d.ts.map +1 -1
  11. package/dist/journal/CoalescingService.d.ts.map +1 -1
  12. package/dist/journal/CoalescingService.js +18 -208
  13. package/dist/journal/CoalescingService.js.map +1 -1
  14. package/dist/journal/GraphJournalService.d.ts +127 -0
  15. package/dist/journal/GraphJournalService.d.ts.map +1 -0
  16. package/dist/journal/GraphJournalService.js +491 -0
  17. package/dist/journal/GraphJournalService.js.map +1 -0
  18. package/dist/journal/JournalRLE.d.ts +2 -2
  19. package/dist/journal/JournalRLE.js +14 -14
  20. package/dist/journal/JournalRLE.js.map +1 -1
  21. package/dist/journal/JournalRepository.js +7 -7
  22. package/dist/journal/JournalRepository.js.map +1 -1
  23. package/dist/journal/JournalService.d.ts.map +1 -1
  24. package/dist/journal/JournalService.js +6 -40
  25. package/dist/journal/JournalService.js.map +1 -1
  26. package/dist/journal/RLECompression.d.ts +9 -9
  27. package/dist/journal/RLECompression.d.ts.map +1 -1
  28. package/dist/journal/RLECompression.js +22 -22
  29. package/dist/journal/RLECompression.js.map +1 -1
  30. package/dist/journal/TextJournalService.d.ts +98 -0
  31. package/dist/journal/TextJournalService.d.ts.map +1 -0
  32. package/dist/journal/TextJournalService.js +401 -0
  33. package/dist/journal/TextJournalService.js.map +1 -0
  34. package/dist/journal/index.d.ts +3 -1
  35. package/dist/journal/index.d.ts.map +1 -1
  36. package/dist/journal/index.js +4 -1
  37. package/dist/journal/index.js.map +1 -1
  38. package/dist/journal/rle-demo.js +11 -11
  39. package/dist/journal/rle-demo.js.map +1 -1
  40. package/dist/serve.d.ts +29 -11
  41. package/dist/serve.d.ts.map +1 -1
  42. package/dist/serve.js +558 -93
  43. package/dist/serve.js.map +1 -1
  44. package/dist/transport/RTCServer.d.ts +2 -2
  45. package/dist/transport/RTCServer.d.ts.map +1 -1
  46. package/dist/transport/RTCServer.js +22 -22
  47. package/dist/transport/RTCServer.js.map +1 -1
  48. package/docs/API.md +642 -0
  49. package/examples/compression-example.ts +3 -3
  50. package/package.json +2 -2
  51. package/prisma/schema.prisma +124 -6
  52. package/src/archive/ArchivalService.ts +1 -1
  53. package/src/broker/InMemoryBroker.ts +4 -4
  54. package/src/broker/types.ts +3 -3
  55. package/src/journal/CoalescingService.ts +18 -235
  56. package/src/journal/{JournalService.ts → GraphJournalService.ts} +34 -74
  57. package/src/journal/JournalRLE.ts +15 -15
  58. package/src/journal/JournalRepository.ts +7 -7
  59. package/src/journal/RLECompression.ts +24 -24
  60. package/src/journal/TextJournalService.ts +483 -0
  61. package/src/journal/index.ts +10 -2
  62. package/src/journal/rle-demo.ts +11 -11
  63. package/src/serve.ts +598 -94
  64. package/src/transport/RTCServer.ts +23 -23
  65. package/tests/benchmark/journal-optimization-benchmark.test.ts +14 -14
  66. package/tests/compression/compression.test.ts +8 -8
  67. package/tests/demo.ts +88 -88
  68. package/tests/e2e/convergence.test.ts +9 -9
  69. package/tests/e2e/helpers/assertions.ts +22 -0
  70. package/tests/e2e/helpers/createTestServer.ts +4 -4
  71. package/tests/e2e/latency.test.ts +47 -41
  72. package/tests/e2e/packet-loss.test.ts +6 -6
  73. package/tests/e2e/relay.test.ts +9 -9
  74. package/tests/e2e/sync-perf.test.ts +5 -5
  75. package/tests/e2e/sync-reconciliation.test.ts +6 -6
  76. package/tests/e2e/text-sync.test.ts +14 -14
  77. package/tests/e2e/tombstone-convergence.test.ts +22 -22
  78. package/tests/fixtures/array-ops.jsonl +6 -6
  79. package/tests/fixtures/boolean-ops.jsonl +6 -6
  80. package/tests/fixtures/color-ops.jsonl +4 -4
  81. package/tests/fixtures/edit-buffer.jsonl +3 -3
  82. package/tests/fixtures/messages.jsonl +4 -4
  83. package/tests/fixtures/node-ops.jsonl +6 -6
  84. package/tests/fixtures/number-ops.jsonl +7 -7
  85. package/tests/fixtures/object-ops.jsonl +4 -4
  86. package/tests/fixtures/operations.jsonl +7 -7
  87. package/tests/fixtures/string-ops.jsonl +4 -4
  88. package/tests/fixtures/undo-redo.jsonl +3 -3
  89. package/tests/fixtures/vector-ops.jsonl +9 -9
  90. package/tests/integration/repositories.test.ts +8 -9
  91. package/tests/journal/compaction-load-bug.test.ts +31 -31
  92. package/tests/journal/compaction.test.ts +26 -26
  93. package/tests/journal/journal-rle.test.ts +38 -38
  94. package/tests/journal/journal-service.test.ts +13 -13
  95. package/tests/journal/lww-ordering-bug.test.ts +39 -39
  96. package/tests/journal/rle-compression.test.ts +71 -71
  97. package/tests/journal/text-coalescing.test.ts +34 -34
  98. package/tests/test-data/datatypes.ts +85 -85
  99. package/tests/test-data/operations-example.ts +62 -62
  100. package/tests/test-data/scene-example.ts +11 -11
  101. package/tests/unit/operations.test.ts +7 -7
  102. package/tests/unit/s3-compression.test.ts +5 -3
  103. package/tests/unit/vectorClock.test.ts +2 -2
  104. package/tests/journal/multi-session-coalescing.test.ts +0 -871
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Journal Service - Business logic for journal operations
2
+ * Graph Journal Service - Business logic for graph document journal operations
3
3
  *
4
4
  * Handles:
5
- * - Receiving and validating CRDTMessages
5
+ * - Receiving and validating CRDTMessages for graph documents
6
6
  * - Storing messages in journal
7
7
  * - Processing meta.undo/meta.redo operations
8
8
  * - Computing current graph state
@@ -21,8 +21,6 @@ import {
21
21
  OperationValidator,
22
22
  TextRope,
23
23
  compactRope,
24
- toRaw,
25
- fromRaw,
26
24
  } from '@vuer-ai/vuer-rtc';
27
25
 
28
26
  import { JournalRepository } from './JournalRepository.js';
@@ -31,7 +29,7 @@ import { DocumentRepository } from '../persistence/DocumentRepository.js';
31
29
  /**
32
30
  * Safely serialize an object, handling circular references by removing them.
33
31
  * Also strips 'parent' references which cause cycles in tree structures.
34
- * Properly serializes TextRope instances using toRaw().
32
+ * TextRope instances are automatically serialized to strings via toJSON().
35
33
  */
36
34
  function safeSerialize(obj: unknown): unknown {
37
35
  const seen = new WeakSet();
@@ -39,14 +37,6 @@ function safeSerialize(obj: unknown): unknown {
39
37
  // Skip parent references which cause cycles
40
38
  if (key === 'parent') return undefined;
41
39
 
42
- // Properly serialize TextRope instances using toRaw()
43
- if (value instanceof TextRope) {
44
- return {
45
- _textRope: true,
46
- raw: toRaw(value),
47
- };
48
- }
49
-
50
40
  if (typeof value === 'object' && value !== null) {
51
41
  if (seen.has(value)) return undefined; // Circular reference
52
42
  seen.add(value);
@@ -71,49 +61,21 @@ export interface DocumentState {
71
61
  journal: JournalEntry[];
72
62
  }
73
63
 
74
- /**
75
- * Recursively walk an object and restore TextRope instances from their raw form.
76
- */
77
- function restoreTextRopes(obj: any): any {
78
- if (obj === null || obj === undefined) return obj;
79
-
80
- // Check if this is a serialized TextRope
81
- if (typeof obj === 'object' && obj._textRope === true && obj.raw) {
82
- return fromRaw(obj.raw);
83
- }
84
-
85
- // Recursively process arrays
86
- if (Array.isArray(obj)) {
87
- return obj.map(restoreTextRopes);
88
- }
89
-
90
- // Recursively process objects
91
- if (typeof obj === 'object') {
92
- const result: any = {};
93
- for (const [key, value] of Object.entries(obj)) {
94
- result[key] = restoreTextRopes(value);
95
- }
96
- return result;
97
- }
98
-
99
- return obj;
100
- }
101
64
 
102
65
  /**
103
66
  * Safely parse a Document.currentState (Json) into a Snapshot,
104
67
  * providing defaults for any missing fields.
105
- * Restores TextRope instances from their serialized raw form.
68
+ * Text properties are stored as plain strings and lazy-upgraded to TextRope on edit.
106
69
  */
107
70
  function parseSnapshot(currentState: unknown): Snapshot {
108
71
  const raw = (currentState ?? {}) as Record<string, unknown>;
109
72
 
110
- // Restore TextRope instances in the graph
111
- const graph = restoreTextRopes(raw.graph as SceneGraph) || createEmptyGraph();
73
+ const graph = (raw.graph as SceneGraph) || createEmptyGraph();
112
74
 
113
75
  return {
114
76
  graph,
115
77
  vectorClock: (raw.vectorClock as Record<string, number>) || {},
116
- lamportTime: (typeof raw.lamportTime === 'number' ? raw.lamportTime : 0),
78
+ lt: (typeof raw.lt === 'number' ? raw.lt : 0),
117
79
  journalIndex: (typeof raw.journalIndex === 'number' ? raw.journalIndex : 0),
118
80
  };
119
81
  }
@@ -159,7 +121,7 @@ function compactTextRopes(graph: SceneGraph): SceneGraph {
159
121
  return { ...graph, nodes };
160
122
  }
161
123
 
162
- export class JournalService {
124
+ export class GraphJournalService {
163
125
  private journalRepo: JournalRepository;
164
126
  private documentRepo: DocumentRepository;
165
127
  private validator: OperationValidator;
@@ -231,10 +193,10 @@ export class JournalService {
231
193
  for (let i = 1; i < memberClocks.length; i++) {
232
194
  const clock = memberClocks[i];
233
195
  // For each session in result, take min with this clock
234
- for (const sessionId of Object.keys(result)) {
235
- result[sessionId] = Math.min(
236
- result[sessionId],
237
- clock[sessionId] ?? 0,
196
+ for (const client of Object.keys(result)) {
197
+ result[client] = Math.min(
198
+ result[client],
199
+ clock[client] ?? 0,
238
200
  );
239
201
  }
240
202
  // Sessions not in result but in clock are implicitly 0 in result,
@@ -249,8 +211,8 @@ export class JournalService {
249
211
  * watermark[S] >= clock[S].
250
212
  */
251
213
  isDominatedByWatermark(msgClock: VectorClock, watermark: VectorClock): boolean {
252
- for (const [sessionId, time] of Object.entries(msgClock)) {
253
- if ((watermark[sessionId] ?? 0) < time) {
214
+ for (const [client, time] of Object.entries(msgClock)) {
215
+ if ((watermark[client] ?? 0) < time) {
254
216
  return false;
255
217
  }
256
218
  }
@@ -308,7 +270,7 @@ export class JournalService {
308
270
  // getSince() now returns PersistedJournalEntry with deletedAt from DB.
309
271
  const persistedEntries = await this.journalRepo.getSince(
310
272
  documentId,
311
- snapshot.lamportTime,
273
+ snapshot.lt,
312
274
  );
313
275
 
314
276
  const journal: JournalEntry[] = persistedEntries.map((entry) => ({
@@ -350,19 +312,19 @@ export class JournalService {
350
312
 
351
313
  // Process meta operations (undo/redo)
352
314
  for (const op of msg.ops) {
353
- if (op.otype === 'meta.undo') {
315
+ if (op.ot === 'meta.undo') {
354
316
  const targetId = (op as any).targetMsgId;
355
317
  const target = state.journal.find((e) => e.msg.id === targetId);
356
318
  if (target) {
357
- target.deletedAt = msg.timestamp;
358
- // Persist deletedAt to DB (timestamp is seconds, DB stores as Date in ms)
319
+ target.deletedAt = msg.ts;
320
+ // Persist deletedAt to DB (ts is seconds, DB stores as Date in ms)
359
321
  await this.journalRepo.updateDeletedAt(
360
322
  documentId,
361
323
  targetId,
362
- new Date(msg.timestamp * 1000),
324
+ new Date(msg.ts * 1000),
363
325
  );
364
326
  }
365
- } else if (op.otype === 'meta.redo') {
327
+ } else if (op.ot === 'meta.redo') {
366
328
  const targetId = (op as any).targetMsgId;
367
329
  const target = state.journal.find((e) => e.msg.id === targetId);
368
330
  if (target) {
@@ -393,7 +355,7 @@ export class JournalService {
393
355
 
394
356
  // Filter out meta ops for graph computation
395
357
  const realOps = entry.msg.ops.filter(
396
- (op) => !op.otype.startsWith('meta.')
358
+ (op) => !op.ot.startsWith('meta.')
397
359
  );
398
360
  if (realOps.length > 0) {
399
361
  graph = applyMessage(graph, { ...entry.msg, ops: realOps });
@@ -425,9 +387,9 @@ export class JournalService {
425
387
  // to handle out-of-order messages correctly after compaction
426
388
  const postSnapshotJournal = state.journal
427
389
  .filter((e) => {
428
- // Include message if ANY session in its clock is ahead of snapshot
429
- for (const [sessionId, time] of Object.entries(e.msg.clock)) {
430
- if (time > (state.snapshot.vectorClock[sessionId] ?? 0)) {
390
+ // Include message if ANY client in its clock is ahead of snapshot
391
+ for (const [client, time] of Object.entries(e.msg.clock)) {
392
+ if (time > (state.snapshot.vectorClock[client] ?? 0)) {
431
393
  return true;
432
394
  }
433
395
  }
@@ -435,10 +397,8 @@ export class JournalService {
435
397
  })
436
398
  .map((e) => e.msg);
437
399
 
438
- // Serialize TextRope instances in snapshot before sending to client.
439
- // The in-memory snapshot contains TextRope instances (from restoreTextRopes),
440
- // but MessagePack can't serialize class instances properly. Convert them to
441
- // {_textRope: true, raw: [...]} format so the client can hydrate them.
400
+ // Serialize snapshot before sending to client. TextRope instances (if any)
401
+ // are automatically converted to plain strings via toJSON().
442
402
  const serializedSnapshot = safeSerialize(state.snapshot) as Snapshot;
443
403
 
444
404
  return {
@@ -485,27 +445,27 @@ export class JournalService {
485
445
  // Build new snapshot from entries [0..compactUpToIndex]
486
446
  let newGraph = state.snapshot.graph;
487
447
  let mergedClock = { ...state.snapshot.vectorClock };
488
- let maxLamport = state.snapshot.lamportTime;
448
+ let maxLamport = state.snapshot.lt;
489
449
 
490
450
  for (let i = 0; i <= compactUpToIndex; i++) {
491
451
  const entry = state.journal[i];
492
452
  if (!entry.deletedAt) {
493
453
  // Filter out meta ops for graph computation
494
454
  const realOps = entry.msg.ops.filter(
495
- (op) => !op.otype.startsWith('meta.')
455
+ (op) => !op.ot.startsWith('meta.')
496
456
  );
497
457
  if (realOps.length > 0) {
498
458
  newGraph = applyMessage(newGraph, { ...entry.msg, ops: realOps });
499
459
  }
500
460
  }
501
461
  // Merge clock and lamport regardless of deletedAt
502
- for (const [sessionId, time] of Object.entries(entry.msg.clock)) {
503
- mergedClock[sessionId] = Math.max(
504
- mergedClock[sessionId] || 0,
462
+ for (const [client, time] of Object.entries(entry.msg.clock)) {
463
+ mergedClock[client] = Math.max(
464
+ mergedClock[client] || 0,
505
465
  time,
506
466
  );
507
467
  }
508
- maxLamport = Math.max(maxLamport, entry.msg.lamportTime);
468
+ maxLamport = Math.max(maxLamport, entry.msg.lt);
509
469
  }
510
470
 
511
471
  // Collect batchIds of compacted entries before mutating state
@@ -526,7 +486,7 @@ export class JournalService {
526
486
  state.snapshot = {
527
487
  graph: newGraph,
528
488
  vectorClock: mergedClock,
529
- lamportTime: maxLamport,
489
+ lt: maxLamport,
530
490
  journalIndex: maxLamport,
531
491
  };
532
492
 
@@ -585,7 +545,7 @@ export class JournalService {
585
545
  snapshot: {
586
546
  graph: createEmptyGraph(),
587
547
  vectorClock: {},
588
- lamportTime: 0,
548
+ lt: 0,
589
549
  journalIndex: 0,
590
550
  },
591
551
  journal: [],
@@ -611,7 +571,7 @@ export class JournalService {
611
571
  snapshot: {
612
572
  graph: createEmptyGraph(),
613
573
  vectorClock: {},
614
- lamportTime: 0,
574
+ lt: 0,
615
575
  journalIndex: 0,
616
576
  },
617
577
  journal: [],
@@ -18,7 +18,7 @@ import type { CRDTMessage } from '@vuer-ai/vuer-rtc';
18
18
  * Compressed operation segment - groups consecutive ops from same agent
19
19
  */
20
20
  export interface RLESegment {
21
- agentId: string; // sessionId from first message in run
21
+ agentId: string; // client from first message in run
22
22
  count: number; // Number of messages in this run
23
23
  messages: CRDTMessage[]; // The actual messages (compressed payload)
24
24
  }
@@ -40,8 +40,8 @@ export interface RLEEncodedJournal {
40
40
  /**
41
41
  * Encode a sequence of CRDTMessages using RLE
42
42
  *
43
- * Groups consecutive messages from the same sessionId into segments.
44
- * Each segment stores the sessionId once and all messages in that run.
43
+ * Groups consecutive messages from the same client into segments.
44
+ * Each segment stores the client once and all messages in that run.
45
45
  */
46
46
  export function encodeJournalRLE(messages: CRDTMessage[]): RLEEncodedJournal {
47
47
  if (messages.length === 0) {
@@ -61,7 +61,7 @@ export function encodeJournalRLE(messages: CRDTMessage[]): RLEEncodedJournal {
61
61
  let currentSegment: RLESegment | null = null;
62
62
 
63
63
  for (const msg of messages) {
64
- const agentId = msg.sessionId;
64
+ const agentId = msg.client;
65
65
 
66
66
  // Start a new segment if agent changes
67
67
  if (!currentSegment || currentSegment.agentId !== agentId) {
@@ -113,12 +113,12 @@ export function decodeJournalRLE(encoded: RLEEncodedJournal): CRDTMessage[] {
113
113
  const messages: CRDTMessage[] = [];
114
114
 
115
115
  for (const segment of encoded.segments) {
116
- // Verify all messages in segment have correct sessionId
116
+ // Verify all messages in segment have correct client
117
117
  for (const msg of segment.messages) {
118
- if (msg.sessionId !== segment.agentId) {
118
+ if (msg.client !== segment.agentId) {
119
119
  throw new Error(
120
- `RLE decode error: sessionId mismatch in segment. ` +
121
- `Expected ${segment.agentId}, got ${msg.sessionId}`
120
+ `RLE decode error: client mismatch in segment. ` +
121
+ `Expected ${segment.agentId}, got ${msg.client}`
122
122
  );
123
123
  }
124
124
  messages.push(msg);
@@ -184,9 +184,9 @@ export function verifyRLEIntegrity(
184
184
  errors.push(`Message ${i}: id mismatch (${orig.id} vs ${dec.id})`);
185
185
  }
186
186
 
187
- // Check sessionId
188
- if (orig.sessionId !== dec.sessionId) {
189
- errors.push(`Message ${i}: sessionId mismatch`);
187
+ // Check client
188
+ if (orig.client !== dec.client) {
189
+ errors.push(`Message ${i}: client mismatch`);
190
190
  }
191
191
 
192
192
  // Check vector clock (deep equality)
@@ -204,13 +204,13 @@ export function verifyRLEIntegrity(
204
204
  }
205
205
 
206
206
  // Check lamport time
207
- if (orig.lamportTime !== dec.lamportTime) {
208
- errors.push(`Message ${i}: lamportTime mismatch`);
207
+ if (orig.lt !== dec.lt) {
208
+ errors.push(`Message ${i}: lt mismatch`);
209
209
  }
210
210
 
211
211
  // Check timestamp
212
- if (orig.timestamp !== dec.timestamp) {
213
- errors.push(`Message ${i}: timestamp mismatch`);
212
+ if (orig.ts !== dec.ts) {
213
+ errors.push(`Message ${i}: ts mismatch`);
214
214
  }
215
215
 
216
216
  // Check operations (deep equality)
@@ -32,13 +32,13 @@ export class JournalRepository {
32
32
  return this.prisma.journalBatch.create({
33
33
  data: {
34
34
  documentId,
35
- sessionId: msg.sessionId,
35
+ client: msg.client,
36
36
  batchId: msg.id,
37
37
  operations: msg.ops as any[],
38
38
  vectorClock: msg.clock as any,
39
- lamportTime: msg.lamportTime,
40
- startTime: new Date(msg.timestamp * 1000),
41
- endTime: new Date(msg.timestamp * 1000),
39
+ lamportTime: msg.lt,
40
+ startTime: new Date(msg.ts * 1000),
41
+ endTime: new Date(msg.ts * 1000),
42
42
  },
43
43
  });
44
44
  }
@@ -67,10 +67,10 @@ export class JournalRepository {
67
67
  return batches.map((batch) => ({
68
68
  msg: {
69
69
  id: batch.batchId,
70
- sessionId: batch.sessionId,
70
+ client: batch.client,
71
71
  clock: (batch.vectorClock as Record<string, number>) ?? {},
72
- lamportTime: batch.lamportTime ?? 0,
73
- timestamp: batch.startTime.getTime() / 1000,
72
+ lt: batch.lamportTime ?? 0,
73
+ ts: batch.startTime.getTime() / 1000,
74
74
  ops: batch.operations as any[],
75
75
  },
76
76
  deletedAt: batch.deletedAt
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * Optimizes journal storage by:
5
5
  * 1. Run-length encoding consecutive operations (e.g., 10 sequential edits from same agent)
6
- * 2. Only storing agent/sessionId when it changes
6
+ * 2. Only storing agent/client when it changes
7
7
  * 3. Preserving CRDT semantics (no operations are combined or lost)
8
8
  *
9
- * Format: { sessionId, count: N, ops: [op1, op2, ...] }
10
- * - sessionId is stored with first op of each run
9
+ * Format: { client, count: N, ops: [op1, op2, ...] }
10
+ * - client is stored with first op of each run
11
11
  * - count tracks consecutive ops from same agent
12
12
  * - ops are stored as-is (no merging/combining)
13
13
  */
@@ -18,8 +18,8 @@ import type { CRDTMessage } from '@vuer-ai/vuer-rtc';
18
18
  * A run-length encoded journal entry
19
19
  */
20
20
  export interface RLEJournalEntry {
21
- sessionId: string; // Agent/session that performed this run
22
- count: number; // Number of consecutive operations from this session
21
+ client: string; // Agent/client that performed this run
22
+ count: number; // Number of consecutive operations from this client
23
23
  lamportTime: number; // Start lamport time of this run
24
24
  endLamportTime: number; // End lamport time (start + count - 1)
25
25
  ops: any[]; // Operations in this run (one per index)
@@ -27,14 +27,14 @@ export interface RLEJournalEntry {
27
27
  }
28
28
 
29
29
  /**
30
- * Encode consecutive operations from the same sessionId using RLE.
31
- * Returns a list of RLE entries where consecutive ops from same session are grouped.
30
+ * Encode consecutive operations from the same client using RLE.
31
+ * Returns a list of RLE entries where consecutive ops from same client are grouped.
32
32
  *
33
33
  * Example:
34
- * Input: [msg1(sid=A), msg2(sid=A), msg3(sid=B), msg4(sid=B), msg5(sid=B)]
34
+ * Input: [msg1(cid=A), msg2(cid=A), msg3(cid=B), msg4(cid=B), msg5(cid=B)]
35
35
  * Output: [
36
- * { sessionId: A, count: 2, ops: [msg1.ops[0], msg2.ops[0]], ... },
37
- * { sessionId: B, count: 3, ops: [msg3.ops[0], msg4.ops[0], msg5.ops[0]], ... }
36
+ * { client: A, count: 2, ops: [msg1.ops[0], msg2.ops[0]], ... },
37
+ * { client: B, count: 3, ops: [msg3.ops[0], msg4.ops[0], msg5.ops[0]], ... }
38
38
  * ]
39
39
  */
40
40
  export function encodeRLE(messages: CRDTMessage[]): RLEJournalEntry[] {
@@ -44,23 +44,23 @@ export function encodeRLE(messages: CRDTMessage[]): RLEJournalEntry[] {
44
44
  let currentRun: RLEJournalEntry | null = null;
45
45
 
46
46
  for (const msg of messages) {
47
- if (!currentRun || currentRun.sessionId !== msg.sessionId) {
47
+ if (!currentRun || currentRun.client !== msg.client) {
48
48
  // Start new run
49
49
  if (currentRun) {
50
50
  encoded.push(currentRun);
51
51
  }
52
52
  currentRun = {
53
- sessionId: msg.sessionId,
53
+ client: msg.client,
54
54
  count: 1,
55
- lamportTime: msg.lamportTime,
56
- endLamportTime: msg.lamportTime,
55
+ lamportTime: msg.lt,
56
+ endLamportTime: msg.lt,
57
57
  ops: [...msg.ops],
58
- timestamp: msg.timestamp,
58
+ timestamp: msg.ts,
59
59
  };
60
60
  } else {
61
61
  // Extend current run
62
62
  currentRun.count++;
63
- currentRun.endLamportTime = msg.lamportTime;
63
+ currentRun.endLamportTime = msg.lt;
64
64
  currentRun.ops.push(...msg.ops);
65
65
  }
66
66
  }
@@ -104,10 +104,10 @@ export function decodeRLE(
104
104
  // Reconstruct message
105
105
  messages.push({
106
106
  id: `msg-${lamportTime}`, // Note: Original IDs are lost; this is a limitation
107
- sessionId: entry.sessionId,
107
+ client: entry.client,
108
108
  clock: {}, // Vector clock info is lost in current RLE format
109
- lamportTime,
110
- timestamp: entry.timestamp,
109
+ lt: lamportTime,
110
+ ts: entry.timestamp,
111
111
  ops: opsForMsg,
112
112
  });
113
113
 
@@ -143,8 +143,8 @@ export function encodeRLEWithMetadata(
143
143
  const vectorClocks: Record<number, Record<string, number>> = {};
144
144
 
145
145
  for (const msg of messages) {
146
- messageIds[msg.lamportTime] = msg.id;
147
- vectorClocks[msg.lamportTime] = msg.clock;
146
+ messageIds[msg.lt] = msg.id;
147
+ vectorClocks[msg.lt] = msg.clock;
148
148
  }
149
149
 
150
150
  return {
@@ -176,10 +176,10 @@ export function decodeRLEWithMetadata(
176
176
 
177
177
  messages.push({
178
178
  id: encoded.messageIds[lamportTime] || `msg-${lamportTime}`,
179
- sessionId: entry.sessionId,
179
+ client: entry.client,
180
180
  clock: encoded.vectorClocks[lamportTime] || {},
181
- lamportTime,
182
- timestamp: entry.timestamp,
181
+ lt: lamportTime,
182
+ ts: entry.timestamp,
183
183
  ops: opsForMsg,
184
184
  });
185
185