@datalayer/lexical-loro 0.0.2 โ 0.0.3
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/README.md +140 -353
- package/lib/LoroCollaborativePlugin.d.ts +2 -1
- package/lib/LoroCollaborativePlugin.js +77 -11
- package/package.json +9 -10
package/README.md
CHANGED
|
@@ -8,10 +8,11 @@ A collaborative editing plugin for [Lexical](https://github.com/facebook/lexical
|
|
|
8
8
|
|
|
9
9
|
## Core Components
|
|
10
10
|
|
|
11
|
-
This package provides
|
|
11
|
+
This package provides three main components for building collaborative text editors:
|
|
12
12
|
|
|
13
13
|
1. **`LoroCollaborativePlugin.tsx`** - A Lexical plugin that integrates Loro CRDT for real-time collaborative editing
|
|
14
|
-
2. **`
|
|
14
|
+
2. **`LexicalModel` Python Library** - A standalone document model for Lexical content with CRDT capabilities
|
|
15
|
+
3. **`lexical-loro` WebSocket Server** - A Python server using [loro-py](https://github.com/loro-dev/loro-py) for real-time collaboration
|
|
15
16
|
|
|
16
17
|
## Quick Start
|
|
17
18
|
|
|
@@ -34,6 +35,34 @@ function MyEditor() {
|
|
|
34
35
|
}
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
### Using the LexicalModel Library
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from lexical_loro import LexicalModel
|
|
42
|
+
|
|
43
|
+
# Create a new document
|
|
44
|
+
model = LexicalModel.create_document("my-document")
|
|
45
|
+
|
|
46
|
+
# Add content
|
|
47
|
+
model.add_block({
|
|
48
|
+
"text": "My Document",
|
|
49
|
+
"format": 0,
|
|
50
|
+
"style": ""
|
|
51
|
+
}, "heading1")
|
|
52
|
+
|
|
53
|
+
model.add_block({
|
|
54
|
+
"text": "This is a paragraph.",
|
|
55
|
+
"format": 0,
|
|
56
|
+
"style": ""
|
|
57
|
+
}, "paragraph")
|
|
58
|
+
|
|
59
|
+
# Save to file
|
|
60
|
+
model.save_to_file("document.json")
|
|
61
|
+
|
|
62
|
+
# Load from file
|
|
63
|
+
loaded_model = LexicalModel.load_from_file("document.json")
|
|
64
|
+
```
|
|
65
|
+
|
|
37
66
|
### Using the Python Server
|
|
38
67
|
|
|
39
68
|
```bash
|
|
@@ -63,9 +92,11 @@ For complete working examples, see the `src/examples/` directory which contains:
|
|
|
63
92
|
- ๐ **Real-time Collaboration**: Multiple users can edit the same document simultaneously
|
|
64
93
|
- ๐ **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
|
|
65
94
|
- ๐ **Lexical Integration**: Seamless integration with Lexical rich text editor
|
|
95
|
+
- ๐ **Standalone Library**: Use LexicalModel independently for document management
|
|
66
96
|
- ๐ **WebSocket Server**: Python server for maintaining document state
|
|
67
97
|
- ๐ก **Connection Management**: Robust WebSocket connection handling
|
|
68
98
|
- โจ **Rich Text Support**: Preserves formatting during collaborative editing
|
|
99
|
+
- ๐พ **Serialization**: JSON export/import and file persistence
|
|
69
100
|
- ๐ง **Extensible**: Plugin-based architecture for easy customization
|
|
70
101
|
|
|
71
102
|
## Technology Stack
|
|
@@ -148,7 +179,56 @@ function CollaborativeEditor() {
|
|
|
148
179
|
}
|
|
149
180
|
```
|
|
150
181
|
|
|
151
|
-
### 2.
|
|
182
|
+
### 2. Standalone LexicalModel Library
|
|
183
|
+
|
|
184
|
+
Use the LexicalModel library independently for document management:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from lexical_loro import LexicalModel
|
|
188
|
+
|
|
189
|
+
# Create a new document
|
|
190
|
+
model = LexicalModel.create_document("my-document")
|
|
191
|
+
|
|
192
|
+
# Add different types of content
|
|
193
|
+
model.add_block({
|
|
194
|
+
"text": "My Document",
|
|
195
|
+
"format": 0,
|
|
196
|
+
"style": ""
|
|
197
|
+
}, "heading1")
|
|
198
|
+
|
|
199
|
+
model.add_block({
|
|
200
|
+
"text": "This is a paragraph with **bold** text.",
|
|
201
|
+
"format": 0,
|
|
202
|
+
"style": ""
|
|
203
|
+
}, "paragraph")
|
|
204
|
+
|
|
205
|
+
model.add_block({
|
|
206
|
+
"text": "",
|
|
207
|
+
"format": 0,
|
|
208
|
+
"style": ""
|
|
209
|
+
}, "list")
|
|
210
|
+
|
|
211
|
+
# Serialize to JSON
|
|
212
|
+
json_data = model.to_json()
|
|
213
|
+
|
|
214
|
+
# Save to file
|
|
215
|
+
model.save_to_file("document.json")
|
|
216
|
+
|
|
217
|
+
# Load from file
|
|
218
|
+
loaded_model = LexicalModel.load_from_file("document.json")
|
|
219
|
+
|
|
220
|
+
# Access blocks
|
|
221
|
+
for block in loaded_model.get_blocks():
|
|
222
|
+
print(f"{block['type']}: {block.get('text', '')}")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
For more examples, see:
|
|
226
|
+
- `examples/memory_only_example.py` - Basic document creation and manipulation
|
|
227
|
+
- `examples/file_sync_example.py` - File persistence and batch operations
|
|
228
|
+
- `examples/collaboration_example.py` - Simulating collaborative editing
|
|
229
|
+
- `docs/LEXICAL_MODEL_GUIDE.md` - Comprehensive documentation
|
|
230
|
+
|
|
231
|
+
### 3. Python Server Setup
|
|
152
232
|
|
|
153
233
|
Start the WebSocket server:
|
|
154
234
|
|
|
@@ -163,7 +243,7 @@ lexical-loro-server --port 8082
|
|
|
163
243
|
lexical-loro-server --port 8081 --log-level DEBUG
|
|
164
244
|
```
|
|
165
245
|
|
|
166
|
-
###
|
|
246
|
+
### 4. Programmatic Server Usage
|
|
167
247
|
|
|
168
248
|
```python
|
|
169
249
|
import asyncio
|
|
@@ -180,7 +260,9 @@ if __name__ == "__main__":
|
|
|
180
260
|
|
|
181
261
|
## Plugin API
|
|
182
262
|
|
|
183
|
-
|
|
263
|
+
For detailed API documentation, see [`docs/API.md`](docs/API.md).
|
|
264
|
+
|
|
265
|
+
### Quick Reference
|
|
184
266
|
|
|
185
267
|
```tsx
|
|
186
268
|
interface LoroCollaborativePluginProps {
|
|
@@ -192,213 +274,15 @@ interface LoroCollaborativePluginProps {
|
|
|
192
274
|
}
|
|
193
275
|
```
|
|
194
276
|
|
|
195
|
-
### Plugin Features
|
|
196
|
-
|
|
197
|
-
- **Real-time Sync**: Automatically syncs all text changes via Loro CRDT
|
|
198
|
-
- **Cursor Tracking**: Shows other users' cursor positions (experimental)
|
|
199
|
-
- **Connection Management**: Handles reconnection and error states
|
|
200
|
-
- **Rich Text Preservation**: Maintains formatting during collaborative edits
|
|
201
|
-
- **Conflict Resolution**: Automatic conflict-free merging via CRDT
|
|
202
|
-
|
|
203
|
-
|
|
204
277
|
## Initialization Best Practices
|
|
205
278
|
|
|
206
|
-
โ ๏ธ **Important**:
|
|
207
|
-
|
|
208
|
-
### Why Initialization Matters
|
|
209
|
-
|
|
210
|
-
Collaborative editing involves complex synchronization between:
|
|
211
|
-
- Local Lexical editor state
|
|
212
|
-
- Remote CRDT document state
|
|
213
|
-
- WebSocket connection establishment
|
|
214
|
-
- Initial document snapshot loading
|
|
215
|
-
|
|
216
|
-
Enabling other plugins or performing operations before this synchronization completes can cause:
|
|
217
|
-
- Document state corruption
|
|
218
|
-
- Lost edits
|
|
219
|
-
- Inconsistent collaborative state
|
|
220
|
-
- Race conditions between local and remote changes
|
|
221
|
-
|
|
222
|
-
### Proper Plugin Ordering
|
|
223
|
-
|
|
224
|
-
```tsx
|
|
225
|
-
function MyEditor() {
|
|
226
|
-
const [isCollabInitialized, setIsCollabInitialized] = useState(false);
|
|
227
|
-
|
|
228
|
-
return (
|
|
229
|
-
<LexicalComposer initialConfig={editorConfig}>
|
|
230
|
-
<div>
|
|
231
|
-
{/* ALWAYS load collaborative plugin first */}
|
|
232
|
-
<LoroCollaborativePlugin
|
|
233
|
-
websocketUrl="ws://localhost:8081"
|
|
234
|
-
docId="my-document"
|
|
235
|
-
onInitialization={(success) => {
|
|
236
|
-
setIsCollabInitialized(success);
|
|
237
|
-
console.log('Collaboration ready:', success);
|
|
238
|
-
}}
|
|
239
|
-
/>
|
|
240
|
-
|
|
241
|
-
{/* WAIT for collaboration before enabling other plugins */}
|
|
242
|
-
{isCollabInitialized && (
|
|
243
|
-
<>
|
|
244
|
-
<HistoryPlugin />
|
|
245
|
-
<AutoLinkPlugin />
|
|
246
|
-
<ListPlugin />
|
|
247
|
-
<CheckListPlugin />
|
|
248
|
-
{/* Other plugins... */}
|
|
249
|
-
</>
|
|
250
|
-
)}
|
|
251
|
-
|
|
252
|
-
<RichTextPlugin
|
|
253
|
-
contentEditable={<ContentEditable />}
|
|
254
|
-
placeholder={<div>Loading collaborative editor...</div>}
|
|
255
|
-
ErrorBoundary={LexicalErrorBoundary}
|
|
256
|
-
/>
|
|
257
|
-
</div>
|
|
258
|
-
</LexicalComposer>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
### Initialization Callback
|
|
264
|
-
|
|
265
|
-
The `onInitialization` callback provides essential feedback:
|
|
266
|
-
|
|
267
|
-
```tsx
|
|
268
|
-
<LoroCollaborativePlugin
|
|
269
|
-
websocketUrl="ws://localhost:8081"
|
|
270
|
-
docId="document-123"
|
|
271
|
-
onInitialization={(success: boolean) => {
|
|
272
|
-
if (success) {
|
|
273
|
-
// โ
Safe to enable other plugins and features
|
|
274
|
-
console.log('Collaboration initialized successfully');
|
|
275
|
-
enableOtherFeatures();
|
|
276
|
-
} else {
|
|
277
|
-
// โ Handle initialization failure
|
|
278
|
-
console.error('Collaboration failed to initialize');
|
|
279
|
-
showErrorMessage('Failed to connect to collaborative server');
|
|
280
|
-
}
|
|
281
|
-
}}
|
|
282
|
-
/>
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
### Visual Status Indicators
|
|
286
|
-
|
|
287
|
-
Provide users with clear feedback about initialization status:
|
|
288
|
-
|
|
289
|
-
```tsx
|
|
290
|
-
function CollaborativeEditor() {
|
|
291
|
-
const [isInitialized, setIsInitialized] = useState(false);
|
|
292
|
-
|
|
293
|
-
return (
|
|
294
|
-
<div>
|
|
295
|
-
<div className="status-bar">
|
|
296
|
-
Collaboration: {isInitialized ? 'โ
Ready' : 'โณ Connecting...'}
|
|
297
|
-
</div>
|
|
298
|
-
|
|
299
|
-
<LexicalComposer initialConfig={editorConfig}>
|
|
300
|
-
<LoroCollaborativePlugin
|
|
301
|
-
websocketUrl="ws://localhost:8081"
|
|
302
|
-
docId="document-123"
|
|
303
|
-
onInitialization={setIsInitialized}
|
|
304
|
-
/>
|
|
305
|
-
|
|
306
|
-
{/* Editor becomes fully functional only after initialization */}
|
|
307
|
-
<RichTextPlugin
|
|
308
|
-
contentEditable={
|
|
309
|
-
<ContentEditable
|
|
310
|
-
style={{
|
|
311
|
-
opacity: isInitialized ? 1 : 0.5,
|
|
312
|
-
pointerEvents: isInitialized ? 'auto' : 'none'
|
|
313
|
-
}}
|
|
314
|
-
/>
|
|
315
|
-
}
|
|
316
|
-
placeholder={
|
|
317
|
-
<div>
|
|
318
|
-
{isInitialized
|
|
319
|
-
? 'Start typing...'
|
|
320
|
-
: 'Connecting to collaboration server...'
|
|
321
|
-
}
|
|
322
|
-
</div>
|
|
323
|
-
}
|
|
324
|
-
ErrorBoundary={LexicalErrorBoundary}
|
|
325
|
-
/>
|
|
326
|
-
</LexicalComposer>
|
|
327
|
-
</div>
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### Common Anti-Patterns to Avoid
|
|
333
|
-
|
|
334
|
-
โ **Don't** enable plugins immediately:
|
|
335
|
-
```tsx
|
|
336
|
-
// WRONG: Race condition risk
|
|
337
|
-
<LoroCollaborativePlugin websocketUrl="..." />
|
|
338
|
-
<HistoryPlugin /> {/* May interfere with initial sync */}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
โ **Don't** perform immediate document operations:
|
|
342
|
-
```tsx
|
|
343
|
-
// WRONG: May overwrite remote content
|
|
344
|
-
useEffect(() => {
|
|
345
|
-
editor.update(() => {
|
|
346
|
-
$getRoot().clear(); // Dangerous before sync!
|
|
347
|
-
});
|
|
348
|
-
}, []);
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
โ **Don't** ignore initialization status:
|
|
352
|
-
```tsx
|
|
353
|
-
// WRONG: No feedback on connection issues
|
|
354
|
-
<LoroCollaborativePlugin websocketUrl="..." />
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
### Debugging Initialization Issues
|
|
279
|
+
โ ๏ธ **Important**: Always wait for collaboration initialization before enabling other plugins.
|
|
358
280
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
4. **Document ID**: Ensure unique document IDs for different documents
|
|
365
|
-
5. **Server Logs**: Enable debug logging on server side
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
# Enable debug logging
|
|
369
|
-
export LEXICAL_LORO_LOG_LEVEL=DEBUG
|
|
370
|
-
lexical-loro-server
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
## Server API
|
|
374
|
-
|
|
375
|
-
### LoroWebSocketServer Class
|
|
376
|
-
|
|
377
|
-
```python
|
|
378
|
-
from lexical_loro import LoroWebSocketServer
|
|
379
|
-
|
|
380
|
-
# Create server instance
|
|
381
|
-
server = LoroWebSocketServer(
|
|
382
|
-
port=8081, # Server port
|
|
383
|
-
host="localhost" # Server host
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
# Start server
|
|
387
|
-
await server.start()
|
|
388
|
-
|
|
389
|
-
# Shutdown server
|
|
390
|
-
await server.shutdown()
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
### Supported Message Types
|
|
394
|
-
|
|
395
|
-
The server handles these WebSocket message types:
|
|
396
|
-
|
|
397
|
-
- `loro-update`: Apply CRDT document updates
|
|
398
|
-
- `snapshot`: Full document state snapshots
|
|
399
|
-
- `request-snapshot`: Request current document state
|
|
400
|
-
- `ephemeral-update`: Cursor and selection updates
|
|
401
|
-
- `awareness-update`: User presence information
|
|
281
|
+
See [`docs/INITIALIZATION_GUIDE.md`](docs/INITIALIZATION_GUIDE.md) for comprehensive guidance on:
|
|
282
|
+
- Proper plugin ordering
|
|
283
|
+
- Initialization callbacks
|
|
284
|
+
- Error handling
|
|
285
|
+
- Common anti-patterns to avoid
|
|
402
286
|
|
|
403
287
|
## Examples
|
|
404
288
|
|
|
@@ -434,8 +318,19 @@ lexical_loro/ # Python WebSocket server package
|
|
|
434
318
|
โโโ __init__.py # Package exports
|
|
435
319
|
โโโ server.py # WebSocket server implementation
|
|
436
320
|
โโโ cli.py # Command line interface
|
|
321
|
+
โโโ model/
|
|
322
|
+
โ โโโ lexical_model.py # Standalone LexicalModel library
|
|
437
323
|
โโโ tests/ # Python test suite
|
|
438
324
|
|
|
325
|
+
docs/
|
|
326
|
+
โโโ LEXICAL_MODEL_GUIDE.md # Comprehensive library documentation
|
|
327
|
+
|
|
328
|
+
examples/
|
|
329
|
+
โโโ memory_only_example.py # Basic LexicalModel usage
|
|
330
|
+
โโโ file_sync_example.py # File persistence example
|
|
331
|
+
โโโ collaboration_example.py # Collaborative editing simulation
|
|
332
|
+
โโโ README.md # Examples documentation
|
|
333
|
+
|
|
439
334
|
pyproject.toml # Python package configuration
|
|
440
335
|
```
|
|
441
336
|
|
|
@@ -467,23 +362,17 @@ src/archive/ # Historical plugin implementations
|
|
|
467
362
|
โโโ LoroCollaborativePlugin5.tsx
|
|
468
363
|
```
|
|
469
364
|
|
|
470
|
-
##
|
|
365
|
+
## Architecture
|
|
471
366
|
|
|
472
|
-
|
|
367
|
+
For detailed architecture documentation, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
|
|
473
368
|
|
|
474
|
-
|
|
369
|
+
### System Overview
|
|
475
370
|
|
|
476
|
-
|
|
477
|
-
- Integrates with Lexical editor as a React plugin
|
|
478
|
-
- Captures text changes and applies them to Loro CRDT document
|
|
479
|
-
- Sends/receives updates via WebSocket connection
|
|
480
|
-
- Handles cursor positioning and user awareness
|
|
371
|
+
The collaboration system consists of three main components:
|
|
481
372
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
- Broadcasts updates to all connected clients
|
|
486
|
-
- Handles client connections and disconnections
|
|
373
|
+
1. **LoroCollaborativePlugin** (Client-side) - Lexical integration
|
|
374
|
+
2. **LoroWebSocketServer** (Server-side) - Real-time synchronization
|
|
375
|
+
3. **LexicalModel** (Standalone Library) - Independent document model
|
|
487
376
|
|
|
488
377
|
### Data Flow
|
|
489
378
|
|
|
@@ -493,168 +382,66 @@ User Types โ Lexical Editor โ Plugin โ Loro CRDT โ WebSocket
|
|
|
493
382
|
WebSocket โ Loro CRDT โ Plugin โ Lexical Editor โ Other Users
|
|
494
383
|
```
|
|
495
384
|
|
|
496
|
-
### CRDT Integration Process
|
|
497
|
-
|
|
498
|
-
1. **Document Creation**: Plugin creates a Loro document with unique identifier
|
|
499
|
-
2. **Local Changes**: User edits trigger Lexical change events
|
|
500
|
-
3. **CRDT Application**: Changes are applied to local Loro document
|
|
501
|
-
4. **Synchronization**: Updates are serialized and sent via WebSocket
|
|
502
|
-
5. **Remote Application**: Other clients receive and apply updates
|
|
503
|
-
6. **Conflict Resolution**: Loro CRDT automatically merges changes without conflicts
|
|
504
|
-
|
|
505
|
-
### Connection Management
|
|
506
|
-
|
|
507
|
-
- **Auto-reconnection**: Plugin handles connection drops gracefully
|
|
508
|
-
- **State Synchronization**: New clients receive full document snapshot
|
|
509
|
-
- **Error Handling**: Connection errors are logged and displayed
|
|
510
|
-
- **User Awareness**: Track online users and cursor positions
|
|
511
|
-
|
|
512
|
-
### Lexical Integration
|
|
513
|
-
|
|
514
|
-
The Lexical editor integration includes:
|
|
515
|
-
|
|
516
|
-
1. **LoroCollaborativePlugin**: A custom Lexical plugin that bridges Lexical and Loro CRDT
|
|
517
|
-
2. **Bidirectional Sync**: Changes flow from Lexical โ Loro โ WebSocket and vice versa
|
|
518
|
-
3. **Rich Text Preservation**: The plugin maintains rich text formatting during collaborative editing
|
|
519
|
-
4. **Independent State**: Lexical editor maintains separate document state from simple text editor
|
|
520
|
-
|
|
521
|
-
### WebSocket Communication
|
|
522
|
-
|
|
523
|
-
The WebSocket server:
|
|
524
|
-
- Maintains connections to all clients
|
|
525
|
-
- Broadcasts Loro document updates to all connected clients with document ID filtering
|
|
526
|
-
- Handles client connections and disconnections
|
|
527
|
-
- Provides connection status feedback
|
|
528
|
-
- Stores separate snapshots for each document type
|
|
529
|
-
|
|
530
|
-
### Real-time Updates
|
|
531
|
-
|
|
532
|
-
1. User types in the text area
|
|
533
|
-
2. Change is applied to local Loro document
|
|
534
|
-
3. Document update is serialized and sent via WebSocket
|
|
535
|
-
4. Other clients receive the update and apply it to their documents
|
|
536
|
-
5. UI is updated to reflect the changes
|
|
537
|
-
|
|
538
|
-
### Initial Content Synchronization
|
|
539
|
-
|
|
540
|
-
When a new collaborator joins:
|
|
541
|
-
|
|
542
|
-
1. **Connection**: New client connects to WebSocket server
|
|
543
|
-
2. **Welcome**: Server sends welcome message to new client
|
|
544
|
-
3. **Snapshot Request**: New client requests current document state
|
|
545
|
-
4. **Snapshot Delivery**: Server sends stored snapshot or requests one from existing clients
|
|
546
|
-
5. **Content Sync**: New client applies snapshot and sees current document content
|
|
547
|
-
6. **Ready to Collaborate**: New client can now participate in real-time editing
|
|
548
|
-
|
|
549
|
-
The server maintains the latest document snapshot to ensure new collaborators always see existing content.
|
|
550
|
-
|
|
551
385
|
## Configuration
|
|
552
386
|
|
|
553
|
-
|
|
387
|
+
For detailed configuration options, see [`docs/API.md`](docs/API.md).
|
|
388
|
+
|
|
389
|
+
### Quick Configuration
|
|
554
390
|
|
|
555
391
|
```tsx
|
|
392
|
+
// Plugin configuration
|
|
556
393
|
<LoroCollaborativePlugin
|
|
557
|
-
websocketUrl="ws://localhost:8081"
|
|
558
|
-
docId="my-document"
|
|
559
|
-
username="user123"
|
|
560
|
-
|
|
561
|
-
debug={true} // Enable debug logs (optional)
|
|
394
|
+
websocketUrl="ws://localhost:8081"
|
|
395
|
+
docId="my-document"
|
|
396
|
+
username="user123"
|
|
397
|
+
debug={true}
|
|
562
398
|
/>
|
|
563
399
|
```
|
|
564
400
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
# Via command line
|
|
569
|
-
lexical-loro-server --port 8081 --host localhost --log-level DEBUG
|
|
570
|
-
|
|
571
|
-
# Via environment variables
|
|
572
|
-
export LEXICAL_LORO_PORT=8081
|
|
573
|
-
export LEXICAL_LORO_HOST=localhost
|
|
574
|
-
export LEXICAL_LORO_LOG_LEVEL=DEBUG
|
|
575
|
-
lexical-loro-server
|
|
576
|
-
|
|
577
|
-
# Programmatically
|
|
578
|
-
server = LoroWebSocketServer(port=8081, host="localhost")
|
|
401
|
+
```bash
|
|
402
|
+
# Server configuration
|
|
403
|
+
lexical-loro-server --port 8081 --log-level DEBUG
|
|
579
404
|
```
|
|
580
405
|
|
|
581
|
-
### Supported Document Types
|
|
582
|
-
|
|
583
|
-
The server supports multiple document types with different IDs:
|
|
584
|
-
- `shared-text`: Basic text collaboration
|
|
585
|
-
- `lexical-shared-doc`: Rich text with Lexical
|
|
586
|
-
- Custom document IDs for multiple simultaneous documents
|
|
587
|
-
|
|
588
406
|
## Development
|
|
589
407
|
|
|
590
|
-
|
|
408
|
+
For comprehensive development guidelines, see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
|
|
591
409
|
|
|
592
|
-
|
|
593
|
-
```bash
|
|
594
|
-
# The plugin is a single TypeScript file
|
|
595
|
-
src/LoroCollaborativePlugin.tsx
|
|
410
|
+
### Quick Start
|
|
596
411
|
|
|
597
|
-
# Dependencies for plugin development
|
|
598
|
-
npm install lexical @lexical/react @lexical/selection loro-crdt
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
**Server Development:**
|
|
602
412
|
```bash
|
|
603
|
-
# Install
|
|
413
|
+
# Install dependencies
|
|
414
|
+
npm install
|
|
604
415
|
pip install -e ".[dev]"
|
|
605
416
|
|
|
606
417
|
# Run tests
|
|
607
|
-
|
|
418
|
+
npm test
|
|
419
|
+
npm run test:py
|
|
608
420
|
|
|
609
|
-
# Start
|
|
610
|
-
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### Testing
|
|
614
|
-
|
|
615
|
-
**Plugin Testing:**
|
|
616
|
-
```bash
|
|
617
|
-
npm run test # Run Vitest tests
|
|
618
|
-
npm run test:js # Run tests once
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
**Server Testing:**
|
|
622
|
-
```bash
|
|
623
|
-
npm run test:py # Run Python tests
|
|
624
|
-
npm run test:py:watch # Run in watch mode
|
|
625
|
-
npm run test:py:coverage # Run with coverage
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
### Example Development
|
|
629
|
-
|
|
630
|
-
To work on the examples:
|
|
631
|
-
```bash
|
|
632
|
-
npm install # Install all dependencies
|
|
633
|
-
npm run example # Start example app with both servers
|
|
634
|
-
npm run example:py # Start with Python server only
|
|
635
|
-
npm run example:js # Start with Node.js server only
|
|
636
|
-
npm run example:vite # Start example app only (no servers)
|
|
421
|
+
# Start development server
|
|
422
|
+
lexical-loro-server --log-level DEBUG
|
|
637
423
|
```
|
|
638
424
|
|
|
639
425
|
## Contributing
|
|
640
426
|
|
|
641
|
-
We welcome contributions
|
|
427
|
+
We welcome contributions! Please see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) for detailed guidelines.
|
|
428
|
+
|
|
429
|
+
### Quick Contributing Guide
|
|
642
430
|
|
|
643
431
|
1. Fork the repository
|
|
644
432
|
2. Create a feature branch
|
|
645
|
-
3. Focus changes on core components
|
|
646
|
-
- `src/LoroCollaborativePlugin.tsx` for plugin improvements
|
|
647
|
-
- `lexical_loro/` for server enhancements
|
|
433
|
+
3. Focus changes on core components
|
|
648
434
|
4. Add tests for new functionality
|
|
649
435
|
5. Update documentation as needed
|
|
650
436
|
6. Submit a pull request
|
|
651
437
|
|
|
652
|
-
|
|
438
|
+
## Documentation
|
|
653
439
|
|
|
654
|
-
- **
|
|
655
|
-
- **
|
|
656
|
-
- **
|
|
657
|
-
- **
|
|
440
|
+
- **[API Documentation](docs/API.md)** - Complete API reference
|
|
441
|
+
- **[Initialization Guide](docs/INITIALIZATION_GUIDE.md)** - Best practices for setup
|
|
442
|
+
- **[Architecture](docs/ARCHITECTURE.md)** - System design and data flow
|
|
443
|
+
- **[Development Guide](docs/DEVELOPMENT.md)** - Contributing and development setup
|
|
444
|
+
- **[LexicalModel Guide](docs/LEXICAL_MODEL_GUIDE.md)** - Standalone library documentation
|
|
658
445
|
|
|
659
446
|
## License
|
|
660
447
|
|
|
@@ -10,6 +10,7 @@ interface LoroCollaborativePluginProps {
|
|
|
10
10
|
isCurrentUser?: boolean;
|
|
11
11
|
}>) => void;
|
|
12
12
|
onInitialization?: (success: boolean) => void;
|
|
13
|
+
onSendMessageReady?: (sendMessageFn: (message: any) => void) => void;
|
|
13
14
|
}
|
|
14
|
-
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization, onSendMessageReady }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
15
16
|
export default LoroCollaborativePlugin;
|
|
@@ -627,7 +627,7 @@ class CursorAwareness {
|
|
|
627
627
|
return this.ephemeralStore.getAllStates();
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
|
-
export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization }) {
|
|
630
|
+
export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization, onSendMessageReady }) {
|
|
631
631
|
const [editor] = useLexicalComposerContext();
|
|
632
632
|
const wsRef = useRef(null);
|
|
633
633
|
const loroDocRef = useRef(new LoroDoc());
|
|
@@ -898,13 +898,29 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
898
898
|
console.log('๐ฏ Set awareness with stable cursor data:', { userWithCursorData, clientId });
|
|
899
899
|
// Send ephemeral update to other clients via WebSocket
|
|
900
900
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && awarenessRef.current) {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
901
|
+
try {
|
|
902
|
+
const ephemeralData = awarenessRef.current.encode();
|
|
903
|
+
// Validate ephemeral data before sending
|
|
904
|
+
if (!ephemeralData || ephemeralData.length === 0) {
|
|
905
|
+
console.warn('โ ๏ธ Empty ephemeral data, skipping send');
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const hexData = Array.from(ephemeralData).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
909
|
+
// Validate hex data
|
|
910
|
+
if (!hexData || hexData.length === 0) {
|
|
911
|
+
console.warn('โ ๏ธ Empty hex data, skipping send');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
wsRef.current.send(JSON.stringify({
|
|
915
|
+
type: 'ephemeral-update',
|
|
916
|
+
docId: docId,
|
|
917
|
+
data: hexData // Convert to hex string
|
|
918
|
+
}));
|
|
919
|
+
console.log('๐ค Sent ephemeral update:', { docId, dataLength: hexData.length });
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
console.error('โ Error encoding/sending ephemeral data:', error);
|
|
923
|
+
}
|
|
908
924
|
}
|
|
909
925
|
}
|
|
910
926
|
catch (error) {
|
|
@@ -1595,10 +1611,12 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1595
1611
|
// WebSocket connection management with stable dependencies
|
|
1596
1612
|
const stableOnConnectionChange = useRef(onConnectionChange);
|
|
1597
1613
|
const stableOnDisconnectReady = useRef(onDisconnectReady);
|
|
1614
|
+
const stableOnSendMessageReady = useRef(onSendMessageReady);
|
|
1598
1615
|
// Update refs when props change without triggering effect
|
|
1599
1616
|
useEffect(() => {
|
|
1600
1617
|
stableOnConnectionChange.current = onConnectionChange;
|
|
1601
1618
|
stableOnDisconnectReady.current = onDisconnectReady;
|
|
1619
|
+
stableOnSendMessageReady.current = onSendMessageReady;
|
|
1602
1620
|
});
|
|
1603
1621
|
useEffect(() => {
|
|
1604
1622
|
// Close any existing connection before creating a new one
|
|
@@ -1664,6 +1682,13 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1664
1682
|
}
|
|
1665
1683
|
};
|
|
1666
1684
|
stableOnDisconnectReady.current?.(disconnectFn);
|
|
1685
|
+
// Provide sendMessage function to parent component
|
|
1686
|
+
const sendMessageFn = (message) => {
|
|
1687
|
+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
|
|
1688
|
+
wsRef.current.send(JSON.stringify(message));
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
stableOnSendMessageReady.current?.(sendMessageFn);
|
|
1667
1692
|
};
|
|
1668
1693
|
ws.onmessage = (event) => {
|
|
1669
1694
|
try {
|
|
@@ -1694,10 +1719,27 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1694
1719
|
if (onInitialization) {
|
|
1695
1720
|
onInitialization(true);
|
|
1696
1721
|
}
|
|
1697
|
-
// Immediately reflect the current Loro
|
|
1722
|
+
// Immediately reflect the current Loro content into the editor after import
|
|
1698
1723
|
try {
|
|
1699
|
-
|
|
1700
|
-
|
|
1724
|
+
// For lexical-shared-doc, we need to get the structured JSON from the content container
|
|
1725
|
+
let currentContent = '';
|
|
1726
|
+
try {
|
|
1727
|
+
// Try to get from 'content' container first (structured JSON)
|
|
1728
|
+
currentContent = loroDocRef.current.getText('content').toString();
|
|
1729
|
+
console.log('๐ Got structured content from "content" container:', currentContent.slice(0, 100) + '...');
|
|
1730
|
+
}
|
|
1731
|
+
catch {
|
|
1732
|
+
// Fallback to docId container if content doesn't exist
|
|
1733
|
+
currentContent = loroDocRef.current.getText(docId).toString();
|
|
1734
|
+
console.log('๐ Fallback to docId container:', currentContent.slice(0, 100) + '...');
|
|
1735
|
+
}
|
|
1736
|
+
if (currentContent && currentContent.trim().length > 0) {
|
|
1737
|
+
updateLexicalFromLoro(editor, currentContent);
|
|
1738
|
+
console.log('โ
Successfully updated Lexical editor from snapshot');
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
console.warn('โ ๏ธ Empty content received from snapshot');
|
|
1742
|
+
}
|
|
1701
1743
|
}
|
|
1702
1744
|
catch (e) {
|
|
1703
1745
|
console.warn('โ ๏ธ Could not immediately reflect snapshot to editor:', e);
|
|
@@ -1827,6 +1869,30 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1827
1869
|
console.warn('๐งน Cannot cleanup - missing client ID or awareness ref');
|
|
1828
1870
|
}
|
|
1829
1871
|
}
|
|
1872
|
+
else if (data.type === 'paragraph-added') {
|
|
1873
|
+
// Handle server broadcast when a new paragraph was added
|
|
1874
|
+
console.log('โ Received paragraph-added broadcast:', {
|
|
1875
|
+
docId: data.docId,
|
|
1876
|
+
message: data.message,
|
|
1877
|
+
addedBy: data.addedBy
|
|
1878
|
+
});
|
|
1879
|
+
// Trigger a sync from Loro to Lexical to reflect the new paragraph
|
|
1880
|
+
if (data.docId === docId) {
|
|
1881
|
+
try {
|
|
1882
|
+
// Request fresh snapshot to get the updated content
|
|
1883
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1884
|
+
ws.send(JSON.stringify({
|
|
1885
|
+
type: 'request-snapshot',
|
|
1886
|
+
docId: docId
|
|
1887
|
+
}));
|
|
1888
|
+
console.log('๐ Requested fresh snapshot after paragraph addition');
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
console.warn('Error handling paragraph-added message:', error);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1830
1896
|
}
|
|
1831
1897
|
catch (err) {
|
|
1832
1898
|
console.error('Error processing WebSocket message in Lexical plugin:', err);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/lexical-loro",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Collaborative editing plugin for Lexical based on Loro CRDT",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -35,14 +35,15 @@
|
|
|
35
35
|
"dev:all:py": "npm run example:py",
|
|
36
36
|
"dev:vite": "npm run example:vite",
|
|
37
37
|
"example": "concurrently \"npm run server\" \"npm run server:py\" \"npm run example:vite\"",
|
|
38
|
-
"example:js": "concurrently \"npm run server\" \"npm run example:vite\"",
|
|
39
|
-
"example:py": "concurrently \"npm run server:py\" \"npm run example:vite\"",
|
|
38
|
+
"example:js": "concurrently \"npm run server\" \"npm run example:vite\"",
|
|
39
|
+
"example:py": "concurrently \"npm run server:py:dev\" \"npm run server:py:minimal\" \"npm run example:vite\"",
|
|
40
40
|
"example:vite": "vite",
|
|
41
41
|
"lint": "eslint .",
|
|
42
42
|
"preview": "vite preview",
|
|
43
|
-
"server": "tsx
|
|
43
|
+
"server": "tsx node-js/server.ts",
|
|
44
44
|
"server:py": "lexical-loro-server",
|
|
45
45
|
"server:py:dev": "python3 -m lexical_loro.cli",
|
|
46
|
+
"server:py:minimal": "python3 -m lexical_loro.cli_minimal",
|
|
46
47
|
"test": "vitest",
|
|
47
48
|
"test:js": "vitest run",
|
|
48
49
|
"test:py": "python3 -m pytest lexical_loro/tests/ -v",
|
|
@@ -51,18 +52,16 @@
|
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"loro-crdt": "^1.5.10",
|
|
54
|
-
"react": "^18
|
|
55
|
-
"react-dom": "^18
|
|
55
|
+
"react": "^18.3.1",
|
|
56
|
+
"react-dom": "^18.3.1",
|
|
56
57
|
"lexical": "^0.33.1",
|
|
57
58
|
"@lexical/react": "^0.33.1",
|
|
58
59
|
"@lexical/selection": "^0.33.1"
|
|
59
60
|
},
|
|
60
|
-
"peerDependencies": {
|
|
61
|
-
},
|
|
62
61
|
"devDependencies": {
|
|
63
62
|
"@eslint/js": "^9.30.1",
|
|
64
|
-
"@types/react": "
|
|
65
|
-
"@types/react-dom": "
|
|
63
|
+
"@types/react": "18.3.20",
|
|
64
|
+
"@types/react-dom": "18.3.6",
|
|
66
65
|
"@types/ws": "^8.18.1",
|
|
67
66
|
"@vitejs/plugin-react": "^4.6.0",
|
|
68
67
|
"concurrently": "^9.2.0",
|