@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.
Files changed (63) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +660 -0
  3. package/dist/index.d.ts +323 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1001 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib.d.ts +36 -0
  8. package/dist/lib.d.ts.map +1 -0
  9. package/dist/lib.js +37 -0
  10. package/dist/lib.js.map +1 -0
  11. package/dist/providers/dummy/index.d.ts +226 -0
  12. package/dist/providers/dummy/index.d.ts.map +1 -0
  13. package/dist/providers/dummy/index.js +326 -0
  14. package/dist/providers/dummy/index.js.map +1 -0
  15. package/dist/providers/gun/index.d.ts +269 -0
  16. package/dist/providers/gun/index.d.ts.map +1 -0
  17. package/dist/providers/gun/index.js +683 -0
  18. package/dist/providers/gun/index.js.map +1 -0
  19. package/dist/providers/indexeddb/index.d.ts +161 -0
  20. package/dist/providers/indexeddb/index.d.ts.map +1 -0
  21. package/dist/providers/indexeddb/index.js +369 -0
  22. package/dist/providers/indexeddb/index.js.map +1 -0
  23. package/dist/providers/matrix/index.d.ts +109 -0
  24. package/dist/providers/matrix/index.d.ts.map +1 -0
  25. package/dist/providers/matrix/index.js +329 -0
  26. package/dist/providers/matrix/index.js.map +1 -0
  27. package/dist/providers/nostr/index.d.ts +172 -0
  28. package/dist/providers/nostr/index.d.ts.map +1 -0
  29. package/dist/providers/nostr/index.js +280 -0
  30. package/dist/providers/nostr/index.js.map +1 -0
  31. package/dist/providers/peerjs/index.d.ts +231 -0
  32. package/dist/providers/peerjs/index.d.ts.map +1 -0
  33. package/dist/providers/peerjs/index.js +1038 -0
  34. package/dist/providers/peerjs/index.js.map +1 -0
  35. package/dist/providers/pubnub/index.d.ts +106 -0
  36. package/dist/providers/pubnub/index.d.ts.map +1 -0
  37. package/dist/providers/pubnub/index.js +357 -0
  38. package/dist/providers/pubnub/index.js.map +1 -0
  39. package/dist/providers/simple-peer/index.d.ts +253 -0
  40. package/dist/providers/simple-peer/index.d.ts.map +1 -0
  41. package/dist/providers/simple-peer/index.js +783 -0
  42. package/dist/providers/simple-peer/index.js.map +1 -0
  43. package/dist/providers/supabase/index.d.ts +80 -0
  44. package/dist/providers/supabase/index.d.ts.map +1 -0
  45. package/dist/providers/supabase/index.js +202 -0
  46. package/dist/providers/supabase/index.js.map +1 -0
  47. package/dist/providers/trystero/index.d.ts +181 -0
  48. package/dist/providers/trystero/index.d.ts.map +1 -0
  49. package/dist/providers/trystero/index.js +187 -0
  50. package/dist/providers/trystero/index.js.map +1 -0
  51. package/dist/providers/websocket/index.d.ts +92 -0
  52. package/dist/providers/websocket/index.d.ts.map +1 -0
  53. package/dist/providers/websocket/index.js +272 -0
  54. package/dist/providers/websocket/index.js.map +1 -0
  55. package/dist/sync-monitor.d.ts +90 -0
  56. package/dist/sync-monitor.d.ts.map +1 -0
  57. package/dist/sync-monitor.js +149 -0
  58. package/dist/sync-monitor.js.map +1 -0
  59. package/dist/transport.d.ts +116 -0
  60. package/dist/transport.d.ts.map +1 -0
  61. package/dist/transport.js +2 -0
  62. package/dist/transport.js.map +1 -0
  63. 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)