Merge pull request #921 from CyberMind-FR/fix/torrent-async-get

torrent v2.1.2: fix remove/files/stream 500 (webtorrent 2.x client.get is async)
This commit is contained in:
CyberMind 2026-07-28 18:17:31 +02:00 committed by GitHub
commit 771b1da3fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 42 additions and 2 deletions

View File

@ -1,3 +1,17 @@
secubox-torrent (2.1.2-1~bookworm1) bookworm; urgency=high
* Fix remove/files/stream 500 + broken stats: WebTorrent 2.x's client.get()
is async (returns a Promise), but engine.get() used it as if sync — every
caller (remove → t.destroy, /files → t.files, /stream, stats → t.progress)
then operated on a Promise instead of the Torrent, giving a 500 (remove,
files) or garbage (stats), and streaming never resolved its file. Match on
the synchronous client.torrents array instead. The FakeWebTorrent test
double had a sync get() that masked this; it is now async to model reality,
with regression tests asserting engine.get() is sync and remove() works.
(ref #917)
-- Gerald KERMA <devel@cybermind.fr> Tue, 28 Jul 2026 18:45:00 +0200
secubox-torrent (2.1.1-1~bookworm1) bookworm; urgency=high
* Fix add-torrent hang / 502: pin uint8-util to 2.2.6 via npm overrides.

View File

@ -62,7 +62,11 @@ export class Engine {
if (t.infoHash || (t.files && t.files.length)) done();
});
}
get(infohash) { return this.client.get(infohash); }
// WebTorrent 2.x's client.get() is ASYNC (returns a Promise) — using it as if
// it were sync makes every caller (stats/remove/files/stream) operate on a
// Promise instead of a Torrent (t.destroy/t.files/t.progress → 500 or garbage).
// client.torrents is a plain array, so match on it synchronously instead.
get(infohash) { return this.client.torrents.find(t => t.infoHash === infohash) || null; }
stats(infohash) {
const t = this.get(infohash); if (!t) return null;
return { progress: t.progress, downloadSpeed: t.downloadSpeed, uploadSpeed: t.uploadSpeed,

View File

@ -43,6 +43,26 @@ test('remove frees capacity for new add', async () => {
assert.equal(r2.infohash, 'b'.repeat(40));
});
test('get returns a torrent synchronously (not a Promise) so callers work', async () => {
const eng = new Engine({ WebTorrentCtor: FakeWebTorrent, downloadDir: '/tmp/x', maxActive: 5, webrtc: true });
const r = await eng.add('magnet:?xt=urn:btih:' + 'a'.repeat(40));
const t = eng.get(r.infohash);
// Regression: real webtorrent client.get() is async — engine must NOT return a
// Promise here or stats/remove/files/stream all operate on the wrong object.
assert.equal(typeof t.then, 'undefined');
assert.equal(t.infoHash, 'a'.repeat(40));
assert.ok(Array.isArray(t.files));
});
test('remove drops the torrent from the client without throwing', async () => {
const eng = new Engine({ WebTorrentCtor: FakeWebTorrent, downloadDir: '/tmp/x', maxActive: 5, webrtc: true });
const r = await eng.add('magnet:?xt=urn:btih:' + 'a'.repeat(40));
assert.equal(eng.client.torrents.length, 1);
eng.remove(r.infohash, { deleteData: true }); // must not throw (t.destroy on a Torrent, not a Promise)
assert.equal(eng.client.torrents.length, 0);
assert.equal(eng.get(r.infohash), null);
});
test('add rejects (never hangs/crashes) when the torrent emits error', async () => {
const client = new FakeWebTorrent();
const eng = new Engine({ WebTorrentCtor: function () { return client; },

View File

@ -38,7 +38,9 @@ export class FakeWebTorrent {
const t = this._next || new FakeTorrent('a'.repeat(40), 'Fake', [{ name: 'movie.mp4', length: 100 }]);
t._client = this; this.torrents.push(t); if (cb) queueMicrotask(() => cb(t)); return t;
}
get(infohash) { return this.torrents.find(t => t.infoHash === infohash) || null; }
// Real WebTorrent 2.x client.get() is async (Promise) — model that so no code
// may treat it as sync again. Engine matches on the sync `torrents` array.
async get(infohash) { return this.torrents.find(t => t.infoHash === infohash) || null; }
on() {} // real WebTorrent client is an EventEmitter; engine attaches 'error'
destroy(cb) { if (cb) cb(); }
}