├── .gitignore ├── .npmrc ├── README.md ├── package.json └── src ├── 00-node-server.js ├── 01-rx.js ├── 02-1fps.js ├── 03-main.js ├── 04-run.js ├── 05-sources.js └── rate-limit.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-rx-cycle 2 | > Example using RxJS and Cycle.js on the server to handle requests (NOT server-side rendering) 3 | 4 | Read the blog post [Node server with Rx and Cycle.js](https://glebbahmutov.com/blog/node-server-with-rx-and-cycle/) 5 | 6 | ## Code 7 | 8 | * [src/00-node-server.js](src/00-node-server.js) - "hello world" example in plain Node 9 | using callback 10 | * [src/01-rx.js](src/01-rx.js) - simple reactive server, responding to requests 11 | * [src/02-1fps.js](src/02-1fps.js) - 1fps rate limited server 12 | * [src/rate-limit.js](src/rate-limit.js) - rate limit stream operation 13 | * [src/03-main.js](src/03-main.js) - refactor to sink and main function 14 | * [src/04-run.js](src/04-run.js) - introduced more general run function 15 | * [src/05-sources.js](src/05-sources.js) - clean separation between read, write effects and main logic 16 | 17 | ### Small print 18 | 19 | Author: Gleb Bahmutov © 2016 20 | 21 | * [@bahmutov](https://twitter.com/bahmutov) 22 | * [glebbahmutov.com](http://glebbahmutov.com) 23 | * [blog](http://glebbahmutov.com/blog/) 24 | 25 | License: MIT - do anything with the code, but don't blame me if it does not work. 26 | 27 | Spread the word: tweet, star on github, etc. 28 | 29 | Support: if you find any problems with this module, email / tweet / 30 | [open issue](https://github.com/bahmutov/node-rx-cycle/issues) on Github 31 | 32 | ## MIT License 33 | 34 | Copyright (c) 2016 Gleb Bahmutov 35 | 36 | Permission is hereby granted, free of charge, to any person 37 | obtaining a copy of this software and associated documentation 38 | files (the "Software"), to deal in the Software without 39 | restriction, including without limitation the rights to use, 40 | copy, modify, merge, publish, distribute, sublicense, and/or sell 41 | copies of the Software, and to permit persons to whom the 42 | Software is furnished to do so, subject to the following 43 | conditions: 44 | 45 | The above copyright notice and this permission notice shall be 46 | included in all copies or substantial portions of the Software. 47 | 48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 49 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 50 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 51 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 52 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 53 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 54 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 55 | OTHER DEALINGS IN THE SOFTWARE. 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-rx-cycle", 3 | "version": "1.0.0", 4 | "description": "Example using RxJS and Cycle.js on the server to handle requests (NOT server-side rendering)", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/bahmutov/node-rx-cycle.git" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "debug": "2.2.0", 16 | "rx": "4.0.7" 17 | }, 18 | "keywords": [ 19 | "reactive", 20 | "rxjs", 21 | "node", 22 | "example", 23 | "demo", 24 | "cycle", 25 | "cycle.js" 26 | ], 27 | "author": "Gleb Bahmutov ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/bahmutov/node-rx-cycle/issues" 31 | }, 32 | "homepage": "https://github.com/bahmutov/node-rx-cycle#readme" 33 | } 34 | -------------------------------------------------------------------------------- /src/00-node-server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const hostname = '127.0.0.1'; 4 | const port = 1337; 5 | 6 | http.createServer((req, res) => { 7 | console.log('sending hello'); 8 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 9 | res.end('Hello World\n'); 10 | }).listen(port, hostname, () => { 11 | console.log(`Server running at http://${hostname}:${port}/`); 12 | }); 13 | -------------------------------------------------------------------------------- /src/01-rx.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rx'); 2 | const requests_ = new Rx.Subject(); 3 | 4 | function sendHello(e) { 5 | console.log('sending hello'); 6 | e.res.writeHead(200, { 'Content-Type': 'text/plain' }); 7 | e.res.end('Hello World\n'); 8 | } 9 | 10 | requests_ 11 | .tap(e => console.log('request to', e.req.url)) 12 | .subscribe( 13 | sendHello, 14 | console.error, 15 | () => console.log('stream is done') 16 | ) 17 | 18 | const http = require('http'); 19 | const hostname = '127.0.0.1'; 20 | const port = 1337; 21 | 22 | http.createServer((req, res) => { 23 | requests_.onNext({ req: req, res: res }); 24 | }).listen(port, hostname, () => { 25 | console.log(`Server running at http://${hostname}:${port}/`); 26 | }); 27 | -------------------------------------------------------------------------------- /src/02-1fps.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rx'); 2 | const requests_ = new Rx.Subject(); 3 | const started = +(new Date()) 4 | 5 | function sendHello(e) { 6 | console.log('sending hello at', +(new Date()) - started); 7 | e.res.writeHead(200, { 'Content-Type': 'text/plain' }); 8 | e.res.end('Hello World\n'); 9 | } 10 | 11 | const interval = 1000 12 | const rateLimit = require('./rate-limit') 13 | 14 | const limited_ = rateLimit( 15 | requests_.tap(e => console.log(`request to ${e.req.url} at`, +(new Date) - started)) 16 | , interval) 17 | 18 | limited_.subscribe(sendHello) 19 | 20 | // server 21 | const http = require('http'); 22 | const hostname = '127.0.0.1'; 23 | const port = 1337; 24 | 25 | var prevMs = +(new Date()) 26 | 27 | http.createServer((req, res) => { 28 | requests_.onNext({ req: req, res: res }); 29 | }).listen(port, hostname, () => { 30 | console.log(`Server running at http://${hostname}:${port}/`); 31 | }); 32 | 33 | -------------------------------------------------------------------------------- /src/03-main.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rx'); 2 | const requests_ = new Rx.Subject(); 3 | 4 | function main() { 5 | return requests_ 6 | .tap(e => console.log('request to', e.req.url)) 7 | } 8 | 9 | function httpEffect(model_) { 10 | model_.subscribe(e => { 11 | console.log('sending hello') 12 | e.res.writeHead(200, { 'Content-Type': 'text/plain' }) 13 | e.res.end('Hello World\n') 14 | }) 15 | } 16 | 17 | const sink = main() 18 | httpEffect(sink) 19 | 20 | const http = require('http') 21 | const hostname = '127.0.0.1' 22 | const port = 1337 23 | 24 | http.createServer((req, res) => { 25 | requests_.onNext({ req: req, res: res }) 26 | }).listen(port, hostname, () => { 27 | console.log(`Server running at http://${hostname}:${port}/`) 28 | }); 29 | -------------------------------------------------------------------------------- /src/04-run.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rx'); 2 | const requests_ = new Rx.Subject(); 3 | 4 | function main() { 5 | return { 6 | HTTP: requests_.tap(e => console.log('request to', e.req.url)) 7 | } 8 | } 9 | 10 | function httpEffect(model_) { 11 | model_.subscribe(e => { 12 | console.log('sending hello') 13 | e.res.writeHead(200, { 'Content-Type': 'text/plain' }) 14 | e.res.end('Hello World\n') 15 | }) 16 | } 17 | 18 | function run(main, effects) { 19 | const sinks = main() 20 | Object.keys(effects).forEach(key => { 21 | effects[key](sinks[key]) 22 | }) 23 | } 24 | 25 | run(main, { 26 | HTTP: httpEffect 27 | }) 28 | 29 | const http = require('http') 30 | const hostname = '127.0.0.1' 31 | const port = 1337 32 | 33 | http.createServer((req, res) => { 34 | requests_.onNext({ req: req, res: res }) 35 | }).listen(port, hostname, () => { 36 | console.log(`Server running at http://${hostname}:${port}/`) 37 | }); 38 | -------------------------------------------------------------------------------- /src/05-sources.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rx'); 2 | 3 | function main(sources) { 4 | return { 5 | HTTP: sources.HTTP.tap(e => console.log('request to', e.req.url)) 6 | } 7 | } 8 | 9 | function makeHttpEffect() { 10 | const requests_ = new Rx.Subject(); 11 | return { 12 | writeEffect: function (model_) { 13 | model_.subscribe(e => { 14 | console.log('sending hello') 15 | e.res.writeHead(200, { 'Content-Type': 'text/plain' }) 16 | e.res.end('Hello World\n') 17 | }) 18 | return requests_ 19 | }, 20 | serverCallback: (req, res) => { 21 | requests_.onNext({ req: req, res: res }) 22 | }, 23 | readEffect: requests_ 24 | } 25 | } 26 | 27 | const httpEffect = makeHttpEffect() 28 | const drivers = { 29 | HTTP: httpEffect 30 | } 31 | 32 | function run(main, drivers) { 33 | const sources = { 34 | HTTP: drivers.HTTP.readEffect 35 | } 36 | const sinks = main(sources) 37 | Object.keys(drivers).forEach(key => { 38 | drivers[key].writeEffect(sinks[key]) 39 | }) 40 | } 41 | 42 | run(main, drivers) 43 | 44 | const http = require('http') 45 | const hostname = '127.0.0.1' 46 | const port = 1337 47 | 48 | http.createServer(httpEffect.serverCallback) 49 | .listen(port, hostname, () => { 50 | console.log(`Server running at http://${hostname}:${port}/`) 51 | }); 52 | -------------------------------------------------------------------------------- /src/rate-limit.js: -------------------------------------------------------------------------------- 1 | const Rx = require('rx') 2 | const debug = require('debug')('rate') 3 | 4 | function rateLimit(stream_, delayMs) { 5 | const out_ = new Rx.Subject() 6 | var nextTimestamp = +(new Date()) 7 | var hasEvents = 0; 8 | const started = nextTimestamp 9 | 10 | function onEvent(e) { 11 | var now = +(new Date()) 12 | debug('now', now - started, 'next', nextTimestamp - started) 13 | 14 | if (now > nextTimestamp) { 15 | nextTimestamp = now + delayMs 16 | debug('now %d set next timestamp at', 17 | now - started, nextTimestamp - started) 18 | out_.onNext(e) 19 | return 20 | } 21 | 22 | // delay the response 23 | const sleepMs = nextTimestamp - now; 24 | debug('need to sleep for %d ms at', sleepMs, now - started) 25 | nextTimestamp += delayMs 26 | hasEvents += 1 27 | 28 | setTimeout(function () { 29 | debug('sending', e, 'at', +(new Date()) - started) 30 | out_.onNext(e) 31 | hasEvents -= 1 32 | if (!hasEvents) { 33 | out_.onCompleted() 34 | } 35 | }, sleepMs) 36 | } 37 | 38 | stream_.subscribe( 39 | onEvent, 40 | out_.onError.bind(out_) 41 | ) 42 | 43 | return out_ 44 | } 45 | 46 | module.exports = rateLimit 47 | 48 | if (!module.parent) { 49 | const in_ = Rx.Observable 50 | .interval(200) 51 | .take(5) 52 | // without rate limit 53 | // in_ 54 | // .timeInterval() 55 | // .subscribe( 56 | // console.log.bind(console), 57 | // console.error.bind(console), 58 | // console.log.bind(console, 'completed') 59 | // ) 60 | // output 61 | // { value: 0, interval: 203 } 62 | // { value: 1, interval: 232 } 63 | // { value: 2, interval: 207 } 64 | // { value: 3, interval: 206 } 65 | // { value: 4, interval: 202 } 66 | // completed 67 | 68 | // with rate limit 69 | const limited_ = rateLimit(in_, 1000) 70 | limited_ 71 | .timeInterval() 72 | .subscribe( 73 | console.log, 74 | console.error, 75 | console.log.bind(console, 'limited completed') 76 | ) 77 | // output 78 | // { value: 0, interval: 203 } 79 | // { value: 1, interval: 1005 } 80 | // { value: 2, interval: 1001 } 81 | // { value: 3, interval: 996 } 82 | // { value: 4, interval: 999 } 83 | // limited completed 84 | } 85 | --------------------------------------------------------------------------------