bun-torrent 0.0.6 → 0.0.8

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 (139) hide show
  1. package/README.md +48 -5
  2. package/dist/app.d.ts +2 -0
  3. package/dist/app.js +136 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/client.d.ts +135 -0
  6. package/dist/client.error.d.ts +11 -0
  7. package/dist/client.error.js +11 -0
  8. package/dist/client.error.js.map +1 -1
  9. package/dist/client.js +130 -3
  10. package/dist/client.js.map +1 -1
  11. package/dist/configs/defaults.d.ts +16 -0
  12. package/dist/configs/defaults.js +11 -0
  13. package/dist/configs/defaults.js.map +1 -1
  14. package/dist/dht/DhtClient.d.ts +52 -0
  15. package/dist/dht/DhtClient.js +279 -0
  16. package/dist/dht/DhtClient.js.map +1 -0
  17. package/dist/dht/KBucket.d.ts +12 -0
  18. package/dist/dht/KBucket.js +43 -0
  19. package/dist/dht/KBucket.js.map +1 -0
  20. package/dist/dht/RoutingTable.d.ts +19 -0
  21. package/dist/dht/RoutingTable.js +75 -0
  22. package/dist/dht/RoutingTable.js.map +1 -0
  23. package/dist/dht/errors.d.ts +19 -0
  24. package/dist/dht/errors.js +24 -0
  25. package/dist/dht/errors.js.map +1 -0
  26. package/dist/dht/index.d.ts +8 -0
  27. package/dist/dht/index.js +5 -0
  28. package/dist/dht/index.js.map +1 -0
  29. package/dist/dht/krpc/TransactionId.d.ts +12 -0
  30. package/dist/dht/krpc/TransactionId.js +23 -0
  31. package/dist/dht/krpc/TransactionId.js.map +1 -0
  32. package/dist/dht/krpc/index.d.ts +5 -0
  33. package/dist/dht/krpc/index.js +4 -0
  34. package/dist/dht/krpc/index.js.map +1 -0
  35. package/dist/dht/krpc/messages.d.ts +3 -0
  36. package/dist/dht/krpc/messages.js +163 -0
  37. package/dist/dht/krpc/messages.js.map +1 -0
  38. package/dist/dht/krpc/types.d.ts +54 -0
  39. package/dist/dht/krpc/types.js +14 -0
  40. package/dist/dht/krpc/types.js.map +1 -0
  41. package/dist/dht/routing-table.d.ts +2 -0
  42. package/dist/dht/routing-table.js +2 -0
  43. package/dist/dht/routing-table.js.map +1 -0
  44. package/dist/dht/utils/compact.d.ts +17 -0
  45. package/dist/dht/utils/compact.js +81 -0
  46. package/dist/dht/utils/compact.js.map +1 -0
  47. package/dist/dht/utils/distance.d.ts +5 -0
  48. package/dist/dht/utils/distance.js +25 -0
  49. package/dist/dht/utils/distance.js.map +1 -0
  50. package/dist/magnet/base32/errors.d.ts +11 -1
  51. package/dist/magnet/base32/errors.js +10 -1
  52. package/dist/magnet/base32/errors.js.map +1 -1
  53. package/dist/magnet/parser/errors.d.ts +20 -1
  54. package/dist/magnet/parser/errors.js +19 -1
  55. package/dist/magnet/parser/errors.js.map +1 -1
  56. package/dist/magnet/parser/index.d.ts +10 -2
  57. package/dist/magnet/parser/index.js +53 -22
  58. package/dist/magnet/parser/index.js.map +1 -1
  59. package/dist/peer/PeerPieceAvailability.d.ts +50 -0
  60. package/dist/peer/PeerPieceAvailability.js +71 -0
  61. package/dist/peer/PeerPieceAvailability.js.map +1 -0
  62. package/dist/peer/availability.d.ts +2 -50
  63. package/dist/peer/availability.js +1 -70
  64. package/dist/peer/availability.js.map +1 -1
  65. package/dist/peer/extended/connection.d.ts +3 -1
  66. package/dist/peer/extended/connection.js +20 -4
  67. package/dist/peer/extended/connection.js.map +1 -1
  68. package/dist/peer/extended/errors.d.ts +1 -0
  69. package/dist/peer/extended/errors.js +1 -0
  70. package/dist/peer/extended/errors.js.map +1 -1
  71. package/dist/peer/extended/metadata/MetadataAssembler.d.ts +12 -0
  72. package/dist/peer/extended/metadata/MetadataAssembler.js +44 -0
  73. package/dist/peer/extended/metadata/MetadataAssembler.js.map +1 -0
  74. package/dist/peer/extended/metadata/assembler.d.ts +1 -12
  75. package/dist/peer/extended/metadata/assembler.js +1 -43
  76. package/dist/peer/extended/metadata/assembler.js.map +1 -1
  77. package/dist/peer/extended/metadata/index.d.ts +1 -0
  78. package/dist/peer/extended/metadata/index.js +20 -8
  79. package/dist/peer/extended/metadata/index.js.map +1 -1
  80. package/dist/peer/index.d.ts +2 -2
  81. package/dist/peer/index.js +1 -1
  82. package/dist/peer/index.js.map +1 -1
  83. package/dist/peer/pool/PeerPool.d.ts +65 -0
  84. package/dist/peer/pool/PeerPool.js +206 -0
  85. package/dist/peer/pool/PeerPool.js.map +1 -0
  86. package/dist/peer/pool/index.d.ts +2 -65
  87. package/dist/peer/pool/index.js +1 -205
  88. package/dist/peer/pool/index.js.map +1 -1
  89. package/dist/peer/pool/pool.error.d.ts +14 -1
  90. package/dist/peer/pool/pool.error.js +13 -1
  91. package/dist/peer/pool/pool.error.js.map +1 -1
  92. package/dist/peer/session/PeerSession.d.ts +35 -0
  93. package/dist/peer/session/PeerSession.js +200 -0
  94. package/dist/peer/session/PeerSession.js.map +1 -0
  95. package/dist/peer/session/index.d.ts +2 -35
  96. package/dist/peer/session/index.js +1 -199
  97. package/dist/peer/session/index.js.map +1 -1
  98. package/dist/peer/session/session.error.d.ts +16 -1
  99. package/dist/peer/session/session.error.js +15 -1
  100. package/dist/peer/session/session.error.js.map +1 -1
  101. package/dist/torrent/download/DownloadManager.d.ts +97 -0
  102. package/dist/torrent/download/DownloadManager.js +383 -0
  103. package/dist/torrent/download/DownloadManager.js.map +1 -0
  104. package/dist/torrent/download/index.d.ts +2 -2
  105. package/dist/torrent/download/index.js +1 -1
  106. package/dist/torrent/download/index.js.map +1 -1
  107. package/dist/torrent/download/manager.d.ts +2 -85
  108. package/dist/torrent/download/manager.js +1 -382
  109. package/dist/torrent/download/manager.js.map +1 -1
  110. package/dist/torrent/parser/parser.error.d.ts +11 -1
  111. package/dist/torrent/parser/parser.error.js +10 -1
  112. package/dist/torrent/parser/parser.error.js.map +1 -1
  113. package/dist/torrent/pieces/planner.error.d.ts +10 -1
  114. package/dist/torrent/pieces/planner.error.js +9 -1
  115. package/dist/torrent/pieces/planner.error.js.map +1 -1
  116. package/dist/torrent/session/Torrent.d.ts +160 -0
  117. package/dist/torrent/session/Torrent.js +192 -0
  118. package/dist/torrent/session/Torrent.js.map +1 -0
  119. package/dist/torrent/session/index.d.ts +2 -77
  120. package/dist/torrent/session/index.js +1 -125
  121. package/dist/torrent/session/index.js.map +1 -1
  122. package/dist/torrent/storage/storage.error.d.ts +10 -1
  123. package/dist/torrent/storage/storage.error.js +9 -1
  124. package/dist/torrent/storage/storage.error.js.map +1 -1
  125. package/dist/torrent/types.d.ts +18 -0
  126. package/dist/tracker/tracker.error.d.ts +12 -1
  127. package/dist/tracker/tracker.error.js +11 -1
  128. package/dist/tracker/tracker.error.js.map +1 -1
  129. package/dist/tracker/udp.js +24 -25
  130. package/dist/tracker/udp.js.map +1 -1
  131. package/dist/utils/UdpSocket.d.ts +12 -0
  132. package/dist/utils/UdpSocket.js +31 -0
  133. package/dist/utils/UdpSocket.js.map +1 -0
  134. package/dist/utils/errors.d.ts +20 -1
  135. package/dist/utils/errors.js +19 -1
  136. package/dist/utils/errors.js.map +1 -1
  137. package/examples/download-torrent.ts +32 -0
  138. package/examples/magnet-dht.ts +32 -0
  139. package/package.json +5 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A minimal Bun-native BitTorrent download-only client written in TypeScript.
4
4
 
5
- `bun-torrent` can parse `.torrent` files and tracker-backed magnet links, announce to HTTP and UDP trackers, connect to peers, download pieces, validate piece hashes, and write the downloaded files to disk. The public API is intentionally small: create a `Client`, inspect a torrent when you need metadata, then call `download()`.
5
+ `bun-torrent` can parse `.torrent` files and magnet links, announce to HTTP and UDP trackers, discover peers through the BitTorrent DHT, connect to peers, download pieces, validate piece hashes, and write the downloaded files to disk. The public API is intentionally small: create a `Client`, inspect a torrent when you need metadata, then call `download()`.
6
6
 
7
7
  It has no runtime dependencies.
8
8
 
@@ -57,6 +57,17 @@ await torrent.done;
57
57
 
58
58
  `download()` returns a `Torrent` instance and starts the download immediately.
59
59
 
60
+ ## Runnable Example Files
61
+
62
+ Runnable example files are available in `examples/`:
63
+
64
+ ```bash
65
+ bun examples/download-torrent.ts ./example.torrent ./downloads
66
+ bun examples/magnet-dht.ts "magnet:?xt=urn:btih:..." ./downloads
67
+ ```
68
+
69
+ The magnet example works with tracker-backed and trackerless magnets. It uses the default in-memory DHT node and closes the client before exiting.
70
+
60
71
  ## Client Setup
61
72
 
62
73
  ```ts
@@ -78,6 +89,7 @@ const client = new Client({
78
89
 
79
90
  Client options:
80
91
 
92
+ - `dht`: optional DHT peer discovery implementation, or `false` to disable DHT. By default, the client creates an in-memory DHT node.
81
93
  - `outputDirectory`: directory where downloaded files are written. Defaults to `process.cwd()`.
82
94
  - `files`: optional default file selection for downloads.
83
95
  - `targetConnections`: preferred number of connected peers. Defaults to `20`.
@@ -109,9 +121,11 @@ console.log(metadata.files);
109
121
 
110
122
  Torrent file input can be a file path, `Uint8Array`, or `ArrayBuffer`.
111
123
 
124
+ Call `client.close()` when the client should stop accepting work and close active torrents and its DHT socket.
125
+
112
126
  ## Magnet Links
113
127
 
114
- Magnet links are supported when they include at least one HTTP, HTTPS, or UDP tracker through the `tr` parameter. Metadata is fetched from peers with the `ut_metadata` extension before the normal download flow starts.
128
+ Magnet links are supported with or without trackers. If `tr` parameters are present, the client asks those trackers first. If no tracker returns peers, or the magnet is trackerless, the client falls back to DHT peer discovery. Metadata is fetched from peers with the `ut_metadata` extension before the normal download flow starts.
115
129
 
116
130
  ```ts
117
131
  const magnet =
@@ -134,13 +148,42 @@ const torrent = await client.download(
134
148
  await torrent.done;
135
149
  ```
136
150
 
151
+ Trackerless magnets use the same API. Keep the same `Client` instance if you inspect first and download later, because the default DHT node keeps discovered peers in memory for the current process.
152
+
153
+ ```ts
154
+ import { Client } from 'bun-torrent';
155
+
156
+ const client = new Client({
157
+ outputDirectory: './downloads',
158
+ });
159
+
160
+ const magnet = 'magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567';
161
+
162
+ try {
163
+ const metadata = await client.inspect({ magnet });
164
+
165
+ console.log(metadata.name);
166
+ console.log(metadata.files);
167
+
168
+ const torrent = await client.download({ meta: metadata });
169
+
170
+ torrent.on('progress', (progress) => {
171
+ console.log(`${(progress.percent * 100).toFixed(2)}%`, progress.speed);
172
+ });
173
+
174
+ await torrent.done;
175
+ } finally {
176
+ client.close();
177
+ }
178
+ ```
179
+
137
180
  Supported magnet fields:
138
181
 
139
182
  - `xt=urn:btih:<infoHash>`: required. Hex-encoded 40-character info hashes and base32 32-character info hashes are supported.
140
183
  - `dn`: optional display name.
141
- - `tr`: optional tracker URL. Multiple `tr` parameters are supported.
184
+ - `tr`: optional tracker URL. Multiple `tr` parameters are supported, but trackerless magnets can resolve through DHT.
142
185
 
143
- Trackerless magnets are not supported yet because DHT peer discovery is not implemented. A magnet without `tr` currently fails during inspection with a DHT-not-implemented error.
186
+ The default DHT node is in-memory. It keeps a routing table and a short-lived peer cache for the current process, so a magnet `inspect()` can populate peers that a later `download({ meta })` call can reuse on the same `Client` instance. The DHT state is not persisted to disk.
144
187
 
145
188
  ## Download Options
146
189
 
@@ -181,7 +224,7 @@ Download options:
181
224
  - `speedSampleIntervalMs`: override speed sample interval.
182
225
  - `onChangeState`: receives client setup states: `parsing`, `tracking`, `connecting`, `downloading`.
183
226
 
184
- Tracker announce failures are treated as non-fatal. If no tracker responds, the client can still continue with an empty peer list instead of throwing during tracking.
227
+ Tracker announce failures are treated as non-fatal. If trackers are missing or do not return peers, the client falls back to DHT when it is enabled. With DHT disabled, the client can still continue with an empty peer list instead of throwing during tracking when `minConnections` is `0`.
185
228
 
186
229
  ## Selecting Files
187
230
 
package/dist/app.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
package/dist/app.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bun
2
+ import { bytesToHex } from './utils/buffers';
3
+ import { formatBytes } from './utils/formats';
4
+ import { Client } from './client';
5
+ const summarizeTorrent = (metadata) => ({
6
+ announce: metadata.announce,
7
+ announceList: metadata.announceList,
8
+ infoHash: bytesToHex(metadata.infoHash),
9
+ name: metadata.name,
10
+ pieceLength: metadata.pieceLength,
11
+ piecesTotal: metadata.pieces.length,
12
+ length: metadata.length,
13
+ files: metadata.files.map((file) => ({
14
+ path: file.path.join('/'),
15
+ length: file.length,
16
+ offset: file.offset,
17
+ })),
18
+ filesTotal: metadata.files.length,
19
+ });
20
+ const usage = () => {
21
+ console.error('Usage:');
22
+ console.error(' bunx bun-torrent inspect <file.torrent>');
23
+ console.error(' bunx bun-torrent inspect <magnet-uri>');
24
+ console.error(' bunx bun-torrent inspect < file.torrent');
25
+ console.error(' bunx bun-torrent download <file.torrent|magnet-uri> [output-directory]');
26
+ };
27
+ const inspect = async (input) => {
28
+ const client = new Client();
29
+ try {
30
+ const metadata = input ? await inspectArgument(client, input) : await inspectStdin(client);
31
+ console.log(JSON.stringify(summarizeTorrent(metadata), null, 2));
32
+ }
33
+ finally {
34
+ client.close();
35
+ }
36
+ };
37
+ const resolveInput = (input) => {
38
+ if (input.startsWith('magnet:')) {
39
+ return { magnet: input };
40
+ }
41
+ return { torrentFile: input };
42
+ };
43
+ const inspectArgument = async (client, input) => {
44
+ return await client.inspect(resolveInput(input));
45
+ };
46
+ const inspectStdin = async (client) => {
47
+ const input = new Uint8Array(await Bun.stdin.arrayBuffer());
48
+ if (input.byteLength === 0) {
49
+ throw new Error('Expected torrent file bytes on stdin');
50
+ }
51
+ return await client.inspect({ torrentFile: input });
52
+ };
53
+ const download = async (input, outputDirectory = './downloads') => {
54
+ if (!input) {
55
+ throw new Error('Expected file.torrent or magnet URI');
56
+ }
57
+ const client = new Client({ outputDirectory });
58
+ let state = 'starting';
59
+ let progress = null;
60
+ let stats = null;
61
+ const render = () => renderProgressLine(state, progress, stats);
62
+ try {
63
+ render();
64
+ const torrent = await client.download(resolveInput(input), {
65
+ progressEvents: 'block',
66
+ onChangeState: (nextState) => {
67
+ state = nextState;
68
+ render();
69
+ },
70
+ });
71
+ progress = torrent.progress;
72
+ stats = torrent.stats;
73
+ render();
74
+ torrent.on('progress', (nextProgress) => {
75
+ progress = nextProgress;
76
+ render();
77
+ });
78
+ torrent.on('peer', (nextStats) => {
79
+ stats = nextStats;
80
+ render();
81
+ });
82
+ await torrent.done;
83
+ state = 'completed';
84
+ progress = torrent.progress;
85
+ stats = torrent.stats;
86
+ render();
87
+ process.stdout.write('\nDownload complete\n');
88
+ }
89
+ finally {
90
+ client.close();
91
+ }
92
+ };
93
+ const renderProgressLine = (state, progress, stats) => {
94
+ const bar = progress ? progressBar(progress.percent) : progressBar(0);
95
+ const percent = progress ? `${(progress.percent * 100).toFixed(2).padStart(6)}%` : ' 0.00%';
96
+ const downloaded = progress ? formatBytes(progress.downloadedBytes) : '0.0 B';
97
+ const total = progress ? formatBytes(progress.totalBytes) : '?';
98
+ const speed = progress?.speed ?? '0.0 Bps';
99
+ const pieces = progress
100
+ ? `pieces=${progress.completedPieces}/${progress.totalPieces}`
101
+ : 'pieces=?/?';
102
+ const peers = stats ? `peers=${stats.connections}/${stats.peers}` : 'peers=0/0';
103
+ const line = `${state.padEnd(11)} ${bar} ${percent} ${downloaded}/${total} ${speed} ${pieces} ${peers}`;
104
+ process.stdout.write(`\r${line}\x1b[K`);
105
+ };
106
+ const progressBar = (percent) => {
107
+ const width = 28;
108
+ const normalized = Math.min(1, Math.max(0, percent));
109
+ const filled = Math.round(normalized * width);
110
+ return `[${'#'.repeat(filled)}${'-'.repeat(width - filled)}]`;
111
+ };
112
+ if (import.meta.main) {
113
+ const command = Bun.argv[2];
114
+ const input = Bun.argv[3];
115
+ const outputDirectory = Bun.argv[4];
116
+ try {
117
+ switch (command) {
118
+ case 'inspect':
119
+ await inspect(input);
120
+ break;
121
+ case 'download':
122
+ await download(input, outputDirectory);
123
+ break;
124
+ default:
125
+ usage();
126
+ process.exitCode = 1;
127
+ }
128
+ }
129
+ catch (error) {
130
+ if (command === 'download')
131
+ process.stdout.write('\n');
132
+ console.error(error instanceof Error ? error.message : String(error));
133
+ process.exitCode = 1;
134
+ }
135
+ }
136
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -2,24 +2,61 @@ import { type DownloadProgressEventMode } from './torrent/download';
2
2
  import { Torrent } from './torrent/session/index';
3
3
  import type { TorrentMetadata } from './torrent/types';
4
4
  import type { TorrentFileSelection } from './torrent/file-selection';
5
+ import type { PeerInfo } from './tracker/types';
6
+ /**
7
+ * Setup phases reported through {@link DownloadOptions.onChangeState} before download starts.
8
+ *
9
+ * The values are emitted in order: `parsing` → `tracking` → `connecting` → `downloading`.
10
+ * Once `downloading` fires, progress events take over via {@link Torrent.on}.
11
+ */
5
12
  export declare enum DownloadState {
6
13
  PARSING = "parsing",
7
14
  TRACKING = "tracking",
8
15
  CONNECTING = "connecting",
9
16
  DOWNLOADING = "downloading"
10
17
  }
18
+ /**
19
+ * Minimal DHT contract used by {@link Client} for peer discovery.
20
+ *
21
+ * Implement this to swap the default in-memory DHT for a persistent or
22
+ * shared implementation. `close` is called from {@link Client.close}.
23
+ */
24
+ export type ClientDht = {
25
+ lookupPeers(infoHash: Uint8Array): Promise<PeerInfo[]>;
26
+ close?(): void;
27
+ };
28
+ /**
29
+ * Default options applied to every download started by a {@link Client}.
30
+ *
31
+ * All fields are optional. Any field can be overridden per-download via
32
+ * {@link DownloadOptions}.
33
+ */
11
34
  export type ClientConfig = {
35
+ /** Custom DHT implementation, or `false` to disable DHT entirely. Defaults to an in-memory {@link DhtClient}. */
36
+ dht?: ClientDht | false;
37
+ /** Default file selection for multi-file torrents. `null`/omitted downloads every file. */
12
38
  files?: TorrentFileSelection;
39
+ /** Maximum block requests in flight per peer. Higher values increase throughput but also memory. Defaults to 300. */
13
40
  maxInFlightRequestsPerPeer?: number;
41
+ /** Maximum simultaneous peer connection attempts. Defaults to 30. */
14
42
  maxConnecting?: number;
43
+ /** Minimum connected peers required before download starts. `0` allows downloads to start with no peers. Defaults to 0. */
15
44
  minConnections?: number;
45
+ /** Directory where downloaded files are written. Defaults to `process.cwd()`. */
16
46
  outputDirectory?: string;
47
+ /** Timeout for a single peer TCP connect + handshake, in milliseconds. Defaults to 5000. */
17
48
  peerConnectTimeoutMs?: number;
49
+ /** Granularity of `progress` events: `'piece'` (once per validated piece) or `'block'` (every received block). Defaults to `'piece'`. */
18
50
  progressEvents?: DownloadProgressEventMode;
51
+ /** Timeout for a single block request, in milliseconds. Timed-out requests are retried on other peers. Defaults to 15000. */
19
52
  requestTimeoutMs?: number;
53
+ /** Reserved for future seeding support. Currently only `false` is supported. */
20
54
  seed?: false;
55
+ /** Minimum interval between speed recalculations, in milliseconds. Defaults to 500. */
21
56
  speedSampleIntervalMs?: number;
57
+ /** Preferred number of connected peers. The pool stops opening new connections once this is reached. Defaults to 20. */
22
58
  targetConnections?: number;
59
+ /** Timeout for tracker announce requests, in milliseconds. Defaults to 5000. */
23
60
  trackerTimeoutMs?: number;
24
61
  };
25
62
  type InspectInput = {
@@ -34,32 +71,130 @@ type InspectInput = {
34
71
  type DownloadInput = {
35
72
  meta: TorrentMetadata;
36
73
  } | InspectInput;
74
+ /**
75
+ * Entry point of the library. Holds shared state (peer id, DHT node) across downloads.
76
+ *
77
+ * One `Client` instance can run multiple concurrent downloads. The same client is also
78
+ * reused across `inspect` → `download` flows so the DHT routing table and peer cache
79
+ * can warm up once and pay off on subsequent calls.
80
+ *
81
+ * Call {@link Client.close} when the client is no longer needed — it closes every
82
+ * active torrent and the DHT socket. After close, every method throws {@link ClientError}.
83
+ *
84
+ * @example
85
+ * const client = new Client({ outputDirectory: './downloads' });
86
+ * try {
87
+ * const torrent = await client.download({ torrentFile: './example.torrent' });
88
+ * await torrent.done;
89
+ * } finally {
90
+ * client.close();
91
+ * }
92
+ */
37
93
  export declare class Client {
38
94
  private readonly config;
39
95
  private readonly peerId;
96
+ private readonly dhtNodeId;
97
+ private readonly dht?;
98
+ private readonly torrents;
99
+ private closed;
100
+ /**
101
+ * Build a client with optional defaults that apply to every download.
102
+ *
103
+ * The constructor is cheap: it generates a peer id and a DHT node id but does not
104
+ * open any sockets. The default DHT instance opens its UDP socket lazily on the
105
+ * first lookup.
106
+ *
107
+ * @param config - Defaults applied to downloads started by this client.
108
+ */
40
109
  constructor(config?: ClientConfig);
110
+ /**
111
+ * Start downloading a torrent and return a {@link Torrent} that emits progress events.
112
+ *
113
+ * The returned promise settles once the client has parsed metadata, discovered peers,
114
+ * opened the peer pool, and started the download manager. The actual download runs
115
+ * in the background — `await torrent.done` or listen for the `done` event to wait
116
+ * for it to finish.
117
+ *
118
+ * Tracker failures are non-fatal: if no tracker returns peers, DHT lookup is attempted
119
+ * when DHT is enabled. With DHT disabled, the download continues with whatever peers
120
+ * were found provided `minConnections` is `0`.
121
+ *
122
+ * @param input - Torrent source: a `.torrent` file path/bytes, a magnet URI, or already-parsed `meta`.
123
+ * @param options - Per-download overrides for {@link ClientConfig}, plus `announcePort` and `onChangeState`.
124
+ * @returns A {@link Torrent} whose `done` promise settles when the download finishes or fails.
125
+ * @throws {ClientError} When the client is closed, the input is unsupported, or file selection is invalid.
126
+ */
41
127
  download(input: DownloadInput, options?: DownloadOptions): Promise<Torrent>;
128
+ /**
129
+ * Parse a `.torrent` file or magnet URI and return its metadata without starting a download.
130
+ *
131
+ * Useful for showing the file list, computing total size, or letting the user pick
132
+ * a subset of files before calling {@link Client.download}. For magnet links this
133
+ * method fetches the info dictionary from a peer using the `ut_metadata` extension,
134
+ * so it may discover and warm DHT state that a later `download({ meta })` call reuses.
135
+ *
136
+ * @param input - Either `{ torrentFile }` (path/bytes/ArrayBuffer) or `{ magnet }` (URI string).
137
+ * @param options - Optional overrides. `timeout` caps the per-peer metadata fetch for magnets.
138
+ * @returns Parsed torrent metadata, including computed info hash.
139
+ * @throws {ClientError} When the client is closed or no input is provided.
140
+ * @throws {TorrentParseError} When the torrent file or magnet info dictionary cannot be parsed.
141
+ * @throws {MagnetParseError} When the magnet URI is invalid or no peer returned metadata.
142
+ */
42
143
  inspect(input: InspectInput, options?: {
43
144
  timeout?: number;
44
145
  }): Promise<TorrentMetadata>;
146
+ /**
147
+ * Stop the client, close every active torrent, and release the DHT socket.
148
+ *
149
+ * Idempotent — calling close more than once is a no-op. After close, every other
150
+ * method throws {@link ClientError} with code `CLIENT_CLOSED`.
151
+ */
152
+ close(): void;
153
+ private discoverPeers;
45
154
  private trackPeers;
155
+ private trackTorrent;
156
+ private assertOpen;
46
157
  }
158
+ /**
159
+ * Per-download overrides passed to {@link Client.download}.
160
+ *
161
+ * Every field except `announcePort` and `onChangeState` overrides the matching field
162
+ * in {@link ClientConfig} for this download only.
163
+ */
47
164
  export type DownloadOptions = {
165
+ /** Port sent to trackers in announce requests. Defaults to 6881. */
48
166
  announcePort?: number;
167
+ /** Restrict the download to specific files. Selectable by `'a/b.txt'` string or `['a','b.txt']` array. */
49
168
  files?: TorrentFileSelection;
169
+ /** Override {@link ClientConfig.maxInFlightRequestsPerPeer} for this download. */
50
170
  maxInFlightRequestsPerPeer?: number;
171
+ /** Override {@link ClientConfig.maxConnecting} for this download. */
51
172
  maxConnecting?: number;
173
+ /** Override {@link ClientConfig.minConnections} for this download. */
52
174
  minConnections?: number;
175
+ /** Called with each setup phase before download starts. See {@link DownloadState}. */
53
176
  onChangeState?: (state: DownloadState) => unknown;
177
+ /** Override {@link ClientConfig.outputDirectory} for this download. */
54
178
  outputDirectory?: string;
179
+ /** Override {@link ClientConfig.peerConnectTimeoutMs} for this download. */
55
180
  peerConnectTimeoutMs?: number;
181
+ /** Override {@link ClientConfig.progressEvents} for this download. */
56
182
  progressEvents?: DownloadProgressEventMode;
183
+ /** Override {@link ClientConfig.requestTimeoutMs} for this download. */
57
184
  requestTimeoutMs?: number;
185
+ /** Reserved for future seeding support. Currently only `false` is supported. */
58
186
  seed?: false;
187
+ /** Override {@link ClientConfig.speedSampleIntervalMs} for this download. */
59
188
  speedSampleIntervalMs?: number;
189
+ /** Override {@link ClientConfig.targetConnections} for this download. */
60
190
  targetConnections?: number;
191
+ /** Override {@link ClientConfig.trackerTimeoutMs} for this download. */
61
192
  trackerTimeoutMs?: number;
62
193
  };
194
+ /** Supported torrent file sources. */
63
195
  export type TorrentFileInput = string | Uint8Array | ArrayBuffer;
196
+ /**
197
+ * @internal
198
+ */
64
199
  export declare const readTorrentFile: (input: unknown) => Promise<Uint8Array>;
65
200
  export { Client as TorrentClient };
@@ -1,8 +1,19 @@
1
1
  import { BunTorrentError } from './utils/errors';
2
+ /** Error codes set on {@link ClientError.code}. */
2
3
  export declare enum ClientErrorCode {
4
+ /** {@link Client.download} or {@link Client.inspect} called after {@link Client.close}. */
5
+ CLOSED = "CLIENT_CLOSED",
6
+ /** `files` option referenced paths that do not exist in the torrent. */
3
7
  INVALID_FILE_SELECTION = "CLIENT_INVALID_FILE_SELECTION",
8
+ /** Torrent file input was neither a string path, `Uint8Array`, nor `ArrayBuffer`. */
4
9
  UNSUPPORTED_TORRENT_FILE_INPUT = "CLIENT_UNSUPPORTED_TORRENT_FILE_INPUT"
5
10
  }
11
+ /**
12
+ * Errors thrown directly by {@link Client}. See {@link ClientErrorCode}.
13
+ *
14
+ * Errors from underlying subsystems (parser, tracker, peer pool, DHT) are thrown as
15
+ * their own classes — this one only wraps client-level failures.
16
+ */
6
17
  export declare class ClientError extends BunTorrentError {
7
18
  constructor(code: ClientErrorCode, message: string);
8
19
  }
@@ -1,9 +1,20 @@
1
1
  import { BunTorrentError } from './utils/errors';
2
+ /** Error codes set on {@link ClientError.code}. */
2
3
  export var ClientErrorCode;
3
4
  (function (ClientErrorCode) {
5
+ /** {@link Client.download} or {@link Client.inspect} called after {@link Client.close}. */
6
+ ClientErrorCode["CLOSED"] = "CLIENT_CLOSED";
7
+ /** `files` option referenced paths that do not exist in the torrent. */
4
8
  ClientErrorCode["INVALID_FILE_SELECTION"] = "CLIENT_INVALID_FILE_SELECTION";
9
+ /** Torrent file input was neither a string path, `Uint8Array`, nor `ArrayBuffer`. */
5
10
  ClientErrorCode["UNSUPPORTED_TORRENT_FILE_INPUT"] = "CLIENT_UNSUPPORTED_TORRENT_FILE_INPUT";
6
11
  })(ClientErrorCode || (ClientErrorCode = {}));
12
+ /**
13
+ * Errors thrown directly by {@link Client}. See {@link ClientErrorCode}.
14
+ *
15
+ * Errors from underlying subsystems (parser, tracker, peer pool, DHT) are thrown as
16
+ * their own classes — this one only wraps client-level failures.
17
+ */
7
18
  export class ClientError extends BunTorrentError {
8
19
  constructor(code, message) {
9
20
  super(message, code);
@@ -1 +1 @@
1
- {"version":3,"file":"client.error.js","sourceRoot":"","sources":["../src/client.error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,CAAN,IAAY,eAGX;AAHD,WAAY,eAAe;IACvB,2EAAwD,CAAA;IACxD,2FAAwE,CAAA;AAC5E,CAAC,EAHW,eAAe,KAAf,eAAe,QAG1B;AAED,MAAM,OAAO,WAAY,SAAQ,eAAe;IAC5C,YAAY,IAAqB,EAAE,OAAe;QAC9C,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC;CACJ"}
1
+ {"version":3,"file":"client.error.js","sourceRoot":"","sources":["../src/client.error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,mDAAmD;AACnD,MAAM,CAAN,IAAY,eAOX;AAPD,WAAY,eAAe;IACvB,2FAA2F;IAC3F,2CAAwB,CAAA;IACxB,wEAAwE;IACxE,2EAAwD,CAAA;IACxD,qFAAqF;IACrF,2FAAwE,CAAA;AAC5E,CAAC,EAPW,eAAe,KAAf,eAAe,QAO1B;AAED;;;;;GAKG;AACH,MAAM,OAAO,WAAY,SAAQ,eAAe;IAC5C,YAAY,IAAqB,EAAE,OAAe;QAC9C,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACzB,CAAC;CACJ"}