@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.
- package/README.md +129 -5
- package/dist/index.d.ts +903 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2723 -428
- package/dist/index.js.map +1 -1
- package/dist/lib.d.ts +1 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +3 -0
- package/dist/lib.js.map +1 -1
- package/dist/providers/ably/index.d.ts +203 -0
- package/dist/providers/ably/index.d.ts.map +1 -0
- package/dist/providers/ably/index.js +561 -0
- package/dist/providers/ably/index.js.map +1 -0
- package/dist/providers/chunking.d.ts +26 -0
- package/dist/providers/chunking.d.ts.map +1 -0
- package/dist/providers/chunking.js +52 -0
- package/dist/providers/chunking.js.map +1 -0
- package/dist/providers/dummy/index.d.ts +138 -2
- package/dist/providers/dummy/index.d.ts.map +1 -1
- package/dist/providers/dummy/index.js +266 -4
- package/dist/providers/dummy/index.js.map +1 -1
- package/dist/providers/gun/index.d.ts +11 -4
- package/dist/providers/gun/index.d.ts.map +1 -1
- package/dist/providers/gun/index.js +104 -22
- package/dist/providers/gun/index.js.map +1 -1
- package/dist/providers/indexeddb/index.d.ts +44 -20
- package/dist/providers/indexeddb/index.d.ts.map +1 -1
- package/dist/providers/indexeddb/index.js +85 -71
- package/dist/providers/indexeddb/index.js.map +1 -1
- package/dist/providers/matrix/index.d.ts +6 -1
- package/dist/providers/matrix/index.d.ts.map +1 -1
- package/dist/providers/matrix/index.js +44 -6
- package/dist/providers/matrix/index.js.map +1 -1
- package/dist/providers/nostr/index.d.ts +69 -5
- package/dist/providers/nostr/index.d.ts.map +1 -1
- package/dist/providers/nostr/index.js +204 -36
- package/dist/providers/nostr/index.js.map +1 -1
- package/dist/providers/peerjs/index.d.ts +11 -1
- package/dist/providers/peerjs/index.d.ts.map +1 -1
- package/dist/providers/peerjs/index.js +32 -2
- package/dist/providers/peerjs/index.js.map +1 -1
- package/dist/providers/pubnub/index.d.ts +35 -1
- package/dist/providers/pubnub/index.d.ts.map +1 -1
- package/dist/providers/pubnub/index.js +56 -35
- package/dist/providers/pubnub/index.js.map +1 -1
- package/dist/providers/simple-peer/index.d.ts +10 -15
- package/dist/providers/simple-peer/index.d.ts.map +1 -1
- package/dist/providers/simple-peer/index.js +81 -109
- package/dist/providers/simple-peer/index.js.map +1 -1
- package/dist/providers/supabase/index.d.ts +43 -1
- package/dist/providers/supabase/index.d.ts.map +1 -1
- package/dist/providers/supabase/index.js +211 -18
- package/dist/providers/supabase/index.js.map +1 -1
- package/dist/providers/trystero/index.d.ts +19 -1
- package/dist/providers/trystero/index.d.ts.map +1 -1
- package/dist/providers/trystero/index.js +37 -2
- package/dist/providers/trystero/index.js.map +1 -1
- package/dist/providers/websocket/index.d.ts +9 -1
- package/dist/providers/websocket/index.d.ts.map +1 -1
- package/dist/providers/websocket/index.js +7 -1
- package/dist/providers/websocket/index.js.map +1 -1
- package/dist/transport.d.ts +69 -16
- package/dist/transport.d.ts.map +1 -1
- 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 =
|
|
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
|
|
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
|
|
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
|