Skip to content

Instantly share code, notes, and snippets.

@jherr
Created May 17, 2021 14:29
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jherr/23101f2cd7980a7839a76b6bdff45583 to your computer and use it in GitHub Desktop.
Save jherr/23101f2cd7980a7839a76b6bdff45583 to your computer and use it in GitHub Desktop.
Code for No BT TS - Challenge 3
class EventProcessor {
handleEvent(eventName: ..., data: ...): void {
}
addHandler(handler: ...) {
}
getProcessedEvents(): ...[] {
}
}
interface EventMap {
login: { user?: string; name?: string; hasSession?: boolean };
logout: { user?: string };
}
class UserEventProcessor extends EventProcessor<EventMap> {}
const uep = new UserEventProcessor();
uep.addHandler({
filterLogin: ({ user }) => Boolean(user),
mapLogin: (data) => ({
...data,
hasSession: Boolean(data.user && data.name),
}),
});
uep.handleEvent("login", {
user: null,
name: "jack",
});
uep.handleEvent("login", {
user: "tom",
name: "tomas",
});
uep.handleEvent("logout", {
user: "tom",
});
console.log(uep.getProcessedEvents());
/*
Result:
[
{
eventName: 'login',
data: { user: 'tom', name: 'tomas', hasSession: true }
},
{ eventName: 'logout', data: { user: 'tom' } }
]
*/
class EventProcessor {
handleEvent(eventName: ..., data: ...): void {
}
addFilter(
eventName: ...,
filter: (data: ...) => boolean
): void {
}
addMap(eventName: ..., map: (data: ...) => ...): void {
}
getProcessedEvents() {
}
}
interface EventMap {
login: { user?: string; name?: string; hasSession?: boolean };
logout: { user?: string };
}
class UserEventProcessor extends EventProcessor<EventMap> {}
const uep = new UserEventProcessor();
uep.addFilter("login", ({ user }) => Boolean(user));
uep.addMap("login", (data) => ({
...data,
hasSession: Boolean(data.user && data.name),
}));
uep.handleEvent("login", {
user: null,
name: "jack",
});
uep.handleEvent("login", {
user: "tom",
name: "tomas",
});
uep.handleEvent("logout", {
user: "tom",
});
console.log(uep.getProcessedEvents());
/*
Result:
[
{
eventName: 'login',
data: { user: 'tom', name: 'tomas', hasSession: true }
},
{ eventName: 'logout', data: { user: 'tom' } }
]
*/
@deamon-cool
Copy link

deamon-cool commented Jul 1, 2022

Hi ! I see no one share any solution up to now. So I take a risk. I have some solution, of course it is not perfect, but the log result is the same :).
basic-event-handler.ts

interface EventName<T> {
  eventName: keyof T;
}

interface MyEvent<T> extends EventName<T> {
  data: T[keyof T];
}

interface MyEventFilter<T> extends EventName<T> {
  filter: (data: T[keyof T]) => boolean;
}

interface MyEventMap<T> extends EventName<T> {
  map: (data: T[keyof T]) => T[keyof T];
}


class EventProcessor<T> {
  protected events: MyEvent<T>[] = [];
  protected eventFilters: MyEventFilter<T>[] = [];
  protected eventMaps: MyEventMap<T>[] = [];

  handleEvent(eventName: keyof T, data: T[keyof T]): void {
    this.events.push({ eventName: eventName, data: data });
  }

  addFilter(eventName: keyof T, filter: (data: T[keyof T]) => boolean): void {
    this.eventFilters.push({ eventName: eventName, filter: filter });
  }

  addMap(eventName: keyof T, map: (data: T[keyof T]) => T[keyof T]): void {
    this.eventMaps.push({ eventName: eventName, map: map });
  }

  getProcessedEvents(): MyEvent<T>[] {
    let processedEvents: MyEvent<T>[] = [...this.events];
    this.eventFilters.forEach(eventFilter => {
      processedEvents = processedEvents.filter(event => {
        if (event.eventName === eventFilter.eventName) {
          return eventFilter.filter(event.data);
        } else {
          return true;
        }
      });
    });

    this.eventMaps.forEach(eventMap => {
      processedEvents = processedEvents.map(event => {
        if (event.eventName === eventMap.eventName) {
          return {
            ...event,
            data: { ...eventMap.map(event.data) }
          };
        }

        return event;
      });
    });

    return processedEvents;
  }
}


interface EventMap {
  login: { user?: string | null; name?: string; hasSession?: boolean };
  logout: { user?: string };
}


class UserEventProcessor extends EventProcessor<EventMap> { }

const uep = new UserEventProcessor();

uep.addFilter("login", ({ user }) => Boolean(user));

uep.addMap("login", (data: EventMap['login']) => ({
  ...data,
  hasSession: Boolean(data.user && data.name),
}));

uep.handleEvent("login", {
  user: null,
  name: "jack",
});
uep.handleEvent("login", {
  user: "tom",
  name: "tomas",
});
uep.handleEvent("logout", {
  user: "tom",
});

console.log(uep.getProcessedEvents());

and advanced-event-handler.ts

interface EventName<T> {
  eventName: keyof T;
}

interface MyEvent<T> extends EventName<T> {
  data: T[keyof T];
}

interface MyEventFilter<T> extends EventName<T> {
  filter: (data: T[keyof T]) => boolean;
}

interface MyEventMap<T> extends EventName<T> {
  map: (data: T[keyof T]) => T[keyof T];
}

type Handler<T> = {
  [Property in keyof T as `filter${Capitalize<string & Property>}`]?: (data: T[Property]) => boolean;
} & {
    [Property in keyof T as `map${Capitalize<string & Property>}`]?: (data: T[Property]) => T[Property];
  }

type HandlerName = 'filter' | 'map';
type EventNameLiteral<T> = string & keyof T;

class EventProcessor<T> {
  protected events: MyEvent<T>[] = [];
  protected handlers: Handler<T> = {} as Handler<T>;

  handleEvent(eventName: keyof T, data: T[keyof T]): void {
    this.events.push({ eventName, data });
  }

  addHandler(handler: Handler<T>):void {
    this.handlers = {
      ...this.handlers,
      ...handler
    };
  }

  getProcessedEvents(): MyEvent<T>[] {
    let { eventFilters, eventMaps }: {
      eventFilters: MyEventFilter<T>[];
      eventMaps: MyEventMap<T>[];
    } = this.getEventFiltersAndMaps();

    let processedEvents: MyEvent<T>[] = [...this.events];
    eventFilters.forEach(eventFilter => {
      processedEvents = processedEvents.filter(event => {
        if (event.eventName === eventFilter.eventName) {
          return eventFilter.filter(event.data);
        } else {
          return true;
        }
      });
    });

    eventMaps.forEach(eventMap => {
      processedEvents = processedEvents.map(event => {
        if (event.eventName === eventMap.eventName) {
          return {
            ...event,
            data: { ...eventMap.map(event.data) }
          };
        }

        return event;
      });
    });

    return processedEvents;
  }

  private getEventFiltersAndMaps(): {
    eventFilters: MyEventFilter<T>[];
    eventMaps: MyEventMap<T>[];
  } {
    let eventFilters: MyEventFilter<T>[] = [];
    let eventMaps: MyEventMap<T>[] = [];
    const mapName: HandlerName = 'map';
    const logoutName: EventNameLiteral<T> = 'logout' as EventNameLiteral<T>;

    Object.entries(this.handlers).forEach(entry => {
      let handlerName: HandlerName = 'filter';
      let eventName: EventNameLiteral<T> = 'login' as EventNameLiteral<T>;
      if (entry[0].search(mapName) > -1) {
        handlerName = 'map';
      }

      if (entry[0].search(logoutName) > -1) {
        eventName = 'logout' as EventNameLiteral<T>;
      }

      if (handlerName === 'filter') {
        eventFilters.push({
          eventName,
          filter: entry[1] as (data: T[keyof T]) => boolean
        });
      } else if (handlerName === 'map') {
        eventMaps.push({
          eventName,
          map: entry[1] as (data: T[keyof T]) => T[keyof T]
        });
      }
    });

    return { eventFilters, eventMaps };
  }
}

interface EventMap {
  login: { user?: string | null; name?: string; hasSession?: boolean };
  logout: { user?: string };
}

class UserEventProcessor extends EventProcessor<EventMap> { }

const uep = new UserEventProcessor();
uep.addHandler({
  filterLogin: ({ user }) => Boolean(user),
  mapLogin: (data) => ({
    ...data,
    hasSession: Boolean(data.user && data.name),
  }),
});

uep.handleEvent("login", {
  user: null,
  name: "jack",
});
uep.handleEvent("login", {
  user: "tom",
  name: "tomas",
});
uep.handleEvent("logout", {
  user: "tom",
});

console.log(uep.getProcessedEvents());

@wtfzambo
Copy link

Holy cow what a complicated challenge! With respect to the previous one this is like 10x the complexity. Anyway, here's my attempt with the basic, I had to peek at @deamon-cool's one at the beginning as I was quite lost! It works but I'm not too happy, as I was able to implement Name extends keyof T only in the handleEvent method.

interface EventName<T> {
  eventName: keyof T;
}

interface MyEvent<T> extends EventName<T> {
  data: T[keyof T];
}

interface MyFilters<T> {
  eventName: keyof T;
  filter: (e: T[keyof T]) => boolean;
}

interface MyMaps<T> extends EventName<T> {
  map: (e: T[keyof T]) => T[keyof T];
}

class EventProcessor<T> {
  protected events: MyEvent<T>[] = [];
  protected filters: MyFilters<T>[] = [];
  protected maps: MyMaps<T>[] = [];

  handleEvent<Name extends keyof T>(eventName: Name, data: T[Name]): void {
    this.events.push({ eventName, data });
  }

  addFilter(eventName: keyof T, filter: (e: T[keyof T]) => boolean): void {
    this.filters.push({ eventName, filter });
  }

  addMap(eventName: keyof T, map: (data: T[keyof T]) => T[keyof T]): void {
    this.maps.push({ eventName, map });
  }

  getProcessedEvents() {
    return this.events.reduce((prev, curr) => {
      let validEvent = true;
      let tempEvent: MyEvent<T> = curr;

      this.filters.every((filt) => {
        if (filt.eventName === curr.eventName)
          if (!filt.filter(curr.data)) {
            validEvent = false;
            return false;
          }
      });

      this.maps.forEach((map) => {
        map.eventName === curr.eventName &&
          (tempEvent = { ...tempEvent, data: { ...map.map(tempEvent.data) } });
      });

      return validEvent ? [...prev, tempEvent] : prev;
    }, [] as MyEvent<T>[]);
  }
}

interface EventMap2 {
  login: { user?: string | null; name?: string; hasSession?: boolean };
  logout: { user?: string };
}

class UserEventProcessor extends EventProcessor<EventMap2> {}

const uep = new UserEventProcessor();

uep.addFilter("login", ({ user }) => Boolean(user));

uep.addMap("login", (data: EventMap2["login"]) => ({
  ...data,
  hasSession: Boolean(data.user && data.name),
}));

uep.handleEvent("login", {
  user: null,
  name: "jack",
});
uep.handleEvent("login", {
  user: "tom",
  name: "tomas",
});
uep.handleEvent("logout", {
  user: "tom",
});

console.log(uep.getProcessedEvents());

@alexanderfanz
Copy link

In the basic solution, I'd like to remove these two lines this.maps[<keyof T>eventName] ||= []; (in addMap) and this.filters[<keyof T>eventName] ||= []; (in addFilter).
Is it possible to create a constructor that initializes both arrays inside the Record in this case?

@ryanzwe
Copy link

ryanzwe commented May 1, 2023

Here's the solution I came to

type EventLog<TInput> = Partial<{
  [K in keyof TInput]: TInput[K];
}>;

interface EventMap {
  login: { user?: string | null; name?: string; hasSession?: boolean };
  logout: { user?: string };
}

class EventProcessor<TInput> {
  private eventLog: EventLog<TInput>[] = [];
  private eventFilters: { [key in keyof TInput]?: (data: TInput[key]) => boolean } = {};
  private eventMaps: { [key in keyof TInput]?: (data: TInput[key]) => TInput[key] } = {};
  handleEvent(eventName: keyof TInput, data: TInput[keyof TInput]): void {
    const curEvent = {
      [eventName]: { ...data },
    } as EventLog<TInput>;
    this.eventLog.push(curEvent as EventLog<TInput>);
  }

  addFilter<U extends keyof TInput>(
    eventName: U,
    filter: (data: TInput[U]) => boolean
  ): void {
    this.eventFilters[eventName] = filter;
  }

  addMap<U extends keyof TInput, O>(
    eventName: U,
    map: (data: TInput[U]) => TInput[U]
  ): void {
    this.eventMaps[eventName] = map;
  }
  
  private runFilters(){
    return this.eventLog.filter((eventLog) => {
      const eventName = Object.keys(eventLog)[0] as keyof TInput;
      const eventData = eventLog[eventName];
      const filter = this.eventFilters[eventName];
      return !filter || filter(eventData);
    });
  }
  private runMaps(filteredObjs: Partial<{ [K in keyof TInput]: TInput[K]; }>[]){
    return filteredObjs.map((val,idx) => {
      const eventName = Object.keys(val)[0] as keyof TInput;
      const eventData = val[eventName]
      const curMapFunc = this.eventMaps[eventName]
      const mappedData = curMapFunc? curMapFunc(eventData) : eventData
      return {
        eventName,
        data:{
          ...mappedData
        }
      }
    })

  }

  getProcessedEvents() {
    return this.runMaps(this.runFilters());
  }
}




class UserEventProcessor extends EventProcessor<EventMap> {}

const uep = new UserEventProcessor();

uep.addFilter("login", ({ user }) => Boolean(user));

uep.addMap("login", (data) => ({
  ...data,
  hasSession: Boolean(data.user && data.name),
}));

uep.handleEvent("login", { user: null, name: "jack" });

uep.handleEvent("login", { user: "tom", name: "tomas" });

uep.handleEvent("logout", { user: "tom" });

console.log(uep.getProcessedEvents());

/*
Result:
[
  {
    eventName: 'login',
    data: { user: 'tom', name: 'tomas', hasSession: true }
  },
  { eventName: 'logout', data: { user: 'tom' } }
]
*/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment