developit / mitt

🥊 Tiny 200 byte functional event emitter / pubsub.
https://npm.im/mitt
MIT License
10.5k stars 427 forks source link

Support for `.once()` API #136

Open 0xTheProDev opened 3 years ago

0xTheProDev commented 3 years ago

Motivation I believe this is quite common use-case where you have to listen for an even only once (for example, module getting ready). The library does not support this inherently but could easily be done with some wiring. This would open up a new use-case support as well as users of the library does not have to write boilerplates on their own.

Example Usage

const emitter: mitt.Emitter = new mitt();

emitter.once('ready', () => console.log("Called Once");

emitter.emit('ready'); // Log: Called Once
emitter.emit('ready'); // No side effect

@developit I would love to know your opinion on this.

betgar commented 3 years ago

54

https://github.com/scottcorgan/tiny-emitter

https://github.com/tunnckoCoreLabs/dush

developit commented 3 years ago

I'm not entirely against adding once(). Whether it's worthwhile depends on the size impact. Here is a polyfill:

function mittWithOnce(all) {
  const inst = mitt(all);
  inst.once = (type, fn) => {
    inst.on(type, fn);
    inst.on(type, inst.off.bind(inst, type, fn));
  };
  return inst;
}

The problem with this, and with all of the implementations I have seen including the above two, is that once() and off() can lead to incorrect/unexpected results when used in combination:

const { once, off, emit } = mitt();

on('foo', foo);  // register a normal handler
once('foo', foo);  // ... and a "once" handler

off('foo', foo);  // question: which does this remove? the "once" handler, or the normal handler?

emit('foo');  // correct - in either case we see one foo() invocation here

emit('foo');  // ... but whether this invokes foo() depends on which handler got removed
0xTheProDev commented 3 years ago

Hmm, that's an interesting use case. And what comes to my mind is either not having once for handler that are already register, or define an order of precedence to remove once and then on. Either way, the behaviour gets defined but a bit opinionated. What do you think?

developit commented 3 years ago

It looks like Node just punts on this - it removes the first listener, regardless of whether it was added via once() or on(). On the web, EventTarget supports a {once:true} option, but EventTarget already silently drops duplicate handlers, so this isn't an issue:

const emitter = new EventTarget();
function foo() {}

emitter.addEventListener('foo', foo);  // adds a listener
emitter.addEventListener('foo', foo, { once: true });  // simply ignored

emitter.removeEventListener('foo', foo); // removes the first (only) listener

emitter.dispatchEvent(new Event('foo')); // no listeners registered

emitter.addEventListener('foo', foo, { once: true });  // adds a "once" listener
emitter.addEventListener('foo', foo);  // ignored (treated as duplicate)

emitter.dispatchEvent(new Event('foo')); // invokes foo(), removes the listener
0xTheProDev commented 3 years ago

Hmm, the web APIs makes sense. Essentially it took the first option that I mention. We can take that as standard. For Node, the behaviour of deletion is then just one directional.

jacob-indieocean commented 3 years ago

I would love to see once implemented.

But doesn't the polyfill you have above leak? The call to once adds two handlers, but only one of them is removed when the event fires.

ferrykranenburgcw commented 2 years ago

once is also needed here, hopefully implemented soon

juliovedovatto commented 2 years ago

My Two cents in this topic:


const emitter = mitt()

 emitter.once = (type, handler) {
    const fn = (...args) => {
      emitter.off(type, fn)
      handler(args)
    }

    emitter.on(type, fn)
  }
}

export default emitter

I used this approach a few months ago on a medium-sized project and afaik it is working until now, no problems using once this way.

We've even created a unit test to make sure it works. Maybe I missed a specific test or two 🤔

developit commented 2 years ago

@juliovedovatto that's how I generally implement this, yep. The reason that solution wouldn't work in Mitt itself is because it becomes impossible to remove a handler added via once() using emitter.off(type, handler).

sealice commented 2 years ago

@juliovedovatto that's how I generally implement this, yep. The reason that solution wouldn't work in Mitt itself is because it becomes impossible to remove a handler added via once() using emitter.off(type, handler).

@developit Can we return a new handler in once() or add the new handler as a property to the handler, so we can use emitter.off(type, handler) to remove the new handler added by once()

emitter.once = (type, handler) {
    const fn = (arg) => {
        emitter.off(type, fn);
        handler(arg);
    };

    emitter.on(type, fn);

    // add a property to the handler
    handler._ = fn;

    // or

    // return this handler
    return fn;
}

This makes it possible to remove handlers added via once() using emitter.off(type, handler).

There is no need to consider how to remove the handlers added by on and once, it is entirely up to the user to decide.

zjcwill commented 1 year ago

Typescirpt version

import mitt, { Emitter, EventHandlerMap, EventType, Handler } from 'mitt';

export function mittWithOnce<Events extends Record<EventType, unknown>>(all?: EventHandlerMap<Events>) {
  const inst = mitt(all);
  inst.once = (type, fn) => {
    inst.on(type, fn);
    inst.on(type, inst.off.bind(inst, type, fn));
  };
  return inst as unknown as {
    once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  } & Emitter<Events>;
}
beijinaqie commented 1 year ago

@juliovedovatto
Me too, if you encounter a bug, please let me know, tks

vascanera commented 1 year ago

Please please please (reiterating this in 2023) add an "official" implementation for .once() to the library. ❤️ Thanks!

cbloss commented 9 months ago

I'm asking too! Thanks for the hard work!

stackoverfloweth commented 1 month ago

this package would also solve your problem https://github.com/kitbagjs/events

vascanera commented 1 month ago

Please please please (reiterating this in 2024) add an "official" implementation for .once() to the library. ❤️ Thanks!

stackoverfloweth commented 1 month ago

Please please please (reiterating this in 2024) add an "official" implementation for .once() to the library. ❤️ Thanks!

I appreciate that you're still holding out hope.

Why not switch to kitbag? It has feature parity, more modern, supports broadcast channel, has your once method, and maybe best of all is actively being maintained.

leejunhui commented 18 hours ago

Please please please (reiterating this in 2024.7.5) add an "official" implementation for .once() to the library. ❤️ Thanks!