Published
Edited
Dec 2, 2020
Importers
Insert cell
md`# EventEmitter & Intents`
Insert cell
/**
* Creates and returns a "{@link deferred}" object - a newly created {@link Promise}
* instance with exposed "resolve", "reject", "end" and "done" methods.
* It also has the "handled" flag showing if the intent was already resolved or
* not. After promise resolving its results are available in the "result" and
* "error" fields.
* @param {Object} deferred deferred object to which to which new fields should
* be attached; if it is not defined then an empty object is used; this method
* returns this object
* @param {Function} deferred.onError method to invoke if the registered
* handlers rises errors
*/
function newDeferred(deferred = {}) {

let resolve, reject, list = [];
let resolveLatch, latch = new Promise(r => resolveLatch = r);

/**
* Promise associated with this deferred object.
*/
deferred.promise = new Promise((y, n) => (resolve = y, reject = n));

/** Unique identifier of this deferred object. Used for debugging. */
deferred.id = `id-${newDeferred._counter = (newDeferred._counter || 0) + 1}`;

/**
* This flag shows if this promise was already resolved or not.
*
* @type {Boolean}
*/
deferred.handled = false;
/**
* Error returned by this deferred object. By default it is undefined.
* @type {any}
*/
deferred.error = undefined;
/**
* Result returned by this deferred object. By default it is undefined.
* @type {any}
*/
deferred.result = undefined;
/**
* This method resolves this promise with the specified value.
* @param result the resulting value for this promise
*/
deferred.resolve = (result) => { deferred.end(undefined, result); }
/**
* This method resolves this promise with the specified error.
* @param err the error which used to resolve this promise
*/
deferred.reject = (err) => { deferred.end(err); }
/**
* This method notifies all registred listeners with promise results.
* @private
*/
const notify = async (h) => {
await latch;
try { await h(deferred.error, deferred.result); }
catch (err) { try { deferred.onError && await deferred.onError(err); } catch (e) {} }
};
/**
* Registers a new listener to invoke *before* this promise returns the control
* to the callee.
* @param {Function} h the listener to register; it will be called
* before the promise returns the control
*/
deferred.done = (h) => deferred.handled ? notify(h) : list.push(h);
/**
* Finalizes the promise with the specified error or the resulting value.
* @param e the error used to resolve the promise; if it is not defined
* (or null) then this promise will be resolved with the resulting value.
* @param r the resulting value used to resolve the promise; it is
* taken into account only if the error is not defined.
*/
deferred.end = async (e, r) => {
deferred.handled = true;
deferred.end = () => {};
try {
deferred.error = await e;
deferred.result = await r;
} catch (error) {
deferred.error = error;
}
resolveLatch();
const array = list; list = null;
await Promise.all(array.map(notify));
if (deferred.error) reject(deferred.error);
else resolve(deferred.result);
}
return deferred;
}

Insert cell
class EventEmitter {

/**
* Registers an event listener for the specified event. If the listener has
* already been registered for the event, this is a no-op.
*
* @param {string} name The event name.
* @param {function} handler The listener function.
*/
addEventListener(name, handler) {
const handlers = this._getHandlers(name, true);
handlers.push(handler);
return () => this.removeEventListener(name, handler);
}
on(...args) { return this.addEventListener(...args); }
off(...args) { return this.removeEventListener(...args); }

/**
* Unregisters an event listener from the specified event. If the listener
* hasn't been registered for the event, this is a no-op.
*
* @param {string} name The event name.
* @param {function} handler The listener function.
*/
removeEventListener(name, handler) {
const handlers = this._getHandlers(name, false);
if (handlers) {
let idx = handlers.indexOf(handler);
if (idx >= 0) handlers.splice(idx, 1);
}
}

/**
* Emits an event, causing all registered event listeners for that event to be
* called in registration order.
*
* @param {string} name The event name.
* @param {...*} args Arguments to call listeners with.
* @return {number} number of called handlers
*/
emit(name, ...args) {
const handlers = this._getHandlers(name, false);
const len = handlers ? handlers.length : 0;
for (let i = 0; i < len; i++) {
const handler = handlers[i];
handler.apply(this, args);
}
return len;
}
_getHandlers(name, create) {
const map = this.__events = this.__events || {};
let handlers = map[name];
if (!handlers && create) handlers = map[name] = [];
return handlers;
}

}
Insert cell

/**
* Intents are {@link Promise Promises} used as events. It allows to notify
* about begin and ends of asynchronous processes.
*
* @typedef {deferred} Intent
*/

/**
* This object contains a set of methods to use as Mixins for other classes
* working with {@link Intent intents}.
*/
class Intents extends EventEmitter {
constructor(...options) {
super(...options);
this._handlers = {};
}

/**
* Creates and returns a new intent with the specified options. An intent
* is a defererred object - a newly created {@link Promise} instance with
* exposed "resolve", "reject", "end" and "done" methods. It also has the
* "handled" flag showing if the intent was already resolved or not.
* @see {@link newDeferred}
* @param {String} key key of the intent
* @param {Object} payload an object containing parameters to add
* to the newly created intent
* @return {Intent} an intent with the specified key and parameters
*/
_newIntent(key, payload = {}) {
return newDeferred({
key,
target : this,
payload
});
}

/**
* Emit the specified intent and delivers it to all registered listeners.
* This method should be overloaded in subclasses.
* @param {Intent} intent to emit
* @return {Int} number of notified handlers
*/
async _emitIntent(intent) {
const delimiter = '.';
const keys = (intent.key || '').split(delimiter);
let n = 0;
while (keys.length) {
n += this.emit(keys.join(delimiter), intent);
keys.pop();
}
return n;
}
async _onUnhandledIntent(prevIntent, nextIntent) {
nextIntent.reject(new Error(`Intent "${nextIntent.key} is not handled yet`));
}

setHandler(key, action) {
this._handlers[key] = action;
return () => (this._handlers[key] === action) && (delete this._handlers[key]);
}

/**
* Creates a new {@link Intent intent}, delivers it to all listeners
* and executes the given action after that. Note that the action is executed
* *after* the intent was delivered to all listeners.
* @param {String} options.key of the key of the intent to create
* @param {Object} options optional intent parameters
* @param {Function} action optional action to execute; if this action
* is not defined then the returned intent will be not resolved
* @return {Intent} corresponding to the specified parameters
*/
run(key, options, action) {
if (action === undefined && typeof options === 'function') {
action = options;
options = {};
}
const intent = this._newIntent(key, options);
const intents = this._intents = this._intents || {};
const prevIntent = intents[key];
let prevPromise;
if (prevIntent && !prevIntent.handled) {
prevPromise = this._onUnhandledIntent(prevIntent, intent);
}
intents[key] = intent;
action = action || this._handlers[key] || this._handlers[''];
Promise.resolve()
.then(() => prevPromise)
.then(() => this._emitIntent(intent))
.then((n) => !intent.handled && action && intent.resolve(action(intent, n)))
.catch(intent.reject);
return intent;
}

getIntent(key) {
const intents = this._intents = this._intents || {};
return intents[key];
}


}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more