├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin.js ├── codecs.proto ├── examples ├── basic-example.js ├── emit-callback-needs-subscriber.js ├── example-schema.proto ├── multiple-connected-emitters.js └── subscriber-synchronisation.js ├── hyperemitter.js ├── package.json └── test ├── fixture ├── basic.proto └── messages.proto └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | build/ 4 | libleveldb.so 5 | libleveldb.a 6 | test-data/ 7 | _benchdb_* 8 | *.sw* 9 | db* 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4.0" 5 | - "iojs-v1" 6 | - "iojs-v2" 7 | - "iojs-v3" 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Hyperemitter is an __mad science__ project and encourages participation. If you feel you can help in any 3 | way, be it with examples, extra testing, or new features please be our guest. 4 | 5 | ## Helping out 6 | Contributing is not always about adding new features, there are plenty of other ways to get 7 | involved, for instance: 8 | 9 | - add more tests, unit or performance based. 10 | - write guides and documentation or proof-read and fix existing ones. 11 | - report, find and/or fix bugs. 12 | - add examples of usage, patterns or integration with other tools. 13 | 14 | Like any other repository there is plenty to be done for people of all skill levels and 15 | specialities, if you have any questions, simply open an issue on our [Issues Board][]. 16 | 17 | ## Obtaining the Source 18 | In order to obtain the source for HyperEmitter we first suggest you clone it in Github. After this 19 | is done, navigate to a suitable directory on your machine and run: 20 | 21 | ``` 22 | git clone https://github.com//hyperemitter.git 23 | ``` 24 | 25 | This will pull your fork into a new folder `/hyperemitter`, move to this directory: 26 | 27 | ``` 28 | cd hyperemitter 29 | ``` 30 | 31 | Finally, install HyperEmitter's dependencies from npm: 32 | 33 | ``` 34 | npm install 35 | ``` 36 | 37 | ## Running Tests and Linting 38 | HyperEmitter's tests are written using [Tape][], a [TAP][] compliant testing module, [Faucet][] 39 | is used to pretty print the TAP stream. Tests are located in the `/test` folder and can be 40 | ran with the following command: 41 | 42 | ``` 43 | npm run test 44 | ``` 45 | 46 | For linting and related checks, HyperEmitter users [JSStandard][]. Linting can be performed with the 47 | following command: 48 | 49 | ``` 50 | npm run lint 51 | ``` 52 | 53 | In both cases, results are outputted to the console window. 54 | 55 | ## Making a Contribution 56 | If you have something you would like to contribute first ensure your master branch is up to date with 57 | ours, we assume a remote named mcollina exists that points to this repo. 58 | 59 | ``` 60 | git checkout master 61 | git pull --rebase mcollina master 62 | ``` 63 | 64 | Next, create a branch for your contribution: 65 | 66 | ``` 67 | git checkout -b name-of-my-branch 68 | ``` 69 | 70 | HyperEmitter uses [Precommit][] to ensure that tests pass and your 71 | code is linted before allowing a commit to be created. Unfortunately this means most visual git tools will 72 | not allow commits. To create a commit at the command line simply do: 73 | 74 | ``` 75 | git add --all 76 | git commit -m "a commit message" 77 | ``` 78 | 79 | If you need to create multiline commits simple press enter before adding the second `"` this will cause 80 | the console to add a line break to the commit and put the curser on a new line, to finish simply close the 81 | string and press enter. 82 | 83 | ## Creating a Pull Request 84 | Commit your changes until you are happy. Once you are ready to submit a pull request jump back out to master 85 | and rebase again to ensure you have the latest source: 86 | 87 | ``` 88 | git checkout master 89 | git pull --rebase mcollina master 90 | git push -f origin master 91 | ``` 92 | 93 | Next jump back on to your branch and rebase it with your newly updated master: 94 | 95 | ``` 96 | git checkout name-of-my-branch 97 | git rebase master 98 | git push -f origin name-of-my-branch 99 | ``` 100 | 101 | Finally navigate to your fork on github. You should see a small marker to create a pull request, just 102 | above the repo explorer. Make sure you add some information along with your pull request, it makes 103 | reviewing it easier. 104 | 105 | ## Got Questions 106 | If you have any issues or need any help please reach out to [@matteocollina][Twitter]. 107 | 108 | [Issues Board]: https://github.com/mcollina/hyperemitter/issues 109 | [Tape]: https://www.npmjs.com/package/tape 110 | [TAP]: https://testanything.org/ 111 | [JSStandard]: https://www.npmjs.com/package/standard 112 | [Precommit]: https://www.npmjs.com/package/pre-commit 113 | [Faucet]: https://www.npmjs.com/package/faucet 114 | [Twitter]: https://twitter.com/matteocollina 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Matteo Collina 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 13 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HyperEmitter   [![Build Status](https://travis-ci.org/mcollina/hyperemitter.png)](https://travis-ci.org/mcollina/hyperemitter) 2 | 3 | HyperEmitter is a horizontally scalable __and__ persistent EventEmitter powered by a [Merkle DAG](http://npm.im/hyperlog). 4 | (Yes, it's like a blockchain). In other contexts, this concept is also called an EventStore. HyperEmitter 5 | uses [protocol-buffers](https://developers.google.com/protocol-buffers/), specifically 6 | [mafintosh's](https://github.com/mafintosh/protocol-buffers) implementation, for handling message schemas, although custom codecs are also supported. 7 | 8 | > This module is __highly experimental__, __possibly under-perfoming__, and __may have bugs__, use with 9 | > caution. On the other end, if the thought of a persistent, horizontally scaleable EventStore gets you 10 | > excite please jump in wherever you feel you can help! 11 | 12 | ## Installation 13 | To install the module for use locally in your project use: 14 | 15 | ``` 16 | npm install hyperemitter --save 17 | ``` 18 | 19 | To install the companion CLI tool, you will need to install globally: 20 | 21 | ``` 22 | npm install hyperemitter -g 23 | ``` 24 | 25 | ## Example 26 | The example below can be found and ran from the [examples](./examples/) folder; it demonstrates 27 | how to connect two HyperEmitters together and how they both receive all messages sent. 28 | 29 | ```javascript 30 | 'use strict' 31 | 32 | var fs = require('fs') 33 | var path = require('path') 34 | 35 | // The emitter itself as well as an in memory 36 | // leveldb based store, any leveldb store will do. 37 | var HyperEmitter = require('../hyperemitter') 38 | var buildDB = require('memdb') 39 | 40 | // use the example-schema.proto as the message schema. 41 | var schema = fs.readFileSync(path.join('.', 'example-schema.proto')) 42 | 43 | // two emitters will be used for this example, notice each 44 | // maintains it's own leveldb store and share the same schema. 45 | var emitterOne = new HyperEmitter(buildDB('a'), schema) 46 | var emitterTwo = new HyperEmitter(buildDB('b'), schema) 47 | 48 | // listen on port 9001, ensure no connection error. 49 | emitterOne.listen(9901, function (err) { 50 | if (err) { return } 51 | 52 | // connect to the first emitter. 53 | emitterTwo.connect(9901, '127.0.0.1', function (err) { 54 | if (err) { return } 55 | }) 56 | 57 | // basic message type 58 | var userAddedMsg = { 59 | id: 1, 60 | username: 'user' 61 | } 62 | 63 | // basic message type 64 | var userRemovedMsg = { 65 | id: 1 66 | } 67 | 68 | // Messages sent on either emitter will be handled. 69 | emitterOne.on('userRemoved', function (msg) { 70 | console.log('userRemoved: ', msg) 71 | }) 72 | 73 | // Messages sent on either emitter will be handled. 74 | emitterTwo.on('userAdded', function (msg) { 75 | console.log('userAdded: ', msg) 76 | }) 77 | 78 | // We send each message across the opposite emitter. 79 | emitterOne.emit('userAdded', userAddedMsg) 80 | emitterTwo.emit('userRemoved', userRemovedMsg) 81 | 82 | function complete () { 83 | emitterOne.close() 84 | emitterTwo.close() 85 | } 86 | 87 | // we will wait for 500ms to see if more than one 88 | // message is delivered to the subscribers above. 89 | setTimeout(complete, 500) 90 | }) 91 | ``` 92 | 93 | ## Using The CLI Tool 94 | HyperEmitter comes with a nice CLI to work with other remote HyperEmitter's. Also, it doubles 95 | as really useful tool for debugging. The sample below uses the schema in `test/fixture/`. 96 | 97 | ``` 98 | hypem --db db --port 1234 fixture/messages.proto 99 | ``` 100 | 101 | On pressing enter, you should see output similar to the following: 102 | 103 | ``` 104 | listening on 1234 127.0.0.1 105 | EventPeer { id: '123', addresses: [ { ip: 'localhost', port: 1234 } ] } 106 | ``` 107 | 108 | From here you can then use the provided REPL to work with HyperEmitter: 109 | 110 | ```javascript 111 | hyper.emit('Hello', { from: 'Matteo' }) 112 | ``` 113 | 114 | And receive the following response: 115 | 116 | ``` 117 | Hello { from: 'Matteo', message: '' } 118 | ``` 119 | 120 | To connect another hyperEmitter in another shell: 121 | 122 | ``` 123 | hypem --db db2 --target-port 1234 fixture/messages.proto 124 | ``` 125 | 126 | Which will output the following: 127 | 128 | ``` 129 | connected to localhost 1234 130 | EventPeer { id: '123', addresses: [ { ip: 'localhost', port: 1234 } ] } 131 | Hello { from: 'Matteo', message: '' } 132 | ``` 133 | 134 | As you can see the events are synced up! 135 | 136 | To export the data in [newline delimited json](http://ndjson.org/), use: 137 | 138 | ``` 139 | hypem --no-repl --db db4 --target-port 1234 fixture/messages.proto 140 | ``` 141 | 142 | Which will produce output like so: 143 | 144 | ``` 145 | {"name":"EventPeer","payload":{"id":"123","addresses":[{"ip":"localhost","port":1234}]}} 146 | {"name":"Hello","payload":{"from":"Matteo","message":""}} 147 | ``` 148 | 149 | The cli tool also works as a input stream, following the UNIX philosophy. If you close a REPL or a ndjson stream, 150 | the next time it will start where it stopped. If you have a stream, you can start back from the beginning using 151 | the `--from-scratch` flag. 152 | 153 | ## API Reference 154 | This page contains a list of public API's exposed by the HyperEmitter module as well as a brief 155 | description of their use. For additional samples please checkout out our [examples](./examples/) 156 | folder. 157 | 158 | * HyperEmitter 159 | * .messages 160 | * .registerCodec() 161 | * .emit() 162 | * .on() 163 | * .removeListener() 164 | * .connect() 165 | * .listen() 166 | * .stream() 167 | * .close() 168 | 169 | 170 | ### HyperEmitter(db, schema, [opts]) 171 | HyperEmitter is both the name of the module and of the function to be required. The function is actually 172 | a class or a class factory depending on how it is used. The code below shows both valid ways to get a 173 | new instance of HyperEmitter for use: 174 | 175 | ``` 176 | var emitterOne = Hyperemitter(someDb, someSchema) 177 | var emitterTwo = new HyperEmitter(someDb, someSchema) 178 | ``` 179 | 180 | * ##### _db_ 181 | The `db` argument accepts a [levelup](http://npm.im/levelup) instance, which in turn is powered by 182 | [leveldb](). We recommend [level](http://npm.im/level) for persistent storage and 183 | [memdb](http://npm.im/memdb) if you require an in memory store. 184 | 185 | * ##### _schema_ 186 | An string or stream that represents all of the messages to support in a given instance of HyperEmitter. 187 | 188 | * ##### _opts_ 189 | An optional object that can be provided to configure the created instance. All available 190 | options are listed below. 191 | 192 | * ___reconnectTimeout:___ The timeout that this instance will wait before reconnecting to peers. 193 | 194 | --- 195 | 196 | ### .messages 197 | Message types are stored in the `.messages` field for each instance created. This field is populated 198 | with a normalized object based on the schema provided. Each property represents a single message, indvidual 199 | messages, as well as their encoder and decoder can be accessed from here. 200 | 201 | ``` js 202 | var emitter = HyperEmitter(someDb, someSchema) 203 | 204 | // the object containing each message as a property 205 | var messages = emitter.messages 206 | 207 | // access an individual message by it's name 208 | var message = messages[messageName] 209 | 210 | // access a given message's encode / decode functions 211 | var encoder = message.encode 212 | var decode = message.decode 213 | ``` 214 | 215 | --- 216 | 217 | ### .emit(event, message, [callback]) 218 | Messages can be emitted from HyperEmitter using the `.emit()` method. This method takes the name of the 219 | event to be emitted and validates `message` against the schema before sending it off to any listening 220 | subscribers, in parallel. Once complete the `callback` function is called, if present. 221 | 222 | * ##### _event_ 223 | The name of one of the message definitions from the provided schema. 224 | 225 | * ##### _message_ 226 | Any object who's shape matches the named event. It's keys are validated against the schema. 227 | 228 | * ##### _callback_ 229 | An optional function that will be called once the emitted message has been added to the log. 230 | 231 | --- 232 | 233 | ### .registerCodec(name, codec | codecs) 234 | Custom codecs can be registered as long as they have both an `encode` and `decode` method. Codecs 235 | are keyed by message name. For ease of use registration params can be provided as args (name, 236 | codec), an object, or an array of `{name: '', codec: obj}`. Only once codec can be registered against one message at any given time. 237 | 238 | * ##### _name_ 239 | The name of the message message this codec handles. 240 | 241 | * ##### _codec_ 242 | Any object which has an `encode` and `decode` method. 243 | 244 | * ##### _codecs_ 245 | An object or array which represents a collection of codecs and their names. 246 | 247 | --- 248 | 249 | 250 | ### .on(event, callback(message[, done])) 251 | Subscribes to and provides a function to be called each time and event is raised. 252 | 253 | * ##### _event_ 254 | The name of the event being subscribed to. 255 | 256 | * ##### _callback_ 257 | The function to be called when a new event is raised. The `message` arg holds the message emitted. The 258 | `done` arg can be used for letting the emitter know when the function has completed the handling of 259 | the event. 260 | 261 | --- 262 | 263 | 264 | ### .removeListener(event, callback(message[, done])) 265 | Removes the listener who matches the one provided. This method does not work with anonymous functions, only a 266 | function with a prior reference can be removed. 267 | 268 | * ##### _event_ 269 | The name of the event the listener is subscribed to. 270 | 271 | * ##### _callback_ 272 | The reference of the function originally used in the `.on` call. 273 | 274 | --- 275 | 276 | 277 | ### .connect(port, host[, done]) 278 | Connects this HyperEmitter with another one which we call a peer. Peers can exist on other machines, HyperEmitter 279 | will communicate over TCP using the `host` and `port` provided. An optional function can be provided that will be 280 | called once the connection has been made. 281 | 282 | * ##### _port_ 283 | The port of the machine the peer to connect to resides on 284 | 285 | * ##### _host_ 286 | The host of the machine the peer to connect to resides on. 287 | 288 | * ##### _done_ 289 | A function to be called when connected. 290 | 291 | --- 292 | 293 | 294 | ### .listen(port[, host[, done]]) 295 | Listen on a given port/host combination. An `EventPeer` event will be 296 | emitter. 297 | 298 | * ##### _port_ 299 | The port to listen on. 300 | 301 | * ##### _host_ 302 | The host to listen on. 303 | 304 | * ##### _done_ 305 | An optional function to be called one listening has begun. 306 | 307 | --- 308 | 309 | 310 | ### .stream([opts]) 311 | A Duplex stream to emit and receive events. 312 | 313 | * ##### _opts_ 314 | An optional object of settings. 315 | 316 | * `from:` The point in the stream to return events since. supports 'beginning' 317 | 318 | --- 319 | 320 | 321 | ### .close(callback) 322 | Close a given HyperEmitter. After, all `emit` will return an error. 323 | 324 | * ##### _callback_ 325 | 326 | An optional function to be called when close has completed. 327 | 328 | 329 | --- 330 | 331 | ## Contributing 332 | HyperEmitter is a mad science project and like all mad science project it requires mad scientists, the more 333 | the merrier! If you feel you can help in any way, be it with examples, extra testing, or new features please 334 | be our guest. See our [Contribution Guide](./CONTRIBUTING.md) for information on obtaining the source and an overview of the 335 | tooling used. 336 | 337 | [![js*standard-style](https://raw.githubusercontent.com/feross/standard/master/badge.png)](https://github.com/feross/standard) 338 | 339 | ## License 340 | 341 | Copyright Matteo Collina 2015, Licensed under [ISC](./LICENSE) 342 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var memdb = require('memdb') 4 | var pump = require('pump') 5 | var level = require('level') 6 | var fs = require('fs') 7 | var vm = require('vm') 8 | var minimist = require('minimist') 9 | var repl = require('repl') 10 | var ndjson = require('ndjson') 11 | var argv = minimist(process.argv.splice(2), { 12 | string: ['host', 'port', 'targetHost', 'db'], 13 | boolean: ['help', 'repl'], 14 | alias: { 15 | 'targetHost': 'target-host', 16 | 'targetPort': 'target-port', 17 | 'fromScratch': 'from-scratch', 18 | 'help': 'h' 19 | }, 20 | default: { 21 | host: 'localhost', 22 | targetHost: 'localhost', 23 | fromScratch: false, 24 | repl: true 25 | } 26 | }) 27 | 28 | function usage () { 29 | console.log('Usage: hypem [--schema SCHEMA] [--port PORT] [--host HOST]\n' + 30 | ' [--target-host HOST] [--target-port PORT]\n' + 31 | ' [--db PATH] [--no-repl] [--from-scratch]') 32 | } 33 | 34 | if (argv.help) { 35 | usage() 36 | process.exit(1) 37 | } 38 | 39 | var messages = null 40 | if (argv._[0]) { 41 | messages = fs.readFileSync(argv._[0]) 42 | } else if (argv.schema) { 43 | messages = fs.readFileSync(argv.schema) 44 | } else { 45 | console.error('Missing schema') 46 | console.log() 47 | usage() 48 | process.exit(1) 49 | } 50 | 51 | var db = argv.db ? level(argv.db) : memdb() 52 | var hyper = require('./')(db, messages) 53 | var start = argv.repl ? startREPL : startStream 54 | 55 | if (argv.port) { 56 | hyper.listen(argv.port, argv.host, function (err, bound) { 57 | if (err) { 58 | throw err 59 | } 60 | 61 | if (argv.repl) { 62 | console.log('listening on', bound.port, bound.address) 63 | } 64 | 65 | connect(start) 66 | }) 67 | } else { 68 | connect(start) 69 | } 70 | 71 | function connect (next) { 72 | if (argv.targetHost && argv.targetPort) { 73 | hyper.connect(argv.targetPort, argv.targetHost, function (err) { 74 | if (err) { 75 | throw err 76 | } 77 | 78 | if (argv.repl) { 79 | console.log('connected to', argv.targetHost, argv.targetPort) 80 | } 81 | 82 | next() 83 | }) 84 | } else { 85 | next() 86 | } 87 | } 88 | 89 | function startREPL (err) { 90 | if (err) { 91 | throw err 92 | } 93 | 94 | var instance = repl.start({ 95 | ignoreUndefined: true, 96 | eval: noOutputEval, 97 | input: process.stdin, 98 | output: process.stdout 99 | }) 100 | 101 | instance.context.hyper = hyper 102 | 103 | Object.keys(hyper.messages).map(function (key) { 104 | return hyper.messages[key] 105 | }).forEach(function (message) { 106 | hyper.on(message.name, function (msg) { 107 | instance.inputStream.write('\n') 108 | console.log(message.name, msg) 109 | 110 | // undocumented function in node and io 111 | instance.displayPrompt() 112 | }) 113 | }) 114 | 115 | instance.on('exit', function () { 116 | process.exit(0) 117 | }) 118 | } 119 | 120 | function noOutputEval (cmd, context, filename, callback) { 121 | var err 122 | 123 | if (cmd === '(\n)') { 124 | return callback(null, undefined) 125 | } 126 | 127 | try { 128 | var script = vm.createScript(cmd, { 129 | filename: filename, 130 | displayErrors: false 131 | }) 132 | } catch (e) { 133 | console.log('parse error', e) 134 | err = e 135 | } 136 | 137 | if (!err) { 138 | try { 139 | script.runInContext(context, { displayErrors: false }) 140 | } catch (e) { 141 | err = e 142 | } 143 | } 144 | 145 | callback(err, undefined) 146 | } 147 | 148 | function startStream () { 149 | var opts = argv.fromScratch ? { from: 'beginning' } : null 150 | var stream = hyper.stream(opts) 151 | 152 | // input pipeline 153 | pump( 154 | process.stdin, 155 | ndjson.parse(), 156 | stream 157 | ) 158 | 159 | // output pipeline 160 | pump( 161 | stream, 162 | ndjson.serialize(), 163 | process.stdout 164 | ) 165 | } 166 | -------------------------------------------------------------------------------- /codecs.proto: -------------------------------------------------------------------------------- 1 | message Event { 2 | required string name = 1; 3 | required bytes payload = 2; 4 | } 5 | 6 | message EventPeer { 7 | required string id = 1; 8 | repeated PeerAddress addresses = 2; 9 | 10 | message PeerAddress { 11 | required string ip = 1; 12 | required int32 port = 2; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic-example.js: -------------------------------------------------------------------------------- 1 | // - Basic Example 2 | // A bare minimum example of two subscribers 3 | // handling some messages. 4 | 5 | 'use strict' 6 | 7 | // Needed to read in the example schema. 8 | var fs = require('fs') 9 | var path = require('path') 10 | 11 | // The emitter itself as well as an in memory 12 | // leveldb based store 13 | var HyperEmitter = require('../hyperemitter') 14 | var buildDB = require('memdb') 15 | 16 | // Create a new emitter using an in memory leveldb 17 | // use the example-schema.proto as the message schema. 18 | var schema = fs.readFileSync(path.join('.', 'example-schema.proto')) 19 | var emitter = new HyperEmitter(buildDB('a'), schema) 20 | 21 | // Listen on port 9001, ensure no connection error. 22 | emitter.listen(9901, function (err) { 23 | if (err) return 24 | 25 | // Messages must match definitions 26 | // added to the emitter earlier. 27 | var userAddedMsg = { 28 | id: 1, 29 | username: 'user' 30 | } 31 | 32 | // Messages are simple objects. 33 | var userRemovedMsg = { 34 | id: 1 35 | } 36 | 37 | // Subscribe and handle any messages that match 38 | // the userAdded definition from the schema. 39 | emitter.on('userAdded', function (msg) { 40 | console.log('userAdded: ', msg) 41 | }) 42 | 43 | // Subscribe and handle any messages that match 44 | // the userRemoved definition from the schema. 45 | emitter.on('userRemoved', function (msg) { 46 | console.log('userRemoved', msg) 47 | }) 48 | 49 | // Emit both messages above. 50 | emitter.emit('userAdded', userAddedMsg) 51 | emitter.emit('userRemoved', userRemovedMsg) 52 | 53 | // Clean up the emitter. 54 | function complete () { 55 | emitter.close() 56 | } 57 | 58 | // We will wait for 500ms to 59 | // let the program run/ 60 | setTimeout(complete, 500) 61 | }) 62 | -------------------------------------------------------------------------------- /examples/emit-callback-needs-subscriber.js: -------------------------------------------------------------------------------- 1 | // - Emit Callback Needs Subscriber 2 | // This example demonstrates that an emit's callback 3 | // is only called when there is a subscriber present. 4 | 5 | 'use strict' 6 | 7 | // Needed to read in the example schema. 8 | var fs = require('fs') 9 | var path = require('path') 10 | 11 | // The emitter itself as well as an in memory 12 | // leveldb based store 13 | var HyperEmitter = require('../hyperemitter') 14 | var buildDB = require('memdb') 15 | 16 | // create a new emitter using an in memory leveldb 17 | // use the exampleSchema.proto as the message schema. 18 | var schema = fs.readFileSync(path.join('.', 'example-schema.proto')) 19 | var emitter = new HyperEmitter(buildDB('a'), schema) 20 | 21 | // listen on port 9001, ensure no connection error. 22 | emitter.listen(9901, function (err) { 23 | if (err) { 24 | return 25 | } 26 | 27 | // a simple obj that matches one example-schema.proto, 28 | // note only the id and username fields are required. 29 | var userAddedMsg = { 30 | id: 1, 31 | username: 'user' 32 | } 33 | 34 | // We have a subscriber embeded in the callback, if we had an external 35 | // subscriber this code would work but because no other subscribers or 36 | // emitter with subscribers the callback is never called. 37 | emitter.emit('userAdded', userAddedMsg, function () { 38 | emitter.on('userAdded', function (msg) { 39 | console.log('userAdded: ', msg) 40 | }) 41 | }) 42 | 43 | // Clean up the emitter and print a complete message 44 | function complete () { 45 | console.log('example complete') 46 | emitter.close() 47 | } 48 | 49 | // After 1000ms the subscriber will still not be called. 50 | setTimeout(complete, 1000) 51 | }) 52 | -------------------------------------------------------------------------------- /examples/example-schema.proto: -------------------------------------------------------------------------------- 1 | message userAdded { 2 | required int32 id = 1; 3 | required string username = 2; 4 | optional string name = 3; 5 | optional string surname = 4; 6 | } 7 | 8 | message userRemoved { 9 | required int32 id = 1; 10 | } 11 | -------------------------------------------------------------------------------- /examples/multiple-connected-emitters.js: -------------------------------------------------------------------------------- 1 | // - Multiple Connected Emitters 2 | // This example demonstrates connecting 3 | // multiple emitters together. 4 | 5 | 'use strict' 6 | 7 | // Needed to read in the example schema. 8 | var fs = require('fs') 9 | var path = require('path') 10 | 11 | // The emitter itself as well as an in memory 12 | // leveldb based store, any leveldb store will do. 13 | var HyperEmitter = require('../hyperemitter') 14 | var buildDB = require('memdb') 15 | 16 | // use the example-schema.proto as the message schema. 17 | var schema = fs.readFileSync(path.join('.', 'example-schema.proto')) 18 | 19 | // two emmiters will be used for this example, notice each 20 | // maintains it's own leveldb store and share the same schema. 21 | var emitterOne = new HyperEmitter(buildDB('a'), schema) 22 | var emitterTwo = new HyperEmitter(buildDB('b'), schema) 23 | 24 | // listen on port 9001, ensure no connection error. 25 | emitterOne.listen(9901, function (err) { 26 | if (err) { 27 | return 28 | } 29 | 30 | // connect to the first emitter. 31 | emitterTwo.connect(9901, '127.0.0.1', function (err) { 32 | if (err) { 33 | return 34 | } 35 | }) 36 | 37 | // basic message type 38 | var userAddedMsg = { 39 | id: 1, 40 | username: 'user' 41 | } 42 | 43 | // basic message type 44 | var userRemovedMsg = { 45 | id: 1 46 | } 47 | 48 | // Messages sent on either emitter will be handled. 49 | emitterOne.on('userRemoved', function (msg) { 50 | console.log('userRemoved: ', msg) 51 | }) 52 | 53 | // Messages sent on either emitter will be handled. 54 | emitterTwo.on('userAdded', function (msg) { 55 | console.log('userAdded: ', msg) 56 | }) 57 | 58 | // We send each message across the opposite emmiter. 59 | emitterOne.emit('userAdded', userAddedMsg) 60 | emitterTwo.emit('userRemoved', userRemovedMsg) 61 | 62 | // call count will be 2. 63 | function complete () { 64 | emitterOne.close() 65 | emitterTwo.close() 66 | } 67 | 68 | // we will wait for 500ms to see if more than one 69 | // message is delivered to the subscribers above. 70 | setTimeout(complete, 500) 71 | }) 72 | -------------------------------------------------------------------------------- /examples/subscriber-synchronisation.js: -------------------------------------------------------------------------------- 1 | // - Subscribers Synchronisation 2 | // This example demonstrates that messages are sent to 3 | // all subscribers regardless of when the connect. 4 | 5 | 'use strict' 6 | 7 | // Needed to read in the example schema. 8 | var fs = require('fs') 9 | var path = require('path') 10 | 11 | // The emitter itself as well as an in memory 12 | // leveldb based store. any leveldb store will do. 13 | var HyperEmitter = require('../hyperemitter') 14 | var buildDB = require('memdb') 15 | 16 | // create a new emitter using an in memory leveldb 17 | // use the example-schema.proto as the message schema. 18 | var schema = fs.readFileSync(path.join('.', 'example-schema.proto')) 19 | var emitter = new HyperEmitter(buildDB('a'), schema) 20 | 21 | // listen on port 9001, ensure no connection error. 22 | emitter.listen(9901, function (err) { 23 | if (err) { 24 | return 25 | } 26 | 27 | // a simple obj that matches one example.Schema.proto, 28 | // note only the id and username fields are required. 29 | var userAddedMsg = { 30 | id: 1, 31 | username: 'user' 32 | } 33 | 34 | // Add the first handler. The second handler would 35 | // not be called if this wasn't here. There needs 36 | // to be at least one subscriber to have emit's 37 | // callback raised. 38 | emitter.on('userAdded', function (msg) { 39 | console.log('userAdded: ', msg) 40 | }) 41 | 42 | // Even though the second subsciber is added only after 43 | // emit is called it still gets the messages once live. 44 | emitter.emit('userAdded', userAddedMsg, function () { 45 | emitter.on('userAdded', function (msg) { 46 | console.log('userAdded: ', msg) 47 | }) 48 | }) 49 | 50 | // Cleanup 51 | function complete () { 52 | emitter.close() 53 | } 54 | 55 | // We will wait for 500ms to see if more than one 56 | // message is delivered to the subscriber above. 57 | setTimeout(complete, 500) 58 | }) 59 | -------------------------------------------------------------------------------- /hyperemitter.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | var hyperlog = require('hyperlog') 3 | var net = require('net') 4 | var cuid = require('cuid') 5 | var os = require('os') 6 | var pump = require('pump') 7 | var fastparallel = require('fastparallel') 8 | var eos = require('end-of-stream') 9 | var bulkws = require('bulk-write-stream') 10 | var through2 = require('through2') 11 | var xtend = require('xtend') 12 | var duplexify = require('duplexify') 13 | var deepEqual = require('deep-equal') 14 | var noop = function () {} 15 | 16 | var STOREID = '!!STOREID!!' 17 | var PEERS = '!!PEERS!!' 18 | var MYEVENTPEER = '!!MYEVENTPEER!!' 19 | var defaults = { 20 | reconnectTimeout: 1000 21 | } 22 | 23 | var fs = require('fs') 24 | var path = require('path') 25 | var protobuf = require('protocol-buffers') 26 | var coreCodecs = protobuf(fs.readFileSync(path.join(__dirname, 'codecs.proto'))) 27 | 28 | function initializeCodecs (codecs) { 29 | codecs = codecs || [] 30 | 31 | if (Buffer.isBuffer(codecs) || typeof codecs === 'string') { 32 | codecs = protobuf(codecs) 33 | } 34 | 35 | Object.keys(coreCodecs).forEach(function (name) { 36 | codecs[name] = coreCodecs[name] 37 | }) 38 | 39 | return codecs 40 | } 41 | 42 | function createChangeStream () { 43 | var readStream = this._hyperlog.createReadStream({ 44 | since: this._hyperlog.changes, 45 | live: true 46 | }) 47 | 48 | return pump(readStream, bulkws.obj(processStream.bind(this))) 49 | } 50 | 51 | function processStream (changes, next) { 52 | var that = this 53 | 54 | that._parallel(that, publish, changes, next) 55 | } 56 | 57 | function publish (change, done) { 58 | var container = this.codecs.Event.decode(change.value) 59 | var name = container.name 60 | var decoder = this.codecs[name] 61 | var event = container.payload 62 | 63 | if (decoder) event = decoder.decode(event) 64 | this._parallel(this, this._listeners[name] || [], event, done) 65 | } 66 | 67 | function createServer () { 68 | var that = this 69 | 70 | this._server = net.createServer(function (peerStream) { 71 | that.status.emit('peerConnected') 72 | 73 | var peerId = that._lastPeerId++ 74 | var localStream = that._hyperlog.replicate({live: true}) 75 | var boundStreams = pump(peerStream, localStream, peerStream, function (err) { 76 | if (err) that.status.emit('peerError', err) 77 | else delete that._peers[peerId] 78 | }) 79 | 80 | that._peers[peerId] = boundStreams 81 | }) 82 | } 83 | 84 | function getLocalAddresses () { 85 | var ifaces = os.networkInterfaces() 86 | return Object.keys(ifaces).reduce(function (addresses, iface) { 87 | return ifaces[iface].filter(function (ifaceIp) { 88 | return !ifaceIp.internal 89 | }).reduce(function (addresses, ifaceIp) { 90 | addresses.push(ifaceIp) 91 | return addresses 92 | }, addresses) 93 | }, []) 94 | } 95 | 96 | function connectToPeer (that, port, host, tries, callback) { 97 | var stream = net.connect(port, host) 98 | var key = host + ':' + port 99 | 100 | if (that._peers[key]) { 101 | return callback ? callback() : undefined 102 | } 103 | 104 | that._peers[key] = stream 105 | 106 | stream.on('connect', function () { 107 | var replicate = that._hyperlog.replicate({ live: true }) 108 | pump(replicate, stream, replicate) 109 | 110 | var peers = Object.keys(that._peers).map(function (key) { 111 | var split = key.split(':') 112 | return { address: split[0], port: split[1] } 113 | }) 114 | 115 | that._db.put(PEERS, JSON.stringify(peers), function (err) { 116 | if (callback) { 117 | callback(err) 118 | callback = null 119 | } 120 | }) 121 | }) 122 | 123 | eos(stream, function (err) { 124 | delete that._peers[key] 125 | if (err) { 126 | that.status.emit('connectionError', err, stream) 127 | if (!that._closed && tries < 10) { 128 | setTimeout(function () { 129 | connectToPeer(that, port, host, tries + 1, callback) 130 | }, that._opts.reconnectTimeout) 131 | } else { 132 | return callback ? callback(err) : undefined 133 | } 134 | } 135 | }) 136 | } 137 | 138 | function connectToKnownPeers () { 139 | var that = this 140 | 141 | this._db.get(PEERS, function (err, peers) { 142 | if (err && err.notFound) return 143 | if (err) return that.status.emit('error', err) 144 | 145 | function connectToPeer (peer, next) { 146 | that.connect(peer.port, peer.address, next) 147 | } 148 | 149 | that._parallel(that, connectToPeer, JSON.parse(peers), noop) 150 | }) 151 | } 152 | 153 | function handleNewPeers () { 154 | var that = this 155 | 156 | this.on('EventPeer', function (peer, callback) { 157 | var port = peer.addresses[0].port 158 | var address = peer.addresses[0].ip 159 | 160 | if (peer.id !== peer.id) that.connect(port, address, callback) 161 | else callback() 162 | }) 163 | } 164 | 165 | function destroyOrClose (resource, callback) { 166 | if (resource.destroy) { 167 | resource.destroy() 168 | setImmediate(callback) 169 | } else { 170 | resource.close(callback) 171 | } 172 | } 173 | 174 | function HyperEmitter (db, codecs, opts) { 175 | if (!(this instanceof HyperEmitter)) { 176 | return new HyperEmitter(db, codecs, opts) 177 | } 178 | 179 | this._opts = xtend(defaults, opts) 180 | this._parallel = fastparallel({ results: false }) 181 | this._db = db 182 | this._hyperlog = hyperlog(db) 183 | 184 | this._closed = false 185 | this._listening = false 186 | 187 | this._peers = {} 188 | this._lastPeerId = 0 189 | this._listeners = {} 190 | 191 | this.status = new EventEmitter() 192 | this.codecs = initializeCodecs(codecs) 193 | this.messages = this.codecs 194 | 195 | createServer.call(this) 196 | connectToKnownPeers.call(this) 197 | handleNewPeers.call(this) 198 | 199 | var that = this 200 | this._hyperlog.ready(function () { 201 | if (that._closed) return 202 | 203 | that.changeStream = createChangeStream.call(that) 204 | that.changes = that.changeStream 205 | that.status.emit('ready') 206 | }) 207 | } 208 | 209 | HyperEmitter.prototype.emit = function (name, data, callback) { 210 | var encoder = this.codecs[name] 211 | 212 | if (encoder) data = encoder.encode(data) 213 | var container = this.codecs.Event.encode({ 214 | name: name, 215 | payload: data 216 | }) 217 | 218 | this._hyperlog.append(container, callback) 219 | 220 | return this 221 | } 222 | 223 | HyperEmitter.prototype.on = function (name, handler) { 224 | var toInsert = handler 225 | 226 | if (toInsert.length < 2) { 227 | toInsert = function (msg, callback) { 228 | handler(msg) 229 | callback() 230 | } 231 | 232 | handler.wrapped = toInsert 233 | } 234 | 235 | this._listeners[name] = this._listeners[name] || [] 236 | this._listeners[name].push(toInsert) 237 | 238 | return this 239 | } 240 | 241 | HyperEmitter.prototype.registerCodec = function (name, codec) { 242 | if (typeof name === 'string') { 243 | this.codecs[name] = codec 244 | return this 245 | } 246 | 247 | var codecs = name 248 | var that = this 249 | 250 | if (Array.isArray(codecs)) { 251 | codecs.forEach(function (element) { 252 | that.codecs[element.name] = element.codec 253 | }) 254 | return that 255 | } 256 | 257 | if (typeof codecs === 'object') { 258 | Object.keys(codecs).forEach(function (name) { 259 | that.codecs[name] = codecs[name] 260 | }) 261 | return that 262 | } 263 | 264 | return this 265 | } 266 | 267 | HyperEmitter.prototype.removeListener = function (name, func) { 268 | if (func.wrapped) { 269 | func = func.wrapped 270 | } 271 | 272 | this._listeners[name].splice(this._listeners[name].indexOf(func), 1) 273 | return this 274 | } 275 | 276 | HyperEmitter.prototype.getId = function (callback) { 277 | if (this.id) { return callback(null, this.id) } 278 | 279 | var that = this 280 | var db = this._db 281 | 282 | db.get(STOREID, function (err, value) { 283 | if (err && !err.notFound) { return callback(err) } 284 | that.id = value || cuid() 285 | db.put(STOREID, that.id, function (err) { 286 | if (err) { 287 | return callback(err) 288 | } 289 | callback(null, that.id) 290 | }) 291 | }) 292 | } 293 | 294 | HyperEmitter.prototype.connect = function (port, host, callback) { 295 | connectToPeer(this, port, host, 1, callback) 296 | } 297 | 298 | HyperEmitter.prototype.listen = function (port, address, callback) { 299 | var that = this 300 | 301 | if (typeof address === 'function') { 302 | callback = address 303 | address = null 304 | } 305 | 306 | this._listening = true 307 | 308 | this.getId(function (err, id) { 309 | if (err) { 310 | return callback(err) 311 | } 312 | 313 | that._server.listen(port, address, function (err) { 314 | if (err) { 315 | return callback(err) 316 | } 317 | 318 | var addresses = address ? [{ address: address }] : getLocalAddresses() 319 | 320 | addresses = addresses.map(function (ip) { 321 | return { 322 | ip: ip.address, 323 | port: that._server.address().port 324 | } 325 | }) 326 | 327 | var toStore = { 328 | id: id, 329 | addresses: addresses 330 | } 331 | 332 | that._db.get(MYEVENTPEER, { valueEncoding: 'json' }, function (err, value) { 333 | if (err && !err.notFound) { 334 | return callback(err) 335 | } 336 | 337 | if (deepEqual(value, toStore)) { 338 | return callback(null, that._server.address()) 339 | } 340 | 341 | that.emit('EventPeer', toStore, function (err) { 342 | if (err) { return callback(err) } 343 | 344 | that._db.put(MYEVENTPEER, JSON.stringify(toStore), function (err) { 345 | if (err) { return callback(err) } 346 | 347 | callback(null, that._server.address()) 348 | }) 349 | }) 350 | }) 351 | }) 352 | }) 353 | } 354 | 355 | HyperEmitter.prototype.stream = function (opts) { 356 | var that = this 357 | var result = duplexify.obj() 358 | var input = through2.obj(function (chunk, enc, next) { 359 | that.emit(chunk.name, chunk.payload, next) 360 | }) 361 | 362 | result.setWritable(input) 363 | 364 | that._hyperlog.ready(function () { 365 | var filter = through2.obj(function (change, enc, next) { 366 | var container = that.codecs.Event.decode(change.value) 367 | var name = container.name 368 | var decoder = that.codecs[name] 369 | var event = container.payload 370 | 371 | if (decoder) event = decoder.decode(event) 372 | 373 | this.push({ 374 | name: name, 375 | payload: event 376 | }) 377 | 378 | next() 379 | }) 380 | 381 | var since = opts && opts.from === 'beginning' ? 0 : that._hyperlog.changes 382 | 383 | pump(that._hyperlog.createReadStream({ 384 | since: since, 385 | live: true 386 | }), filter) 387 | 388 | result.setReadable(filter) 389 | }) 390 | 391 | return result 392 | } 393 | 394 | HyperEmitter.prototype.close = function (callback) { 395 | var resources = [this._db] 396 | 397 | if (this.changeStream) resources.push(this.changeStream) 398 | if (this._listening) resources.push(this._server) 399 | 400 | var that = this 401 | Object.keys(this._peers).forEach(function (peerId) { 402 | resources.unshift(that._peers[peerId]) 403 | }) 404 | 405 | this._closed = true 406 | this._parallel(this, destroyOrClose, resources, callback || noop) 407 | } 408 | 409 | module.exports = HyperEmitter 410 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperemitter", 3 | "version": "1.3.0", 4 | "description": "Horizontally Scalable EventEmitter powered by a Merkle DAG", 5 | "main": "hyperemitter.js", 6 | "scripts": { 7 | "test": "tape test/test.js | faucet", 8 | "lint": "standard" 9 | }, 10 | "bin": { 11 | "hypem": "./bin.js" 12 | }, 13 | "pre-commit": [ 14 | "lint", 15 | "test" 16 | ], 17 | "keywords": [ 18 | "event", 19 | "store", 20 | "eventstore", 21 | "emitter", 22 | "merkle", 23 | "dag" 24 | ], 25 | "author": "Matteo Collina ", 26 | "contributors": [ 27 | "Dean McDonnell " 28 | ], 29 | "license": "ISC", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/mcollina/hyperemitter.git" 33 | }, 34 | "devDependencies": { 35 | "faucet": "0.0.1", 36 | "pre-commit": "^1.0.6", 37 | "standard": "^5.1.0", 38 | "tape": "^4.2.0" 39 | }, 40 | "dependencies": { 41 | "bulk-write-stream": "^1.0.0", 42 | "cuid": "^1.2.4", 43 | "deep-equal": "^1.0.0", 44 | "duplexify": "^3.3.0", 45 | "end-of-stream": "^1.1.0", 46 | "fastparallel": "^1.5.0", 47 | "hyperlog": "^4.5.0", 48 | "level": "^1.3.0", 49 | "memdb": "^1.0.1", 50 | "minimist": "^1.1.1", 51 | "ndjson": "^1.3.0", 52 | "protocol-buffers": "^3.0.0", 53 | "pump": "^1.0.0", 54 | "through2": "^2.0.0", 55 | "xtend": "^4.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/fixture/basic.proto: -------------------------------------------------------------------------------- 1 | message Test1 { 2 | optional string foo = 1; 3 | required int32 num = 2; 4 | } 5 | 6 | message Test2 { 7 | optional string bar = 1; 8 | required int32 id = 2; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/messages.proto: -------------------------------------------------------------------------------- 1 | message Hello { 2 | optional string from = 1; 3 | optional string message = 2; 4 | } 5 | 6 | message Integer { 7 | optional int32 value = 1; 8 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var HyperEmitter = require('../hyperemitter') 3 | var protobuf = require('protocol-buffers') 4 | var memdb = require('memdb') 5 | 6 | var fs = require('fs') 7 | var path = require('path') 8 | var basicProto = fs.readFileSync(path.join(__dirname, 'fixture', 'basic.proto')) 9 | 10 | test('standalone works', function (t) { 11 | t.plan(5) 12 | 13 | var emitter = new HyperEmitter(memdb(), basicProto) 14 | 15 | var test1 = { 16 | foo: 'hello', 17 | num: 42 18 | } 19 | 20 | var test2 = { 21 | bar: 'world', 22 | id: 23 23 | } 24 | 25 | var count = 2 26 | 27 | emitter.emit('Test1', test1, function (err) { 28 | t.error(err, 'no error') 29 | }) 30 | 31 | emitter.emit('Test2', test2, function (err) { 32 | t.error(err, 'no error') 33 | }) 34 | 35 | emitter.on('Test1', function (msg) { 36 | t.deepEqual(msg, test1, 'Test1 event matches') 37 | release() 38 | }) 39 | 40 | emitter.on('Test2', function (msg, cb) { 41 | t.deepEqual(msg, test2, 'Test2 event matches') 42 | 43 | // second argument can be a function, backpressure is supported 44 | cb() 45 | release() 46 | }) 47 | 48 | function release () { 49 | if (--count === 0) { 50 | emitter.close(t.pass.bind(t, 'closed successfully')) 51 | } 52 | } 53 | }) 54 | 55 | test('using registerCodec works', function (t) { 56 | t.plan(5) 57 | 58 | var codecs = protobuf(basicProto) 59 | var emitter = new HyperEmitter(memdb()) 60 | 61 | emitter.registerCodec('Test1', codecs.Test1) 62 | .registerCodec('Test2', codecs.Test2) 63 | 64 | var test1 = { 65 | foo: 'hello', 66 | num: 42 67 | } 68 | 69 | var test2 = { 70 | bar: 'world', 71 | id: 23 72 | } 73 | 74 | var count = 2 75 | 76 | emitter.emit('Test1', test1, function (err) { 77 | t.error(err, 'no error') 78 | }) 79 | 80 | emitter.emit('Test2', test2, function (err) { 81 | t.error(err, 'no error') 82 | }) 83 | 84 | emitter.on('Test1', function (msg) { 85 | t.deepEqual(msg, test1, 'Test1 event matches') 86 | release() 87 | }) 88 | 89 | emitter.on('Test2', function (msg) { 90 | t.deepEqual(msg, test2, 'Test2 event matches') 91 | release() 92 | }) 93 | 94 | function release () { 95 | if (--count === 0) { 96 | emitter.close(t.pass.bind(t, 'closed successfully')) 97 | } 98 | } 99 | }) 100 | 101 | test('registerCodec supports objects', function (t) { 102 | t.plan(5) 103 | 104 | var codecs = protobuf(basicProto) 105 | var emitter = new HyperEmitter(memdb()) 106 | 107 | emitter.registerCodec(codecs) 108 | 109 | var test1 = { 110 | foo: 'hello', 111 | num: 42 112 | } 113 | 114 | var test2 = { 115 | bar: 'world', 116 | id: 23 117 | } 118 | 119 | var count = 2 120 | 121 | emitter.emit('Test1', test1, function (err) { 122 | t.error(err, 'no error') 123 | }) 124 | 125 | emitter.emit('Test2', test2, function (err) { 126 | t.error(err, 'no error') 127 | }) 128 | 129 | emitter.on('Test1', function (msg) { 130 | t.deepEqual(msg, test1, 'Test1 event matches') 131 | release() 132 | }) 133 | 134 | emitter.on('Test2', function (msg) { 135 | t.deepEqual(msg, test2, 'Test2 event matches') 136 | release() 137 | }) 138 | 139 | function release () { 140 | if (--count === 0) { 141 | emitter.close(t.pass.bind(t, 'closed successfully')) 142 | } 143 | } 144 | }) 145 | 146 | test('registerCodec supports arrays', function (t) { 147 | t.plan(5) 148 | 149 | var codecs = protobuf(basicProto) 150 | var emitter = new HyperEmitter(memdb()) 151 | 152 | emitter.registerCodec([ 153 | {name: 'Test1', codec: codecs.Test1}, 154 | {name: 'Test2', codec: codecs.Test2} 155 | ]) 156 | 157 | var test1 = { 158 | foo: 'hello', 159 | num: 42 160 | } 161 | 162 | var test2 = { 163 | bar: 'world', 164 | id: 23 165 | } 166 | 167 | var count = 2 168 | 169 | emitter.emit('Test1', test1, function (err) { 170 | t.error(err, 'no error') 171 | }) 172 | 173 | emitter.emit('Test2', test2, function (err) { 174 | t.error(err, 'no error') 175 | }) 176 | 177 | emitter.on('Test1', function (msg) { 178 | t.deepEqual(msg, test1, 'Test1 event matches') 179 | release() 180 | }) 181 | 182 | emitter.on('Test2', function (msg) { 183 | t.deepEqual(msg, test2, 'Test2 event matches') 184 | release() 185 | }) 186 | 187 | function release () { 188 | if (--count === 0) { 189 | emitter.close(t.pass.bind(t, 'closed successfully')) 190 | } 191 | } 192 | }) 193 | 194 | test('paired works', function (t) { 195 | t.plan(7) 196 | 197 | var emitter1 = new HyperEmitter(memdb(), basicProto) 198 | 199 | var emitter2 = new HyperEmitter(memdb(), basicProto) 200 | 201 | emitter1.listen(9901, function (err) { 202 | t.error(err, 'no error') 203 | 204 | emitter2.connect(9901, '127.0.0.1', function (err) { 205 | t.error(err, 'no error') 206 | }) 207 | }) 208 | 209 | var test1 = { 210 | foo: 'hello', 211 | num: 42 212 | } 213 | 214 | var test2 = { 215 | bar: 'world', 216 | id: 23 217 | } 218 | 219 | var count = 2 220 | 221 | emitter2.on('Test1', function (msg) { 222 | t.deepEqual(msg, test1, 'Test1 event matches') 223 | release() 224 | }) 225 | 226 | emitter1.on('Test2', function (msg) { 227 | t.deepEqual(msg, test2, 'Test2 event matches') 228 | release() 229 | }) 230 | 231 | emitter1.emit('Test1', test1, function (err) { 232 | t.error(err, 'no error') 233 | }) 234 | 235 | emitter2.emit('Test2', test2, function (err) { 236 | t.error(err, 'no error') 237 | }) 238 | 239 | function release () { 240 | if (--count === 0) { 241 | emitter1.close(function () { 242 | emitter2.close(t.pass.bind(t, 'closed successfully')) 243 | }) 244 | } 245 | } 246 | }) 247 | 248 | test('three way works', function (t) { 249 | t.plan(9) 250 | 251 | var emitter1 = new HyperEmitter(memdb(), basicProto) 252 | 253 | var emitter2 = new HyperEmitter(memdb(), basicProto) 254 | 255 | var emitter3 = new HyperEmitter(memdb(), basicProto) 256 | 257 | emitter1.listen(9901, '127.0.0.1', function (err) { 258 | t.error(err, 'no error') 259 | 260 | emitter2.connect(9901, '127.0.0.1', function (err) { 261 | t.error(err, 'no error') 262 | 263 | emitter2.listen(9902, '127.0.0.1', function (err) { 264 | t.error(err, 'no error') 265 | 266 | emitter3.connect(9902, '127.0.0.1', function (err) { 267 | t.error(err, 'no error') 268 | }) 269 | }) 270 | }) 271 | }) 272 | 273 | var test1 = { 274 | foo: 'hello', 275 | num: 42 276 | } 277 | 278 | var test2 = { 279 | bar: 'world', 280 | id: 23 281 | } 282 | 283 | var count = 2 284 | 285 | emitter3.on('Test1', function (msg) { 286 | t.deepEqual(msg, test1, 'Test1 event matches') 287 | release() 288 | }) 289 | 290 | emitter3.on('Test2', function (msg) { 291 | t.deepEqual(msg, test2, 'Test2 event matches') 292 | release() 293 | }) 294 | 295 | emitter1.emit('Test1', test1, function (err) { 296 | t.error(err, 'no error') 297 | }) 298 | 299 | emitter1.emit('Test2', test2, function (err) { 300 | t.error(err, 'no error') 301 | }) 302 | 303 | function release () { 304 | if (--count === 0) { 305 | emitter1.close(function () { 306 | emitter2.close(function () { 307 | emitter3.close(t.pass.bind(t, 'closed successfully')) 308 | }) 309 | }) 310 | } 311 | } 312 | }) 313 | 314 | test('remove listeners', function (t) { 315 | t.plan(2) 316 | 317 | var emitter = new HyperEmitter(memdb(), basicProto) 318 | 319 | var test1 = { 320 | foo: 'hello', 321 | num: 42 322 | } 323 | 324 | emitter.on('Test1', onEvent) 325 | emitter.removeListener('Test1', onEvent) 326 | 327 | emitter.emit('Test1', test1, function (err) { 328 | t.error(err, 'no error') 329 | emitter.close(t.pass.bind(t, 'closed successfully')) 330 | }) 331 | 332 | function onEvent (msg, cb) { 333 | t.fail('this should never be called') 334 | } 335 | }) 336 | 337 | test('offline peer sync', function (t) { 338 | t.plan(8) 339 | 340 | var emitter1 = new HyperEmitter(memdb(), basicProto) 341 | 342 | var emitter2db = memdb() 343 | 344 | var emitter2 = new HyperEmitter(emitter2db, basicProto) 345 | 346 | emitter1.listen(9901, function (err) { 347 | t.error(err, 'no error') 348 | 349 | emitter2.connect(9901, '127.0.0.1', function (err) { 350 | t.error(err, 'no error') 351 | }) 352 | }) 353 | 354 | var test1 = { 355 | foo: 'hello', 356 | num: 42 357 | } 358 | 359 | var test2 = { 360 | bar: 'world', 361 | id: 23 362 | } 363 | 364 | emitter1.emit('Test1', test1, function (err) { 365 | t.error(err, 'no error') 366 | }) 367 | 368 | var oldClose = emitter2db.close 369 | emitter2db.close = function (cb) { 370 | return cb() 371 | } 372 | 373 | emitter2.on('Test1', function (msg) { 374 | t.deepEqual(msg, test1, 'Test1 event matches') 375 | 376 | emitter2.close(function () { 377 | emitter2db.close = oldClose 378 | emitter2 = new HyperEmitter(emitter2db, basicProto) 379 | 380 | emitter1.emit('Test2', test2, function (err) { 381 | t.error(err, 'no error') 382 | }) 383 | 384 | emitter2.on('Test2', function (msg) { 385 | t.deepEqual(msg, test2, 'Test2 event matches') 386 | 387 | emitter1.close(function () { 388 | emitter2.close(t.pass.bind(t, 'closed successfully')) 389 | }) 390 | }) 391 | 392 | emitter2.connect(9901, '127.0.0.1', function (err) { 393 | t.error(err, 'no error') 394 | }) 395 | }) 396 | }) 397 | }) 398 | 399 | test('offline reconnect', function (t) { 400 | t.plan(7) 401 | 402 | var emitter1 = new HyperEmitter(memdb(), basicProto) 403 | 404 | var emitter2db = memdb() 405 | 406 | var emitter2 = new HyperEmitter(emitter2db, basicProto) 407 | 408 | emitter1.listen(9901, function (err) { 409 | t.error(err, 'no error') 410 | 411 | emitter2.connect(9901, '127.0.0.1', function (err) { 412 | t.error(err, 'no error') 413 | }) 414 | }) 415 | 416 | var test1 = { 417 | foo: 'hello', 418 | num: 42 419 | } 420 | 421 | var test2 = { 422 | bar: 'world', 423 | id: 23 424 | } 425 | 426 | emitter1.emit('Test1', test1, function (err) { 427 | t.error(err, 'no error') 428 | }) 429 | 430 | var oldClose = emitter2db.close 431 | emitter2db.close = function (cb) { 432 | return cb() 433 | } 434 | 435 | emitter2.on('Test1', function (msg) { 436 | t.deepEqual(msg, test1, 'Test1 event matches') 437 | 438 | emitter2.close(function () { 439 | emitter2db.close = oldClose 440 | emitter2 = new HyperEmitter(emitter2db, basicProto) 441 | 442 | emitter1.emit('Test2', test2, function (err) { 443 | t.error(err, 'no error') 444 | }) 445 | 446 | emitter2.on('Test2', function (msg) { 447 | t.deepEqual(msg, test2, 'Test2 event matches') 448 | 449 | emitter1.close(function () { 450 | emitter2.close(t.pass.bind(t, 'closed successfully')) 451 | }) 452 | }) 453 | }) 454 | }) 455 | }) 456 | 457 | test('automatically reconnects', function (t) { 458 | t.plan(7) 459 | 460 | var emitter1 = new HyperEmitter(memdb(), basicProto) 461 | 462 | var emitter2 = new HyperEmitter(memdb(), basicProto, { 463 | reconnectTimeout: 10 464 | }) 465 | 466 | emitter1.listen(9901, function (err) { 467 | t.error(err, 'no error') 468 | 469 | emitter2.connect(9901, '127.0.0.1', function (err) { 470 | t.error(err, 'no error') 471 | }) 472 | }) 473 | 474 | var test1 = { 475 | foo: 'hello', 476 | num: 42 477 | } 478 | 479 | var test2 = { 480 | bar: 'world', 481 | id: 23 482 | } 483 | 484 | emitter1.emit('Test1', test1, function (err) { 485 | t.error(err, 'no error') 486 | }) 487 | 488 | emitter2.on('Test1', function (msg) { 489 | t.deepEqual(msg, test1, 'Test1 event matches') 490 | 491 | // using internal data to fake a connection failure 492 | emitter2._peers['127.0.0.1:9901'].destroy() 493 | 494 | setImmediate(function () { 495 | emitter1.emit('Test2', test2, function (err) { 496 | t.error(err, 'no error') 497 | }) 498 | 499 | emitter2.on('Test2', function (msg) { 500 | t.deepEqual(msg, test2, 'Test2 event matches') 501 | 502 | emitter1.close(function () { 503 | emitter2.close(t.pass.bind(t, 'closed successfully')) 504 | }) 505 | }) 506 | }) 507 | }) 508 | }) 509 | 510 | test('do not re-emit old events', function (t) { 511 | t.plan(3) 512 | 513 | var db = memdb() 514 | var emitter = new HyperEmitter(db, basicProto) 515 | 516 | var test1 = { 517 | foo: 'hello', 518 | num: 42 519 | } 520 | 521 | emitter.emit('Test1', test1, function (err) { 522 | t.error(err, 'no error') 523 | }) 524 | 525 | var oldClose = db.close 526 | db.close = function (cb) { 527 | return cb() 528 | } 529 | 530 | emitter.on('Test1', function (msg) { 531 | t.deepEqual(msg, test1, 'Test1 event matches') 532 | 533 | emitter.close(function () { 534 | db.close = oldClose 535 | emitter = new HyperEmitter(db, basicProto) 536 | 537 | emitter.on('Test1', function () { 538 | t.fail('this should not happen') 539 | }) 540 | 541 | // timeout needed to wait for the Test1 event to 542 | // be eventually emitted 543 | setTimeout(function () { 544 | emitter.close(t.pass.bind(t, 'closed successfully')) 545 | }, 100) 546 | }) 547 | }) 548 | }) 549 | 550 | test('as stream', function (t) { 551 | t.plan(6) 552 | 553 | var emitter = new HyperEmitter(memdb(), basicProto) 554 | var stream = emitter.stream() 555 | 556 | var test1 = { 557 | foo: 'hello', 558 | num: 42 559 | } 560 | 561 | var test2 = { 562 | bar: 'world', 563 | id: 23 564 | } 565 | 566 | var count = 2 567 | 568 | emitter.emit('Test1', test1, function (err) { 569 | t.error(err, 'no error') 570 | 571 | stream.end({ 572 | name: 'Test2', 573 | payload: test2 574 | }, function (err) { 575 | t.error(err, 'no error') 576 | }) 577 | }) 578 | 579 | stream.once('data', function (msg) { 580 | t.deepEqual(msg, { 581 | name: 'Test1', 582 | payload: test1 583 | }, 'Test1 event matches') 584 | 585 | stream.once('data', function (msg) { 586 | t.deepEqual(msg, { 587 | name: 'Test2', 588 | payload: test2 589 | }, 'Test2 event matches') 590 | }) 591 | release() 592 | }) 593 | 594 | emitter.on('Test2', function (msg, cb) { 595 | t.deepEqual(msg, test2, 'Test2 event matches') 596 | 597 | // second argument can be a function, backpressure is supported 598 | cb() 599 | release() 600 | }) 601 | 602 | function release () { 603 | if (--count === 0) { 604 | emitter.close(t.pass.bind(t, 'closed successfully')) 605 | } 606 | } 607 | }) 608 | 609 | test('as stream starting from a certain point', function (t) { 610 | t.plan(3) 611 | 612 | var emitter = new HyperEmitter(memdb(), basicProto) 613 | 614 | var test1 = { 615 | foo: 'hello', 616 | num: 42 617 | } 618 | 619 | var test2 = { 620 | bar: 'world', 621 | id: 23 622 | } 623 | 624 | emitter.on('Test1', function (msg, cb) { 625 | var stream = emitter.stream() 626 | 627 | emitter.emit('Test2', test2) 628 | 629 | stream.once('data', function (msg) { 630 | t.deepEqual(msg, { 631 | name: 'Test2', 632 | payload: test2 633 | }, 'Test2 event matches') 634 | 635 | emitter.close(t.pass.bind(t, 'closed successfully')) 636 | }) 637 | }) 638 | 639 | emitter.emit('Test1', test1, function (err) { 640 | t.error(err, 'no error') 641 | }) 642 | }) 643 | 644 | test('as stream starting from the beginning', function (t) { 645 | t.plan(4) 646 | 647 | var emitter = new HyperEmitter(memdb(), basicProto) 648 | 649 | var test1 = { 650 | foo: 'hello', 651 | num: 42 652 | } 653 | 654 | var test2 = { 655 | bar: 'world', 656 | id: 23 657 | } 658 | 659 | emitter.on('Test1', function (msg, cb) { 660 | var stream = emitter.stream({ from: 'beginning' }) 661 | 662 | emitter.emit('Test2', test2) 663 | stream.once('data', function (msg) { 664 | t.deepEqual(msg, { 665 | name: 'Test1', 666 | payload: test1 667 | }, 'Test1 event matches') 668 | 669 | stream.once('data', function (msg) { 670 | t.deepEqual(msg, { 671 | name: 'Test2', 672 | payload: test2 673 | }, 'Test2 event matches') 674 | 675 | emitter.close(t.pass.bind(t, 'closed successfully')) 676 | }) 677 | }) 678 | }) 679 | 680 | emitter.emit('Test1', test1, function (err) { 681 | t.error(err, 'no error') 682 | }) 683 | }) 684 | 685 | test('no eventpeer if it is not needed', function (t) { 686 | t.plan(3) 687 | 688 | var db = memdb() 689 | 690 | var emitter = new HyperEmitter(db, basicProto) 691 | 692 | emitter.listen(9901, function (err) { 693 | t.error(err, 'no error') 694 | 695 | var oldClose = db.close 696 | db.close = function (cb) { 697 | return cb() 698 | } 699 | 700 | emitter.close(function () { 701 | db.close = oldClose 702 | emitter = new HyperEmitter(db, basicProto) 703 | 704 | emitter.on('EventPeer', function (msg) { 705 | t.fail('EventPeer should never be emitted') 706 | }) 707 | 708 | emitter.listen(9901, function (err) { 709 | t.error(err, 'no error') 710 | 711 | // wait some time for the event to be published 712 | setTimeout(function () { 713 | emitter.close(t.pass.bind(t, 'closed successfully')) 714 | }, 50) 715 | }) 716 | }) 717 | }) 718 | }) 719 | 720 | test('not idempotent', function (t) { 721 | t.plan(5) 722 | 723 | var emitter = new HyperEmitter(memdb(), basicProto) 724 | 725 | var test1 = { 726 | foo: 'hello', 727 | num: 42 728 | } 729 | 730 | var count = 2 731 | 732 | emitter.emit('Test1', test1, function (err) { 733 | t.error(err, 'no error') 734 | }) 735 | 736 | emitter.emit('Test1', test1, function (err) { 737 | t.error(err, 'no error') 738 | }) 739 | 740 | emitter.on('Test1', function (msg) { 741 | t.deepEqual(msg, test1, 'Test1 event matches') 742 | release() 743 | }) 744 | 745 | function release () { 746 | if (--count === 0) { 747 | emitter.close(t.pass.bind(t, 'closed successfully')) 748 | } 749 | } 750 | }) 751 | --------------------------------------------------------------------------------