bun-torrent 0.0.1-beta

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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/package.json +49 -0
  4. package/src/app.ts +65 -0
  5. package/src/client.error.ts +12 -0
  6. package/src/client.test.ts +117 -0
  7. package/src/client.ts +185 -0
  8. package/src/index.test.ts +7 -0
  9. package/src/index.ts +76 -0
  10. package/src/peer/__tests__/peer-id.test.ts +24 -0
  11. package/src/peer/availability.test.ts +50 -0
  12. package/src/peer/availability.ts +88 -0
  13. package/src/peer/consts.ts +11 -0
  14. package/src/peer/handshake/handshake.error.ts +15 -0
  15. package/src/peer/handshake/handshake.test.ts +125 -0
  16. package/src/peer/handshake/index.ts +83 -0
  17. package/src/peer/index.ts +31 -0
  18. package/src/peer/messages/error.ts +16 -0
  19. package/src/peer/messages/helpers.ts +36 -0
  20. package/src/peer/messages/index.ts +174 -0
  21. package/src/peer/messages/messages.test.ts +231 -0
  22. package/src/peer/messages/types.ts +76 -0
  23. package/src/peer/peer-id.ts +9 -0
  24. package/src/peer/pool/index.ts +301 -0
  25. package/src/peer/pool/pool.error.ts +17 -0
  26. package/src/peer/pool/pool.test.ts +305 -0
  27. package/src/peer/session/index.ts +276 -0
  28. package/src/peer/session/session.error.ts +19 -0
  29. package/src/peer/session/session.test.ts +110 -0
  30. package/src/peer/types.ts +6 -0
  31. package/src/torrent/bencode/__tests__/decoder.test.ts +212 -0
  32. package/src/torrent/bencode/__tests__/encoder.test.ts +138 -0
  33. package/src/torrent/bencode/__tests__/encoder.unit.test.ts +24 -0
  34. package/src/torrent/bencode/__tests__/integration.test.ts +64 -0
  35. package/src/torrent/bencode/decoder.error.ts +36 -0
  36. package/src/torrent/bencode/decoder.ts +180 -0
  37. package/src/torrent/bencode/encoder.error.ts +14 -0
  38. package/src/torrent/bencode/encoder.ts +86 -0
  39. package/src/torrent/bencode/index.ts +9 -0
  40. package/src/torrent/bencode/types.ts +15 -0
  41. package/src/torrent/bencode/utils.ts +17 -0
  42. package/src/torrent/download/index.ts +9 -0
  43. package/src/torrent/download/manager.test.ts +393 -0
  44. package/src/torrent/download/manager.ts +376 -0
  45. package/src/torrent/file-selection.ts +51 -0
  46. package/src/torrent/index.ts +61 -0
  47. package/src/torrent/parser/helpers.ts +44 -0
  48. package/src/torrent/parser/index.ts +197 -0
  49. package/src/torrent/parser/info-hash.test.ts +39 -0
  50. package/src/torrent/parser/info-hash.ts +7 -0
  51. package/src/torrent/parser/parser.error.ts +21 -0
  52. package/src/torrent/parser/parser.test.ts +286 -0
  53. package/src/torrent/pieces/DefaultPlanner.ts +257 -0
  54. package/src/torrent/pieces/index.ts +20 -0
  55. package/src/torrent/pieces/planner.error.ts +18 -0
  56. package/src/torrent/pieces/planner.test.ts +303 -0
  57. package/src/torrent/pieces/planner.ts +16 -0
  58. package/src/torrent/pieces/types.ts +59 -0
  59. package/src/torrent/pieces/utils.ts +85 -0
  60. package/src/torrent/pieces/validation.test.ts +32 -0
  61. package/src/torrent/pieces/validation.ts +27 -0
  62. package/src/torrent/session/index.ts +195 -0
  63. package/src/torrent/session/session.test.ts +279 -0
  64. package/src/torrent/storage/index.ts +161 -0
  65. package/src/torrent/storage/storage.error.ts +18 -0
  66. package/src/torrent/storage/storage.test.ts +326 -0
  67. package/src/torrent/storage/types.ts +13 -0
  68. package/src/torrent/types.ts +16 -0
  69. package/src/tracker/http.test.ts +66 -0
  70. package/src/tracker/http.ts +137 -0
  71. package/src/tracker/index.ts +93 -0
  72. package/src/tracker/tracker.error.ts +26 -0
  73. package/src/tracker/types.ts +17 -0
  74. package/src/tracker/udp.test.ts +155 -0
  75. package/src/tracker/udp.ts +234 -0
  76. package/src/utils/__tests__/sha1.test.ts +16 -0
  77. package/src/utils/buffers.ts +28 -0
  78. package/src/utils/errors.ts +9 -0
  79. package/src/utils/formats.ts +8 -0
  80. package/src/utils/sha1.ts +4 -0
@@ -0,0 +1,86 @@
1
+ import { compareBytes, concatBytes } from '@utils/buffers';
2
+ import { BencodeEncodeError, BencodeEncodeErrorCode } from './encoder.error';
3
+ import { FLAG, type BValue } from './types';
4
+
5
+ export const encodeBencode = (input: BValue): Uint8Array => {
6
+ const encoder = createEncoder({ input });
7
+ return encoder.encode();
8
+ };
9
+
10
+ const IDENTIFIERS = {
11
+ integer: new Uint8Array([FLAG.INTEGER]),
12
+ delimiter: new Uint8Array([FLAG.STR_DELIMITER]),
13
+ list: new Uint8Array([FLAG.LIST]),
14
+ dictionary: new Uint8Array([FLAG.DICTIONARY]),
15
+ end: new Uint8Array([FLAG.END]),
16
+ } as const;
17
+
18
+ const createEncoder = ({ input }: { input: BValue }) => {
19
+ const textEncoder = new TextEncoder('utf-8');
20
+ const buffer: Array<Uint8Array> = [];
21
+
22
+ const pushAscii = (value: string): void => {
23
+ buffer.push(textEncoder.encode(value));
24
+ };
25
+
26
+ const encodeInteger = (value: number): void => {
27
+ if (!Number.isInteger(value))
28
+ fail(BencodeEncodeErrorCode.INVALID_INTEGER, 'Bencode integer must be an integer');
29
+ if (!Number.isSafeInteger(value))
30
+ fail(BencodeEncodeErrorCode.UNSAFE_INTEGER, 'Bencode integer must be safe');
31
+ if (Object.is(value, -0))
32
+ fail(BencodeEncodeErrorCode.NEGATIVE_ZERO, 'Bencode integer cannot be -0');
33
+
34
+ buffer.push(IDENTIFIERS.integer);
35
+ pushAscii(String(value));
36
+ buffer.push(IDENTIFIERS.end);
37
+ };
38
+
39
+ const encodeBytes = (data: Uint8Array): void => {
40
+ pushAscii(String(data.byteLength));
41
+ buffer.push(IDENTIFIERS.delimiter, data);
42
+ };
43
+
44
+ const encodeList = (list: BValue[]): void => {
45
+ buffer.push(IDENTIFIERS.list);
46
+ for (const value of list) {
47
+ encodeValue(value);
48
+ }
49
+ buffer.push(IDENTIFIERS.end);
50
+ };
51
+
52
+ const encodeDictionary = (dict: Map<string, BValue>): void => {
53
+ buffer.push(IDENTIFIERS.dictionary);
54
+
55
+ const entries = [...dict.entries()].sort(([left], [right]) =>
56
+ compareBytes(textEncoder.encode(left), textEncoder.encode(right)),
57
+ );
58
+
59
+ for (const [key, value] of entries) {
60
+ encodeBytes(textEncoder.encode(key));
61
+ encodeValue(value);
62
+ }
63
+
64
+ buffer.push(IDENTIFIERS.end);
65
+ };
66
+
67
+ const encodeValue = (value: BValue): void => {
68
+ if (typeof value === 'number') return encodeInteger(value);
69
+ if (value instanceof Uint8Array) return encodeBytes(value);
70
+ if (Array.isArray(value)) return encodeList(value);
71
+ if (value instanceof Map) return encodeDictionary(value);
72
+
73
+ return fail(BencodeEncodeErrorCode.UNSUPPORTED_VALUE, 'Unsupported bencode value');
74
+ };
75
+
76
+ const encode = (): Uint8Array => {
77
+ encodeValue(input);
78
+ return concatBytes(buffer);
79
+ };
80
+
81
+ const fail = (code: BencodeEncodeErrorCode, message: string): never => {
82
+ throw new BencodeEncodeError(code, message);
83
+ };
84
+
85
+ return { encode };
86
+ };
@@ -0,0 +1,9 @@
1
+ export { decodeBencode } from './decoder';
2
+ export { encodeBencode } from './encoder';
3
+ export { toBValue } from './utils';
4
+
5
+ export { BencodeDecodeError, BencodeDecodeErrorCode } from './decoder.error';
6
+ export { BencodeEncodeError, BencodeEncodeErrorCode } from './encoder.error';
7
+
8
+ export type { BBytes, BDict, BInteger, BList, BValue } from './types';
9
+ export type { BencodeInput } from './utils';
@@ -0,0 +1,15 @@
1
+ export type BValue = BBytes | BInteger | BList | BDict;
2
+
3
+ export type BBytes = Uint8Array;
4
+ export type BInteger = number;
5
+ export type BList = BValue[];
6
+ export type BDict = Map<string, BValue>;
7
+
8
+ export enum FLAG {
9
+ INTEGER = 0x69,
10
+ MINUS = 0x2d,
11
+ LIST = 0x6c,
12
+ DICTIONARY = 0x64,
13
+ END = 0x65,
14
+ STR_DELIMITER = 0x3a,
15
+ }
@@ -0,0 +1,17 @@
1
+ import type { BValue } from './types';
2
+
3
+ export type BencodeInput =
4
+ | number
5
+ | string
6
+ | Uint8Array
7
+ | BencodeInput[]
8
+ | { readonly [key: string]: BencodeInput };
9
+
10
+ export const toBValue = (input: BencodeInput): BValue => {
11
+ if (typeof input === 'number') return input;
12
+ if (typeof input === 'string') return new TextEncoder('utf-8').encode(input);
13
+ if (input instanceof Uint8Array) return input;
14
+ if (Array.isArray(input)) return input.map(toBValue);
15
+
16
+ return new Map(Object.entries(input).map(([key, value]) => [key, toBValue(value)]));
17
+ };
@@ -0,0 +1,9 @@
1
+ export { DownloadManager } from './manager';
2
+ export type {
3
+ DownloadManagerOptions,
4
+ DownloadPeerPool,
5
+ DownloadPeerSession,
6
+ DownloadProgress,
7
+ DownloadProgressEventMode,
8
+ DownloadProgressListener,
9
+ } from './manager';
@@ -0,0 +1,393 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import type { PeerMessage } from '@peer/messages';
4
+ import { createPiecePlanner, type PieceAvailability, type PieceCompletion } from '../pieces';
5
+ import type { TorrentMetadata } from '../types';
6
+ import {
7
+ DownloadManager,
8
+ type DownloadManagerOptions,
9
+ type DownloadPeerPool,
10
+ type DownloadProgress,
11
+ } from './manager';
12
+
13
+ const makeMetadata = ({
14
+ length = 4,
15
+ pieceLength = 4,
16
+ pieces = 1,
17
+ }: {
18
+ length?: number;
19
+ pieceLength?: number;
20
+ pieces?: number;
21
+ } = {}): TorrentMetadata => ({
22
+ announceList: [],
23
+ infoHash: new Uint8Array(20),
24
+ name: 'file.bin',
25
+ pieceLength,
26
+ pieces: Array.from({ length: pieces }, (_, index) => new Uint8Array(20).fill(index)),
27
+ length,
28
+ files: [{ path: ['file.bin'], length, offset: 0 }],
29
+ });
30
+
31
+ class FakeAvailability implements PieceAvailability {
32
+ public constructor(private readonly pieces: Set<number>) {}
33
+
34
+ public hasPiece(pieceIndex: number): boolean {
35
+ return this.pieces.has(pieceIndex);
36
+ }
37
+ }
38
+
39
+ class FakePeer {
40
+ public sent: PeerMessage[] = [];
41
+ public peerAvailability: PieceAvailability;
42
+
43
+ private readonly messageListeners = new Set<(message: PeerMessage) => void>();
44
+ private readonly closeListeners = new Set<() => void>();
45
+
46
+ public constructor({
47
+ choked = true,
48
+ pieces = [0],
49
+ }: {
50
+ choked?: boolean;
51
+ pieces?: number[];
52
+ } = {}) {
53
+ this.choked = choked;
54
+ this.peerAvailability = new FakeAvailability(new Set(pieces));
55
+ }
56
+
57
+ public choked: boolean;
58
+
59
+ public onClose(callback: () => void): () => void {
60
+ this.closeListeners.add(callback);
61
+ return () => this.closeListeners.delete(callback);
62
+ }
63
+
64
+ public onMessage(callback: (message: PeerMessage) => void): () => void {
65
+ this.messageListeners.add(callback);
66
+ return () => this.messageListeners.delete(callback);
67
+ }
68
+
69
+ public sendMessage(message: PeerMessage): void {
70
+ this.sent.push(message);
71
+ }
72
+
73
+ public emit(message: PeerMessage): void {
74
+ if (message.type === 'choke') this.choked = true;
75
+ if (message.type === 'unchoke') this.choked = false;
76
+
77
+ for (const listener of this.messageListeners) listener(message);
78
+ }
79
+
80
+ public close(): void {
81
+ for (const listener of this.closeListeners) listener();
82
+ }
83
+ }
84
+
85
+ class FakePeerPool implements DownloadPeerPool<FakePeer> {
86
+ private readonly peers: FakePeer[] = [];
87
+ private readonly listeners = new Set<(peer: FakePeer) => void>();
88
+
89
+ public onSession(callback: (session: FakePeer) => void): () => void {
90
+ this.listeners.add(callback);
91
+ for (const peer of this.peers) callback(peer);
92
+
93
+ return () => {
94
+ this.listeners.delete(callback);
95
+ };
96
+ }
97
+
98
+ public add(peer: FakePeer): void {
99
+ this.peers.push(peer);
100
+ for (const listener of this.listeners) listener(peer);
101
+ }
102
+ }
103
+
104
+ type WriteValidatedPiece = NonNullable<DownloadManagerOptions<FakePeer>['writeValidatedPiece']>;
105
+
106
+ const makeWriteValidated = ({
107
+ completions,
108
+ valid = true,
109
+ }: {
110
+ completions?: PieceCompletion[];
111
+ valid?: boolean;
112
+ } = {}): WriteValidatedPiece => {
113
+ return async (_metadata, piece) => {
114
+ completions?.push(piece);
115
+
116
+ return {
117
+ pieceIndex: piece.pieceIndex,
118
+ actualHash: new Uint8Array(20),
119
+ expectedHash: piece.expectedHash,
120
+ valid,
121
+ };
122
+ };
123
+ };
124
+
125
+ const nextTick = async (): Promise<void> => {
126
+ await new Promise((resolve) => setTimeout(resolve, 0));
127
+ };
128
+
129
+ const requestMessages = (peer: FakePeer): Extract<PeerMessage, { type: 'request' }>[] =>
130
+ peer.sent.filter((message): message is Extract<PeerMessage, { type: 'request' }> => {
131
+ return message.type === 'request';
132
+ });
133
+
134
+ describe('DownloadManager', () => {
135
+ test('sends interested while choked when the peer has useful pieces', () => {
136
+ const pool = new FakePeerPool();
137
+ const manager = new DownloadManager({
138
+ metadata: makeMetadata(),
139
+ outputDirectory: '/tmp/download',
140
+ peerPool: pool,
141
+ writeValidatedPiece: makeWriteValidated(),
142
+ });
143
+ const peer = new FakePeer({ choked: true });
144
+
145
+ manager.start();
146
+ pool.add(peer);
147
+
148
+ expect(peer.sent).toEqual([{ type: 'interested' }]);
149
+ });
150
+
151
+ test('requests blocks from an unchoked peer and respects the in-flight limit', () => {
152
+ const pool = new FakePeerPool();
153
+ const manager = new DownloadManager({
154
+ metadata: makeMetadata({ length: 8, pieceLength: 4, pieces: 2 }),
155
+ outputDirectory: '/tmp/download',
156
+ peerPool: pool,
157
+ maxInFlightRequestsPerPeer: 2,
158
+ writeValidatedPiece: makeWriteValidated(),
159
+ });
160
+ const peer = new FakePeer({ choked: false, pieces: [0, 1] });
161
+
162
+ manager.start();
163
+ pool.add(peer);
164
+
165
+ expect(peer.sent[0]).toEqual({ type: 'interested' });
166
+ expect(requestMessages(peer)).toEqual([
167
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
168
+ { type: 'request', pieceIndex: 1, offset: 0, length: 4 },
169
+ ]);
170
+ });
171
+
172
+ test('writes completed pieces and resolves when the torrent is complete', async () => {
173
+ const completions: PieceCompletion[] = [];
174
+ const pool = new FakePeerPool();
175
+ const manager = new DownloadManager({
176
+ metadata: makeMetadata(),
177
+ outputDirectory: '/tmp/download',
178
+ peerPool: pool,
179
+ writeValidatedPiece: makeWriteValidated({ completions }),
180
+ });
181
+ const peer = new FakePeer({ choked: false });
182
+
183
+ manager.start();
184
+ pool.add(peer);
185
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 0, block: new Uint8Array([1, 2, 3, 4]) });
186
+
187
+ expect(manager.done).resolves.toBeUndefined();
188
+ expect(completions).toEqual([
189
+ {
190
+ pieceIndex: 0,
191
+ data: new Uint8Array([1, 2, 3, 4]),
192
+ expectedHash: new Uint8Array(20),
193
+ },
194
+ ]);
195
+ });
196
+
197
+ test('reports progress snapshots for completed pieces by default', async () => {
198
+ const progress: DownloadProgress[] = [];
199
+ const pool = new FakePeerPool();
200
+ const metadata = makeMetadata({ length: 4, pieceLength: 4, pieces: 1 });
201
+ const manager = new DownloadManager({
202
+ metadata,
203
+ outputDirectory: '/tmp/download',
204
+ peerPool: pool,
205
+ maxInFlightRequestsPerPeer: 2,
206
+ planner: createPiecePlanner(metadata, { blockLength: 2 }),
207
+ writeValidatedPiece: makeWriteValidated(),
208
+ });
209
+ const peer = new FakePeer({ choked: false });
210
+
211
+ manager.onProgress((snapshot) => progress.push(snapshot));
212
+ manager.start();
213
+ pool.add(peer);
214
+
215
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 0, block: new Uint8Array([1, 2]) });
216
+ expect(progress).toEqual([]);
217
+
218
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 2, block: new Uint8Array([3, 4]) });
219
+ await manager.done;
220
+
221
+ expect(progress).toEqual([
222
+ {
223
+ totalBytes: 4,
224
+ receivedBytes: 4,
225
+ downloadedBytes: 4,
226
+ totalPieces: 1,
227
+ completedPieces: 1,
228
+ percent: 1,
229
+ speedBytesPerSecond: 0,
230
+ speed: '0.0 Bps',
231
+ },
232
+ ]);
233
+ });
234
+
235
+ test('reports progress snapshots as blocks arrive when configured', async () => {
236
+ const progress: DownloadProgress[] = [];
237
+ const pool = new FakePeerPool();
238
+ const metadata = makeMetadata({ length: 4, pieceLength: 4, pieces: 1 });
239
+ const manager = new DownloadManager({
240
+ metadata,
241
+ outputDirectory: '/tmp/download',
242
+ peerPool: pool,
243
+ maxInFlightRequestsPerPeer: 2,
244
+ progressEvents: 'block',
245
+ planner: createPiecePlanner(metadata, { blockLength: 2 }),
246
+ writeValidatedPiece: makeWriteValidated(),
247
+ });
248
+ const peer = new FakePeer({ choked: false });
249
+
250
+ expect(manager.progress).toEqual({
251
+ totalBytes: 4,
252
+ receivedBytes: 0,
253
+ downloadedBytes: 0,
254
+ totalPieces: 1,
255
+ completedPieces: 0,
256
+ percent: 0,
257
+ speedBytesPerSecond: 0,
258
+ speed: '0.0 Bps',
259
+ });
260
+
261
+ manager.onProgress((snapshot) => progress.push(snapshot));
262
+ manager.start();
263
+ pool.add(peer);
264
+
265
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 0, block: new Uint8Array([1, 2]) });
266
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 2, block: new Uint8Array([3, 4]) });
267
+ await manager.done;
268
+
269
+ expect(progress).toEqual([
270
+ {
271
+ totalBytes: 4,
272
+ receivedBytes: 2,
273
+ downloadedBytes: 0,
274
+ totalPieces: 1,
275
+ completedPieces: 0,
276
+ percent: 0,
277
+ speedBytesPerSecond: 0,
278
+ speed: '0.0 Bps',
279
+ },
280
+ {
281
+ totalBytes: 4,
282
+ receivedBytes: 4,
283
+ downloadedBytes: 4,
284
+ totalPieces: 1,
285
+ completedPieces: 1,
286
+ percent: 1,
287
+ speedBytesPerSecond: 0,
288
+ speed: '0.0 Bps',
289
+ },
290
+ ]);
291
+ });
292
+
293
+ test('updates speed only after the sample interval passes', async () => {
294
+ const progress: DownloadProgress[] = [];
295
+ const pool = new FakePeerPool();
296
+ const metadata = makeMetadata({ length: 4, pieceLength: 4, pieces: 1 });
297
+ const manager = new DownloadManager({
298
+ metadata,
299
+ outputDirectory: '/tmp/download',
300
+ peerPool: pool,
301
+ maxInFlightRequestsPerPeer: 2,
302
+ progressEvents: 'block',
303
+ speedSampleIntervalMs: 1,
304
+ planner: createPiecePlanner(metadata, { blockLength: 2 }),
305
+ writeValidatedPiece: makeWriteValidated(),
306
+ });
307
+ const peer = new FakePeer({ choked: false });
308
+
309
+ manager.onProgress((snapshot) => progress.push(snapshot));
310
+ manager.start();
311
+ pool.add(peer);
312
+
313
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 0, block: new Uint8Array([1, 2]) });
314
+ await new Promise((resolve) => setTimeout(resolve, 5));
315
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 2, block: new Uint8Array([3, 4]) });
316
+ await manager.done;
317
+
318
+ expect(progress[0]!.speedBytesPerSecond).toBe(0);
319
+ expect(progress[1]!.speedBytesPerSecond).toBeGreaterThan(0);
320
+ expect(progress[1]!.speed).not.toBe('0.0 Bps');
321
+ });
322
+
323
+ test('retries a piece when validation fails', async () => {
324
+ const pool = new FakePeerPool();
325
+ const manager = new DownloadManager({
326
+ metadata: makeMetadata(),
327
+ outputDirectory: '/tmp/download',
328
+ peerPool: pool,
329
+ writeValidatedPiece: makeWriteValidated({ valid: false }),
330
+ });
331
+ const peer = new FakePeer({ choked: false });
332
+
333
+ manager.start();
334
+ pool.add(peer);
335
+ peer.emit({ type: 'piece', pieceIndex: 0, offset: 0, block: new Uint8Array([1, 2, 3, 4]) });
336
+ await nextTick();
337
+
338
+ expect(requestMessages(peer)).toEqual([
339
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
340
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
341
+ ]);
342
+ });
343
+
344
+ test('returns pending peer requests to the planner when a peer closes', () => {
345
+ const pool = new FakePeerPool();
346
+ const manager = new DownloadManager({
347
+ metadata: makeMetadata({ length: 8, pieceLength: 4, pieces: 2 }),
348
+ outputDirectory: '/tmp/download',
349
+ peerPool: pool,
350
+ writeValidatedPiece: makeWriteValidated(),
351
+ });
352
+ const firstPeer = new FakePeer({ choked: false, pieces: [0] });
353
+ const secondPeer = new FakePeer({ choked: false, pieces: [0] });
354
+
355
+ manager.start();
356
+ pool.add(firstPeer);
357
+ firstPeer.close();
358
+ pool.add(secondPeer);
359
+
360
+ expect(requestMessages(firstPeer)).toEqual([
361
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
362
+ ]);
363
+ expect(requestMessages(secondPeer)).toEqual([
364
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
365
+ ]);
366
+ });
367
+
368
+ test('requeues timed-out requests to other peers first', async () => {
369
+ const pool = new FakePeerPool();
370
+ const manager = new DownloadManager({
371
+ metadata: makeMetadata({ length: 4, pieceLength: 4, pieces: 1 }),
372
+ outputDirectory: '/tmp/download',
373
+ peerPool: pool,
374
+ requestTimeoutMs: 50,
375
+ writeValidatedPiece: makeWriteValidated(),
376
+ });
377
+ const slowPeer = new FakePeer({ choked: false, pieces: [0] });
378
+ const nextPeer = new FakePeer({ choked: false, pieces: [0] });
379
+
380
+ manager.start();
381
+ pool.add(slowPeer);
382
+ pool.add(nextPeer);
383
+ await new Promise((resolve) => setTimeout(resolve, 60));
384
+
385
+ expect(requestMessages(slowPeer)).toEqual([
386
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
387
+ ]);
388
+ expect(requestMessages(nextPeer)).toEqual([
389
+ { type: 'request', pieceIndex: 0, offset: 0, length: 4 },
390
+ ]);
391
+ manager.close();
392
+ });
393
+ });