stem-core
Version:
Frontend and core-library framework
297 lines (260 loc) • 8.67 kB
JavaScript
class DispatcherHandle {
constructor(dispatcher, callback) {
this.dispatcher = dispatcher;
this.callback = callback;
}
remove() {
if (!this.dispatcher) {
console.warn("Removing a dispatcher twice");
return;
}
this.dispatcher.removeListener(this.callback);
this.dispatcher = undefined;
this.callback = undefined;
}
cleanup() {
this.remove();
}
}
class Dispatcher {
constructor(options = {}) {
this.options = options;
this.listeners = [];
}
callbackExists(callback) {
for (let i = 0; i < this.listeners.length; i += 1) {
if (this.listeners[i] === callback) {
return true;
}
}
return false;
}
addListener(callback) {
if (!(typeof callback === "function")) {
console.error("The listener needs to be a function: ", callback);
return;
}
if (this.callbackExists(callback)) {
console.error("Can't re-register for the same callback: ", this, " ", callback);
return;
}
this.listeners.push(callback);
return new DispatcherHandle(this, callback);
};
addListenerOnce(callback) {
let handler = this.addListener(function () {
callback(...arguments);
handler.remove();
});
return handler;
}
removeListener(callback) {
for (let i = 0; i < this.listeners.length; i += 1) {
if (this.listeners[i] === callback) {
// Erase and return
return this.listeners.splice(i, 1)[0];
}
}
};
removeAllListeners() {
this.listeners = [];
}
dispatch(payload) {
for (let i = 0; i < this.listeners.length; ) {
let listener = this.listeners[i];
// TODO: optimize common cases
listener(...arguments);
// In case the current listener deleted itself, keep the loop counter the same
// If it deleted listeners that were executed before it, that's just wrong and there are no guaranteed about
if (listener === this.listeners[i]) {
i++;
}
}
};
}
export const DispatchersSymbol = Symbol("Dispatchers");
class Dispatchable {
get dispatchers() {
return this[DispatchersSymbol] || (this[DispatchersSymbol] = new Map());
}
getDispatcher(name, addIfMissing) {
let dispatcher = this.dispatchers.get(name);
if (!dispatcher && addIfMissing) {
dispatcher = new Dispatcher();
this.dispatchers.set(name, dispatcher);
}
return dispatcher;
}
dispatch(name, payload) {
let dispatcher = this.getDispatcher(name);
if (dispatcher) {
// Optimize the average case
if (arguments.length <= 2) {
dispatcher.dispatch(payload);
} else {
let args = Array.prototype.slice.call(arguments, 1);
dispatcher.dispatch(...args);
}
}
}
addListenerGeneric(methodName, name, callback) {
if (Array.isArray(name)) {
return new CleanupJobs(name.map(x => this[methodName](x, callback)));
}
return this.getDispatcher(name, true)[methodName](callback);
}
addListener(name, callback) {
return this.addListenerGeneric("addListener", name, callback);
}
addListenerOnce(name, callback) {
return this.addListenerGeneric("addListenerOnce", name, callback);
}
removeListener(name, callback) {
let dispatcher = this.getDispatcher(name);
if (dispatcher) {
dispatcher.removeListener(callback);
}
}
removeAllListeners(name) {
let dispatcher = this.getDispatcher(name);
if (dispatcher) {
dispatcher.removeAllListeners();
}
}
cleanup() {
this.runCleanupJobs();
delete this[DispatchersSymbol];
}
// These function don't really belong here, but they don't really hurt here and I don't want a long proto chain
// Add anything that needs to be called on cleanup here (dispatchers, etc)
addCleanupJob(cleanupJob) {
if (!this.hasOwnProperty("_cleanupJobs")) {
this._cleanupJobs = new CleanupJobs();
}
this._cleanupJobs.add(cleanupJob);
return cleanupJob;
}
runCleanupJobs() {
if (this._cleanupJobs) {
this._cleanupJobs.cleanup();
}
}
detachListener(dispatcherHandle) {
if (this._cleanupJobs) {
this._cleanupJobs.remove(dispatcherHandle);
} else {
dispatcherHandle.remove();
}
}
attachTimeout(callback, timeout) {
let executed = false;
const timeoutId = setTimeout((...args) => {
executed = true;
callback(...args);
}, timeout);
this.addCleanupJob(() => {
if (!executed) {
clearTimeout(timeoutId);
}
});
return timeoutId;
}
attachInterval(callback, timeout) {
const intervalId = setInterval(callback, timeout);
this.addCleanupJob(() => {
clearInterval(intervalId);
});
return intervalId;
}
}
// Creates a method that calls the method methodName on obj, and adds the result as a cleanup task
function getAttachCleanupJobMethod(methodName) {
let addMethodName = "add" + methodName;
let removeMethodName = "remove" + methodName;
return function (obj) {
let args = Array.prototype.slice.call(arguments, 1);
let handler = obj[addMethodName](...args);
// TODO: This should be changed. It is bad to receive 2 different types of handlers.
if (!handler) {
handler = () => {
obj[removeMethodName](...args);
}
}
this.addCleanupJob(handler);
return handler;
}
}
// Not sure if these should be added like this, but meh
Dispatchable.prototype.attachListener = getAttachCleanupJobMethod("Listener");
Dispatchable.prototype.attachEventListener = getAttachCleanupJobMethod("EventListener");
Dispatchable.prototype.attachCreateListener = getAttachCleanupJobMethod("CreateListener");
Dispatchable.prototype.attachUpdateListener = getAttachCleanupJobMethod("UpdateListener");
Dispatchable.prototype.attachDeleteListener = getAttachCleanupJobMethod("DeleteListener");
Dispatchable.prototype.attachChangeListener = getAttachCleanupJobMethod("ChangeListener");
Dispatchable.prototype.attachListenerOnce = getAttachCleanupJobMethod("ListenerOnce");
Dispatcher.Global = new Dispatchable();
class RunOnce {
run(callback, timeout = 0) {
if (this.timeout) {
return;
}
this.timeout = setTimeout(() => {
callback();
this.timeout = undefined;
}, timeout);
}
}
class CleanupJobs {
constructor(jobs = []) {
this.jobs = jobs;
}
add(job) {
this.jobs.push(job);
}
cleanup() {
for (let job of this.jobs) {
if (typeof job.cleanup === "function") {
job.cleanup();
} else if (typeof job.remove === "function" ) {
job.remove();
} else {
job();
}
}
this.jobs = [];
}
remove(job) {
if (job) {
const index = this.jobs.indexOf(job);
if (index >= 0) {
this.jobs.splice(index, 1);
}
job.remove();
} else {
this.cleanup();
}
}
}
// Class that can be used to pass around ownership of a resource.
// It informs the previous owner of the change (once) and dispatches the new element for all listeners
// TODO: a better name
class SingleActiveElementDispatcher extends Dispatcher {
setActive(element, addChangeListener, forceDispatch) {
if (!forceDispatch && element === this._active) {
return;
}
this._active = element;
this.dispatch(element);
if (addChangeListener) {
this.addListenerOnce((newElement) => {
if (newElement != element) {
addChangeListener(newElement);
}
});
}
}
getActive() {
return this._active;
}
}
export {Dispatcher, Dispatchable, RunOnce, CleanupJobs, SingleActiveElementDispatcher, getAttachCleanupJobMethod};