I am facing a challenge in React where I need to upload a bunch of images one after another. For this, I made a sort of a queue in which as the user selects the image it is added to the queue and the queue processes it by uploading the image to s3 and returns the uploaded URL and updated status here is the code for this:
import { uploadToS3 } from '~/utils/s3Upload';
import { PPFInspectionImage } from '~/context/PPFInspectionContext';
type StatusCallback = (
status: PPFInspectionImage['uploadStatus'],
serverUrl?: string,
key?: string
) => void;
interface QueueItem {
image: PPFInspectionImage;
partId: number;
onStatusChange: StatusCallback;
}
export class UploadManager {
private static instance: UploadManager;
private uploadQueue: QueueItem[] = [];
private isProcessing = false;
private maxConcurrent = 2;
private token: string;
private constructor(token: string) {
this.token = token;
}
static getInstance(token: string): UploadManager {
if (!UploadManager.instance || UploadManager.instance.token !== token) {
UploadManager.instance = new UploadManager(token);
}
return UploadManager.instance;
}
addToQueue(image: PPFInspectionImage, partId: number, onStatusChange: StatusCallback) {
this.uploadQueue.push({ image, partId, onStatusChange });
this.processQueue();
}
private async processQueue() {
if (this.isProcessing) return;
this.isProcessing = true;
try {
while (this.uploadQueue.length > 0) {
const batch = this.uploadQueue.splice(0, this.maxConcurrent);
await Promise.all(batch.map((item) => this.uploadImage(item)));
}
} catch (error) {
console.error('Error processing queue:', error);
} finally {
this.isProcessing = false;
}
}
private async uploadImage({ image, partId, onStatusChange }: QueueItem) {
onStatusChange('uploading');
try {
const response = await uploadToS3({
uri: image.localUri,
token: this.token,
inspectionType: 'ppf',
});
const serverUrl = `https://${process.env.EXPO_PUBLIC_BUCKET_NAME}.s3.${process.env.EXPO_PUBLIC_BUCKET_REGION}.amazonaws.com/${process.env.EXPO_PUBLIC_PPF_FOLDER}/${response.fileName}`;
onStatusChange('completed', serverUrl, response.key);
} catch (error) {
console.error('Upload failed:', error);
onStatusChange('failed');
// Add back to queue if retries remaining
if ((image.retryCount || 0) < 3) {
this.uploadQueue.push({
image: { ...image, retryCount: (image.retryCount || 0) + 1 },
partId,
onStatusChange,
});
}
}
}
// Method to retry failed uploads
retryUpload(image: PPFInspectionImage, partId: number, onStatusChange: StatusCallback) {
this.addToQueue({ ...image, retryCount: 0 }, partId, onStatusChange);
}
}
now the thing is I want to update the state after this queue is done processing the image for which I have the statusCallback and in that status callback I am calling a useReducer action like this
// Start background upload
UploadManager.getInstance(token).addToQueue(newImage, partId, (status, serverUrl, key) => {
dispatch({
type: 'UPDATE_IMAGE_STATUS',
payload: { partId, id: newImage.id, status, serverUrl, key },
});
});
The problem is if I add the images slowly they get processed correctly. The state is also updated correctly but when I quickly add the images one after another the queue processes them and uploads them and returns the correct complete statuses but when the state is updated multiple times, sometimes it does not set up the status correctly which results in one or two images status still being set to uploading or pending but in reality, they are uploaded successfully.
here is my reducer:
case 'UPDATE_IMAGE_STATUS': {
const part = state.faults[action.payload.partId];
if (!part) return state;
return {
...state,
faults: {
...state.faults,
[action.payload.partId]: {
...part,
images: part.images.map((img) =>
img.id === action.payload.id
? {
...img,
uploadStatus: action.payload.status,
serverUrl: action.payload.serverUrl,
key: action.payload.key,
}
: img
),
},
},
};
}