@trystero-p2p/torrent 0.24.0 → 0.25.0

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 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(peerId => console.log(`${peerId} joined`))
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(peerId => console.log(`${peerId} left`))
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
- (stream, peerId) => (peerElements[peerId].video.srcObject = stream)
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 [sendDrink, getDrink] = room.makeAction('drink')
190
+ const drink = room.makeAction('drink')
192
191
 
193
192
  // buy drink for a friend
194
- sendDrink({drink: 'negroni', withIce: true}, friendId)
193
+ drink.send({drink: 'negroni', withIce: true}, {target: friendId})
195
194
 
196
- // buy round for the house (second argument omitted)
197
- sendDrink({drink: 'mezcal', withIce: false})
195
+ // buy round for the house
196
+ drink.send({drink: 'mezcal', withIce: false})
198
197
 
199
198
  // listen for drinks sent to you
200
- getDrink((data, peerId) =>
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 [sendCursor, getCursor] = room.makeAction<CursorMove>('cursor-move')
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 [sendPic, getPic] = room.makeAction('pic')
258
+ const pic = room.makeAction('pic')
219
259
 
220
260
  // blobs are automatically handled, as are any form of TypedArray
221
- canvas.toBlob(blob => sendPic(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
- getPic(
226
- (data, peerId) => (imgs[peerId].src = URL.createObjectURL(new Blob([data])))
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 [sendName, getName] = room.makeAction('name')
273
+ const name = room.makeAction('name')
235
274
 
236
275
  // tell new peers your name when they connect
237
- room.onPeerJoin(peerId => sendName('Oedipa', peerId))
276
+ room.onPeerJoin = peerId => name.send('Oedipa', {target: peerId})
238
277
 
239
278
  // listen for peers naming themselves
240
- getName((name, peerId) => (idsToNames[peerId] = name))
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 => sendName(e.target.value))
282
+ nameInput.addEventListener('change', e => name.send(e.target.value))
244
283
 
245
- room.onPeerLeave(peerId =>
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(peerId => room.addStream(selfStream, peerId))
311
+ room.onPeerJoin = peerId => room.addStream(selfStream, {target: peerId})
274
312
 
275
313
  // handle streams from other peers
276
- room.onPeerStream((stream, peerId) => {
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((stream, peerId) => {
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 [sendFile, getFile] = room.makeAction('file')
360
+ const file = room.makeAction('file')
323
361
 
324
- getFile((data, peerId, metadata) =>
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
- // to send metadata, pass a third argument
332
- // to broadcast to the whole room, set the second peer ID argument to null
333
- sendFile(buffer, null, {name: 'The Courierʼs Tragedy', type: 'application/pdf'})
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 sendFile(amplePayload)
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
- sendFile(
356
- payload,
357
- // notice the peer target argument for any action sender can be a single peer
358
- // ID, an array of IDs, or null (meaning send to all peers in the room)
359
- [peerIdA, peerIdB, peerIdC],
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 [sendFile, getFile, onFileProgress] = room.makeAction('file')
402
+ const file = room.makeAction('file')
373
403
 
374
- onFileProgress((percent, peerId, metadata) =>
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 [sendColor, getColor] = room.makeAction('color')
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(peer => sendColor(myColor, peer))
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
- getColor((color, peer) =>
434
- setPeerColors(peerColors => ({...peerColors, [peer]: color}))
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
- sendColor(value)
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 `createStrategy`.
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 {createStrategy, selfId, toJson} from '@trystero-p2p/core'
630
+ import {createTopicStrategy, toJson} from '@trystero-p2p/core'
601
631
 
602
- export const joinRoom = createStrategy({
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 takes 5 arguments:
615
- // 1. One of the relay clients you defined in init.
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 {topic, payload} = JSON.parse(String(event.data))
647
+ const message = JSON.parse(String(event.data))
629
648
 
630
- if (!topics.includes(topic)) {
649
+ if (message.topic !== topic) {
631
650
  return
632
651
  }
633
652
 
634
- onMessage(topic, payload, (peerTopic, signal) =>
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
- topics.forEach(topic => client.send(toJson({type: 'subscribe', topic})))
657
+ client.send(toJson({type: 'subscribe', topic}))
643
658
 
644
659
  return () => {
645
- topics.forEach(topic => client.send(toJson({type: 'unsubscribe', topic})))
660
+ client.send(toJson({type: 'unsubscribe', topic}))
646
661
  client.removeEventListener('message', onWsMessage)
647
662
  }
648
663
  },
649
664
 
650
- // announce takes a relay client and a rootTopic
651
- announce: (client, rootTopic) =>
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: rootTopic,
656
- payload: toJson({peerId: selfId})
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 or when handshake admission fails (including timeout). `details` is
796
- an object containing `appId`, `roomId`, `peerId`, and `error` describing the
797
- failure.
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
- - ### `addStream(stream, [targetPeers], [metadata])`
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
- - `targetPeers` - **(optional)** If specified, the stream is sent only to the
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 type) to
866
- be sent with the stream. This is useful when sending multiple streams so
867
- recipients know which is which (e.g. a webcam versus a screen capture). If
868
- you want to broadcast a stream to all peers in the room with a metadata
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, [targetPeers])`
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
- - `targetPeers` - **(optional)** If specified, the stream is removed only from
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, [targetPeers], [metadata])`
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
- - `targetPeers` - **(optional)** If specified, the track is sent only to the
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 type) to
890
- be sent with the track. See `metadata` notes for `addStream()` above for
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, [targetPeers])`
926
+ - ### `removeTrack(track, [options])`
894
927
 
895
928
  Removes a media track.
896
929
  - `track` - The `MediaStreamTrack` to remove.
897
930
 
898
- - `targetPeers` - **(optional)** If specified, the track is removed only from
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, [targetPeers], [metadata])`
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
- - `targetPeers` - **(optional)** If specified, the track is replaced only for
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 type) to
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(callback)`
947
+ - ### `onPeerJoin`
915
948
 
916
- Registers a callback function that will be called when a peer joins the room.
917
- If called more than once, only the latest callback registered is ever called.
918
- Existing active peers are immediately replayed to a newly registered callback.
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(peerId => console.log(`${peerId} joined`))
958
+ room.onPeerJoin = peerId => console.log(`${peerId} joined`)
926
959
  ```
927
960
 
928
- - ### `onPeerLeave(callback)`
961
+ - ### `onPeerLeave`
929
962
 
930
- Registers a callback function that will be called when a peer leaves the room.
931
- If called more than once, only the latest callback registered is ever called.
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(peerId => console.log(`${peerId} left`))
971
+ room.onPeerLeave = peerId => console.log(`${peerId} left`)
939
972
  ```
940
973
 
941
- - ### `onPeerStream(callback)`
974
+ - ### `onPeerStream`
942
975
 
943
- Registers a callback function that will be called when a peer sends a media
944
- stream. If called more than once, only the latest callback registered is ever
945
- called.
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((stream, peerId) =>
986
+ room.onPeerStream = (stream, peerId) =>
954
987
  console.log(`got stream from ${peerId}`, stream)
955
- )
956
988
  ```
957
989
 
958
- - ### `onPeerTrack(callback)`
990
+ - ### `onPeerTrack`
959
991
 
960
- Registers a callback function that will be called when a peer sends a media
961
- track. If called more than once, only the latest callback registered is ever
962
- called.
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((track, stream, peerId) =>
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
- Returns an array of three functions:
981
- 1. #### Sender
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
- - `peerId` - The ID string of the sending peer.
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
- - `metadata` - **(optional)** Optional metadata object supplied by the
1014
- sender if `data` is binary, e.g. a filename.
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
- 3. #### Progress handler
1017
- - Registers a callback function that runs when partial data is received
1018
- from peers. You can use this for tracking large binary transfers. See
1019
- [Progress updates](#progress-updates) for an example.
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 [sendCursor, getCursor] = room.makeAction('cursormove')
1035
+ const cursor = room.makeAction('cursormove')
1034
1036
 
1035
- window.addEventListener('mousemove', e => sendCursor([e.clientX, e.clientY]))
1037
+ window.addEventListener('mousemove', e => cursor.send([e.clientX, e.clientY]))
1036
1038
 
1037
- getCursor(([x, y], peerId) => {
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(peerId =>
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;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;KA8CY,iBAAA,GAAoB,cAAA;AAAA,cA6VnB,QAAA,EAAU,QAAA,CAAS,iBAAA;AAAA,cAcnB,eAAA;AAAA,cAIA,gBAAA"}
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 announce = async () => {
160
- if (activeTokens[rootTopic] !== subscriptionToken) return;
161
- send(client, rootTopic, {
162
- numwant: offerPoolSize,
163
- offers: entries(await ensureOutstandingOffers(rootTopic, getOffers)).map(([id, { offer }]) => ({
164
- offer_id: id,
165
- offer: {
166
- type: "offer",
167
- sdp: offer
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
- relayIntervals[rootTopic] = setInterval(announce, trackerAnnounceMs[client.url]);
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]) reclaimAllOutstandingOffers(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) => trackerAnnounceMs[client.url]
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,
@@ -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.24.0",
4
- "description": "Torrent tracker strategy for Trystero",
3
+ "version": "0.25.0",
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.24.0"
38
+ "@trystero-p2p/core": "0.25.0"
38
39
  }
39
40
  }