async-multiple
Version:
a lite asynchronous queue runtime library!
322 lines (307 loc) • 9.87 kB
JavaScript
import Lib from './index'
import orderBy from 'orderby'
import { PENDING, RESOLVE, REJECT, UNCALL, CALLING, CALLED } from '../constants'
export default class Each extends Lib {
constructor(...param) {
super()
this._defaultParams = {
task: {
type: [Array, Object],
default: [],
},
rejectOnError: {
type: Boolean,
default: false,
},
handleStart: () => {},
handleEnd: () => {},
randomStep: {
type: Boolean,
default: false,
},
stepBettwen: {
type: [Array, Number],
default: 0,
min: 0,
},
stepTimeout: {
type: Number,
default: undefined,
min: 0,
},
errorReplace: undefined,
errorRetry: {
type: Number,
default: 0,
min: 0,
},
maxCall: {
type: Number,
default: 1,
min: 0,
},
maxSuccessCount: {
type: Number,
default: undefined,
},
showProgress: {
type: Boolean,
default: false,
},
alias: {
type: String,
default: `TASK_${this.randomString(8)}`,
},
debug: {
type: Boolean,
default: false,
},
}
this._formatType = 'each'
this.taskStatus = UNCALL
this._stopAtNextTrick = false
this._actionName = this.randomString(8)
// check params
this._checkParams(param)
// init task
this._initTask()
// init hooks
this._initHooks()
}
start() {
// handle result
this.taskStatus = CALLING
return this._beginTask().then(response => {
const allArray = orderBy(response, ['order'])
const allObject = {}
const outputArray = allArray.map(item => {
allObject[this._formatType === 'map' ? item.input : item.order] = item.output
return item.output
})
return Promise.resolve({
response,
allObject,
allArray,
outputArray,
toObject: () => {
return allObject
},
toArray: () => {
return allArray
},
})
})
}
cancelTask() {
this._debug('Calling the cancelTask method, task will stop at next trick!')
this._stopAtNextTrick = true
return new Promise((resolve, reject) => {
this.event.on('TASKSTOPED', resolve())
})
}
get _getTaskOutput() {
if (!this.isType(this.errorReplace, undefined)) return this.errorReplace
return null
}
_assign(THIS, ...params) {
params = params.map(item => {
if (!this.isType(item, {})) return undefined
const newObj = {}
for (let key in item) {
const value = item[key]
if (this.isType(value, {})) {
if (!THIS.hasOwnProperty(key)) newObj[key] = this.isType(value.default, () => {}) ? value.default() : value.default
} else {
newObj[key] = value
}
}
return newObj
})
return this.assign(THIS, ...params)
}
_checkParams(param) {
this._assign(this, this._defaultParams)
if (this.isType(param[0], {})) {
this.assign(this, param[0])
} else if (this.isType(param[0], [])) {
this.task = param[0]
this.rejectOnError = param[1] || false
if (param[2]) {
if (this.isType(param[2], {})) {
this.assign(this, param[2])
} else {
throw this._errorManage('option expect to a object, please check!')
}
}
}
for (let name in this._defaultParams) {
const param = this._defaultParams[name]
if (this.isType(param, undefined)) {
continue
}
if (this.isType(param, {})) {
if (param.hasOwnProperty('type') && !this.isType(this[name], undefined)) {
param.type = this.isType(param.type, []) ? param.type : [param.type]
if (param.type.filter(item => item === this[name].constructor).length === 0) {
const types = param.type.map(item => item.name)
throw this._errorManage(`${name} expect to a ${types} but get a ${this.whatType(this[name])}, please check!`)
}
} else {
continue
}
} else if (!this.isType(param, this[name])) {
throw this._errorManage(`${name} expect to a ${this.whatType(param)} but get a ${this.whatType(this[name])}, please check!`)
}
}
}
_initTask() {
let order = 0
const task = []
for (const key in this.task) {
task.push({
order,
stepKey: key,
handle: this.task[key],
status: UNCALL,
})
order++
}
this.task = this.randomStep ? this.shuffle(task) : task
}
_initHooks() {
if (this.showProgress) {
const progress = ({ step, all, progress }) => {
const completeStep = this.task.filter(s => s.status === CALLED).length
console.log(`[ ${this.alias} ] all: ${all} complete: ${completeStep + 1} progress: ${progress}`)
}
const oldHandleEnd = this.handleEnd
this.handleEnd = (...params) => {
oldHandleEnd.apply(this, params)
progress.apply(this, params)
}
}
}
_getStepBettwen() {
return this.isType(this.stepBettwen, []) ? this.randomNumber(...this.stepBettwen.slice(0, 2)) : this.stepBettwen
}
_errorManage(message = 'unexpected error!') {
if (!this.isType(message, '')) message = message.toString()
return new Error(`[async-multiple ERROR]: ${message}`)
}
_makeError(err) {
if (err instanceof Error) return err
if (err instanceof String) return new Error(err)
return new Error(err.toString())
}
_createQueue() {
const unCalledTask = this.task.filter(s => s.status === UNCALL)
const callingTask = this.task.filter(s => s.status === CALLING)
const orderList = this.task.map(s => s.order)
const canCallCount = Math.min(this.maxCall - callingTask.length, unCalledTask.length)
if (canCallCount < 1) return
unCalledTask.slice(0, canCallCount).map(s => {
const step = orderList.indexOf(s.order)
this.task[step].status = CALLING
this.sleep(this._getStepBettwen()).then(() => {
this._callTaskHandle(step)
})
})
}
_beginTask() {
let singleTaskCalledNum = 0
const result = []
if (this.task.length === 0) return Promise.resolve([])
this._createQueue()
return new Promise((resolve, reject) => {
// bind action
this.event.on(this._actionName, ({ error, output, step }) => {
if (this.taskStatus === CALLED) return
// check if continue
if (this._stopAtNextTrick) {
this.taskStatus = CALLED
resolve(result)
this.event.emit('TASKSTOPED')
return
}
// chech if enough success result
if (this.maxSuccessCount && result.filter(item => !item.error).length >= this.maxSuccessCount) {
this._debug('Enough success result, task will stop!')
this.taskStatus = CALLED
resolve(result)
return
}
if (error) {
// if set errorRetry
if (this.errorRetry > singleTaskCalledNum) {
singleTaskCalledNum += 1
this.task[step].status = UNCALL
// reinit queue
this._createQueue()
return
}
error = this._makeError(error)
if (this.rejectOnError) {
return reject(error)
}
}
singleTaskCalledNum = 0
this.task[step].status = CALLED
result.push(
this.removeUndefined({
...this.task[step],
output,
error,
handle: undefined,
status: undefined,
stepKey: undefined
})
)
if (this.task.filter(s => s.status === CALLED).length !== this.task.length) {
this._createQueue()
} else {
this.taskStatus = CALLED
resolve(result)
}
})
})
}
_debug(...params) {
if (this.debug) {
super._debug(...params)
}
}
_callTaskHandle(step = 0, isRetry = false) {
if (step > this.task.length - 1 || step < 0) return Promise.reject(this._errorManage('step overflow!'))
let status = PENDING
let timer = null
const allStep = this.task.length
const completeStep = this.task.filter(s => s.status === CALLED).length
const progress = s => ((100 * s) / allStep).toFixed(2) + '%'
if (this.handleStart) this.handleStart({ step, all: allStep, progress: progress(completeStep), ...this.task[step], isRetry, cancelTask: this.cancelTask.bind(this) })
const succcesCallback = output => {
if (this.taskStatus === CALLED) return
if (status !== PENDING) return
status = RESOLVE
if (timer) clearInterval(timer)
const response = { ...this.task[step], all: allStep, progress: progress(completeStep + 1), output, error: null, step, isRetry, cancelTask: this.cancelTask.bind(this) }
if (this.handleEnd) this.handleEnd(response)
this.event.emit(this._actionName, response)
}
const errorCallback = error => {
if (this.taskStatus === CALLED) return
if (status !== PENDING) return
status = REJECT
if (timer) clearInterval(timer)
const response = { ...this.task[step], all: allStep, progress: progress(completeStep + 1), output: this._getTaskOutput, error, step, isRetry }
if (this.handleEnd) this.handleEnd(response)
this.event.emit(this._actionName, response)
}
if (!this.isType(this.stepTimeout, undefined)) {
timer = setTimeout(() => {
errorCallback(this._makeError('timeout!'))
}, this.stepTimeout)
}
const res = this.task[step].handle(this.task[step].input, step, this.cancelTask.bind(this), this.task[step].stepKey)
if (res !== undefined) this.makePromise(res).then(succcesCallback, errorCallback)
}
}