DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 967,611 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Gabriel Vaquer
Gabriel Vaquer

Posted on

Can you make it even smaller?

I have this pubsub implementation and I was just curious about how would you reduce it any further.

I'm talking about the implementation, not the TypeScript part.

type Observer<T> = (payload: T) => void;

export interface Publer<T> {
  subscribe<K extends keyof T>(
    channel: K,
    observer: Observer<T[K]>,
  ): () => void;
  unsubscribe<K extends keyof T>(channel?: K): void;
  publish<K extends keyof T>(
    channel: K,
    ...a: T[K] extends undefined ? [] : [T[K]]
  ): void;
}

export const publer = <T>(): Publer<T> => {
  const eventMap = new Map<keyof T, Set<Observer<any>>>();

  return {
    subscribe: (channel, observer) =>
      (eventMap.get(channel)?.add(observer) ||
        eventMap.set(channel, new Set([observer]))) &&
      (() => eventMap.get(channel)?.delete(observer)),
    unsubscribe: (channel) =>
      channel ? eventMap.get(channel)?.clear() : eventMap.clear(),
    publish: (channel, ...[payload]) =>
      eventMap.get(channel)?.forEach((observer) => observer(payload)),
  } as const;
};
Enter fullscreen mode Exit fullscreen mode

This is the smallest way I could come up with.

interface Events {
  login: { token: string }
}

const pubsub = publer<Events>();

const unsubscribe = pubsub.subscribe('login', ({ token }) => {
  // Payload is inferred based on the event name
})

// Payload is required based on event name
pubsub.publish('login', { token: "ABC" });
Enter fullscreen mode Exit fullscreen mode

I would love to see what other ways to make it even smaller you guys think of. Cheers!

Top comments (2)

Collapse
 
valeriavg profile image
Valeria • Edited on
  • Having subscriber return unsubscribe function can simplify the unsubscribe logic since all the needed pointers can be accessed within the closure (which you do already). And I kinda like that pubsub will only return pub and sub:-)
  • If channel is always a string it can be a bit smaller if one uses pure objects instead of map to store channel observers.
  • Furthermore, one could return a tuple of [pub,sub] instead of object with named properties

But, honestly, I'm not sure why it needs to be smaller, apart from the obvious challenge:-)
Looks good the way it is

Collapse
 
brielov profile image
Gabriel Vaquer Author • Edited on

Nice! I took on your suggestion regarding the tuple and now it weights 150 bytes gzipped having removed the "global" unsubscribe function, leaving only the unsubscribe from subscribe.

export const publer = <T>(): PublerTuple<T, keyof T> => {
  const eventMap = new Map<keyof T, Set<Observer<any>>>();
  return [
    (channel, ...[payload]) =>
      eventMap.get(channel)?.forEach((observer) => observer(payload)),
    (channel, observer) =>
      (eventMap.get(channel)?.add(observer) ||
        eventMap.set(channel, new Set([observer]))) &&
      (() => eventMap.get(channel)?.delete(observer)),
  ];
};

interface Events {
  login: { token: string }
}

const [pub, sub] = publer<Events>()
Enter fullscreen mode Exit fullscreen mode

I didn't swap the Map with the plain object because I'd need to get rid of the subscribe one-liner.

Anyways, thanks for the feedback. The reason I asked here is because publer is a real package on NPM and I'm kinda obsessed about releasing the smallest possible code without loosing functionality.

Visualizing Promises and Async/Await 🀯

async await

☝️ Check out this all-time classic DEV post