UNPKG

queue-conductor

Version:

Queue manager with condition-based listening and task subscription

685 lines (602 loc) 25.7 kB
/* Copyright Conley Johnson 2018 License:MIT */ if (!window) window = {} //<-- for node import { ObjectAnimator } from 'deep-animator' var clearResults = (p = {}) => { if (p.constructor === Array) { p = { l: p } } var { l = [] } = p l = arrayWrap(l) var len = l.length for (var index = 0; index < len; index++) { if (l[index].result !== undefined) { delete l[index].result } l[index].resolved = false l[index].initiated = false l[index].evaluation = null } return l } var arrayWrap = function (thing) { if (Object.prototype.toString.call(thing) !== '[object Array]') { return [thing] } else { return thing } } var format = (p) => { p = p ? p : () => { } return clearResults( arrayWrap(p).map((val) => { if (val.constructor === Promise) { val = Queue.promise(val) } if (typeof val === 'object' && val.task && !val.preCondition && !val.postCondition && val.wait !== false) { val.wait = true }//if a task is submitted in an object, without conditions and without wait, set wait to true. Otherwise, a function can be submitted raw if (val.constructor === Queue) { val = Queue.queue(val); } if (typeof val === 'function') { val = { task: val } } var { task = () => { }, preCondition = () => true, postCondition = () => true, wait = false, name = undefined, comment = '', sec = 3600, timeout = () => { }, earlyTermination = () => { }, getValue = false, getValueFromTask = false, queue = false } = val return { task, preCondition, postCondition, wait, name, comment: task.toString(), sec, timeout, earlyTermination, getValue, getValueFromTask, initiated: false, resolved: false, queue, subscriptions: new Map(), evaluation: null } }) ) } //formfunctions function formToObject(form) { var obj = {}; var elemarray if (typeof form === 'string' || typeof form === 'number') { elemArray = document.getElementById(form).elements; } else { elemarray = form.elements } var elemlen = elemarray.length; var checknum//for trying to convert strings to numbers for (var i = 0; i < elemlen; i++) { var n = elemarray[i].name.replace(/\[\]/g, ''); if (n == '') { n = elemarray[i].id } obj[n] = elemarray[i].value; checknum = Number(obj[n]); if (!isNaN(checknum)) { obj[n] = checknum } if (elemarray[i].type == 'checkbox') { obj[n] = elemarray[i].checked; } if (elemarray[i].type == 'select-multiple') { obj[n] = getMultipleSelectValues(elemarray[i]); } } return obj; } function getMultipleSelectValues(select) { var result = []; var options = select && select.options; var opt; for (var i = 0, iLen = options.length; i < iLen; i++) { opt = options[i]; if (opt.selected) { result.push(opt.value || opt.text); } } result = result.map(cSTFV) return result; } function cSTFV(val) {//convert strings to functional values if (val === 'true') { return true } if (val === 'false') { return false } var v = Number(val); if (!isNaN(val)) { return v } else { return val } } //end form functions function Queue() { var queueLine = [];//where the tasks and conditions are stored var queueMap = new Map()//taskobj->name,index //queueMap is for quick searches var queueIndex = 0;//iteratorguide for the queue var queueLength = 0;//number of tasks queued up var queueHandle = false;//the handle for the timeouts var timeoutHandle = false//each var checkSpeed = 175;//interval for conditional checks var checksRunning = false, waitRunning = false; var terminated = false//stopped callback has been called, and it is locked, unless explicitly opened by calling a modifier (like add, or splice) or calling start({unterminate:true}) var callBack = () => { } var initialValue = undefined var subscriptions = new Map() var repeat = false this.running = (p = {}) => { if (p.detail) { return { checksRunning, waitRunning } }; return checksRunning || waitRunning } this.setCheckSpeed = (speed) => { checkSpeed = speed; return this } this.finally = (cb) => { callBack = cb; if (!more() && queueLength > 0 && queueLine[queueIndex].resolved) { cb(this.status(true)) }; return this } this.initVal = (v) => { initialValue = v; return this } this.status = function (control = false) { return { currentTask: queueLine[queueIndex], queueLength, queueIndex, waitRunning, checksRunning, queueLine: queueLine.slice(), control: control ? this : null } } this.currentVal = function () { return queueLength > 0 && queueLine[queueIndex] ? queueLine[queueIndex].result : initialValue } this.lastVal = function () { return queueLength > 0 && queueLine[queueLength - 1] ? queueLine[queueLength - 1].result : initialValue } this.add = function (p = {}, insert) { //takes single value or an array, and each task(exposed or wrapped in an array) is either a function or a task object var actions = this.format(p) if (!insert) { queueLine.push.apply(queueLine, actions) } else { queueLine.splice(queueIndex + 1, 0, ...actions); } bookKeep() return this } this.insert = function (p = {}) { //takes single value or an array, and each task(exposed or wrapped in an array) is either a function or a task object return this.add(p, true) } this.repeat = (p = {}) => { repeat = true } this.clear = function () { this.stop({ clear: true }); queueIndex = 0; queueLine.length = 0; bookKeep(); return this } this.stop = function (p = {}) { //wait bool, clear bool, terminate bool clearTimeout(queueHandle); checksRunning = false; waitRunning = false; if (p.wait && !p.clear) { waitRunning = true; } else { clearTimeout(timeoutHandle) } if (p.terminate) { terminated = true; clearTimeout(timeoutHandle); callBack(this.status(true)) this.publish() } return this } this.kickStart = (p = {}) => { if (this.running()) { return } this.start() return this } this.start = (p = {}) => { if (terminated && !p.unterminate) { } else { terminated = false } if (p.indexMatch) { if (p.indexMatch !== queueIndex) { return this } } if (p.initialValue !== undefined) { initialValue = p.initialValue } if (p.callBack) { this.finally(p.callBack) } clearTimeout(queueHandle); clearTimeout(timeoutHandle); listen(); return this } var listen = () => { checksRunning = true; waitRunning = true var q = queueLine[queueIndex]; if (!q.initiated) { if (q.preCondition()) { var result = q.task(getControlPackage()) if (result instanceof Promise) { this.insert(result) } if (!q.wait) { q.result = result } q.initiated = true } if (q.wait) { this.stop({ wait: true }); return; } } if (q.initiated) { if (q.postCondition()) { if (q.getValue) { q.result = q.getValue(getControlPackage({ includeDone: false })) }; q.resolved = true; this.moveOn(); return } } queueHandle = setTimeout(() => { listen(); }, checkSpeed); } var getControlPackage = (p = {}) => { var { includeDone = true } = p var ret = { control: this, done: function (indexMatch, result) { if (queueLine[indexMatch] === undefined) { return } queueLine[indexMatch].result = result; if (!waitRunning) { return } setTimeout(() => { this.start({ indexMatch }) }, 0) }.bind(this, queueIndex), result: getPreviousResult(), evaluate: function (task, val) { task.evaluation = val }.bind(this, queueLine[queueIndex]), } if (!includeDone) { delete ret.done } return ret } var finished = () => { return queueIndex >= queueLength - 1 && terminated } var more = () => { return queueLine[queueIndex + 1] } this.moveOn = () => {//move to the next step if it exists. if not, terminate this.publishTask({ task: queueLine[queueIndex] }) if (!more() && !repeat) { this.stop({ terminate: true }); return false } if (!repeat) { queueIndex++ } else { clearResults([queueLine[queueIndex]]) } repeat = false this.start() startTimeout() return true } var startTimeout = () => { clearTimeout(timeoutHandle) timeoutHandle = setTimeout(() => { queueLine[queueIndex].timeout(getControlPackage({ includeDone: false })) this.moveOn() }, queueLine[queueIndex].sec * 1000) } var getPreviousResult = () => { if (queueLine[queueIndex].getValueFromTask) { var look = queueLine[queueIndex].getValueFromTask if (typeof look === 'string') { for (let m = 0; m < queueLength; m++) { if (queueLine[m].name === look) { return queueLine[m].result } } } else if (typeof look === 'number' && Math.abs(look) >= m) { return queueLine[queueIndex - Math.abs(look)].result } } return queueIndex > 0 ? queueLine[queueIndex - 1].result : initialValue } this.change = (p = {}) => { queueLine.splice(queueIndex + 1); return this.add(p) } this.interrupt = (p = {}) => { var { and } = p if (queueLine[queueIndex]) { queueLine[queueIndex].earlyTermination(getControlPackage({ includeDone: false })) } if (and === 'stop') { this.stop() } else { this.moveOn() } return this } this.splice = (p = {}) => { var { replacement = [] } = p var { from, to } = getIndexes(p)//{action, index} if (from.index < queueIndex + 1) { console.log('from index' + from.index + ' includes past/current tasks in the queue. setting from index to very next task index.') } from.index = queueIndex + 1 if ((from.index > to.index)) { console.error('indexes off->queue.js->splice()', { from, to }); } else { var remove = to.index - from.index + 1//plus 1 is to make it inclusive of the last element queueLine.splice.apply(queueLine, [from.index, remove].concat(this.format(replacement))) bookKeep() } return this } this.delete = function (p = {}) { var found = this.find(p) if (found.index < queueIndex + 1) { console.error('task is past or current. Cannot be deleted'); return this } if (found) { queueLine.splice(found.index, 1); bookKeep() } return this } this.pop = () => { if (queueIndex === queueLength - 1) { console.error('tried to pop last task, which has already been initiated queue.js->pop'); return this } queueLine.pop(); bookKeep(); return this } this.slice = function (p = {}) { //returns a new array with new shallow action object clones. setting their properties will not alter the action objects they were cloned from //however,performing methods on the sliced action objects will operate on the originals //and in fact some of the functions submitted will operate on important closure variables var { from, to } = p.from || p.to ? getIndexes(p) : { from: { index: 0 } }//if none submitted, splice the whole queueline if ((to && from.index > to.index) || !from) { console.error('indexes off->queue.js->get()', { from, to }); return [] } else { var args = to ? [from.index, to.index] : [from.index] if (args.indexOf(undefined) !== -1) { console.error(new Error('unable to find criteria'), from, to); return [] } var ret = clearResults(queueLine.slice.apply(queueLine, args).map((val) => { return Object.assign({}, val) })) ret.forEach((val) => { var newMap = new Map() val.subscriptions.forEach((v, k) => { newMap.set(k, v) }) val.subscriptions = newMap }) return ret } } var getIndexes = (p = {}) => { return { from: this.find(p.from), to: this.find(p.to) } } this.find = function (p = {}) { return queueMap.get(p.name || p.index || p.taskObj || p.task) } var bookKeep = () => {//stores keys by name, index,task[the function to execute], action[the action package submitted] //tears map down and rebuilds because queueLine indexes shift queueMap.clear(); queueLength = queueLine.length for (var index = 0; index < queueLength; index++) { var action = queueLine[index] var pack = { action, index }, task = action.task, name = action.name if (this.find({ name })) { console.error('duplicate names queue.js bookKeep', name, index) } if (this.find({ taskObj: pack })) { console.error('duplicate taskObjects. second dup. will be skipped. queue.js bookKeep', name, index) } if (this.find({ task })) { console.error('duplicate tasks, only the last inserted can be returned through find queue.js bookKeep', name, index) } if (name !== undefined) { queueMap.set(name, pack) } queueMap.set(index, pack) queueMap.set(action, pack) queueMap.set(task, pack) } if (!this.allDone()) { terminated = false } } this.clearResults = () => { clearResults(queueLine); return this } this.reset = () => { queueIndex = 0; this.clearResults(); return this } this.format = (p) => { return format(p) } this.allDone = () => { return queueLine.every((val) => { val.resolved }) } this.subscribe = (p = {}) => { var { cb } = p if (typeof p === 'function') { cb = p } if (!cb) { console.error('queue.js subscribe(func||{cb:func})->no function') } subscriptions.set(cb, 1) return this } this.unsubscribe = (p) => { var { cb } = p if (typeof p === 'function') { cb = p } subscriptions.delete(cb) return this } this.publish = function () {//publishes that the entire queue is finished subscriptions.forEach((v, k) => { k(this.status()) }) subscriptions.clear() } this.subscribeTask = (p) => { var { name, index, cb } = p if (!cb) { console.error('queue.js subscribeTask({name||index,cb})->no function to attach:', p); return this } var task = this.find(p) if (!task) { console.error('queue.js subscribeTask()->unable to find task:', p); return this } task = task.action if (task.resolved) { cb(task); return this } task.subscriptions.set(cb, 1) return this } this.unsubscribeTask = (p) => { var { name, index, cb, check = false } = p if (!cb) { console.error('queue.js subscribeTask({name||index,cb})->no function to unattach:', p); return this } var task = this.find(p) if (!task) { console.error('queue.js subscribeTask()->unable to find task:', p); return this } task = task.action if (check) { console.log('task', task, 'has callback' + cb.toString(), task.subscriptions.has(cb)) } task.subscriptions.delete(cb, 1) return this } this.publishTask = function (p = {}) { var { task } = p; if (!task) { return this } var subscriptions = task.subscriptions subscriptions.forEach((v, k) => { k(task) }) subscriptions.clear() return this } this.all = (p) => { this.add(Queue.all(p)); return this } this.insertAll = (p) => { this.insert(Queue.all(p)); return this } this.race = (p) => { this.add(Queue.race(p)); return this } this.insertRace = (p) => { this.insert(Queue.race(p)); return this } this.wait = (p) => { this.add(Queue.wait(p)); return this } this.insertWait = (p) => { this.insert(Queue.wait(p)); return this } this.animate = (p) => { this.add(Queue.animate(p)); return this } this.insertAnimate = (p) => { this.insert(Queue.animate(p)); return this } this.transition = (p) => { this.add(Queue.transition(p)); return this } this.insertTransition = (p) => { this.insert(Queue.transition(p)); return this } this.blink = (p) => { this.add(Queue.blink(p)); return this } this.insertBlink = (p) => { this.insert(Queue.blink(p)); return this } this.listen = (p) => { this.add(Queue.listen(p)); return this } this.insertListen = (p) => { this.insert(Queue.listen(p)); return this } this.listenTask = (p) => { this.add(Queue.listenTask(p)); return this } this.insertListenTask = (p) => { this.insert(Queue.listenTask(p)); return this } this.ajax = (p) => { this.add(Queue.ajax(p)); return this } this.insertAjax = (p) => { this.insert(Queue.ajax(p)); return this } this.fetch = (p) => { this.add(Queue.fetch(p)); return this } this.insertFetch = (p) => { this.insert(Queue.fetch(p)); return this } this.loadIFrame = (p) => { this.add(Queue.loadIFrame(p)); return this } this.insertLoadIFrame = (p) => { this.insert(Queue.loadIFrame(p)); return this } } Queue.wait = (time) => {//time in ms if (typeof time === 'object' && time.from && time.to) { var time = Math.random() * (time.to - time.from) } var task = (p) => { setTimeout(() => { p.done(p.result); }, time) } return { task } } Queue.queue = function (p = {}) {//starts another Queue instance and hooks up the call back var { queue } = p if (p.constructor === Queue) { queue = p } var task = function (p) { queue.clearResults().reset().finally(p.done).kickStart(); } return Object.assign({ task, wait: true, queue }, p) } Queue.promiseWrap = function (p = {}) {//creates a promise, hooks this queue up to that promise (using finally) and returns the promise var { toDo } = p if (!toDo || !toDo.constructor) { console.log('wrong argument in promiseWrap queue.js argument submitted:', p); return } var resolve = function (res) { if (toDo.constructor === Queue) { this.finally(res) } } return new Promise(resolve) } Queue.promise = function (p = {}) {//wraps a queue in a task which does then->p.done var { promise } = p if (p instanceof Promise) { promise = p } var task = function (par) {//executed when reached in the queue. promise.then((result) => { par.done(result) }, (result) => { par.done(result) }) } return Object.assign({ task, promise, wait: true }, p) } //takes (query,data) , ({query,data}) , ({taskParams, fetchPackage:{query,data}}) Queue.fetch = function (pack = {}, data = {}) { if (!pack.fetchPackage) { pack = { fetchPackage: { query: typeof pack === 'string' ? pack : pack.query, data: pack.data ? pack.data : data } } } if (!pack.fetchPackage.query) { console.error('invalid fetchPackage subimtted to fetch in Queue', pack) }//use the package if it doesn't contain designated package for the fetch function pack.task = (p) => { var prom = fetch(pack.fetchPackage.query, pack.fetchPackage.data) return prom instanceof Promise ? prom : new Promise(function (resolve) { setTimeout(() => { resolve() }, 0) }) } pack.wait = false return pack } Queue.ajax = function (p = {}) { var { url = '', data = {}, synch = true, method = 'POST' } = p var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); var task = function (par = {}) { var params if (data.nodeName && data.nodeName.toLowerCase() === 'form') { if (data.action) { url = data.action }; params = formToObject(data) } params = typeof data == 'string' ? data : Object.keys(data).map( function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]) } ).join('&'); xhr.addEventListener('readystatechange', () => { console.log(xhr.status) }) xhr.addEventListener('load', function () { par.done({ response: xhr.responseText, status: xhr.status }) }) xhr.open(method, url, synch); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.send(params); } return Object.assign({ task, wait: true, xhr }, p) } Queue.loadIFrame = function (p = {}) { var { url, IFrame, src } = p if (!url) { url = src } var task = (par = {}) => { IFrame.addEventListener('load', function () { par.done(IFrame) }) IFrame.src = url } return Object.assign({ task }, p) } Queue.listen = function (p = {}) { var { queue, start = false } = p if (p.constructor === Queue) { queue = p } var task = (p) => { if (queue.allDone()) { p.done(); return } queue.subscribe({ cb: p.done }) if (start) { queue.kickStart() } } return Object.assign({ task, wait: true }, p) } Queue.listenTask = function (p = {}) { var { queue = new Queue(), name = 'null12345', index, start = false } = p var task = (p) => { var t = queue.find({ name, index }) if (!t) { console.error('queue.js Queue.listenTask({queue,name,index,start}) task not found for listening') } if (t && t.resolved) { p.done(t.result); return } queue.subscribeTask({ name, index, cb: p.done }) if (start) { queue.kickStart() } } return Object.assign({ task, wait: true }, p) } Queue.race = function (p) { return Queue.all(p, true) } Queue.all = function (p = {}, race) { var stalls = [] var queue, useP = p.constructor === Array var actions = (useP ? p : p.actions).map((val) => { return format(val) }) if (actions.length === 0) { return {} } var doneHolder = { done: () => { } } var check = () => { return stalls.every(val => val.resolved) } var retrieve = function (i, result) { stalls[i].result = result stalls[i].resolved = true if (race) { doneHolder.done(result); check = () => { return false } } if (check()) { doneHolder.done(stalls.map(val => val.result)) } } actions.forEach(function (val, i) { queue = new Queue().add(val).finally(retrieve.bind(null, i)) stalls[i] = { queue, result: undefined, resolved: false } }) var earlyTermination = () => { stalls.forEach((val) => { if (val.queue) { val.queue.interrupt({ and: 'stop' }) } }) } var task = (p) => { doneHolder.done = p.done stalls.forEach((val) => { val.queue.start() }) } return Object.assign({ task, earlyTermination }, useP ? {} : p) } Queue.transition = function (par = {}) { var { node, style, duration = 1, timing = 'ease-in-out', synch = true, contours = {} } = par var task = (p) => { //hook up the function var evFunc = function () { node.removeEventListener('transitionend', evFunc) p.done() } node.addEventListener('transitionend', evFunc) //set the transition if (synch && style.transform) { var { dest } = Queue.synchTransform({ orig: node.style.transform, dest: style.transform }) style.transform = dest } var transProps = [] for (let k in style) { transProps.push(k + ' ' + duration + 's ' + timing) } Object.assign(node.style, { transition: transProps.join(', ') }, style) //set the style } return Object.assign({ task, wait: true }, par) } Queue.animate = function (par = {}) { if (!ObjectAnimator) { console.error('You need the animator dependency to use this method.'); return () => { } } var animation = new ObjectAnimator()//if the conductor is on the window, it will be used. otherwise, either the animator will run on it's own, or a dev will need to store a conductor somewhere then use " var animation=new ObjectAnimator({conductor})" var task = (p) => { par.postAnim = () => { p.done() } animation.loadAnimation(par).animate() } return Object.assign({ task, wait: true, earlyTermination: () => { animation.stop() }, animation }, par) } Queue.blink = function (p = {}) { var { node, repeat = 1, interval = 500, proportion = .5 } = p var tasks = [] for (let i = 0; i < repeat; i++) { tasks.push( () => { node.style.opacity = 0 }, Queue.wait(interval * (1 - proportion)), () => { node.style.opacity = 1 }, Queue.wait(interval * proportion) ) } tasks.pop() return tasks } Queue.synchTransform = function (p = {}) {//orig and dest should be strings var { orig, dest } = p, origArray = [], destArray = [], origU = new Map(), destU = new Map(), origVal var breakOrig = orig.match(/[a-zA-Z]+\([^\)]*\)/g), breakDest = dest.match(/[a-zA-Z]+\([^\)]*\)/g) if (breakOrig) { breakOrig.forEach((val, i) => { val = val.replace(/\s/g, ''); origU.set(val.split('(')[0], val) }) } else { breakOrig = [] } if (breakDest) { breakDest.forEach((val, i) => { val = val.replace(/\s/g, ''); destU.set(val.split('(')[0], val) }) } else { breakDest = [] } origU.forEach((v, k) => { destArray.push(destU.has(k) ? destU.get(k) : origU.get(k)) origArray.push(origU.get(k)) destU.delete(k) }) destU.forEach((v, k) => { destArray.push(destU.get(k)) origArray.push(origU.has(k) ? origU.get(k) : transformDefaultTable[k]) }) return { orig: origArray.join(' '), dest: destArray.join(' ') } } var transformDefaultTable = { translate: 'translate(0,0)', translate3d: 'translate3d(0,0,0)', translateX: 'translateX(0)', translateY: 'translateY(0)', translateZ: 'translateZ(0)', scale: 'scale(1,1)', scale3d: 'scale3d(1,1,1)', scaleX: 'scaleX(1)', scaleY: 'scaleY(1)', scaleZ: 'scaleZ(1)', rotate: 'rotate(0deg)', rotate3d: 'rotate3d(0,0,0,0deg)', rotateX: 'rotateX(0deg)', rotateY: 'rotateY(0deg)', rotateZ: 'rotateZ(0deg)', skew: 'skew(0deg,0deg)', skewX: 'skewX(0deg)', skewY: 'skewY(0deg)', } export { Queue }