@trystero-p2p/torrent 0.25.4 → 0.27.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 +238 -990
- package/dist/index.d.mts +7 -10
- package/dist/index.mjs +283 -196
- package/package.json +11 -2
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { JoinRoom, JoinRoomConfig,
|
|
2
|
-
export type * from "@trystero-p2p/core";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
declare const
|
|
6
|
-
declare const
|
|
7
|
-
|
|
8
|
-
//#endregion
|
|
9
|
-
export { TorrentRoomConfig, defaultRelayUrls, getRelaySockets, joinRoom, pauseRelayReconnection, resumeRelayReconnection, selfId };
|
|
10
|
-
//# sourceMappingURL=index.d.mts.map
|
|
1
|
+
import { JoinRoom, JoinRoomConfig, clearUserIdentity, createUserIdentity, exportUserIdentity, importUserIdentity, roles, scale, setRelayConnectivity } from "@trystero-p2p/core";
|
|
2
|
+
export type * from "@trystero-p2p/core/types";
|
|
3
|
+
export type TorrentRoomConfig = JoinRoomConfig;
|
|
4
|
+
export declare const joinRoom: JoinRoom<TorrentRoomConfig>;
|
|
5
|
+
export declare const getRelaySockets: () => Record<string, WebSocket>;
|
|
6
|
+
export declare const defaultRelayUrls: string[];
|
|
7
|
+
export { clearUserIdentity, createUserIdentity, exportUserIdentity, importUserIdentity, roles, scale, setRelayConnectivity };
|
package/dist/index.mjs
CHANGED
|
@@ -1,243 +1,330 @@
|
|
|
1
|
-
import { createRelayManager, createStrategy,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const topicStates =
|
|
12
|
-
const roomOutstandingOffers = {};
|
|
13
|
-
const roomOfferGenerationPromises = {};
|
|
14
|
-
const roomSubscriberCounts = {};
|
|
1
|
+
import { clearUserIdentity, createRelayManager, createRelayScopedStore, createStrategy, createUserIdentity, exportUserIdentity, fromJson, getRelays, hashTopic, importUserIdentity, libName, makeSocket, mkErr, roles, scale, setRelayConnectivity, toJson, unrefTimer } from "@trystero-p2p/core";
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
const unreachable = (x) => {
|
|
5
|
+
throw mkErr(`unreachable: ${String(x)}`);
|
|
6
|
+
};
|
|
7
|
+
const relayManager = /*@__PURE__*/ createRelayManager((client) => client.socket, (client) => client.close?.());
|
|
8
|
+
const topicSessions = /*@__PURE__*/ new Map();
|
|
9
|
+
const infoHashToTopic = /*@__PURE__*/ new Map();
|
|
10
|
+
const handledSignals = /*@__PURE__*/ new Map();
|
|
11
|
+
const topicStates = /*@__PURE__*/ createRelayScopedStore();
|
|
15
12
|
const trackerAction = "announce";
|
|
16
13
|
const hashLimit = 20;
|
|
17
|
-
const
|
|
14
|
+
const offersPerAnnounce = 1;
|
|
15
|
+
const requestedPeers = 3;
|
|
18
16
|
const defaultAnnounceMs = 1e4;
|
|
19
17
|
const dormantAnnounceMs = 12e4;
|
|
20
18
|
const offerRetentionMs = 12e4;
|
|
21
19
|
const signalDedupeWindowMs = 4e3;
|
|
22
20
|
const defaultRedundancy = 3;
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
const resolveSessionInfoHash = (topicSession) => {
|
|
22
|
+
if (topicSession.closed) return Promise.resolve(null);
|
|
23
|
+
if (topicSession.infoHash !== null) return Promise.resolve(topicSession.infoHash);
|
|
24
|
+
return topicSession.infoHashP ??= hashTopic(topicSession.rootTopic).then((fullHash) => {
|
|
25
|
+
if (topicSession.closed) return null;
|
|
26
|
+
const hash = fullHash.slice(0, hashLimit);
|
|
27
|
+
topicSession.infoHash = hash;
|
|
28
|
+
infoHashToTopic.set(hash, topicSession.rootTopic);
|
|
29
|
+
return hash;
|
|
30
|
+
}, (err) => {
|
|
31
|
+
topicSession.infoHashP = null;
|
|
32
|
+
throw err;
|
|
33
|
+
});
|
|
29
34
|
};
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const acquirePeerSession = (rootTopic, peerId) => {
|
|
36
|
+
let topicSession = topicSessions.get(rootTopic);
|
|
37
|
+
if (!topicSession || topicSession.closed) {
|
|
38
|
+
topicSession = {
|
|
39
|
+
rootTopic,
|
|
40
|
+
closed: false,
|
|
41
|
+
infoHash: null,
|
|
42
|
+
infoHashP: null,
|
|
43
|
+
peers: /* @__PURE__ */ new Map()
|
|
44
|
+
};
|
|
45
|
+
topicSessions.set(rootTopic, topicSession);
|
|
46
|
+
resolveSessionInfoHash(topicSession).catch(() => {});
|
|
47
|
+
}
|
|
48
|
+
let peerSession = topicSession.peers.get(peerId);
|
|
49
|
+
if (!peerSession) {
|
|
50
|
+
peerSession = {
|
|
51
|
+
subscriberCount: 0,
|
|
52
|
+
nextGeneration: 0,
|
|
53
|
+
pool: {
|
|
54
|
+
phase: "idle",
|
|
55
|
+
offers: /* @__PURE__ */ new Map()
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
topicSession.peers.set(peerId, peerSession);
|
|
59
|
+
}
|
|
60
|
+
peerSession.subscriberCount += 1;
|
|
61
|
+
return {
|
|
62
|
+
topicSession,
|
|
63
|
+
peerSession
|
|
64
|
+
};
|
|
41
65
|
};
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
const disposeOfferPool = (peerSession) => {
|
|
67
|
+
switch (peerSession.pool.phase) {
|
|
68
|
+
case "idle":
|
|
69
|
+
case "generating": {
|
|
70
|
+
const existingOffers = [...peerSession.pool.offers.values()];
|
|
71
|
+
peerSession.pool.offers.clear();
|
|
72
|
+
peerSession.pool = { phase: "closed" };
|
|
73
|
+
existingOffers.forEach((offer) => {
|
|
74
|
+
offer.reclaim?.();
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case "closed": return;
|
|
79
|
+
default: unreachable(peerSession.pool);
|
|
80
|
+
}
|
|
50
81
|
};
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
82
|
+
const releasePeerSession = (topicSession, peerId, peerSession) => {
|
|
83
|
+
peerSession.subscriberCount = Math.max(0, peerSession.subscriberCount - 1);
|
|
84
|
+
if (peerSession.subscriberCount === 0) {
|
|
85
|
+
disposeOfferPool(peerSession);
|
|
86
|
+
if (topicSession.peers.get(peerId) === peerSession) topicSession.peers.delete(peerId);
|
|
87
|
+
}
|
|
88
|
+
if (topicSession.peers.size === 0 && !topicSession.closed) {
|
|
89
|
+
topicSession.closed = true;
|
|
90
|
+
if (topicSession.infoHash !== null) infoHashToTopic.delete(topicSession.infoHash);
|
|
91
|
+
if (topicSessions.get(topicSession.rootTopic) === topicSession) topicSessions.delete(topicSession.rootTopic);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const send = (client, topicSession, peerId, payload) => {
|
|
95
|
+
const transmit = (infoHash) => {
|
|
96
|
+
if (infoHash === null || client.isClosed) return;
|
|
97
|
+
client.send(toJson({
|
|
98
|
+
action: trackerAction,
|
|
99
|
+
info_hash: infoHash,
|
|
100
|
+
peer_id: peerId,
|
|
101
|
+
...payload
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
104
|
+
if (topicSession.infoHash !== null && !topicSession.closed) {
|
|
105
|
+
transmit(topicSession.infoHash);
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
}
|
|
108
|
+
return resolveSessionInfoHash(topicSession).then(transmit);
|
|
58
109
|
};
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
110
|
+
const warn = (url, msg, didFail = false) => console.warn(`${libName}: torrent tracker ${didFail ? "failure" : "warning"} from ${url} - ${msg}`);
|
|
111
|
+
const takeOutstandingOffer = (peerSession, offerId, action) => {
|
|
112
|
+
if (peerSession.pool.phase === "closed") return;
|
|
113
|
+
const offer = peerSession.pool.offers.get(offerId);
|
|
114
|
+
if (!offer) return;
|
|
115
|
+
peerSession.pool.offers.delete(offerId);
|
|
116
|
+
offer[action]?.();
|
|
117
|
+
return offer;
|
|
62
118
|
};
|
|
63
|
-
const pruneOutstandingOffers = (
|
|
119
|
+
const pruneOutstandingOffers = (peerSession) => {
|
|
120
|
+
if (peerSession.pool.phase === "closed") return;
|
|
64
121
|
const now = Date.now();
|
|
65
|
-
|
|
66
|
-
if (now - offer.createdAt > offerRetentionMs) reclaimOutstandingOffer(rootTopic, offerId);
|
|
67
|
-
});
|
|
122
|
+
for (const [offerId, offer] of peerSession.pool.offers) if (now - offer.createdAt > offerRetentionMs) takeOutstandingOffer(peerSession, offerId, "reclaim");
|
|
68
123
|
};
|
|
69
|
-
const ensureOutstandingOffers = async (
|
|
70
|
-
while (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
124
|
+
const ensureOutstandingOffers = async (peerSession, getOffers) => {
|
|
125
|
+
while (peerSession.pool.phase === "generating") await peerSession.pool.promise;
|
|
126
|
+
if (peerSession.pool.phase === "closed") return null;
|
|
127
|
+
pruneOutstandingOffers(peerSession);
|
|
128
|
+
if (peerSession.pool.phase !== "idle") return null;
|
|
129
|
+
const { offers } = peerSession.pool;
|
|
130
|
+
const missingOffers = Math.max(0, offersPerAnnounce - offers.size);
|
|
131
|
+
if (missingOffers === 0) return offers;
|
|
132
|
+
const generation = ++peerSession.nextGeneration;
|
|
133
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
134
|
+
peerSession.pool = {
|
|
135
|
+
phase: "generating",
|
|
136
|
+
offers,
|
|
137
|
+
promise,
|
|
138
|
+
generation
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
const allocated = await getOffers(missingOffers);
|
|
142
|
+
if (peerSession.pool.phase !== "generating" || peerSession.pool.generation !== generation) {
|
|
143
|
+
allocated.forEach((offer) => {
|
|
144
|
+
offer.reclaim?.();
|
|
145
|
+
});
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const createdAt = Date.now();
|
|
149
|
+
allocated.forEach((peerAndOffer) => {
|
|
150
|
+
offers.set(peerAndOffer.offerId, {
|
|
78
151
|
...peerAndOffer,
|
|
79
|
-
createdAt
|
|
80
|
-
};
|
|
152
|
+
createdAt
|
|
153
|
+
});
|
|
81
154
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
155
|
+
peerSession.pool = {
|
|
156
|
+
phase: "idle",
|
|
157
|
+
offers
|
|
158
|
+
};
|
|
159
|
+
return offers;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (peerSession.pool.phase === "generating" && peerSession.pool.generation === generation) peerSession.pool = {
|
|
162
|
+
phase: "idle",
|
|
163
|
+
offers
|
|
164
|
+
};
|
|
165
|
+
throw err;
|
|
166
|
+
} finally {
|
|
167
|
+
resolve();
|
|
168
|
+
}
|
|
88
169
|
};
|
|
89
|
-
const
|
|
170
|
+
const joinRoom = /*@__PURE__*/ createStrategy({
|
|
171
|
+
defaultConfig: { trickleIce: false },
|
|
90
172
|
init: (config) => getRelays(config, defaultRelayUrls, defaultRedundancy).map((rawUrl) => {
|
|
91
|
-
const client = relayManager.register(rawUrl, () =>
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
173
|
+
const client = relayManager.register(rawUrl, () => {
|
|
174
|
+
const created = makeSocket(rawUrl, (rawData) => {
|
|
175
|
+
const parsed = fromJson(rawData);
|
|
176
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
177
|
+
const data = parsed;
|
|
178
|
+
const errMsg = data["failure reason"];
|
|
179
|
+
const warnMsg = data["warning message"];
|
|
180
|
+
const { interval } = data;
|
|
181
|
+
const topic = typeof data.info_hash === "string" ? infoHashToTopic.get(data.info_hash) : void 0;
|
|
182
|
+
if (errMsg) {
|
|
183
|
+
if (config.relayConfig?.warnOnRelayFailure !== false) warn(created.url, errMsg, true);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (warnMsg && config.relayConfig?.warnOnRelayFailure !== false) warn(created.url, warnMsg);
|
|
187
|
+
const peerMap = topic ? topicStates.forRelay(created)[topic] : void 0;
|
|
188
|
+
if (!peerMap || !topic) return;
|
|
189
|
+
if (interval) {
|
|
190
|
+
const clampedMs = Math.min(Math.max(interval * 1e3, defaultAnnounceMs), offerRetentionMs);
|
|
191
|
+
peerMap.forEach((state) => {
|
|
192
|
+
state.announceMs = clampedMs;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if ((data.offer || data.answer) && data.offer_id) {
|
|
196
|
+
const signalType = data.offer ? "offer" : "answer";
|
|
197
|
+
const nowMs = Date.now();
|
|
198
|
+
for (const [key, handledAtMs] of handledSignals) if (nowMs - handledAtMs > signalDedupeWindowMs * 6) handledSignals.delete(key);
|
|
199
|
+
for (const state of peerMap.values()) {
|
|
200
|
+
if (state.peerId && data.peer_id === state.peerId) continue;
|
|
201
|
+
if (data.to_peer_id && state.peerId && data.to_peer_id !== state.peerId) continue;
|
|
202
|
+
const signalKey = `${topic}:${state.peerId}:${signalType}:${data.offer_id}:${data.peer_id ?? ""}`;
|
|
203
|
+
const lastHandledMs = handledSignals.get(signalKey);
|
|
204
|
+
if (typeof lastHandledMs === "number" && nowMs - lastHandledMs < signalDedupeWindowMs) continue;
|
|
205
|
+
handledSignals.set(signalKey, nowMs);
|
|
206
|
+
state.handler(data);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}, () => Object.values(topicStates.forRelay(created)).forEach((peerMap) => peerMap.forEach((state) => void state.announce())));
|
|
210
|
+
return created;
|
|
211
|
+
});
|
|
212
|
+
return relayManager.lease(client, client.ready);
|
|
117
213
|
}),
|
|
118
214
|
subscribe: (client, rootTopic, _, onMessage, getOffers, context) => {
|
|
119
|
-
const
|
|
120
|
-
const relayFns = announceFns.forRelay(client);
|
|
121
|
-
const relayAnnounceMs = trackerAnnounceMs.forRelay(client);
|
|
122
|
-
const activeTokens = subscriptionTokens.forRelay(client);
|
|
123
|
-
const states = topicStates.forRelay(client);
|
|
215
|
+
const relayTopics = topicStates.forRelay(client);
|
|
124
216
|
const subscriptionToken = Symbol(rootTopic);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
let passiveAnnounceTimeout;
|
|
151
|
-
const stopPassiveAnnouncements = () => {
|
|
152
|
-
clearTimeout(passiveAnnounceTimeout);
|
|
153
|
-
passiveAnnounceTimeout = void 0;
|
|
154
|
-
};
|
|
217
|
+
const isPassive = Boolean(context?.config.passive);
|
|
218
|
+
const peerId = context?.peerId ?? "";
|
|
219
|
+
const { topicSession, peerSession } = acquirePeerSession(rootTopic, peerId);
|
|
220
|
+
const peerMap = relayTopics[rootTopic] ??= /* @__PURE__ */ new Map();
|
|
221
|
+
const prevState = peerMap.get(peerId);
|
|
222
|
+
if (prevState && prevState.passiveTimer !== null) {
|
|
223
|
+
clearTimeout(prevState.passiveTimer);
|
|
224
|
+
prevState.passiveTimer = null;
|
|
225
|
+
}
|
|
155
226
|
const topicState = {
|
|
156
227
|
announce: async () => {
|
|
157
|
-
if (
|
|
158
|
-
if (
|
|
159
|
-
send(client,
|
|
228
|
+
if (peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
229
|
+
if (isPassive) {
|
|
230
|
+
await send(client, topicSession, peerId, {
|
|
160
231
|
left: 0,
|
|
161
|
-
numwant:
|
|
232
|
+
numwant: requestedPeers,
|
|
162
233
|
offers: []
|
|
163
234
|
});
|
|
164
235
|
return;
|
|
165
236
|
}
|
|
166
|
-
|
|
237
|
+
let outstandingOffers;
|
|
238
|
+
try {
|
|
239
|
+
outstandingOffers = await ensureOutstandingOffers(peerSession, getOffers);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
if (!outstandingOffers || peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
245
|
+
const offers = [...outstandingOffers.entries()].map(([id, { offer }]) => ({
|
|
167
246
|
offer_id: id,
|
|
168
247
|
offer: {
|
|
169
248
|
type: "offer",
|
|
170
249
|
sdp: offer
|
|
171
250
|
}
|
|
172
251
|
}));
|
|
173
|
-
send(client,
|
|
174
|
-
numwant:
|
|
252
|
+
await send(client, topicSession, peerId, {
|
|
253
|
+
numwant: requestedPeers,
|
|
175
254
|
offers
|
|
176
255
|
});
|
|
177
256
|
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
257
|
+
announceMs: defaultAnnounceMs,
|
|
258
|
+
handler: (data) => {
|
|
259
|
+
if (data.offer && data.peer_id && data.offer_id) onMessage(rootTopic, {
|
|
260
|
+
offer: data.offer.sdp,
|
|
261
|
+
offerId: data.offer_id,
|
|
262
|
+
peerId: data.peer_id
|
|
263
|
+
}, (_, signal) => void send(client, topicSession, peerId, {
|
|
264
|
+
answer: {
|
|
265
|
+
type: "answer",
|
|
266
|
+
sdp: fromJson(signal).answer
|
|
267
|
+
},
|
|
268
|
+
offer_id: data.offer_id,
|
|
269
|
+
to_peer_id: data.peer_id
|
|
270
|
+
}));
|
|
271
|
+
else if (data.answer && data.offer_id && data.peer_id) {
|
|
272
|
+
const offer = takeOutstandingOffer(peerSession, data.offer_id, "claim");
|
|
273
|
+
if (offer) {
|
|
274
|
+
onMessage(rootTopic, {
|
|
275
|
+
answer: data.answer.sdp,
|
|
276
|
+
offerId: data.offer_id,
|
|
277
|
+
peerId: data.peer_id,
|
|
278
|
+
peer: offer.peer
|
|
279
|
+
}, () => {});
|
|
280
|
+
topicState.announce();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
184
283
|
},
|
|
185
|
-
|
|
284
|
+
peerId,
|
|
285
|
+
passiveTimer: null,
|
|
286
|
+
token: subscriptionToken
|
|
186
287
|
};
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
288
|
+
peerMap.set(peerId, topicState);
|
|
289
|
+
if (isPassive) {
|
|
290
|
+
const schedulePassiveAnnounce = () => {
|
|
291
|
+
if (peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
292
|
+
topicState.announce();
|
|
293
|
+
if (peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
294
|
+
if (topicState.passiveTimer !== null) clearTimeout(topicState.passiveTimer);
|
|
295
|
+
topicState.passiveTimer = setTimeout(schedulePassiveAnnounce, Math.max(topicState.announceMs, dormantAnnounceMs));
|
|
296
|
+
unrefTimer(topicState.passiveTimer);
|
|
297
|
+
};
|
|
298
|
+
schedulePassiveAnnounce();
|
|
299
|
+
}
|
|
300
|
+
let closed = false;
|
|
192
301
|
return () => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
reclaimAllOutstandingOffers(rootTopic);
|
|
199
|
-
delete states[rootTopic];
|
|
200
|
-
}
|
|
201
|
-
return;
|
|
302
|
+
if (closed) return;
|
|
303
|
+
closed = true;
|
|
304
|
+
if (topicState.passiveTimer !== null) {
|
|
305
|
+
clearTimeout(topicState.passiveTimer);
|
|
306
|
+
topicState.passiveTimer = null;
|
|
202
307
|
}
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!roomSubscriberCounts[rootTopic]) reclaimAllOutstandingOffers(rootTopic);
|
|
308
|
+
if (peerMap.get(peerId)?.token === subscriptionToken) {
|
|
309
|
+
peerMap.delete(peerId);
|
|
310
|
+
if (peerMap.size === 0 && relayTopics[rootTopic] === peerMap) delete relayTopics[rootTopic];
|
|
311
|
+
}
|
|
312
|
+
releasePeerSession(topicSession, peerId, peerSession);
|
|
209
313
|
};
|
|
210
314
|
},
|
|
211
|
-
announce: (client, rootTopic) => {
|
|
212
|
-
const state = topicStates.forRelay(client)[rootTopic];
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
state.stopPassiveAnnouncements();
|
|
216
|
-
state.isActive = true;
|
|
217
|
-
}
|
|
218
|
-
if (fn) fn();
|
|
219
|
-
return trackerAnnounceMs.forRelay(client)[rootTopic] ?? defaultAnnounceMs;
|
|
220
|
-
},
|
|
221
|
-
deactivate: (client, rootTopic) => {
|
|
222
|
-
const state = topicStates.forRelay(client)[rootTopic];
|
|
223
|
-
if (state) state.isActive = false;
|
|
224
|
-
reclaimAllOutstandingOffers(rootTopic);
|
|
225
|
-
state?.startPassiveAnnouncements();
|
|
315
|
+
announce: async (client, rootTopic, _selfTopic, context) => {
|
|
316
|
+
const state = topicStates.forRelay(client)[rootTopic]?.get(context?.peerId ?? "");
|
|
317
|
+
await state?.announce();
|
|
318
|
+
return state?.announceMs ?? defaultAnnounceMs;
|
|
226
319
|
}
|
|
227
320
|
});
|
|
228
|
-
const
|
|
229
|
-
...config,
|
|
230
|
-
trickleIce: config.trickleIce ?? false
|
|
231
|
-
}, roomId, callbacks);
|
|
232
|
-
const getRelaySockets = relayManager.getSockets;
|
|
321
|
+
const getRelaySockets = () => relayManager.getSockets();
|
|
233
322
|
const defaultRelayUrls = [
|
|
234
|
-
"open.ftorrent.com",
|
|
235
|
-
"tracker.webtorrent.dev",
|
|
236
|
-
"tracker.openwebtorrent.com",
|
|
237
|
-
"tracker.btorrent.xyz",
|
|
238
|
-
"tracker.files.fm:7073/announce"
|
|
239
|
-
]
|
|
240
|
-
//#endregion
|
|
241
|
-
export { defaultRelayUrls, getRelaySockets, joinRoom, pauseRelayReconnection, resumeRelayReconnection, selfId };
|
|
323
|
+
"wss://open.ftorrent.com",
|
|
324
|
+
"wss://tracker.webtorrent.dev",
|
|
325
|
+
"wss://tracker.openwebtorrent.com",
|
|
326
|
+
"wss://tracker.btorrent.xyz",
|
|
327
|
+
"wss://tracker.files.fm:7073/announce"
|
|
328
|
+
];
|
|
242
329
|
|
|
243
|
-
|
|
330
|
+
export { clearUserIdentity, createUserIdentity, defaultRelayUrls, exportUserIdentity, getRelaySockets, importUserIdentity, joinRoom, roles, scale, setRelayConnectivity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trystero-p2p/torrent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Simple, serverless WebRTC (via Torrent)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bittorrent",
|
|
@@ -25,16 +25,25 @@
|
|
|
25
25
|
"email": "dan@oxism.com",
|
|
26
26
|
"url": "https://oxism.com"
|
|
27
27
|
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/dmotz/trystero.git",
|
|
31
|
+
"directory": "packages/torrent"
|
|
32
|
+
},
|
|
28
33
|
"files": [
|
|
29
34
|
"dist"
|
|
30
35
|
],
|
|
31
36
|
"type": "module",
|
|
37
|
+
"sideEffects": false,
|
|
32
38
|
"types": "./dist/index.d.mts",
|
|
33
39
|
"exports": {
|
|
34
40
|
".": "./dist/index.mjs",
|
|
35
41
|
"./package.json": "./package.json"
|
|
36
42
|
},
|
|
37
43
|
"dependencies": {
|
|
38
|
-
"@trystero-p2p/core": "0.
|
|
44
|
+
"@trystero-p2p/core": "0.27.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22"
|
|
39
48
|
}
|
|
40
49
|
}
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;KAmDY,oBAAoB;cAyZnB,UAAU,SAAS;cAcnB;cAIA"}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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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"}
|