@trystero-p2p/torrent 0.24.0 → 0.25.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/README.md +187 -186
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +64 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -142,21 +142,20 @@ Supabase, it should be your project URL.
|
|
|
142
142
|
Listen for peers joining the room:
|
|
143
143
|
|
|
144
144
|
```js
|
|
145
|
-
room.onPeerJoin
|
|
145
|
+
room.onPeerJoin = peerId => console.log(`${peerId} joined`)
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
Listen for peers leaving the room:
|
|
149
149
|
|
|
150
150
|
```js
|
|
151
|
-
room.onPeerLeave
|
|
151
|
+
room.onPeerLeave = peerId => console.log(`${peerId} left`)
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
Listen for peers sending their audio/video streams:
|
|
155
155
|
|
|
156
156
|
```js
|
|
157
|
-
room.onPeerStream(
|
|
158
|
-
(
|
|
159
|
-
)
|
|
157
|
+
room.onPeerStream = (stream, peerId) =>
|
|
158
|
+
(peerElements[peerId].video.srcObject = stream)
|
|
160
159
|
```
|
|
161
160
|
|
|
162
161
|
To unsubscribe from events, leave the room:
|
|
@@ -188,20 +187,61 @@ room.addStream(stream)
|
|
|
188
187
|
Send and subscribe to custom peer-to-peer actions:
|
|
189
188
|
|
|
190
189
|
```js
|
|
191
|
-
const
|
|
190
|
+
const drink = room.makeAction('drink')
|
|
192
191
|
|
|
193
192
|
// buy drink for a friend
|
|
194
|
-
|
|
193
|
+
drink.send({drink: 'negroni', withIce: true}, {target: friendId})
|
|
195
194
|
|
|
196
|
-
// buy round for the house
|
|
197
|
-
|
|
195
|
+
// buy round for the house
|
|
196
|
+
drink.send({drink: 'mezcal', withIce: false})
|
|
198
197
|
|
|
199
198
|
// listen for drinks sent to you
|
|
200
|
-
|
|
199
|
+
drink.onMessage = (data, {peerId}) =>
|
|
201
200
|
console.log(
|
|
202
201
|
`got a ${data.drink} with${data.withIce ? '' : 'out'} ice from ${peerId}`
|
|
203
202
|
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Actions can also use request/response semantics:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
const isEven = room.makeAction('is-even', {
|
|
209
|
+
kind: 'request',
|
|
210
|
+
onRequest: n => n % 2 === 0
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const result = await isEven.request(42, {
|
|
214
|
+
target: friendId,
|
|
215
|
+
timeoutMs: 1000
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
To ask multiple peers at once, use `requestMany()`. It resolves with a
|
|
220
|
+
peer-labeled result for every target, while `onResult` lets you react as each
|
|
221
|
+
peer answers:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const availability = room.makeAction('availability', {
|
|
225
|
+
kind: 'request',
|
|
226
|
+
onRequest: ({date}) => calendar.isFree(date)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const results = await availability.requestMany(
|
|
230
|
+
{date: '2026-05-04'},
|
|
231
|
+
{
|
|
232
|
+
targets: teammateIds,
|
|
233
|
+
timeoutMs: 1000,
|
|
234
|
+
onResult: result => {
|
|
235
|
+
if (result.status === 'fulfilled') {
|
|
236
|
+
updateAvailabilityBadge(result.peerId, result.value)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
204
240
|
)
|
|
241
|
+
|
|
242
|
+
const freePeers = results
|
|
243
|
+
.filter(result => result.status === 'fulfilled' && result.value)
|
|
244
|
+
.map(result => result.peerId)
|
|
205
245
|
```
|
|
206
246
|
|
|
207
247
|
If you're using TypeScript, you can add a type hint to the action:
|
|
@@ -209,42 +249,40 @@ If you're using TypeScript, you can add a type hint to the action:
|
|
|
209
249
|
```typescript
|
|
210
250
|
type CursorMove = {x: number; y: number}
|
|
211
251
|
|
|
212
|
-
const
|
|
252
|
+
const cursor = room.makeAction<CursorMove>('cursor-move')
|
|
213
253
|
```
|
|
214
254
|
|
|
215
255
|
You can also use actions to send binary data, like images:
|
|
216
256
|
|
|
217
257
|
```js
|
|
218
|
-
const
|
|
258
|
+
const pic = room.makeAction('pic')
|
|
219
259
|
|
|
220
260
|
// blobs are automatically handled, as are any form of TypedArray
|
|
221
|
-
canvas.toBlob(blob =>
|
|
261
|
+
canvas.toBlob(blob => pic.send(blob))
|
|
222
262
|
|
|
223
263
|
// binary data is received as raw ArrayBuffers so your handling code should
|
|
224
264
|
// interpret it in a way that makes sense
|
|
225
|
-
|
|
226
|
-
(
|
|
227
|
-
)
|
|
265
|
+
pic.onMessage = (data, {peerId}) =>
|
|
266
|
+
(imgs[peerId].src = URL.createObjectURL(new Blob([data])))
|
|
228
267
|
```
|
|
229
268
|
|
|
230
269
|
Let's say we want users to be able to name themselves:
|
|
231
270
|
|
|
232
271
|
```js
|
|
233
272
|
const idsToNames = {}
|
|
234
|
-
const
|
|
273
|
+
const name = room.makeAction('name')
|
|
235
274
|
|
|
236
275
|
// tell new peers your name when they connect
|
|
237
|
-
room.onPeerJoin
|
|
276
|
+
room.onPeerJoin = peerId => name.send('Oedipa', {target: peerId})
|
|
238
277
|
|
|
239
278
|
// listen for peers naming themselves
|
|
240
|
-
|
|
279
|
+
name.onMessage = (value, {peerId}) => (idsToNames[peerId] = value)
|
|
241
280
|
|
|
242
281
|
// tell all peers at once when your name changes
|
|
243
|
-
nameInput.addEventListener('change', e =>
|
|
282
|
+
nameInput.addEventListener('change', e => name.send(e.target.value))
|
|
244
283
|
|
|
245
|
-
room.onPeerLeave
|
|
284
|
+
room.onPeerLeave = peerId =>
|
|
246
285
|
console.log(`${idsToNames[peerId] || 'a weird stranger'} left`)
|
|
247
|
-
)
|
|
248
286
|
```
|
|
249
287
|
|
|
250
288
|
> Actions are smart and handle serialization and chunking for you behind the
|
|
@@ -270,10 +308,10 @@ const selfStream = await navigator.mediaDevices.getUserMedia({
|
|
|
270
308
|
room.addStream(selfStream)
|
|
271
309
|
|
|
272
310
|
// send stream to peers who join later
|
|
273
|
-
room.onPeerJoin
|
|
311
|
+
room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId})
|
|
274
312
|
|
|
275
313
|
// handle streams from other peers
|
|
276
|
-
room.onPeerStream(
|
|
314
|
+
room.onPeerStream = (stream, peerId) => {
|
|
277
315
|
// create an audio instance and set the incoming stream
|
|
278
316
|
const audio = new Audio()
|
|
279
317
|
audio.srcObject = stream
|
|
@@ -282,7 +320,7 @@ room.onPeerStream((stream, peerId) => {
|
|
|
282
320
|
// add the audio to peerAudios object if you want to address it for something
|
|
283
321
|
// later (volume, etc.)
|
|
284
322
|
peerAudios[peerId] = audio
|
|
285
|
-
}
|
|
323
|
+
}
|
|
286
324
|
```
|
|
287
325
|
|
|
288
326
|
Doing the same with video is similar, just be sure to add incoming streams to
|
|
@@ -292,7 +330,7 @@ video elements in the DOM:
|
|
|
292
330
|
const peerVideos = {}
|
|
293
331
|
const videoContainer = document.getElementById('videos')
|
|
294
332
|
|
|
295
|
-
room.onPeerStream(
|
|
333
|
+
room.onPeerStream = (stream, peerId) => {
|
|
296
334
|
let video = peerVideos[peerId]
|
|
297
335
|
|
|
298
336
|
// if this peer hasn't sent a stream before, create a video element
|
|
@@ -306,7 +344,7 @@ room.onPeerStream((stream, peerId) => {
|
|
|
306
344
|
|
|
307
345
|
video.srcObject = stream
|
|
308
346
|
peerVideos[peerId] = video
|
|
309
|
-
}
|
|
347
|
+
}
|
|
310
348
|
```
|
|
311
349
|
|
|
312
350
|
## Advanced
|
|
@@ -319,18 +357,17 @@ interpreted. Instead of manually adding metadata bytes to the buffer you can
|
|
|
319
357
|
simply pass a metadata argument in the sender action for your binary payload:
|
|
320
358
|
|
|
321
359
|
```js
|
|
322
|
-
const
|
|
360
|
+
const file = room.makeAction('file')
|
|
323
361
|
|
|
324
|
-
|
|
362
|
+
file.onMessage = (data, {peerId, metadata}) =>
|
|
325
363
|
console.log(
|
|
326
364
|
`got a file (${metadata.name}) from ${peerId} with type ${metadata.type}`,
|
|
327
365
|
data
|
|
328
366
|
)
|
|
329
|
-
)
|
|
330
367
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
368
|
+
file.send(buffer, {
|
|
369
|
+
metadata: {name: 'The Courierʼs Tragedy', type: 'application/pdf'}
|
|
370
|
+
})
|
|
334
371
|
```
|
|
335
372
|
|
|
336
373
|
### Action promises
|
|
@@ -340,7 +377,7 @@ sending. You can optionally use this to indicate to the user when a large
|
|
|
340
377
|
transfer is done.
|
|
341
378
|
|
|
342
379
|
```js
|
|
343
|
-
await
|
|
380
|
+
await file.send(amplePayload)
|
|
344
381
|
console.log('done sending to all peers')
|
|
345
382
|
```
|
|
346
383
|
|
|
@@ -352,30 +389,22 @@ a progress bar to the sender for large transfers. The callback is called with a
|
|
|
352
389
|
percentage value between 0 and 1 and the receiving peer's ID:
|
|
353
390
|
|
|
354
391
|
```js
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// metadata, which can also be null if you're only interested in the
|
|
361
|
-
// progress handler
|
|
362
|
-
{filename: 'paranoids.flac'},
|
|
363
|
-
// assuming each peer has a loading bar added to the DOM, its value is
|
|
364
|
-
// updated here
|
|
365
|
-
(percent, peerId) => (loadingBars[peerId].value = percent)
|
|
366
|
-
)
|
|
392
|
+
file.send(payload, {
|
|
393
|
+
target: [peerIdA, peerIdB, peerIdC],
|
|
394
|
+
metadata: {filename: 'paranoids.flac'},
|
|
395
|
+
onProgress: (percent, {peerId}) => (loadingBars[peerId].value = percent)
|
|
396
|
+
})
|
|
367
397
|
```
|
|
368
398
|
|
|
369
399
|
Similarly you can listen for progress events as a receiver like this:
|
|
370
400
|
|
|
371
401
|
```js
|
|
372
|
-
const
|
|
402
|
+
const file = room.makeAction('file')
|
|
373
403
|
|
|
374
|
-
|
|
404
|
+
file.onReceiveProgress = (percent, {peerId, metadata}) =>
|
|
375
405
|
console.log(
|
|
376
406
|
`${percent * 100}% done receiving ${metadata.filename} from ${peerId}`
|
|
377
407
|
)
|
|
378
|
-
)
|
|
379
408
|
```
|
|
380
409
|
|
|
381
410
|
Notice that any metadata is sent with progress events so you can show the
|
|
@@ -422,23 +451,22 @@ const trysteroConfig = {appId: 'thurn-und-taxis'}
|
|
|
422
451
|
|
|
423
452
|
export default function App({roomId}) {
|
|
424
453
|
const room = joinRoom(trysteroConfig, roomId)
|
|
425
|
-
const
|
|
454
|
+
const colorAction = room.makeAction('color')
|
|
426
455
|
const [myColor, setMyColor] = useState('#c0ffee')
|
|
427
456
|
const [peerColors, setPeerColors] = useState({})
|
|
428
457
|
|
|
429
458
|
// whenever new peers join the room, send my color to them:
|
|
430
|
-
room.onPeerJoin
|
|
459
|
+
room.onPeerJoin = peer => colorAction.send(myColor, {target: peer})
|
|
431
460
|
|
|
432
461
|
// listen for peers sending their colors and update the state accordingly:
|
|
433
|
-
|
|
434
|
-
setPeerColors(peerColors => ({...peerColors, [
|
|
435
|
-
)
|
|
462
|
+
colorAction.onMessage = (color, {peerId}) =>
|
|
463
|
+
setPeerColors(peerColors => ({...peerColors, [peerId]: color}))
|
|
436
464
|
|
|
437
465
|
const updateColor = e => {
|
|
438
466
|
const {value} = e.target
|
|
439
467
|
|
|
440
468
|
// when updating my own color, broadcast it to all peers:
|
|
441
|
-
|
|
469
|
+
colorAction.send(value)
|
|
442
470
|
setMyColor(value)
|
|
443
471
|
}
|
|
444
472
|
|
|
@@ -585,7 +613,9 @@ are no public default servers. You can pass multiple.
|
|
|
585
613
|
### Write your own strategy
|
|
586
614
|
|
|
587
615
|
If you want to provide your own signaling backend, you can build a custom
|
|
588
|
-
strategy with `
|
|
616
|
+
strategy with `createTopicStrategy`. This is the recommended helper for simple
|
|
617
|
+
pub/sub relays because Trystero handles room lifecycle details like passive
|
|
618
|
+
mode, announce scheduling, and peer-specific signaling topics.
|
|
589
619
|
|
|
590
620
|
The example below assumes a WebSocket relay that does simple pub/sub routing:
|
|
591
621
|
|
|
@@ -597,9 +627,9 @@ This is just to show you how it works; if you want a simple self-hosted
|
|
|
597
627
|
solution, use the `ws-relay` package explained above.
|
|
598
628
|
|
|
599
629
|
```js
|
|
600
|
-
import {
|
|
630
|
+
import {createTopicStrategy, toJson} from '@trystero-p2p/core'
|
|
601
631
|
|
|
602
|
-
export const joinRoom =
|
|
632
|
+
export const joinRoom = createTopicStrategy({
|
|
603
633
|
// Define init as a function that returns a promise of your signaling client.
|
|
604
634
|
// Resolve the promise when your client is ready to send messages.
|
|
605
635
|
// You can also return an array of client promises for redundancy.
|
|
@@ -611,49 +641,34 @@ export const joinRoom = createStrategy({
|
|
|
611
641
|
ws.addEventListener('error', reject, {once: true})
|
|
612
642
|
}),
|
|
613
643
|
|
|
614
|
-
// Subscribe
|
|
615
|
-
|
|
616
|
-
// 2. rootTopic is the channel where peers in a room announce their presence.
|
|
617
|
-
// 3. selfTopic is the current peer's own channel where messages specific to
|
|
618
|
-
// it should flow (i.e. offers and answers from external peers).
|
|
619
|
-
// 4. onMessage is a function that should be called when your relay client
|
|
620
|
-
// gets a message. Call it with the topic, data payload, and a callback
|
|
621
|
-
// function that takes another topic and payload.
|
|
622
|
-
// 5. getOffers is an optional helper for precomputed offers (only needed if
|
|
623
|
-
// the strategy needs to send offers in bulk, like torrents).
|
|
624
|
-
// It should return a cleanup function that unsubscribes
|
|
625
|
-
subscribe: (client, rootTopic, selfTopic, onMessage, _getOffers) => {
|
|
626
|
-
const topics = [rootTopic, selfTopic]
|
|
644
|
+
// Subscribe to one topic. Trystero decides which topics are needed and when.
|
|
645
|
+
subscribeTopic: (client, topic, onMessage) => {
|
|
627
646
|
const onWsMessage = event => {
|
|
628
|
-
const
|
|
647
|
+
const message = JSON.parse(String(event.data))
|
|
629
648
|
|
|
630
|
-
if (
|
|
649
|
+
if (message.topic !== topic) {
|
|
631
650
|
return
|
|
632
651
|
}
|
|
633
652
|
|
|
634
|
-
onMessage(topic, payload
|
|
635
|
-
client.send(
|
|
636
|
-
toJson({type: 'publish', topic: peerTopic, payload: signal})
|
|
637
|
-
)
|
|
638
|
-
)
|
|
653
|
+
onMessage(message.topic, message.payload)
|
|
639
654
|
}
|
|
640
655
|
|
|
641
656
|
client.addEventListener('message', onWsMessage)
|
|
642
|
-
|
|
657
|
+
client.send(toJson({type: 'subscribe', topic}))
|
|
643
658
|
|
|
644
659
|
return () => {
|
|
645
|
-
|
|
660
|
+
client.send(toJson({type: 'unsubscribe', topic}))
|
|
646
661
|
client.removeEventListener('message', onWsMessage)
|
|
647
662
|
}
|
|
648
663
|
},
|
|
649
664
|
|
|
650
|
-
//
|
|
651
|
-
|
|
665
|
+
// Publish a payload to one topic.
|
|
666
|
+
publishTopic: (client, topic, payload) =>
|
|
652
667
|
client.send(
|
|
653
668
|
toJson({
|
|
654
669
|
type: 'publish',
|
|
655
|
-
topic
|
|
656
|
-
payload
|
|
670
|
+
topic,
|
|
671
|
+
payload
|
|
657
672
|
})
|
|
658
673
|
)
|
|
659
674
|
})
|
|
@@ -664,6 +679,9 @@ const room = joinRoom(
|
|
|
664
679
|
)
|
|
665
680
|
```
|
|
666
681
|
|
|
682
|
+
For non-pub/sub signaling protocols, such as trackers that exchange offers in
|
|
683
|
+
bulk, `createStrategy` is available as a lower-level advanced API.
|
|
684
|
+
|
|
667
685
|
### Supabase setup
|
|
668
686
|
|
|
669
687
|
To use the Supabase strategy:
|
|
@@ -731,6 +749,14 @@ the same namespace will return the same room instance.
|
|
|
731
749
|
name. A custom password must match between any peers in the room for them to
|
|
732
750
|
connect. See [encryption](#encryption) for more details.
|
|
733
751
|
|
|
752
|
+
- `passive` - **(optional)** Boolean for backup or relay peers that should
|
|
753
|
+
listen for active peers without announcing themselves while a room is
|
|
754
|
+
dormant. Passive peers activate only after hearing a non-passive peer and
|
|
755
|
+
include a passive flag in their signaling so passive peers do not connect to
|
|
756
|
+
each other. For BitTorrent, dormant passive rooms announce as seeders
|
|
757
|
+
(`left: 0`) without offers, which avoids passive-to-passive discovery while
|
|
758
|
+
keeping tracker load low.
|
|
759
|
+
|
|
734
760
|
- `relayConfig` - **(optional unless required by your chosen strategy)**
|
|
735
761
|
Object containing strategy-specific relay settings:
|
|
736
762
|
- `urls` - **(optional for 🌊 BitTorrent, 🐦 Nostr, 📡 MQTT; required for 🔌
|
|
@@ -792,9 +818,12 @@ the same namespace will return the same room instance.
|
|
|
792
818
|
|
|
793
819
|
- `callbacks` - **(optional)** Callback config object containing:
|
|
794
820
|
- `onJoinError(details)` - Called when room join fails due to an incorrect
|
|
795
|
-
password
|
|
796
|
-
|
|
797
|
-
|
|
821
|
+
password, when handshake admission fails (including timeout), or when peers
|
|
822
|
+
exchange SDP but WebRTC still cannot establish a direct connection. This
|
|
823
|
+
last case usually means TURN servers are needed or misconfigured (see
|
|
824
|
+
[Troubleshooting connection issues](#troubleshooting-connection-issues)).
|
|
825
|
+
`details` is an object containing `appId`, `roomId`, `peerId`, and `error`
|
|
826
|
+
describing the failure.
|
|
798
827
|
|
|
799
828
|
- `onPeerHandshake(peerId, send, receive, isInitiator)` - Async predicate that
|
|
800
829
|
runs after the transport connects but before the peer becomes active.
|
|
@@ -853,96 +882,100 @@ Returns an object with the following methods:
|
|
|
853
882
|
for the peers present in room (not including the local user). The keys of this
|
|
854
883
|
object are the respective peers' IDs.
|
|
855
884
|
|
|
856
|
-
- ### `
|
|
885
|
+
- ### `isPassive()`
|
|
886
|
+
|
|
887
|
+
Returns whether the room was joined with `passive: true`.
|
|
888
|
+
|
|
889
|
+
- ### `addStream(stream, [options])`
|
|
857
890
|
|
|
858
891
|
Broadcasts media stream to other peers.
|
|
859
892
|
- `stream` - A `MediaStream` with audio and/or video to send to peers in the
|
|
860
893
|
room.
|
|
861
894
|
|
|
862
|
-
- `
|
|
863
|
-
target peer ID (string) or list of peer IDs (array).
|
|
895
|
+
- `options.target` - **(optional)** If specified, the stream is sent only to
|
|
896
|
+
the target peer ID (string) or list of peer IDs (array). Passing `null` or
|
|
897
|
+
omitting this option sends to all peers in the room.
|
|
864
898
|
|
|
865
|
-
- `metadata` - **(optional)** Additional metadata (any serializable
|
|
866
|
-
be sent with the stream. This is useful when sending multiple
|
|
867
|
-
recipients know which is which (e.g. a webcam versus a screen
|
|
868
|
-
|
|
869
|
-
argument, pass `null` as the second argument.
|
|
899
|
+
- `options.metadata` - **(optional)** Additional metadata (any serializable
|
|
900
|
+
type) to be sent with the stream. This is useful when sending multiple
|
|
901
|
+
streams so recipients know which is which (e.g. a webcam versus a screen
|
|
902
|
+
capture).
|
|
870
903
|
|
|
871
|
-
- ### `removeStream(stream, [
|
|
904
|
+
- ### `removeStream(stream, [options])`
|
|
872
905
|
|
|
873
906
|
Stops sending previously sent media stream to other peers.
|
|
874
907
|
- `stream` - A previously sent `MediaStream` to stop sending.
|
|
875
908
|
|
|
876
|
-
- `
|
|
877
|
-
the target peer ID (string) or list of peer IDs (array).
|
|
909
|
+
- `options.target` - **(optional)** If specified, the stream is removed only
|
|
910
|
+
from the target peer ID (string) or list of peer IDs (array).
|
|
878
911
|
|
|
879
|
-
- ### `addTrack(track, stream, [
|
|
912
|
+
- ### `addTrack(track, stream, [options])`
|
|
880
913
|
|
|
881
914
|
Adds a new media track to a stream.
|
|
882
915
|
- `track` - A `MediaStreamTrack` to add to an existing stream.
|
|
883
916
|
|
|
884
917
|
- `stream` - The target `MediaStream` to attach the new track to.
|
|
885
918
|
|
|
886
|
-
- `
|
|
887
|
-
target peer ID (string) or list of peer IDs (array).
|
|
919
|
+
- `options.target` - **(optional)** If specified, the track is sent only to
|
|
920
|
+
the target peer ID (string) or list of peer IDs (array).
|
|
888
921
|
|
|
889
|
-
- `metadata` - **(optional)** Additional metadata (any serializable
|
|
890
|
-
be sent with the track. See `metadata` notes for `addStream()`
|
|
891
|
-
more details.
|
|
922
|
+
- `options.metadata` - **(optional)** Additional metadata (any serializable
|
|
923
|
+
type) to be sent with the track. See `metadata` notes for `addStream()`
|
|
924
|
+
above for more details.
|
|
892
925
|
|
|
893
|
-
- ### `removeTrack(track, [
|
|
926
|
+
- ### `removeTrack(track, [options])`
|
|
894
927
|
|
|
895
928
|
Removes a media track.
|
|
896
929
|
- `track` - The `MediaStreamTrack` to remove.
|
|
897
930
|
|
|
898
|
-
- `
|
|
899
|
-
the target peer ID (string) or list of peer IDs (array).
|
|
931
|
+
- `options.target` - **(optional)** If specified, the track is removed only
|
|
932
|
+
from the target peer ID (string) or list of peer IDs (array).
|
|
900
933
|
|
|
901
|
-
- ### `replaceTrack(oldTrack, newTrack, [
|
|
934
|
+
- ### `replaceTrack(oldTrack, newTrack, [options])`
|
|
902
935
|
|
|
903
936
|
Replaces a media track with a new one.
|
|
904
937
|
- `oldTrack` - The `MediaStreamTrack` to remove.
|
|
905
938
|
|
|
906
939
|
- `newTrack` - A `MediaStreamTrack` to attach.
|
|
907
940
|
|
|
908
|
-
- `
|
|
909
|
-
the target peer ID (string) or list of peer IDs (array).
|
|
941
|
+
- `options.target` - **(optional)** If specified, the track is replaced only
|
|
942
|
+
for the target peer ID (string) or list of peer IDs (array).
|
|
910
943
|
|
|
911
|
-
- `metadata` - **(optional)** Additional metadata (any serializable
|
|
912
|
-
be sent with the replacement track.
|
|
944
|
+
- `options.metadata` - **(optional)** Additional metadata (any serializable
|
|
945
|
+
type) to be sent with the replacement track.
|
|
913
946
|
|
|
914
|
-
- ### `onPeerJoin
|
|
947
|
+
- ### `onPeerJoin`
|
|
915
948
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
Existing active peers are immediately replayed to a newly
|
|
949
|
+
A callback property that will be called when a peer joins the room. Assigning
|
|
950
|
+
a new function replaces the previous handler; assigning `null` clears it.
|
|
951
|
+
Existing active peers are immediately replayed to a newly assigned handler.
|
|
919
952
|
- `callback(peerId)` - Function to run whenever a peer joins, called with the
|
|
920
953
|
peer's ID.
|
|
921
954
|
|
|
922
955
|
Example:
|
|
923
956
|
|
|
924
957
|
```js
|
|
925
|
-
onPeerJoin
|
|
958
|
+
room.onPeerJoin = peerId => console.log(`${peerId} joined`)
|
|
926
959
|
```
|
|
927
960
|
|
|
928
|
-
- ### `onPeerLeave
|
|
961
|
+
- ### `onPeerLeave`
|
|
929
962
|
|
|
930
|
-
|
|
931
|
-
|
|
963
|
+
A callback property that will be called when a peer leaves the room. Assigning
|
|
964
|
+
a new function replaces the previous handler; assigning `null` clears it.
|
|
932
965
|
- `callback(peerId)` - Function to run whenever a peer leaves, called with the
|
|
933
966
|
peer's ID.
|
|
934
967
|
|
|
935
968
|
Example:
|
|
936
969
|
|
|
937
970
|
```js
|
|
938
|
-
onPeerLeave
|
|
971
|
+
room.onPeerLeave = peerId => console.log(`${peerId} left`)
|
|
939
972
|
```
|
|
940
973
|
|
|
941
|
-
- ### `onPeerStream
|
|
974
|
+
- ### `onPeerStream`
|
|
942
975
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
976
|
+
A callback property that will be called when a peer sends a media stream.
|
|
977
|
+
Assigning a new function replaces the previous handler; assigning `null`
|
|
978
|
+
clears it.
|
|
946
979
|
- `callback(stream, peerId, metadata)` - Function to run whenever a peer sends
|
|
947
980
|
a media stream, called with the the peer's stream, ID, and optional metadata
|
|
948
981
|
(see `addStream()` above for details).
|
|
@@ -950,16 +983,15 @@ Returns an object with the following methods:
|
|
|
950
983
|
Example:
|
|
951
984
|
|
|
952
985
|
```js
|
|
953
|
-
onPeerStream(
|
|
986
|
+
room.onPeerStream = (stream, peerId) =>
|
|
954
987
|
console.log(`got stream from ${peerId}`, stream)
|
|
955
|
-
)
|
|
956
988
|
```
|
|
957
989
|
|
|
958
|
-
- ### `onPeerTrack
|
|
990
|
+
- ### `onPeerTrack`
|
|
959
991
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
992
|
+
A callback property that will be called when a peer sends a media track.
|
|
993
|
+
Assigning a new function replaces the previous handler; assigning `null`
|
|
994
|
+
clears it.
|
|
963
995
|
- `callback(track, stream, peerId, metadata)` - Function to run whenever a
|
|
964
996
|
peer sends a media track, called with the the peer's track, attached stream,
|
|
965
997
|
ID, and optional metadata (see `addTrack()` above for details).
|
|
@@ -967,78 +999,48 @@ Returns an object with the following methods:
|
|
|
967
999
|
Example:
|
|
968
1000
|
|
|
969
1001
|
```js
|
|
970
|
-
onPeerTrack(
|
|
1002
|
+
room.onPeerTrack = (track, stream, peerId) =>
|
|
971
1003
|
console.log(`got track from ${peerId}`, track)
|
|
972
|
-
)
|
|
973
1004
|
```
|
|
974
1005
|
|
|
975
|
-
- ### `makeAction(actionId)`
|
|
1006
|
+
- ### `makeAction(actionId, [config])`
|
|
976
1007
|
|
|
977
1008
|
Listen for and send custom data actions.
|
|
978
1009
|
- `actionId` - A string to register this action consistently among all peers.
|
|
979
1010
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
- Sends data to peers and returns a promise that resolves when all target
|
|
983
|
-
peers are finished sending locally.
|
|
984
|
-
|
|
985
|
-
- `(data, [targetPeers], [metadata], [onProgress])`
|
|
986
|
-
- `data` - Any value to send (primitive, object, binary). Serialization
|
|
987
|
-
and chunking is handled automatically. Binary data (e.g. `Blob`,
|
|
988
|
-
`TypedArray`) is received by other peer as an agnostic `ArrayBuffer`.
|
|
989
|
-
|
|
990
|
-
- `targetPeers` - **(optional)** Either a peer ID (string), an array of
|
|
991
|
-
peer IDs, or `null` (indicating to send to all peers in the room).
|
|
992
|
-
|
|
993
|
-
- `metadata` - **(optional)** If the data is binary, you can send an
|
|
994
|
-
optional metadata object describing it (see
|
|
995
|
-
[Binary metadata](#binary-metadata)).
|
|
996
|
-
|
|
997
|
-
- `onProgress` - **(optional)** A callback function that will be called
|
|
998
|
-
as every chunk for every peer is transmitted. The function will be
|
|
999
|
-
called with a value between 0 and 1 and a peer ID. See
|
|
1000
|
-
[Progress updates](#progress-updates) for an example.
|
|
1001
|
-
|
|
1002
|
-
2. #### Receiver
|
|
1003
|
-
- Registers a callback function that runs when data for this action is
|
|
1004
|
-
received from other peers.
|
|
1005
|
-
|
|
1006
|
-
- `(data, peerId, metadata)`
|
|
1007
|
-
- `data` - The value transmitted by the sending peer. Deserialization is
|
|
1008
|
-
handled automatically, i.e. a number will be received as a number, an
|
|
1009
|
-
object as an object, etc.
|
|
1011
|
+
If `config.kind` is omitted, the action is a one-way message action. Passing
|
|
1012
|
+
`kind: 'request'` creates a request/response action.
|
|
1010
1013
|
|
|
1011
|
-
|
|
1014
|
+
Message actions expose:
|
|
1015
|
+
- `send(data, [options])` - Sends data to peers and resolves when local
|
|
1016
|
+
sending is complete.
|
|
1017
|
+
- `onMessage` - Nullable callback property for received messages.
|
|
1018
|
+
- `onReceiveProgress` - Nullable callback property for inbound progress.
|
|
1012
1019
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1020
|
+
Request actions expose:
|
|
1021
|
+
- `request(data, options)` - Sends to one peer and resolves with its response.
|
|
1022
|
+
- `requestMany(data, options)` - Sends to many peers and resolves with
|
|
1023
|
+
peer-labeled settled results.
|
|
1024
|
+
- `onRequest` - Nullable callback property that returns the response.
|
|
1025
|
+
- `onReceiveProgress` - Nullable callback property for inbound progress.
|
|
1015
1026
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
- `(percent, peerId, metadata)`
|
|
1022
|
-
- `percent` - A number between 0 and 1 indicating the percentage complete
|
|
1023
|
-
of the transfer.
|
|
1024
|
-
|
|
1025
|
-
- `peerId` - The ID string of the sending peer.
|
|
1026
|
-
|
|
1027
|
-
- `metadata` - **(optional)** Optional metadata object supplied by the
|
|
1028
|
-
sender.
|
|
1027
|
+
Send options use `target`, `metadata`, `onProgress`, and `signal`. Request
|
|
1028
|
+
options use `target`, `metadata`, `timeoutMs`, `onProgress`, and `signal`.
|
|
1029
|
+
`requestMany()` uses `targets` plus optional `onResult` for each peer result
|
|
1030
|
+
as it arrives.
|
|
1029
1031
|
|
|
1030
1032
|
Example:
|
|
1031
1033
|
|
|
1032
1034
|
```js
|
|
1033
|
-
const
|
|
1035
|
+
const cursor = room.makeAction('cursormove')
|
|
1034
1036
|
|
|
1035
|
-
window.addEventListener('mousemove', e =>
|
|
1037
|
+
window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY]))
|
|
1036
1038
|
|
|
1037
|
-
|
|
1039
|
+
cursor.onMessage = ([x, y], {peerId}) => {
|
|
1038
1040
|
const peerCursor = cursorMap[peerId]
|
|
1039
1041
|
peerCursor.style.left = x + 'px'
|
|
1040
1042
|
peerCursor.style.top = y + 'px'
|
|
1041
|
-
}
|
|
1043
|
+
}
|
|
1042
1044
|
```
|
|
1043
1045
|
|
|
1044
1046
|
- ### `ping(peerId)`
|
|
@@ -1051,12 +1053,11 @@ Returns an object with the following methods:
|
|
|
1051
1053
|
|
|
1052
1054
|
```js
|
|
1053
1055
|
// log round-trip time every 2 seconds
|
|
1054
|
-
room.onPeerJoin
|
|
1056
|
+
room.onPeerJoin = peerId =>
|
|
1055
1057
|
setInterval(
|
|
1056
1058
|
async () => console.log(`took ${await room.ping(peerId)}ms`),
|
|
1057
1059
|
2000
|
|
1058
1060
|
)
|
|
1059
|
-
)
|
|
1060
1061
|
```
|
|
1061
1062
|
|
|
1062
1063
|
### `selfId`
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JoinRoom, JoinRoomConfig, pauseRelayReconnection, resumeRelayReconnection, selfId } from "@trystero-p2p/core";
|
|
2
|
-
export * from "@trystero-p2p/core";
|
|
2
|
+
export type * from "@trystero-p2p/core";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
5
5
|
type TorrentRoomConfig = JoinRoomConfig;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;KAmDY,iBAAA,GAAoB,cAAc;AAAA,cA6ZjC,QAAA,EAAU,QAAQ,CAAC,iBAAA;AAAA,cAcnB,eAAA;AAAA,cAIA,gBAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const subscriptionTokens = relayManager.scoped();
|
|
|
9
9
|
const trackerAnnounceMs = {};
|
|
10
10
|
const handledSignals = {};
|
|
11
11
|
const msgHandlers = relayManager.scoped();
|
|
12
|
+
const topicStates = relayManager.scoped();
|
|
12
13
|
const roomOutstandingOffers = {};
|
|
13
14
|
const roomOfferGenerationPromises = {};
|
|
14
15
|
const roomSubscriberCounts = {};
|
|
@@ -16,6 +17,7 @@ const trackerAction = "announce";
|
|
|
16
17
|
const hashLimit = 20;
|
|
17
18
|
const offerPoolSize = 3;
|
|
18
19
|
const defaultAnnounceMs = 1e4;
|
|
20
|
+
const dormantAnnounceMs = 12e4;
|
|
19
21
|
const maxAnnounceMs = 2e4;
|
|
20
22
|
const offerRetentionMs = 12e4;
|
|
21
23
|
const signalDedupeWindowMs = 4e3;
|
|
@@ -125,11 +127,12 @@ const joinRoomStrategy = createStrategy({
|
|
|
125
127
|
}));
|
|
126
128
|
return client.ready;
|
|
127
129
|
}),
|
|
128
|
-
subscribe: (client, rootTopic, _, onMessage, getOffers) => {
|
|
130
|
+
subscribe: (client, rootTopic, _, onMessage, getOffers, context) => {
|
|
129
131
|
const handlers = msgHandlers.forRelay(client);
|
|
130
132
|
const relayFns = announceFns.forRelay(client);
|
|
131
133
|
const relayIntervals = announceIntervals.forRelay(client);
|
|
132
134
|
const activeTokens = subscriptionTokens.forRelay(client);
|
|
135
|
+
const states = topicStates.forRelay(client);
|
|
133
136
|
const subscriptionToken = Symbol(rootTopic);
|
|
134
137
|
activeTokens[rootTopic] = subscriptionToken;
|
|
135
138
|
roomSubscriberCounts[rootTopic] = (roomSubscriberCounts[rootTopic] ?? 0) + 1;
|
|
@@ -156,28 +159,45 @@ const joinRoomStrategy = createStrategy({
|
|
|
156
159
|
}
|
|
157
160
|
};
|
|
158
161
|
handlers[rootTopic] = topicHandler;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
162
|
+
const topicState = {
|
|
163
|
+
announce: async () => {
|
|
164
|
+
if (activeTokens[rootTopic] !== subscriptionToken) return;
|
|
165
|
+
if (!topicState.isActive) {
|
|
166
|
+
send(client, rootTopic, {
|
|
167
|
+
left: 0,
|
|
168
|
+
numwant: offerPoolSize,
|
|
169
|
+
offers: []
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
send(client, rootTopic, {
|
|
174
|
+
numwant: offerPoolSize,
|
|
175
|
+
offers: entries(await ensureOutstandingOffers(rootTopic, getOffers)).map(([id, { offer }]) => ({
|
|
176
|
+
offer_id: id,
|
|
177
|
+
offer: {
|
|
178
|
+
type: "offer",
|
|
179
|
+
sdp: offer
|
|
180
|
+
}
|
|
181
|
+
}))
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
isActive: !context?.isPassive
|
|
171
185
|
};
|
|
172
186
|
trackerAnnounceMs[client.url] = defaultAnnounceMs;
|
|
187
|
+
const { announce } = topicState;
|
|
173
188
|
relayFns[rootTopic] = announce;
|
|
174
|
-
|
|
189
|
+
states[rootTopic] = topicState;
|
|
190
|
+
const initialInterval = topicState.isActive ? trackerAnnounceMs[client.url] : dormantAnnounceMs;
|
|
191
|
+
relayIntervals[rootTopic] = setInterval(announce, initialInterval);
|
|
175
192
|
announce();
|
|
176
193
|
return () => {
|
|
177
194
|
roomSubscriberCounts[rootTopic] = Math.max(0, (roomSubscriberCounts[rootTopic] ?? 1) - 1);
|
|
178
195
|
if (!roomSubscriberCounts[rootTopic]) delete roomSubscriberCounts[rootTopic];
|
|
179
196
|
if (activeTokens[rootTopic] !== subscriptionToken) {
|
|
180
|
-
if (!roomSubscriberCounts[rootTopic])
|
|
197
|
+
if (!roomSubscriberCounts[rootTopic]) {
|
|
198
|
+
reclaimAllOutstandingOffers(rootTopic);
|
|
199
|
+
delete states[rootTopic];
|
|
200
|
+
}
|
|
181
201
|
return;
|
|
182
202
|
}
|
|
183
203
|
const interval = relayIntervals[rootTopic];
|
|
@@ -188,10 +208,38 @@ const joinRoomStrategy = createStrategy({
|
|
|
188
208
|
if (handlers[rootTopic] === topicHandler) delete handlers[rootTopic];
|
|
189
209
|
if (relayFns[rootTopic] === announce) delete relayFns[rootTopic];
|
|
190
210
|
delete activeTokens[rootTopic];
|
|
211
|
+
if (states[rootTopic] === topicState) delete states[rootTopic];
|
|
191
212
|
if (!roomSubscriberCounts[rootTopic]) reclaimAllOutstandingOffers(rootTopic);
|
|
192
213
|
};
|
|
193
214
|
},
|
|
194
|
-
announce: (client) =>
|
|
215
|
+
announce: (client, rootTopic) => {
|
|
216
|
+
const state = topicStates.forRelay(client)[rootTopic];
|
|
217
|
+
const relayIntervals = announceIntervals.forRelay(client);
|
|
218
|
+
const fn = announceFns.forRelay(client)[rootTopic];
|
|
219
|
+
if (state) state.isActive = true;
|
|
220
|
+
if (fn && relayIntervals[rootTopic]) {
|
|
221
|
+
clearInterval(relayIntervals[rootTopic]);
|
|
222
|
+
relayIntervals[rootTopic] = setInterval(() => {
|
|
223
|
+
fn();
|
|
224
|
+
}, trackerAnnounceMs[client.url]);
|
|
225
|
+
fn();
|
|
226
|
+
}
|
|
227
|
+
return trackerAnnounceMs[client.url];
|
|
228
|
+
},
|
|
229
|
+
deactivate: (client, rootTopic) => {
|
|
230
|
+
const state = topicStates.forRelay(client)[rootTopic];
|
|
231
|
+
const relayIntervals = announceIntervals.forRelay(client);
|
|
232
|
+
const fn = announceFns.forRelay(client)[rootTopic];
|
|
233
|
+
if (state) state.isActive = false;
|
|
234
|
+
reclaimAllOutstandingOffers(rootTopic);
|
|
235
|
+
if (fn && relayIntervals[rootTopic]) {
|
|
236
|
+
clearInterval(relayIntervals[rootTopic]);
|
|
237
|
+
relayIntervals[rootTopic] = setInterval(() => {
|
|
238
|
+
fn();
|
|
239
|
+
}, dormantAnnounceMs);
|
|
240
|
+
fn();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
195
243
|
});
|
|
196
244
|
const joinRoom = (config, roomId, callbacks) => joinRoomStrategy({
|
|
197
245
|
...config,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import {\n createRelayManager,\n createStrategy,\n entries,\n fromJson,\n genId,\n getRelays,\n keys,\n libName,\n makeSocket,\n pauseRelayReconnection,\n resumeRelayReconnection,\n selfId,\n sha1,\n toJson,\n type JoinRoom,\n type JoinRoomConfig,\n type OfferRecord,\n type SocketClient\n} from '@trystero-p2p/core'\n\nconst relayManager = createRelayManager<SocketClient>(client => client.socket)\nconst topicToInfoHash: Record<string, string> = {}\nconst infoHashToTopic: Record<string, string> = {}\nconst announceIntervals = relayManager.scoped<ReturnType<typeof setInterval>>()\nconst announceFns = relayManager.scoped<() => void | Promise<void>>()\nconst subscriptionTokens = relayManager.scoped<symbol>()\nconst trackerAnnounceMs: Record<string, number> = {}\nconst handledSignals: Record<string, number> = {}\nconst msgHandlers = relayManager.scoped<(data: TrackerMessage) => void>()\nconst roomOutstandingOffers: Record<\n string,\n Record<string, OfferRecord & {createdAt: number}>\n> = {}\nconst roomOfferGenerationPromises: Record<string, Promise<void> | undefined> =\n {}\nconst roomSubscriberCounts: Record<string, number> = {}\nconst trackerAction = 'announce'\nconst hashLimit = 20\nconst offerPoolSize = 3\nconst defaultAnnounceMs = 10_000\nconst maxAnnounceMs = 20_000\nconst offerRetentionMs = 120_000\nconst signalDedupeWindowMs = 4_000\nconst defaultRedundancy = 3\n\nexport type TorrentRoomConfig = JoinRoomConfig\n\ntype TrackerMessage = {\n offer?: {\n type: 'offer'\n sdp: string\n }\n answer?: {\n type: 'answer'\n sdp: string\n }\n offer_id?: string\n peer_id?: string\n info_hash?: string\n interval?: number\n ['failure reason']?: string\n ['warning message']?: string\n}\n\nconst getInfoHash = async (topic: string): Promise<string> => {\n if (topicToInfoHash[topic]) {\n return topicToInfoHash[topic]\n }\n\n const hash = (await sha1(topic)).slice(0, hashLimit)\n topicToInfoHash[topic] = hash\n infoHashToTopic[hash] = topic\n\n return hash\n}\n\nconst send = async (\n client: SocketClient,\n topic: string,\n payload: Record<string, unknown>\n): Promise<void> =>\n client.send(\n toJson({\n action: trackerAction,\n info_hash: await getInfoHash(topic),\n peer_id: selfId,\n ...payload\n })\n )\n\nconst warn = (url: string, msg: string, didFail = false): void =>\n console.warn(\n `${libName}: torrent tracker ${didFail ? 'failure' : 'warning'} from ${url} - ${msg}`\n )\n\nconst getRoomOutstandingOffers = (\n rootTopic: string\n): Record<string, OfferRecord & {createdAt: number}> =>\n (roomOutstandingOffers[rootTopic] ??= {})\n\nconst deleteRoomOfferBookkeeping = (rootTopic: string): void => {\n delete roomOutstandingOffers[rootTopic]\n delete roomOfferGenerationPromises[rootTopic]\n}\n\nconst claimOutstandingOffer = (\n rootTopic: string,\n offerId: string\n): OfferRecord | undefined => {\n const outstandingOffers = roomOutstandingOffers[rootTopic]\n const offer = outstandingOffers?.[offerId]\n\n if (!offer) {\n return\n }\n\n delete outstandingOffers[offerId]\n offer.claim?.()\n\n if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) {\n deleteRoomOfferBookkeeping(rootTopic)\n }\n\n return offer\n}\n\nconst reclaimOutstandingOffer = (rootTopic: string, offerId: string): void => {\n const outstandingOffers = roomOutstandingOffers[rootTopic]\n const offer = outstandingOffers?.[offerId]\n\n if (!offer) {\n return\n }\n\n delete outstandingOffers[offerId]\n offer.reclaim?.()\n\n if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) {\n deleteRoomOfferBookkeeping(rootTopic)\n }\n}\n\nconst reclaimAllOutstandingOffers = (rootTopic: string): void => {\n keys(getRoomOutstandingOffers(rootTopic)).forEach(offerId =>\n reclaimOutstandingOffer(rootTopic, offerId)\n )\n deleteRoomOfferBookkeeping(rootTopic)\n}\n\nconst pruneOutstandingOffers = (rootTopic: string): void => {\n const now = Date.now()\n\n entries(getRoomOutstandingOffers(rootTopic)).forEach(([offerId, offer]) => {\n if (now - offer.createdAt > offerRetentionMs) {\n reclaimOutstandingOffer(rootTopic, offerId)\n }\n })\n}\n\nconst ensureOutstandingOffers = async (\n rootTopic: string,\n getOffers: (n: number) => Promise<OfferRecord[]>\n): Promise<Record<string, OfferRecord & {createdAt: number}>> => {\n while (roomOfferGenerationPromises[rootTopic]) {\n await roomOfferGenerationPromises[rootTopic]\n }\n\n const nextPromise = (async () => {\n pruneOutstandingOffers(rootTopic)\n\n const outstandingOffers = getRoomOutstandingOffers(rootTopic)\n const outstandingCount = keys(outstandingOffers).length\n const missingOffers = Math.max(0, offerPoolSize - outstandingCount)\n\n if (missingOffers > 0) {\n ;(await getOffers(missingOffers)).forEach(peerAndOffer => {\n outstandingOffers[genId(hashLimit)] = {\n ...peerAndOffer,\n createdAt: Date.now()\n }\n })\n }\n })().finally(() => {\n if (roomOfferGenerationPromises[rootTopic] === nextPromise) {\n delete roomOfferGenerationPromises[rootTopic]\n }\n })\n\n roomOfferGenerationPromises[rootTopic] = nextPromise\n await nextPromise\n\n return getRoomOutstandingOffers(rootTopic)\n}\n\nconst joinRoomStrategy: JoinRoom<TorrentRoomConfig> = createStrategy({\n init: config =>\n getRelays(config, defaultRelayUrls, defaultRedundancy).map(rawUrl => {\n const client = relayManager.register(\n rawUrl,\n makeSocket(rawUrl, rawData => {\n const data = fromJson<TrackerMessage>(rawData)\n const errMsg = data['failure reason']\n const warnMsg = data['warning message']\n const {interval} = data\n const topic = data.info_hash\n ? infoHashToTopic[data.info_hash]\n : undefined\n\n if (errMsg) {\n warn(client.url, errMsg, true)\n return\n }\n\n if (warnMsg) {\n warn(client.url, warnMsg)\n }\n\n if (\n interval &&\n interval * 1000 >\n (trackerAnnounceMs[client.url] ?? defaultAnnounceMs) &&\n topic &&\n announceFns.forKey(rawUrl)[topic]\n ) {\n const nextInterval = Math.min(interval * 1000, maxAnnounceMs)\n const relayIntervals = announceIntervals.forKey(rawUrl)\n const relayFns = announceFns.forKey(rawUrl)\n\n if (relayIntervals[topic]) {\n clearInterval(relayIntervals[topic])\n }\n trackerAnnounceMs[client.url] = nextInterval\n const relayFn = relayFns[topic]\n\n if (relayFn) {\n relayIntervals[topic] = setInterval(() => {\n void relayFn()\n }, nextInterval)\n }\n }\n\n if ((data.offer || data.answer) && topic && data.offer_id) {\n if (data.peer_id === selfId) {\n return\n }\n\n const signalType = data.offer ? 'offer' : 'answer'\n const signalKey = `${topic}:${signalType}:${data.offer_id}:${data.peer_id ?? ''}`\n const nowMs = Date.now()\n const lastHandledMs = handledSignals[signalKey]\n\n if (\n typeof lastHandledMs === 'number' &&\n nowMs - lastHandledMs < signalDedupeWindowMs\n ) {\n return\n }\n\n handledSignals[signalKey] = nowMs\n\n entries(handledSignals).forEach(([key, handledAtMs]) => {\n if (nowMs - handledAtMs > signalDedupeWindowMs * 6) {\n delete handledSignals[key]\n }\n })\n\n msgHandlers.forKey(rawUrl)[topic]?.(data)\n }\n })\n )\n\n return client.ready\n }),\n\n subscribe: (client, rootTopic, _, onMessage, getOffers) => {\n const handlers = msgHandlers.forRelay(client)\n const relayFns = announceFns.forRelay(client)\n const relayIntervals = announceIntervals.forRelay(client)\n const activeTokens = subscriptionTokens.forRelay(client)\n const subscriptionToken = Symbol(rootTopic)\n\n activeTokens[rootTopic] = subscriptionToken\n roomSubscriberCounts[rootTopic] = (roomSubscriberCounts[rootTopic] ?? 0) + 1\n\n const topicHandler = (data: TrackerMessage): void => {\n if (data.offer && data.peer_id && data.offer_id) {\n void onMessage(\n rootTopic,\n {\n offer: data.offer.sdp,\n peerId: data.peer_id,\n hasOutgoingOffer:\n keys(getRoomOutstandingOffers(rootTopic)).length > 0\n },\n (_, signal) =>\n void send(client, rootTopic, {\n answer: {\n type: 'answer',\n sdp: fromJson<{answer: string}>(signal).answer\n },\n offer_id: data.offer_id,\n to_peer_id: data.peer_id\n })\n )\n } else if (data.answer && data.offer_id && data.peer_id) {\n const offer = claimOutstandingOffer(rootTopic, data.offer_id)\n\n if (offer) {\n void onMessage(\n rootTopic,\n {\n answer: data.answer.sdp,\n peerId: data.peer_id,\n peer: offer.peer\n },\n () => {}\n )\n }\n }\n }\n\n handlers[rootTopic] = topicHandler\n\n const announce = async (): Promise<void> => {\n if (activeTokens[rootTopic] !== subscriptionToken) {\n return\n }\n\n const outstandingOffers = await ensureOutstandingOffers(\n rootTopic,\n getOffers\n )\n const offers = entries(outstandingOffers).map(([id, {offer}]) => ({\n offer_id: id,\n offer: {type: 'offer', sdp: offer}\n }))\n\n void send(client, rootTopic, {\n numwant: offerPoolSize,\n offers\n })\n }\n\n trackerAnnounceMs[client.url] = defaultAnnounceMs\n relayFns[rootTopic] = announce\n relayIntervals[rootTopic] = setInterval(\n announce,\n trackerAnnounceMs[client.url]\n )\n void announce()\n\n return () => {\n roomSubscriberCounts[rootTopic] = Math.max(\n 0,\n (roomSubscriberCounts[rootTopic] ?? 1) - 1\n )\n\n if (!roomSubscriberCounts[rootTopic]) {\n delete roomSubscriberCounts[rootTopic]\n }\n\n if (activeTokens[rootTopic] !== subscriptionToken) {\n if (!roomSubscriberCounts[rootTopic]) {\n reclaimAllOutstandingOffers(rootTopic)\n }\n\n return\n }\n\n const interval = relayIntervals[rootTopic]\n if (interval) {\n clearInterval(interval)\n delete relayIntervals[rootTopic]\n }\n\n if (handlers[rootTopic] === topicHandler) {\n delete handlers[rootTopic]\n }\n\n if (relayFns[rootTopic] === announce) {\n delete relayFns[rootTopic]\n }\n\n delete activeTokens[rootTopic]\n\n if (!roomSubscriberCounts[rootTopic]) {\n reclaimAllOutstandingOffers(rootTopic)\n }\n }\n },\n\n announce: client => trackerAnnounceMs[client.url]\n})\n\nexport const joinRoom: JoinRoom<TorrentRoomConfig> = (\n config,\n roomId,\n callbacks\n) =>\n joinRoomStrategy(\n {\n ...config,\n trickleIce: config.trickleIce ?? false\n },\n roomId,\n callbacks\n )\n\nexport const getRelaySockets = relayManager.getSockets\n\nexport {pauseRelayReconnection, resumeRelayReconnection, selfId}\n\nexport const defaultRelayUrls = [\n 'tracker.webtorrent.dev',\n 'tracker.openwebtorrent.com',\n 'tracker.btorrent.xyz',\n 'tracker.files.fm:7073/announce'\n].map(url => 'wss://' + url)\n\nexport type * from '@trystero-p2p/core'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import {\n createRelayManager,\n createStrategy,\n entries,\n fromJson,\n genId,\n getRelays,\n keys,\n libName,\n makeSocket,\n pauseRelayReconnection,\n resumeRelayReconnection,\n selfId,\n sha1,\n toJson,\n type JoinRoom,\n type JoinRoomConfig,\n type OfferRecord,\n type SocketClient\n} from '@trystero-p2p/core'\n\nconst relayManager = createRelayManager<SocketClient>(client => client.socket)\nconst topicToInfoHash: Record<string, string> = {}\nconst infoHashToTopic: Record<string, string> = {}\nconst announceIntervals = relayManager.scoped<ReturnType<typeof setInterval>>()\nconst announceFns = relayManager.scoped<() => void | Promise<void>>()\nconst subscriptionTokens = relayManager.scoped<symbol>()\nconst trackerAnnounceMs: Record<string, number> = {}\nconst handledSignals: Record<string, number> = {}\nconst msgHandlers = relayManager.scoped<(data: TrackerMessage) => void>()\nconst topicStates = relayManager.scoped<{\n announce: () => void | Promise<void>\n isActive: boolean\n}>()\nconst roomOutstandingOffers: Record<\n string,\n Record<string, OfferRecord & {createdAt: number}>\n> = {}\nconst roomOfferGenerationPromises: Record<string, Promise<void> | undefined> =\n {}\nconst roomSubscriberCounts: Record<string, number> = {}\nconst trackerAction = 'announce'\nconst hashLimit = 20\nconst offerPoolSize = 3\nconst defaultAnnounceMs = 10_000\nconst dormantAnnounceMs = 120_000\nconst maxAnnounceMs = 20_000\nconst offerRetentionMs = 120_000\nconst signalDedupeWindowMs = 4_000\nconst defaultRedundancy = 3\n\nexport type TorrentRoomConfig = JoinRoomConfig\n\ntype TrackerMessage = {\n offer?: {\n type: 'offer'\n sdp: string\n }\n answer?: {\n type: 'answer'\n sdp: string\n }\n offer_id?: string\n peer_id?: string\n info_hash?: string\n interval?: number\n ['failure reason']?: string\n ['warning message']?: string\n}\n\nconst getInfoHash = async (topic: string): Promise<string> => {\n if (topicToInfoHash[topic]) {\n return topicToInfoHash[topic]\n }\n\n const hash = (await sha1(topic)).slice(0, hashLimit)\n topicToInfoHash[topic] = hash\n infoHashToTopic[hash] = topic\n\n return hash\n}\n\nconst send = async (\n client: SocketClient,\n topic: string,\n payload: Record<string, unknown>\n): Promise<void> =>\n client.send(\n toJson({\n action: trackerAction,\n info_hash: await getInfoHash(topic),\n peer_id: selfId,\n ...payload\n })\n )\n\nconst warn = (url: string, msg: string, didFail = false): void =>\n console.warn(\n `${libName}: torrent tracker ${didFail ? 'failure' : 'warning'} from ${url} - ${msg}`\n )\n\nconst getRoomOutstandingOffers = (\n rootTopic: string\n): Record<string, OfferRecord & {createdAt: number}> =>\n (roomOutstandingOffers[rootTopic] ??= {})\n\nconst deleteRoomOfferBookkeeping = (rootTopic: string): void => {\n delete roomOutstandingOffers[rootTopic]\n delete roomOfferGenerationPromises[rootTopic]\n}\n\nconst claimOutstandingOffer = (\n rootTopic: string,\n offerId: string\n): OfferRecord | undefined => {\n const outstandingOffers = roomOutstandingOffers[rootTopic]\n const offer = outstandingOffers?.[offerId]\n\n if (!offer) {\n return\n }\n\n delete outstandingOffers[offerId]\n offer.claim?.()\n\n if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) {\n deleteRoomOfferBookkeeping(rootTopic)\n }\n\n return offer\n}\n\nconst reclaimOutstandingOffer = (rootTopic: string, offerId: string): void => {\n const outstandingOffers = roomOutstandingOffers[rootTopic]\n const offer = outstandingOffers?.[offerId]\n\n if (!offer) {\n return\n }\n\n delete outstandingOffers[offerId]\n offer.reclaim?.()\n\n if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) {\n deleteRoomOfferBookkeeping(rootTopic)\n }\n}\n\nconst reclaimAllOutstandingOffers = (rootTopic: string): void => {\n keys(getRoomOutstandingOffers(rootTopic)).forEach(offerId =>\n reclaimOutstandingOffer(rootTopic, offerId)\n )\n deleteRoomOfferBookkeeping(rootTopic)\n}\n\nconst pruneOutstandingOffers = (rootTopic: string): void => {\n const now = Date.now()\n\n entries(getRoomOutstandingOffers(rootTopic)).forEach(([offerId, offer]) => {\n if (now - offer.createdAt > offerRetentionMs) {\n reclaimOutstandingOffer(rootTopic, offerId)\n }\n })\n}\n\nconst ensureOutstandingOffers = async (\n rootTopic: string,\n getOffers: (n: number) => Promise<OfferRecord[]>\n): Promise<Record<string, OfferRecord & {createdAt: number}>> => {\n while (roomOfferGenerationPromises[rootTopic]) {\n await roomOfferGenerationPromises[rootTopic]\n }\n\n const nextPromise = (async () => {\n pruneOutstandingOffers(rootTopic)\n\n const outstandingOffers = getRoomOutstandingOffers(rootTopic)\n const outstandingCount = keys(outstandingOffers).length\n const missingOffers = Math.max(0, offerPoolSize - outstandingCount)\n\n if (missingOffers > 0) {\n ;(await getOffers(missingOffers)).forEach(peerAndOffer => {\n outstandingOffers[genId(hashLimit)] = {\n ...peerAndOffer,\n createdAt: Date.now()\n }\n })\n }\n })().finally(() => {\n if (roomOfferGenerationPromises[rootTopic] === nextPromise) {\n delete roomOfferGenerationPromises[rootTopic]\n }\n })\n\n roomOfferGenerationPromises[rootTopic] = nextPromise\n await nextPromise\n\n return getRoomOutstandingOffers(rootTopic)\n}\n\nconst joinRoomStrategy: JoinRoom<TorrentRoomConfig> = createStrategy({\n init: config =>\n getRelays(config, defaultRelayUrls, defaultRedundancy).map(rawUrl => {\n const client = relayManager.register(\n rawUrl,\n makeSocket(rawUrl, rawData => {\n const data = fromJson<TrackerMessage>(rawData)\n const errMsg = data['failure reason']\n const warnMsg = data['warning message']\n const {interval} = data\n const topic = data.info_hash\n ? infoHashToTopic[data.info_hash]\n : undefined\n\n if (errMsg) {\n warn(client.url, errMsg, true)\n return\n }\n\n if (warnMsg) {\n warn(client.url, warnMsg)\n }\n\n if (\n interval &&\n interval * 1000 >\n (trackerAnnounceMs[client.url] ?? defaultAnnounceMs) &&\n topic &&\n announceFns.forKey(rawUrl)[topic]\n ) {\n const nextInterval = Math.min(interval * 1000, maxAnnounceMs)\n const relayIntervals = announceIntervals.forKey(rawUrl)\n const relayFns = announceFns.forKey(rawUrl)\n\n if (relayIntervals[topic]) {\n clearInterval(relayIntervals[topic])\n }\n trackerAnnounceMs[client.url] = nextInterval\n const relayFn = relayFns[topic]\n\n if (relayFn) {\n relayIntervals[topic] = setInterval(() => {\n void relayFn()\n }, nextInterval)\n }\n }\n\n if ((data.offer || data.answer) && topic && data.offer_id) {\n if (data.peer_id === selfId) {\n return\n }\n\n const signalType = data.offer ? 'offer' : 'answer'\n const signalKey = `${topic}:${signalType}:${data.offer_id}:${data.peer_id ?? ''}`\n const nowMs = Date.now()\n const lastHandledMs = handledSignals[signalKey]\n\n if (\n typeof lastHandledMs === 'number' &&\n nowMs - lastHandledMs < signalDedupeWindowMs\n ) {\n return\n }\n\n handledSignals[signalKey] = nowMs\n\n entries(handledSignals).forEach(([key, handledAtMs]) => {\n if (nowMs - handledAtMs > signalDedupeWindowMs * 6) {\n delete handledSignals[key]\n }\n })\n\n msgHandlers.forKey(rawUrl)[topic]?.(data)\n }\n })\n )\n\n return client.ready\n }),\n\n subscribe: (client, rootTopic, _, onMessage, getOffers, context) => {\n const handlers = msgHandlers.forRelay(client)\n const relayFns = announceFns.forRelay(client)\n const relayIntervals = announceIntervals.forRelay(client)\n const activeTokens = subscriptionTokens.forRelay(client)\n const states = topicStates.forRelay(client)\n const subscriptionToken = Symbol(rootTopic)\n\n activeTokens[rootTopic] = subscriptionToken\n roomSubscriberCounts[rootTopic] = (roomSubscriberCounts[rootTopic] ?? 0) + 1\n\n const topicHandler = (data: TrackerMessage): void => {\n if (data.offer && data.peer_id && data.offer_id) {\n void onMessage(\n rootTopic,\n {\n offer: data.offer.sdp,\n peerId: data.peer_id,\n hasOutgoingOffer:\n keys(getRoomOutstandingOffers(rootTopic)).length > 0\n },\n (_, signal) =>\n void send(client, rootTopic, {\n answer: {\n type: 'answer',\n sdp: fromJson<{answer: string}>(signal).answer\n },\n offer_id: data.offer_id,\n to_peer_id: data.peer_id\n })\n )\n } else if (data.answer && data.offer_id && data.peer_id) {\n const offer = claimOutstandingOffer(rootTopic, data.offer_id)\n\n if (offer) {\n void onMessage(\n rootTopic,\n {\n answer: data.answer.sdp,\n peerId: data.peer_id,\n peer: offer.peer\n },\n () => {}\n )\n }\n }\n }\n\n handlers[rootTopic] = topicHandler\n\n const topicState: {\n announce: () => void | Promise<void>\n isActive: boolean\n } = {\n announce: async () => {\n if (activeTokens[rootTopic] !== subscriptionToken) {\n return\n }\n\n if (!topicState.isActive) {\n void send(client, rootTopic, {\n left: 0,\n numwant: offerPoolSize,\n offers: []\n })\n return\n }\n\n const outstandingOffers = await ensureOutstandingOffers(\n rootTopic,\n getOffers\n )\n const offers = entries(outstandingOffers).map(([id, {offer}]) => ({\n offer_id: id,\n offer: {type: 'offer', sdp: offer}\n }))\n\n void send(client, rootTopic, {\n numwant: offerPoolSize,\n offers\n })\n },\n isActive: !context?.isPassive\n }\n\n trackerAnnounceMs[client.url] = defaultAnnounceMs\n const {announce} = topicState\n relayFns[rootTopic] = announce\n states[rootTopic] = topicState\n const initialInterval = topicState.isActive\n ? trackerAnnounceMs[client.url]\n : dormantAnnounceMs\n\n relayIntervals[rootTopic] = setInterval(announce, initialInterval)\n void announce()\n\n return () => {\n roomSubscriberCounts[rootTopic] = Math.max(\n 0,\n (roomSubscriberCounts[rootTopic] ?? 1) - 1\n )\n\n if (!roomSubscriberCounts[rootTopic]) {\n delete roomSubscriberCounts[rootTopic]\n }\n\n if (activeTokens[rootTopic] !== subscriptionToken) {\n if (!roomSubscriberCounts[rootTopic]) {\n reclaimAllOutstandingOffers(rootTopic)\n delete states[rootTopic]\n }\n\n return\n }\n\n const interval = relayIntervals[rootTopic]\n if (interval) {\n clearInterval(interval)\n delete relayIntervals[rootTopic]\n }\n\n if (handlers[rootTopic] === topicHandler) {\n delete handlers[rootTopic]\n }\n\n if (relayFns[rootTopic] === announce) {\n delete relayFns[rootTopic]\n }\n\n delete activeTokens[rootTopic]\n\n if (states[rootTopic] === topicState) {\n delete states[rootTopic]\n }\n\n if (!roomSubscriberCounts[rootTopic]) {\n reclaimAllOutstandingOffers(rootTopic)\n }\n }\n },\n\n announce: (client, rootTopic) => {\n const state = topicStates.forRelay(client)[rootTopic]\n const relayIntervals = announceIntervals.forRelay(client)\n const relayFns = announceFns.forRelay(client)\n const fn = relayFns[rootTopic]\n\n if (state) {\n state.isActive = true\n }\n\n if (fn && relayIntervals[rootTopic]) {\n clearInterval(relayIntervals[rootTopic])\n relayIntervals[rootTopic] = setInterval(() => {\n void fn()\n }, trackerAnnounceMs[client.url])\n void fn()\n }\n\n return trackerAnnounceMs[client.url]\n },\n\n deactivate: (client, rootTopic) => {\n const state = topicStates.forRelay(client)[rootTopic]\n const relayIntervals = announceIntervals.forRelay(client)\n const relayFns = announceFns.forRelay(client)\n const fn = relayFns[rootTopic]\n\n if (state) {\n state.isActive = false\n }\n\n reclaimAllOutstandingOffers(rootTopic)\n\n if (fn && relayIntervals[rootTopic]) {\n clearInterval(relayIntervals[rootTopic])\n relayIntervals[rootTopic] = setInterval(() => {\n void fn()\n }, dormantAnnounceMs)\n void fn()\n }\n }\n})\n\nexport const joinRoom: JoinRoom<TorrentRoomConfig> = (\n config,\n roomId,\n callbacks\n) =>\n joinRoomStrategy(\n {\n ...config,\n trickleIce: config.trickleIce ?? false\n },\n roomId,\n callbacks\n )\n\nexport const getRelaySockets = relayManager.getSockets\n\nexport {pauseRelayReconnection, resumeRelayReconnection, selfId}\n\nexport const defaultRelayUrls = [\n 'tracker.webtorrent.dev',\n 'tracker.openwebtorrent.com',\n 'tracker.btorrent.xyz',\n 'tracker.files.fm:7073/announce'\n].map(url => 'wss://' + url)\n\nexport type * from '@trystero-p2p/core'\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trystero-p2p/torrent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.25.1",
|
|
4
|
+
"description": "Simple, serverless WebRTC (via Torrent)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bittorrent",
|
|
7
7
|
"chat",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"serverless",
|
|
16
16
|
"signaling",
|
|
17
17
|
"torrent",
|
|
18
|
+
"trystero",
|
|
18
19
|
"webrtc",
|
|
19
20
|
"webtorrent"
|
|
20
21
|
],
|
|
@@ -34,6 +35,6 @@
|
|
|
34
35
|
"./package.json": "./package.json"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@trystero-p2p/core": "0.
|
|
38
|
+
"@trystero-p2p/core": "0.25.1"
|
|
38
39
|
}
|
|
39
40
|
}
|