Автоматическое создание и объединение примесей для класса машинописного текста

Вот что у меня есть

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.info(`Event type: ${event.type}, payload:`, event.payload);
    },
};

Вот что я хочу сделать с миксинами:

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.info(`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 }"

В чем проблема?

Когда я делаю это так, у меня возникают две проблемы.

  1. Самая большая проблема: каждый раз, когда я создаю новый тип события, мне также нужно реализовать миксины, которые на 99% идентичны всем остальным миксинам. Я хочу, чтобы это было автоматизировано. В идеале, вызывая что-то вроде createEventMixin<CustomEvent1>(), которое создает метод emitCustomEvent1. Это возможно?
  2. Написание чего-то вроде producesCustomEvent1(producesCustomEvent2(BaseClass)) не очень читаемо, когда я добавляю в класс все больше и больше событий. И это причина, по которой вообще не следует использовать generic, потому что будут случаи, когда будет много разных созданных событий. Есть ли способ иметь что-то вроде построителя типов? Что-то вроде const CustomEventEmitter = Builder(BaseClass).withProducingEvent1().withProducingEvent2().return().

1. Почему бы и нет extends EventEmitter? 2. Почему бы и нет .emit('custom1', payload) ? 3. Нужна ли вам проверка во время выполнения или только проверка типов?

Dimava 05.04.2023 11:49

По моему мнению, EventEmitter всегда должен быть реализован в общем виде (либо как класс, либо как миксин), что означает, что всегда и везде есть только один метод emit/dispatch. Какой вид/тип события будет генерироваться, должно зависеть от контекста объекта, генерирующего событие. Также Event, на мой взгляд, должен быть реализован в целом. В контексте испускания нужно решить, какие значения присваиваются свойствам event и type en payload. Может быть, ОП прямо сейчас возится с проблемой, которой даже не существует.

Peter Seliger 05.04.2023 12:50

@PeterSeliger @Dimava EventEmitter должен оставаться общим. Это также может быть что-то вроде .emit('custom1', payload). Дело в том, что классы, производящие события, должны через код указывать, какие события они производят, и иметь только функции для создания этих событий. Этим я собираюсь устранить слабость архитектуры, управляемой событиями, которая теряет обзор. Когда я могу выполнить свое требование, я могу автоматически создать диаграмму, какой объект производит/потребляет какие события.

Yggdrasil 07.04.2023 06:32

Почему бы тогда не реализовать эмиттер событий таким образом, чтобы ... .emit(event) считывал type значение из переданного event объекта, чтобы уведомить всех type конкретных слушателей. Нет необходимости в пользовательских эмиттерах событий. Какой бы (наблюдаемый) объект (экземпляры классов, генерирующих события, упомянутых в OP) не вызывал emit, просто нужно передать событие своего типа.

Peter Seliger 07.04.2023 23:41

@PeterSeliger, потому что это делает практически невозможным создание диаграммы со всеми зависимостями, когда я не знаю, кто создает какое событие. Кроме того, я хочу реализовать то же самое для потребления событий, просто упустив это, чтобы упростить проблему.

Yggdrasil 09.04.2023 07:05

@Yggdrasil ... 1/3 ... Любая emit функциональность, которая является просто псевдонимом для EventTargetdispatchEvent, требует наличия addEventListener реализации. Последнее — это то самое место, где начинается управление всеми listeners, следовательно, всеми типами событий и всеми обработчиками, связанными с каждым типом. Следовательно, через любой обработчик уже можно отслеживать любое отправленное событие и, следовательно, его тип и цель отправки.

Peter Seliger 11.04.2023 13:13

@Yggdrasil ... 2/3 ... А что касается потребителей событий ... Можно было бы реализовать собственный тип TraceableEventTarget, где можно было бы разрешить addEventListener принимать необязательную функцию tracer (обратный вызов) и / или необязательный объект defaultEventData, где либо из них будет нести информацию о потребителе, который добавил слушателя.

Peter Seliger 11.04.2023 13:13

@Yggdrasil ... 3/3 ... Я до сих пор не вижу необходимости в миксинах, создающих фабрики, когда все можно решить с помощью одной реализации, например. TraceableEventTarget предоставляется либо как класс , либо как миксин.

Peter Seliger 11.04.2023 13:13
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
8
164
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чего ОП пытается достичь с помощью ...

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 }"

... где приведенный выше код вводит методы emit с разными именами, такие как emitCustomEvent1 и emitCustomEvent2, которые служат для отправки каждого события (типа) через один и тот же пользовательский эмиттер, покрывается методом EventTargetdispatchEvent, где к типу события напрямую обращаются либо через строку, специфичную для типа, либо через атрибут type объекта, подобного событию. Безопасность типов (отсутствие возможности подмены событий) последнего достигается путем инкапсуляции (и последующего чтения) начального события, которое создается с каждым вновь добавленным прослушивателем событий.

Браузеры и Node.js уже поддерживают EventTarget и поэтому имеют такую ​​систему «Сигналы и слоты». Но для достижения дополнительных задач ОП, которые ...

  • определение/регистрация конкретных базовых данных типа события,
  • отслеживание любого типа отправленного события, полезной нагрузки, цели и потребителя,

... внедрять такую ​​систему нужно самому. Вдобавок к этому можно реализовать функции, которые ищет OP.

Вышеприведенный связанный вариант JavaScript реализации функции на основе EventTargetMixin необходимо изменить, добавив еще два метода... putBaseEventData и deleteBaseEventData... в дополнение к уже существующим... dispatchEvent, hasEventListener, addEventListener и removeEventListener.

Метод putBaseEventData является подвеской к тому, чего пытается достичь OP, создавая настраиваемые методы отправки с разными именами для предопределенных (и именованных по-разному) настраиваемых событий и с ними.

И функция отслеживания достигается с помощью чего-то такого простого, как информирование миксина о принятии метода tracer во время применения миксина. Этот трассировщик внутренне передается любому вновь добавленному обработчику событий (метод addEventListener должен быть соответствующим образом адаптирован), чтобы дополнительно включить метод handleEvent обработчика для передачи всех интересующих данных в tracer.

Примечание

В случае, если следующий предоставленный отслеживаемый и «помещаемый» подход к целевому событию решит проблему OP, необходимо переписать миксин JavaScript в класс TypeScript, чтобы сделать из него собственные пользовательские типы/классы.

// - tracer function ...
//   ...could be later renamed to `collectAllDispatchedEventData`
function traceAnyDispatchedEventData({ type, baseData, event, consumer }) {
  console.info({ 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>

Примечание:

Другие вопросы по теме