@micosmo/ticker
Version:
System for managing processes that are dependent on timer tick events
508 lines (466 loc) • 14.7 kB
JavaScript
/*
* ticker.js
* Provide services to describe and manage ticker based processes.
* The basic ticker process element is based on a generator function.
*/
var { declareMethod, method } = require('../core/method');
// var { declareMethod, method } = require('@micosmo/core/method');
const ProtGenFn = Object.getPrototypeOf(function * () { });
const TickerPrototype = _TickerPrototype();
const DefaultTicker = Ticker('DefaultTicker');
var idTickerProcess = 1;
module.exports = {
startProcess,
createProcess,
Ticker,
DefaultTicker,
looper,
iterator,
timer,
sTimer,
msTimer,
waiter,
sWaiter,
msWaiter,
beater,
sBeater,
msBeater,
};
// Ticker handler
function Ticker(name) {
if (typeof name !== 'string')
throw new Error(`micosmo:Ticker: Name is required`)
return Object.setPrototypeOf({
isaTicker: true,
isStarted: true,
name,
processes: [],
tick: fNoop,
}, TickerPrototype);
};
function _TickerPrototype() {
return {
assignAsDefaultTo(...processes) {
if (processes.length === 1 && Array.isArray(processes[0]))
processes = processes[0];
processes.forEach(process => {
if (typeof process !== 'object' || !process.isaProcess)
throw new Error(`micosmo:ticker:assignAsDefaultTo: Invalid process parameter`);
if (process.isStarted) {
console.warn(`micosmo:ticker:assignAsDefaultTo: Process '${process.name}' started. Cannot assign ticker`);
return this;
}
process.ticker = this;
})
return this;
},
start() {
if (this.isStarted)
return false;
this.tick = this.processes.length > 0 ? method(tickTicker) : fNoop;
return (this.isStarted = true);
},
stop() {
const rc = this.pause();
if (this.processes.length > 0)
this.processes = [];
return rc;
},
pause() {
if (!this.isStarted)
return false;
this.tick = fNoop;
this.isStarted = false;
return true;
},
isRunning() {
return this.isStarted;
},
isPaused() {
return !this.isStarted;
}
};
}
var tickTicker = declareMethod(function (tm, dt) {
const processes = this.processes;
var nProcesses = processes.length; // Don't want to process anything that is attached in this tick cycle
for (let i = 0; i < nProcesses;) {
const process = processes[i];
if (!process || (!process._isPaused && process.tick(tm, dt) === 'stopped')) {
processes.splice(i, 1);
nProcesses--;
if (processes.length <= 0)
this.tick = fNoop;
} else
i++;
}
});
function attachTicker(ticker, process) {
if (ticker.tick === fNoop)
ticker.tick = method(tickTicker);
ticker.processes.push(process);
}
function detachTicker(ticker, process) {
const processes = ticker.processes;
const iProcess = processes.findIndex(proc => proc === process);
if (iProcess < 0)
throw new Error(`micosmo:ticker: Process '${process.name}' is not attached to ticker '${ticker.name}'`)
processes[iProcess] = undefined; // Let tick cycle remove the entry
}
function fNoop () { }
// Function startProcess / createProcess
// Accepts 1 or 2 arguments as follows:
// 1. onTick function or configuration object
// 2. onTick function, ticker | el | selector (cascading)
//
// Configuration object: {
// name: Name of the process. String
// onTick: function or generator function (tm, dt [, rem])
// onEnd: Function dispatched at end of process. Always dispatched. Parms(flTimeout)
// msTimeout: Timeout interval in milliseconds.
// sTimeout: Timeout interval in seconds
// micosmo:ticker: ticker
// }
function startProcess(...args) {
return new _Process(validateArgs('startProcess', ...args)).start();
}
function createProcess(...args) {
return new _Process(validateArgs('createProcess', ...args));
};
function validateArgs(fn, ...args) {
const cfg = {};
const msg = `micosmo:ticker:${fn}: Invalid parameter`;
var flCfgPassed = false;
switch (args.length) {
case 0:
throw new Error(`micosmo:ticker:${fn}: onTick or configuration parameter required`);
case 1:
mapArg(cfg, args, 0, msg,
[['function', 'onTick'], ['object', o => { flCfgPassed = true; Object.assign(cfg, o) }]]);
break;
case 2:
mapArg(cfg, args, 0, msg, [['function', 'onTick']]);
mapArg(cfg, args, 1, msg, [['object', 'ticker']]);
break;
default:
throw new Error(`micosmo:ticker:${fn}: Too many parameters`);
}
return validateConfig(fn, cfg, flCfgPassed);
}
function validateConfig(fn, cfg, flCfgPassed) {
// name: Name of the process. String
const id = idTickerProcess++;
if (!cfg.name)
cfg.name = `<TickerProcess:${id}>`
else if (typeof cfg.name !== 'string')
throw new Error(`micosmo:ticker:${fn}: Invalid 'name' (${cfg.name})`);
// onTick: function or generator function
if (!cfg.onTick)
throw new Error(`micosmo:ticker:${fn}: 'onTick' function required`);
else if (typeof cfg.onTick !== 'function')
throw new Error(`micosmo:ticker:${fn}: Invalid 'onTick' (${typeof cfg.onTick})`);
// micosmo:ticker: ticker
if (!cfg.ticker)
cfg.ticker = DefaultTicker;
else if (!cfg.ticker.isaTicker)
throw new Error(`micosmo:ticker:${fn}: 'ticker' is not a Ticker`);
if (flCfgPassed) {
// onEnd: Function dispatched at end of process. Always dispatched. Parms(process, flTimeout)
if (cfg.onEnd) {
if (typeof cfg.onEnd !== 'function')
throw new Error(`micosmo:ticker:${fn}: Invalid 'onEnd' (${typeof cfg.onEnd})`);
this.onEnd = cfg.onEnd;
}
// msTimeout: Timeout interval in milliseconds.
// sTimeout: Timeout interval in seconds
if (cfg.sTimeout && cfg.msTimeout)
throw new Error(`micosmo:ticker:${fn}: Either 'sTimeout' or 'msTimeout'`);
if (!cfg.msTimeout) {
if (cfg.sTimeout) {
if (typeof cfg.sTimeout !== 'number' || cfg.sTimeout <= 0)
throw new Error(`micosmo:ticker:${fn}: Invalid 'sTimeout' (${typeof cfg.sTimeout})`);
cfg.msTimeout = cfg.sTimeout * 1000;
delete cfg.sTimeout;
}
} else if (typeof cfg.msTimeout !== 'number' || cfg.msTimeout <= 0)
throw new Error(`micosmo:ticker:${fn}: Invalid 'msTimeout' (${typeof cfg.msTimeout})`);
}
return cfg
}
function mapArg(cfg, args, iArg, msg, argMap) {
const arg = args[iArg];
const ty = typeof arg;
for (let i = 0; i < argMap.length; i++) {
const e = argMap[i];
if ((e[0] === 'array' && Array.isArray(arg)) || ty === e[0]) {
if (typeof e[1] === 'function')
e[1](arg);
else
cfg[e[1]] = arg;
return i;
}
}
throw new Error(`${iArg + 1}. ${msg}`);
}
function _Process(cfg) {
Object.assign(this, cfg);
this.defaultTicker = this.ticker;
this.onTick = getGeneratorFunction(this.onTick);
this.isaProcess = true;
this.gTickStack = [];
this.state = {
name: this.name,
tm: 0,
dt: 0,
data: 0
}
return this;
};
var tickProcess = declareMethod(_tickProcess);
_Process.prototype = {
start(ticker) {
if (this.isStarted)
throw new Error(`micosmo:ticker:process:start: Process '${this.name}' already started`);
if (ticker) {
if (typeof ticker !== 'object' || !ticker.isaTicker)
throw new Error(`micosmo:ticker:process:start: Invalid ticker parameter`);
this.ticker = ticker;
} else
this.ticker = this.defaultTicker;
this.isStarted = true;
this.tick = method(tickProcess);
if (this.msTimeout)
this.tick = getTimeoutFn(method(tickProcess).bind(this), this.msTimeout);
this.gTick = this.onTick(this.state); // Create an instance of our generator.
attachTicker(this.ticker, this);
return this;
},
stop() {
if (!this.isStarted)
return this;
detachTicker(this.ticker, this);
stopProcess(this, 'stop');
return this;
},
isAttached() {
return this.isStarted;
},
isPaused() {
return this._isPaused;
},
pause() {
this._isPaused = true;
return this;
},
resume() {
this._isPaused = false;
return this;
},
};
function _tickProcess(tm, dt) {
// Manage the generators for this process.
// Note that we preserve the state.data across calls.
// Provides support for iterator like generators
const state = this.state;
state.tm = tm;
state.dt = dt;
do {
const resp = this.gTick.next();
const value = resp.value;
if (resp.done) {
// From return statement
if (!value || value === 'done') {
if (this.gTickStack.length > 0) {
this.gTick = this.gTickStack.pop();
return;
}
return stopProcess(this, 'done');
}
if (typeof value === 'function') {
// Chain to this generator
this.gTick = getGeneratorFunction(value)(state);
return;
}
if (value !== 'stop') {
if (!this._haveIssuedBadReturnCodeWarning)
console.warn(`micosmo:ticker:process:tick: Process '${this.name}' received an invalid return code '${value}'. Assumed 'done'.`);
this._haveIssuedBadReturnCodeWarning = true;
if (this.gTickStack.length > 0) {
this.gTick = this.gTickStack.pop();
return;
}
return stopProcess(this, 'done');
}
return stopProcess(this, 'stop');
}
// From yield statement
if (!value)
return;
if (typeof value === 'function') {
// Push current generator and replace with response function
// and call in the same tick cycle
this.gTickStack.push(this.gTick);
this.gTick = getGeneratorFunction(value)(state);
continue;
}
return value === 'stop' ? stopProcess(this, 'stop') : undefined;
} while (true);
}
function stopProcess(process, rsn) {
// May have already explicitly stopped the process
if (!process.isStarted)
return 'stopped';
process.isStarted = false;
process.tick = undefined;
process.gTick = undefined;
process.gTickStack = [];
process.ticker = undefined;
if (process.onEnd)
process.onEnd(rsn, process);
return 'stopped';
}
function getTimeoutFn(onTick, msTimeout) {
return function(tm, dt) {
const rc = onTick(tm, dt);
if (rc === 'stopped')
return 'stopped';
if ((msTimeout -= dt) <= 0)
return stopProcess(process, 'timeout');
};
}
// Generator function support services
function getGeneratorFunction(f) {
return isaGeneratorFunction(f) ? f : makeGeneratorFunction(f);
}
function makeGeneratorFunction(f) {
return function * (state) {
do {
const resp = f(state.tm, state.dt, state.data, state.name);
if (!resp || resp === 'done')
return;
else if (resp === 'more')
yield;
else
return resp; // 'stop' or chained function
} while (true);
}
}
function isaGeneratorFunction(f) {
return Object.getPrototypeOf(f) === ProtGenFn;
}
// Inbuilt generators
function looper(count, f) {
if (typeof count !== 'number' || typeof f !== 'function')
throw new Error(`micosmo:ticker:looper: Invalid parameter`);
return function * (state) {
for (let i = 0; i < count; i++) {
state.data = i;
yield f;
}
}
}
function iterator(...af) {
if (af.length === 1 && Array.isArray(af[0]))
af = af[0];
return function * (state) {
for (const f of af) {
yield f;
}
}
}
function waiter(s, f) {
if (s >= 50)
throw new Error(`micosmo:ticker:waiter: Time unit maybe milliseconds. Use sWaiter otherwise`);
return msWaiter(s * 1000, f);
}
function sWaiter(s, f) {
return msWaiter(s * 1000);
}
function msWaiter(ms, f) {
if (typeof ms !== 'number')
throw new Error(`micosmo:ticker:msWaiter: Invalid parameter`);
return function * (state) {
for (let t = ms; t > 0; t -= state.dt)
yield;
return f;
}
}
function timer(s, f) {
if (s >= 50)
throw new Error(`micosmo:ticker:timer: Time unit maybe milliseconds. Use sTimer otherwise`);
return msTimer(s * 1000, f);
}
function sTimer(s, f) {
return msTimer(s * 1000, f);
}
function msTimer(ms, f) {
if (typeof ms !== 'number' || typeof f !== 'function')
throw new Error(`micosmo:ticker:msTimer: Invalid parameter`);
return isaGeneratorFunction(f) ? msGenTimer(ms, f) : msFuncTimer(ms, f);
}
function msGenTimer(ms, f) {
return function * (state) {
const fi = f(state);
for (let t = ms; t > 0; t -= state.dt) {
state.data = t;
const resp = fi.next();
if (resp.done)
return resp.value;
yield resp.value;
}
}
}
function msFuncTimer(ms, f) {
return function * (state) {
for (let t = ms; t > 0; t -= state.dt) {
const resp = f(state.tm, state.dt, t, state.name);
if (resp === 'more')
yield
else
return resp; // undefined, 'done, 'stop' or chained function
}
}
}
function beater(s, f) {
if (s >= 50)
throw new Error(`micosmo:ticker:beater: Time unit maybe milliseconds. Use sBeater otherwise`);
return _msBeater(s * 1000, f, true);
}
function sBeater(s, f) {
return _msBeater(s * 1000, f, true);
}
function msBeater(ms, f) {
return _msBeater(ms, f, false);
}
function _msBeater(ms, f, isInSeconds) {
if (typeof ms !== 'number' || typeof f !== 'function')
throw new Error(`micosmo:ticker:msBeater: Invalid parameter`);
return isaGeneratorFunction(f) ? msGenBeater(ms, f, isInSeconds) : msFuncBeater(ms, f, isInSeconds);
}
function msGenBeater(ms, f, isInSeconds) {
return function * (state) {
const fi = f(state);
for (let t = 0, it = ms; ; it += ms, t += ms) {
for (; it > 0; it -= state.dt)
yield;
state.data = isInSeconds ? Math.trunc(t / 1000) : t;
const resp = fi.next();
if (resp.done)
return resp.value;
yield resp.value;
}
}
}
function msFuncBeater(ms, f, isInSeconds) {
return function * (state) {
for (let t = 0, it = ms; ; it += ms) {
for (; it > 0; it -= state.dt, t += state.dt)
yield;
const resp = f(state.tm, state.dt, isInSeconds ? Math.trunc(t / 1000) : t, state.name);
if (resp === 'more')
yield
else
return resp; // undefined, 'done, 'stop' or chained function
}
}
}