// - tracer function ...
// ...could be later renamed to `collectAllDispatchedEventData`
function traceAnyDispatchedEventData({ type, baseData, event, consumer }) {
console.log({ type, baseData, event, consumer });
}
class ObservableTraceableType {
constructor(name) {
this.name = name;
TraceablePutableEventTargetMixin.call(this, traceAnyDispatchedEventData);
}
}
// // for TypeScript ...
// class ObservableTraceableType extends TraceablePutableEventTarget { /* ... */ }
// // ... with a class based `TraceablePutableEventTarget` implementation.
const a = new ObservableTraceableType('A');
const b = new ObservableTraceableType('B');
const c = new ObservableTraceableType('C');
function cosumingHandlerX(/*evt*/) { /* do something with `evt` */}
function cosumingHandlerY(/*evt*/) { /* do something with `evt` */}
a.putBaseEventData('payload-with-value', { payload: { value: 1234 } });
a.putBaseEventData('payload-with-message', { payload: { message: 'missing' } });
a.addEventListener('payload-with-value', cosumingHandlerX);
a.addEventListener('payload-with-message', cosumingHandlerX);
a.addEventListener('payload-with-value', cosumingHandlerY);
a.dispatchEvent('payload-with-value');
a.dispatchEvent({
type: 'payload-with-message',
payload: { message: 'legally altered payload default message' },
});
function cosumingHandlerQ(/*evt*/) { /* do something with `evt` */}
function cosumingHandlerR(/*evt*/) { /* do something with `evt` */}
b.putBaseEventData('payload-with-value', { payload: { value: 5678 } });
b.putBaseEventData('payload-with-message', { payload: { message: 'default message' } });
b.addEventListener('payload-with-message', cosumingHandlerQ);
b.addEventListener('payload-with-value', cosumingHandlerQ);
b.addEventListener('payload-with-value', cosumingHandlerR);
b.dispatchEvent('payload-with-message');
b.dispatchEvent({
type: 'payload-with-value',
id: 'spoof-attempt-for_event-id',
target: { spoof: 'attempt-for_event-target' },
payload: { value: 9876, message: 'legally altered payload default message' },
});
function cosumingHandlerK(/*evt*/) { /* do something with `evt` */}
function cosumingHandlerL(/*evt*/) { /* do something with `evt` */}
c.addEventListener('non-prestored-event-data-FF', cosumingHandlerK);
c.addEventListener('non-prestored-event-data-FF', cosumingHandlerL);
c.addEventListener('non-prestored-event-data-GG', cosumingHandlerL);
c.dispatchEvent('non-prestored-event-data-FF');
c.dispatchEvent({
type: 'non-prestored-event-data-GG',
foo: 'FOO',
bar: { baz: 'BAZ' },
});
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// import `TraceablePutableEventTargetMixin` from module.
const TraceablePutableEventTargetMixin = (function () {
// implementation / module scope.
function isString(value/*:{any}*/)/*:{boolean}*/ {
return (/^\[object\s+String\]$/)
.test(Object.prototype.toString.call(value));
}
function isFunction(value/*:{any}*/)/*:{boolean}*/ {
return (
('function' === typeof value) &&
('function' === typeof value.call) &&
('function' === typeof value.apply)
);
}
// either `uuid` as of e.g. Robert Kieffer's
// ... [https://github.com/broofa/node-uuid]
// or ... Jed Schmidt's [https://gist.github.com/jed/982883]
function uuid(value)/*:{string}*/ {
return value
? (value^Math.random() * 16 >> value / 4).toString(16)
: ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, uuid);
}
function getSanitizedObject(value/*:{any}*/)/*:{Object}*/ {
return ('object' === typeof value) && value || {};
}
class Event {
constructor({
id/*:{string}*/ = uuid(),
type/*:{string}*/,
target/*:{Object}*/,
...additionalData/*:{Object}*/
}) {
Object.assign(this, {
id,
type,
target,
...additionalData
});
}
}
class TracingEventListener {
constructor(
target/*:{Object}*/,
type/*:{string}*/,
handler/*:{Function}*/,
tracer/*:{Function}*/,
) {
const initialEvent/*:{Event}*/ = new Event({ target, type });
function handleEvent(evt/*:{Object|string}*/, baseData/*:{Object}*/)/*:{void}*/ {
const {
id/*:{string|undefined}*/,
type/*:{string|undefined}*/,
target/*:{string|undefined}*/,
...allowedOverwriteData/*:{Object}*/
} = getSanitizedObject(evt);
// prevent spoofing of any trusted `initialEvent` data.
const trustedEvent/*:{Event}*/ = new Event(
Object.assign(
{}, getSanitizedObject(baseData), initialEvent, allowedOverwriteData
)
);
// handle event non blocking
setTimeout(handler, 0, trustedEvent);
// trace event non blocking
setTimeout(tracer, 0, {
type: trustedEvent.type, baseData, event: trustedEvent, consumer: handler,
});
};
function getHandler()/*:{Function}*/ {
return handler;
};
function getType()/*:{string}*/ {
return type;
};
Object.assign(this, {
handleEvent,
getHandler,
getType,
});
}
}
function TraceablePutableEventTargetMixin(tracer/*{Function}*/) {
if (!isFunction(tracer)) {
tracer = _=>_;
}
const observableTarget/*:{Object}*/ = this;
const listenersRegistry/*:{Map}*/ = new Map;
const eventDataRegistry/*:{Map}*/ = new Map;
function putBaseEventData(
type/*:{string}*/, { type: ignoredType/*:{any}*/, ...baseData/*:{Object}*/ }
)/*:{void}*/ {
if (isString(type)) {
eventDataRegistry.set(type, baseData);
}
}
function deleteBaseEventData(type/*:{string}*/)/*:{boolean}*/ {
let result = false;
if (isString(type)) {
result = eventDataRegistry.delete(type);
}
return result;
}
function addEventListener(
type/*:{string}*/, handler/*:{Function}*/,
)/*:{TracingEventListener|undefined}*/ {
let reference/*:{TracingEventListener|undefined}*/;
if (isString(type) && isFunction(handler)) {
const listeners/*:{Array}*/ = listenersRegistry.get(type) ?? [];
reference = listeners
.find(listener => listener.getHandler() === handler);
if (!reference) {
reference = new TracingEventListener(
observableTarget, type, handler, tracer
);
if (listeners.push(reference) === 1) {
listenersRegistry.set(type, listeners);
}
}
}
return reference;
}
function removeEventListener(
type/*:{string}*/, handler/*:{Function}*/,
)/*:{boolean}*/ {
let successfully = false;
const listeners/*:{Array}*/ = listenersRegistry.get(type) ?? [];
const idx/*:{number}*/ = listeners
.findIndex(listener => listener.getHandler() === handler);
if (idx >= 0) {
listeners.splice(idx, 1);
successfully = true;
}
return successfully;
}
function dispatchEvent(evt/*:{Object|string}*/ = {})/*:{boolean}*/ {
const type = (
(evt && ('object' === typeof evt) && isString(evt.type) && evt.type) ||
(isString(evt) ? evt : null)
);
const listeners/*:{Array}*/ = listenersRegistry.get(type) ?? [];
const baseData/*:{Object}*/ = eventDataRegistry.get(type) ?? {};
listeners
.forEach(({ handleEvent }) => handleEvent(evt, baseData));
// success state
return (listeners.length >= 1);
}
function hasEventListener(type/*:{string}*/, handler/*:{Function}*/)/*:{boolean}*/ {
return !!(
listenersRegistry.get(type) ?? []
)
.find(listener => listener.getHandler() === handler);
}
Object.defineProperties(observableTarget, {
putBaseEventData: {
value: putBaseEventData,
},
deleteBaseEventData: {
value: deleteBaseEventData,
},
addEventListener: {
value: addEventListener,
},
removeEventListener: {
value: function (
typeOrListener/*:{TracingEventListener|string}*/,
handler/*:{Function}*/,
)/*:{boolean}*/ {
return (
isString(typeOrListener) &&
isFunction(handler) &&
removeEventListener(typeOrListener, handler)
) || (
(typeOrListener instanceof TracingEventListener) &&
removeEventListener(typeOrListener.getType(), typeOrListener.getHandler())
) || false;
},
},
hasEventListener: {
value: function (
typeOrListener/*:{TracingEventListener|string}*/,
handler/*:{Function}*/,
)/*:{boolean}*/ {
return (
isString(typeOrListener) &&
isFunction(handler) &&
hasEventListener(typeOrListener, handler)
) || (
(typeOrListener instanceof TracingEventListener) &&
hasEventListener(typeOrListener.getType(), typeOrListener.getHandler())
) || false;
},
},
dispatchEvent: {
value: dispatchEvent,
},
});
// return observable target/type.
return observableTarget;
}
// module's default export.
return TraceablePutableEventTargetMixin;
}());
</script>