yuumi-request
Version:
request queue for browser
507 lines (501 loc) • 16.4 kB
JavaScript
var JobStatus;
(function (JobStatus) {
JobStatus[JobStatus["WAITING"] = 0] = "WAITING";
JobStatus[JobStatus["PENDING"] = 1] = "PENDING";
JobStatus[JobStatus["FULLFILLED"] = 2] = "FULLFILLED";
JobStatus[JobStatus["REJECT"] = 3] = "REJECT";
JobStatus[JobStatus["CANCELED"] = 4] = "CANCELED";
})(JobStatus || (JobStatus = {}));
class Job {
constructor(tasks) {
this.status = JobStatus.WAITING;
/** 事件监听 */
this.lisenters = {
success: [],
fail: [],
complete: [],
cancel: []
};
this.tasks = tasks || [];
}
/**
* 添加一个任务到工作
* @param resolve Promise.resolve
* @param reject Promise.reject
* @returns Job
*/
addTask(resolve, reject) {
this.tasks.push([resolve, reject]);
return this;
}
/**
* 运行Job
* @returns Promise
*/
run() {
if (this.status === JobStatus.CANCELED)
return Promise.reject({ code: -1, message: "job be canceled." });
if (this.status !== JobStatus.WAITING)
return Promise.reject({ code: -1, message: "job is runing." });
this.status = JobStatus.PENDING;
const token = new Promise((resolve, reject) => {
this.complete = () => {
this.dispatch("complete");
resolve(void 0);
};
this.abort = () => reject({ code: -1, message: "job be canceled." });
});
let promise = Promise.resolve();
this.tasks.forEach(([resolve, reject]) => {
if (typeof reject !== 'function') {
reject = (reason) => Promise.reject(reason);
}
promise = promise.then((value) => {
// 防止调用cancel后,后续还在走resolve
if (this.status === JobStatus.CANCELED) {
return Promise.reject({ code: -1, message: "job be canceled." });
}
return resolve(value);
}, reject);
});
promise = promise.then((value) => {
this.status = JobStatus.FULLFILLED;
this.dispatch("success", value);
return value;
}, (reason) => {
this.status = JobStatus.REJECT;
this.dispatch("fail", reason);
return Promise.reject(reason);
});
return Promise.race([promise, token]).finally(() => {
this.complete && this.complete;
});
}
cancel() {
if (this.status !== JobStatus.WAITING && this.status !== JobStatus.PENDING)
return;
this.abort && this.abort();
this.status = JobStatus.CANCELED;
this.dispatch("cancel");
}
dispatch(name, value) {
if (!this.lisenters[name])
return;
this.lisenters[name].forEach((item) => item(value));
}
addLisenter(name, lisenter) {
if (!this.lisenters[name]) {
this.lisenters[name] = [];
}
this.lisenters[name].push(lisenter);
}
removeListenr(name, lisenter) {
if (!this.lisenters[name])
return;
const index = this.lisenters[name].findIndex((item) => item === lisenter);
if (index >= 0) {
this.lisenters[name].splice(index, 1);
}
}
}
var QueueStatus;
(function (QueueStatus) {
QueueStatus[QueueStatus["DEFAULT"] = 0] = "DEFAULT";
QueueStatus[QueueStatus["STARTING"] = 1] = "STARTING";
QueueStatus[QueueStatus["STOPED"] = 2] = "STOPED";
QueueStatus[QueueStatus["ENDED"] = 3] = "ENDED";
})(QueueStatus || (QueueStatus = {}));
class Queue {
constructor(jobs, options) {
this.jobs = [];
this.status = QueueStatus.DEFAULT;
this.session = 0;
/** 事件监听 */
this.lisenters = {
fail: [],
start: [],
stop: [],
end: []
};
if (jobs) {
this.jobs = jobs;
}
const { concurrency, autoStart } = Object.assign({
concurrency: 4,
autoStart: false
}, options);
this.concurrency = concurrency;
if (autoStart) {
this.start();
}
}
_run() {
if (this.status > QueueStatus.STARTING)
return;
if (this.session >= this.concurrency)
return;
const job = this.jobs.shift();
if (!job)
return;
this.session++;
job.addLisenter("afterXHR", () => {
this.session--;
});
job.run().finally(() => this._run()).catch((err) => {
this.dispatch("fail", err);
});
this._run();
}
/**
* 开始运行
*/
start() {
if (this.status === QueueStatus.STARTING)
return;
this.status = QueueStatus.STARTING;
this._run();
this.dispatch("start");
}
/**
* 停止运行
*/
stop() {
if (this.status === QueueStatus.STOPED)
return;
this.status = QueueStatus.STOPED;
this.dispatch("stop");
}
/**
* 结束运行
*/
end() {
if (this.status === QueueStatus.ENDED)
return;
this.status = QueueStatus.ENDED;
this.jobs = [];
this.dispatch("end");
}
/**
* 在末尾添加一个工作
* @param job 工作
*/
push(job) {
if (job.constructor.name !== Job.name)
return;
this.jobs.push(job);
if (this.status === QueueStatus.STARTING) {
this._run();
}
}
/**
* 在开始添加一个工作
* @param job 工作
*/
unshift(job) {
if (job.constructor.name !== Job.name)
return;
this.jobs.unshift(job);
if (this.status === QueueStatus.STARTING) {
this._run();
}
}
dispatch(name, value) {
this.lisenters[name].forEach((item) => item(value));
}
addLisenter(name, lisenter) {
this.lisenters[name].push(lisenter);
}
removeListenr(name, lisenter) {
const index = this.lisenters[name].findIndex((item) => item === lisenter);
if (index >= 0) {
this.lisenters[name].splice(index, 1);
}
}
}
function isUndefine(input) {
return input === null || input === undefined;
}
function isBoolean(input) {
return typeof input === 'boolean';
}
function isNumber(input) {
return typeof input === 'number';
}
function isNaN(input) {
return Number.isNaN ? Number.isNaN(input) : isNumber(input) && input.toString() === "NaN";
}
function isObject(input) {
return Object.prototype.toString.call(input) === '[object Object]';
}
function isArray(input) {
return Object.prototype.toString.call(input) === '[object Array]';
}
function isFormData(input) {
return Object.prototype.toString.call(input) === '[object FormData]';
}
function paramStringify(params) {
if (Object.prototype.toString.call(params) !== '[object Object]')
return '';
const items = [];
for (const key in params) {
const item = params[key];
if (isUndefine(item))
continue;
if (isNaN(item))
continue;
if (isArray(item)) {
items.push(item.map((child, idx) => {
return isObject(child) ? `${key}[${idx}]=${JSON.stringify(child)}` : `${key}[${idx}]=${child}`;
}).join('&'));
continue;
}
if (isObject(item)) {
items.push(`${key}=${JSON.stringify(item)}`);
continue;
}
items.push(`${key}=${item}`);
}
return items.join('&');
}
class XHR {
constructor(option) {
this.url = option.url;
this.method = option.method;
this.async = isBoolean(option.async) ? option.async : true;
this.headers = option.headers;
this.data = option.data;
this.cancelToken = option.cancelToken;
this.timeout = option.timeout;
this.uploader = option.uploader;
}
exec() {
const proxy = {};
const promise = new Promise((resolve, reject) => {
proxy.resolve = resolve;
proxy.reject = reject;
});
const xhr = new XMLHttpRequest();
xhr.open(this.method, this.url, this.async);
this.setHeaders(xhr);
this.addTimeoutListener(xhr, () => {
proxy.reject({ code: -1, message: "request:timeout" });
});
this.addAbortListener(xhr, () => {
proxy.reject({ code: -1, message: "request:aborted" });
});
this.addUploadListener(xhr);
xhr.addEventListener("error", (e) => {
proxy.reject({ code: -1, message: e.toString() });
});
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === 4 && xhr.status !== 0) {
if (/^20[01234]$/.test(xhr.status.toString())) {
proxy.resolve({
request: this,
status: xhr.status,
response: xhr.response
});
}
else {
proxy.reject({
request: this,
status: xhr.status,
response: xhr.response
});
}
}
});
this.sendData(xhr);
return promise;
}
sendData(xhr) {
if (/get/i.test(this.method) || !this.data) {
return xhr.send(undefined);
}
const headers = this.headers;
if (isFormData(this.data)) {
xhr.send(this.data);
}
else if (headers && /application\/json/i.test(headers['Content-Type'])) {
xhr.send(JSON.stringify(this.data));
}
else if (headers && /application\/x-www-form-urlencoded/i.test(headers['Content-Type'])) {
xhr.send(this.encodeData);
}
else {
xhr.send(this.data);
}
}
setHeaders(xhr) {
if (!this.headers)
return;
for (const key in this.headers) {
if (!this.headers.hasOwnProperty(key))
continue;
xhr.setRequestHeader(key, this.headers[key]);
}
}
addTimeoutListener(xhr, listener) {
const timeout = this.timeout;
if (timeout && isNumber(timeout) && timeout > 0) {
xhr.timeout = timeout;
}
xhr.addEventListener("timeout", listener);
}
addAbortListener(xhr, listener) {
xhr.addEventListener("abort", listener);
const cancelToken = this.cancelToken;
if (typeof cancelToken !== "function")
return;
cancelToken(() => xhr.abort());
// reset cancel token to undefined
xhr.addEventListener("error", () => {
cancelToken();
});
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === 4 && xhr.status !== 0) {
cancelToken();
}
});
}
addUploadListener(xhr) {
if (!this.uploader)
return;
for (const event in this.uploader) {
if (!this.uploader.hasOwnProperty(event))
continue;
xhr.upload.addEventListener(event, this.uploader[event]);
}
}
}
class YuumiRequestInterceptor {
constructor() {
this.requestInterceptors = [];
this.responseInterceptors = [];
}
request(resolve, reject) {
this.requestInterceptors.push([resolve, reject]);
}
response(resolve, reject) {
this.responseInterceptors.push([resolve, reject]);
}
}
class YuumiRequest {
constructor(option) {
// 拦截器
this.interceptor = new YuumiRequestInterceptor();
const _option = Object.assign({
baseURI: "",
headers: {},
concurrency: 4,
timeout: 0,
xhr: (config) => new XHR(config).exec()
}, option);
this.baseURI = _option.baseURI;
this.headers = _option.headers;
this.concurrency = _option.concurrency;
this.timeout = _option.timeout;
this.paramStringify = _option.paramStringify;
this.xhr = _option.xhr;
this.queue = new Queue([], { concurrency: this.concurrency, autoStart: true });
}
request(option) {
let _url = `${this.baseURI}${option.path}`;
const query = this.paramStringify ? this.paramStringify(option.params) : paramStringify(option.params);
if (query) {
_url = /\?/.test(_url) ? `${_url}&${query}` : `${_url}?${query}`;
}
const config = {
url: _url,
method: option.method,
async: option.async,
headers: Object.assign({}, this.headers, option.headers),
data: option.data,
encodeData: this.paramStringify ? this.paramStringify(option.data) : paramStringify(option.data),
cancelToken: option.cancelToken,
timeout: option.timeout || this.timeout,
uploader: option.uploader
};
const job = new Job();
job.addTask(() => Promise.resolve(config));
// request interceptors
this.interceptor.requestInterceptors.forEach((item) => {
job.addTask(...item);
});
// request
job.addTask((value) => {
// 拼接拦截器添加的params
if (value.params) {
const _query = this.paramStringify ? this.paramStringify(value.params) : paramStringify(value.params);
if (_query) {
value.url = /\?/.test(_url) ? `${_url}&${_query}` : `${_url}?${_query}`;
}
}
return this.xhr(value).finally(() => {
job.dispatch("afterXHR");
});
});
// response interceptors
this.interceptor.responseInterceptors.forEach((item) => {
job.addTask(...item);
});
const result = new Promise((resolve, reject) => {
// 当job在queue中等待时的cancelToken, 取消job直接返回
if (typeof config.cancelToken === 'function') {
config.cancelToken(() => {
job.cancel();
reject({ code: -1, message: "request:aborted" });
});
}
job.addTask(resolve, reject);
});
if (option.enforce === "pre") {
this.queue.unshift(job);
}
else {
this.queue.push(job);
}
return result;
}
get(path, params, options) {
const _options = Object.assign({}, options, {
path: path,
method: 'GET',
params: Object.assign({}, options === null || options === void 0 ? void 0 : options.params, params)
});
return this.request.call(this, _options);
}
post(path, data, options) {
const _options = Object.assign({}, options, {
path: path,
method: 'POST',
data: Object.assign({}, options === null || options === void 0 ? void 0 : options.data, data)
});
return this.request.call(this, _options);
}
patch(path, data, options) {
const _options = Object.assign({}, options, {
path: path,
method: 'PATCH',
data: Object.assign({}, options === null || options === void 0 ? void 0 : options.data, data)
});
return this.request.call(this, _options);
}
put(path, data, options) {
const _options = Object.assign({}, options, {
path: path,
method: 'PUT',
data: Object.assign({}, options === null || options === void 0 ? void 0 : options.data, data)
});
return this.request.call(this, _options);
}
delete(path, data, options) {
const _options = Object.assign({}, options, {
path: path,
method: 'DELETE',
data: Object.assign({}, options === null || options === void 0 ? void 0 : options.data, data)
});
return this.request.call(this, _options);
}
}
export { YuumiRequest, YuumiRequestInterceptor };