diff --git a/README.md b/README.md index d4f4280..1b9ad7a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ # :skull::bulb: Dead simple PubSub and EventEmitter + > Small, readable, almost-tweetable modules utilising classic patterns in modern language. [![Build Status](https://img.shields.io/badge/build-passed-brightgreen.svg?style=flat-square)](https://semaphoreci.com/robinpokorny/dead-simple) [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/robinpokorny/dead-simple/blob/master/LICENSE) [![git3moji](https://img.shields.io/badge/git3moji-%E2%9A%A1%EF%B8%8F%F0%9F%90%9B%F0%9F%93%BA%F0%9F%91%AE%F0%9F%94%A4-fffad8.svg?style=flat-square)](https://robinpokorny.github.io/git3moji/) -[![js-standard-style](https://img.shields.io/badge/code%20style-standard-lightgrey.svg?style=flat-square)](http://standardjs.com/) [![Managed by Yarn](https://img.shields.io/badge/managed%20by-Yarn-2C8EBB.svg?style=flat-square)](https://yarnpkg.com/) ## Features -* **Small:** PubSub: 127 bytes, EventEmitter: 138 bytes, **together: 191 bytes** (all gzipped) -* **Best practices:** Subscribe returns unsubscribe. So you can use anonymous functions. -* **Readable:** There is nothing magical in the code; except the simplicity. -* **Modern:** Uses new, yet [well-supported](#browser-compatibility) features. -* **Efficient:** No memory leaks, no duplicate calls, no extra looping. -* **Clean API:** `sub` and `pub` (or `on` and `emit`), need no more, can't go less. -* **Modular:** Use only what you need. + +- **Small:** PubSub: 127 bytes, EventEmitter: 138 bytes, **together: 191 bytes** (all gzipped) +- **Best practices:** Subscribe returns unsubscribe. So you can use anonymous functions. +- **Readable:** There is nothing magical in the code; except the simplicity. +- **Modern:** Uses new, yet [well-supported](#browser-compatibility) features. +- **Efficient:** No memory leaks, no duplicate calls, no extra looping. +- **Clean API:** `sub` and `pub` (or `on` and `emit`), need no more, can't go less. +- **Modular:** Use only what you need. ## Install @@ -29,42 +30,43 @@ npm install --save dead-simple ## Usage ```js -import pubsub from 'dead-simple/pubsub' -import eventEmitter from 'dead-simple/eventEmitter' +import pubsub from "dead-simple/pubsub"; +import eventEmitter from "dead-simple/eventEmitter"; // Alternatively: // import { pubsub, eventEmitter } from 'dead-simple' // === PubSub ====== -const clicks = pubsub() +const clicks = pubsub(); -const unSub = clicks.sub((target) => console.log(`Clicked on ${target}!`)) +const unSub = clicks.sub(target => console.log(`Clicked on ${target}!`)); -clicks.pub('button') +clicks.pub("button"); // -> Clicked on button! -unSub() +unSub(); -clicks.pub('link') +clicks.pub("link"); // nothing - // === eventEmitter === // eventEmitter = named PubSub -const events = eventEmitter() +const events = eventEmitter(); -events.on('click', (target) => console.log(`Clicked on ${target}!`)) +events.on("click", target => console.log(`Clicked on ${target}!`)); -const unSubChange = events.on('change', (newValue) => console.log(`Value is now ${newValue}!`)) +const unSubChange = events.on("change", newValue => + console.log(`Value is now ${newValue}!`) +); -events.emit('change', 1968) +events.emit("change", 1968); // -> Value is now 1968! -unSubChange() +unSubChange(); -events.emit('change', 1968) +events.emit("change", 1968); // nothing -events.emit('click', 'button') +events.emit("click", "button"); // -> Clicked on button! ``` @@ -78,26 +80,39 @@ Used ES6: [const](http://kangax.github.io/compat-table/es6/#test-const), [Set](http://kangax.github.io/compat-table/es6/#test-Set), [object shorthand](http://kangax.github.io/compat-table/es6/#test-object_literal_extensions_shorthand_properties) -Chrome\* | Edge | FF | IE | Opera | Safari | iOS | Node ----------|------|----|-----|-------|--------|-----|----- -38 | 12 | 13 | -\* | 25 | 7.1 | 8 | 4 +| Chrome\* | Edge | FF | IE | Opera | Safari | iOS | Node | +| -------- | ---- | --- | --- | ----- | ------ | --- | ---- | +| 38 | 12 | 13 | -\* | 25 | 7.1 | 8 | 4 | + +_Notes:_ -*Notes:* - * Chrome includes mobile Chrome (Android 4+). - * IE 11 does *not* support only syntax feature, arrow functions and object shorthand. - * The module needs to be bundled, of course. +- Chrome includes mobile Chrome (Android 4+). +- IE 11 does _not_ support only syntax feature, arrow functions and object shorthand. +- The module needs to be bundled, of course. ### Super small versions + This project started a [pair](https://gist.github.com/robinpokorny/d743ed9e0bc5214f79076a16c8e44a8f) of [gists](https://gist.github.com/robinpokorny/dd97bd013dc5198a5bd0556c591f661c) which included a hand minified version, too. We were able to get down to **91B** for PubSub: + ```js -export default (s=new Set)=>({pub:d=>s.forEach(f=>f(d)),sub:f=>s.add(f).delete.bind(s,f)}) +export default (s = new Set()) => ({ + pub: d => s.forEach(f => f(d)), + sub: f => s.add(f).delete.bind(s, f) +}); ``` And **139B** for EventEmitter (`p` is PubSub): + ```js -export default e=>(e=new Map, Object.freeze({on:(n,f)=>(e.has(name)||e.set(n,p()),e.get(n).sub(f)),emit:(n,d)=>e.has(n)&&e.get(n).pub(d)})) +export default e => ( + (e = new Map()), + Object.freeze({ + on: (n, f) => (e.has(name) || e.set(n, p()), e.get(n).sub(f)), + emit: (n, d) => e.has(n) && e.get(n).pub(d) + }) +); ``` These versions are for fun, more like a proof of concept and may not work in some browsers. diff --git a/eventEmitter.js b/eventEmitter.js index bd928a5..b0f6b84 100644 --- a/eventEmitter.js +++ b/eventEmitter.js @@ -1,16 +1,15 @@ -const pubsub = require('./pubsub') +const pubsub = require("./pubsub"); module.exports = () => { - const events = new Map() + const events = new Map(); const on = (name, fn) => { - if (!events.has(name)) events.set(name, pubsub()) + if (!events.has(name)) events.set(name, pubsub()); - return events.get(name).sub(fn) - } + return events.get(name).sub(fn); + }; - const emit = (name, data) => - events.has(name) && events.get(name).pub(data) + const emit = (name, data) => events.has(name) && events.get(name).pub(data); - return Object.freeze({ on, emit }) -} + return Object.freeze({ on, emit }); +}; diff --git a/eventEmitter.spec.js b/eventEmitter.spec.js index 34f2703..181d9ce 100644 --- a/eventEmitter.spec.js +++ b/eventEmitter.spec.js @@ -1,62 +1,64 @@ /* eslint-env jest */ -const eventEmitter = require('./eventEmitter') +const eventEmitter = require("./eventEmitter"); -describe('Event Emitter', () => { - let emitter - const callback1 = jest.fn() - const callback2 = jest.fn() +describe("Event Emitter", () => { + let emitter; + const callback1 = jest.fn(); + const callback2 = jest.fn(); beforeEach(() => { - emitter = eventEmitter() - callback1.mockClear() - callback2.mockClear() - }) - - it('has simple API', () => { - const keys = Object.keys(emitter).sort().join() - expect(keys).toBe('emit,on') - }) - - it('passes data to registered callbacks', () => { - emitter.on('event', callback1) - emitter.on('click', callback2) - - emitter.emit('event', 'foo') - emitter.emit('click', 'bar') - - expect(callback1).toHaveBeenCalledTimes(1) - expect(callback1).toHaveBeenCalledWith('foo') - expect(callback2).toHaveBeenCalledTimes(1) - expect(callback2).toHaveBeenCalledWith('bar') - }) - - it('removes callback when it is unsubscibed', () => { - const unSub = emitter.on('event', callback1) - unSub() - emitter.emit('event', 'foo') - - expect(callback1).not.toHaveBeenCalled() - }) - - it('does not call a callback twice', () => { - emitter.on('event', callback1) - emitter.on('event', callback1) - emitter.emit('event', 'foo') - - expect(callback1).toHaveBeenCalledTimes(1) - expect(callback1).toHaveBeenCalledWith('foo') - }) - - it('does not leak on sucessfull emit', () => { - emitter.on('event', callback1) - const result = emitter.emit('event', 'foo') - - expect(result).toBeUndefined() - }) - - it('returns false on empty emit', () => { - const result = emitter.emit('event', 'foo') - - expect(result).toBe(false) - }) -}) + emitter = eventEmitter(); + callback1.mockClear(); + callback2.mockClear(); + }); + + it("has simple API", () => { + const keys = Object.keys(emitter) + .sort() + .join(); + expect(keys).toBe("emit,on"); + }); + + it("passes data to registered callbacks", () => { + emitter.on("event", callback1); + emitter.on("click", callback2); + + emitter.emit("event", "foo"); + emitter.emit("click", "bar"); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback1).toHaveBeenCalledWith("foo"); + expect(callback2).toHaveBeenCalledTimes(1); + expect(callback2).toHaveBeenCalledWith("bar"); + }); + + it("removes callback when it is unsubscibed", () => { + const unSub = emitter.on("event", callback1); + unSub(); + emitter.emit("event", "foo"); + + expect(callback1).not.toHaveBeenCalled(); + }); + + it("does not call a callback twice", () => { + emitter.on("event", callback1); + emitter.on("event", callback1); + emitter.emit("event", "foo"); + + expect(callback1).toHaveBeenCalledTimes(1); + expect(callback1).toHaveBeenCalledWith("foo"); + }); + + it("does not leak on sucessfull emit", () => { + emitter.on("event", callback1); + const result = emitter.emit("event", "foo"); + + expect(result).toBeUndefined(); + }); + + it("returns false on empty emit", () => { + const result = emitter.emit("event", "foo"); + + expect(result).toBe(false); + }); +}); diff --git a/index.js b/index.js index 1869709..0c93390 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const pubsub = require('./pubsub') -const eventEmitter = require('./eventEmitter') +const pubsub = require("./pubsub"); +const eventEmitter = require("./eventEmitter"); -module.exports = { pubsub, eventEmitter } +module.exports = { pubsub, eventEmitter }; diff --git a/pubsub.js b/pubsub.js index 21d9c95..b973cdd 100644 --- a/pubsub.js +++ b/pubsub.js @@ -1,13 +1,13 @@ module.exports = () => { - const subscribers = new Set() + const subscribers = new Set(); - const sub = (fn) => { - subscribers.add(fn) + const sub = fn => { + subscribers.add(fn); - return () => subscribers.delete(fn) - } + return () => subscribers.delete(fn); + }; - const pub = (data) => subscribers.forEach((fn) => fn(data)) + const pub = data => subscribers.forEach(fn => fn(data)); - return Object.freeze({ pub, sub }) -} + return Object.freeze({ pub, sub }); +}; diff --git a/pubsub.spec.js b/pubsub.spec.js index 88a63fd..ef7fb82 100644 --- a/pubsub.spec.js +++ b/pubsub.spec.js @@ -1,49 +1,51 @@ /* eslint-env jest */ -const pubsub = require('./pubsub') +const pubsub = require("./pubsub"); -describe('PubSub', () => { - let events - const callback = jest.fn() +describe("PubSub", () => { + let events; + const callback = jest.fn(); beforeEach(() => { - events = pubsub() - callback.mockClear() - }) - - it('has simple API', () => { - const keys = Object.keys(events).sort().join() - expect(keys).toBe('pub,sub') - }) - - it('passes data to callback', () => { - events.sub(callback) - events.pub('foo') - - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith('foo') - }) - - it('removes callback when it is unsubscibed', () => { - const unSub = events.sub(callback) - unSub() - events.pub('foo') - - expect(callback).not.toHaveBeenCalled() - }) - - it('does not call a callback twice', () => { - events.sub(callback) - events.sub(callback) - events.pub('foo') - - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith('foo') - }) - - it('does not leak on pub', () => { - events.sub(callback) - const result = events.pub('foo') - - expect(result).toBeUndefined() - }) -}) + events = pubsub(); + callback.mockClear(); + }); + + it("has simple API", () => { + const keys = Object.keys(events) + .sort() + .join(); + expect(keys).toBe("pub,sub"); + }); + + it("passes data to callback", () => { + events.sub(callback); + events.pub("foo"); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith("foo"); + }); + + it("removes callback when it is unsubscibed", () => { + const unSub = events.sub(callback); + unSub(); + events.pub("foo"); + + expect(callback).not.toHaveBeenCalled(); + }); + + it("does not call a callback twice", () => { + events.sub(callback); + events.sub(callback); + events.pub("foo"); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith("foo"); + }); + + it("does not leak on pub", () => { + events.sub(callback); + const result = events.pub("foo"); + + expect(result).toBeUndefined(); + }); +});