├── .gitignore ├── package.json ├── index.js ├── test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.map 3 | node_modules/ 4 | dist 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "evx", 3 | "version": "1.1.1", 4 | "description": "Handy dandy persistent-state pub/sub with multi & wildcard subscriptions.", 5 | "source": "index.js", 6 | "module": "dist/evx.es.js", 7 | "main": "dist/evx.js", 8 | "umd:main": "dist/evx.umd.js", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "microbundle build", 14 | "postbuild": "npm run test", 15 | "watch": "microbundle watch --compress false", 16 | "test": "ava" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+ssh://git@github.com/estrattonbailey/evx.git" 21 | }, 22 | "author": "estrattonbailey", 23 | "keywords": [ 24 | "keystroke", 25 | "key watch" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/estrattonbailey/evx/issues" 30 | }, 31 | "homepage": "https://github.com/estrattonbailey/evx#readme", 32 | "devDependencies": { 33 | "ava": "^1.4.1", 34 | "microbundle": "^0.9.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const isObj = v => typeof v === 'object' && !Array.isArray(v) 2 | 3 | const validate = o => { 4 | if (!isObj(o)) throw 'state should be an object' 5 | } 6 | 7 | const uniq = arr => arr.reduce((a, b, i) => { 8 | if (a.indexOf(b) > -1) return a 9 | return a.concat(b) 10 | }, []) 11 | 12 | const fire = (evs, events, state, transient) => uniq(evs) 13 | .reduce((fns, ev) => fns.concat(events[ev] || []), []) 14 | .map(fn => fn(state, transient)) 15 | 16 | const evx = create() 17 | 18 | export const on = evx.on 19 | export const emit = evx.emit 20 | export const hydrate = evx.hydrate 21 | export const getState = evx.getState 22 | 23 | export function create (state = {}) { 24 | const events = {} 25 | 26 | return { 27 | getState () { 28 | return Object.assign({}, state) 29 | }, 30 | hydrate (s) { 31 | validate(s) 32 | 33 | Object.assign(state, s) 34 | 35 | return () => { 36 | const evs = ['*'].concat(Object.keys(s)) 37 | fire(evs, events, state) 38 | } 39 | }, 40 | on (evs, fn) { 41 | evs = [].concat(evs) 42 | evs.map(ev => events[ev] = (events[ev] || []).concat(fn)) 43 | return () => evs.map( 44 | ev => events[ev].splice(events[ev].indexOf(fn), 1) 45 | ) 46 | }, 47 | emit (ev, data, transient) { 48 | let evs = (ev === '*' ? [] : ['*']).concat(ev) 49 | 50 | data = typeof data === 'function' ? data(state) : data 51 | 52 | if (data) { 53 | validate(data) 54 | Object.assign(state, data) 55 | evs = evs.concat(Object.keys(data)) 56 | } 57 | 58 | fire(evs, events, state, transient) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { on, emit, hydrate, getState, create } from './dist/evx.js' 3 | 4 | test('single listener', t => { 5 | on('a', () => t.pass()) 6 | emit('a') 7 | }) 8 | test('multi listener', t => { 9 | on(['a', 'b'], () => t.pass()) 10 | emit('b') 11 | }) 12 | test('wildcard', t => { 13 | on('*', () => t.pass()) 14 | emit('b') 15 | }) 16 | test('state', t => { 17 | on('*', state => { 18 | if (state.foo) t.pass() 19 | }) 20 | emit('b', { foo: true }) 21 | }) 22 | test('hydrate', t => { 23 | hydrate({ bar: 'hello' }) 24 | t.is(getState().bar, 'hello') 25 | }) 26 | test('create hydrate', t => { 27 | const s = { bar: null } 28 | const evx = create(s) 29 | evx.hydrate({ bar: 'hello' }) 30 | t.is(evx.getState().bar, 'hello') 31 | }) 32 | test('multiple instances', t => { 33 | const a = create() 34 | const b = create() 35 | 36 | a.hydrate({ foo: true }) 37 | b.hydrate({ foo: false }) 38 | 39 | t.is(a.getState().foo, true) 40 | t.is(b.getState().foo, false) 41 | }) 42 | test('immutable state', t => { 43 | const s = { bar: null } 44 | const evx = create(s) 45 | const _s = evx.getState() 46 | _s.bar = 'hello' // won't mutate 47 | t.is(evx.getState().bar, null) 48 | }) 49 | test('destroy', t => { 50 | let foo = false 51 | const d = on('*', () => foo = !foo) 52 | emit('b') 53 | d() 54 | emit('b') 55 | if (foo) t.pass() 56 | }) 57 | test('hydrate emit wildcard', t => { 58 | on('*', state => t.pass()) 59 | const fire = hydrate({ hydrate: true }) 60 | t.is(getState().hydrate, true) 61 | fire() 62 | }) 63 | test('test key-named events with emit', t => { 64 | t.plan(2) 65 | on('foo', state => t.pass()) // should fire only once 66 | on('bar', state => t.pass()) 67 | emit('foo', { foo: true, bar: true }) 68 | }) 69 | test('test key-named events with hydrate', t => { 70 | t.plan(2) 71 | on('foo', state => t.pass()) 72 | on('bar', state => t.pass()) 73 | on('baz', state => t.pass()) // won't get called 74 | hydrate({ foo: true, bar: true })() 75 | }) 76 | test('state should be object', t => { 77 | t.plan(2) 78 | 79 | try { 80 | hydrate('foo')() 81 | } catch (e) { 82 | t.pass() 83 | } 84 | 85 | try { 86 | emit('*', 'foo') 87 | } catch (e) { 88 | t.pass() 89 | } 90 | }) 91 | test('emit array of events', t => { 92 | t.plan(2) 93 | on('foo', state => t.pass()) 94 | on('bar', state => t.pass()) 95 | emit([ 'foo', 'bar' ]) 96 | }) 97 | test('emit transient data', t => { 98 | t.plan(2) 99 | on('pubsub', (state, data) => { 100 | t.false(getState().foo) 101 | t.true(data.foo) 102 | }) 103 | hydrate({ foo: false }) 104 | emit('pubsub', null, { foo: true }) 105 | }) 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # evx 2 | Handy dandy persistent-state pub/sub with multi, wildcard, and single-property subscriptions. **400 3 | bytes gzipped.** 4 | 5 | ## Install 6 | ```bash 7 | npm i evx --save 8 | ``` 9 | 10 | # Usage 11 | `evx` is just a simple pub/sub bus: 12 | ```javascript 13 | import { on, emit } from 'evx' 14 | 15 | on('foo', () => console.log('foo was emitted!')) 16 | 17 | emit('foo') 18 | ``` 19 | 20 | But it also allows you to subscribe to multiple events at once: 21 | ```javascript 22 | // fires once 23 | on(['foo', 'bar'], () => console.log('foo or bar was emitted!')) 24 | 25 | emit('bar') 26 | ``` 27 | 28 | And emit multiple events at once: 29 | ```javascript 30 | // fires twice 31 | on(['foo', 'bar'], () => console.log('foo or bar was emitted!')) 32 | 33 | emit([ 'bar', 'foo' ]) 34 | ``` 35 | 36 | It has wildcard support: 37 | ```javascript 38 | on('*', () => console.log('an event was emitted!')) 39 | 40 | emit('baz') 41 | ``` 42 | 43 | Additionally, you can subscribe to specific property values by passing the 44 | property *key* as the event name: 45 | ```javascript 46 | on('someProperty', state => {}) // someProperty updated 47 | 48 | emit('foo', { someProperty: true }) // will fire 49 | hydrate({ someProperty: true })() // will also fire 50 | ``` 51 | 52 | ### State 53 | 54 | Additionally, it has a concept of state. In `evx` state *is always an object*. 55 | Any object passed to `emit` will be *shallowly* merged with global state: 56 | ```javascript 57 | emit('foo', { value: true }) 58 | ``` 59 | 60 | And all subscribers are passed the full state object: 61 | ```javascript 62 | on('foo', state => console.log(state.value)) // true 63 | ``` 64 | 65 | To emit transient data that does not get merged into the global state, pass an object as the third argument to `emit`: 66 | ```javascript 67 | emit('event', null, { message: 'Hello' }) 68 | ``` 69 | 70 | And access via the second argument subscribers: 71 | ```javascript 72 | on('event', (state, data) => console.log(data.message)) // Hello 73 | ``` 74 | 75 | If you need to add some state but don't want to emit any events, use `hydrate`: 76 | ```javascript 77 | import { hydrate } from 'evx' 78 | 79 | hydrate({ baz: true }) 80 | ``` 81 | 82 | But for convenience, `hydrate` also returns a function that, when called, will 83 | emit a '*' event: 84 | ```javascript 85 | hydrate({ baz: true })() 86 | ``` 87 | 88 | The current read-only state is accessible as well: 89 | ```javascript 90 | import { hydrate, getState } from 'evx' 91 | 92 | hydrate({ baz: true }) 93 | 94 | getState() // { baz: true } 95 | ``` 96 | 97 | ### Cleanup 98 | 99 | Subscribers return a function that will *unsubscribe* from that event: 100 | ```javascript 101 | const unsubscribe = on('foo', () => {}) 102 | 103 | emit('foo') // will fire 104 | 105 | unsubscribe() 106 | 107 | emit('foo') // will not fire 108 | ``` 109 | 110 | ### Multiple instances 111 | 112 | If you need to create a discrete instance of `evx`, use `create`: 113 | ```javascript 114 | import { create } from 'evx' 115 | 116 | const bus = create() 117 | ``` 118 | 119 | All methods above are now accessible on `bus`. 120 | 121 | You can also pass an optional initial state object to `create`: 122 | ```javascript 123 | const bus = create({ foo: 'hello' }) 124 | ``` 125 | 126 | ## License 127 | MIT License © [Eric Bailey](https://estrattonbailey.com) 128 | --------------------------------------------------------------------------------