@trystero-p2p/torrent 0.26.0 → 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 +237 -1033
- package/dist/index.d.mts +4 -7
- package/dist/index.mjs +247 -145
- 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
|
-
//#region src/index.d.ts
|
|
1
|
+
import { JoinRoom, JoinRoomConfig, clearUserIdentity, createUserIdentity, exportUserIdentity, importUserIdentity, roles, scale, setRelayConnectivity } from "@trystero-p2p/core";
|
|
2
|
+
export type * from "@trystero-p2p/core/types";
|
|
4
3
|
export type TorrentRoomConfig = JoinRoomConfig;
|
|
5
4
|
export declare const joinRoom: JoinRoom<TorrentRoomConfig>;
|
|
6
|
-
export declare const getRelaySockets:
|
|
5
|
+
export declare const getRelaySockets: () => Record<string, WebSocket>;
|
|
7
6
|
export declare const defaultRelayUrls: string[];
|
|
8
|
-
|
|
9
|
-
export { pauseRelayReconnection, resumeRelayReconnection, selfId };
|
|
10
|
-
//# sourceMappingURL=index.d.mts.map
|
|
7
|
+
export { clearUserIdentity, createUserIdentity, exportUserIdentity, importUserIdentity, roles, scale, setRelayConnectivity };
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { createRelayManager, createStrategy,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
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();
|
|
11
12
|
const trackerAction = "announce";
|
|
12
13
|
const hashLimit = 20;
|
|
13
14
|
const offersPerAnnounce = 1;
|
|
@@ -17,108 +18,216 @@ const dormantAnnounceMs = 12e4;
|
|
|
17
18
|
const offerRetentionMs = 12e4;
|
|
18
19
|
const signalDedupeWindowMs = 4e3;
|
|
19
20
|
const defaultRedundancy = 3;
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
});
|
|
26
34
|
};
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
};
|
|
65
|
+
};
|
|
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
|
+
}
|
|
38
81
|
};
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
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);
|
|
109
|
+
};
|
|
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);
|
|
42
114
|
if (!offer) return;
|
|
43
|
-
delete
|
|
115
|
+
peerSession.pool.offers.delete(offerId);
|
|
44
116
|
offer[action]?.();
|
|
45
|
-
if (!keys(outstandingOffers).length && !roomSubscriberCounts[rootTopic]) deleteRoomOfferBookkeeping(rootTopic);
|
|
46
117
|
return offer;
|
|
47
118
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
deleteRoomOfferBookkeeping(rootTopic);
|
|
51
|
-
};
|
|
52
|
-
const pruneOutstandingOffers = (rootTopic) => {
|
|
119
|
+
const pruneOutstandingOffers = (peerSession) => {
|
|
120
|
+
if (peerSession.pool.phase === "closed") return;
|
|
53
121
|
const now = Date.now();
|
|
54
|
-
|
|
55
|
-
if (now - offer.createdAt > offerRetentionMs) takeOutstandingOffer(rootTopic, offerId, "reclaim");
|
|
56
|
-
});
|
|
122
|
+
for (const [offerId, offer] of peerSession.pool.offers) if (now - offer.createdAt > offerRetentionMs) takeOutstandingOffer(peerSession, offerId, "reclaim");
|
|
57
123
|
};
|
|
58
|
-
const ensureOutstandingOffers = async (
|
|
59
|
-
while (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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, {
|
|
67
151
|
...peerAndOffer,
|
|
68
|
-
createdAt
|
|
69
|
-
};
|
|
152
|
+
createdAt
|
|
153
|
+
});
|
|
70
154
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
}
|
|
77
169
|
};
|
|
78
|
-
const
|
|
170
|
+
const joinRoom = /*@__PURE__*/ createStrategy({
|
|
171
|
+
defaultConfig: { trickleIce: false },
|
|
79
172
|
init: (config) => getRelays(config, defaultRelayUrls, defaultRedundancy).map((rawUrl) => {
|
|
80
|
-
const client = relayManager.register(rawUrl, () =>
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
107
213
|
}),
|
|
108
214
|
subscribe: (client, rootTopic, _, onMessage, getOffers, context) => {
|
|
109
|
-
const
|
|
215
|
+
const relayTopics = topicStates.forRelay(client);
|
|
110
216
|
const subscriptionToken = Symbol(rootTopic);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
226
|
const topicState = {
|
|
118
227
|
announce: async () => {
|
|
119
|
-
if (
|
|
120
|
-
if (
|
|
121
|
-
send(client,
|
|
228
|
+
if (peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
229
|
+
if (isPassive) {
|
|
230
|
+
await send(client, topicSession, peerId, {
|
|
122
231
|
left: 0,
|
|
123
232
|
numwant: requestedPeers,
|
|
124
233
|
offers: []
|
|
@@ -127,20 +236,20 @@ const joinRoomStrategy = createStrategy({
|
|
|
127
236
|
}
|
|
128
237
|
let outstandingOffers;
|
|
129
238
|
try {
|
|
130
|
-
outstandingOffers = await ensureOutstandingOffers(
|
|
239
|
+
outstandingOffers = await ensureOutstandingOffers(peerSession, getOffers);
|
|
131
240
|
} catch (error) {
|
|
132
|
-
if (
|
|
241
|
+
if (peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
133
242
|
throw error;
|
|
134
243
|
}
|
|
135
|
-
if (
|
|
136
|
-
const offers = entries(
|
|
244
|
+
if (!outstandingOffers || peerMap.get(peerId)?.token !== subscriptionToken) return;
|
|
245
|
+
const offers = [...outstandingOffers.entries()].map(([id, { offer }]) => ({
|
|
137
246
|
offer_id: id,
|
|
138
247
|
offer: {
|
|
139
248
|
type: "offer",
|
|
140
249
|
sdp: offer
|
|
141
250
|
}
|
|
142
251
|
}));
|
|
143
|
-
send(client,
|
|
252
|
+
await send(client, topicSession, peerId, {
|
|
144
253
|
numwant: requestedPeers,
|
|
145
254
|
offers
|
|
146
255
|
});
|
|
@@ -151,7 +260,7 @@ const joinRoomStrategy = createStrategy({
|
|
|
151
260
|
offer: data.offer.sdp,
|
|
152
261
|
offerId: data.offer_id,
|
|
153
262
|
peerId: data.peer_id
|
|
154
|
-
}, (_, signal) => void send(client,
|
|
263
|
+
}, (_, signal) => void send(client, topicSession, peerId, {
|
|
155
264
|
answer: {
|
|
156
265
|
type: "answer",
|
|
157
266
|
sdp: fromJson(signal).answer
|
|
@@ -160,7 +269,7 @@ const joinRoomStrategy = createStrategy({
|
|
|
160
269
|
to_peer_id: data.peer_id
|
|
161
270
|
}));
|
|
162
271
|
else if (data.answer && data.offer_id && data.peer_id) {
|
|
163
|
-
const offer = takeOutstandingOffer(
|
|
272
|
+
const offer = takeOutstandingOffer(peerSession, data.offer_id, "claim");
|
|
164
273
|
if (offer) {
|
|
165
274
|
onMessage(rootTopic, {
|
|
166
275
|
answer: data.answer.sdp,
|
|
@@ -172,57 +281,50 @@ const joinRoomStrategy = createStrategy({
|
|
|
172
281
|
}
|
|
173
282
|
}
|
|
174
283
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
stopPassiveAnnouncements();
|
|
178
|
-
if (states[rootTopic]?.token !== subscriptionToken || topicState.isActive) return;
|
|
179
|
-
topicState.announce();
|
|
180
|
-
passiveAnnounceTimeout = setTimeout(topicState.startPassiveAnnouncements, Math.max(topicState.announceMs, dormantAnnounceMs));
|
|
181
|
-
},
|
|
182
|
-
stopPassiveAnnouncements,
|
|
284
|
+
peerId,
|
|
285
|
+
passiveTimer: null,
|
|
183
286
|
token: subscriptionToken
|
|
184
287
|
};
|
|
185
|
-
|
|
186
|
-
if (
|
|
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;
|
|
187
301
|
return () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
302
|
+
if (closed) return;
|
|
303
|
+
closed = true;
|
|
304
|
+
if (topicState.passiveTimer !== null) {
|
|
305
|
+
clearTimeout(topicState.passiveTimer);
|
|
306
|
+
topicState.passiveTimer = null;
|
|
307
|
+
}
|
|
308
|
+
if (peerMap.get(peerId)?.token === subscriptionToken) {
|
|
309
|
+
peerMap.delete(peerId);
|
|
310
|
+
if (peerMap.size === 0 && relayTopics[rootTopic] === peerMap) delete relayTopics[rootTopic];
|
|
193
311
|
}
|
|
194
|
-
|
|
312
|
+
releasePeerSession(topicSession, peerId, peerSession);
|
|
195
313
|
};
|
|
196
314
|
},
|
|
197
|
-
announce: async (client, rootTopic) => {
|
|
198
|
-
const state = topicStates.forRelay(client)[rootTopic];
|
|
199
|
-
|
|
200
|
-
state.stopPassiveAnnouncements();
|
|
201
|
-
state.isActive = true;
|
|
202
|
-
await state.announce();
|
|
203
|
-
}
|
|
315
|
+
announce: async (client, rootTopic, _selfTopic, context) => {
|
|
316
|
+
const state = topicStates.forRelay(client)[rootTopic]?.get(context?.peerId ?? "");
|
|
317
|
+
await state?.announce();
|
|
204
318
|
return state?.announceMs ?? defaultAnnounceMs;
|
|
205
|
-
},
|
|
206
|
-
deactivate: (client, rootTopic) => {
|
|
207
|
-
const state = topicStates.forRelay(client)[rootTopic];
|
|
208
|
-
if (state) state.isActive = false;
|
|
209
|
-
reclaimAllOutstandingOffers(rootTopic);
|
|
210
|
-
state?.startPassiveAnnouncements();
|
|
211
319
|
}
|
|
212
320
|
});
|
|
213
|
-
const
|
|
214
|
-
...config,
|
|
215
|
-
trickleIce: config.trickleIce ?? false
|
|
216
|
-
}, roomId, callbacks);
|
|
217
|
-
const getRelaySockets = relayManager.getSockets;
|
|
321
|
+
const getRelaySockets = () => relayManager.getSockets();
|
|
218
322
|
const defaultRelayUrls = [
|
|
219
|
-
"open.ftorrent.com",
|
|
220
|
-
"tracker.webtorrent.dev",
|
|
221
|
-
"tracker.openwebtorrent.com",
|
|
222
|
-
"tracker.btorrent.xyz",
|
|
223
|
-
"tracker.files.fm:7073/announce"
|
|
224
|
-
]
|
|
225
|
-
//#endregion
|
|
226
|
-
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
|
+
];
|
|
227
329
|
|
|
228
|
-
|
|
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":";;;YAqDY,oBAAoB;qBAsXnB,UAAU,SAAS;qBAcnB;qBAIA"}
|
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> = {}\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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"}
|