allex_listenablemaplowlevellib
Version:
JavaScript map that allows listening for a property
279 lines (268 loc) • 8.75 kB
JavaScript
function createListenableMap(Map, _EventEmitter, inherit, runNext, isArray, isDefined, isDefinedAndNotNull, isEqual, containerDestroyDeep, arryDestroyAll) {
'use strict';
function MapEventHandlerBase(name, cb, onlywhennotnull, singleshot) {
this.name = name;
this.cb = cb;
this.onlywhennotnull = onlywhennotnull;
this.singleshot = singleshot;
this.listener = null;
}
MapEventHandlerBase.prototype.destroy = function () {
if (this.listener) {
runNext(this.listener.destroy.bind(this.listener));
}
this.listener = null;
this.singleshot = null;
this.onlywhennotnull = null;
this.cb = null;
this.name = null;
};
MapEventHandlerBase.prototype.trigger = function (name, val) {
if (!this.cb) {
return;
}
if (this.isNameOK(name)) {
if (!isDefinedAndNotNull(val) && this.onlywhennotnull) {
return;
}
this.emitData(name, val);
if (this.singleshot) {
this.destroy();
}
}
};
function StringMapEventHandler(name, cb, onlywhennotnull, singleshot) {
MapEventHandlerBase.call(this, name, cb, onlywhennotnull, singleshot);
}
inherit(StringMapEventHandler, MapEventHandlerBase);
StringMapEventHandler.prototype.isNameOK = function (name) {
return this.name === name;
};
StringMapEventHandler.prototype.emitData = function (name, val) {
if (this.cb) {
this.cb(val);
}
};
function RegexMapEventHandler(name, cb, onlywhennotnull, singleshot) {
MapEventHandlerBase.call(this, name, cb, onlywhennotnull, singleshot);
}
inherit(RegexMapEventHandler, MapEventHandlerBase);
RegexMapEventHandler.prototype.isNameOK = function (name) {
return this.name.test(name);
};
RegexMapEventHandler.prototype.emitData = function (name, val) {
if (this.cb) {
this.cb(name, val);
}
};
function MultiEventWaiter (listenablemap, names, cb, acceptnulls, data, index) {
this.acceptnulls = acceptnulls;
this.cb = cb;
this.vals = data || new Array(names.length);
this.index = index || 0;
this.listeners = new Map();
this.lastVals = null;
if (!this.cb) {
this.destroy();
} else {
names.forEach(this.buildListener.bind(this, listenablemap));
if (this.satisfied()) {
runNext(fireCB.bind(this));
}
}
listenablemap = null;
}
MultiEventWaiter.prototype.destroy = function () {
this.lastVals = null;
if (this.listeners) {
runNext(containerDestroyDeep.bind(null,this.listeners));
}
this.listeners = null;
this.index = null;
this.vals = null;
this.cb = null;
this.acceptnulls = null;
};
MultiEventWaiter.prototype.buildListener = function (listenablemap, name, index) {
if (!listenablemap) {
this.destroy();
return;
}
index += this.index;
//console.log(listenablemap.get(name), '(', name, ') goes to index', index);
this.vals[index] = listenablemap.get(name);
this.listeners.add(name, listenablemap.changed.attach(this.satisfyName.bind(this, name, index)));
};
MultiEventWaiter.prototype.satisfied = function () {
var checker, i;
if (!this.listeners) {
return false;
}
checker = this.acceptnulls ? isDefined : isDefinedAndNotNull;
for (i=this.index; i<this.index + this.listeners.count; i++){
if (!checker(this.vals[i])){
return false;
}
}
return true;
//return this.acceptnulls ? this.vals.every(isDefined) : this.vals.every(isDefinedAndNotNull);
};
MultiEventWaiter.prototype.satisfyName = function (name, index, valname, val) {
if (!this.cb) {
name = null;
index = null;
return;
}
if (name !== valname) {
name = null;
index = null;
return;
}
var isval = this.acceptnulls ? isDefined(val) : isDefinedAndNotNull(val);
//console.log(val, '(', name, ') updates index', index);
this.vals[index] = val;
if (isval && this.satisfied()) {
fireCB.call(this);
}
name = null;
index = null;
};
//statics on MultiEventWaiter
function fireCB () {
if (isEqual(this.vals, this.lastVals)) {
return;
}
this.lastVals = this.vals.slice();
this.cb(this.vals, this);
}
//endof statics on MultiEventWaiter
function EventSpreader(listenablemap, names, cb, acceptnulls) {
this.cb = cb;
this.waiter = new MultiEventWaiter(listenablemap, names, this.onMulti.bind(this), acceptnulls);
}
EventSpreader.prototype.destroy = function () {
if (this.waiter) {
this.waiter.destroy();
}
this.waiter = null;
this.cb = null;
};
EventSpreader.prototype.onMulti = function (multis, waiter) {
this.cb.apply(null, multis);
};
function MultiMultiEventWaiter (multis, cb) {
this.cb = cb;
this.data = [];
this.multis = multis.map(function() {return null;});
multis.forEach(this.buildMulti.bind(this));
//this.trigger();
}
MultiMultiEventWaiter.prototype.destroy = function () {
if (this.multis) {
runNext(arryDestroyAll.bind(null,this.multis));
}
this.multis = null;
this.data = null;
this.cb = null;
};
MultiMultiEventWaiter.prototype.buildMulti = function (multidesc, index) {
var nameslen = multidesc.names.length, startindex = this.data.length;
this.data.push.apply(this.data, new Array(nameslen));
this.multis[index] = (new MultiEventWaiter(multidesc.map, multidesc.names, this.trigger.bind(this), multidesc.acceptnulls, this.data, startindex));
};
function satisfied(m) {
return m && m.satisfied();
}
MultiMultiEventWaiter.prototype.trigger = function () {
if (!this.multis) {
return;
}
if (this.multis.every(satisfied)){
//console.log('MultiMultiEventWaiter finds all ok on', this.multis);
this.cb(this.data, this);
}
};
function ListenableMap() {
Map.call(this);
this.changed = new _EventEmitter();
}
inherit(ListenableMap, Map);
ListenableMap.prototype.destroy = function () {
if (this.changed) {
this.changed.destruct();
}
this.changed = null;
Map.prototype.destroy.call(this);
};
ListenableMap.prototype.add = function (name, val) {
var ret = Map.prototype.add.call(this, name, val);
if (this.changed) {
this.changed.fire(name, val);
}
return ret;
};
ListenableMap.prototype.replace = function (name, val) {
var ret = Map.prototype.replace.call(this, name, val);
if (isDefined(ret) && this.changed) {
this.changed.fire(name, val);
}
return ret;
};
ListenableMap.prototype.remove = function (name) {
var ret = Map.prototype.remove.call(this, name);
if (this.changed) {
this.changed.fire(name);
}
return ret;
};
const _dummyFunc = () => {};
ListenableMap.prototype.listenFor = function (name, cb, onlywhennotnull, singleshot) {
//TODO only a String? what about Number?
//TODO integrate checks
var isre = name instanceof RegExp, meh, _meh;
if (!name) {
throw new Error("name must be a string");
}
//TODO more type checking
if (!this.changed) {
return {destroy: _dummyFunc};
}
if (isre) {
meh = new RegexMapEventHandler(name, cb, onlywhennotnull, singleshot);
_meh = meh;
this.traverse(function (val, name) {
_meh.trigger(name, val);
});
_meh = null;
} else {
meh = new StringMapEventHandler(name, cb, onlywhennotnull, singleshot);
meh.trigger(name, this.get(name));
}
if (!this.changed) {
return {destroy: _dummyFunc};
}
//TODO if not singleshot?
meh.listener = this.changed.attach(meh.trigger.bind(meh));
return meh;
};
ListenableMap.prototype.listenForMulti = function (names, cb, acceptnulls) {
if (!isArray(names)) {
throw new Error("names must be an Array");
}
//TODO more type checking
return new MultiEventWaiter(this, names, cb, acceptnulls);
};
ListenableMap.multiListenForMulti = function (listendescriptors, cb) {
// listendescriptors =
// [
// {map: map1, names: names1, acceptnulls:true},
// {map: map2, names: names2 /*acceptnulls:false*/}
// ]
return new MultiMultiEventWaiter(listendescriptors, cb);
};
ListenableMap.prototype.spread = function (names, cb, acceptnulls) {
return new EventSpreader(this, names, cb, acceptnulls);
};
return ListenableMap;
}
module.exports = createListenableMap;