UNPKG

yuumi-request

Version:

request queue for browser

512 lines (504 loc) 16.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); 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); } } exports.YuumiRequest = YuumiRequest; exports.YuumiRequestInterceptor = YuumiRequestInterceptor;