2

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.

  1. The biggest problem: Every time I create a new event type I need to implement the mixins as well, which are 99% identical to all other mixins. I want to have that automated. Ideally by calling something like createEventMixin<CustomEvent1>() which creates the emitCustomEvent1 method. Is that possible?
  2. Writing something like 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().
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
Yggdrasil
  • 1,377
  • 2
  • 13
  • 27
  • 1. Why not `extends EventEmitter`? 2. Why not `.emit('custom1', payload)` ? 3. Do you need runtime check or only type check? – Dimava Apr 05 '23 at 09:49
  • An `EventEmitter` in my Opinion should be always generically implemented (either as class or as mixin) which means there is just one `emit`/`dispatch` method, always and everywhere. Which kind/type of event one is going to emit should depend on the context of the event-emitting object. Also an `Event` in my opinion should be generically implemented. Within the emitting context one should decide which values are being assigned to en `event`'s `type` and `payload` property. Maybe the OP right now is fiddling with a problem which does not even exist. – Peter Seliger Apr 05 '23 at 10:50
  • @PeterSeliger @Dimava The EventEmitter should remain generic. This could also be something like `.emit('custom1', payload)`. The point is that the classes that are producing events should specify via code which events they are producing and only have functions to produce these events. With that I am going to eliminate a weakness of event-driven architecture, which is loosing the overview. When I can fulfill my requirement I can automatically generate a diagram which object produces/consumes which events. – Yggdrasil Apr 07 '23 at 04:32
  • Why not implementing then an event emitter in a way that `... .emit(event)` reads the `type` value from the passed `event` object in order to notify all `type` specific listeners. There is no need for custom event emitters. Whichever (observable) object (instances of the event generating classes, mentioned by the OP) invokes `emit` just needs to pass its type specific event. – Peter Seliger Apr 07 '23 at 21:41
  • @PeterSeliger because this makes it nearly impossible to generate a diagram with all the dependencies, when i do not know who produces which event. Additionally I want to implement the same thing for consuming events, just left that out, to make the problem more simple. – Yggdrasil Apr 09 '23 at 05:05
  • @Yggdrasil ... 1/3 ... Any `emit` functionality which is just an alias for an [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)'s `dispatchEvent` forces the presence of an `addEventListener` implementation. The latter is the very place at which one starts controlling all `listeners` hence all event types and all of each type's related handlers. Therefore one, via any handler, already could trace any dispatched event and thus its type and the dispatching target. – Peter Seliger Apr 11 '23 at 11:13
  • @Yggdrasil ... 2/3 ... And as for the event consumers ... One could implement an own `TraceableEventTarget` type where one would enable `addEventListener` to accept an optional `tracer` (callback) function and/or an optional `defaultEventData` object where either of them would carry the information of the consumer who did add the listener. – Peter Seliger Apr 11 '23 at 11:13
  • @Yggdrasil ... 3/3 ... I still do not see any need of mixin creating factories when everything could be solved with a single implementation of e.g. a `TraceableEventTarget` provided either as [class](https://stackoverflow.com/a/63584189/2627243) or as [mixin](https://stackoverflow.com/a/74140087/2627243). – Peter Seliger Apr 11 '23 at 11:13

1 Answers1

1

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 ...

  • defining/registering event-type specific base-data,
  • tracing of any dispatched event's type, payload, target and consumer,

... 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:

Peter Seliger
  • 11,747
  • 3
  • 28
  • 37