diff --git a/README.md b/README.md
index 7490676..ac410e2 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ online example: https://upload.react-component.vercel.app/
## Feature
-* support IE11+, Chrome, Firefox, Safari
+- support IE11+, Chrome, Firefox, Safari
## install
@@ -54,29 +54,30 @@ React.render(, container);
### props
-|name|type|default| description|
-|-----|---|--------|----|
-|name | string | file| file param post to server |
-|style | object | {}| root component inline style |
-|className | string | - | root component className |
-|disabled | boolean | false | whether disabled |
-|component | "div"|"span" | "span"| wrap component name |
-|action| string | function(file): string | Promise<string> | | form action url |
-|method | string | post | request method |
-|directory| boolean | false | support upload whole directory |
-|data| object/function(file) | | other data object to post or a function which returns a data object(a promise object which resolve a data object) |
-|headers| object | {} | http headers to post, available in modern browsers |
-|accept | string | | input accept attribute |
-|capture | string | | input capture attribute |
-|multiple | boolean | false | only support ie10+|
-|onStart | function| | start upload file |
-|onError| function| | error callback |
-|onSuccess | function | | success callback |
-|onProgress | function || progress callback, only for modern browsers|
-|beforeUpload| function |null| before upload check, return false or a rejected Promise will stop upload, only for modern browsers|
-|customRequest | function | null | provide an override for the default xhr behavior for additional customization|
-|withCredentials | boolean | false | ajax upload with cookie send |
-|openFileDialogOnClick | boolean | true | useful for drag only upload as it does not trigger on enter key or click event |
+| name | type | default | description |
+| --- | --- | --- | --- |
+| name | string | file | file param post to server |
+| style | object | {} | root component inline style |
+| className | string | - | root component className |
+| disabled | boolean | false | whether disabled |
+| component | "div" | "span" | "span" | wrap component name |
+| action | string | function(file): string | Promise<string> | | form action url |
+| method | string | post | request method |
+| directory | boolean | false | support upload whole directory |
+| data | object/function(file) | | other data object to post or a function which returns a data object(a promise object which resolve a data object) |
+| headers | object | {} | http headers to post, available in modern browsers |
+| accept | string | | input accept attribute |
+| capture | string | | input capture attribute |
+| multiple | boolean | false | only support ie10+ |
+| concurrencyLimit | number | undefined | undefined | asynchronously posts files with the concurrency limit |
+| onStart | function | | start upload file |
+| onError | function | | error callback |
+| onSuccess | function | | success callback |
+| onProgress | function | | progress callback, only for modern browsers |
+| beforeUpload | function | null | before upload check, return false or a rejected Promise will stop upload, only for modern browsers |
+| customRequest | function | null | provide an override for the default xhr behavior for additional customization |
+| withCredentials | boolean | false | ajax upload with cookie send |
+| openFileDialogOnClick | boolean | true | useful for drag only upload as it does not trigger on enter key or click event |
#### onError arguments
@@ -88,9 +89,7 @@ React.render(, container);
1. `result`: response body
2. `file`: upload file
-3. `xhr`: xhr header, only for modern browsers which support AJAX upload. since
- 2.4.0
-
+3. `xhr`: xhr header, only for modern browsers which support AJAX upload. since 2.4.0
### customRequest
@@ -98,16 +97,15 @@ Allows for advanced customization by overriding default behavior in AjaxUploader
customRequest callback is passed an object with:
-* `onProgress: (event: { percent: number }): void`
-* `onError: (event: Error, body?: Object): void`
-* `onSuccess: (body: Object): void`
-* `data: Object`
-* `filename: String`
-* `file: File`
-* `withCredentials: Boolean`
-* `action: String`
-* `headers: Object`
-
+- `onProgress: (event: { percent: number }): void`
+- `onError: (event: Error, body?: Object): void`
+- `onSuccess: (body: Object): void`
+- `data: Object`
+- `filename: String`
+- `file: File`
+- `withCredentials: Boolean`
+- `action: String`
+- `headers: Object`
### methods
diff --git a/src/AjaxUploader.tsx b/src/AjaxUploader.tsx
index e88c291..1cc0ef0 100644
--- a/src/AjaxUploader.tsx
+++ b/src/AjaxUploader.tsx
@@ -13,6 +13,7 @@ import type {
import defaultRequest from './request';
import traverseFileTree from './traverseFileTree';
import getUid from './uid';
+import ConcurrencyRequester from './concurrencyRequest';
interface ParsedFileInfo {
origin: RcFile;
@@ -26,6 +27,8 @@ class AjaxUploader extends Component {
reqs: any = {};
+ private concurrencyRequester?: ConcurrencyRequester;
+
private fileInput: HTMLInputElement;
private _isMounted: boolean;
@@ -111,10 +114,14 @@ class AjaxUploader extends Component {
return this.processFile(file, originFiles);
});
+ const { onBatchStart, concurrencyLimit } = this.props;
+
+ if (concurrencyLimit) {
+ this.concurrencyRequester = new ConcurrencyRequester(concurrencyLimit);
+ }
+
// Batch upload files
Promise.all(postFiles).then(fileList => {
- const { onBatchStart } = this.props;
-
onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })));
fileList
@@ -122,6 +129,11 @@ class AjaxUploader extends Component {
.forEach(file => {
this.post(file);
});
+
+ // Asynchronously posts files with the concurrency limit.
+ if (this.concurrencyRequester) {
+ this.concurrencyRequester.send();
+ }
});
};
@@ -230,7 +242,11 @@ class AjaxUploader extends Component {
};
onStart(origin);
- this.reqs[uid] = request(requestOption);
+ if (this.concurrencyRequester) {
+ this.reqs[uid] = this.concurrencyRequester.append(requestOption);
+ } else {
+ this.reqs[uid] = request(requestOption);
+ }
}
reset() {
diff --git a/src/concurrencyRequest.ts b/src/concurrencyRequest.ts
new file mode 100644
index 0000000..97bce5b
--- /dev/null
+++ b/src/concurrencyRequest.ts
@@ -0,0 +1,155 @@
+import type { ConcurrencyRequestTask, UploadRequestOption } from './interface';
+import { onXHRLoad, prepareData, prepareXHR } from './request';
+
+/**
+ * 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.
+ */
+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);
+}
+
+type DataType = 'form' | 'blob' | 'string';
+
+/**
+ * Represents a class for handling concurrent requests with a specified concurrency limit.
+ *
+ * @template T - The type of data to be uploaded.
+ */
+export default class ConcurrencyRequester {
+ /**
+ * The concurrency limit for handling requests simultaneously.
+ */
+ private concurrencyLimit: number;
+
+ /**
+ * An array to store the tasks for concurrent requests.
+ */
+ private tasks: ConcurrencyRequestTask[] = [];
+
+ /**
+ * The type of data to be sent in the request ('form', 'blob', or 'string').
+ */
+ private dataType: DataType;
+
+ /**
+ * Creates an instance of ConcurrencyRequester.
+ *
+ * @param {number} concurrencyLimit - The concurrency limit for handling requests simultaneously.
+ * @param {DataType} [dataType='form'] - The type of data to be sent in the request ('form', 'blob', or 'string').
+ */
+ constructor(concurrencyLimit: number, dataType: DataType = 'form') {
+ this.concurrencyLimit = concurrencyLimit;
+ this.dataType = dataType;
+ }
+
+ /**
+ * Prepares data based on the specified data type.
+ *
+ * @param {UploadRequestOption} option - The upload request option.
+ * @returns {string | Blob | FormData} - The prepared data based on the specified data type.
+ * @private
+ */
+ private prepareData = (option: UploadRequestOption): string | Blob | FormData => {
+ if (this.dataType === 'form') {
+ return prepareData(option);
+ }
+
+ return option.file;
+ };
+
+ /**
+ * Prepares a task for a concurrent request.
+ *
+ * @param {UploadRequestOption} option - The upload request option.
+ * @returns {ConcurrencyRequestTask} - The prepared task for the concurrent request.
+ * @private
+ */
+ private prepare = (option: UploadRequestOption): ConcurrencyRequestTask => {
+ const xhr = prepareXHR(option);
+
+ const data = this.prepareData(option);
+
+ const task: ConcurrencyRequestTask = { xhr, data };
+
+ xhr.onerror = function error(e) {
+ task.done?.();
+ option.onError(e);
+ };
+
+ xhr.onload = function onload() {
+ task.done?.();
+
+ onXHRLoad(this, option);
+ };
+
+ return task;
+ };
+
+ /**
+ * Appends a new upload request to the tasks array.
+ *
+ * @param {UploadRequestOption} option - The upload request option.
+ * @returns {{ abort: () => void }} - An object with an `abort` function to cancel the request.
+ */
+ append = (option: UploadRequestOption): { abort: () => void } => {
+ const task = this.prepare(option);
+
+ this.tasks.push(task);
+
+ return {
+ abort() {
+ task.xhr.abort();
+ },
+ };
+ };
+
+ /**
+ * Sends all the appended requests concurrently.
+ */
+ send = (): void => {
+ asyncPool(
+ this.concurrencyLimit,
+ this.tasks,
+ item =>
+ new Promise(resolve => {
+ const xhr = item.xhr;
+
+ item.done = resolve;
+
+ xhr.send(item.data);
+ }),
+ );
+ };
+}
diff --git a/src/interface.tsx b/src/interface.tsx
index 9d02f27..b994693 100644
--- a/src/interface.tsx
+++ b/src/interface.tsx
@@ -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 ConcurrencyRequestTask {
+ xhr: XMLHttpRequest;
+ data: File | FormData | string | Blob;
+ done?: () => void;
+}
diff --git a/src/request.ts b/src/request.ts
index 898847d..56fe726 100644
--- a/src/request.ts
+++ b/src/request.ts
@@ -1,4 +1,4 @@
-import type { UploadRequestOption, UploadRequestError, UploadProgressEvent } from './interface';
+import type { UploadProgressEvent, UploadRequestError, UploadRequestOption } from './interface';
function getError(option: UploadRequestOption, xhr: XMLHttpRequest) {
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
@@ -22,7 +22,17 @@ function getBody(xhr: XMLHttpRequest) {
}
}
-export default function upload(option: UploadRequestOption) {
+export function onXHRLoad(xhr: XMLHttpRequest, option: UploadRequestOption) {
+ // allow success when 2xx status
+ // see https://github.com/react-component/upload/issues/34
+ if (xhr.status < 200 || xhr.status >= 300) {
+ return option.onError(getError(option, xhr), getBody(xhr));
+ }
+
+ return option.onSuccess(getBody(xhr), xhr);
+}
+
+export function prepareXHR(option: UploadRequestOption): XMLHttpRequest {
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest();
@@ -35,6 +45,40 @@ export default function upload(option: UploadRequestOption) {
};
}
+ xhr.open(option.method, option.action, true);
+
+ // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
+ if (option.withCredentials && 'withCredentials' in xhr) {
+ xhr.withCredentials = true;
+ }
+
+ const headers = option.headers || {};
+
+ // when set headers['X-Requested-With'] = null , can close default XHR header
+ // see https://github.com/react-component/upload/issues/33
+ if (headers['X-Requested-With'] !== null) {
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ }
+
+ Object.keys(headers).forEach(h => {
+ if (headers[h] !== null) {
+ xhr.setRequestHeader(h, headers[h]);
+ }
+ });
+
+ xhr.onerror = function error(e) {
+ option.onError(e);
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ xhr.onload = function onload(_) {
+ onXHRLoad(this, option);
+ };
+
+ return xhr;
+}
+
+export function prepareData(option: UploadRequestOption) {
// eslint-disable-next-line no-undef
const formData = new FormData();
@@ -62,40 +106,14 @@ export default function upload(option: UploadRequestOption) {
formData.append(option.filename, option.file);
}
- xhr.onerror = function error(e) {
- option.onError(e);
- };
-
- xhr.onload = function onload() {
- // allow success when 2xx status
- // see https://github.com/react-component/upload/issues/34
- if (xhr.status < 200 || xhr.status >= 300) {
- return option.onError(getError(option, xhr), getBody(xhr));
- }
-
- return option.onSuccess(getBody(xhr), xhr);
- };
-
- xhr.open(option.method, option.action, true);
-
- // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
- if (option.withCredentials && 'withCredentials' in xhr) {
- xhr.withCredentials = true;
- }
-
- const headers = option.headers || {};
+ return formData;
+}
- // when set headers['X-Requested-With'] = null , can close default XHR header
- // see https://github.com/react-component/upload/issues/33
- if (headers['X-Requested-With'] !== null) {
- xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
- }
+export default function upload(option: UploadRequestOption) {
+ const xhr = prepareXHR(option);
- Object.keys(headers).forEach(h => {
- if (headers[h] !== null) {
- xhr.setRequestHeader(h, headers[h]);
- }
- });
+ // eslint-disable-next-line no-undef
+ const formData = prepareData(option);
xhr.send(formData);