@datalayer/lexical-loro 0.0.1 → 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
@@ -2,386 +2,248 @@
2
2
 
3
3
  [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](https://github.com/sponsors/datalayer)
4
4
 
5
- # Collaborative Plugin for Lexical based on Loro CRDT
5
+ # ✍️ 🦜 Lexical Loro - Collaborative Plugin for Lexical with Loro CRDT
6
6
 
7
- A real-time collaborative editing application for [Lexical](https://github.com/facebook/lexical) built with [Loro CRDT](https://github.com/loro-dev), React, TypeScript, Vite with a Python WebSocket server using [loro-py](https://github.com/loro-dev/loro-py) to maintain the Lexical JSON model sever-side
7
+ A collaborative editing plugin for [Lexical](https://github.com/facebook/lexical) Rich Editor built with [Loro](https://github.com/loro-dev) CRDT, providing real-time collaborative editing capabilities with conflict-free synchronization.
8
8
 
9
- Features both simple text editing and rich text editing with Lexical. Multiple users can edit the same documents simultaneously with conflict-free collaborative editing powered by Conflict-free Replicated Data Types (CRDTs).
9
+ ## Core Components
10
10
 
11
- **DISCLAIMER** Collaborative Cursors still need fixes, see [this issue](https://github.com/datalayer/lexical-loro/issues/1).
12
-
13
- **NEW** Now supports both Node.js and Python WebSocket servers!
14
-
15
- <div align="center" style="text-align: center">
16
- <img alt="" src="https://assets.datalayer.tech/lexical-loro.gif" />
17
- </div>
18
-
19
- ## Features
20
-
21
- - 🔄 **Real-time Collaboration**: Multiple users can edit the same document simultaneously
22
- - 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
23
- - 📝 **Dual Editor Support**: Choose between simple text area or rich text Lexical editor
24
- - 🌐 **Multi-server Support**: Choose between Node.js and Python WebSocket servers
25
- - ⚡ **Fast Development**: Built with Vite for lightning-fast development
26
- - 🎨 **Responsive Design**: Works on desktop and mobile devices
27
- - 📡 **Connection Status**: Visual indicators for connection state
28
- - ✨ **Rich Text Features**: Bold, italic, underline with real-time formatting sync
29
- - 🔧 **Server Selection**: Switch between Node.js and Python backends
11
+ This package provides three main components for building collaborative text editors:
30
12
 
31
- ## Technology Stack
32
-
33
- - **Frontend**: React 19 + TypeScript + Vite
34
- - **CRDT Library**: Loro CRDT
35
- - **Rich Text Editor**: Lexical (Facebook's extensible text editor)
36
- - **Backend Options**:
37
- - Node.js + TypeScript + ws library
38
- - Python + loro-py + websockets library
39
- - **Real-time Communication**: WebSockets (ws)
40
- - **Styling**: CSS3 with responsive design
41
- - **Development Tools**: ESLint, tsx, concurrently
42
-
43
- ## Getting Started
44
-
45
- ### Prerequisites
46
-
47
- - Node.js (v16 or higher)
48
- - npm or yarn
49
- - Python 3.8+ (for Python server option)
50
- - pip3 (for Python dependencies)
13
+ 1. **`LoroCollaborativePlugin.tsx`** - A Lexical plugin that integrates Loro CRDT for real-time collaborative editing
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
51
16
 
52
- ### Installation
17
+ ## Quick Start
53
18
 
54
- 1. Install Node.js dependencies:
55
- ```bash
56
- npm install
57
- ```
19
+ ### Using the Lexical Plugin
58
20
 
59
- 2. Install Python dependencies (optional - for Python server):
60
- ```bash
61
- pip3 install -r requirements.txt
62
- # or run the setup script
63
- ./setup-python.sh
64
- ```
65
-
66
- ### Running the Application
67
-
68
- #### Option 1: All Servers (Recommended)
69
- ```bash
70
- npm run dev:all
71
- ```
72
- This starts **both** WebSocket servers (Node.js on port 8080 and Python on port 8081) plus the React development server (port 5173). You can then switch between servers using the UI.
21
+ ```tsx
22
+ import { LoroCollaborativePlugin } from './src/LoroCollaborativePlugin';
73
23
 
74
- #### Option 2: Python Server Only
75
- ```bash
76
- npm run dev:all:py
24
+ function MyEditor() {
25
+ return (
26
+ <LexicalComposer initialConfig={editorConfig}>
27
+ <RichTextPlugin />
28
+ <LoroCollaborativePlugin
29
+ websocketUrl="ws://localhost:8081"
30
+ docId="my-document"
31
+ username="user1"
32
+ />
33
+ </LexicalComposer>
34
+ );
35
+ }
77
36
  ```
78
- This starts only the Python WebSocket server (port 8081) and React development server.
79
37
 
80
- #### Option 3: Node.js Server Only
81
- ```bash
82
- npm run dev:all:js
83
- ```
84
- This starts only the Node.js WebSocket server (port 8080) and React development server.
38
+ ### Using the LexicalModel Library
85
39
 
86
- #### Option 4: Run servers separately
40
+ ```python
41
+ from lexical_loro import LexicalModel
87
42
 
88
- **All servers manually:**
89
- ```bash
90
- # Terminal 1: Start Node.js WebSocket server
91
- npm run server
43
+ # Create a new document
44
+ model = LexicalModel.create_document("my-document")
92
45
 
93
- # Terminal 2: Start Python WebSocket server
94
- npm run server:py
46
+ # Add content
47
+ model.add_block({
48
+ "text": "My Document",
49
+ "format": 0,
50
+ "style": ""
51
+ }, "heading1")
95
52
 
96
- # Terminal 3: Start React development server
97
- npm run dev
98
- ```
53
+ model.add_block({
54
+ "text": "This is a paragraph.",
55
+ "format": 0,
56
+ "style": ""
57
+ }, "paragraph")
99
58
 
100
- **Node.js Server only:**
101
- ```bash
102
- # Terminal 1: Start Node.js WebSocket server
103
- npm run server
59
+ # Save to file
60
+ model.save_to_file("document.json")
104
61
 
105
- # Terminal 2: Start React development server
106
- npm run dev
62
+ # Load from file
63
+ loaded_model = LexicalModel.load_from_file("document.json")
107
64
  ```
108
65
 
109
- **Python Server only:**
110
- ```bash
111
- # Terminal 1: Start Python WebSocket server
112
- npm run server:py
113
- # or directly: python3 server.py
66
+ ### Using the Python Server
114
67
 
115
- # Terminal 2: Start React development server
116
- npm run dev
117
- ```
118
-
119
- 2. In another terminal, start the React development server:
120
- ```bash
121
- npm run dev
122
- ```
123
-
124
- ### Usage
125
-
126
- 1. Open your browser and navigate to the development server URL (typically `http://localhost:5173`)
127
- 2. **Select Server Type**: Use the server selection radio buttons to choose:
128
- - **Node.js Server**: `ws://localhost:8080` (TypeScript implementation)
129
- - **Python Server**: `ws://localhost:8081` (Python + loro-py implementation)
130
-
131
- 💡 **Tip**: When using `npm run dev:all`, both servers are running simultaneously, so you can switch between them in real-time!
132
-
133
- 3. **Choose Editor Type**: Click the tabs to select:
134
- - **Simple Text Editor**: A basic textarea for plain text collaboration
135
- - **Rich Text Editor (Lexical)**: A full-featured rich text editor with Bold/Italic/Underline formatting
136
- 4. Start typing in either editor
137
- 5. Open another browser window/tab or share the URL with others
138
- 6. All users will see real-time updates as they type in the same editor type
139
- 7. Each editor maintains its own document state (they are separate collaborative spaces)
140
-
141
- **Note**: You must disconnect from the current server before switching to a different server type.
142
-
143
- ### Testing Collaboration
144
-
145
- To test the real-time collaboration:
146
-
147
- 1. Open multiple browser tabs/windows to the development server URL
148
- 2. **Select the same server** in all tabs (Node.js or Python)
149
- 3. **Test Simple Text Editor**:
150
- - Keep all tabs on the "Simple Text Editor" tab
151
- - Start typing in one window - you'll see the changes appear in other windows instantly
152
- 4. **Test Lexical Rich Text Editor**:
153
- - Switch all tabs to the "Rich Text Editor (Lexical)" tab
154
- - Try formatting text with the toolbar buttons (Bold, Italic, Underline)
155
- - Changes and formatting will sync in real-time across all tabs
156
- 5. **Test Cross-Server Compatibility**:
157
- - Verify that documents are properly synchronized between Node.js and Python servers
158
- - Each server maintains its own document state
159
- 6. **Test Independent Documents**:
160
- - Have some tabs on "Simple Text Editor" and others on "Lexical Editor"
161
- - Notice that each editor type maintains its own separate document
162
- 5. **New collaborators will automatically receive the current document content** when they join
163
-
164
- **Note**: The application now properly synchronizes initial content:
165
- - When a new collaborator joins, they automatically receive the current document state for both editors
166
- - If no snapshot is available on the server, existing clients will provide their current state
167
- - The first client to join with content will automatically share their document state
168
- - Each editor type (simple text vs Lexical) maintains separate collaborative documents
169
-
170
- ## Project Structure
68
+ ```bash
69
+ # Install the Python package
70
+ pip install -e .
171
71
 
72
+ # Start the server
73
+ lexical-loro-server --port 8081
172
74
  ```
173
- src/
174
- ├── App.tsx # Main application component with tabbed interface
175
- ├── App.css # Application styles
176
- ├── CollaborativeEditor.tsx # Simple text editor component with Loro CRDT integration
177
- ├── CollaborativeEditor.css # Simple editor styles
178
- ├── LexicalCollaborativeEditor.tsx # Lexical rich text editor component
179
- ├── LexicalCollaborativeEditor.css # Lexical editor styles
180
- ├── LoroCollaborativePlugin.tsx # Lexical plugin for Loro CRDT integration
181
- ├── main.tsx # React application entry point
182
- └── vite-env.d.ts # Vite type definitions
183
-
184
- server.ts # WebSocket server for real-time communication
185
- package.json # Dependencies and scripts
186
- ```
187
-
188
- ## How It Works
189
75
 
190
- ### Loro CRDT Integration
76
+ ## Examples
191
77
 
192
- The application uses Loro CRDT to manage collaborative editing across two different editor types:
78
+ For complete working examples, see the `src/examples/` directory which contains:
79
+ - Full React application with dual editor support
80
+ - Server selection interface
81
+ - Connection status indicators
82
+ - Rich text formatting examples
193
83
 
194
- 1. **Document Creation**: Each editor type creates its own Loro document with a unique identifier:
195
- - Simple Text Editor: `shared-text`
196
- - Lexical Editor: `lexical-shared-doc`
197
- 2. **Local Changes**: When a user types, changes are applied to the corresponding local Loro document
198
- 3. **Change Detection**: The application detects insertions, deletions, and replacements
199
- 4. **Synchronization**: Changes are serialized and sent to other clients via WebSocket with document ID
200
- 5. **Conflict Resolution**: Loro CRDT automatically merges changes without conflicts
201
-
202
- The Complete Flow Diagram
203
-
204
-
205
- Remote User Types
206
-
207
- WebSocket Message
208
-
209
- loro-update received
210
-
211
- loroDocRef.current.import(update)
212
-
213
- doc.subscribe() callback fires
214
-
215
- updateLexicalFromLoro(editor, newText)
216
-
217
- editor.update() with new content
218
-
219
- Lexical State Updated
220
-
221
- UI Re-renders with New Content
222
-
223
- Protection Against Infinite Loops
224
-
225
- The system uses several mechanisms to prevent loops:
226
-
227
- isLocalChange.current flag - Prevents local changes from triggering remote updates
228
- { tag: 'collaboration' } on editor.update() - Allows the update listener to ignore these changes
229
- JSON comparison in updateLexicalFromLoro to avoid redundant updates
230
-
231
- When a Loro update is received, the Lexical state is updated through:
232
-
233
- WebSocket receives loro-update message
234
-
235
- loroDocRef.current.import(update) applies the change to Loro
236
- doc.subscribe() callback automatically fires
237
- updateLexicalFromLoro() converts Loro text to Lexical state
238
- editor.setEditorState() or DOM manipulation updates the editor
239
-
240
- The bridge is the doc.subscribe() callback on line 1901 - this is what makes Lexical automatically reflect any Loro document changes!
241
-
242
- ### Lexical Integration
243
-
244
- The Lexical editor integration includes:
245
-
246
- 1. **LoroCollaborativePlugin**: A custom Lexical plugin that bridges Lexical and Loro CRDT
247
- 2. **Bidirectional Sync**: Changes flow from Lexical → Loro → WebSocket and vice versa
248
- 3. **Rich Text Preservation**: The plugin maintains rich text formatting during collaborative editing
249
- 4. **Independent State**: Lexical editor maintains separate document state from simple text editor
250
-
251
- ### WebSocket Communication
252
-
253
- The WebSocket server:
254
- - Maintains connections to all clients
255
- - Broadcasts Loro document updates to all connected clients with document ID filtering
256
- - Handles client connections and disconnections
257
- - Provides connection status feedback
258
- - Stores separate snapshots for each document type
84
+ **DISCLAIMER** Collaborative Cursors still need fixes, see [this issue](https://github.com/datalayer/lexical-loro/issues/1).
259
85
 
260
- ### Real-time Updates
86
+ <div align="center" style="text-align: center">
87
+ <img alt="" src="https://assets.datalayer.tech/lexical-loro.gif" />
88
+ </div>
261
89
 
262
- 1. User types in the text area
263
- 2. Change is applied to local Loro document
264
- 3. Document update is serialized and sent via WebSocket
265
- 4. Other clients receive the update and apply it to their documents
266
- 5. UI is updated to reflect the changes
90
+ ## Core Features
267
91
 
268
- ### Initial Content Synchronization
92
+ - 🔄 **Real-time Collaboration**: Multiple users can edit the same document simultaneously
93
+ - 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
94
+ - 📝 **Lexical Integration**: Seamless integration with Lexical rich text editor
95
+ - 📚 **Standalone Library**: Use LexicalModel independently for document management
96
+ - 🌐 **WebSocket Server**: Python server for maintaining document state
97
+ - 📡 **Connection Management**: Robust WebSocket connection handling
98
+ - ✨ **Rich Text Support**: Preserves formatting during collaborative editing
99
+ - 💾 **Serialization**: JSON export/import and file persistence
100
+ - 🔧 **Extensible**: Plugin-based architecture for easy customization
269
101
 
270
- When a new collaborator joins:
102
+ ## Technology Stack
271
103
 
272
- 1. **Connection**: New client connects to WebSocket server
273
- 2. **Welcome**: Server sends welcome message to new client
274
- 3. **Snapshot Request**: New client requests current document state
275
- 4. **Snapshot Delivery**: Server sends stored snapshot or requests one from existing clients
276
- 5. **Content Sync**: New client applies snapshot and sees current document content
277
- 6. **Ready to Collaborate**: New client can now participate in real-time editing
104
+ **Core Dependencies:**
105
+ - **Lexical**: v0.33.1 (Facebook's extensible text editor framework)
106
+ - **Loro CRDT**: v1.5.10 (Conflict-free replicated data types)
107
+ - **React**: 18/19 (for plugin hooks and components)
108
+ - **Python**: 3.8+ with loro-py and websockets
278
109
 
279
- The server maintains the latest document snapshot to ensure new collaborators always see existing content.
110
+ **Development Dependencies:**
111
+ - **TypeScript**: For type safety
112
+ - **Vite**: For building and development (examples only)
113
+ - **pytest**: Python testing
114
+ - **ESLint**: Code linting
280
115
 
281
- ## Configuration
116
+ ## Installation
282
117
 
283
- ### WebSocket Server URL
118
+ ### Core Plugin
284
119
 
285
- You can configure the WebSocket server URL in the UI or by modifying the default in `CollaborativeEditor.tsx`:
120
+ The Lexical plugin is a single TypeScript/React component that you can copy into your project:
286
121
 
287
- ```typescript
288
- const [websocketUrl, setWebsocketUrl] = useState('ws://localhost:8080')
122
+ ```bash
123
+ # Copy the plugin file
124
+ cp src/LoroCollaborativePlugin.tsx your-project/src/
289
125
  ```
290
126
 
291
- ### Server Port
292
-
293
- To change the server port, modify `server.ts`:
294
-
295
- ```typescript
296
- const server = new LoroWebSocketServer(8080); // Change port here
127
+ **Dependencies required:**
128
+ ```bash
129
+ npm install lexical @lexical/react @lexical/selection loro-crdt react react-dom
297
130
  ```
298
131
 
299
- ## Development Scripts
300
-
301
- - `npm run dev` - Start React development server
302
- - `npm run server` - Start WebSocket server
303
- - `npm run dev:all` - Start both server and client
304
- - `npm run build` - Build for production
305
- - `npm run lint` - Run ESLint
306
- - `npm run preview` - Preview production build
307
-
308
- ## Production Deployment
309
-
310
- 1. Build the application:
311
- ```bash
312
- npm run build
313
- ```
132
+ ### Python Server
314
133
 
315
- 2. Deploy the `dist` folder to your web server
316
-
317
- 3. Deploy the WebSocket server to your backend infrastructure
318
-
319
- 4. Update the WebSocket URL in the application to point to your production server
320
-
321
- ## Contributing
134
+ Install the Python WebSocket server:
322
135
 
323
- 1. Fork the repository
324
- 2. Create a feature branch
325
- 3. Make your changes
326
- 4. Add tests if applicable
327
- 5. Submit a pull request
328
-
329
- ## License
330
-
331
- This project is open source and available under the [MIT License](LICENSE).
332
-
333
- ## Acknowledgments
334
-
335
- - [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
336
- - [Vite](https://vitejs.dev/) - Fast development build tool
337
- - [React](https://reactjs.org/) - UI library
338
- - [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
339
-
340
- # Lexical Loro Python Package
341
-
342
- A Python package for Lexical + Loro CRDT integration, providing a WebSocket server for real-time collaborative text editing.
343
-
344
- ## Features
345
-
346
- - **Real-time collaboration**: WebSocket server for live document collaboration
347
- - **Loro CRDT integration**: Uses Loro CRDT for conflict-free replicated data types
348
- - **Lexical compatibility**: Designed to work with Lexical rich text editor
349
- - **Ephemeral data support**: Handles cursor positions and selections
350
- - **Multiple document support**: Manages multiple collaborative documents
136
+ ```bash
137
+ # Install from this repository
138
+ pip install -e .
351
139
 
352
- ## Installation
140
+ # Or install specific dependencies
141
+ pip install websockets click loro
142
+ ```
353
143
 
354
- ### From PyPI (when published)
144
+ ## Usage
355
145
 
356
- ```bash
357
- pip install lexical-loro
146
+ ### 1. Lexical Plugin Integration
147
+
148
+ Add the plugin to your Lexical editor:
149
+
150
+ ```tsx
151
+ import { LexicalComposer } from '@lexical/react/LexicalComposer';
152
+ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
153
+ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
154
+ import { LoroCollaborativePlugin } from './LoroCollaborativePlugin';
155
+
156
+ const editorConfig = {
157
+ namespace: 'MyEditor',
158
+ theme: {},
159
+ onError: console.error,
160
+ };
161
+
162
+ function CollaborativeEditor() {
163
+ return (
164
+ <LexicalComposer initialConfig={editorConfig}>
165
+ <div className="editor-container">
166
+ <RichTextPlugin
167
+ contentEditable={<ContentEditable className="editor-input" />}
168
+ placeholder={<div className="editor-placeholder">Start typing...</div>}
169
+ ErrorBoundary={() => <div>Error occurred</div>}
170
+ />
171
+ <LoroCollaborativePlugin
172
+ websocketUrl="ws://localhost:8081"
173
+ docId="shared-document"
174
+ username="user123"
175
+ />
176
+ </div>
177
+ </LexicalComposer>
178
+ );
179
+ }
358
180
  ```
359
181
 
360
- ### Local Development
182
+ ### 2. Standalone LexicalModel Library
361
183
 
362
- ```bash
363
- # Install in development mode
364
- pip install -e "python_src/[dev]"
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', '')}")
365
223
  ```
366
224
 
367
- ## Usage
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
368
230
 
369
- ### Command Line
231
+ ### 3. Python Server Setup
370
232
 
371
- Start the server using the command line interface:
233
+ Start the WebSocket server:
372
234
 
373
235
  ```bash
374
- # Start server on default port (8081)
236
+ # Default port (8081)
375
237
  lexical-loro-server
376
238
 
377
- # Start server on custom port
239
+ # Custom port
378
240
  lexical-loro-server --port 8082
379
241
 
380
- # Start with debug logging
381
- lexical-loro-server --log-level DEBUG
242
+ # With debug logging
243
+ lexical-loro-server --port 8081 --log-level DEBUG
382
244
  ```
383
245
 
384
- ### Programmatic Usage
246
+ ### 4. Programmatic Server Usage
385
247
 
386
248
  ```python
387
249
  import asyncio
@@ -390,162 +252,204 @@ from lexical_loro import LoroWebSocketServer
390
252
  async def main():
391
253
  server = LoroWebSocketServer(port=8081)
392
254
  await server.start()
255
+ print("Server running on ws://localhost:8081")
393
256
 
394
257
  if __name__ == "__main__":
395
258
  asyncio.run(main())
396
259
  ```
397
260
 
398
- ### Integration with Node.js/TypeScript Projects
261
+ ## Plugin API
399
262
 
400
- Update your `package.json` scripts:
263
+ For detailed API documentation, see [`docs/API.md`](docs/API.md).
401
264
 
402
- ```json
403
- {
404
- "scripts": {
405
- "server:py": "lexical-loro-server",
406
- "dev:py": "concurrently \"lexical-loro-server\" \"npm run dev\""
407
- }
265
+ ### Quick Reference
266
+
267
+ ```tsx
268
+ interface LoroCollaborativePluginProps {
269
+ websocketUrl: string; // WebSocket server URL
270
+ docId: string; // Unique document identifier
271
+ username: string; // User identifier
272
+ userColor?: string; // User cursor color (optional)
273
+ debug?: boolean; // Enable debug logging (optional)
408
274
  }
409
275
  ```
410
276
 
411
- ## API Reference
277
+ ## Initialization Best Practices
412
278
 
413
- ### LoroWebSocketServer
279
+ ⚠️ **Important**: Always wait for collaboration initialization before enabling other plugins.
414
280
 
415
- Main server class for handling WebSocket connections and Loro document management.
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
416
286
 
417
- #### Constructor
287
+ ## Examples
418
288
 
419
- ```python
420
- server = LoroWebSocketServer(port=8081)
289
+ For complete working examples and demonstrations, see the `src/examples/` directory:
290
+
291
+ ```bash
292
+ # Run the example application
293
+ npm install
294
+ npm run example
295
+
296
+ # This starts both Node.js and Python servers plus a React demo app
297
+ # Open http://localhost:5173 to see dual editor interface
421
298
  ```
422
299
 
423
- #### Methods
300
+ The examples include:
301
+ - **Complete React App**: Full collaborative editor with UI
302
+ - **Server Selection**: Switch between Node.js and Python backends
303
+ - **Dual Editors**: Simple text area and rich Lexical editor
304
+ - **Real-time Demo**: Multi-user collaboration testing
424
305
 
425
- - `start()`: Start the WebSocket server
426
- - `shutdown()`: Gracefully shutdown the server
427
- - `handle_client(websocket)`: Handle new client connections
428
- - `handle_message(client_id, message)`: Process messages from clients
306
+ See `src/examples/README.md` for detailed example documentation.
429
307
 
430
- ### Client
308
+ ## Project Structure
431
309
 
432
- Represents a connected client with metadata.
310
+ ### Core Components
433
311
 
434
- ```python
435
- class Client:
436
- def __init__(self, websocket, client_id):
437
- self.websocket = websocket
438
- self.id = client_id
439
- self.color = self._generate_color()
312
+ ```
313
+ src/
314
+ ├── LoroCollaborativePlugin.tsx # Main Lexical plugin for collaboration
315
+ └── vite-env.d.ts # TypeScript definitions
316
+
317
+ lexical_loro/ # Python WebSocket server package
318
+ ├── __init__.py # Package exports
319
+ ├── server.py # WebSocket server implementation
320
+ ├── cli.py # Command line interface
321
+ ├── model/
322
+ │ └── lexical_model.py # Standalone LexicalModel library
323
+ └── tests/ # Python test suite
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
+
334
+ pyproject.toml # Python package configuration
440
335
  ```
441
336
 
442
- ## Development
337
+ ### Examples Directory
443
338
 
444
- ### Setup Development Environment
339
+ ```
340
+ src/examples/ # Complete demo application
341
+ ├── App.tsx # Demo app with dual editors
342
+ ├── LexicalCollaborativeEditor.tsx # Rich text editor example
343
+ ├── TextAreaCollaborativeEditor.tsx # Simple text editor example
344
+ ├── ServerSelector.tsx # Server selection UI
345
+ ├── LexicalToolbar.tsx # Rich text toolbar
346
+ ├── main.tsx # Demo app entry point
347
+ └── *.css # Styling for examples
348
+
349
+ servers/
350
+ └── server.ts # Node.js server (for comparison)
351
+ ```
445
352
 
446
- ```bash
447
- # Install development dependencies
448
- pip install -e "python_src/[dev]"
353
+ ### Archive
449
354
 
450
- # Run tests
451
- pytest
355
+ ```
356
+ src/archive/ # Historical plugin implementations
357
+ ├── LoroCollaborativePlugin0.tsx # Previous versions for reference
358
+ ├── LoroCollaborativePlugin1.tsx
359
+ ├── LoroCollaborativePlugin2.tsx
360
+ ├── LoroCollaborativePlugin3.tsx
361
+ ├── LoroCollaborativePlugin4.tsx
362
+ └── LoroCollaborativePlugin5.tsx
363
+ ```
452
364
 
453
- # Run tests with coverage
454
- pytest --cov=lexical_loro --cov-report=html
365
+ ## Architecture
455
366
 
456
- # Format code
457
- black python_src/
367
+ For detailed architecture documentation, see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
458
368
 
459
- # Lint code
460
- ruff python_src/
369
+ ### System Overview
461
370
 
462
- # Type checking
463
- mypy python_src/
464
- ```
371
+ The collaboration system consists of three main components:
465
372
 
466
- ### Testing
373
+ 1. **LoroCollaborativePlugin** (Client-side) - Lexical integration
374
+ 2. **LoroWebSocketServer** (Server-side) - Real-time synchronization
375
+ 3. **LexicalModel** (Standalone Library) - Independent document model
467
376
 
468
- The package includes comprehensive tests for:
377
+ ### Data Flow
469
378
 
470
- - WebSocket connection handling
471
- - Loro document operations
472
- - Message processing
473
- - Client management
474
- - Error handling
379
+ ```
380
+ User Types → Lexical Editor → Plugin → Loro CRDT → WebSocket
381
+
382
+ WebSocket Loro CRDT ← Plugin ← Lexical Editor ← Other Users
383
+ ```
475
384
 
476
- Run tests:
385
+ ## Configuration
477
386
 
478
- ```bash
479
- pytest tests/ -v
480
- ```
387
+ For detailed configuration options, see [`docs/API.md`](docs/API.md).
481
388
 
482
- ### Building
389
+ ### Quick Configuration
483
390
 
484
- Build the package:
391
+ ```tsx
392
+ // Plugin configuration
393
+ <LoroCollaborativePlugin
394
+ websocketUrl="ws://localhost:8081"
395
+ docId="my-document"
396
+ username="user123"
397
+ debug={true}
398
+ />
399
+ ```
485
400
 
486
401
  ```bash
487
- pip install build
488
- python -m build
402
+ # Server configuration
403
+ lexical-loro-server --port 8081 --log-level DEBUG
489
404
  ```
490
405
 
491
- ## Protocol
406
+ ## Development
492
407
 
493
- The server communicates with clients using a JSON-based WebSocket protocol:
408
+ For comprehensive development guidelines, see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
494
409
 
495
- ### Message Types
410
+ ### Quick Start
496
411
 
497
- - `loro-update`: Apply Loro CRDT updates
498
- - `snapshot`: Full document snapshots
499
- - `request-snapshot`: Request current document state
500
- - `ephemeral-update`: Cursor and selection updates
501
- - `awareness-update`: User presence information
412
+ ```bash
413
+ # Install dependencies
414
+ npm install
415
+ pip install -e ".[dev]"
502
416
 
503
- ### Example Messages
417
+ # Run tests
418
+ npm test
419
+ npm run test:py
504
420
 
505
- ```json
506
- {
507
- "type": "loro-update",
508
- "docId": "lexical-shared-doc",
509
- "updateHex": "deadbeef..."
510
- }
421
+ # Start development server
422
+ lexical-loro-server --log-level DEBUG
511
423
  ```
512
424
 
513
- ## Configuration
425
+ ## Contributing
514
426
 
515
- ### Environment Variables
427
+ We welcome contributions! Please see [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) for detailed guidelines.
516
428
 
517
- - `LEXICAL_LORO_PORT`: Default server port (default: 8081)
518
- - `LEXICAL_LORO_HOST`: Host to bind to (default: localhost)
519
- - `LEXICAL_LORO_LOG_LEVEL`: Logging level (default: INFO)
429
+ ### Quick Contributing Guide
520
430
 
521
- ### Supported Documents
431
+ 1. Fork the repository
432
+ 2. Create a feature branch
433
+ 3. Focus changes on core components
434
+ 4. Add tests for new functionality
435
+ 5. Update documentation as needed
436
+ 6. Submit a pull request
522
437
 
523
- The server pre-initializes several document types:
438
+ ## Documentation
524
439
 
525
- - `shared-text`: Basic text document
526
- - `lexical-shared-doc-v0`: Minimal plugin document
527
- - `lexical-shared-doc-v1`: Full-featured plugin document
528
- - `lexical-shared-doc-v2`: Clean JSON plugin document
529
- - `lexical-shared-doc-v3`: Text-only plugin document
530
- - `lexical-shared-doc-v4`: Smart hybrid plugin document
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
531
445
 
532
446
  ## License
533
447
 
534
- MIT License - see LICENSE file for details.
535
-
536
- ## Contributing
537
-
538
- 1. Fork the repository
539
- 2. Create a feature branch
540
- 3. Make your changes
541
- 4. Add tests
542
- 5. Run the test suite
543
- 6. Submit a pull request
544
-
545
- ## Support
546
-
547
- For issues and questions:
448
+ This project is open source and available under the [MIT License](LICENSE).
548
449
 
549
- - GitHub Issues: https://github.com/datalayer/lexical-loro/issues
550
- - Documentation: https://github.com/datalayer/lexical-loro#readme
450
+ ## Acknowledgments
551
451
 
452
+ - [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
453
+ - [Lexical](https://lexical.dev/) - Facebook's extensible text editor framework
454
+ - [React](https://reactjs.org/) - UI library for plugin hooks
455
+ - [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) - Real-time communication
@@ -9,6 +9,8 @@ interface LoroCollaborativePluginProps {
9
9
  userName: string;
10
10
  isCurrentUser?: boolean;
11
11
  }>) => void;
12
+ onInitialization?: (success: boolean) => void;
13
+ onSendMessageReady?: (sendMessageFn: (message: any) => void) => void;
12
14
  }
13
- export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }: 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;
14
16
  export default LoroCollaborativePlugin;
@@ -1,4 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2023-2025 Datalayer, Inc.
4
+ * Distributed under the terms of the MIT License.
5
+ */
2
6
  import { useEffect, useRef, useCallback, useState } from 'react';
3
7
  import { createPortal } from 'react-dom';
4
8
  import { $createParagraphNode, $getRoot, $getSelection, $isRangeSelection, $getNodeByKey, $isTextNode, $isElementNode, $isLineBreakNode, $createTextNode, createState, $getState, $setState } from 'lexical';
@@ -623,7 +627,7 @@ class CursorAwareness {
623
627
  return this.ephemeralStore.getAllStates();
624
628
  }
625
629
  }
626
- export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }) {
630
+ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization, onSendMessageReady }) {
627
631
  const [editor] = useLexicalComposerContext();
628
632
  const wsRef = useRef(null);
629
633
  const loroDocRef = useRef(new LoroDoc());
@@ -894,13 +898,29 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
894
898
  console.log('🎯 Set awareness with stable cursor data:', { userWithCursorData, clientId });
895
899
  // Send ephemeral update to other clients via WebSocket
896
900
  if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && awarenessRef.current) {
897
- const ephemeralData = awarenessRef.current.encode();
898
- const hexData = Array.from(ephemeralData).map(b => b.toString(16).padStart(2, '0')).join('');
899
- wsRef.current.send(JSON.stringify({
900
- type: 'ephemeral-update',
901
- docId: docId,
902
- data: hexData // Convert to hex string
903
- }));
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
+ }
904
924
  }
905
925
  }
906
926
  catch (error) {
@@ -1591,10 +1611,12 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1591
1611
  // WebSocket connection management with stable dependencies
1592
1612
  const stableOnConnectionChange = useRef(onConnectionChange);
1593
1613
  const stableOnDisconnectReady = useRef(onDisconnectReady);
1614
+ const stableOnSendMessageReady = useRef(onSendMessageReady);
1594
1615
  // Update refs when props change without triggering effect
1595
1616
  useEffect(() => {
1596
1617
  stableOnConnectionChange.current = onConnectionChange;
1597
1618
  stableOnDisconnectReady.current = onDisconnectReady;
1619
+ stableOnSendMessageReady.current = onSendMessageReady;
1598
1620
  });
1599
1621
  useEffect(() => {
1600
1622
  // Close any existing connection before creating a new one
@@ -1660,6 +1682,13 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1660
1682
  }
1661
1683
  };
1662
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);
1663
1692
  };
1664
1693
  ws.onmessage = (event) => {
1665
1694
  try {
@@ -1686,13 +1715,38 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1686
1715
  loroDocRef.current.import(snapshot);
1687
1716
  hasReceivedInitialSnapshot.current = true;
1688
1717
  console.log('📄 Lexical editor received and applied initial snapshot');
1689
- // Immediately reflect the current Loro text into the editor after import
1718
+ // Notify parent component about successful initialization
1719
+ if (onInitialization) {
1720
+ onInitialization(true);
1721
+ }
1722
+ // Immediately reflect the current Loro content into the editor after import
1690
1723
  try {
1691
- const currentText = loroDocRef.current.getText(docId).toString();
1692
- 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
+ }
1693
1743
  }
1694
1744
  catch (e) {
1695
1745
  console.warn('⚠️ Could not immediately reflect snapshot to editor:', e);
1746
+ // Notify parent component about failed initialization
1747
+ if (onInitialization) {
1748
+ onInitialization(false);
1749
+ }
1696
1750
  }
1697
1751
  }
1698
1752
  else if (data.type === 'ephemeral-update' || data.type === 'ephemeral-event') {
@@ -1815,6 +1869,30 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1815
1869
  console.warn('🧹 Cannot cleanup - missing client ID or awareness ref');
1816
1870
  }
1817
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
+ }
1818
1896
  }
1819
1897
  catch (err) {
1820
1898
  console.error('Error processing WebSocket message in Lexical plugin:', err);
@@ -1842,6 +1920,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
1842
1920
  ws.onerror = (err) => {
1843
1921
  isConnectingRef.current = false;
1844
1922
  console.error('WebSocket error in Lexical plugin:', err);
1923
+ // Notify initialization failure if we haven't received initial content yet
1924
+ if (!hasReceivedInitialSnapshot.current && onInitialization) {
1925
+ onInitialization(false);
1926
+ }
1845
1927
  };
1846
1928
  }
1847
1929
  catch (err) {
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./LoroCollaborativePlugin";
package/lib/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2025 Datalayer, Inc.
3
+ * Distributed under the terms of the MIT License.
4
+ */
5
+ export * from "./LoroCollaborativePlugin";
package/package.json CHANGED
@@ -1,10 +1,22 @@
1
1
  {
2
2
  "name": "@datalayer/lexical-loro",
3
3
  "private": false,
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
+ "description": "Collaborative editing plugin for Lexical based on Loro CRDT",
7
+ "main": "lib/index.js",
8
+ "types": "lib/index.d.ts",
6
9
  "files": [
7
- "lib/**/*"
10
+ "lib/**/*.*"
11
+ ],
12
+ "keywords": [
13
+ "lexical",
14
+ "loro",
15
+ "crdt",
16
+ "collaborative",
17
+ "editor",
18
+ "plugin",
19
+ "react"
8
20
  ],
9
21
  "repository": {
10
22
  "type": "git",
@@ -18,33 +30,38 @@
18
30
  "scripts": {
19
31
  "build": "tsc -b && vite build",
20
32
  "clean": "rimraf dist lib",
33
+ "dev": "npm run example",
34
+ "dev:all:js": "npm run example:js",
35
+ "dev:all:py": "npm run example:py",
36
+ "dev:vite": "npm run example:vite",
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:dev\" \"npm run server:py:minimal\" \"npm run example:vite\"",
40
+ "example:vite": "vite",
21
41
  "lint": "eslint .",
22
42
  "preview": "vite preview",
23
- "server": "tsx servers/server.ts",
43
+ "server": "tsx node-js/server.ts",
24
44
  "server:py": "lexical-loro-server",
25
45
  "server:py:dev": "python3 -m lexical_loro.cli",
26
- "test:py": "python3 -m pytest lexical_loro/tests/ -v",
27
- "test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f",
28
- "test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
46
+ "server:py:minimal": "python3 -m lexical_loro.cli_minimal",
29
47
  "test": "vitest",
30
48
  "test:js": "vitest run",
31
- "dev": "concurrently \"npm run server\" \"npm run server:py\" \"npm run dev:vite\"",
32
- "dev:vite": "vite",
33
- "dev:all:py": "concurrently \"npm run server:py\" \"npm run dev:vite\"",
34
- "dev:all:js": "concurrently \"npm run server\" \"npm run dev:vite\""
49
+ "test:py": "python3 -m pytest lexical_loro/tests/ -v",
50
+ "test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
51
+ "test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f"
35
52
  },
36
53
  "dependencies": {
37
- "@lexical/react": "^0.33.1",
38
- "@lexical/selection": "^0.33.1",
39
- "lexical": "^0.33.1",
40
54
  "loro-crdt": "^1.5.10",
41
- "react": "^18 || ^19.1.0",
42
- "react-dom": "^18 || ^19.1.0"
55
+ "react": "^18.3.1",
56
+ "react-dom": "^18.3.1",
57
+ "lexical": "^0.33.1",
58
+ "@lexical/react": "^0.33.1",
59
+ "@lexical/selection": "^0.33.1"
43
60
  },
44
61
  "devDependencies": {
45
62
  "@eslint/js": "^9.30.1",
46
- "@types/react": "^19.1.8",
47
- "@types/react-dom": "^19.1.6",
63
+ "@types/react": "18.3.20",
64
+ "@types/react-dom": "18.3.6",
48
65
  "@types/ws": "^8.18.1",
49
66
  "@vitejs/plugin-react": "^4.6.0",
50
67
  "concurrently": "^9.2.0",