Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 59 additions & 24 deletions react/src/components/FileUploadManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ type UploadRequest = {

type UploadStatusInfo = {
vFolderName: string;
// Unique notification key for the current upload cycle. Regenerated when a
// new cycle starts (i.e., previous cycle ended) so that progress, resolved,
// and rejected notifications of different cycles do not overwrite each other.
cycleKey: string;
pendingFiles: Array<string>;
completedFiles: Array<string>;
failedFiles: Array<string>;
Expand Down Expand Up @@ -85,12 +89,21 @@ const FileUploadManager: React.FC = () => {
const queue = new PQueue({ concurrency: maxConcurrentUploads || 2 });

const pendingDeltaBytesRef = useRef<Record<string, number>>({});
// Tracks cycleKeys that have already been notified with their terminal
// (resolved/rejected) state. The cycle-end effect runs on every uploadStatus
// change and iterates over all folders, so without this guard a finished
// cycle's notification would be re-fired whenever an unrelated folder
// updates state.
const notifiedCyclesRef = useRef<Set<string>>(new Set());
const throttledUploadRequests = _.throttle(
(vFolderId: string, fileName: string) => {
const accumulatedDelta = pendingDeltaBytesRef.current[vFolderId] || 0;
pendingDeltaBytesRef.current[vFolderId] = 0;

setUploadStatus((prev) => {
const cycleKey = prev[vFolderId]?.cycleKey;
if (!cycleKey) return prev;

const uploadedFilesCount = prev[vFolderId]?.completedFiles?.length || 0;
const totalUploadedFilesCount =
(prev[vFolderId]?.completedFiles?.length || 0) +
Expand All @@ -102,7 +115,7 @@ const FileUploadManager: React.FC = () => {
(prev[vFolderId]?.completedBytes || 0) + accumulatedDelta;

upsertNotification({
key: 'upload:' + vFolderId,
key: cycleKey,
backgroundTask: {
status: 'pending',
percent:
Expand Down Expand Up @@ -158,16 +171,39 @@ const FileUploadManager: React.FC = () => {
);

setUploadStatus((prev) => {
const isFirstUpload = !prev[vFolderId];
const newTotalExpectedSize =
(prev[vFolderId]?.totalExpectedSize || 0) + currUploadTotalSize;
const currPct = isFirstUpload
// A cycle is considered ended when no files are pending. On a new
// upload that starts after the previous cycle ended, reset the
// accumulated completed/failed/bytes/total so the new cycle starts
// fresh. While a cycle is in progress, accumulate as before.
const isPreviousCycleEnded = _.isEmpty(prev[vFolderId]?.pendingFiles);
// Use a fresh notification key for each new cycle so that the previous
// cycle's resolved/rejected notification is not overwritten when a new
// upload starts.
const cycleKey = isPreviousCycleEnded
? `upload:${vFolderId}:${Date.now()}`
: (prev[vFolderId]?.cycleKey ?? `upload:${vFolderId}:${Date.now()}`);
const baseCompletedFiles = isPreviousCycleEnded
? []
: prev[vFolderId]?.completedFiles || [];
const baseFailedFiles = isPreviousCycleEnded
? []
: prev[vFolderId]?.failedFiles || [];
const baseCompletedBytes = isPreviousCycleEnded
? 0
: prev[vFolderId]?.completedBytes || 0;
const baseTotalExpectedSize = isPreviousCycleEnded
? 0
: ((prev[vFolderId]?.completedBytes || 0) / newTotalExpectedSize) *
100;
: prev[vFolderId]?.totalExpectedSize || 0;

const newTotalExpectedSize =
baseTotalExpectedSize + currUploadTotalSize;
const currPct =
newTotalExpectedSize > 0
? (baseCompletedBytes / newTotalExpectedSize) * 100
: 0;

upsertNotification({
key: 'upload:' + vFolderId,
key: cycleKey,
open: true,
message: (
<span>
Expand All @@ -178,7 +214,7 @@ const FileUploadManager: React.FC = () => {
}}
to={generateFolderPath(vFolderId)}
onClick={() => {
closeNotification('upload:' + vFolderId);
closeNotification(cycleKey);
}}
>{`${vFolderName}`}</BAILink>
</span>
Expand All @@ -197,15 +233,16 @@ const FileUploadManager: React.FC = () => {
...prev,
[vFolderId]: {
vFolderName,
cycleKey,
pendingFiles: [
...(prev[vFolderId]?.pendingFiles || []),
...uploadFileInfo.map(
(info) => info.file.webkitRelativePath || info.file.name,
),
],
completedFiles: prev[vFolderId]?.completedFiles || [],
failedFiles: prev[vFolderId]?.failedFiles || [],
completedBytes: prev[vFolderId]?.completedBytes || 0,
completedFiles: baseCompletedFiles,
failedFiles: baseFailedFiles,
completedBytes: baseCompletedBytes,
totalExpectedSize: newTotalExpectedSize,
},
};
Expand Down Expand Up @@ -276,10 +313,15 @@ const FileUploadManager: React.FC = () => {
useEffect(() => {
Object.entries(uploadStatus).forEach(([vFolderId, status]) => {
if (!_.isEmpty(status?.pendingFiles)) return;
if (!status?.cycleKey) return;
const cycleKey = status.cycleKey;
// Avoid re-firing the terminal notification when this effect runs again
// for unrelated reasons (e.g., another folder's upload updates state).
if (notifiedCyclesRef.current.has(cycleKey)) return;

if (!_.isEmpty(status?.failedFiles)) {
upsertNotification({
key: 'upload:' + vFolderId,
key: cycleKey,
open: true,
message: t('explorer.UploadFailed', {
folderName: status?.vFolderName,
Expand All @@ -295,9 +337,10 @@ const FileUploadManager: React.FC = () => {
},
extraDescription: _.join(status?.failedFiles, ', '),
});
notifiedCyclesRef.current.add(cycleKey);
} else if (!_.isEmpty(status?.completedFiles)) {
upsertNotification({
key: 'upload:' + vFolderId,
key: cycleKey,
open: true,
message: (
<span>
Expand All @@ -308,7 +351,7 @@ const FileUploadManager: React.FC = () => {
}}
to={generateFolderPath(vFolderId)}
onClick={() => {
closeNotification('upload:' + vFolderId);
closeNotification(cycleKey);
}}
>{`${status?.vFolderName}`}</BAILink>
</span>
Expand All @@ -322,15 +365,7 @@ const FileUploadManager: React.FC = () => {
},
duration: 3,
});
setUploadStatus((prev) => ({
...prev,
[vFolderId]: {
...prev[vFolderId],
completedFiles: [],
completedBytes: 0,
totalExpectedSize: 0,
},
}));
notifiedCyclesRef.current.add(cycleKey);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
Loading