diff --git a/.metadocrc b/.metadocrc index 4bcfcdef..fc197819 100644 --- a/.metadocrc +++ b/.metadocrc @@ -6,6 +6,7 @@ "files": [ "lib/adapters.js", "lib/array.js", + "lib/async-emitter.js", "lib/async-iterator.js", "lib/chain.js", "lib/collector.js", diff --git a/README.md b/README.md index 5d4c8b15..4c618ee7 100644 --- a/README.md +++ b/README.md @@ -301,6 +301,78 @@ Asynchronous some (iterate in series) Non-blocking synchronous map +### class AsyncEmitter + +#### AsyncEmitter.prototype.constructor() + +#### AsyncEmitter.prototype.event(name) + +- `name`: [``][string] event name + +_Returns:_ { on: [``][set], once: [``][set] } } + +Get or create event + +#### AsyncEmitter.prototype.on(name, fn) + +- `name`: [``][string] event name +- `fn`: [``][function] listener + +Add listener + +#### AsyncEmitter.prototype.once(name, fn) + +- `name`: [``][string] event name +- `fn`: [``][function] listener + +_Returns:_ [``][promise]|[``][null] + +Add listener + +#### AsyncEmitter.prototype.emit(name, args) + +- `name`: [``][string] event name +- `args`: `` + +_Returns:_ [``][promise]|[``][null] + +Emit event + +#### AsyncEmitter.prototype.remove(name, fn) + +- `name`: [``][string] event name +- `fn`: [``][function] listener to remove + +Remove event listener + +#### AsyncEmitter.prototype.clear(name) + +- `name`: [``][string] event name + +Remove all listeners or by name + +#### AsyncEmitter.prototype.count(name) + +- `name`: [``][string] event name + +_Returns:_ [``][number] + +Get listeners count by event name + +#### AsyncEmitter.prototype.listeners(name) + +- `name`: [``][string] event name + +_Returns:_ [``][function] + +Get listeners array by event name + +#### AsyncEmitter.prototype.names() + +_Returns:_ [``][string] names + +Get event names array + ### asyncIter(base) - `base`: [``][iterable]|[``][asynciterable] an @@ -921,6 +993,7 @@ Set timeout for asynchronous function execution [object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object [function]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function [promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[set]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set [array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array [error]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error [boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type diff --git a/lib/async-emitter.js b/lib/async-emitter.js new file mode 100644 index 00000000..92556312 --- /dev/null +++ b/lib/async-emitter.js @@ -0,0 +1,106 @@ +'use strict'; + +class AsyncEmitter { + constructor() { + this.events = new Map(); + } + + // Get or create event + // name event name + // Returns: { on: , once: } } + event(name) { + const { events } = this; + const event = events.get(name); + if (event) return event; + const res = { on: new Set(), once: new Set() }; + events.set(name, res); + return res; + } + + // Add listener + // name event name + // fn listener + on(name, fn) { + this.event(name).on.add(fn); + } + + // Add listener + // name event name + // fn listener + // Returns: | + once(name, fn) { + if (fn === undefined) { + return new Promise(resolve => { + this.once(name, resolve); + }); + } + this.event(name).once.add(fn); + return Promise.resolve(); + } + + // Emit event + // name event name + // args + // Returns: + emit(name, ...args) { + const { events } = this; + const event = events.get(name); + if (!event) return Promise.resolve(); + const { on, once } = event; + const promises = [...on, ...once].map(fn => fn(...args)); + once.clear(); + if (on.size === 0) events.delete(name); + return Promise.all(promises); + } + + // Remove event listener + // name event name + // fn listener to remove + remove(name, fn) { + const { events } = this; + const event = events.get(name); + if (!event) return; + const { on, once } = event; + on.delete(fn); + once.delete(fn); + if (on.size === 0 && once.size === 0) { + events.delete(name); + } + } + + // Remove all listeners or by name + // name event name + clear(name) { + const { events } = this; + if (!name) events.clear(); + else events.delete(name); + } + + // Get listeners count by event name + // name event name + // Returns: + count(name) { + const event = this.events.get(name); + if (!event) return 0; + const { on, once } = event; + return on.size + once.size; + } + + // Get listeners array by event name + // name event name + // Returns: + listeners(name) { + const event = this.events.get(name); + if (!event) return []; + const { on, once } = event; + return [...on, ...once]; + } + + // Get event names array + // Returns: names + names() { + return [...this.events.keys()]; + } +} + +module.exports = { AsyncEmitter }; diff --git a/metasync.js b/metasync.js index a51f620a..7b46034b 100644 --- a/metasync.js +++ b/metasync.js @@ -7,6 +7,7 @@ const submodules = [ 'composition', // Unified abstraction 'adapters', // Adapters to convert different async contracts 'array', // Array utilities + 'async-emitter', // AsyncEmitter 'chain', // Process arrays sync and async array in chain 'collector', // DataCollector and KeyCollector 'control', // Control flow utilities diff --git a/test/async-emitter.js b/test/async-emitter.js new file mode 100644 index 00000000..8ec82271 --- /dev/null +++ b/test/async-emitter.js @@ -0,0 +1,262 @@ +'use strict'; + +const { AsyncEmitter } = require('..'); +const metatests = require('metatests'); + +metatests.test('AsyncEmitter on/emit', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustCall(async (a, b, c, d) => { + test.strictSame(a, 1); + test.strictSame(b, 2); + test.strictSame(c, 3); + test.strictSame(d, 4); + }); + + ae.on('e1', fn); + + await ae.emit('e1', 1, 2, 3, 4); + + test.strictSame(ae.count('e1'), 1); + test.strictSame(ae.names().length, 1); + + test.end(); +}); + +metatests.test('AsyncEmitter once', async test => { + const ae = new AsyncEmitter(); + + ae.once('e1', test.mustCall()); + ae.once('e1', test.mustCall()); + + test.strictSame(ae.count('e1'), 2); + test.strictSame(ae.names().length, 1); + + await ae.emit('e1'); + await ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter await once', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustCall(() => { + test.strictSame(ae.count('e1'), 1); + test.strictSame(ae.names().length, 1); + ae.emit('e1'); + }); + + setTimeout(fn, 0); + await ae.once('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter on/once/emit', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustCall(() => {}, 2); + + ae.on('e1', fn); + ae.once('e1', fn); + + test.strictSame(ae.count('e1'), 2); + + ae.emit('e1'); + + test.strictSame(ae.names().length, 1); + + test.end(); +}); + +metatests.test('AsyncEmitter remove', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustCall(); + + ae.on('e1', fn); + ae.emit('e1'); + + ae.remove('e1', () => {}); + ae.remove('e1', fn); + ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + ae.remove('e1', fn); + ae.emit('e1'); + + test.end(); +}); + +metatests.test('AsyncEmitter remove once', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustNotCall(); + + ae.on('e1', fn); + ae.once('e1', fn); + + test.strictSame(ae.count('e1'), 2); + test.strictSame(ae.names().length, 1); + + ae.remove('e1', fn); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter on/once/remove different', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustNotCall(); + + ae.on('e1', fn); + ae.once('e1', fn); + ae.remove('e1', fn); + ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter on/once/remove different', async test => { + const ae = new AsyncEmitter(); + + const fn = test.mustNotCall(); + + ae.on('e1', fn); + ae.once('e2', fn); + ae.remove('e1', fn); + ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.count('e2'), 1); + test.strictSame(ae.names().length, 1); + + test.end(); +}); + +metatests.test('AsyncEmitter clear all', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', test.mustNotCall()); + ae.clear(); + ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter clear by name', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', test.mustNotCall()); + ae.clear('e1'); + ae.clear('e2'); + ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.names().length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter clear once', async test => { + const ae = new AsyncEmitter(); + + ae.once('e1', test.mustNotCall()); + ae.once('e2', test.mustNotCall()); + + test.strictSame(ae.count('e1'), 1); + test.strictSame(ae.count('e2'), 1); + test.strictSame(ae.names().length, 2); + + ae.clear('e1'); + ae.emit('e1'); + + test.strictSame(ae.count('e1'), 0); + test.strictSame(ae.count('e2'), 1); + test.strictSame(ae.names().length, 1); + + test.end(); +}); + +metatests.test('AsyncEmitter names', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', () => {}); + ae.on('e1', () => {}); + ae.on('e2', () => {}); + ae.on('e3', () => {}); + + test.strictSame(ae.names(), ['e1', 'e2', 'e3']); + + test.end(); +}); + +metatests.test('AsyncEmitter listeners', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', () => {}); + ae.on('e1', () => {}); + ae.on('e2', () => {}); + ae.on('e3', () => {}); + + test.strictSame(ae.listeners('e1').length, 2); + test.strictSame(ae.listeners('e2').length, 1); + test.strictSame(ae.listeners('e3').length, 1); + test.strictSame(ae.listeners('e4').length, 0); + + test.end(); +}); + +metatests.test('AsyncEmitter await', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', test.mustCall()); + await ae.emit('e1'); + + test.end(); +}); + +metatests.test('AsyncEmitter await multiple listeners', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', test.mustCall()); + ae.on('e1', test.mustCall()); + ae.on('e1', test.mustCall()); + + await ae.emit('e1'); + + test.strictSame(ae.count('e1'), 3); + test.strictSame(ae.names().length, 1); + + test.end(); +}); + +metatests.test('AsyncEmitter await multiple events', async test => { + const ae = new AsyncEmitter(); + + ae.on('e1', test.mustCall()); + ae.on('e2', test.mustCall()); + + await ae.emit('e1'); + await ae.emit('e2'); + + test.end(); +});