@edryslabs/genericprovider 1.0.1
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/LICENSE +24 -0
- package/README.md +660 -0
- package/dist/index.d.ts +323 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1001 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +36 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +37 -0
- package/dist/lib.js.map +1 -0
- package/dist/providers/dummy/index.d.ts +226 -0
- package/dist/providers/dummy/index.d.ts.map +1 -0
- package/dist/providers/dummy/index.js +326 -0
- package/dist/providers/dummy/index.js.map +1 -0
- package/dist/providers/gun/index.d.ts +269 -0
- package/dist/providers/gun/index.d.ts.map +1 -0
- package/dist/providers/gun/index.js +683 -0
- package/dist/providers/gun/index.js.map +1 -0
- package/dist/providers/indexeddb/index.d.ts +161 -0
- package/dist/providers/indexeddb/index.d.ts.map +1 -0
- package/dist/providers/indexeddb/index.js +369 -0
- package/dist/providers/indexeddb/index.js.map +1 -0
- package/dist/providers/matrix/index.d.ts +109 -0
- package/dist/providers/matrix/index.d.ts.map +1 -0
- package/dist/providers/matrix/index.js +329 -0
- package/dist/providers/matrix/index.js.map +1 -0
- package/dist/providers/nostr/index.d.ts +172 -0
- package/dist/providers/nostr/index.d.ts.map +1 -0
- package/dist/providers/nostr/index.js +280 -0
- package/dist/providers/nostr/index.js.map +1 -0
- package/dist/providers/peerjs/index.d.ts +231 -0
- package/dist/providers/peerjs/index.d.ts.map +1 -0
- package/dist/providers/peerjs/index.js +1038 -0
- package/dist/providers/peerjs/index.js.map +1 -0
- package/dist/providers/pubnub/index.d.ts +106 -0
- package/dist/providers/pubnub/index.d.ts.map +1 -0
- package/dist/providers/pubnub/index.js +357 -0
- package/dist/providers/pubnub/index.js.map +1 -0
- package/dist/providers/simple-peer/index.d.ts +253 -0
- package/dist/providers/simple-peer/index.d.ts.map +1 -0
- package/dist/providers/simple-peer/index.js +783 -0
- package/dist/providers/simple-peer/index.js.map +1 -0
- package/dist/providers/supabase/index.d.ts +80 -0
- package/dist/providers/supabase/index.d.ts.map +1 -0
- package/dist/providers/supabase/index.js +202 -0
- package/dist/providers/supabase/index.js.map +1 -0
- package/dist/providers/trystero/index.d.ts +181 -0
- package/dist/providers/trystero/index.d.ts.map +1 -0
- package/dist/providers/trystero/index.js +187 -0
- package/dist/providers/trystero/index.js.map +1 -0
- package/dist/providers/websocket/index.d.ts +92 -0
- package/dist/providers/websocket/index.d.ts.map +1 -0
- package/dist/providers/websocket/index.js +272 -0
- package/dist/providers/websocket/index.js.map +1 -0
- package/dist/sync-monitor.d.ts +90 -0
- package/dist/sync-monitor.d.ts.map +1 -0
- package/dist/sync-monitor.js +149 -0
- package/dist/sync-monitor.js.map +1 -0
- package/dist/transport.d.ts +116 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +2 -0
- package/dist/transport.js.map +1 -0
- package/package.json +137 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
# Generic Yjs Provider
|
|
2
|
+
|
|
3
|
+
A clean, minimal, backend-agnostic provider for Yjs that lets you implement any transport mechanism.
|
|
4
|
+
|
|
5
|
+
## 🚀 Live Demos
|
|
6
|
+
|
|
7
|
+
Try out all the different transport providers in action:
|
|
8
|
+
|
|
9
|
+
**[👉 View Live Demos](https://andre-dietrich.github.io/y-generic/)**
|
|
10
|
+
|
|
11
|
+
Explore 9 different transport implementations:
|
|
12
|
+
- **Dummy** - In-memory testing with network simulation
|
|
13
|
+
- **SimplePeer** - WebRTC peer-to-peer
|
|
14
|
+
- **PeerJS** - Simplified WebRTC with cloud signaling
|
|
15
|
+
- **IndexedDB** - Local browser persistence
|
|
16
|
+
- **Gun** - Decentralized graph database
|
|
17
|
+
- **Trystero** - Serverless WebRTC (Nostr, BitTorrent, MQTT, IPFS)
|
|
18
|
+
- **PubNub** - Cloud pub/sub with global CDN
|
|
19
|
+
- **WebSocket** - Standard client-server (y-websocket compatible)
|
|
20
|
+
- **Matrix** - Federated communication protocol
|
|
21
|
+
|
|
22
|
+
## Design Philosophy
|
|
23
|
+
|
|
24
|
+
**You implement**: How data is transmitted (WebSocket, WebRTC, PubNub, IndexedDB, etc.)
|
|
25
|
+
**Provider handles**: All Yjs synchronization logic (updates, awareness, state vectors)
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Implement the Transport Interface
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import type { Transport, ConnectionConfig } from './transport'
|
|
33
|
+
|
|
34
|
+
class MyTransport implements Transport {
|
|
35
|
+
private connection: any
|
|
36
|
+
private messageCallback?: (data: Uint8Array) => void
|
|
37
|
+
|
|
38
|
+
get isConnected(): boolean {
|
|
39
|
+
return !!this.connection
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async connect(config: ConnectionConfig): Promise<void> {
|
|
43
|
+
// Connect to your backend
|
|
44
|
+
this.connection = await connectToBackend(config.room)
|
|
45
|
+
|
|
46
|
+
// Listen for incoming data
|
|
47
|
+
this.connection.on('data', (data: Uint8Array) => {
|
|
48
|
+
if (this.messageCallback) {
|
|
49
|
+
this.messageCallback(data)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
disconnect(): void {
|
|
55
|
+
this.connection?.close()
|
|
56
|
+
this.connection = null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
send(data: Uint8Array): void {
|
|
60
|
+
this.connection?.send(data)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onMessage(callback: (data: Uint8Array) => void): () => void {
|
|
64
|
+
this.messageCallback = callback
|
|
65
|
+
return () => {
|
|
66
|
+
this.messageCallback = undefined
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Use the Provider
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import * as Y from 'yjs'
|
|
76
|
+
import { GenericProvider } from './GenericProvider'
|
|
77
|
+
import { MyTransport } from './MyTransport'
|
|
78
|
+
|
|
79
|
+
// Create Yjs document
|
|
80
|
+
const doc = new Y.Doc()
|
|
81
|
+
|
|
82
|
+
// Create your transport
|
|
83
|
+
const transport = new MyTransport()
|
|
84
|
+
|
|
85
|
+
// Create provider
|
|
86
|
+
const provider = new GenericProvider(doc, transport)
|
|
87
|
+
|
|
88
|
+
// Connect
|
|
89
|
+
await provider.connect({
|
|
90
|
+
room: 'my-room-123',
|
|
91
|
+
url: 'wss://myserver.com'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Use Yjs as normal - everything syncs automatically!
|
|
95
|
+
const ytext = doc.getText('content')
|
|
96
|
+
ytext.insert(0, 'Hello World')
|
|
97
|
+
|
|
98
|
+
// Access awareness for presence/cursors
|
|
99
|
+
provider.awareness.setLocalStateField('user', {
|
|
100
|
+
name: 'Alice',
|
|
101
|
+
color: '#ff0000'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Listen to events
|
|
105
|
+
provider.on('synced', (synced) => {
|
|
106
|
+
console.log('Synced:', synced)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
provider.on('status', (status) => {
|
|
110
|
+
console.log('Status:', status.state)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Cleanup
|
|
114
|
+
provider.disconnect()
|
|
115
|
+
provider.destroy()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## API Reference
|
|
119
|
+
|
|
120
|
+
### Transport Interface
|
|
121
|
+
|
|
122
|
+
The minimal interface your transport must implement:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
interface Transport {
|
|
126
|
+
// Lifecycle
|
|
127
|
+
connect(config: ConnectionConfig): Promise<void>
|
|
128
|
+
disconnect(): void
|
|
129
|
+
|
|
130
|
+
// Data transmission
|
|
131
|
+
send(data: Uint8Array): void | Promise<void>
|
|
132
|
+
onMessage(callback: (data: Uint8Array) => void): () => void
|
|
133
|
+
|
|
134
|
+
// Status
|
|
135
|
+
readonly isConnected: boolean
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**That's it!** Just 4 methods + 1 property.
|
|
140
|
+
|
|
141
|
+
### GenericProvider Class
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
class GenericProvider extends Observable<string> {
|
|
145
|
+
// Properties
|
|
146
|
+
readonly doc: Y.Doc
|
|
147
|
+
readonly transport: Transport
|
|
148
|
+
readonly awareness: Awareness
|
|
149
|
+
readonly pubsub: PubSubChannel
|
|
150
|
+
readonly status: ConnectionStatus
|
|
151
|
+
readonly connected: boolean
|
|
152
|
+
readonly synced: boolean
|
|
153
|
+
|
|
154
|
+
// Methods
|
|
155
|
+
constructor(doc: Y.Doc, transport: Transport, options?: {
|
|
156
|
+
awareness?: Awareness
|
|
157
|
+
syncInterval?: number // Auto-sync interval in ms (default: 5000, set 0 to disable)
|
|
158
|
+
verifyUpdates?: boolean // Send hash with each update for fast desync detection (default: true)
|
|
159
|
+
batchUpdates?: number // Batch/debounce updates in ms (default: 0 = disabled, recommended: 50-200)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
connect(config: ConnectionConfig): Promise<void>
|
|
163
|
+
disconnect(): void
|
|
164
|
+
destroy(): void
|
|
165
|
+
syncNow(): void // Force immediate sync (useful after network interruptions)
|
|
166
|
+
|
|
167
|
+
// Events (inherited from Observable)
|
|
168
|
+
on(event: 'status', callback: (status: ConnectionStatus) => void): void
|
|
169
|
+
on(event: 'synced', callback: (synced: boolean) => void): void
|
|
170
|
+
off(event: string, callback: Function): void
|
|
171
|
+
emit(event: string, args: any[]): void
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
class PubSubChannel extends Observable<string> {
|
|
175
|
+
// Subscribe to a topic
|
|
176
|
+
subscribe(topic: string, callback: (message: any) => void): () => void
|
|
177
|
+
|
|
178
|
+
// Publish to a topic
|
|
179
|
+
publish(topic: string, message: any): void
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Events
|
|
184
|
+
|
|
185
|
+
- **`status`**: Emitted when connection status changes
|
|
186
|
+
- `{ state: 'disconnected' }`
|
|
187
|
+
- `{ state: 'connecting' }`
|
|
188
|
+
- `{ state: 'connected' }`
|
|
189
|
+
- `{ state: 'error', error: Error }`
|
|
190
|
+
|
|
191
|
+
- **`synced`**: Emitted when initial sync completes
|
|
192
|
+
- `true` when synced
|
|
193
|
+
- `false` when connection lost
|
|
194
|
+
|
|
195
|
+
## Built-in Features
|
|
196
|
+
|
|
197
|
+
### ✅ Automatic Document Sync
|
|
198
|
+
All changes to the Yjs document are automatically sent through your transport.
|
|
199
|
+
|
|
200
|
+
### ✅ Awareness Protocol
|
|
201
|
+
Presence information (cursors, users online, etc.) is handled automatically.
|
|
202
|
+
|
|
203
|
+
### ✅ State Vector Sync
|
|
204
|
+
Efficient synchronization using Yjs state vectors - only missing data is transmitted.
|
|
205
|
+
|
|
206
|
+
### ✅ Loop Prevention
|
|
207
|
+
Transaction origins prevent infinite update loops.
|
|
208
|
+
|
|
209
|
+
### ✅ Reconnection Ready
|
|
210
|
+
Can disconnect and reconnect without recreating the provider.
|
|
211
|
+
|
|
212
|
+
### ✅ Pub/Sub Channel
|
|
213
|
+
Built-in topic-based messaging for real-time communication that doesn't need CRDT properties (chat, notifications, RPC calls, etc).
|
|
214
|
+
|
|
215
|
+
## Pub/Sub Channel
|
|
216
|
+
|
|
217
|
+
In addition to CRDT synchronization, the provider includes a pub/sub channel for real-time messaging:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Subscribe to a topic
|
|
221
|
+
const unsubscribe = provider.pubsub.subscribe('chat', (message: any) => {
|
|
222
|
+
console.log('Chat message:', message)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Publish to a topic
|
|
226
|
+
provider.pubsub.publish('chat', {
|
|
227
|
+
user: 'Alice',
|
|
228
|
+
text: 'Hello!',
|
|
229
|
+
timestamp: Date.now()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Subscribe to all topics (wildcard)
|
|
233
|
+
provider.pubsub.subscribe('*', (message: any) => {
|
|
234
|
+
console.log('All messages:', message)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Unsubscribe when done
|
|
238
|
+
unsubscribe()
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Use Cases for Pub/Sub
|
|
242
|
+
|
|
243
|
+
- **Chat Messages**: Ephemeral messages that don't need to be stored in CRDT
|
|
244
|
+
- **Notifications**: User joined, left, status changes
|
|
245
|
+
- **RPC Calls**: Request/response patterns between clients
|
|
246
|
+
- **Events**: Button clicks, interactions, temporary states
|
|
247
|
+
- **Presence**: Real-time updates that complement awareness
|
|
248
|
+
|
|
249
|
+
### Pub/Sub vs Awareness
|
|
250
|
+
|
|
251
|
+
- **Awareness**: For user presence data (cursors, names, colors) - maintained by Yjs
|
|
252
|
+
- **Pub/Sub**: For custom real-time messaging - you control the schema
|
|
253
|
+
|
|
254
|
+
### Example: Chat System
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Setup
|
|
258
|
+
const doc = new Y.Doc()
|
|
259
|
+
const transport = new WebSocketTransport()
|
|
260
|
+
const provider = new GenericProvider(doc, transport)
|
|
261
|
+
|
|
262
|
+
await provider.connect({ room: 'chat-room' })
|
|
263
|
+
|
|
264
|
+
// Subscribe to chat
|
|
265
|
+
provider.pubsub.subscribe('chat', (msg) => {
|
|
266
|
+
displayMessage(msg.user, msg.text)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Send message
|
|
270
|
+
function sendMessage(text: string) {
|
|
271
|
+
provider.pubsub.publish('chat', {
|
|
272
|
+
user: getCurrentUser(),
|
|
273
|
+
text: text,
|
|
274
|
+
timestamp: Date.now()
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// The Yjs doc can still be used for persistent chat history if needed
|
|
279
|
+
const yhistory = doc.getArray('chatHistory')
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Example Implementations
|
|
283
|
+
|
|
284
|
+
See `examples.ts` for complete implementations of:
|
|
285
|
+
- **WebSocketTransport**: Real-time WebSocket communication
|
|
286
|
+
- **PubNubTransport**: Pub/sub messaging
|
|
287
|
+
- **IndexedDBTransport**: Local persistence (acts as a "transport")
|
|
288
|
+
|
|
289
|
+
## How It Works
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
┌─────────────────────────────────────────────┐
|
|
293
|
+
│ Your Application │
|
|
294
|
+
│ (Uses Y.Doc, Y.Text, Y.Array, etc.) │
|
|
295
|
+
└──────────────────┬──────────────────────────┘
|
|
296
|
+
│
|
|
297
|
+
│ Automatic sync
|
|
298
|
+
▼
|
|
299
|
+
┌─────────────────────────────────────────────┐
|
|
300
|
+
│ GenericProvider │
|
|
301
|
+
│ • Listens to doc changes │
|
|
302
|
+
│ • Encodes updates with y-protocols │
|
|
303
|
+
│ • Manages awareness │
|
|
304
|
+
│ • Handles sync protocol │
|
|
305
|
+
└──────────────────┬──────────────────────────┘
|
|
306
|
+
│
|
|
307
|
+
│ send(Uint8Array)
|
|
308
|
+
│ onMessage(callback)
|
|
309
|
+
▼
|
|
310
|
+
┌─────────────────────────────────────────────┐
|
|
311
|
+
│ Your Transport │
|
|
312
|
+
│ • WebSocket.send() │
|
|
313
|
+
│ • PubNub.publish() │
|
|
314
|
+
│ • WebRTC.sendData() │
|
|
315
|
+
│ • IndexedDB.put() │
|
|
316
|
+
│ • etc. │
|
|
317
|
+
└─────────────────────────────────────────────┘
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Advanced Usage
|
|
321
|
+
|
|
322
|
+
### Custom Awareness
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { Awareness } from 'y-protocols/awareness'
|
|
326
|
+
|
|
327
|
+
const awareness = new Awareness(doc)
|
|
328
|
+
const provider = new GenericProvider(doc, transport, { awareness })
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Connection Configuration
|
|
332
|
+
|
|
333
|
+
Pass any config your transport needs:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
await provider.connect({
|
|
337
|
+
room: 'my-room',
|
|
338
|
+
url: 'wss://server.com',
|
|
339
|
+
apiKey: 'secret-key',
|
|
340
|
+
timeout: 5000,
|
|
341
|
+
// ... any custom fields
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Async Transports
|
|
346
|
+
|
|
347
|
+
Your `send()` method can return a Promise:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
async send(data: Uint8Array): Promise<void> {
|
|
351
|
+
await this.backend.publish(data)
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Error Handling
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
provider.on('status', (status) => {
|
|
359
|
+
if (status.state === 'error') {
|
|
360
|
+
console.error('Connection error:', status.error)
|
|
361
|
+
// Implement retry logic
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Force Re-Sync
|
|
367
|
+
|
|
368
|
+
After network interruptions or when you suspect clients are out of sync, trigger an immediate sync:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// Manual re-sync
|
|
372
|
+
provider.syncNow()
|
|
373
|
+
|
|
374
|
+
// Re-sync when network comes back online
|
|
375
|
+
window.addEventListener('online', () => {
|
|
376
|
+
console.log('Network restored')
|
|
377
|
+
provider.syncNow()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Re-sync after connection issues
|
|
381
|
+
provider.on('status', (status) => {
|
|
382
|
+
if (status.state === 'connected') {
|
|
383
|
+
// Give transport a moment to stabilize
|
|
384
|
+
setTimeout(() => provider.syncNow(), 100)
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**What `syncNow()` does:**
|
|
390
|
+
1. Sends the full local document state to all peers (ensures offline changes are transmitted)
|
|
391
|
+
2. Sends a sync request (SyncStep1) to get missing updates from others
|
|
392
|
+
3. Broadcasts the current awareness state
|
|
393
|
+
|
|
394
|
+
This is especially important after a client has been offline and made local changes, as it ensures those changes are sent to other clients when reconnecting.
|
|
395
|
+
|
|
396
|
+
### Automatic Periodic Sync
|
|
397
|
+
|
|
398
|
+
By default, the provider automatically calls `syncNow()` every 5 seconds to recover from packet loss or intermittent network issues:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// Default behavior - auto-sync every 5 seconds
|
|
402
|
+
const provider = new GenericProvider(doc, transport)
|
|
403
|
+
|
|
404
|
+
// Faster recovery for unreliable networks
|
|
405
|
+
const provider = new GenericProvider(doc, transport, {
|
|
406
|
+
syncInterval: 2000, // Retry every 2 seconds
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// Disable automatic sync (not recommended)
|
|
410
|
+
const provider = new GenericProvider(doc, transport, {
|
|
411
|
+
syncInterval: 0, // Manual sync only
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Why automatic sync is important:**
|
|
416
|
+
- Yjs requires multi-step handshakes (SyncStep1 → SyncStep2 → Updates)
|
|
417
|
+
- If any message is lost due to packet loss, sync stalls
|
|
418
|
+
- Periodic retries ensure eventual consistency even on unreliable networks
|
|
419
|
+
- Performance impact is minimal (only sends if there are changes)
|
|
420
|
+
|
|
421
|
+
For most production scenarios, the default 5-second interval provides good resilience without excessive traffic. For testing with simulated packet loss, use a shorter interval (e.g., 2 seconds).
|
|
422
|
+
|
|
423
|
+
### Update Batching (Debouncing)
|
|
424
|
+
|
|
425
|
+
By default, every document change triggers an immediate network transmission. For performance optimization, you can enable **update batching** (also called **debouncing**):
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// Default behavior - send updates immediately
|
|
429
|
+
const provider = new GenericProvider(doc, transport)
|
|
430
|
+
|
|
431
|
+
// Batch updates: collect changes and send after 100ms of inactivity
|
|
432
|
+
const provider = new GenericProvider(doc, transport, {
|
|
433
|
+
batchUpdates: 100, // milliseconds
|
|
434
|
+
})
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**How it works:**
|
|
438
|
+
1. User types "hello" (5 keystrokes)
|
|
439
|
+
2. Instead of 5 separate network messages, wait 100ms after last keystroke
|
|
440
|
+
3. Send **one combined update** containing all changes
|
|
441
|
+
4. Result: 1 message instead of 5
|
|
442
|
+
|
|
443
|
+
**Benefits:**
|
|
444
|
+
- ✅ **Reduces network traffic** by ~70-90% during rapid typing
|
|
445
|
+
- ✅ **Lower bandwidth usage** - fewer messages, less overhead per message
|
|
446
|
+
- ✅ **Better performance** on slow or metered networks
|
|
447
|
+
- ✅ **Still feels instant** - 50-200ms delay is imperceptible to users
|
|
448
|
+
- ✅ Yjs automatically merges sequential updates efficiently
|
|
449
|
+
|
|
450
|
+
**Recommended values:**
|
|
451
|
+
- `0` (default): Immediate transmission, no batching - best for slow networks or real-time cursors
|
|
452
|
+
- `50-100ms`: Good balance for most collaborative editing (typing, formatting)
|
|
453
|
+
- `200ms`: Maximum delay while still feeling responsive
|
|
454
|
+
- `500ms+`: Not recommended (users notice lag)
|
|
455
|
+
|
|
456
|
+
**When to use:**
|
|
457
|
+
- ✅ Collaborative text editing (typing)
|
|
458
|
+
- ✅ Drawing/whiteboard applications (mouse movements)
|
|
459
|
+
- ✅ Metered or slow networks
|
|
460
|
+
- ✅ Mobile/battery-conscious applications
|
|
461
|
+
|
|
462
|
+
**When NOT to use:**
|
|
463
|
+
- ❌ Real-time cursor tracking (needs immediate updates)
|
|
464
|
+
- ❌ Turn-based interactions (each change is discrete)
|
|
465
|
+
- ❌ Ultra-low latency requirements (<50ms)
|
|
466
|
+
|
|
467
|
+
**Example - Optimized collaborative editor:**
|
|
468
|
+
```typescript
|
|
469
|
+
const provider = new GenericProvider(doc, transport, {
|
|
470
|
+
batchUpdates: 100, // Batch updates for efficiency
|
|
471
|
+
verifyUpdates: true, // Hash verification for reliability
|
|
472
|
+
syncInterval: 5000, // Periodic backup sync
|
|
473
|
+
})
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Fast Desync Detection
|
|
477
|
+
|
|
478
|
+
By default, the provider includes a **document hash with every update** for immediate detection of sync problems:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
// Default behavior - hash verification enabled
|
|
482
|
+
const provider = new GenericProvider(doc, transport, {
|
|
483
|
+
verifyUpdates: true, // Default: true
|
|
484
|
+
})
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**How it works:**
|
|
488
|
+
1. When sending an update, include hash of document state after the update
|
|
489
|
+
2. Receiver applies update, computes own hash, compares
|
|
490
|
+
3. If hashes don't match → **immediately trigger re-sync** (no waiting!)
|
|
491
|
+
4. Much faster than waiting for periodic sync interval
|
|
492
|
+
|
|
493
|
+
**Exponential backoff:**
|
|
494
|
+
- First mismatch: retry after 10ms
|
|
495
|
+
- Second mismatch: retry after 50ms
|
|
496
|
+
- Third mismatch: retry after 250ms
|
|
497
|
+
- Fourth mismatch: retry after 1.25s
|
|
498
|
+
- Further mismatches: retry after 6.25s, capped at 10s
|
|
499
|
+
- If stable for 10 seconds, counter resets
|
|
500
|
+
- Prevents "sync storm" if clients persistently disagree
|
|
501
|
+
|
|
502
|
+
**Performance:**
|
|
503
|
+
- Hash computation is fast (~1ms for typical documents)
|
|
504
|
+
- Only happens when updates occur (not constantly)
|
|
505
|
+
- Adds minimal bandwidth (~4 bytes per update message)
|
|
506
|
+
|
|
507
|
+
**When to disable:**
|
|
508
|
+
```typescript
|
|
509
|
+
// Disable for lowest possible bandwidth usage
|
|
510
|
+
const provider = new GenericProvider(doc, transport, {
|
|
511
|
+
verifyUpdates: false, // Rely only on periodic sync
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Only disable if you have a very reliable transport (no packet loss) or bandwidth is extremely constrained.
|
|
516
|
+
|
|
517
|
+
**Comparison:**
|
|
518
|
+
- **Without verification**: Lost message → wait up to 5 seconds → retry
|
|
519
|
+
- **With verification** (default): Lost message → next update detects mismatch → immediate retry
|
|
520
|
+
|
|
521
|
+
This makes sync much more responsive on unreliable networks!
|
|
522
|
+
|
|
523
|
+
### Sequence Numbers for Ordering
|
|
524
|
+
|
|
525
|
+
When `verifyUpdates` is enabled, the provider automatically includes **sequence numbers** with each update for total ordering guarantees:
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// Automatic with verifyUpdates enabled (default)
|
|
529
|
+
const provider = new GenericProvider(doc, transport, {
|
|
530
|
+
verifyUpdates: true, // Includes sequence numbers
|
|
531
|
+
})
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**How it works:**
|
|
535
|
+
1. Each client maintains its own sequence counter (increments with each update sent)
|
|
536
|
+
2. Updates include: `[seqNum, clientID, update, hash]`
|
|
537
|
+
3. Receiver tracks last seen sequence number for each remote client
|
|
538
|
+
4. Detects and handles:
|
|
539
|
+
- **Duplicates**: seqNum ≤ last seen → skip (already processed)
|
|
540
|
+
- **Out-of-order**: seqNum arrives before earlier ones → log warning
|
|
541
|
+
- **Gaps**: seqNum jumps (e.g., 5 → 8) → log packet loss, Yjs handles recovery
|
|
542
|
+
|
|
543
|
+
**Benefits:**
|
|
544
|
+
- **Duplicate detection**: Prevents processing the same update twice
|
|
545
|
+
- **Order verification**: Confirms updates arrive in causal order
|
|
546
|
+
- **Gap detection**: Identifies packet loss immediately (diagnostic)
|
|
547
|
+
- **Debugging**: Easier to trace message flow and diagnose issues
|
|
548
|
+
|
|
549
|
+
**Example console output:**
|
|
550
|
+
```
|
|
551
|
+
[GenericProvider] Sequence gap detected from client 423891: expected 12, got 15 (gap of 3 messages)
|
|
552
|
+
[GenericProvider] Duplicate update detected from client 892301: seqNum 8 <= lastSeen 10
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Performance:**
|
|
556
|
+
- Adds ~2-8 bytes per update (variable-length encoding)
|
|
557
|
+
- O(1) duplicate detection (hash map lookup)
|
|
558
|
+
- Negligible CPU overhead
|
|
559
|
+
|
|
560
|
+
**Why this matters:**
|
|
561
|
+
- In unreliable networks (high packet loss), updates can arrive multiple times via retries
|
|
562
|
+
- Without sequence numbers, duplicate updates would cause unnecessary reprocessing
|
|
563
|
+
- With sequence numbers, duplicates are instantly detected and skipped
|
|
564
|
+
- Provides diagnostic information about network quality
|
|
565
|
+
|
|
566
|
+
### Built-in Rate Limiting
|
|
567
|
+
|
|
568
|
+
The provider includes **automatic rate limiting** on sync requests to prevent spam and protect against malicious or buggy clients:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
// Default behavior - built-in rate limiting
|
|
572
|
+
const provider = new GenericProvider(doc, transport)
|
|
573
|
+
// Max 20 sync requests per 10-second window
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**How it works:**
|
|
577
|
+
1. Tracks all sync requests (SyncStep1) sent within a 10-second sliding window
|
|
578
|
+
2. If more than 20 requests are attempted in that window, additional requests are dropped
|
|
579
|
+
3. Warning logged to console when rate limit is hit
|
|
580
|
+
4. Prevents "sync storm" scenarios where buggy code spams sync requests
|
|
581
|
+
|
|
582
|
+
**Protection against:**
|
|
583
|
+
- Buggy code with infinite retry loops
|
|
584
|
+
- Malicious clients attempting to flood the network
|
|
585
|
+
- Accidental DoS from misconfigured exponential backoff
|
|
586
|
+
- Network issues causing excessive retry attempts
|
|
587
|
+
|
|
588
|
+
**Why this is safe:**
|
|
589
|
+
- 20 requests per 10 seconds = 2 per second average (very generous)
|
|
590
|
+
- Normal operation uses far fewer requests (typically < 5 per 10 seconds)
|
|
591
|
+
- Combined with exponential backoff, prevents most abuse scenarios
|
|
592
|
+
- Doesn't affect normal document updates (only sync requests are limited)
|
|
593
|
+
|
|
594
|
+
The rate limiting is transparent and automatic - no configuration needed!
|
|
595
|
+
|
|
596
|
+
## Migration from Existing Providers
|
|
597
|
+
|
|
598
|
+
If you have existing providers (like your PubNub/P2PT implementations), wrap them:
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
class PubNubTransportAdapter implements Transport {
|
|
602
|
+
constructor(private oldProvider: OldPubNubSync) {}
|
|
603
|
+
|
|
604
|
+
async connect(config) {
|
|
605
|
+
return this.oldProvider.connect(config)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
send(data: Uint8Array) {
|
|
609
|
+
this.oldProvider.broadcast(true, data)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
onMessage(callback) {
|
|
613
|
+
this.oldProvider.onReceive = callback
|
|
614
|
+
return () => { this.oldProvider.onReceive = undefined }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ... etc
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Optional Monitoring Utilities
|
|
622
|
+
|
|
623
|
+
The GenericProvider includes **built-in hash verification** (enabled by default with `verifyUpdates: true`). This is sufficient for most use cases.
|
|
624
|
+
|
|
625
|
+
However, for **advanced diagnostics and monitoring**, the `SyncHealthMonitor` utility is available:
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
import { SyncHealthMonitor } from './sync-monitor'
|
|
629
|
+
|
|
630
|
+
const monitor = new SyncHealthMonitor(provider, {
|
|
631
|
+
checkInterval: 10000, // Check every 10 seconds
|
|
632
|
+
onDesync: (details) => {
|
|
633
|
+
// Send to error tracking, alerting system, etc.
|
|
634
|
+
console.error('Desync detected across peers!', details)
|
|
635
|
+
}
|
|
636
|
+
})
|
|
637
|
+
monitor.start()
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### When to use SyncHealthMonitor:
|
|
641
|
+
- ✅ You need to **monitor ALL peers simultaneously** via pub/sub broadcasts
|
|
642
|
+
- ✅ You want **periodic health checks** for diagnostics/alerting
|
|
643
|
+
- ✅ You need separate monitoring infrastructure from core sync
|
|
644
|
+
- ✅ Production monitoring and error tracking
|
|
645
|
+
|
|
646
|
+
### When to use built-in verification (default):
|
|
647
|
+
- ✅ **Fast desync detection** (immediate, on every update)
|
|
648
|
+
- ✅ **Zero configuration** (automatic)
|
|
649
|
+
- ✅ Exponential backoff, rate limiting, sequence numbers
|
|
650
|
+
- ✅ Recommended for most applications
|
|
651
|
+
|
|
652
|
+
**Key difference:**
|
|
653
|
+
- Built-in: Point-to-point verification (sender ↔ receiver) on each update
|
|
654
|
+
- SyncHealthMonitor: Periodic broadcasts across all peers for diagnostics
|
|
655
|
+
|
|
656
|
+
Most applications should use the built-in verification. Use SyncHealthMonitor only if you need additional monitoring for alerting/diagnostics.
|
|
657
|
+
|
|
658
|
+
## License
|
|
659
|
+
|
|
660
|
+
Same as LiaScript (BSD-3-Clause)
|