@@ -41,6 +41,9 @@ export interface MetadataEvent {
4141 infoHash : string ; // The torrent info hash
4242}
4343
44+ // Global flag to ensure we only add the uncaught exception handler once
45+ let globalErrorHandlerAdded = false ;
46+
4447export class FileClient extends EventEmitter implements IFileClient {
4548 private gunRegistry : GunRegistry ;
4649 private webTorrentClient : WebTorrent . Instance | null = null ;
@@ -50,6 +53,9 @@ export class FileClient extends EventEmitter implements IFileClient {
5053 constructor ( options : FileClientOptions = { } ) {
5154 super ( ) ; // Call EventEmitter constructor
5255
56+ // Increase max listeners to prevent warnings during testing/multiple usage
57+ this . setMaxListeners ( 20 ) ;
58+
5359 this . options = {
5460 peers : options . peers || [ "http://dig-relay-prod.eba-2cmanxbe.us-east-1.elasticbeanstalk.com/gun" ] ,
5561 namespace : options . namespace || "dig-nat-tools" ,
@@ -235,6 +241,33 @@ export class FileClient extends EventEmitter implements IFileClient {
235241 } ;
236242 }
237243
244+ /**
245+ * Safely destroy a torrent with error handling for WebTorrent internal issues
246+ */
247+ private safeTorrentDestroy ( torrent : WebTorrent . Torrent ) : void {
248+ try {
249+ torrent . destroy ( ) ;
250+ } catch ( error ) {
251+ const errorCode = ( error as unknown as { code ?: string } ) . code ;
252+ if ( errorCode === 'ERR_INVALID_ARG_TYPE' && error instanceof Error && error . message . includes ( 'listener' ) ) {
253+ this . logger . warn ( "⚠️ WebTorrent internal error during torrent destroy (handled):" , {
254+ message : error . message ,
255+ code : errorCode ,
256+ torrentName : torrent . name || 'Unknown' ,
257+ infoHash : torrent . infoHash || 'Unknown'
258+ } ) ;
259+ } else {
260+ this . logger . error ( "❌ Error destroying torrent:" , {
261+ ...this . serializeError ( error ) ,
262+ torrentName : torrent . name || 'Unknown' ,
263+ infoHash : torrent . infoHash || 'Unknown'
264+ } ) ;
265+ // Re-throw non-listener errors as they might be important
266+ throw error ;
267+ }
268+ }
269+ }
270+
238271 /**
239272 * Parse magnet URI for debugging purposes
240273 */
@@ -288,6 +321,9 @@ export class FileClient extends EventEmitter implements IFileClient {
288321 try {
289322 this . webTorrentClient = new WebTorrent ( ) ;
290323
324+ // Increase max listeners for the WebTorrent client to prevent warnings
325+ this . webTorrentClient . setMaxListeners ( 20 ) ;
326+
291327 // Log client status (using safe property access)
292328 this . logger . debug ( `🔧 WebTorrent client created:` , {
293329 activeTorrents : this . webTorrentClient . torrents . length ,
@@ -308,6 +344,26 @@ export class FileClient extends EventEmitter implements IFileClient {
308344 } ) ;
309345 } ) ;
310346
347+ // Add global error handling for uncaught WebTorrent internal errors (only once)
348+ if ( ! globalErrorHandlerAdded ) {
349+ globalErrorHandlerAdded = true ;
350+ process . on ( 'uncaughtException' , ( error ) => {
351+ const errorCode = ( error as unknown as { code ?: string } ) . code ;
352+ if ( error . message && error . message . includes ( 'listener' ) && errorCode === 'ERR_INVALID_ARG_TYPE' ) {
353+ // Use console.warn directly since we can't access logger from global scope
354+ console . warn ( "⚠️ WebTorrent internal event listener error (handled):" , {
355+ message : error . message ,
356+ code : errorCode ,
357+ stack : error . stack ?. split ( '\n' ) . slice ( 0 , 5 ) . join ( '\n' ) // Truncate stack trace
358+ } ) ;
359+ // Don't re-throw this specific error as it's a WebTorrent internal cleanup issue
360+ } else {
361+ // Re-throw other uncaught exceptions
362+ throw error ;
363+ }
364+ } ) ;
365+ }
366+
311367 this . logger . debug ( `✅ WebTorrent client initialized` ) ;
312368 }
313369
@@ -347,7 +403,7 @@ export class FileClient extends EventEmitter implements IFileClient {
347403 ) ;
348404
349405 if ( torrent ! . files . length === 0 ) {
350- torrent ! . destroy ( ) ;
406+ this . safeTorrentDestroy ( torrent ! ) ;
351407 this . logger . error ( "❌ No files in torrent" , {
352408 name : torrent ! . name ,
353409 infoHash : torrent ! . infoHash ,
@@ -359,7 +415,7 @@ export class FileClient extends EventEmitter implements IFileClient {
359415
360416 // Check file size against maximum allowed size
361417 if ( options . maxFileSizeBytes && torrent ! . length > options . maxFileSizeBytes ) {
362- torrent ! . destroy ( ) ;
418+ this . safeTorrentDestroy ( torrent ! ) ;
363419 const fileSizeMB = ( torrent ! . length / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
364420 const maxSizeMB = ( options . maxFileSizeBytes / ( 1024 * 1024 ) ) . toFixed ( 2 ) ;
365421 this . logger . warn ( `⚠️ File too large: ${ fileSizeMB } MB > ${ maxSizeMB } MB` ) ;
@@ -388,12 +444,12 @@ export class FileClient extends EventEmitter implements IFileClient {
388444 ) ;
389445
390446 // Destroy torrent to clean up
391- torrent ! . destroy ( ) ;
447+ this . safeTorrentDestroy ( torrent ! ) ;
392448 resolve ( buffer ) ;
393449 } ) ;
394450
395451 stream . on ( "error" , ( error : unknown ) => {
396- torrent ! . destroy ( ) ;
452+ this . safeTorrentDestroy ( torrent ! ) ;
397453 this . logger . error ( "❌ Stream error during download:" , {
398454 ...this . serializeError ( error ) ,
399455 fileName : file . name ,
@@ -431,7 +487,7 @@ export class FileClient extends EventEmitter implements IFileClient {
431487 // Add download progress event emission
432488 torrent . on ( "download" , ( _bytes : number ) => {
433489 const progressData : DownloadProgressEvent = {
434- downloaded : torrent ! . downloaded ,
490+ downloaded : torrent ! . downloaded * 100 ,
435491 downloadSpeed : torrent ! . downloadSpeed * 100 ,
436492 progress : torrent ! . progress * 100 ,
437493 name : torrent ! . name || 'Unknown' ,
@@ -656,9 +712,31 @@ export class FileClient extends EventEmitter implements IFileClient {
656712 */
657713 public async destroy ( ) : Promise < void > {
658714 if ( this . webTorrentClient ) {
659- this . webTorrentClient . destroy ( ) ;
660- this . webTorrentClient = null ;
661- this . logger . debug ( "✅ WebTorrent client destroyed" ) ;
715+ try {
716+ // First destroy all active torrents safely
717+ if ( this . webTorrentClient . torrents ) {
718+ for ( const torrent of this . webTorrentClient . torrents ) {
719+ this . safeTorrentDestroy ( torrent ) ;
720+ }
721+ }
722+
723+ // Then destroy the client
724+ this . webTorrentClient . destroy ( ) ;
725+ this . webTorrentClient = null ;
726+ this . logger . debug ( "✅ WebTorrent client destroyed" ) ;
727+ } catch ( error ) {
728+ const errorCode = ( error as unknown as { code ?: string } ) . code ;
729+ if ( errorCode === 'ERR_INVALID_ARG_TYPE' && error instanceof Error && error . message . includes ( 'listener' ) ) {
730+ this . logger . warn ( "⚠️ WebTorrent cleanup error (handled):" , {
731+ message : error . message ,
732+ code : errorCode
733+ } ) ;
734+ } else {
735+ this . logger . error ( "❌ Error destroying WebTorrent client:" , this . serializeError ( error ) ) ;
736+ throw error ;
737+ }
738+ this . webTorrentClient = null ;
739+ }
662740 }
663741 }
664742}
0 commit comments