mitt icon indicating copy to clipboard operation
mitt copied to clipboard

Support for `.once()` API

Open 0xTheProDev opened this issue 3 years ago • 19 comments

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.

0xTheProDev avatar Jun 01 '21 09:06 0xTheProDev

#54

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

https://github.com/tunnckoCoreLabs/dush

betgar avatar Jun 18 '21 01:06 betgar

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

developit avatar Jun 23 '21 15:06 developit

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?

0xTheProDev avatar Jun 23 '21 16:06 0xTheProDev

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

developit avatar Jun 23 '21 19:06 developit

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.

0xTheProDev avatar Jun 23 '21 20:06 0xTheProDev

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.

jacob-indieocean avatar Jun 28 '21 17:06 jacob-indieocean

once is also needed here, hopefully implemented soon

ferrykranenburgcw avatar Oct 28 '21 11:10 ferrykranenburgcw

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 🤔

juliovedovatto avatar Nov 24 '21 14:11 juliovedovatto

@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 avatar Jan 31 '22 15:01 developit

@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.

sealice avatar Feb 08 '22 08:02 sealice

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

zjcwill avatar Aug 24 '22 04:08 zjcwill

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

beijinaqie avatar Sep 30 '22 02:09 beijinaqie

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

vascanera avatar Feb 19 '23 22:02 vascanera

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

cbloss avatar Sep 14 '23 17:09 cbloss