Skip to content

Instantly share code, notes, and snippets.

@PAEz
Last active December 7, 2020 04:26
Show Gist options
  • Save PAEz/b6fc1687e4e963796f189d539d8d9d0c to your computer and use it in GitHub Desktop.
Save PAEz/b6fc1687e4e963796f189d539d8d9d0c to your computer and use it in GitHub Desktop.
EmitterObject- Mixing an event emitter with object-path to get me an object that tells me when its changed. I can just subscribe to * to know when a change happens.
let objectPath = require("object-path");
import EventEmitter from "./event-emitter.js";
// Doesnt take into account inherited properties like object-path does
// I was only interested in enumarable properties, so it uses Object.keys
function find(obj, what, all, index, value) {
let result = [];
const isArray = Array.isArray(obj);
const keys = isArray ? undefined : Object.keys(obj);
for (let i = 0, end = isArray ? obj.length : keys.length; i < end; i++) {
if (!all && result.length) return result[0];
const element = obj[isArray ? i : keys[i]];
if (objectPath.has(element, what)) {
if (arguments.length == 4) {
result.push(index ? isArray ? i : keys[i] : element);
continue;
}
let foundValue = objectPath.get(element, what);
if (!Array.isArray(value)) {
if (foundValue === value) result.push(index ? isArray ? i : keys[i] : element);
continue;
} else for (let j = 0; j < value.length; j++) if (value[j] === foundValue) {
result.push(index ? isArray ? i : keys[i] : element);
if (!all) return result[0];
}
}
}
return result
}
function getKey(key) {
var intKey = parseInt(key);
if (intKey.toString() === key) {
return intKey;
}
return key;
}
function pathInvalid(obj, path, includeInheritedProps) {
if (typeof path === 'number') {
path = [path];
} else if (typeof path === 'string') {
path = path.split('.');
}
if (!path || path.length === 0) {
return !!obj;
}
for (var i = 0; i < path.length; i++) {
var j = getKey(path[i]);
if ((typeof j === 'number' && Array.isArray(obj) && j < obj.length) ||
(includeInheritedProps ? (j in Object(obj)) : obj.hasOwnProperty(j))) {
obj = obj[j];
} else {
return path.slice(0, i + 1);
}
}
return false;
};
/*
The error message is handled this way becuase I didnt want it creating objects on every
call leading to GC and what not.
This should be fine aslong as you remember that EmitterObject.lastError is always a refference
to the same object.
So, this wont work...
oe.get('some.where');
getError=oe.lastError;
oe.set('some.where);
setError=oe.lastError;
...in this case getError===setError as they both refference the same object.
You would have to....
getError={...oe.lastError}
...to make a copy of it that you can use later
*/
let _errorMessage = {
error: "Path unresolvable",
path: "path",
failPoint: "invalidPath",
command: "command",
arguments: "args"
};
export default class EmitterObject extends EventEmitter {
constructor(obj, checkErrors) {
super();
this.checkErrors = checkErrors;
if (obj) this.obj = obj;
else this.obj = {};
this.lastError = undefined;
}
checkPath(path, command, args) {
this.lastError = undefined;
let invalidPath = pathInvalid(this.__obj, path);
if (invalidPath) {
_errorMessage.error = "Path unresolvable";
_errorMessage.path = path;
_errorMessage.failPoint = invalidPath;
_errorMessage.command = command || "checkPath";
_errorMessage.arguments = args || [path];
this.emit("error", _errorMessage);
this.lastError = _errorMessage;
return false
}
return true
}
find(where, what, value) {
if (this.checkErrors && !this.checkPath(where, "find", [...arguments])) return;
const obj = this._obj.get(where);
return arguments.length == 2 ? find(obj, what, false, false) : find(obj, what, false, false, value);
}
findAll(where, what, value) {
if (this.checkErrors && !this.checkPath(where, "findAll", [...arguments])) return;
const obj = this._obj.get(where);
return arguments.length == 2 ? find(obj, what, true, false) : find(obj, what, true, false, value);
}
findIndex(where, what, value) {
if (this.checkErrors && !this.checkPath(where, "findIndex", [...arguments])) return;
const obj = this._obj.get(where);
return arguments.length == 2 ? find(obj, what, false, true) : find(obj, what, false, true, value);
}
findIndexAll(where, what, value) {
if (this.checkErrors && !this.checkPath(where, "findIndexAll", [...arguments])) return;
const obj = this._obj.get(where);
return arguments.length == 2 ? find(obj, what, true, true) : find(obj, what, true, true, value);
}
get(where, DEFAULT) {
if (this.checkErrors && arguments.length == 1 && !this.checkPath(where, "get", [...arguments])) return;
return this._obj.get(where, DEFAULT)
}
set(where, what) {
const result = this._obj.set(where, what);
this.emit('set,*', where, what);
return result
}
push(where, what) {
if (arguments.length == 1) {
what = where;
where = "";
}
if (this.checkErrors && !(where == "" && Array.isArray(this.__obj)) && !this.checkPath(where, "push", [...arguments])) return;
const result = this._obj.push(where, what);
this.emit('push,*', where, what);
return result
}
insert(where, what, pos) {
if (arguments.length == 2) {
pos = what;
what = where;
where = "";
}
if (this.checkErrors && !(where == "" && Array.isArray(this.__obj)) && !this.checkPath(where, "insert", [...arguments])) return;
const result = this._obj.insert(where, what);
this.emit('insert,*', where, what, pos);
return result
}
delete(where) {
if (this.checkErrors && !this.checkPath(where, "delete", [...arguments])) return;
const result = this._obj.del(where);
this.emit('delete,*', where);
return result
}
has(where, what) {
if (arguments.length == 2 && this._obj.get(where) == what) return true;
if (where == "" && this.__obj) return true;
return this._obj.has(where);
}
set obj(what) {
this.__obj = what;
this._obj = objectPath(what);
this.emit('setObject,*');
}
get obj() {
return this.__obj;
}
}
* global WeakMap */
// https://github.com/hsocarras/es-event-emitter/blob/master/src/event-emitter.js
// PAEz - I just added some stuff to allow for on,once,off and emit to allow for multiple targets
const privateMap = new WeakMap();
// For making private properties.
function internal(obj) {
if (!privateMap.has(obj)) {
privateMap.set(obj, {});
}
return privateMap.get(obj);
}
// Excluding callbacks from internal(_callbacks) for speed perfomance.
let _callbacks = {};
/** Class EventEmitter for event-driven architecture. */
export default class EventEmitter {
/**
* Constructor.
*
* @constructor
* @param {number|null} maxListeners.
* @param {object} localConsole.
*
* Set private initial parameters:
* _events, _callbacks, _maxListeners, _console.
*
* @return {this}
*/
constructor(maxListeners = null, localConsole = console) {
const self = internal(this);
self._events = new Set();
self._console = localConsole;
self._maxListeners = maxListeners === null ?
null : parseInt(maxListeners, 10);
return this;
}
/**
* Add callback to the event.
*
* @param {string} eventName.
* @param {function} callback
* @param {object|null} context - In than context will be called callback.
* @param {number} weight - Using for sorting callbacks calls.
*
* @return {this}
*/
_addCallback(eventName, callback, context, weight) {
this._getCallbacks(eventName)
.push({
callback,
context,
weight
});
// @todo instead of sorting insert to right place in Array.
// @link http://rjzaworski.com/2013/03/composition-in-javascript
// Sort the array of callbacks in
// the order of their call by "weight".
this._getCallbacks(eventName)
.sort((a, b) => b.weight - a.weight);
return this;
}
/**
* Get all callback for the event.
*
* @param {string} eventName
*
* @return {object|undefined}
*/
_getCallbacks(eventName) {
return _callbacks[eventName];
}
/**
* Get callback's index for the event.
*
* @param {string} eventName
* @param {callback} callback
*
* @return {number|null}
*/
_getCallbackIndex(eventName, callback) {
return this._has(eventName) ?
this._getCallbacks(eventName)
.findIndex(element => element.callback === callback) : -1;
}
/**
* Check if we achive maximum of listeners for the event.
*
* @param {string} eventName
*
* @return {bool}
*/
_achieveMaxListener(eventName) {
return (internal(this)._maxListeners !== null &&
internal(this)._maxListeners <= this.listenersNumber(eventName));
}
/**
* Check if callback is already exists for the event.
*
* @param {string} eventName
* @param {function} callback
* @param {object|null} context - In than context will be called callback.
*
* @return {bool}
*/
_callbackIsExists(eventName, callback, context) {
const callbackInd = this._getCallbackIndex(eventName, callback);
const activeCallback = callbackInd !== -1 ?
this._getCallbacks(eventName)[callbackInd] : void 0;
return (callbackInd !== -1 && activeCallback &&
activeCallback.context === context);
}
/**
* Check is the event was already added.
*
* @param {string} eventName
*
* @return {bool}
*/
_has(eventName) {
return internal(this)._events.has(eventName);
}
_executeMany(eventName, func, args) {
if (eventName.includes(',')) {
let events = eventName.split(',');
events.forEach(event => {
event = event.trim();
args[0] = event;
func.apply(this, args);
})
return true
}
}
/**
* Add the listener.
*
* @param {string} eventName
* @param {function} callback
* @param {object|null} context - In than context will be called callback.
* @param {number} weight - Using for sorting callbacks calls.
*
* @return {this}
*/
on(eventName, callback, context = null, weight = 1) {
if (this._executeMany(eventName, this.on, arguments)) {
return this
}
/* eslint no-unused-vars: 0 */
const self = internal(this);
if (typeof callback !== 'function') {
throw new TypeError(`${callback} is not a function`);
}
// If event wasn't added before - just add it
// and define callbacks as an empty object.
if (!this._has(eventName)) {
self._events.add(eventName);
_callbacks[eventName] = [];
} else {
// Check if we reached maximum number of listeners.
if (this._achieveMaxListener(eventName)) {
self._console.warn(`Max listeners (${self._maxListeners})` +
` for event "${eventName}" is reached!`);
}
// Check if the same callback has already added.
if (this._callbackIsExists(...arguments)) {
self._console.warn(`Event "${eventName}"` +
` already has the callback ${callback}.`);
}
}
this._addCallback(...arguments);
return this;
}
/**
* Add the listener which will be executed only once.
*
* @param {string} eventName
* @param {function} callback
* @param {object|null} context - In than context will be called callback.
* @param {number} weight - Using for sorting callbacks calls.
*
* @return {this}
*/
once(eventName, callback, context = null, weight = 1) {
if (this._executeMany(eventName, this.once, arguments)) {
return this
}
const onceCallback = (...args) => {
this.off(eventName, onceCallback);
return callback.apply(context, args);
};
return this.on(eventName, onceCallback, context, weight);
}
/**
* Remove an event at all or just remove selected callback from the event.
*
* @param {string} eventName
* @param {function} callback
*
* @return {this}
*/
off(eventName, callback = null) {
if (this._executeMany(eventName, this.off, arguments)) {
return this
}
const self = internal(this);
let callbackInd;
if (this._has(eventName)) {
if (callback === null) {
// Remove the event.
self._events.delete(eventName);
// Remove all listeners.
_callbacks[eventName] = null;
} else {
callbackInd = this._getCallbackIndex(eventName, callback);
if (callbackInd !== -1) {
this._getCallbacks(eventName).splice(callbackInd, 1);
// Remove all equal callbacks.
this.off(...arguments);
}
}
}
return this;
}
/**
* Trigger the event.
*
* @param {string} eventName
* @param {...args} args - All arguments which should be passed into callbacks.
*
* @return {this}
*/
emit(eventName/* , ...args*/) {
if (this._executeMany(eventName, this.emit, arguments)) {
return this
}
/*
if (this._has(eventName)) {
this._getCallbacks(eventName)
.forEach(element =>
element.callback.call(element.context, args)
);
}
*/
// It works ~3 times faster.
const custom = _callbacks[eventName];
// Number of callbacks.
let i = custom ? custom.length : 0;
let len = arguments.length;
let args;
let current;
if (i > 0 && len > 1) {
args = Array.prototype.slice.call(arguments, 1);
}
while (i--) {
current = custom[i];
if (arguments.length > 1) {
current.callback.apply(current.context, args);
} else {
current.callback.call(current.context);
}
}
// Just clean it.
args = null;
return this;
}
/**
* Clear all events and callback links.
*
* @return {this}
*/
clear() {
internal(this)._events.clear();
_callbacks = {};
return this;
}
/**
* Returns number of listeners for the event.
*
* @param {string} eventName
*
* @return {number|null} - Number of listeners for event
* or null if event isn't exists.
*/
listenersNumber(eventName) {
return this._has(eventName) ?
_callbacks[eventName].length : null;
}
}
@vitaly-t
Copy link

vitaly-t commented Dec 7, 2020

If there is any interest I just wrote a simpler resolver path-value, which does tell you where it failed, as you asked here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment