import clone from 'clone' import RxClass from '@uci-utils/rx-class' const DEFAULT_SETTINGS = { enabled: false, timer: true, // will update countdown and other dynamic values every minute or second } let update_all=false let init = true // Queue Multiple Schedulers class UCIScheduleRunner extends RxClass { constructor(opts) { super(opts) this.name = opts.name || 'Scheule Runner' this.set('state',false) // if runner is actually started this.set('timer',0) // opts.timer != null ? opts.timer : DEFAULT_SETTINGS.timer) // console.log('timer', this.timer, DEFAULT_SETTINGS.timer) this.schedules = [] this._toID = 0 // id of timeout // this._delayed = [] // active schedules that are delayed this._running = [] this._queue = [] // object or lists of all active (running or queued) schedules by timeout id this._disabledSchedules = [] this.set('running','') // list of currently running schedules this.set('queue','') // list of queued names awaiting run this.set('nextTS',0) this.set('nextDT','') this.set('nextName','') this.set('queueCount',0) this.set('countdown','') this.set('runningCount',0) // console.log('passed schedule',opts.schedules.map(sch=>sch.id)) if (opts.schedules) opts.schedules.forEach(sch => { this.addSchedule(sch) }) this.rxSubscribe('runningCount','ready',count => { if (!count && this._queue.length) { setTimeout(()=>this.runFromQueue(),0) } }) this.rxSubscribe('timer','countdown', (interval) => { // console.log('timer state changed', interval) clearInterval(this.countdownInterval) if (interval) this.countdownInterval = setInterval(this._countdown.bind(this),interval*1000) else this.countdown = '' }) this.set('enabled', opts.enabled != null ? opts.enabled: DEFAULT_SETTINGS.enabled, { subscribe:{ name:'start', handler: (start) => { console.log('enabled', start) if (start) this.start() else if (!init) this.stop() }} } ) init=false } // end construtor _countdown(now) { const first = this.schedules[0] // console.log('first', first.name) if (!first) return this.countdown = now ? first._countdown() : first.countdown // console.log(this.countdown) return this.countdown } get _countdownMS() { return (this.schedules[0]||{}).countdownMS } async start() { console.log('-----------starting scheduler runner-----------------') this.state = true await this.update() this._getEnabledSchedule().forEach(sch=> sch.timer=1.1) // fire off timers this.timer = 1.1 this.rxSubscribe('nextTS','resetTO', () => { // console.log('^^^^runner nextTS changed, resetting runner timeout') this._countdown(true) this.runnerTimeout() }) } stop (now=true) { console.log('--------------stopping scheduler runner-------------------') this.state = false this.rxUnsubscribe('nextTS','resetTO') this.timer = 0 this._getEnabledSchedule().forEach(sch=> sch.timer=0) this.nextDT = '' this.countdown = '' this.nextName = '' this.runnerTimeout('stop') // this.removeActive() } // main timeout based on the first schedule to trigger runnerTimeout (stop) { clearTimeout(this._runnerTimeout) if (stop) {console.log('stopped, timeout cleared');return} // pass anything to stop if (!this.nextSchedule) return this._toID++ console.log(this._toID, '+++++ next schedule trigger in', this.countdown,this.nextName,'++++') // console.log('current schedules run order\n',this.schedules.map(sch=>`${sch.nextDT} : ${sch.name}`)) this._runnerTimeout = setTimeout(async () => { console.log('**********************timeout triggered for ID', this._toID, '****************************') let updates = [] this.schedules.forEach(sch =>{ // add first and any others set for ~ same time if (sch.countdownMS <= 1000 ) { // should catch any with same TS updates.push(sch.update.bind(sch)) const activeSch = clone(sch) activeSch.toid = this._toID activeSch.runID = `${this._toID}:${sch.id}` this.activeCount++ if (activeSch.settings.simultaneous) this.runSchedule(sch) else this.queueSchedule(activeSch) } }) if (!this.runningCount && this._queue.length) this.runFromQueue() // this._queue[this._toID].next = 0 // this.trigger(this._queue[this._toID].length,this._toID) updates.forEach(update=>update()) // only need update the ones that were active // await this.update() // updates all _enabled },this._countdownMS) } runSchedule(sch) { this.startScheduleAction(sch) this._running.push(sch) this.runningCount=this._running.length this.running = this._running.map(s=>s.name).join(',') } runFromQueue(){ if (this._queue.length) { const run = this._queue.shift() this.queueCount=this._queue.length this.queue = this._queue.map(s=>s.name).join(',') this._running.push(run) this.runningCount=this._running.length this.running = this._running.map(s=>s.name).join(',') this.startScheduleAction(run) } } queueSchedule(sch){ this._queue.push(sch) this.queueCount=this._queue.length this.queue = this._queue.map(s=>s.name).join(',') } // async abortRunning(id) { // console.log('aborting running schedule(s)', id||'') // const running = id ? (this.running.filter(sch=>sch.id===id) ||[]) : this.running // for (let sch of running) { // if (sch.stopAction) await sch.stopAction() // else console.log(sch.name,'no stop action registered can not force active schedule to stop') // } // } // removeFromQueue(id) { // console.log('removing queued schedule(s)', id||'') // const curr = this.queue.length // if (id) this.queue = this.queue.filter(s=>s.id!==id) // else this.queue = [] // this.activeCount-= curr-this.queue.length // } // // async removeFromDelayed(id) { // console.log('removing delayed schedule(s)', id||'') // if (id) this._delayed = this._delayed.filter(s=>s.id!==id) // else this._delayed = [] // return // } // // async removeActive(id) { // await this.removeFromDelayed(id) // await this.abortRunning(id) // this.removeFromQueue(id) // console.log('clearing list activated schedule(s)',id||'') // if (id) { // for (let toid in this._queue) { // this._queue[toid] = this._queue[toid].filter(s=>s.id!==id) // } // } else this._queue = {} // } // trigger (count,toid) { // trigger all schedules actions that have same timestamp as first in queue // if (count < 1 || count > this._queue[toid].length) { // // console.log('done: returning') // return // } // // console.log(count,toid,this._queue[toid]) // const idx = this._queue[toid].length-count // const sch = this._queue[toid][idx] // count-- // console.log('adding to queue',sch.id) // this.activeCount++ // this.queue = this.queue.concat([sch]) // for rx must overwrite not push // if (sch.simultaneous) setTimeout(() => this.startScheduleAction(sch),0) // else if(!this.runningCount) setTimeout(() => this.startScheduleAction(sch),0) // // if (idx===0 && !this.runningCount) this.startScheduleAction(sch) // else { // console.log('adding schedule to delay',sch.name,sch.toid) // this._delayed.push(sch) // } // // this.trigger(count,toid) // } addSchedule (sch) { // console.log('####################',sch.id,sch.enabled,'adding###########') this.schedules.push(sch) this.enableSchedule(sch.id,sch.enabled) if (getBaseClass(sch) === 'UCISchedule') { sch.rxSubscribe('settings.enabled','runner',function (sch) { // console.log('runner: schedule enabled change',value) this.enableSchedule(sch) }.bind(this,sch)) // console.log(sch.id,'enabled subscriptions',sch.$get('_rx_.props.settings.enabled.subs')) sch.rxSubscribe('nextTS','runner', function () { // console.log('current runner timestamp', this.nextTS) if (!update_all && this.state) { // console.log('RUNNER:',sch.id,'has updated its time stamp', sch.nextTS, sch.nextDT) this.sort() } // console.log('updated runner schedules',this.nextTS,this.nextDT,this.nextName) }.bind(this) ) // sch.update(true) } else { console.log('ERROR: passed schedule was not a instance of UCISchedule') console.log(sch.name) } } // schedule id removeSchedule (id) { this.schedules = this.schedules.filter(a => a.id !== id) this._disabledSchedules = this._disabledSchedules.filter(a => a.id !== id) } // enables dissables the schdule in the runner enableSchedule(sch) { if (typeof sch ==='string') sch = this.getSchedule(sch) if (!(sch || {}).id) return // console.log(sch.id,'<<<<---------------enable/disable schedue--------->>>>',sch.enabled) // console.log('enabled schedules', this.schedules.map(sch=>sch.id)) // console.log('disabled schedules', this._disabledSchedules.map(sch=>sch.id)) // if (state == null) state = true if (sch.enabled) { if (this._disabledSchedules.find(a => a.id === sch.id)) { this.schedules.push(sch) // remove from disabled list this._disabledSchedules = this._disabledSchedules.filter(a => a.id !== sch.id) } } else { if(this.schedules.find(a => a.id === sch.id)) { this._disabledSchedules.push(sch) this.schedules = this.schedules.filter(a => a.id !== sch.id) } } // if (this._running) this.sort() this.sort() // console.log('after') // console.log('enabled schedules', this.schedules.map(sch=>sch.id)) // console.log('disabled schedules', this._disabledSchedules.map(sch=>sch.id)) } getSchedule (id) { if (!id) return this.schedules.concat(this._disabledSchedules) return this._getEnabledSchedule(id) || this._getDisabledSchedule(id) } get nextSchedule () { return this.schedules[0] } // force an update updateSchedule (id) { this.getSchedule(id).update() } _getEnabledSchedule (id) { if (!id) return this.schedules return this.schedules.find(a => a.id === id) } _getDisabledSchedule (id) { if (!id) return this._disabledSchedules return this._disabledSchedules.find(a => a.id === id) } async startScheduleAction (sch) { // toid = sch.toid || toid console.log('---- in RUNNER: scheduled run started----',sch.runID) // TODO check if async or promise sch.startAction(sch.runID) sch.once(sch.runID,removeScheduleFromActiveList.bind(this)) // emitted on abort too function removeScheduleFromActiveList() { console.log('-----scheduled run is complete or aborted removing run list ----') this._running = this._running.filter(s=>s.runID!==sch.runID) this.running = this._running.map(s=>s.name).join(',') this.runningCount = this._running.length if (!this._queue.length) { console.log('========== the queue is empty nothing left to run =========') } // console.log('schedule action complete', sch.name,toid, this.runningCount) } } // this will force update all schedules enabled or disabled async update () { update_all = true // console.log('updating all',this.schedules.length, update_all) for(let sch of this._getEnabledSchedule()) { await sch.update(true) // console.log('done updating', sch.id) } console.log('updated all schedules') update_all = false await this.sort() } async sort (force) { const oldFirst = (this.nextSchedule || {}).id const curTS = this.nextTS || 0 // console.log('current first',oldFirst) if (this.nextSchedule == null) return // console.log('Before Sorting>>>>>>>>>>>', this.schedules.map(sch => { return { name: sch.name, ts: sch.nextTS }}) ) this.schedules.sort((a,b) => a.nextTS - b.nextTS) // console.log('sorted', this.schedules.map(sch => { return { name: sch.name, ts: sch.nextTS }}) ) const first = this.nextSchedule || {} // console.log('apply sort',first.id,oldFirst,curTS,first.nextTS) if (first && first.nextTS) { if (oldFirst.id !== first.id || first.nextTS > curTS || force) { // console.log('!!!!!!!!!!sort resulted a change of nextTS', first.nextTS, first.name) this.nextName = first.name this.nextDT = first.nextDT this.nextTS = first.nextTS } } } } // end class export default UCIScheduleRunner export { UCIScheduleRunner as Runner } function getBaseClass(targetClass){ if(targetClass instanceof Object){ let baseClass = targetClass while (baseClass){ const newBaseClass = Object.getPrototypeOf(baseClass) if(newBaseClass && newBaseClass !== Object && newBaseClass.name){ baseClass = newBaseClass }else{ break } } return baseClass.constructor.name } }