1+ export class BatchUploadFiles < T extends { _id : string , name : string } , R extends { id : string } > {
2+ private failedCount : number = 0 ;
3+ private isDestroyed : boolean = false ;
4+ private uploadResults : R [ ] = [ ] ;
5+ private uploadQueue : T [ ] = [ ] ;
6+ private concurrentUploads : number = 0 ;
7+ private fileUploadsProgress : { name : string , progress : number } [ ] = [ ] ;
8+ private readonly maxUploadConcurrencyAndriod : number = 3 ;
9+ private readonly maxUploadConcurrencyIOS : number = 5 ;
10+ private readonly maxUploadConcurrencyOther : number = 7 ;
11+ private readonly maxUploadConcurrency : number ;
12+ private readonly minUploadConcurrency : number = 1 ;
13+ private readonly startUploadConcurrency : number = 3 ;
14+ private readonly maxFailedLimit : number = 20 ;
15+
16+ private uploadConcurrency : number = this . startUploadConcurrency ;
17+
18+ constructor (
19+ private readonly photos : T [ ] ,
20+ private readonly onUpload : ( index : number ) => void ,
21+ private readonly onUploadProgress : ( photoId : string , percent : number ) => void ,
22+ private readonly onFileUploadsProgressCb : ( fileUploadsProgress : { name : string , progress : number } [ ] ) => void ,
23+ private readonly onFailure : ( error : unknown , result : R [ ] , failed ?: T [ ] ) => void ,
24+ private readonly onSuccess : ( result : R [ ] ) => void ,
25+ private readonly uploadPhotoTask : ( photo : T , progressCb : ( name : string , progress : number ) => void ) => Promise < R > ,
26+ private readonly deviceType : 'ANDROID' | 'IOS' | 'OTHER' , // Other includes laptop so high powered
27+ ) {
28+ this . uploadQueue = [ ...photos ] ;
29+ this . maxUploadConcurrency = {
30+ 'ANDROID' : this . maxUploadConcurrencyAndriod ,
31+ 'IOS' : this . maxUploadConcurrencyIOS ,
32+ 'OTHER' : this . maxUploadConcurrencyOther ,
33+ } [ deviceType ] ;
34+
35+ this . fillUploadersConcurrently ( ) ;
36+ }
37+
38+
39+ private fillUploadersConcurrently ( ) : void {
40+ // check if there's photos on the uploadQueue and space on the concurrent queue
41+ if ( this . concurrentUploads < this . uploadConcurrency && ! ! this . uploadQueue . length ) {
42+ do {
43+ // if there's space, fill the concurrent uploads, shift()...ing from uploadQueue
44+ const photo : T | undefined = this . uploadQueue . shift ( ) ;
45+
46+ if ( ! ! photo ) {
47+ // and incrementing concurrentUploads, with each shifted photo then call processQueue
48+ this . concurrentUploads += 1 ;
49+
50+ this . uploaderRun ( photo ) ;
51+ } else {
52+ break ;
53+ }
54+ } while ( this . concurrentUploads < this . uploadConcurrency )
55+ } else {
56+ // if no space, DO NOTHING
57+ // console.warn('QUEUE FULL')
58+ }
59+ }
60+
61+ private uploaderRun ( photo : T ) : void {
62+ let fileUploadsProgressRef : { name : string ; progress : number ; } | null
63+ = { name : photo . name , progress : 0 } ;
64+
65+ this . fileUploadsProgress . push ( fileUploadsProgressRef ) ;
66+
67+ this . onFileUploadsProgressCb ( this . fileUploadsProgress ) ;
68+
69+ // call upload task
70+ this . uploadPhotoTask (
71+ photo ,
72+ ( name , progress ) => {
73+ if ( ! ! fileUploadsProgressRef ) {
74+ fileUploadsProgressRef . progress = progress ;
75+
76+ this . onFileUploadsProgressCb ( this . fileUploadsProgress ) ;
77+ }
78+ } ,
79+ ) . then (
80+ ( successPhoto : R ) => {
81+ const uploadResultsLength : number = this . uploadResults . length + 1 ;
82+
83+ // progress event handlers
84+ this . onUpload ( uploadResultsLength ) ;
85+ this . onUploadProgress ( successPhoto . id , uploadResultsLength ) ;
86+
87+ // call onAfterUploaderRun with result
88+ this . onAfterUploaderRun ( successPhoto ) ;
89+ }
90+ ) . catch ( // call onAfterUploaderRun with result
91+ ( e : Error ) => this . onAfterUploaderRun ( undefined , photo , e ) ,
92+ ) . finally (
93+ ( ) => {
94+ const photoIndex : number
95+ = this . fileUploadsProgress . findIndex (
96+ ( { name } ) => name === photo . name ,
97+ ) ;
98+
99+ if ( photoIndex !== - 1 ) {
100+ this . fileUploadsProgress . splice ( photoIndex , 1 ) ;
101+
102+ this . onFileUploadsProgressCb ( this . fileUploadsProgress ) ;
103+ }
104+
105+ fileUploadsProgressRef = null ;
106+ }
107+ ) ;
108+
109+ }
110+
111+ private onAfterUploaderRun (
112+ successPhoto : R | undefined ,
113+ failedPhoto ?: T ,
114+ error ?: unknown
115+ ) : void {
116+ if ( this . isDestroyed ) {
117+ return ;
118+ }
119+
120+ // decrement concurrentUploads
121+ this . concurrentUploads -= 1 ;
122+
123+ if ( ! ! failedPhoto ) {
124+ // if error increment failedCount, check if its reached maxFailedLimit
125+ this . failedCount += 1 ;
126+
127+ //unshift() failedPhoto back to the uploadQueue
128+ this . uploadQueue . unshift ( failedPhoto ) ;
129+
130+ if ( this . failedCount >= this . maxFailedLimit ) {
131+ // if its reached, call failedHandler with results at the point and remaining photos
132+ const uploadedIds : string [ ] = this . uploadResults . map ( ( { id } ) => id ) ;
133+
134+ this . onFailure (
135+ error ,
136+ this . uploadResults ,
137+ this . photos . filter ( ( { _id } ) => ! uploadedIds . includes ( _id ) ) ,
138+ ) ;
139+
140+ //call destroy
141+ this . destroy ( ) ;
142+
143+ //and return
144+ return ;
145+ } else {
146+ // if its not reached call decreaseUploadConcurrency
147+ this . decreaseUploadConcurrency ( ) ;
148+ }
149+ } else if ( ! ! successPhoto ) {
150+ // if no error, reset failedCount to 0, push successPhoto to uploadResults
151+
152+ this . failedCount && ( this . failedCount = 0 ) ;
153+
154+ this . uploadResults . push ( successPhoto ) ;
155+
156+ const photosLength : number = this . photos . length ;
157+ const uploadQueueLength : number = this . uploadQueue . length ;
158+ const uploadResultsLength : number = this . uploadResults . length ;
159+
160+
161+ // if upload queue empty and photosLength more than or equal to photosLength
162+ if ( uploadQueueLength === 0 && uploadResultsLength >= photosLength ) {
163+ // call success handler
164+ this . onSuccess ( this . uploadResults ) ;
165+
166+ // call destroy
167+ this . destroy ( ) ;
168+
169+ // and return
170+ return ;
171+ } else {
172+ this . increaseUploadConcurrency ( ) ;
173+ }
174+ } else {
175+ console . error ( 'Wierd situation, neither failedPhotos nor successPhotos are set' )
176+ }
177+
178+ // call fillUploadersConcurrently
179+ this . fillUploadersConcurrently ( ) ;
180+ }
181+
182+ destroy ( ) : void {
183+ this . isDestroyed = true ;
184+
185+ this . uploadResults = [ ] ;
186+ this . uploadQueue = [ ] ;
187+ }
188+
189+ private increaseUploadConcurrency ( ) : void {
190+ this . uploadConcurrency < this . maxUploadConcurrency && ( this . uploadConcurrency += 1 ) ;
191+ }
192+
193+ private decreaseUploadConcurrency ( ) : void {
194+ this . uploadConcurrency > this . minUploadConcurrency && ( this . uploadConcurrency -= 1 ) ;
195+ }
196+ }
0 commit comments