Skip to content

Commit 093d7cc

Browse files
committed
fix: add batch upload files class
1 parent 41cf400 commit 093d7cc

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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+
}

projects/ngx-file-upload/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './lib/file-upload/file-upload-timeout.error';
77
export * from './lib/file-upload/file-upload.service';
88
export * from './lib/environment-config.interface';
99
export * from './lib/ngx-file-upload.module';
10+
export * from './lib/batch-upload-files';

0 commit comments

Comments
 (0)