diff --git a/README.md b/README.md
index 7490676..8875ad8 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ React.render(, container);
|style | object | {}| root component inline style |
|className | string | - | root component className |
|disabled | boolean | false | whether disabled |
-|component | "div"|"span" | "span"| wrap component name |
+|component | "div"|"span" | "span"|
|action| string | function(file): string | Promise<string> | | form action url |
|method | string | post | request method |
|directory| boolean | false | support upload whole directory |
@@ -69,6 +69,7 @@ React.render(, container);
|accept | string | | input accept attribute |
|capture | string | | input capture attribute |
|multiple | boolean | false | only support ie10+|
+|concurrencyLimit | number | | asynchronously posts files with the concurrency limit |
|onStart | function| | start upload file |
|onError| function| | error callback |
|onSuccess | function | | success callback |
diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx
index e88c291..e35d5cb 100644
--- a/src/AjaxUploader.tsx
+++ b/src/AjaxUploader.tsx
@@ -6,6 +6,7 @@ import attrAccept from './attr-accept';
import type {
BeforeUploadFileType,
RcFile,
+ RequestTask,
UploadProgressEvent,
UploadProps,
UploadRequestError,
@@ -13,6 +14,7 @@ import type {
import defaultRequest from './request';
import traverseFileTree from './traverseFileTree';
import getUid from './uid';
+import asyncPool from './asyncPool';
interface ParsedFileInfo {
origin: RcFile;
@@ -21,8 +23,13 @@ interface ParsedFileInfo {
parsedFile: RcFile;
}
-class AjaxUploader extends Component {
- state = { uid: getUid() };
+interface UploadState {
+ uid: string;
+ requestTasks: RequestTask[];
+}
+
+class AjaxUploader extends Component {
+ state = { uid: getUid(), requestTasks: [] as RequestTask[] };
reqs: any = {};
@@ -30,6 +37,10 @@ class AjaxUploader extends Component {
private _isMounted: boolean;
+ appendRequstTask = (task: RequestTask) => {
+ this.setState(pre => ({ ...pre, requestTasks: [...pre.requestTasks, task] }));
+ };
+
onChange = (e: React.ChangeEvent) => {
const { accept, directory } = this.props;
const { files } = e.target;
@@ -111,17 +122,32 @@ class AjaxUploader extends Component {
return this.processFile(file, originFiles);
});
+ const { onBatchStart, concurrencyLimit } = this.props;
// Batch upload files
Promise.all(postFiles).then(fileList => {
- const { onBatchStart } = this.props;
-
onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })));
- fileList
- .filter(file => file.parsedFile !== null)
- .forEach(file => {
+ const parsedFiles = fileList.filter(file => file.parsedFile !== null);
+ if (concurrencyLimit) {
+ // Asynchronously posts files with the concurrency limit.
+ asyncPool(
+ concurrencyLimit,
+ this.state.requestTasks,
+ item =>
+ new Promise(resolve => {
+ const xhr = item.xhr;
+
+ item.done = resolve;
+
+ xhr.send(item.data);
+ }),
+ );
+ } else {
+ parsedFiles.forEach(file => {
this.post(file);
});
+ this.state.requestTasks.forEach(({ xhr, data }) => xhr.send(data));
+ }
});
};
@@ -162,7 +188,7 @@ class AjaxUploader extends Component {
const { data } = this.props;
let mergedData: Record;
if (typeof data === 'function') {
- mergedData = await data(file);
+ mergedData = data(file);
} else {
mergedData = data;
}
@@ -230,12 +256,13 @@ class AjaxUploader extends Component {
};
onStart(origin);
- this.reqs[uid] = request(requestOption);
+ this.reqs[uid] = request(requestOption, this.appendRequstTask);
}
reset() {
this.setState({
uid: getUid(),
+ requestTasks: [],
});
}
diff --git a/src/asyncPool.ts b/src/asyncPool.ts
new file mode 100644
index 0000000..d5ed7eb
--- /dev/null
+++ b/src/asyncPool.ts
@@ -0,0 +1,38 @@
+/**
+ * Asynchronously processes an array of items with a concurrency limit.
+ *
+ * @template T - Type of the input items.
+ * @template U - Type of the result of the asynchronous task.
+ *
+ * @param {number} concurrencyLimit - The maximum number of asynchronous tasks to execute concurrently.
+ * @param {T[]} items - The array of items to process asynchronously.
+ * @param {(item: T) => Promise} asyncTask - The asynchronous task to be performed on each item.
+ *
+ * @returns {Promise} - A promise that resolves to an array of results from the asynchronous tasks.
+ */
+export default async function asyncPool(
+ concurrencyLimit: number,
+ items: T[],
+ asyncTask: (item: T) => Promise,
+): Promise {
+ const tasks: Promise[] = [];
+ const pendings: Promise[] = [];
+
+ for (const item of items) {
+ const task = asyncTask(item);
+ tasks.push(task);
+
+ if (concurrencyLimit <= items.length) {
+ task.then(() => {
+ pendings.splice(pendings.indexOf(task), 1);
+ });
+ pendings.push(task);
+
+ if (pendings.length >= concurrencyLimit) {
+ await Promise.race(pendings);
+ }
+ }
+ }
+
+ return Promise.all(tasks);
+}
diff --git a/src/interface.tsx b/src/interface.tsx
index 9d02f27..0b8d390 100644
--- a/src/interface.tsx
+++ b/src/interface.tsx
@@ -29,7 +29,7 @@ export interface UploadProps
file: RcFile,
FileList: RcFile[],
) => BeforeUploadFileType | Promise | void;
- customRequest?: (option: UploadRequestOption) => void;
+ customRequest?: (option: UploadRequestOption) => void | { abort: () => void };
withCredentials?: boolean;
openFileDialogOnClick?: boolean;
prefixCls?: string;
@@ -44,6 +44,7 @@ export interface UploadProps
input?: React.CSSProperties;
};
hasControlInside?: boolean;
+ concurrencyLimit?: number;
}
export interface UploadProgressEvent extends Partial {
@@ -76,3 +77,9 @@ export interface UploadRequestOption {
export interface RcFile extends File {
uid: string;
}
+
+export interface RequestTask {
+ xhr: XMLHttpRequest;
+ data: File | FormData;
+ done?: () => void;
+}
diff --git a/src/request.ts b/src/request.ts
index 898847d..712af61 100644
--- a/src/request.ts
+++ b/src/request.ts
@@ -1,4 +1,9 @@
-import type { UploadRequestOption, UploadRequestError, UploadProgressEvent } from './interface';
+import type {
+ UploadRequestOption,
+ UploadRequestError,
+ UploadProgressEvent,
+ RequestTask,
+} from './interface';
function getError(option: UploadRequestOption, xhr: XMLHttpRequest) {
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
@@ -22,7 +27,10 @@ function getBody(xhr: XMLHttpRequest) {
}
}
-export default function upload(option: UploadRequestOption) {
+export default function upload(
+ option: UploadRequestOption,
+ appendTask: (task: RequestTask) => void,
+) {
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest();
@@ -62,11 +70,16 @@ export default function upload(option: UploadRequestOption) {
formData.append(option.filename, option.file);
}
+ const task: RequestTask = { xhr, data: formData };
+
xhr.onerror = function error(e) {
option.onError(e);
+ task.done?.();
};
xhr.onload = function onload() {
+ task.done?.();
+
// allow success when 2xx status
// see https://github.com/react-component/upload/issues/34
if (xhr.status < 200 || xhr.status >= 300) {
@@ -97,7 +110,7 @@ export default function upload(option: UploadRequestOption) {
}
});
- xhr.send(formData);
+ appendTask(task);
return {
abort() {