import debugging from 'debug' ; import { SimplePeerData } from 'simple-peer' ; import Transfer from "./Transfer" ; import UploadProvider from "./uploadProviders/UploadProvider" ; const debug = debugging ( 'blymp:UploadService' ) ; /**
 * Upload Service that handles uploading data to the partner peer
 */ export default class UploadService { /**
     * Current parent transfer
     */ private transfer : Transfer ; /**
     * Current Upload Provider that returns the data slices needed for uploading
     */ private uploadProvider : UploadProvider ; /**
     * Estimates made about the remaining time.
     * This is used to make the displayed time remaining more smoothed out instead
     * of creating a blank estimate on each packet
     */ lastEstimates : number [ ] = [ ] ; lastProgress = 0 ; timeSince = 50 ; /**
     * Size of a file chunk used when uploading in bytes
     */ chunkSize : number ; /**
     * Saves, if this instance has already been used to upload files
     */ private hasUploaded : boolean = false ; size = 0 ; transmitted = 0 ; /**
     * Interval that gets executed every 50ms to update the estimated time remaining
     */ estimateInterval : number | NodeJS . Timeout | undefined ; /**
     * Function to call after all files have been transferred
     */ private onUploadDone : Function = ( ) => { } ; param transfer param provider constructor ( transfer , provider ) { this . transfer = transfer ; this . uploadProvider = provider ; this . chunkSize = transfer . connection . method === 'webrtc' ? 1024 * 15 : 1024 * 512 ; } param data param isFilePart param callback private emit ( data , isFilePart = false , callback = false ) { if ( this . transfer . connection . method === 'webrtc' ) { if ( ! this . transfer . connection . peer ) { throw new Error ( 'Internal error: Cannot send data as no peer is connected' ) ; } if ( isFilePart ) { try { this . transfer . connection . peer . send ( data as SimplePeerData ) ; if ( callback ) { this . transfer . connection . socket . once ( "acknowledge rtc data" , callback ) ; } } catch ( e ) { debug ( 'Peer connection has been reset - falling back to sockets' ) ; this . lastEstimates = [ ] ; this . transfer . connection . method = 'socket' ; this . transfer . connection . socket . emit ( 'proxy to partner' , this . transfer . receiverId , { type : 'use transfer method' , method : 'socket' } ) ; this . emit ( data , isFilePart , callback ) ; return ; } } else { try { this . transfer . connection . peer . send ( JSON . stringify ( data ) ) ; if ( callback ) { callback ( ) ; } } catch ( e ) { debug ( 'Peer connection has been reset - falling back to sockets' ) ; this . lastEstimates = [ ] ; this . transfer . connection . method = 'socket' ; this . transfer . connection . socket . emit ( 'proxy to partner' , this . transfer . receiverId , { type : 'use transfer method' , method : 'socket' } ) ; this . emit ( data , isFilePart , callback ) ; return ; } } } else if ( isFilePart ) { this . transfer . connection . socket . emit ( 'proxy to partner' , this . transfer . receiverId , `###${ data }` , callback ) ; } else { this . transfer . connection . socket . emit ( 'proxy to partner' , this . transfer . receiverId , data ) ; if ( callback ) { callback ( ) ; } } } ; /**
     * Update the estimated time remaining.
     * This function will be executed every 50ms and check how much the transfer has progressed in that time
     */ private updateEstimate ( ) { if ( this . lastProgress !== this . transfer . progress ) { const timeForOnePercent = this . timeSince / ( this . transfer . progress - this . lastProgress ) ; const percentLeft = 100 - this . transfer . progress ; const currentEstimate = Math . round ( ( percentLeft * timeForOnePercent ) / 1000 ) ; this . lastEstimates . push ( currentEstimate ) ; const scale = ( num , inMin , inMax , outMin , outMax ) => ( num - inMin ) * ( outMax - outMin ) / ( inMax - inMin ) + outMin ; const keepEstimates = scale ( this . timeSince , 50 , 1000 , 50 , 5 ) ; while ( this . lastEstimates . length > keepEstimates ) { this . lastEstimates . shift ( ) ; } let estimate = 0 ; for ( const es of this . lastEstimates ) { estimate += es ; } estimate /= this . lastEstimates . length ; estimate -= scale ( this . lastEstimates . length , 5 , 50 , 0.5 , 2 ) ; estimate = Math . round ( estimate ) ; if ( estimate < 0 ) { estimate = 0 ; } this . emit ( { type : 'time estimate' , estimate } ) ; this . transfer . estimate = estimate ; this . transfer . triggerUpdate ( ) ; this . lastProgress = this . transfer . progress ; this . timeSince = 50 ; } else { this . timeSince += 50 ; } } param position private async sendFileData ( position ) { if ( ! this . uploadProvider ) { throw new Error ( 'Internal error: Cannot upload files as no files were selected' ) ; } this . transfer . triggerUpdate ( ) ; if ( position === 0 ) { const fileInfoBeforePrepare = this . uploadProvider . getFileInfo ( this . transfer . currentFile , true ) ; this . transfer . currentFileName = fileInfoBeforePrepare . name ; this . transfer . triggerUpdate ( ) ; await this . uploadProvider . prepareFile ( this . transfer . currentFile ) ; } const fileInfo = this . uploadProvider . getFileInfo ( this . transfer . currentFile , false ) ; if ( position === 0 ) { this . emit ( { type : 'new file' , ... fileInfo } ) ; this . size = fileInfo . size ; this . transfer . currentFileName = fileInfo . name ; this . transfer . triggerUpdate ( ) ; debug ( 'Starting new file' , fileInfo ) ; } const start = position * this . chunkSize ; const end = start + Math . min ( this . chunkSize , ( fileInfo . size - start ) ) ; debug ( "Requesting new file slice from provider" ) ; this . uploadProvider . getFileSlice ( this . transfer . currentFile , start , end ) . then ( ( v ) => this . handleFileSlice ( v ) ) ; } param value private handleFileSlice ( value ) { debug ( "Got new file slice" ) ; let data = this . transfer . connection . prepareFileSliceForCurrentMethod ( value ) ; debug ( "Transmitting to partner..." ) ; this . emit ( data , true , ( ) => { debug ( "Transmission successful" ) ; this . transmitted += this . chunkSize ; if ( this . transmitted >= this . size ) { debug ( 'Completed transfer of file' , this . transfer . currentFile ) ; this . transmitted = 0 ; this . transfer . currentFile += 1 ; this . emit ( { type : 'file complete' } ) ; this . transfer . estimate = - 1 ; this . lastEstimates = [ ] ; this . transfer . triggerUpdate ( ) ; if ( this . transfer . currentFile >= this . uploadProvider . getNumberOfFiles ( ) ) { debug ( 'Finished transferring all files' ) ; this . transfer . finishedTransfer = true ; this . onUploadDone ( ) ; this . transfer . openPage ( '/completed' ) ; clearInterval ( this . estimateInterval as number ) ; return ; } debug ( 'Transferring next file' , this . transfer . currentFile , this . uploadProvider . getNumberOfFiles ( ) ) ; } this . transfer . progress = ( this . transmitted / this . size ) * 100 ; this . emit ( { type : 'upload progress' , progress : this . transfer . progress } ) ; this . transfer . triggerUpdate ( ) ; const position = this . transmitted / this . chunkSize ; this . sendFileData ( position ) ; } ) ; } /**
     * Start the upload of the files in the upload provider
     */ startUpload ( ) { if ( this . hasUploaded ) { throw new Error ( 'Illegal state: UploadService can only be used once! Please create a new instance instead' ) ; } return new Promise ( resolve => { this . onUploadDone = resolve ; this . transfer . setTransferStatusText ( "Transferring files..." ) ; this . emit ( { type : 'use processor' , processor : this . uploadProvider . getDownloadProcessor ( ) } ) ; this . emit ( { type : 'number of files' , num : this . uploadProvider . getNumberOfFiles ( ) } ) ; this . transfer . totalFiles = this . uploadProvider . getNumberOfFiles ( ) ; this . transfer . progress = 0 ; this . estimateInterval = setInterval ( ( ) => this . updateEstimate ( ) , 50 ) ; this . transfer . triggerUpdate ( ) ; this . sendFileData ( 0 ) ; } ) ; } }