Last active
December 7, 2020 04:26
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* 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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If there is any interest I just wrote a simpler resolver path-value, which does tell you where it failed, as you asked here.