├── test ├── mocha.opts ├── rethinkdb-observable.unit.js └── rethinkdb-observable.functional.test.js ├── index.js ├── .travis.yml ├── CHANGELOG.md ├── .gitignore ├── LICENSE ├── package.json ├── lib └── cursor-observable.js └── README.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/cursor-observable.js') 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | rethinkdb: "2.3" 4 | node_js: 5 | - "0.10" 6 | - "0.12" 7 | - "4" 8 | - "5" 9 | sudo: false 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [2.0.0] 2 | * Simplify logic, by using `rxjs` 3 | * _breaking_ `observable.subscribe` now returns a `Subscription` instead of `Disposable` (`val.unsubscribe()` vs `val.dispose()`). 4 | 5 | # [1.0.0] 6 | * Initial implementation w/ 100% test coverage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tejesh Mehta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rethinkdb-observable", 3 | "version": "2.0.0", 4 | "description": "Convert a rethinkdb cursor into an observable", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "istanbul cover _mocha ./test && istanbul --text check-coverage --statements 100 --functions 100 --branches 100 --lines 100", 8 | "test-watch": "nodemon -x mocha ./test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/tjmehta/rethinkdb-observable.git" 13 | }, 14 | "keywords": [ 15 | "rethinkdb", 16 | "rethink", 17 | "cursor", 18 | "observable", 19 | "observe", 20 | "subscribe", 21 | "reactive" 22 | ], 23 | "author": "Tejesh Mehta", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/tjmehta/rethinkdb-observable/issues" 27 | }, 28 | "homepage": "https://github.com/tjmehta/rethinkdb-observable", 29 | "dependencies": {}, 30 | "peerDependencies": { 31 | "rxjs": "^5.0.0-beta.10" 32 | }, 33 | "devDependencies": { 34 | "101": "^1.6.1", 35 | "any-promise": "^1.3.0", 36 | "callback-count": "^0.2.0", 37 | "chai": "^3.5.0", 38 | "es6-promise": "^3.2.1", 39 | "ignore-errors": "^1.0.0", 40 | "istanbul": "^0.4.4", 41 | "mocha": "^2.5.3", 42 | "rethinkdb": "^2.3.2", 43 | "rxjs": "^5.0.0-beta.10", 44 | "sinon": "^1.17.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/cursor-observable.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assert = require('assert') 4 | var util = require('util') 5 | 6 | var Observable = require('rxjs/Observable').Observable 7 | var Subscription = require('rxjs/Subscription').Subscription 8 | 9 | module.exports = CursorObservable 10 | 11 | function CursorObservable (cursor) { 12 | if (!(this instanceof CursorObservable)) { 13 | return new CursorObservable(cursor) 14 | } 15 | this._cursor = cursor 16 | this.__subscribed = false 17 | Observable.call(this, this._subscribe) 18 | } 19 | 20 | // inherit from base observable 21 | util.inherits(CursorObservable, Observable) 22 | 23 | /** 24 | * subscribe to this observable 25 | * @param {SubscriptionObserver} observer Subscription observer wraps the original given callbacks w/ "safe" versions 26 | * @return {Subscription} subscription { unsubscribe: fn } 27 | */ 28 | CursorObservable.prototype._subscribe = function (observer) { 29 | assert(!this.__subscribed, 'CursorObservable can only be subscribed to once. To subscribe multiple times see docs (.publish().refCount()).') 30 | var self = this 31 | this.__subscribed = true 32 | this._cursor.each(function (err, next) { 33 | // each callback 34 | if (err) { 35 | observer.error(err) 36 | return 37 | } 38 | observer.next(next) 39 | }, function () { 40 | // completed callback 41 | observer.complete() 42 | }) 43 | return new Subscription(function () { 44 | this.__subscribed = false 45 | self._cursor.close() 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rethinkdb-observable [![Build Status](https://travis-ci.org/tjmehta/rethinkdb-observable.svg?branch=master)](https://travis-ci.org/tjmehta/rethinkdb-observable) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 2 | Convert a rethinkdb cursor into an observable 3 | 4 | # Installation 5 | ```js 6 | npm i --save rethinkdb-observable 7 | npm i --save rxjs # peer dependency 8 | ``` 9 | 10 | # Usage 11 | #### Example: observable w/ single subscribe/unsubscribe 12 | ```js 13 | var createObservable = require('rethinkdb-observable') 14 | var r = require('rethinkdb') 15 | 16 | rethinkdb.table('test').run(conn).then(function (cursor) { 17 | // Note: this is a basic observable and only allows ONE subscription. for multiple, see example below. 18 | var observable = createObservable(cursor) 19 | // subscribe usage 20 | var subscription = observable.subscribe( 21 | function onNext (next) { 22 | // onNext will be passed each item as they are recieved from the cursor 23 | }, 24 | function onError (err) { 25 | // onError will trigger for any cursor errors 26 | }, 27 | function onCompleted () { 28 | // on complete will trigger after last "next" has been pushed 29 | // and cursor has closed successfully 30 | } 31 | ) 32 | // unsubscribe usage 33 | subscription.unsubscribe() 34 | // unsubscribe will detach the subscription callbacks and close the cursor 35 | }) 36 | ``` 37 | 38 | #### Example: observable w/ multiple subscriptions 39 | Uses [rxjs](https://github.com/ReactiveX/rxjs) ConnectableObservable by using `publish`. 40 | To learn more about ReactiveX observables checkout: [reactivex.io](http://reactivex.io) or [intro to rx](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754) 41 | ```js 42 | var createObservable = require('rethinkdb-observable') 43 | var r = require('rethinkdb') 44 | // required to use publish 45 | require('rxjs/add/operator/publish') 46 | 47 | rethinkdb.table('test').run(conn).then(function (cursor) { 48 | // `publish` creates a connectable observable (multiple subscriptions) 49 | // `refCount` invokes subscribe on the first subscription 50 | var observable = rethinkdbObservable(cursor).publish().refCount() 51 | // subscribe usage 52 | var subscription = observable.subscribe( 53 | function onNext (next) { 54 | // onNext will be passed each item as they are recieved from the cursor 55 | }, 56 | function onError (err) { 57 | // onError will trigger for any cursor errors 58 | }, 59 | function onCompleted () { 60 | // on complete will trigger after last "next" has been pushed 61 | // and cursor has closed successfully 62 | } 63 | ) 64 | // unsubscribe usage 65 | subscription.unsubscribe() 66 | // unsubscribe will detach the subscription callbacks and close the cursor 67 | }) 68 | ``` 69 | 70 | 71 | # License 72 | MIT 73 | -------------------------------------------------------------------------------- /test/rethinkdb-observable.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('es6-promise').polyfill() 4 | 5 | var beforeEach = global.beforeEach 6 | var describe = global.describe 7 | var it = global.it 8 | 9 | var expect = require('chai').expect 10 | var noop = require('101/noop') 11 | var sinon = require('sinon') 12 | 13 | var rethinkdbObservable = require('../index.js') 14 | 15 | describe('rethinkdb-observable unit tests', function () { 16 | beforeEach(function () { 17 | this.cursor = { 18 | each: sinon.stub(), 19 | close: sinon.stub() 20 | } 21 | }) 22 | 23 | it('should create an observable', function () { 24 | var observable = rethinkdbObservable(this.cursor) 25 | expect(observable.subscribe).to.exist 26 | }) 27 | 28 | describe('subscribe', function () { 29 | beforeEach(function () { 30 | var callbacks = this.callbacks = [] 31 | this.createCallbackStubs = function () { 32 | var ret = { 33 | onCompleted: sinon.stub(), 34 | onError: sinon.stub(), 35 | onNext: sinon.stub() 36 | } 37 | callbacks.push(ret) 38 | return ret 39 | } 40 | }) 41 | 42 | it('should allow subscriptions', function () { 43 | var observable = rethinkdbObservable(this.cursor) 44 | var subscription = observable.subscribe(noop) 45 | expect(subscription.unsubscribe).to.exist 46 | sinon.assert.calledOnce(this.cursor.each) 47 | }) 48 | 49 | it('should NOT allow multiple subscriptions', function () { 50 | // to subscribe multiple times.. use connectable observable by calling publish()... 51 | var observable = rethinkdbObservable(this.cursor) 52 | observable.subscribe(noop) 53 | expect(function () { 54 | observable.subscribe(noop) 55 | }).to.throw(/subscribed to once/) 56 | }) 57 | 58 | it('should invoke onNext', function () { 59 | var observable = rethinkdbObservable(this.cursor) 60 | var onNext = sinon.stub() 61 | var onError = sinon.stub() 62 | var onCompleted = sinon.stub() 63 | // subscribe 64 | observable.subscribe(onNext, onError, onCompleted) 65 | // mock cursor next 66 | var next = {} 67 | this.cursor.each.firstCall.args[0](null, next) 68 | sinon.assert.calledOnce(onNext) 69 | sinon.assert.calledWith(onNext, next) 70 | sinon.assert.notCalled(onError) 71 | sinon.assert.notCalled(onCompleted) 72 | }) 73 | 74 | it('should invoke onError', function () { 75 | var observable = rethinkdbObservable(this.cursor) 76 | var onNext = sinon.stub() 77 | var onError = sinon.stub() 78 | var onCompleted = sinon.stub() 79 | // subscribe 80 | observable.subscribe(onNext, onError, onCompleted) 81 | // mock cursor error 82 | var err = new Error('err') 83 | this.cursor.each.firstCall.args[0](err) 84 | sinon.assert.calledOnce(onError) 85 | sinon.assert.calledWith(onError, err) 86 | sinon.assert.notCalled(onNext) 87 | sinon.assert.notCalled(onCompleted) 88 | }) 89 | 90 | it('should invoke onCompleted', function () { 91 | var observable = rethinkdbObservable(this.cursor) 92 | var onNext = sinon.stub() 93 | var onError = sinon.stub() 94 | var onCompleted = sinon.stub() 95 | // subscribe 96 | observable.subscribe(onNext, onError, onCompleted) 97 | // mock cursor completed 98 | this.cursor.each.firstCall.args[1]() 99 | sinon.assert.notCalled(onNext) 100 | sinon.assert.notCalled(onError) 101 | sinon.assert.calledOnce(onCompleted) 102 | }) 103 | }) 104 | 105 | describe('subscription.dispose', function () { 106 | it('should dispose subscription', function () { 107 | var observable = rethinkdbObservable(this.cursor) 108 | var onNext = sinon.stub() 109 | var subscription = observable.subscribe(onNext) 110 | // dispose this cursor 111 | subscription.unsubscribe() 112 | sinon.assert.calledOnce(this.cursor.close) 113 | // mock cursor next 114 | var next = {} 115 | this.cursor.each.firstCall.args[0](null, next) 116 | sinon.assert.notCalled(onNext) 117 | }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/rethinkdb-observable.functional.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('es6-promise').polyfill() 4 | 5 | var beforeEach = global.beforeEach 6 | var describe = global.describe 7 | var it = global.it 8 | 9 | var callbackCount = require('callback-count') 10 | var expect = require('chai').expect 11 | var ignore = require('ignore-errors') 12 | var pick = require('101/pick') 13 | var r = require('rethinkdb') 14 | var sinon = require('sinon') 15 | 16 | var TABLE = 'rethinkdb_observable_test' 17 | var sortBy = function (key) { 18 | return function (a, b) { 19 | return (a[key] < b[key]) 20 | ? -1 21 | : (a[key] > b[key]) 22 | ? 1 23 | : 0 24 | } 25 | } 26 | 27 | var rethinkdbObservable = require('../index.js') 28 | 29 | describe('rethinkdb-observable functional tests', function () { 30 | describe('query cursor', function () { 31 | beforeEach(function () { 32 | var self = this 33 | this.rows = [ 34 | { foo: 1 }, 35 | { foo: 2 }, 36 | { foo: 3 }, 37 | { foo: 4 } 38 | ] 39 | return r.connect({ host: 'localhost', db: 'test' }).then(function (conn) { 40 | self.conn = conn 41 | }).then(function () { 42 | return r.tableDrop(TABLE).run(self.conn).catch(ignore(/exist/)) 43 | }).then(function () { 44 | return r.tableCreate(TABLE).run(self.conn).catch(ignore(/exist/)) 45 | }).then(function () { 46 | return Promise.all(self.rows.map(function (row) { 47 | return r.table(TABLE).insert(row).run(self.conn) 48 | })) 49 | }) 50 | }) 51 | 52 | it('should subscribe, next all rows, and complete', function (done) { 53 | var self = this 54 | r.table(TABLE).run(this.conn).then(function (cursor) { 55 | var results = [] 56 | var subscription = rethinkdbObservable(cursor).subscribe( 57 | // onNext 58 | function (row) { 59 | return results.push(row) 60 | }, 61 | // onError 62 | done, 63 | // onComplete 64 | function () { 65 | expect(subscription.unsubscribe).to.be.a('function') 66 | expect(results.sort(sortBy('foo')).map(pick('foo'))).to.deep.equal(self.rows) 67 | done() 68 | } 69 | ) 70 | }).catch(done) 71 | }) 72 | 73 | it('should unsubscribe', function (done) { 74 | var self = this 75 | r.table(TABLE).run(this.conn).then(function (cursor) { 76 | self.cursor = cursor 77 | sinon.spy(cursor, 'close') 78 | var results = [] 79 | var subscription = rethinkdbObservable(cursor).subscribe( 80 | // onNext 81 | function (row) { 82 | unsubscribeAndFinish() 83 | return results.push(row) 84 | }, 85 | // onError 86 | done, 87 | // onComplete 88 | done.bind(null, new Error('should not complete')) 89 | ) 90 | function unsubscribeAndFinish () { 91 | // async assert to make extra sure onComplete is not called.. 92 | subscription.unsubscribe() 93 | sinon.assert.calledOnce(self.cursor.close) 94 | setTimeout(function () { 95 | expect(subscription.unsubscribe).to.be.a('function') 96 | expect(results.length).to.equal(1) 97 | done() 98 | }, 1) 99 | } 100 | }).catch(done) 101 | }) 102 | 103 | describe('connectable observable', function () { 104 | it('should subscribe TWICE, next all rows, and complete', function (done) { 105 | require('rxjs/add/operator/publish') 106 | var self = this 107 | r.table(TABLE).run(this.conn).then(function (cursor) { 108 | var results = [] 109 | var results2 = [] 110 | var count = callbackCount(finish) 111 | var observable = rethinkdbObservable(cursor).publish().refCount() 112 | observable.subscribe( 113 | // onNext 114 | function (row) { 115 | return results.push(row) 116 | }, 117 | // onError 118 | count.next, 119 | // onComplete 120 | count.inc().next 121 | ) 122 | observable.subscribe( 123 | // onNext 124 | function (row) { 125 | return results2.push(row) 126 | }, 127 | // onError 128 | count.next, 129 | // onComplete 130 | count.inc().next 131 | ) 132 | function finish (err) { 133 | if (err) { return done(err) } 134 | expect(results.sort(sortBy('foo')).map(pick('foo'))).to.deep.equal(self.rows) 135 | expect(results2.sort(sortBy('foo')).map(pick('foo'))).to.deep.equal(self.rows) 136 | done() 137 | } 138 | }).catch(done) 139 | }) 140 | 141 | it('should unsubscribe TWICE', function (done) { 142 | require('rxjs/add/operator/publish') 143 | var self = this 144 | r.table(TABLE).run(this.conn).then(function (cursor) { 145 | self.cursor = cursor 146 | sinon.spy(cursor, 'close') 147 | var results = [] 148 | var results2 = [] 149 | var observable = rethinkdbObservable(cursor).publish().refCount() 150 | var subscription = observable.subscribe( 151 | // onNext 152 | function (row) { 153 | if (results.length === 1) { 154 | unsubscribeAndFinish() 155 | } 156 | return results.push(row) 157 | }, 158 | // onError 159 | done, 160 | // onComplete 161 | done.bind(null, new Error('should not complete')) 162 | ) 163 | var subscription2 = observable.subscribe( 164 | // onNext 165 | function (row) { 166 | return results2.push(row) 167 | }, 168 | // onError 169 | done, 170 | // onComplete 171 | done.bind(null, new Error('should not complete')) 172 | ) 173 | function unsubscribeAndFinish () { 174 | // async assert to make extra sure onComplete is not called.. 175 | subscription.unsubscribe() 176 | sinon.assert.notCalled(self.cursor.close) 177 | subscription2.unsubscribe() 178 | sinon.assert.calledOnce(self.cursor.close) 179 | setTimeout(function () { 180 | expect(results.length).to.equal(2) 181 | expect(results2.length).to.equal(1) 182 | done() 183 | }, 1) 184 | } 185 | }).catch(done) 186 | }) 187 | }) 188 | }) 189 | }) 190 | --------------------------------------------------------------------------------