@datalayer/lexical-loro 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +519 -402
- package/lib/LoroCollaborativePlugin.d.ts +2 -1
- package/lib/LoroCollaborativePlugin.js +17 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +5 -0
- package/package.json +31 -13
package/README.md
CHANGED
|
@@ -2,550 +2,667 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/sponsors/datalayer)
|
|
4
4
|
|
|
5
|
-
# Collaborative Plugin for Lexical
|
|
5
|
+
# ✍️ 🦜 Lexical Loro - Collaborative Plugin for Lexical with Loro CRDT
|
|
6
6
|
|
|
7
|
-
A
|
|
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
|
-
|
|
9
|
+
## Core Components
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This package provides two main components for building collaborative text editors:
|
|
12
|
+
|
|
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
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Using the Lexical Plugin
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { LoroCollaborativePlugin } from './src/LoroCollaborativePlugin';
|
|
22
|
+
|
|
23
|
+
function MyEditor() {
|
|
24
|
+
return (
|
|
25
|
+
<LexicalComposer initialConfig={editorConfig}>
|
|
26
|
+
<RichTextPlugin />
|
|
27
|
+
<LoroCollaborativePlugin
|
|
28
|
+
websocketUrl="ws://localhost:8081"
|
|
29
|
+
docId="my-document"
|
|
30
|
+
username="user1"
|
|
31
|
+
/>
|
|
32
|
+
</LexicalComposer>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Using the Python Server
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Install the Python package
|
|
41
|
+
pip install -e .
|
|
42
|
+
|
|
43
|
+
# Start the server
|
|
44
|
+
lexical-loro-server --port 8081
|
|
45
|
+
```
|
|
12
46
|
|
|
13
|
-
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
For complete working examples, see the `src/examples/` directory which contains:
|
|
50
|
+
- Full React application with dual editor support
|
|
51
|
+
- Server selection interface
|
|
52
|
+
- Connection status indicators
|
|
53
|
+
- Rich text formatting examples
|
|
54
|
+
|
|
55
|
+
**DISCLAIMER** Collaborative Cursors still need fixes, see [this issue](https://github.com/datalayer/lexical-loro/issues/1).
|
|
14
56
|
|
|
15
57
|
<div align="center" style="text-align: center">
|
|
16
58
|
<img alt="" src="https://assets.datalayer.tech/lexical-loro.gif" />
|
|
17
59
|
</div>
|
|
18
60
|
|
|
19
|
-
## Features
|
|
61
|
+
## Core Features
|
|
20
62
|
|
|
21
63
|
- 🔄 **Real-time Collaboration**: Multiple users can edit the same document simultaneously
|
|
22
|
-
- 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
|
|
23
|
-
- 📝 **
|
|
24
|
-
- 🌐 **
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- ✨ **Rich Text Features**: Bold, italic, underline with real-time formatting sync
|
|
29
|
-
- 🔧 **Server Selection**: Switch between Node.js and Python backends
|
|
64
|
+
- 🚀 **Conflict-free**: Uses Loro CRDT to automatically resolve conflicts
|
|
65
|
+
- 📝 **Lexical Integration**: Seamless integration with Lexical rich text editor
|
|
66
|
+
- 🌐 **WebSocket Server**: Python server for maintaining document state
|
|
67
|
+
- 📡 **Connection Management**: Robust WebSocket connection handling
|
|
68
|
+
- ✨ **Rich Text Support**: Preserves formatting during collaborative editing
|
|
69
|
+
- 🔧 **Extensible**: Plugin-based architecture for easy customization
|
|
30
70
|
|
|
31
71
|
## Technology Stack
|
|
32
72
|
|
|
33
|
-
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
|
|
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
|
|
73
|
+
**Core Dependencies:**
|
|
74
|
+
- **Lexical**: v0.33.1 (Facebook's extensible text editor framework)
|
|
75
|
+
- **Loro CRDT**: v1.5.10 (Conflict-free replicated data types)
|
|
76
|
+
- **React**: 18/19 (for plugin hooks and components)
|
|
77
|
+
- **Python**: 3.8+ with loro-py and websockets
|
|
44
78
|
|
|
45
|
-
|
|
79
|
+
**Development Dependencies:**
|
|
80
|
+
- **TypeScript**: For type safety
|
|
81
|
+
- **Vite**: For building and development (examples only)
|
|
82
|
+
- **pytest**: Python testing
|
|
83
|
+
- **ESLint**: Code linting
|
|
46
84
|
|
|
47
|
-
|
|
48
|
-
- npm or yarn
|
|
49
|
-
- Python 3.8+ (for Python server option)
|
|
50
|
-
- pip3 (for Python dependencies)
|
|
51
|
-
|
|
52
|
-
### Installation
|
|
53
|
-
|
|
54
|
-
1. Install Node.js dependencies:
|
|
55
|
-
```bash
|
|
56
|
-
npm install
|
|
57
|
-
```
|
|
85
|
+
## Installation
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
pip3 install -r requirements.txt
|
|
62
|
-
# or run the setup script
|
|
63
|
-
./setup-python.sh
|
|
64
|
-
```
|
|
87
|
+
### Core Plugin
|
|
65
88
|
|
|
66
|
-
|
|
89
|
+
The Lexical plugin is a single TypeScript/React component that you can copy into your project:
|
|
67
90
|
|
|
68
|
-
#### Option 1: All Servers (Recommended)
|
|
69
91
|
```bash
|
|
70
|
-
|
|
92
|
+
# Copy the plugin file
|
|
93
|
+
cp src/LoroCollaborativePlugin.tsx your-project/src/
|
|
71
94
|
```
|
|
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.
|
|
73
95
|
|
|
74
|
-
|
|
96
|
+
**Dependencies required:**
|
|
75
97
|
```bash
|
|
76
|
-
npm
|
|
98
|
+
npm install lexical @lexical/react @lexical/selection loro-crdt react react-dom
|
|
77
99
|
```
|
|
78
|
-
This starts only the Python WebSocket server (port 8081) and React development server.
|
|
79
100
|
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
npm run dev:all:js
|
|
83
|
-
```
|
|
84
|
-
This starts only the Node.js WebSocket server (port 8080) and React development server.
|
|
101
|
+
### Python Server
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
Install the Python WebSocket server:
|
|
87
104
|
|
|
88
|
-
**All servers manually:**
|
|
89
105
|
```bash
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# Terminal 2: Start Python WebSocket server
|
|
94
|
-
npm run server:py
|
|
106
|
+
# Install from this repository
|
|
107
|
+
pip install -e .
|
|
95
108
|
|
|
96
|
-
#
|
|
97
|
-
|
|
109
|
+
# Or install specific dependencies
|
|
110
|
+
pip install websockets click loro
|
|
98
111
|
```
|
|
99
112
|
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
# Terminal 1: Start Node.js WebSocket server
|
|
103
|
-
npm run server
|
|
113
|
+
## Usage
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
### 1. Lexical Plugin Integration
|
|
116
|
+
|
|
117
|
+
Add the plugin to your Lexical editor:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
121
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
122
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
123
|
+
import { LoroCollaborativePlugin } from './LoroCollaborativePlugin';
|
|
124
|
+
|
|
125
|
+
const editorConfig = {
|
|
126
|
+
namespace: 'MyEditor',
|
|
127
|
+
theme: {},
|
|
128
|
+
onError: console.error,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
function CollaborativeEditor() {
|
|
132
|
+
return (
|
|
133
|
+
<LexicalComposer initialConfig={editorConfig}>
|
|
134
|
+
<div className="editor-container">
|
|
135
|
+
<RichTextPlugin
|
|
136
|
+
contentEditable={<ContentEditable className="editor-input" />}
|
|
137
|
+
placeholder={<div className="editor-placeholder">Start typing...</div>}
|
|
138
|
+
ErrorBoundary={() => <div>Error occurred</div>}
|
|
139
|
+
/>
|
|
140
|
+
<LoroCollaborativePlugin
|
|
141
|
+
websocketUrl="ws://localhost:8081"
|
|
142
|
+
docId="shared-document"
|
|
143
|
+
username="user123"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</LexicalComposer>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
107
149
|
```
|
|
108
150
|
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
# Terminal 1: Start Python WebSocket server
|
|
112
|
-
npm run server:py
|
|
113
|
-
# or directly: python3 server.py
|
|
114
|
-
|
|
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
|
|
151
|
+
### 2. Python Server Setup
|
|
169
152
|
|
|
170
|
-
|
|
153
|
+
Start the WebSocket server:
|
|
171
154
|
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
155
|
+
```bash
|
|
156
|
+
# Default port (8081)
|
|
157
|
+
lexical-loro-server
|
|
183
158
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
```
|
|
159
|
+
# Custom port
|
|
160
|
+
lexical-loro-server --port 8082
|
|
187
161
|
|
|
188
|
-
|
|
162
|
+
# With debug logging
|
|
163
|
+
lexical-loro-server --port 8081 --log-level DEBUG
|
|
164
|
+
```
|
|
189
165
|
|
|
190
|
-
###
|
|
166
|
+
### 3. Programmatic Server Usage
|
|
191
167
|
|
|
192
|
-
|
|
168
|
+
```python
|
|
169
|
+
import asyncio
|
|
170
|
+
from lexical_loro import LoroWebSocketServer
|
|
193
171
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
172
|
+
async def main():
|
|
173
|
+
server = LoroWebSocketServer(port=8081)
|
|
174
|
+
await server.start()
|
|
175
|
+
print("Server running on ws://localhost:8081")
|
|
201
176
|
|
|
202
|
-
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
asyncio.run(main())
|
|
179
|
+
```
|
|
203
180
|
|
|
181
|
+
## Plugin API
|
|
204
182
|
|
|
205
|
-
|
|
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
|
|
183
|
+
### LoroCollaborativePlugin Props
|
|
222
184
|
|
|
223
|
-
|
|
185
|
+
```tsx
|
|
186
|
+
interface LoroCollaborativePluginProps {
|
|
187
|
+
websocketUrl: string; // WebSocket server URL
|
|
188
|
+
docId: string; // Unique document identifier
|
|
189
|
+
username: string; // User identifier
|
|
190
|
+
userColor?: string; // User cursor color (optional)
|
|
191
|
+
debug?: boolean; // Enable debug logging (optional)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
224
194
|
|
|
225
|
-
|
|
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
|
+
## Initialization Best Practices
|
|
205
|
+
|
|
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
|
+
```
|
|
226
262
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
```
|
|
230
284
|
|
|
231
|
-
|
|
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
|
+
```
|
|
232
331
|
|
|
233
|
-
|
|
332
|
+
### Common Anti-Patterns to Avoid
|
|
234
333
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
334
|
+
❌ **Don't** enable plugins immediately:
|
|
335
|
+
```tsx
|
|
336
|
+
// WRONG: Race condition risk
|
|
337
|
+
<LoroCollaborativePlugin websocketUrl="..." />
|
|
338
|
+
<HistoryPlugin /> {/* May interfere with initial sync */}
|
|
339
|
+
```
|
|
239
340
|
|
|
240
|
-
|
|
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
|
+
```
|
|
241
350
|
|
|
242
|
-
|
|
351
|
+
❌ **Don't** ignore initialization status:
|
|
352
|
+
```tsx
|
|
353
|
+
// WRONG: No feedback on connection issues
|
|
354
|
+
<LoroCollaborativePlugin websocketUrl="..." />
|
|
355
|
+
```
|
|
243
356
|
|
|
244
|
-
|
|
357
|
+
### Debugging Initialization Issues
|
|
245
358
|
|
|
246
|
-
|
|
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
|
|
359
|
+
If initialization fails, check:
|
|
250
360
|
|
|
251
|
-
|
|
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
|
|
252
366
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
-
|
|
257
|
-
|
|
258
|
-
- Stores separate snapshots for each document type
|
|
367
|
+
```bash
|
|
368
|
+
# Enable debug logging
|
|
369
|
+
export LEXICAL_LORO_LOG_LEVEL=DEBUG
|
|
370
|
+
lexical-loro-server
|
|
371
|
+
```
|
|
259
372
|
|
|
260
|
-
|
|
373
|
+
## Server API
|
|
261
374
|
|
|
262
|
-
|
|
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
|
|
375
|
+
### LoroWebSocketServer Class
|
|
267
376
|
|
|
268
|
-
|
|
377
|
+
```python
|
|
378
|
+
from lexical_loro import LoroWebSocketServer
|
|
269
379
|
|
|
270
|
-
|
|
380
|
+
# Create server instance
|
|
381
|
+
server = LoroWebSocketServer(
|
|
382
|
+
port=8081, # Server port
|
|
383
|
+
host="localhost" # Server host
|
|
384
|
+
)
|
|
271
385
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
386
|
+
# Start server
|
|
387
|
+
await server.start()
|
|
278
388
|
|
|
279
|
-
|
|
389
|
+
# Shutdown server
|
|
390
|
+
await server.shutdown()
|
|
391
|
+
```
|
|
280
392
|
|
|
281
|
-
|
|
393
|
+
### Supported Message Types
|
|
282
394
|
|
|
283
|
-
|
|
395
|
+
The server handles these WebSocket message types:
|
|
284
396
|
|
|
285
|
-
|
|
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
|
|
286
402
|
|
|
287
|
-
|
|
288
|
-
const [websocketUrl, setWebsocketUrl] = useState('ws://localhost:8080')
|
|
289
|
-
```
|
|
403
|
+
## Examples
|
|
290
404
|
|
|
291
|
-
|
|
405
|
+
For complete working examples and demonstrations, see the `src/examples/` directory:
|
|
292
406
|
|
|
293
|
-
|
|
407
|
+
```bash
|
|
408
|
+
# Run the example application
|
|
409
|
+
npm install
|
|
410
|
+
npm run example
|
|
294
411
|
|
|
295
|
-
|
|
296
|
-
|
|
412
|
+
# This starts both Node.js and Python servers plus a React demo app
|
|
413
|
+
# Open http://localhost:5173 to see dual editor interface
|
|
297
414
|
```
|
|
298
415
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
-
|
|
304
|
-
- `npm run build` - Build for production
|
|
305
|
-
- `npm run lint` - Run ESLint
|
|
306
|
-
- `npm run preview` - Preview production build
|
|
416
|
+
The examples include:
|
|
417
|
+
- **Complete React App**: Full collaborative editor with UI
|
|
418
|
+
- **Server Selection**: Switch between Node.js and Python backends
|
|
419
|
+
- **Dual Editors**: Simple text area and rich Lexical editor
|
|
420
|
+
- **Real-time Demo**: Multi-user collaboration testing
|
|
307
421
|
|
|
308
|
-
|
|
422
|
+
See `src/examples/README.md` for detailed example documentation.
|
|
309
423
|
|
|
310
|
-
|
|
311
|
-
```bash
|
|
312
|
-
npm run build
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
2. Deploy the `dist` folder to your web server
|
|
424
|
+
## Project Structure
|
|
316
425
|
|
|
317
|
-
|
|
426
|
+
### Core Components
|
|
318
427
|
|
|
319
|
-
|
|
428
|
+
```
|
|
429
|
+
src/
|
|
430
|
+
├── LoroCollaborativePlugin.tsx # Main Lexical plugin for collaboration
|
|
431
|
+
└── vite-env.d.ts # TypeScript definitions
|
|
320
432
|
|
|
321
|
-
|
|
433
|
+
lexical_loro/ # Python WebSocket server package
|
|
434
|
+
├── __init__.py # Package exports
|
|
435
|
+
├── server.py # WebSocket server implementation
|
|
436
|
+
├── cli.py # Command line interface
|
|
437
|
+
└── tests/ # Python test suite
|
|
322
438
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
3. Make your changes
|
|
326
|
-
4. Add tests if applicable
|
|
327
|
-
5. Submit a pull request
|
|
439
|
+
pyproject.toml # Python package configuration
|
|
440
|
+
```
|
|
328
441
|
|
|
329
|
-
|
|
442
|
+
### Examples Directory
|
|
330
443
|
|
|
331
|
-
|
|
444
|
+
```
|
|
445
|
+
src/examples/ # Complete demo application
|
|
446
|
+
├── App.tsx # Demo app with dual editors
|
|
447
|
+
├── LexicalCollaborativeEditor.tsx # Rich text editor example
|
|
448
|
+
├── TextAreaCollaborativeEditor.tsx # Simple text editor example
|
|
449
|
+
├── ServerSelector.tsx # Server selection UI
|
|
450
|
+
├── LexicalToolbar.tsx # Rich text toolbar
|
|
451
|
+
├── main.tsx # Demo app entry point
|
|
452
|
+
└── *.css # Styling for examples
|
|
453
|
+
|
|
454
|
+
servers/
|
|
455
|
+
└── server.ts # Node.js server (for comparison)
|
|
456
|
+
```
|
|
332
457
|
|
|
333
|
-
|
|
458
|
+
### Archive
|
|
334
459
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
460
|
+
```
|
|
461
|
+
src/archive/ # Historical plugin implementations
|
|
462
|
+
├── LoroCollaborativePlugin0.tsx # Previous versions for reference
|
|
463
|
+
├── LoroCollaborativePlugin1.tsx
|
|
464
|
+
├── LoroCollaborativePlugin2.tsx
|
|
465
|
+
├── LoroCollaborativePlugin3.tsx
|
|
466
|
+
├── LoroCollaborativePlugin4.tsx
|
|
467
|
+
└── LoroCollaborativePlugin5.tsx
|
|
468
|
+
```
|
|
339
469
|
|
|
340
|
-
|
|
470
|
+
## How It Works
|
|
341
471
|
|
|
342
|
-
|
|
472
|
+
### Architecture Overview
|
|
343
473
|
|
|
344
|
-
|
|
474
|
+
The collaboration system consists of two main components:
|
|
345
475
|
|
|
346
|
-
|
|
347
|
-
-
|
|
348
|
-
-
|
|
349
|
-
-
|
|
350
|
-
-
|
|
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
|
|
351
481
|
|
|
352
|
-
|
|
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
|
|
353
487
|
|
|
354
|
-
###
|
|
488
|
+
### Data Flow
|
|
355
489
|
|
|
356
|
-
```bash
|
|
357
|
-
pip install lexical-loro
|
|
358
490
|
```
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
```bash
|
|
363
|
-
# Install in development mode
|
|
364
|
-
pip install -e "python_src/[dev]"
|
|
491
|
+
User Types → Lexical Editor → Plugin → Loro CRDT → WebSocket
|
|
492
|
+
↓
|
|
493
|
+
WebSocket ← Loro CRDT ← Plugin ← Lexical Editor ← Other Users
|
|
365
494
|
```
|
|
366
495
|
|
|
367
|
-
|
|
496
|
+
### CRDT Integration Process
|
|
368
497
|
|
|
369
|
-
|
|
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
|
|
370
504
|
|
|
371
|
-
|
|
505
|
+
### Connection Management
|
|
372
506
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
376
511
|
|
|
377
|
-
|
|
378
|
-
lexical-loro-server --port 8082
|
|
512
|
+
### Lexical Integration
|
|
379
513
|
|
|
380
|
-
|
|
381
|
-
lexical-loro-server --log-level DEBUG
|
|
382
|
-
```
|
|
514
|
+
The Lexical editor integration includes:
|
|
383
515
|
|
|
384
|
-
|
|
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
|
|
385
520
|
|
|
386
|
-
|
|
387
|
-
import asyncio
|
|
388
|
-
from lexical_loro import LoroWebSocketServer
|
|
521
|
+
### WebSocket Communication
|
|
389
522
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
393
529
|
|
|
394
|
-
|
|
395
|
-
asyncio.run(main())
|
|
396
|
-
```
|
|
530
|
+
### Real-time Updates
|
|
397
531
|
|
|
398
|
-
|
|
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
|
|
399
537
|
|
|
400
|
-
|
|
538
|
+
### Initial Content Synchronization
|
|
401
539
|
|
|
402
|
-
|
|
403
|
-
{
|
|
404
|
-
"scripts": {
|
|
405
|
-
"server:py": "lexical-loro-server",
|
|
406
|
-
"dev:py": "concurrently \"lexical-loro-server\" \"npm run dev\""
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
```
|
|
540
|
+
When a new collaborator joins:
|
|
410
541
|
|
|
411
|
-
|
|
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
|
|
412
548
|
|
|
413
|
-
|
|
549
|
+
The server maintains the latest document snapshot to ensure new collaborators always see existing content.
|
|
414
550
|
|
|
415
|
-
|
|
551
|
+
## Configuration
|
|
416
552
|
|
|
417
|
-
|
|
553
|
+
### Plugin Configuration
|
|
418
554
|
|
|
419
|
-
```
|
|
420
|
-
|
|
555
|
+
```tsx
|
|
556
|
+
<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)
|
|
562
|
+
/>
|
|
421
563
|
```
|
|
422
564
|
|
|
423
|
-
|
|
565
|
+
### Server Configuration
|
|
424
566
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
-
|
|
428
|
-
- `handle_message(client_id, message)`: Process messages from clients
|
|
429
|
-
|
|
430
|
-
### Client
|
|
567
|
+
```python
|
|
568
|
+
# Via command line
|
|
569
|
+
lexical-loro-server --port 8081 --host localhost --log-level DEBUG
|
|
431
570
|
|
|
432
|
-
|
|
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
|
|
433
576
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def __init__(self, websocket, client_id):
|
|
437
|
-
self.websocket = websocket
|
|
438
|
-
self.id = client_id
|
|
439
|
-
self.color = self._generate_color()
|
|
577
|
+
# Programmatically
|
|
578
|
+
server = LoroWebSocketServer(port=8081, host="localhost")
|
|
440
579
|
```
|
|
441
580
|
|
|
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
|
+
|
|
442
588
|
## Development
|
|
443
589
|
|
|
444
|
-
###
|
|
590
|
+
### Core Components Development
|
|
445
591
|
|
|
592
|
+
**Plugin Development:**
|
|
446
593
|
```bash
|
|
447
|
-
#
|
|
448
|
-
|
|
594
|
+
# The plugin is a single TypeScript file
|
|
595
|
+
src/LoroCollaborativePlugin.tsx
|
|
449
596
|
|
|
450
|
-
#
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
# Run tests with coverage
|
|
454
|
-
pytest --cov=lexical_loro --cov-report=html
|
|
597
|
+
# Dependencies for plugin development
|
|
598
|
+
npm install lexical @lexical/react @lexical/selection loro-crdt
|
|
599
|
+
```
|
|
455
600
|
|
|
456
|
-
|
|
457
|
-
|
|
601
|
+
**Server Development:**
|
|
602
|
+
```bash
|
|
603
|
+
# Install Python package in development mode
|
|
604
|
+
pip install -e ".[dev]"
|
|
458
605
|
|
|
459
|
-
#
|
|
460
|
-
|
|
606
|
+
# Run tests
|
|
607
|
+
pytest lexical_loro/tests/ -v
|
|
461
608
|
|
|
462
|
-
#
|
|
463
|
-
|
|
609
|
+
# Start server in development mode
|
|
610
|
+
python3 -m lexical_loro.cli --port 8081 --log-level DEBUG
|
|
464
611
|
```
|
|
465
612
|
|
|
466
613
|
### Testing
|
|
467
614
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
- WebSocket connection handling
|
|
471
|
-
- Loro document operations
|
|
472
|
-
- Message processing
|
|
473
|
-
- Client management
|
|
474
|
-
- Error handling
|
|
475
|
-
|
|
476
|
-
Run tests:
|
|
477
|
-
|
|
615
|
+
**Plugin Testing:**
|
|
478
616
|
```bash
|
|
479
|
-
|
|
617
|
+
npm run test # Run Vitest tests
|
|
618
|
+
npm run test:js # Run tests once
|
|
480
619
|
```
|
|
481
620
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
Build the package:
|
|
485
|
-
|
|
621
|
+
**Server Testing:**
|
|
486
622
|
```bash
|
|
487
|
-
|
|
488
|
-
|
|
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
|
|
489
626
|
```
|
|
490
627
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
The server communicates with clients using a JSON-based WebSocket protocol:
|
|
494
|
-
|
|
495
|
-
### Message Types
|
|
496
|
-
|
|
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
|
|
628
|
+
### Example Development
|
|
502
629
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
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)
|
|
511
637
|
```
|
|
512
638
|
|
|
513
|
-
##
|
|
514
|
-
|
|
515
|
-
### Environment Variables
|
|
639
|
+
## Contributing
|
|
516
640
|
|
|
517
|
-
|
|
518
|
-
- `LEXICAL_LORO_HOST`: Host to bind to (default: localhost)
|
|
519
|
-
- `LEXICAL_LORO_LOG_LEVEL`: Logging level (default: INFO)
|
|
641
|
+
We welcome contributions to both the Lexical plugin and Python server:
|
|
520
642
|
|
|
521
|
-
|
|
643
|
+
1. Fork the repository
|
|
644
|
+
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
|
|
648
|
+
4. Add tests for new functionality
|
|
649
|
+
5. Update documentation as needed
|
|
650
|
+
6. Submit a pull request
|
|
522
651
|
|
|
523
|
-
|
|
652
|
+
### Development Guidelines
|
|
524
653
|
|
|
525
|
-
-
|
|
526
|
-
-
|
|
527
|
-
-
|
|
528
|
-
-
|
|
529
|
-
- `lexical-shared-doc-v3`: Text-only plugin document
|
|
530
|
-
- `lexical-shared-doc-v4`: Smart hybrid plugin document
|
|
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
|
|
531
658
|
|
|
532
659
|
## License
|
|
533
660
|
|
|
534
|
-
|
|
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:
|
|
661
|
+
This project is open source and available under the [MIT License](LICENSE).
|
|
548
662
|
|
|
549
|
-
|
|
550
|
-
- Documentation: https://github.com/datalayer/lexical-loro#readme
|
|
663
|
+
## Acknowledgments
|
|
551
664
|
|
|
665
|
+
- [Loro CRDT](https://loro.dev/) - The CRDT library powering collaborative editing
|
|
666
|
+
- [Lexical](https://lexical.dev/) - Facebook's extensible text editor framework
|
|
667
|
+
- [React](https://reactjs.org/) - UI library for plugin hooks
|
|
668
|
+
- [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) - Real-time communication
|
|
@@ -9,6 +9,7 @@ interface LoroCollaborativePluginProps {
|
|
|
9
9
|
userName: string;
|
|
10
10
|
isCurrentUser?: boolean;
|
|
11
11
|
}>) => void;
|
|
12
|
+
onInitialization?: (success: boolean) => void;
|
|
12
13
|
}
|
|
13
|
-
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChange, onDisconnectReady, onPeerIdChange, onAwarenessChange, onInitialization }: LoroCollaborativePluginProps): import("react/jsx-runtime").JSX.Element;
|
|
14
15
|
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 }) {
|
|
627
631
|
const [editor] = useLexicalComposerContext();
|
|
628
632
|
const wsRef = useRef(null);
|
|
629
633
|
const loroDocRef = useRef(new LoroDoc());
|
|
@@ -1686,6 +1690,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1686
1690
|
loroDocRef.current.import(snapshot);
|
|
1687
1691
|
hasReceivedInitialSnapshot.current = true;
|
|
1688
1692
|
console.log('📄 Lexical editor received and applied initial snapshot');
|
|
1693
|
+
// Notify parent component about successful initialization
|
|
1694
|
+
if (onInitialization) {
|
|
1695
|
+
onInitialization(true);
|
|
1696
|
+
}
|
|
1689
1697
|
// Immediately reflect the current Loro text into the editor after import
|
|
1690
1698
|
try {
|
|
1691
1699
|
const currentText = loroDocRef.current.getText(docId).toString();
|
|
@@ -1693,6 +1701,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1693
1701
|
}
|
|
1694
1702
|
catch (e) {
|
|
1695
1703
|
console.warn('⚠️ Could not immediately reflect snapshot to editor:', e);
|
|
1704
|
+
// Notify parent component about failed initialization
|
|
1705
|
+
if (onInitialization) {
|
|
1706
|
+
onInitialization(false);
|
|
1707
|
+
}
|
|
1696
1708
|
}
|
|
1697
1709
|
}
|
|
1698
1710
|
else if (data.type === 'ephemeral-update' || data.type === 'ephemeral-event') {
|
|
@@ -1842,6 +1854,10 @@ export function LoroCollaborativePlugin({ websocketUrl, docId, onConnectionChang
|
|
|
1842
1854
|
ws.onerror = (err) => {
|
|
1843
1855
|
isConnectingRef.current = false;
|
|
1844
1856
|
console.error('WebSocket error in Lexical plugin:', err);
|
|
1857
|
+
// Notify initialization failure if we haven't received initial content yet
|
|
1858
|
+
if (!hasReceivedInitialSnapshot.current && onInitialization) {
|
|
1859
|
+
onInitialization(false);
|
|
1860
|
+
}
|
|
1845
1861
|
};
|
|
1846
1862
|
}
|
|
1847
1863
|
catch (err) {
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LoroCollaborativePlugin";
|
package/lib/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/lexical-loro",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
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,28 +30,34 @@
|
|
|
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\" \"npm run example:vite\"",
|
|
40
|
+
"example:vite": "vite",
|
|
21
41
|
"lint": "eslint .",
|
|
22
42
|
"preview": "vite preview",
|
|
23
43
|
"server": "tsx servers/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",
|
|
29
46
|
"test": "vitest",
|
|
30
47
|
"test:js": "vitest run",
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"dev:all:js": "concurrently \"npm run server\" \"npm run dev:vite\""
|
|
48
|
+
"test:py": "python3 -m pytest lexical_loro/tests/ -v",
|
|
49
|
+
"test:py:coverage": "python3 -m pytest lexical_loro/tests/ --cov=. --cov-report=html --cov-report=term",
|
|
50
|
+
"test:py:watch": "python3 -m pytest lexical_loro/tests/ -v --tb=short -f"
|
|
35
51
|
},
|
|
36
52
|
"dependencies": {
|
|
37
|
-
"@lexical/react": "^0.33.1",
|
|
38
|
-
"@lexical/selection": "^0.33.1",
|
|
39
|
-
"lexical": "^0.33.1",
|
|
40
53
|
"loro-crdt": "^1.5.10",
|
|
41
54
|
"react": "^18 || ^19.1.0",
|
|
42
|
-
"react-dom": "^18 || ^19.1.0"
|
|
55
|
+
"react-dom": "^18 || ^19.1.0",
|
|
56
|
+
"lexical": "^0.33.1",
|
|
57
|
+
"@lexical/react": "^0.33.1",
|
|
58
|
+
"@lexical/selection": "^0.33.1"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
43
61
|
},
|
|
44
62
|
"devDependencies": {
|
|
45
63
|
"@eslint/js": "^9.30.1",
|