@edryslabs/genericprovider 1.0.4 → 1.5.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.
Files changed (64) hide show
  1. package/README.md +129 -5
  2. package/dist/index.d.ts +903 -60
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2723 -428
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib.d.ts +1 -0
  7. package/dist/lib.d.ts.map +1 -1
  8. package/dist/lib.js +3 -0
  9. package/dist/lib.js.map +1 -1
  10. package/dist/providers/ably/index.d.ts +203 -0
  11. package/dist/providers/ably/index.d.ts.map +1 -0
  12. package/dist/providers/ably/index.js +561 -0
  13. package/dist/providers/ably/index.js.map +1 -0
  14. package/dist/providers/chunking.d.ts +26 -0
  15. package/dist/providers/chunking.d.ts.map +1 -0
  16. package/dist/providers/chunking.js +52 -0
  17. package/dist/providers/chunking.js.map +1 -0
  18. package/dist/providers/dummy/index.d.ts +138 -2
  19. package/dist/providers/dummy/index.d.ts.map +1 -1
  20. package/dist/providers/dummy/index.js +266 -4
  21. package/dist/providers/dummy/index.js.map +1 -1
  22. package/dist/providers/gun/index.d.ts +11 -4
  23. package/dist/providers/gun/index.d.ts.map +1 -1
  24. package/dist/providers/gun/index.js +104 -22
  25. package/dist/providers/gun/index.js.map +1 -1
  26. package/dist/providers/indexeddb/index.d.ts +44 -20
  27. package/dist/providers/indexeddb/index.d.ts.map +1 -1
  28. package/dist/providers/indexeddb/index.js +85 -71
  29. package/dist/providers/indexeddb/index.js.map +1 -1
  30. package/dist/providers/matrix/index.d.ts +6 -1
  31. package/dist/providers/matrix/index.d.ts.map +1 -1
  32. package/dist/providers/matrix/index.js +44 -6
  33. package/dist/providers/matrix/index.js.map +1 -1
  34. package/dist/providers/nostr/index.d.ts +69 -5
  35. package/dist/providers/nostr/index.d.ts.map +1 -1
  36. package/dist/providers/nostr/index.js +204 -36
  37. package/dist/providers/nostr/index.js.map +1 -1
  38. package/dist/providers/peerjs/index.d.ts +11 -1
  39. package/dist/providers/peerjs/index.d.ts.map +1 -1
  40. package/dist/providers/peerjs/index.js +32 -2
  41. package/dist/providers/peerjs/index.js.map +1 -1
  42. package/dist/providers/pubnub/index.d.ts +35 -1
  43. package/dist/providers/pubnub/index.d.ts.map +1 -1
  44. package/dist/providers/pubnub/index.js +56 -35
  45. package/dist/providers/pubnub/index.js.map +1 -1
  46. package/dist/providers/simple-peer/index.d.ts +10 -15
  47. package/dist/providers/simple-peer/index.d.ts.map +1 -1
  48. package/dist/providers/simple-peer/index.js +81 -109
  49. package/dist/providers/simple-peer/index.js.map +1 -1
  50. package/dist/providers/supabase/index.d.ts +43 -1
  51. package/dist/providers/supabase/index.d.ts.map +1 -1
  52. package/dist/providers/supabase/index.js +211 -18
  53. package/dist/providers/supabase/index.js.map +1 -1
  54. package/dist/providers/trystero/index.d.ts +19 -1
  55. package/dist/providers/trystero/index.d.ts.map +1 -1
  56. package/dist/providers/trystero/index.js +37 -2
  57. package/dist/providers/trystero/index.js.map +1 -1
  58. package/dist/providers/websocket/index.d.ts +9 -1
  59. package/dist/providers/websocket/index.d.ts.map +1 -1
  60. package/dist/providers/websocket/index.js +7 -1
  61. package/dist/providers/websocket/index.js.map +1 -1
  62. package/dist/transport.d.ts +69 -16
  63. package/dist/transport.d.ts.map +1 -1
  64. package/package.json +35 -24
package/README.md CHANGED
@@ -18,6 +18,7 @@ Explore 9 different transport implementations:
18
18
  - **PubNub** - Cloud pub/sub with global CDN
19
19
  - **WebSocket** - Standard client-server (y-websocket compatible)
20
20
  - **Matrix** - Federated communication protocol
21
+ - **Ably** - Cloud pub/sub with global edge network
21
22
 
22
23
  ## Design Philosophy
23
24
 
@@ -129,14 +130,69 @@ interface Transport {
129
130
 
130
131
  // Data transmission
131
132
  send(data: Uint8Array): void | Promise<void>
132
- onMessage(callback: (data: Uint8Array) => void): () => void
133
+ onMessage(callback: (data: Uint8Array, from?: string) => void): () => void
133
134
 
134
135
  // Status
135
136
  readonly isConnected: boolean
137
+
138
+ // Optional - each one unlocks a cheaper code path when present
139
+ onPeerConnect?(callback: (peerId: string) => void): () => void
140
+ onPeerDisconnect?(callback: (peerId: string) => void): () => void
141
+ sendTo?(peerId: string, data: Uint8Array): void | Promise<void>
142
+ readonly preferredBatchMs?: number
143
+ readonly expectedRttMs?: number
144
+ readonly preferredCompressMinBytes?: number
145
+ readonly preferredAwarenessMs?: number
136
146
  }
137
147
  ```
138
148
 
139
- **That's it!** Just 4 methods + 1 property.
149
+ **That's it!** Just 4 methods + 1 property. The optional members:
150
+
151
+ - `onPeerConnect` - mesh transports fire it per newly opened peer channel so
152
+ the provider can send that peer a sync beacon right away (a full-state
153
+ push to every connection before round 5).
154
+ - `onPeerDisconnect` - the counterpart: a peer's channel closed, or the
155
+ backend's presence service reports it gone (peerjs, simple-peer,
156
+ trystero, PubNub implement it). The provider drops that peer's presence
157
+ at once instead of after the awareness lease, and lets the lease default
158
+ to 5 minutes instead of 30 s - which removes the 15 s presence renewal
159
+ every peer otherwise broadcasts (80 % of an idle room's traffic once the
160
+ beacons have backed off).
161
+ - `from` + `sendTo` - if your transport knows which peer a message came
162
+ from, pass that peer's id as the second callback argument and implement
163
+ `sendTo`. The provider then answers that peer's sync requests directly
164
+ (SyncStep2, acks, presence) instead of broadcasting the answer to the
165
+ room: about three unicast replies per join instead of every peer
166
+ answering everyone. Relays that only see a room leave both out and keep
167
+ today's broadcast behaviour.
168
+ - `preferredBatchMs` - default `batchUpdates` for transports with a high
169
+ per-message cost (HTTP polling, internally debounced relays).
170
+ - `expectedRttMs` - the round-trip time class you expect (e.g. 700 for a
171
+ Matrix homeserver). Seeds the provider's latency estimate so the first
172
+ join on a slow transport does not retry before the first replies can
173
+ have arrived; measured samples take over immediately.
174
+ - `preferredCompressMinBytes` - default `compressionThresholdBytes` for
175
+ transports that cap or bill message size (Ably, PubNub, Matrix, Nostr,
176
+ Supabase set 2048): a full-document push is compressed before it is
177
+ chunked. Same-version-room rule applies, as for every wire change.
178
+ - `preferredAwarenessMs` - default `awarenessInterval` for transports
179
+ whose backend rate-limits sends per user (Matrix sets 2000 against
180
+ Synapse's default 0.2 messages/s).
181
+
182
+ On a transport without `onPeerDisconnect` (Gun, Nostr, a plain WebSocket
183
+ relay) every peer still re-announces its presence every half lease so the
184
+ others do not drop it - with the default 30 s lease that is most of what
185
+ an idle room sends once the beacons have backed off. Setting
186
+ `awarenessTimeoutMs: 120000` in the app (the Gun, Nostr and WebSocket
187
+ playgrounds do) cuts those renewals by three quarters; the price is a
188
+ cursor that lingers up to 2 minutes after a tab is killed (clean closes
189
+ are still announced at once). Every peer of a room must use the same
190
+ value.
191
+
192
+ When a persistence provider (IndexedDB) shares the document, pass its
193
+ connect() promise as `connect({ room, waitFor })`: the first beacon then
194
+ says what is already on disk, the load is not re-broadcast, and the room
195
+ answers with nothing instead of the whole document.
140
196
 
141
197
  ### GenericProvider Class
142
198
 
@@ -155,8 +211,12 @@ class GenericProvider extends Observable<string> {
155
211
  constructor(doc: Y.Doc, transport: Transport, options?: {
156
212
  awareness?: Awareness
157
213
  syncInterval?: number // Auto-sync interval in ms (default: 5000, set 0 to disable)
214
+ idleBackoffEnabled?: boolean // Double the interval while the room is idle, up to idleBackoffMaxMs (default: true)
215
+ idleBackoffMaxMs?: number // Ceiling for the backed-off interval (default: 60000)
216
+ trickleK?: number // Skip a periodic beacon when this many equal digests were overheard since the last one (default: 1, 0 = off)
217
+ awarenessTimeoutMs?: number // Presence lease; renew after half of it (default: 30000, or 300000 when the transport has onPeerDisconnect)
158
218
  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)
219
+ batchUpdates?: number // Batch/debounce updates in ms (default: 0 = end of the current task, recommended: 50-200)
160
220
  })
161
221
 
162
222
  connect(config: ConnectionConfig): Promise<void>
@@ -199,6 +259,10 @@ All changes to the Yjs document are automatically sent through your transport.
199
259
 
200
260
  ### ✅ Awareness Protocol
201
261
  Presence information (cursors, users online, etc.) is handled automatically.
262
+ A joiner asks for the room's presence once; on a relay transport one peer
263
+ answers with the whole awareness table (the others stay silent when that
264
+ table carried their state), on a transport with `sendTo` each peer answers
265
+ the joiner directly. Nobody re-announces presence on a timer.
202
266
 
203
267
  ### ✅ State Vector Sync
204
268
  Efficient synchronization using Yjs state vectors - only missing data is transmitted.
@@ -286,6 +350,12 @@ See `examples.ts` for complete implementations of:
286
350
  - **PubNubTransport**: Pub/sub messaging
287
351
  - **IndexedDBTransport**: Local persistence (acts as a "transport")
288
352
 
353
+ When you combine a persistence provider with a network provider on the
354
+ same `Y.Doc`, connect the persistence provider first and wait for its
355
+ `synced` event before calling `connect()` on the network provider. The
356
+ network provider's first request then carries your real state vector and
357
+ the reply is only the tail you are missing, instead of the whole document.
358
+
289
359
  ## How It Works
290
360
 
291
361
  ```
@@ -352,6 +422,31 @@ async send(data: Uint8Array): Promise<void> {
352
422
  }
353
423
  ```
354
424
 
425
+ ### Persistence Transports
426
+
427
+ A transport that stores what it is given (IndexedDB, a Dexie table) is
428
+ handed every frame the provider sends - presence, beacons and requests
429
+ included. Store only the document: `extractDocUpdates(frame)` returns the
430
+ Yjs updates a frame carries (none for presence, beacons and requests), and
431
+ `frameDocUpdate(update)` wraps a stored (merged) update as the SyncStep2
432
+ the provider applies on load - `synced` fires, nothing is sent back.
433
+ `providers/indexeddb` is the reference; `connect({ waitFor })` pairs it
434
+ with a network provider on the same document.
435
+
436
+ ```typescript
437
+ import { extractDocUpdates, frameDocUpdate } from 'genericprovider'
438
+ import * as Y from 'yjs'
439
+
440
+ send(frame: Uint8Array) {
441
+ const updates = extractDocUpdates(frame)
442
+ if (updates.length > 0) this.rows.push(Y.mergeUpdates(updates))
443
+ }
444
+ onMessage(callback) {
445
+ if (this.rows.length > 0) callback(frameDocUpdate(Y.mergeUpdates(this.rows)))
446
+ return () => {}
447
+ }
448
+ ```
449
+
355
450
  ### Error Handling
356
451
 
357
452
  ```typescript
@@ -416,13 +511,42 @@ const provider = new GenericProvider(doc, transport, {
416
511
  - Yjs requires multi-step handshakes (SyncStep1 → SyncStep2 → Updates)
417
512
  - If any message is lost due to packet loss, sync stalls
418
513
  - Periodic retries ensure eventual consistency even on unreliable networks
419
- - Performance impact is minimal (only sends if there are changes)
514
+ - Performance impact is minimal: each tick sends one small digest beacon
515
+ (state vector plus a hash of the delete set), and peers answer only when
516
+ the sender is actually missing something. A fully synced room exchanges
517
+ beacons and nothing else - and since round 5 a peer that overheard a
518
+ beacon with exactly its own digest since its last tick stays silent at
519
+ the next one (Trickle, RFC 6206; `trickleK`), so an idle room sends one
520
+ or two beacons per interval in total instead of one per peer.
420
521
 
421
522
  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
523
 
524
+ While a room is idle the interval doubles after each quiet tick, up to
525
+ `idleBackoffMaxMs` (60 s by default); a local edit resets it to
526
+ `syncInterval` at once (remote updates and presence changes do not - a
527
+ peer that only listens has nothing a beacon would announce, so one typist
528
+ does not keep the whole room at the base cadence). A peer that missed the
529
+ last message before the room went quiet does not wait for its own
530
+ backed-off tick: the editor's next beacon, one base interval away at most,
531
+ makes it ask for the difference (after a short grace for messages still in
532
+ flight). `idleBackoffEnabled: false` keeps the fixed cadence.
533
+
534
+ > **Wire compatibility.** All peers in a room must run the same version of
535
+ > this library. Sync requests travel as a private digest message (state
536
+ > vector + delete-set hash, with join/ack/confirm flags), the connect-time
537
+ > full-state push has its own message type, and several messages are
538
+ > batched into one envelope; an older peer drops all of them unread. This
539
+ > has been the case since message batching landed and is not new to the
540
+ > digest format.
541
+
423
542
  ### Update Batching (Debouncing)
424
543
 
425
- By default, every document change triggers an immediate network transmission. For performance optimization, you can enable **update batching** (also called **debouncing**):
544
+ By default, every document change is sent at the end of the task that
545
+ produced it (a microtask, no timer): the several Yjs transactions one
546
+ input event can produce leave as one message, and the cursor update an
547
+ editor binding sets right after the text change rides in the same wire
548
+ message - one keystroke, one message. For further reduction you can
549
+ enable **update batching** (also called **debouncing**):
426
550
 
427
551
  ```typescript
428
552
  // Default behavior - send updates immediately