@trystero-p2p/torrent 0.25.4 → 0.26.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
@@ -36,6 +36,7 @@ You can see what people are building with Trystero
36
36
  - [Audio and video](#audio-and-video)
37
37
  - [Advanced](#advanced)
38
38
  - [Binary metadata](#binary-metadata)
39
+ - [Receiving files](#receiving-files)
39
40
  - [Action promises](#action-promises)
40
41
  - [Progress updates](#progress-updates)
41
42
  - [Encryption](#encryption)
@@ -288,7 +289,9 @@ room.onPeerLeave = peerId =>
288
289
  > Actions are smart and handle serialization and chunking for you behind the
289
290
  > scenes. This means you can send very large files and whatever data you send
290
291
  > will be received on the other side as the same type (a number as a number, a
291
- > string as a string, an object as an object, binary as binary, etc.).
292
+ > string as a string, an object as an object, binary as binary, etc.). See
293
+ > [receiving files](#receiving-files) for optional size limits and acceptance
294
+ > callbacks.
292
295
 
293
296
  ## Audio and video
294
297
 
@@ -370,11 +373,38 @@ file.send(buffer, {
370
373
  })
371
374
  ```
372
375
 
376
+ ### Receiving files
377
+
378
+ Incoming data is accepted automatically. Use `onReceive` to accept or decline
379
+ payloads based on their size, sender, or metadata:
380
+
381
+ ```js
382
+ const file = room.makeAction('file', {
383
+ onReceive: ({byteLength}) => byteLength <= 100 * 1024 ** 2,
384
+ onMessage: (data, {metadata}) => saveFile(data, metadata.name)
385
+ })
386
+ ```
387
+
388
+ Return `true` or `false`, or a promise of either. Large payloads wait for
389
+ approval before sending their contents; declined small messages are discarded
390
+ after arrival. You can also assign `file.onReceive` later or set it to `null` to
391
+ restore automatic acceptance.
392
+
393
+ `byteLength` is the payload size in bytes (UTF-8 for strings/JSON), excluding
394
+ metadata. The context's `signal` aborts when the transfer ends; use it to
395
+ dismiss pending approval UI.
396
+
397
+ Each room defaults to a 256 MiB receive budget. Set `maxReceiveBytes` in the
398
+ room config to allow larger files or reduce memory use. Concurrent transfers
399
+ queue automatically when the budget is occupied. Received files must fit in
400
+ memory.
401
+
373
402
  ### Action promises
374
403
 
375
- Action sender functions return a promise that resolves when they're done
376
- sending. You can optionally use this to indicate to the user when a large
377
- transfer is done.
404
+ Sender promises resolve when local sending finishes; they do not confirm
405
+ delivery or application handling. If a receiver declines a large transfer, the
406
+ promise rejects with `error.kind === 'rejected'`. Use request actions when you
407
+ need a response.
378
408
 
379
409
  ```js
380
410
  await file.send(amplePayload)
@@ -754,6 +784,11 @@ the same namespace will return the same room instance.
754
784
  name. A custom password must match between any peers in the room for them to
755
785
  connect. See [encryption](#encryption) for more details.
756
786
 
787
+ - `maxReceiveBytes` - **(optional)** Maximum payload size and budget for
788
+ concurrent large incoming transfers per room, in bytes. Defaults to
789
+ `256 * 1024 ** 2` (256 MiB). Must be a positive integer. Raise it for larger
790
+ files or lower it to reduce memory use.
791
+
757
792
  - `passive` - **(optional)** Boolean for backup or relay peers that should
758
793
  listen for active peers without announcing themselves while a room is
759
794
  dormant. Passive peers activate only after hearing a non-passive peer and
@@ -1023,7 +1058,9 @@ Returns an object with the following methods:
1023
1058
 
1024
1059
  Message actions expose:
1025
1060
  - `send(data, [options])` - Sends data to peers and resolves when local
1026
- sending is complete.
1061
+ sending is complete. To cancel an in-progress send, pass an
1062
+ `AbortController`'s `signal` in the options (e.g.
1063
+ `send(data, {signal: controller.signal})`) and call `controller.abort()`.
1027
1064
  - `onMessage` - Nullable callback property for received messages.
1028
1065
  - `onReceiveProgress` - Nullable callback property for inbound progress.
1029
1066
 
@@ -1034,10 +1071,17 @@ Returns an object with the following methods:
1034
1071
  - `onRequest` - Nullable callback property that returns the response.
1035
1072
  - `onReceiveProgress` - Nullable callback property for inbound progress.
1036
1073
 
1074
+ Both kinds expose `onReceive`, a nullable callback accepting
1075
+ `{byteLength, peerId, metadata, kind, signal}` and returning a boolean or a
1076
+ promise of a boolean. It can also be provided in `config`. `kind` is
1077
+ `'message'`, `'request'`, or `'response'`; request actions apply it to both
1078
+ requests and responses. Responses have no metadata. See
1079
+ [receiving files](#receiving-files).
1080
+
1037
1081
  Send options use `target`, `metadata`, `onProgress`, and `signal`. Request
1038
1082
  options use `target`, `metadata`, `timeoutMs`, `onProgress`, and `signal`.
1039
1083
  `requestMany()` uses `targets` plus optional `onResult` for each peer result
1040
- as it arrives.
1084
+ as it arrives. Request timeouts cover approval, sending, and the response.
1041
1085
 
1042
1086
  Example:
1043
1087
 
package/dist/index.d.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { JoinRoom, JoinRoomConfig, pauseRelayReconnection, resumeRelayReconnection, selfId } from "@trystero-p2p/core";
2
2
  export type * from "@trystero-p2p/core";
3
3
  //#region src/index.d.ts
4
- type TorrentRoomConfig = JoinRoomConfig;
5
- declare const joinRoom: JoinRoom<TorrentRoomConfig>;
6
- declare const getRelaySockets: any;
7
- declare const defaultRelayUrls: string[];
4
+ export type TorrentRoomConfig = JoinRoomConfig;
5
+ export declare const joinRoom: JoinRoom<TorrentRoomConfig>;
6
+ export declare const getRelaySockets: any;
7
+ export declare const defaultRelayUrls: string[];
8
8
  //#endregion
9
- export { TorrentRoomConfig, defaultRelayUrls, getRelaySockets, joinRoom, pauseRelayReconnection, resumeRelayReconnection, selfId };
9
+ export { pauseRelayReconnection, resumeRelayReconnection, selfId };
10
10
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;KAmDY,oBAAoB;cAyZnB,UAAU,SAAS;cAcnB;cAIA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;YAqDY,oBAAoB;qBAsXnB,UAAU,SAAS;qBAcnB;qBAIA"}
package/dist/index.mjs CHANGED
@@ -3,18 +3,15 @@ import { createRelayManager, createStrategy, entries, fromJson, genId, getRelays
3
3
  const relayManager = createRelayManager((client) => client.socket);
4
4
  const topicToInfoHash = {};
5
5
  const infoHashToTopic = {};
6
- const announceFns = relayManager.scoped();
7
- const subscriptionTokens = relayManager.scoped();
8
- const trackerAnnounceMs = relayManager.scoped();
9
6
  const handledSignals = {};
10
- const msgHandlers = relayManager.scoped();
11
7
  const topicStates = relayManager.scoped();
12
8
  const roomOutstandingOffers = {};
13
9
  const roomOfferGenerationPromises = {};
14
10
  const roomSubscriberCounts = {};
15
11
  const trackerAction = "announce";
16
12
  const hashLimit = 20;
17
- const offerPoolSize = 3;
13
+ const offersPerAnnounce = 1;
14
+ const requestedPeers = 3;
18
15
  const defaultAnnounceMs = 1e4;
19
16
  const dormantAnnounceMs = 12e4;
20
17
  const offerRetentionMs = 12e4;
@@ -39,31 +36,23 @@ const deleteRoomOfferBookkeeping = (rootTopic) => {
39
36
  delete roomOutstandingOffers[rootTopic];
40
37
  delete roomOfferGenerationPromises[rootTopic];
41
38
  };
42
- const claimOutstandingOffer = (rootTopic, offerId) => {
39
+ const takeOutstandingOffer = (rootTopic, offerId, action) => {
43
40
  const outstandingOffers = roomOutstandingOffers[rootTopic];
44
41
  const offer = outstandingOffers?.[offerId];
45
42
  if (!offer) return;
46
43
  delete outstandingOffers[offerId];
47
- offer.claim?.();
44
+ offer[action]?.();
48
45
  if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) deleteRoomOfferBookkeeping(rootTopic);
49
46
  return offer;
50
47
  };
51
- const reclaimOutstandingOffer = (rootTopic, offerId) => {
52
- const outstandingOffers = roomOutstandingOffers[rootTopic];
53
- const offer = outstandingOffers?.[offerId];
54
- if (!offer) return;
55
- delete outstandingOffers[offerId];
56
- offer.reclaim?.();
57
- if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) deleteRoomOfferBookkeeping(rootTopic);
58
- };
59
48
  const reclaimAllOutstandingOffers = (rootTopic) => {
60
- keys(getRoomOutstandingOffers(rootTopic)).forEach((offerId) => reclaimOutstandingOffer(rootTopic, offerId));
49
+ keys(getRoomOutstandingOffers(rootTopic)).forEach((offerId) => takeOutstandingOffer(rootTopic, offerId, "reclaim"));
61
50
  deleteRoomOfferBookkeeping(rootTopic);
62
51
  };
63
52
  const pruneOutstandingOffers = (rootTopic) => {
64
53
  const now = Date.now();
65
54
  entries(getRoomOutstandingOffers(rootTopic)).forEach(([offerId, offer]) => {
66
- if (now - offer.createdAt > offerRetentionMs) reclaimOutstandingOffer(rootTopic, offerId);
55
+ if (now - offer.createdAt > offerRetentionMs) takeOutstandingOffer(rootTopic, offerId, "reclaim");
67
56
  });
68
57
  };
69
58
  const ensureOutstandingOffers = async (rootTopic, getOffers) => {
@@ -72,7 +61,7 @@ const ensureOutstandingOffers = async (rootTopic, getOffers) => {
72
61
  pruneOutstandingOffers(rootTopic);
73
62
  const outstandingOffers = getRoomOutstandingOffers(rootTopic);
74
63
  const outstandingCount = keys(outstandingOffers).length;
75
- const missingOffers = Math.max(0, offerPoolSize - outstandingCount);
64
+ const missingOffers = Math.max(0, offersPerAnnounce - outstandingCount);
76
65
  if (missingOffers > 0) (await getOffers(missingOffers)).forEach((peerAndOffer) => {
77
66
  outstandingOffers[genId(hashLimit)] = {
78
67
  ...peerAndOffer,
@@ -99,7 +88,8 @@ const joinRoomStrategy = createStrategy({
99
88
  return;
100
89
  }
101
90
  if (warnMsg && config.relayConfig?.warnOnRelayFailure !== false) warn(client.url, warnMsg);
102
- if (interval && topic && announceFns.forKey(rawUrl)[topic]) trackerAnnounceMs.forKey(rawUrl)[topic] = Math.min(Math.max(interval * 1e3, defaultAnnounceMs), offerRetentionMs);
91
+ const state = topic ? topicStates.forKey(rawUrl)[topic] : void 0;
92
+ if (interval && state) state.announceMs = Math.min(Math.max(interval * 1e3, defaultAnnounceMs), offerRetentionMs);
103
93
  if ((data.offer || data.answer) && topic && data.offer_id) {
104
94
  if (data.peer_id === selfId) return;
105
95
  const signalKey = `${topic}:${data.offer ? "offer" : "answer"}:${data.offer_id}:${data.peer_id ?? ""}`;
@@ -110,43 +100,15 @@ const joinRoomStrategy = createStrategy({
110
100
  entries(handledSignals).forEach(([key, handledAtMs]) => {
111
101
  if (nowMs - handledAtMs > signalDedupeWindowMs * 6) delete handledSignals[key];
112
102
  });
113
- msgHandlers.forKey(rawUrl)[topic]?.(data);
103
+ state?.handler(data);
114
104
  }
115
- }, () => Object.values(announceFns.forKey(rawUrl)).forEach((announce) => void announce())));
105
+ }, () => Object.values(topicStates.forKey(rawUrl)).forEach((state) => void state.announce())));
116
106
  return client.ready;
117
107
  }),
118
108
  subscribe: (client, rootTopic, _, onMessage, getOffers, context) => {
119
- const handlers = msgHandlers.forRelay(client);
120
- const relayFns = announceFns.forRelay(client);
121
- const relayAnnounceMs = trackerAnnounceMs.forRelay(client);
122
- const activeTokens = subscriptionTokens.forRelay(client);
123
109
  const states = topicStates.forRelay(client);
124
110
  const subscriptionToken = Symbol(rootTopic);
125
- activeTokens[rootTopic] = subscriptionToken;
126
111
  roomSubscriberCounts[rootTopic] = (roomSubscriberCounts[rootTopic] ?? 0) + 1;
127
- const topicHandler = (data) => {
128
- if (data.offer && data.peer_id && data.offer_id) onMessage(rootTopic, {
129
- offer: data.offer.sdp,
130
- peerId: data.peer_id,
131
- hasOutgoingOffer: keys(getRoomOutstandingOffers(rootTopic)).length > 0
132
- }, (_, signal) => void send(client, rootTopic, {
133
- answer: {
134
- type: "answer",
135
- sdp: fromJson(signal).answer
136
- },
137
- offer_id: data.offer_id,
138
- to_peer_id: data.peer_id
139
- }));
140
- else if (data.answer && data.offer_id && data.peer_id) {
141
- const offer = claimOutstandingOffer(rootTopic, data.offer_id);
142
- if (offer) onMessage(rootTopic, {
143
- answer: data.answer.sdp,
144
- peerId: data.peer_id,
145
- peer: offer.peer
146
- }, () => {});
147
- }
148
- };
149
- handlers[rootTopic] = topicHandler;
150
112
  let passiveAnnounceTimeout;
151
113
  const stopPassiveAnnouncements = () => {
152
114
  clearTimeout(passiveAnnounceTimeout);
@@ -154,16 +116,24 @@ const joinRoomStrategy = createStrategy({
154
116
  };
155
117
  const topicState = {
156
118
  announce: async () => {
157
- if (activeTokens[rootTopic] !== subscriptionToken) return;
119
+ if (states[rootTopic]?.token !== subscriptionToken) return;
158
120
  if (!topicState.isActive) {
159
121
  send(client, rootTopic, {
160
122
  left: 0,
161
- numwant: offerPoolSize,
123
+ numwant: requestedPeers,
162
124
  offers: []
163
125
  });
164
126
  return;
165
127
  }
166
- const offers = entries(await ensureOutstandingOffers(rootTopic, getOffers)).map(([id, { offer }]) => ({
128
+ let outstandingOffers;
129
+ try {
130
+ outstandingOffers = await ensureOutstandingOffers(rootTopic, getOffers);
131
+ } catch (error) {
132
+ if (states[rootTopic]?.token !== subscriptionToken) return;
133
+ throw error;
134
+ }
135
+ if (states[rootTopic]?.token !== subscriptionToken || !topicState.isActive) return;
136
+ const offers = entries(outstandingOffers).map(([id, { offer }]) => ({
167
137
  offer_id: id,
168
138
  offer: {
169
139
  type: "offer",
@@ -171,52 +141,67 @@ const joinRoomStrategy = createStrategy({
171
141
  }
172
142
  }));
173
143
  send(client, rootTopic, {
174
- numwant: offerPoolSize,
144
+ numwant: requestedPeers,
175
145
  offers
176
146
  });
177
147
  },
148
+ announceMs: defaultAnnounceMs,
149
+ handler: (data) => {
150
+ if (data.offer && data.peer_id && data.offer_id) onMessage(rootTopic, {
151
+ offer: data.offer.sdp,
152
+ offerId: data.offer_id,
153
+ peerId: data.peer_id
154
+ }, (_, signal) => void send(client, rootTopic, {
155
+ answer: {
156
+ type: "answer",
157
+ sdp: fromJson(signal).answer
158
+ },
159
+ offer_id: data.offer_id,
160
+ to_peer_id: data.peer_id
161
+ }));
162
+ else if (data.answer && data.offer_id && data.peer_id) {
163
+ const offer = takeOutstandingOffer(rootTopic, data.offer_id, "claim");
164
+ if (offer) {
165
+ onMessage(rootTopic, {
166
+ answer: data.answer.sdp,
167
+ offerId: data.offer_id,
168
+ peerId: data.peer_id,
169
+ peer: offer.peer
170
+ }, () => {});
171
+ topicState.announce();
172
+ }
173
+ }
174
+ },
178
175
  isActive: !context?.isPassive,
179
176
  startPassiveAnnouncements: () => {
180
177
  stopPassiveAnnouncements();
181
- if (activeTokens[rootTopic] !== subscriptionToken || topicState.isActive) return;
178
+ if (states[rootTopic]?.token !== subscriptionToken || topicState.isActive) return;
182
179
  topicState.announce();
183
- passiveAnnounceTimeout = setTimeout(topicState.startPassiveAnnouncements, Math.max(relayAnnounceMs[rootTopic] ?? dormantAnnounceMs, dormantAnnounceMs));
180
+ passiveAnnounceTimeout = setTimeout(topicState.startPassiveAnnouncements, Math.max(topicState.announceMs, dormantAnnounceMs));
184
181
  },
185
- stopPassiveAnnouncements
182
+ stopPassiveAnnouncements,
183
+ token: subscriptionToken
186
184
  };
187
- relayAnnounceMs[rootTopic] = defaultAnnounceMs;
188
- const { announce } = topicState;
189
- relayFns[rootTopic] = announce;
190
185
  states[rootTopic] = topicState;
191
186
  if (!topicState.isActive) topicState.startPassiveAnnouncements();
192
187
  return () => {
193
188
  topicState.stopPassiveAnnouncements();
194
189
  roomSubscriberCounts[rootTopic] = Math.max(0, (roomSubscriberCounts[rootTopic] ?? 1) - 1);
195
- if (!roomSubscriberCounts[rootTopic]) delete roomSubscriberCounts[rootTopic];
196
- if (activeTokens[rootTopic] !== subscriptionToken) {
197
- if (!roomSubscriberCounts[rootTopic]) {
198
- reclaimAllOutstandingOffers(rootTopic);
199
- delete states[rootTopic];
200
- }
201
- return;
190
+ if (!roomSubscriberCounts[rootTopic]) {
191
+ delete roomSubscriberCounts[rootTopic];
192
+ reclaimAllOutstandingOffers(rootTopic);
202
193
  }
203
- if (handlers[rootTopic] === topicHandler) delete handlers[rootTopic];
204
- if (relayFns[rootTopic] === announce) delete relayFns[rootTopic];
205
- delete relayAnnounceMs[rootTopic];
206
- delete activeTokens[rootTopic];
207
- if (states[rootTopic] === topicState) delete states[rootTopic];
208
- if (!roomSubscriberCounts[rootTopic]) reclaimAllOutstandingOffers(rootTopic);
194
+ if (states[rootTopic]?.token === subscriptionToken) delete states[rootTopic];
209
195
  };
210
196
  },
211
- announce: (client, rootTopic) => {
197
+ announce: async (client, rootTopic) => {
212
198
  const state = topicStates.forRelay(client)[rootTopic];
213
- const fn = announceFns.forRelay(client)[rootTopic];
214
199
  if (state) {
215
200
  state.stopPassiveAnnouncements();
216
201
  state.isActive = true;
202
+ await state.announce();
217
203
  }
218
- if (fn) fn();
219
- return trackerAnnounceMs.forRelay(client)[rootTopic] ?? defaultAnnounceMs;
204
+ return state?.announceMs ?? defaultAnnounceMs;
220
205
  },
221
206
  deactivate: (client, rootTopic) => {
222
207
  const state = topicStates.forRelay(client)[rootTopic];
@@ -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 announceFns = relayManager.scoped<() => void | Promise<void>>()\nconst subscriptionTokens = relayManager.scoped<symbol>()\nconst trackerAnnounceMs = relayManager.scoped<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 startPassiveAnnouncements: () => void\n stopPassiveAnnouncements: () => void\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 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(rawUrl, () =>\n makeSocket(\n rawUrl,\n 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 if (config.relayConfig?.warnOnRelayFailure !== false) {\n warn(client.url, errMsg, true)\n }\n\n return\n }\n\n if (warnMsg && config.relayConfig?.warnOnRelayFailure !== false) {\n warn(client.url, warnMsg)\n }\n\n if (interval && topic && announceFns.forKey(rawUrl)[topic]) {\n trackerAnnounceMs.forKey(rawUrl)[topic] = Math.min(\n Math.max(interval * 1000, defaultAnnounceMs),\n offerRetentionMs\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 Object.values(announceFns.forKey(rawUrl)).forEach(\n announce => void announce()\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 relayAnnounceMs = trackerAnnounceMs.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 let passiveAnnounceTimeout: ReturnType<typeof setTimeout> | undefined\n const stopPassiveAnnouncements = (): void => {\n clearTimeout(passiveAnnounceTimeout)\n passiveAnnounceTimeout = undefined\n }\n const topicState = {\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 startPassiveAnnouncements: (): void => {\n stopPassiveAnnouncements()\n\n if (\n activeTokens[rootTopic] !== subscriptionToken ||\n topicState.isActive\n ) {\n return\n }\n\n void topicState.announce()\n passiveAnnounceTimeout = setTimeout(\n topicState.startPassiveAnnouncements,\n Math.max(\n relayAnnounceMs[rootTopic] ?? dormantAnnounceMs,\n dormantAnnounceMs\n )\n )\n },\n stopPassiveAnnouncements\n }\n\n relayAnnounceMs[rootTopic] = defaultAnnounceMs\n const {announce} = topicState\n relayFns[rootTopic] = announce\n states[rootTopic] = topicState\n\n if (!topicState.isActive) {\n topicState.startPassiveAnnouncements()\n }\n\n return () => {\n topicState.stopPassiveAnnouncements()\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 if (handlers[rootTopic] === topicHandler) {\n delete handlers[rootTopic]\n }\n\n if (relayFns[rootTopic] === announce) {\n delete relayFns[rootTopic]\n }\n\n delete relayAnnounceMs[rootTopic]\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 relayFns = announceFns.forRelay(client)\n const fn = relayFns[rootTopic]\n\n if (state) {\n state.stopPassiveAnnouncements()\n state.isActive = true\n }\n\n if (fn) {\n void fn()\n }\n\n return trackerAnnounceMs.forRelay(client)[rootTopic] ?? defaultAnnounceMs\n },\n\n deactivate: (client, rootTopic) => {\n const state = topicStates.forRelay(client)[rootTopic]\n\n if (state) {\n state.isActive = false\n }\n\n reclaimAllOutstandingOffers(rootTopic)\n\n state?.startPassiveAnnouncements()\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 'open.ftorrent.com',\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> = {}\ntype TopicState = {\n announce: () => void | Promise<void>\n announceMs: number\n handler: (data: TrackerMessage) => void\n isActive: boolean\n startPassiveAnnouncements: () => void\n stopPassiveAnnouncements: () => void\n token: symbol\n}\n\nconst handledSignals: Record<string, number> = {}\nconst topicStates = relayManager.scoped<TopicState>()\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 offersPerAnnounce = 1\nconst requestedPeers = 3\nconst defaultAnnounceMs = 10_000\nconst dormantAnnounceMs = 120_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 takeOutstandingOffer = (\n rootTopic: string,\n offerId: string,\n action: 'claim' | 'reclaim'\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[action]?.()\n\n if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) {\n deleteRoomOfferBookkeeping(rootTopic)\n }\n\n return offer\n}\n\nconst reclaimAllOutstandingOffers = (rootTopic: string): void => {\n keys(getRoomOutstandingOffers(rootTopic)).forEach(offerId =>\n takeOutstandingOffer(rootTopic, offerId, 'reclaim')\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 takeOutstandingOffer(rootTopic, offerId, 'reclaim')\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, offersPerAnnounce - 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(rawUrl, () =>\n makeSocket(\n rawUrl,\n 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 if (config.relayConfig?.warnOnRelayFailure !== false) {\n warn(client.url, errMsg, true)\n }\n\n return\n }\n\n if (warnMsg && config.relayConfig?.warnOnRelayFailure !== false) {\n warn(client.url, warnMsg)\n }\n\n const state = topic ? topicStates.forKey(rawUrl)[topic] : undefined\n\n if (interval && state) {\n state.announceMs = Math.min(\n Math.max(interval * 1000, defaultAnnounceMs),\n offerRetentionMs\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 state?.handler(data)\n }\n },\n () =>\n Object.values(topicStates.forKey(rawUrl)).forEach(\n state => void state.announce()\n )\n )\n )\n\n return client.ready\n }),\n\n subscribe: (client, rootTopic, _, onMessage, getOffers, context) => {\n const states = topicStates.forRelay(client)\n const subscriptionToken = Symbol(rootTopic)\n\n roomSubscriberCounts[rootTopic] = (roomSubscriberCounts[rootTopic] ?? 0) + 1\n\n let passiveAnnounceTimeout: ReturnType<typeof setTimeout> | undefined\n const stopPassiveAnnouncements = (): void => {\n clearTimeout(passiveAnnounceTimeout)\n passiveAnnounceTimeout = undefined\n }\n const topicState: TopicState = {\n announce: async () => {\n if (states[rootTopic]?.token !== subscriptionToken) {\n return\n }\n\n if (!topicState.isActive) {\n void send(client, rootTopic, {\n left: 0,\n numwant: requestedPeers,\n offers: []\n })\n return\n }\n\n let outstandingOffers: Record<string, OfferRecord & {createdAt: number}>\n\n try {\n outstandingOffers = await ensureOutstandingOffers(\n rootTopic,\n getOffers\n )\n } catch (error) {\n if (states[rootTopic]?.token !== subscriptionToken) {\n return\n }\n\n throw error\n }\n\n if (\n states[rootTopic]?.token !== subscriptionToken ||\n !topicState.isActive\n ) {\n return\n }\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: requestedPeers,\n offers\n })\n },\n announceMs: defaultAnnounceMs,\n handler: (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 offerId: data.offer_id,\n peerId: data.peer_id\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 = takeOutstandingOffer(rootTopic, data.offer_id, 'claim')\n\n if (offer) {\n void onMessage(\n rootTopic,\n {\n answer: data.answer.sdp,\n offerId: data.offer_id,\n peerId: data.peer_id,\n peer: offer.peer\n },\n () => {}\n )\n void topicState.announce()\n }\n }\n },\n isActive: !context?.isPassive,\n startPassiveAnnouncements: (): void => {\n stopPassiveAnnouncements()\n\n if (\n states[rootTopic]?.token !== subscriptionToken ||\n topicState.isActive\n ) {\n return\n }\n\n void topicState.announce()\n passiveAnnounceTimeout = setTimeout(\n topicState.startPassiveAnnouncements,\n Math.max(topicState.announceMs, dormantAnnounceMs)\n )\n },\n stopPassiveAnnouncements,\n token: subscriptionToken\n }\n\n states[rootTopic] = topicState\n\n if (!topicState.isActive) {\n topicState.startPassiveAnnouncements()\n }\n\n return () => {\n topicState.stopPassiveAnnouncements()\n roomSubscriberCounts[rootTopic] = Math.max(\n 0,\n (roomSubscriberCounts[rootTopic] ?? 1) - 1\n )\n\n if (!roomSubscriberCounts[rootTopic]) {\n delete roomSubscriberCounts[rootTopic]\n reclaimAllOutstandingOffers(rootTopic)\n }\n\n if (states[rootTopic]?.token === subscriptionToken) {\n delete states[rootTopic]\n }\n }\n },\n\n announce: async (client, rootTopic) => {\n const state = topicStates.forRelay(client)[rootTopic]\n\n if (state) {\n state.stopPassiveAnnouncements()\n state.isActive = true\n await state.announce()\n }\n\n return state?.announceMs ?? defaultAnnounceMs\n },\n\n deactivate: (client, rootTopic) => {\n const state = topicStates.forRelay(client)[rootTopic]\n\n if (state) {\n state.isActive = false\n }\n\n reclaimAllOutstandingOffers(rootTopic)\n\n state?.startPassiveAnnouncements()\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 'open.ftorrent.com',\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,6 +1,6 @@
1
1
  {
2
2
  "name": "@trystero-p2p/torrent",
3
- "version": "0.25.4",
3
+ "version": "0.26.0",
4
4
  "description": "Simple, serverless WebRTC (via Torrent)",
5
5
  "keywords": [
6
6
  "bittorrent",
@@ -35,6 +35,6 @@
35
35
  "./package.json": "./package.json"
36
36
  },
37
37
  "dependencies": {
38
- "@trystero-p2p/core": "0.25.4"
38
+ "@trystero-p2p/core": "0.26.0"
39
39
  }
40
40
  }