\

Show HN: JavaScript PubSub in 163 Bytes

104 points - 03/31/2025

Source
  • sltkr

    04/01/2025

    The API feels wrong. The object that was passed to pub() is the object that should be received by the callback passed to sub().

    The use of EventTarget/CustomEvent is an implementation detail; it should not be part of the API.

    As a result, every callback implementation is larger because it must explicitly unwrap the CustomEvent object.

    Essentially, the author made the library smaller by pushing necessary code to unwrap the CustomEvent object to the callsites. That's the opposite of what good libraries do!

    The mentioned nano-pubsub gets this right, and it even gets the types correct (which the posted code doesn't even try).

      • nine_k

        04/01/2025

        The point of this exercise, to my mind, is to show the utter simplicity of pub-sub. Such code belongs to the API documentation, like the code snippets on MDN.

        Proper code would have expressive parameter names, good doc comments, types (TS FTW) and the niceties like unpacking you mention. One of them would be named topics mapped to EventTargets, so that publishers and subscribers won't need to have visibility into this implementation detail.

        • hmmokidk

          04/01/2025

          I disagree with the first point, and agree with the second.

          The usage, to me, feels appropriate for JS.

          I agree that event.detail should be returned instead of the whole event. Can definitely save some space at the callsites there!

          • 04/01/2025

        • zeroq

          04/01/2025

          In similar spirit, a minimal implemention of KV store, in 22 bytes:

            export default new Map

          • arnorhs

            04/01/2025

            I'm not a huge fan of using CustomEvent for this.. esp. in terms of interoperability (which for these <kb challenges probably doesnt matter)

            personally, i'll just roll with something like this which also is typed etc:

                export function createPubSub<T extends readonly any[]>() {
                  const l = new Set<(...args: T) => void>()
            
                  return {
                    pub: (...args: T) => l.forEach((f) => f(...args)),
                    sub: (f: (...args: T) => void) => l.add(f) && (() => l.delete(f)),
                  }
                }
            
                // usage:
                const greetings = createPubSub<[string]>()
                const unsubscribe = greetings.sub((name) => {
                  console.log('hi there', name)
                })
                greetings.pub('Dudeman')
                unsubscribe()

              • Joeri

                04/01/2025

                If listeners of this implementation aren’t unsubscribed they can’t be garbage collected, and in a real world codebase that means memory leaks are inevitable. EventDispatcher has weak refs to its listeners, so it doesn’t have this problem.

                  • AgentME

                    04/01/2025

                    The listeners can be garbage-collected if the `greetings` publisher object and any unsubscribe callbacks are garbage-collectable. This is consistent with normal Javascript EventTargets which don't use weak refs.

                    If only weak refs were kept to listeners, then any listeners you don't plan to unsubscribe and don't keep that callback around will effectively auto-unsubscribe themselves. If this was done and you called `greetings.sub((name) => console.log("hi there", name));` to greet every published value, then published values will stop being greeted whenever a garbage collection happens.

                      • arnorhs

                        04/02/2025

                        This is correct.

                        The subscribers are unlikely to be garbage collected with a weak ref as long as something else is pointing to the subscriber, so it would be a viable alternative to manual unsubscriptions - but personally I prefer to give explicit lifecycle controls to the subscriber, if possible.

                          • AgentME

                            04/03/2025

                            If the listener is a fresh function passed straight to the listen method as in my example, nothing else will have a reference to it besides the event target, and if that's a weak reference then it will get collected eventually and effectively unsubscribed on its own. Weak references don't make sense at all to use for general event listeners like this.

                    • 04/01/2025

                  • 04/01/2025

                    • chrismorgan

                      04/02/2025

                      Using the event dispatch mechanism is flat-out bigger, anyway. Here’s the interface of the original script (that is, global pub/sub functions taking a name), except that the receiver site no longer needs to look at the .detail property so it’s better:

                        let t={};
                        sub=(e,c)=>((e=t[e]??=new Set).add(c),()=>e.delete(c));
                        pub=(n,d)=>t[n]?.forEach(f=>f(d))
                      
                      The original was 149 bytes; this is 97.

                      (The nullish coalescing assignment operator ??= has been supported across the board for 4½ years. Avoiding it will cost six more bytes.)

                        • ftigis

                          04/02/2025

                          This isn't the same though. With EventTarget, if one of the callback throws, the later callbacks would still get called. With yours the later callbacks don't get called.

                            • chrismorgan

                              04/02/2025

                              True, I forgot about that. Habit of working in Rust, perhaps, and generally avoiding exceptions when working in JavaScript.

                              Well then, a few alternatives to replace f=>f(d), each with slightly different semantics:

                              • async f=>f(d) (+6, 103 bytes).

                              • f=>{try{f(d)}catch{}} (+14, 111 bytes).

                              • f=>setTimeout(()=>f(d)) (+16 bytes, 113 bytes).

                              • f=>queueMicrotask(()=>f(d)) (+20 bytes, 117 bytes).

                      • nsonha

                        04/02/2025

                        if one listener throws it will break the entire channel

                    • est

                      04/01/2025

                      TIL CustomEvent

                      https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent...

                        • bodantogat

                          04/01/2025

                          Incredibly useful, especially with React, where the Context API, state lifting, and prop drilling often feel clunky. That said, it can lead to messy code if not carefully managed.

                            • jilles

                              04/01/2025

                              Bingo! Having tons of `CustomEvents` with arbitrary handlers gets unwieldy. One way we "solved" this is by only allowing custom events in a `events.ts` file and document them pretty extensively.

                      • test1072

                        04/01/2025

                        Perhaps "eventlistener" word can be extracted, and dynamically called as string to reduce bytes

                          • 04/02/2025

                            • chrismorgan

                              04/02/2025

                              This has been a popular technique at times, but it tends to increase compressed sizes: gzip and similar are better at common string deduplication, having lower overhead. Such shenanigans are also bad for performance, especially in hot paths due to making it harder for the browser to optimise it.

                              • 04/01/2025

                                • hmmokidk

                                  04/01/2025

                                  You joke, but I think about things like this...a lot.

                              • giancarlostoro

                                04/01/2025

                                So why would I use this as opposed to BroadcastChannel?

                                  • ChocolateGod

                                    04/01/2025

                                    Overkill if you don't want to cross between browser frames I think, and I assume you can't pass references.

                                • thewisenerd

                                  04/01/2025

                                  good to know pub-sub shenanigans are ubiquitous lol

                                  here's my implementation from a while back with `setTimeout` like semantics; used it to avoid prop-drilling in an internal dashboard (sue me)

                                  https://gist.github.com/thewisenerd/768db2a0046ca716e28ff14b...

                                    • tubs

                                      04/02/2025

                                          sub => ref = 0
                                          sub => ref = 1
                                          unsub(0)
                                          sub => ref = 1 (two subs with same ref!)

                                  • nsonha

                                    04/01/2025

                                    is this like left-pad but for EventTarget? If being small is the PRIMARY goal, then we are already able to do it without a wrapper.

                                      • singpolyma3

                                        04/01/2025

                                        I think that's the (tounge in cheek) point being made

                                        • 04/01/2025

                                      • pjc50

                                        04/01/2025

                                        This is local pubsub within an application, right? i.e. corresponding to C#'s 'event' keyword.

                                      • h1fra

                                        04/01/2025

                                        sure if you remove the whole native package it's small

                                        • lerp-io

                                          03/31/2025

                                          should this copy paste macro even be a package lol

                                            • hu3

                                              03/31/2025

                                              In the author's defense they do write the entire source code in README.md, including source for alternatives.

                                              • nesarkvechnep

                                                03/31/2025

                                                Of course not but it's JavaScript, why don't we pile more on top of the garbage mountain.

                                                  • kreetx

                                                    04/01/2025

                                                    Not expert enough in pub/sub to tell whether these are sufficient, but perhaps these two functions could be folded into built-ins?

                                            • curtisszmania

                                              04/01/2025

                                              [dead]

                                              • RazorDev

                                                04/01/2025

                                                [flagged]

                                                  • sltkr

                                                    04/01/2025

                                                    Surely this comment was generated by an LLM?

                                                • tipiirai

                                                  04/01/2025

                                                  Thanks! Definitely going to use `new EventTarget()` in Nue. So obvious.

                                                  https://nuejs.org/

                                                  • blatantly

                                                    04/01/2025

                                                    23 byte version:

                                                        // Lib code>>
                                                        s={};call=(n)=>{s[n]()}
                                                        // <<
                                                    
                                                        s.hello=()=>console.log('hello');
                                                        call('hello');
                                                        delete s.hello;

                                                      • pavlov

                                                        04/01/2025

                                                        This is missing the subscription feature?

                                                        Multiple independent listeners should be able to attach a callback that fires when “hello” is called.