@vuer-ai/vuer-rtc-server 0.2.0 → 0.2.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.
- package/.env +1 -0
- package/S3_COMPRESSION_GUIDE.md +233 -0
- package/dist/archive/ArchivalService.d.ts +117 -0
- package/dist/archive/ArchivalService.d.ts.map +1 -0
- package/dist/archive/ArchivalService.js +181 -0
- package/dist/archive/ArchivalService.js.map +1 -0
- package/dist/broker/InMemoryBroker.d.ts +2 -0
- package/dist/broker/InMemoryBroker.d.ts.map +1 -1
- package/dist/broker/InMemoryBroker.js +4 -0
- package/dist/broker/InMemoryBroker.js.map +1 -1
- package/dist/compression/CompressionUtils.d.ts +57 -0
- package/dist/compression/CompressionUtils.d.ts.map +1 -0
- package/dist/compression/CompressionUtils.js +90 -0
- package/dist/compression/CompressionUtils.js.map +1 -0
- package/dist/compression/index.d.ts +7 -0
- package/dist/compression/index.d.ts.map +1 -0
- package/dist/compression/index.js +7 -0
- package/dist/compression/index.js.map +1 -0
- package/dist/journal/CoalescingService.d.ts +63 -0
- package/dist/journal/CoalescingService.d.ts.map +1 -0
- package/dist/journal/CoalescingService.js +507 -0
- package/dist/journal/CoalescingService.js.map +1 -0
- package/dist/journal/JournalRLE.d.ts +81 -0
- package/dist/journal/JournalRLE.d.ts.map +1 -0
- package/dist/journal/JournalRLE.js +199 -0
- package/dist/journal/JournalRLE.js.map +1 -0
- package/dist/journal/JournalService.d.ts +7 -3
- package/dist/journal/JournalService.d.ts.map +1 -1
- package/dist/journal/JournalService.js +152 -12
- package/dist/journal/JournalService.js.map +1 -1
- package/dist/journal/RLECompression.d.ts +73 -0
- package/dist/journal/RLECompression.d.ts.map +1 -0
- package/dist/journal/RLECompression.js +152 -0
- package/dist/journal/RLECompression.js.map +1 -0
- package/dist/journal/rle-demo.d.ts +8 -0
- package/dist/journal/rle-demo.d.ts.map +1 -0
- package/dist/journal/rle-demo.js +159 -0
- package/dist/journal/rle-demo.js.map +1 -0
- package/dist/persistence/S3ColdStorage.d.ts +62 -0
- package/dist/persistence/S3ColdStorage.d.ts.map +1 -0
- package/dist/persistence/S3ColdStorage.js +88 -0
- package/dist/persistence/S3ColdStorage.js.map +1 -0
- package/dist/persistence/S3ColdStorageIntegration.d.ts +78 -0
- package/dist/persistence/S3ColdStorageIntegration.d.ts.map +1 -0
- package/dist/persistence/S3ColdStorageIntegration.js +93 -0
- package/dist/persistence/S3ColdStorageIntegration.js.map +1 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +623 -15
- package/dist/serve.js.map +1 -1
- package/docs/RLE_COMPRESSION.md +397 -0
- package/examples/compression-example.ts +259 -0
- package/package.json +14 -14
- package/src/archive/ArchivalService.ts +250 -0
- package/src/broker/InMemoryBroker.ts +5 -0
- package/src/compression/CompressionUtils.ts +113 -0
- package/src/compression/index.ts +14 -0
- package/src/journal/COALESCING.md +267 -0
- package/src/journal/CoalescingService.ts +626 -0
- package/src/journal/JournalRLE.ts +265 -0
- package/src/journal/JournalService.ts +163 -11
- package/src/journal/RLECompression.ts +210 -0
- package/src/journal/rle-demo.ts +193 -0
- package/src/serve.ts +702 -15
- package/tests/benchmark/journal-optimization-benchmark.test.ts +482 -0
- package/tests/compression/compression.test.ts +343 -0
- package/tests/integration/repositories.test.ts +89 -0
- package/tests/journal/compaction-load-bug.test.ts +409 -0
- package/tests/journal/compaction.test.ts +42 -2
- package/tests/journal/journal-rle.test.ts +511 -0
- package/tests/journal/lww-ordering-bug.test.ts +248 -0
- package/tests/journal/multi-session-coalescing.test.ts +871 -0
- package/tests/journal/rle-compression.test.ts +526 -0
- package/tests/journal/text-coalescing.test.ts +210 -0
- package/tests/unit/s3-compression.test.ts +257 -0
- package/PHASE1_SUMMARY.md +0 -94
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Operation Coalescing Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Coalescing reduces journal size by merging compatible operations. This improves:
|
|
6
|
+
- Storage efficiency (fewer operations stored)
|
|
7
|
+
- Replay performance (fewer operations to apply)
|
|
8
|
+
- State serialization depth (fewer items in B-trees)
|
|
9
|
+
|
|
10
|
+
## Session Boundaries and Causality Preservation
|
|
11
|
+
|
|
12
|
+
**Critical constraint**: Coalescing MUST preserve the **interleaved order** of operations across sessions to maintain causality.
|
|
13
|
+
|
|
14
|
+
### The Problem
|
|
15
|
+
|
|
16
|
+
Given messages in timestamp order:
|
|
17
|
+
```
|
|
18
|
+
msg1: sessionA [op1, op2]
|
|
19
|
+
msg2: sessionA [op3]
|
|
20
|
+
msg3: sessionB [op4]
|
|
21
|
+
msg4: sessionA [op5]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**WRONG** - Grouping all operations by session:
|
|
25
|
+
```
|
|
26
|
+
coalesced-A: [op1, op2, op3, op5] // ❌ Lost causality
|
|
27
|
+
coalesced-B: [op4]
|
|
28
|
+
```
|
|
29
|
+
This violates causality because `op5` happened after `op4` was received. If `op5` was created in response to seeing `op4`, reordering them breaks the happened-before relation.
|
|
30
|
+
|
|
31
|
+
**CORRECT** - Preserve interleaved order, coalesce consecutive runs:
|
|
32
|
+
```
|
|
33
|
+
run1-A: [op1, op2, op3] // Consecutive messages from A
|
|
34
|
+
run2-B: [op4] // Session switch to B
|
|
35
|
+
run3-A: [op5] // Back to A (preserves that op5 came AFTER op4)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Implementation Strategy
|
|
39
|
+
|
|
40
|
+
1. Process messages in timestamp order
|
|
41
|
+
2. Detect "session switches" (when `msg[i].sessionId !== msg[i-1].sessionId`)
|
|
42
|
+
3. Coalesce each consecutive run from the same session
|
|
43
|
+
4. Emit coalesced messages in the same order as the runs
|
|
44
|
+
|
|
45
|
+
This approach preserves Lamport's "happened-before" relation while still achieving compression benefits.
|
|
46
|
+
|
|
47
|
+
## Types of Coalescing
|
|
48
|
+
|
|
49
|
+
### 1. Client-Side Edit Batching (Optional, UI-Level)
|
|
50
|
+
|
|
51
|
+
**Status**: Optional feature controlled by UI toggle
|
|
52
|
+
|
|
53
|
+
**Current behavior**: Operations are batched with a timer (default 300ms), but each edit creates a separate operation in the batch.
|
|
54
|
+
|
|
55
|
+
**Purpose**: Reduces network traffic by delaying commits during rapid editing.
|
|
56
|
+
|
|
57
|
+
**Implementation location**: `packages/vuer-rtc/src/client/createGraph.ts`
|
|
58
|
+
|
|
59
|
+
### 2. Server-Side TextRope Coalescing (Automatic)
|
|
60
|
+
|
|
61
|
+
**Status**: ✅ **Implemented** (automatically runs during compaction)
|
|
62
|
+
|
|
63
|
+
**How it works**: When the server compacts the journal, it merges single-character TextRope items into multi-character spans, preventing B-tree depth explosion.
|
|
64
|
+
|
|
65
|
+
**Performance impact**: Up to 5000x speedup on real editing traces (based on "CRDTs Go Brrr")
|
|
66
|
+
|
|
67
|
+
**Implementation location**: `packages/vuer-rtc-server/src/journal/JournalService.ts` (compactTextRopes function)
|
|
68
|
+
|
|
69
|
+
### 3. Server-Side Operation Coalescing (Journal-Level)
|
|
70
|
+
|
|
71
|
+
**Status**: Partially implemented via CoalescingService
|
|
72
|
+
|
|
73
|
+
Merges historical operations in the journal. Called via `POST /api/documents/:id/coalesce`.
|
|
74
|
+
|
|
75
|
+
## Coalescing Rules
|
|
76
|
+
|
|
77
|
+
### Rule 1: Text Insert Coalescing
|
|
78
|
+
|
|
79
|
+
Consecutive `text.insert` operations from the same session, where each insert's position follows the previous insert, are merged into a single multi-character insert.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Before:
|
|
83
|
+
{ otype: "text.insert", position: 0, value: "a" }
|
|
84
|
+
{ otype: "text.insert", position: 1, value: "b" }
|
|
85
|
+
{ otype: "text.insert", position: 2, value: "c" }
|
|
86
|
+
|
|
87
|
+
After:
|
|
88
|
+
{ otype: "text.insert", position: 0, value: "abc" }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Constraints**:
|
|
92
|
+
- Same session ID
|
|
93
|
+
- Positions are sequential (insert_n.position === insert_n-1.position + len(insert_n-1.value))
|
|
94
|
+
- Same target node and path
|
|
95
|
+
|
|
96
|
+
### Rule 1b: Text Delete Coalescing ✅ **NEW**
|
|
97
|
+
|
|
98
|
+
Consecutive `text.delete` operations are merged based on deletion direction:
|
|
99
|
+
|
|
100
|
+
**Forward deletion (Delete key)** - Same position:
|
|
101
|
+
```
|
|
102
|
+
Before:
|
|
103
|
+
{ otype: "text.delete", position: 10, length: 1 }
|
|
104
|
+
{ otype: "text.delete", position: 10, length: 1 }
|
|
105
|
+
{ otype: "text.delete", position: 10, length: 1 }
|
|
106
|
+
|
|
107
|
+
After:
|
|
108
|
+
{ otype: "text.delete", position: 10, length: 3 }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Backward deletion (Backspace)** - Position moves left:
|
|
112
|
+
```
|
|
113
|
+
Before:
|
|
114
|
+
{ otype: "text.delete", position: 10, length: 1 }
|
|
115
|
+
{ otype: "text.delete", position: 9, length: 1 }
|
|
116
|
+
{ otype: "text.delete", position: 8, length: 1 }
|
|
117
|
+
|
|
118
|
+
After:
|
|
119
|
+
{ otype: "text.delete", position: 8, length: 3 }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Constraints**:
|
|
123
|
+
- Same session ID, key, and path
|
|
124
|
+
- Either: same position (forward) OR position decreases by previous length (backward)
|
|
125
|
+
|
|
126
|
+
### Rule 2: Set Operation Coalescing (with Time Threshold)
|
|
127
|
+
|
|
128
|
+
Multiple `set` operations on the same `(key, path)` are merged, keeping only the latest value.
|
|
129
|
+
|
|
130
|
+
**Time Threshold**: If the gap between two operations exceeds `SET_COALESCE_THRESHOLD_MS` (default: 5000ms), they are NOT coalesced. This preserves meaningful state changes while coalescing rapid updates (like dragging a slider).
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
Before (within threshold):
|
|
134
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.5, ts: 1000 }
|
|
135
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.6, ts: 1200 }
|
|
136
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.7, ts: 1400 }
|
|
137
|
+
|
|
138
|
+
After:
|
|
139
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.7, ts: 1400 }
|
|
140
|
+
|
|
141
|
+
Before (exceeds threshold):
|
|
142
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.5, ts: 1000 }
|
|
143
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.9, ts: 7000 } // Gap > 5s
|
|
144
|
+
|
|
145
|
+
After (no coalescing):
|
|
146
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.5, ts: 1000 }
|
|
147
|
+
{ otype: "set", key: "cube", path: "opacity", value: 0.9, ts: 7000 }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Rule 3: Vector Add Coalescing
|
|
151
|
+
|
|
152
|
+
Multiple `vector3.add` operations on the same `(key, path)` are summed.
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Before:
|
|
156
|
+
{ otype: "vector3.add", key: "cube", path: "position", value: [1, 0, 0] }
|
|
157
|
+
{ otype: "vector3.add", key: "cube", path: "position", value: [0, 1, 0] }
|
|
158
|
+
|
|
159
|
+
After:
|
|
160
|
+
{ otype: "vector3.add", key: "cube", path: "position", value: [1, 1, 0] }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Constraints**: Same time threshold as `set` operations.
|
|
164
|
+
|
|
165
|
+
## Configuration
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
interface CoalescingConfig {
|
|
169
|
+
// Time threshold for set/vector operations (ms)
|
|
170
|
+
setThresholdMs: number; // default: 5000
|
|
171
|
+
|
|
172
|
+
// Enable/disable specific rules
|
|
173
|
+
enableTextCoalesce: boolean; // default: true
|
|
174
|
+
enableSetCoalesce: boolean; // default: true
|
|
175
|
+
enableVectorCoalesce: boolean; // default: true
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## API
|
|
180
|
+
|
|
181
|
+
### Client-Side
|
|
182
|
+
|
|
183
|
+
Already exists in `createGraph`:
|
|
184
|
+
```typescript
|
|
185
|
+
store.setCoalescingEnabled(true);
|
|
186
|
+
store.setCoalescingDelay(300); // Batch window in ms
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Enhancement needed**: Actually merge operations in the edit buffer, not just batch them.
|
|
190
|
+
|
|
191
|
+
### Server-Side
|
|
192
|
+
|
|
193
|
+
```http
|
|
194
|
+
POST /api/documents/:id/coalesce
|
|
195
|
+
Content-Type: application/json
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
"setThresholdMs": 5000,
|
|
199
|
+
"dryRun": false
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Response:
|
|
203
|
+
{
|
|
204
|
+
"ok": true,
|
|
205
|
+
"before": { "journalBatches": 100, "operations": 5000 },
|
|
206
|
+
"after": { "journalBatches": 50, "operations": 1200 },
|
|
207
|
+
"reduction": { "journalBatches": 50, "operations": 3800 }
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Implementation Plan
|
|
212
|
+
|
|
213
|
+
1. **Client-side op merging** (`vuer-rtc/src/client/actions.ts`)
|
|
214
|
+
- Add `mergeOperations()` function that merges compatible ops in edit buffer
|
|
215
|
+
- Call before `commitEdits()` when coalescing is enabled
|
|
216
|
+
|
|
217
|
+
2. **Server-side coalescing** (`vuer-rtc-server/src/journal/CoalescingService.ts`)
|
|
218
|
+
- New service that processes journal batches
|
|
219
|
+
- Merges operations within each batch
|
|
220
|
+
- Re-computes snapshot after coalescing
|
|
221
|
+
- Updates database atomically
|
|
222
|
+
|
|
223
|
+
3. **Text CRDT depth fix** (`vuer-rtc/src/crdt/Rope.ts`)
|
|
224
|
+
- Multi-char inserts create fewer B-tree nodes
|
|
225
|
+
- Each insert operation adds one item, not N items for N characters
|
|
226
|
+
|
|
227
|
+
## Metrics & Debugging
|
|
228
|
+
|
|
229
|
+
Add logging for coalescing effectiveness:
|
|
230
|
+
```
|
|
231
|
+
[coalesce] document=demo-room ops_before=5000 ops_after=1200 reduction=76%
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Known Issues
|
|
235
|
+
|
|
236
|
+
### B-Tree Depth Explosion
|
|
237
|
+
|
|
238
|
+
The text CRDT B-tree (`_textRope.content._tree`) grows very deep with many single-char inserts. Each character becomes a separate node in the tree. With thousands of characters, the tree exceeds msgpack's depth limit.
|
|
239
|
+
|
|
240
|
+
**Root cause**: Each `text.insert` with single char creates a separate item in the B-tree.
|
|
241
|
+
|
|
242
|
+
**Solution**:
|
|
243
|
+
1. Short-term: Increase msgpack `maxDepth` (done: 100 → 500, need more)
|
|
244
|
+
2. Long-term: Coalesce inserts client-side to create multi-char operations
|
|
245
|
+
3. Alternative: Refactor B-tree serialization to use flat structure
|
|
246
|
+
|
|
247
|
+
## Testing
|
|
248
|
+
|
|
249
|
+
1. Type rapidly in text CRDT demo
|
|
250
|
+
2. Verify message log shows fewer, larger operations
|
|
251
|
+
3. Verify journal in DB Viewer shows coalesced operations
|
|
252
|
+
4. Test undo/redo still works after coalescing
|
|
253
|
+
5. Test multi-client conflict resolution after coalescing
|
|
254
|
+
|
|
255
|
+
## References
|
|
256
|
+
|
|
257
|
+
### Causality and Operation Ordering
|
|
258
|
+
|
|
259
|
+
- [Peritext: A CRDT for Collaborative Rich Text Editing](https://dspace.mit.edu/bitstream/handle/1721.1/147641/3555644.pdf?sequence=1&isAllowed=y) - MIT research on collaborative text editing with CRDTs
|
|
260
|
+
- [Real-time collaborative editing using CRDTs](http://www.diva-portal.org/smash/get/diva2:1304659/FULLTEXT01.pdf) - Comprehensive study on CRDT-based collaborative editing
|
|
261
|
+
- [Geometry-Aware CRDTs for Efficient Collaborative Geospatial Editing](https://www.mdpi.com/2220-9964/14/12/468) - Research on preserving both temporal causality and spatial dependencies, includes discussion of operation batching and causality preservation
|
|
262
|
+
|
|
263
|
+
### Key Concepts
|
|
264
|
+
|
|
265
|
+
- **Lamport's "happened-before" relation**: Operations must maintain causality - if op B was created after seeing op A, that ordering must be preserved
|
|
266
|
+
- **Interleaving**: When operations from different sessions are interspersed, coalescing must not reorder them
|
|
267
|
+
- **Vector clocks**: Track causality across distributed sessions
|