Here is what I have
interface BaseEvent {
type: string;
payload: any;
}
interface EventEmitter {
emit(event: BaseEvent): void;
}
class BaseClass {
constructor(protected eventEmitter: EventEmitter) {}
emit(event: BaseEvent) {
this.eventEmitter.emit(event);
}
}
interface CustomEvent1 extends BaseEvent {
type: 'custom1';
payload: {
message: string;
};
}
interface CustomEvent2 extends BaseEvent {
type: 'custom2';
payload: {
value: number;
};
}
const eventEmitter: EventEmitter = {
emit(event: BaseEvent) {
console.log(`Event type: ${event.type}, payload:`, event.payload);
},
};
Here is what I want to do with mixins:
type Constructor<T> = new (...args: any[]) => T;
function producesCustomEvent1<TBase extends Constructor<BaseClass>>(Base: TBase) {
return class extends Base {
emitCustomEvent1(event: CustomEvent1) {
this.emit(event);
}
};
}
function producesCustomEvent2<TBase extends Constructor<BaseClass>>(Base: TBase) {
return class extends Base {
emitCustomEvent2(event: CustomEvent2) {
this.emit(event);
}
};
}
class CustomEventEmitter extends producesCustomEvent1(producesCustomEvent2(BaseClass)) {
constructor(eventEmitter: EventEmitter) {
super(eventEmitter);
}
}
const eventEmitter: EventEmitter = {
emit(event: BaseEvent) {
console.log(`Event type: ${event.type}, payload:`, event.payload);
},
};
const customEventEmitter = new CustomEventEmitter(eventEmitter);
const event1: CustomEvent1 = { type: 'custom1', payload: { message: 'Hello, world!' } };
customEventEmitter.emitCustomEvent1(event1); // Output: "Event type: custom1, payload: { message: 'Hello, world!' }"
const event2: CustomEvent2 = { type: 'custom2', payload: { value: 42 } };
customEventEmitter.emitCustomEvent2(event2); // Output: "Event type: custom2, payload: { value: 42 }"
What is the problem?
When I do it like this I have two problems.
createEventMixin<CustomEvent1>()
which creates the emitCustomEvent1
method. Is that possible?producesCustomEvent1(producesCustomEvent2(BaseClass))
is not really readable when I add more and more events to the class. And this is the reason to not use generic in the first place, because there will be instances where there will be a lot of different produced events. Is there a way to have something like a type builder of sorts? So something like const CustomEventEmitter = Builder(BaseClass).withProducingEvent1().withProducingEvent2().return()
.What the OP tries to achieve by ...
const customEventEmitter = new CustomEventEmitter(eventEmitter);
const event1: CustomEvent1 = { type: 'custom1', payload: { message: 'Hello, world!' } };
customEventEmitter.emitCustomEvent1(event1); // Output: "Event type: custom1, payload: { message: 'Hello, world!' }"
const event2: CustomEvent2 = { type: 'custom2', payload: { value: 42 } };
customEventEmitter.emitCustomEvent2(event2); // Output: "Event type: custom2, payload: { value: 42 }"
... where the above code introduces differently named emit
methods like emitCustomEvent1
and emitCustomEvent2
, which both serve the purpose of emitting each a different (type of) event through one and the same custom emitter, gets covered by an EventTarget
's dispatchEvent
method where the event type is directly addressed either by a type-specific string or via the type
attribute of an event-like object. Type safety (no chance of event spoofing) of the latter gets achieved by encapsulating (and later reading from) an initial event which gets created with every newly added event listener.
Browsers and Node.js already do support EventTarget
and therefore do feature such a "Signals and Slots" system. But in order to achieve the additional tasks of the OP which are ...
... one needs to implement such a system oneself. On top of that one then can implement the features the OP is looking for.
The above linked JavaScript variant of a function based EventTargetMixin
implementation needs to be altered towards featuring two more methods ... putBaseEventData
and deleteBaseEventData
... in addition to the already existing ... dispatchEvent
, hasEventListener
, addEventListener
and removeEventListener
.
The putBaseEventData
method is the pendant to what the OP tries to achieve with creating differently named custom dispatch methods for and with predefined (and differently named) custom events.
And the traceability feature gets achieved by something as simple as making the mixin being aware of accepting a tracer
method at the mixin's apply time. This tracer internally gets passed to any newly added event handler (the addEventListener
method has to be adapted accordingly) in order to additionally enable a handler's handleEvent
method of passing all data of interest into the tracer
.
Note
In case the following provided traceable and "putable" event-target approach would solve the OP's problem, one needs to rewrite the JavaScript mixin into a TypeScript class in order to make own custom types/classes extend from it.
// - 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>
Note: