@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 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 two main components for building collaborative text editors:
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. **`lexical-loro` Python package** - A WebSocket server using [loro-py](https://github.com/loro-dev/loro-py) for maintaining document state server-side
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. Python Server Setup
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
- ### 3. Programmatic Server Usage
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
- ### LoroCollaborativePlugin Props
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**: To avoid race conditions and initial state corruption, always wait for collaboration initialization to complete before enabling other Lexical plugins or performing document operations.
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
- If initialization fails, check:
360
-
361
- 1. **WebSocket Connection**: Ensure server is running and accessible
362
- 2. **Network Issues**: Check browser network tab for connection errors
363
- 3. **CORS Settings**: Verify server allows cross-origin WebSocket connections
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
- ## How It Works
365
+ ## Architecture
471
366
 
472
- ### Architecture Overview
367
+ For detailed architecture documentation, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
473
368
 
474
- The collaboration system consists of two main components:
369
+ ### System Overview
475
370
 
476
- 1. **LoroCollaborativePlugin** (Client-side)
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
- 2. **LoroWebSocketServer** (Server-side)
483
- - Python WebSocket server using loro-py
484
- - Maintains authoritative document state
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
- ### Plugin Configuration
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" // Server URL
558
- docId="my-document" // Document identifier
559
- username="user123" // User identifier
560
- userColor="#ff0000" // Cursor color (optional)
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
- ### Server Configuration
566
-
567
- ```python
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
- ### Core Components Development
408
+ For comprehensive development guidelines, see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
591
409
 
592
- **Plugin Development:**
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 Python package in development mode
413
+ # Install dependencies
414
+ npm install
604
415
  pip install -e ".[dev]"
605
416
 
606
417
  # Run tests
607
- pytest lexical_loro/tests/ -v
418
+ npm test
419
+ npm run test:py
608
420
 
609
- # Start server in development mode
610
- python3 -m lexical_loro.cli --port 8081 --log-level DEBUG
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 to both the Lexical plugin and Python server:
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
- ### Development Guidelines
438
+ ## Documentation
653
439
 
654
- - **Plugin**: Keep the plugin self-contained and dependency-light
655
- - **Server**: Maintain compatibility with loro-py and WebSocket standards
656
- - **Examples**: Use examples to demonstrate new features
657
- - **Tests**: Ensure both JavaScript and Python tests pass
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
- const ephemeralData = awarenessRef.current.encode();
902
- const hexData = Array.from(ephemeralData).map(b => b.toString(16).padStart(2, '0')).join('');
903
- wsRef.current.send(JSON.stringify({
904
- type: 'ephemeral-update',
905
- docId: docId,
906
- data: hexData // Convert to hex string
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 text into the editor after import
1722
+ // Immediately reflect the current Loro content into the editor after import
1698
1723
  try {
1699
- const currentText = loroDocRef.current.getText(docId).toString();
1700
- updateLexicalFromLoro(editor, currentText);
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.2",
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 servers/server.ts",
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 || ^19.1.0",
55
- "react-dom": "^18 || ^19.1.0",
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": "^19.1.8",
65
- "@types/react-dom": "^19.1.6",
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",