UNPKG

marsdb

Version:

MarsDB is a lightweight client-side MongoDB-like database, Promise based, written in ES6

544 lines (501 loc) 17.8 kB
import Collection from '../../lib/Collection'; import CursorObservable from '../../lib/CursorObservable'; import chai, {expect} from 'chai'; import sinon from 'sinon'; chai.use(require('chai-as-promised')); chai.use(require('sinon-chai')); chai.should(); function resolvePromises(count) { const resolver = () => { return Promise.resolve().then(() => { count -= 1; if (count > 0) { return resolver(); } }) }; return resolver(); } describe('CursorObservable', () => { let db, dbDocs; beforeEach(function () { db = new Collection('test'); dbDocs = [ {_id: '1', a: 'a', b: 1, c: 'some text 1', g: 'g1', f: 1, j: '2'}, {_id: '2', a: 'b', b: 2, c: 'some text 2', g: 'g1', f: 10, j: '3'}, {_id: '3', a: 'c', b: 3, c: 'some text 3', g: 'g1', f: 11, j: '4'}, {_id: '4', a: 'd', b: 4, c: 'some text 4', g: 'g1', f: 12, j: '5'}, {_id: '5', a: 'e', b: 5, c: 'some text 5', g: 'g2', d: 234, f: 2, j: '6'}, {_id: '6', a: 'f', b: 6, c: 'some text 6', g: 'g2', f: 20, k: {a: 1}, j: ['7', '5']}, {_id: '7', a: 'g', b: 7, c: 'some text 7', g: 'g2', f: 21, j: [{_id: '1'}, {_id: '2'}]}, ]; return db.insertAll(dbDocs); }); describe('#defaultDebounce', function () { it('should return default 1000/60', function () { CursorObservable.defaultDebounce().should.be.equal(1000/60); }); it('should set default debounce time', function () { const oldDebounce = CursorObservable.defaultDebounce(); const newDebouce = 100; CursorObservable.defaultDebounce(newDebouce); CursorObservable.defaultDebounce().should.be.equal(newDebouce); CursorObservable.defaultDebounce(oldDebounce); }); }); describe('#defaultBatchSize', function () { it('should return default 10', function () { CursorObservable.defaultBatchSize().should.be.equal(10); }); it('should set default batch size', function () { const oldBatchSize = CursorObservable.defaultBatchSize(); const newBatchSize = 100; CursorObservable.defaultBatchSize(newBatchSize); CursorObservable.defaultBatchSize().should.be.equal(newBatchSize); CursorObservable.defaultBatchSize(oldBatchSize); }); }); describe('#stopObservers', function () { it('should stop all listeners', function () { const cursor = new CursorObservable(db); const cb1 = sinon.spy(); const cb2 = sinon.spy(); cursor.on('stop', cb1); cursor.on('stop', cb2); cursor.emit('stop'); cb1.should.have.callCount(1); cb2.should.have.callCount(1); cursor.stopObservers(); cb1.should.have.callCount(2); cb2.should.have.callCount(2); }); it('should cancel any active updates', function (done) { const cursor = new CursorObservable(db); const cb1 = sinon.spy(); cursor.on('update', cb1); cursor.debounce(50); cursor.update(); cursor.update(); cb1.should.have.callCount(0); cursor.stopObservers(); setTimeout(() => { cb1.should.have.callCount(0); done(); }, 30); }); }); describe('#observe', function () { it('should generate `observeStopped` event when all observers stopped', function () { const cursor = db.find({b: 1}) const cb = sinon.spy(); cursor.on('observeStopped', cb); const obs1 = cursor.observe(() => {}); const obs2 = cursor.observe(() => {}); obs1.stop(); cb.should.have.callCount(0); obs2.stop(); cb.should.have.callCount(1); }); it('should return result of previous execution', function () { const cursor = db.find({b: 1}) let result; return cursor.observe((res) => { result = res; }).then(() => { return cursor.observe((new_res) => { new_res.should.be.equal(result); }).then((new_res) => { new_res.should.be.equal(result); }); }) }); it('should observe insert without debounce and batchSize eq 1', function (done) { var calls = 0; const cursor = new CursorObservable(db); cursor.batchSize(1); cursor.debounce(0); cursor.find({b: {$gt: 4, $lte: 7}}).observe((result) => { expect(result).to.be.an('array'); calls += 1; if (calls === 1) { result.should.have.length(3); } else if (calls > 1) { result.should.have.length(4); done(); } }).then(() => { db.insert({b: 4.5}); }); }); it('should not update if inserted document not match query', function (done) { const cursor = new CursorObservable(db); cursor.batchSize(1); cursor.debounce(0); cursor.find({b: {$gt: 4, $lte: 7}}).observe((result) => { if (result.length > 3) { done(new Error()); } }).then((result) => { expect(result).to.be.an('array'); return db.insert({b: 3.5}); }).then(() => { setTimeout(() => {done()}, 10); }); }); it('should stop observing by calling stop method', function (done) { var calls = 0; const cursor = new CursorObservable(db); cursor.batchSize(1); cursor.debounce(0); const stopper = cursor.find({b: {$gt: 4, $lte: 7}}).observe((result) => { calls > 0 && done(new Error()); calls += 1; }).then((result) => { expect(result).to.be.an('array'); stopper.stop(); return db.insert({b: 4.5}); }).then(() => { setTimeout(() => {done()}, 10); }); }); it('should execute once after immidiatelly stop observing', function (done) { var calls = 0; const cursor = new CursorObservable(db); cursor.batchSize(1); cursor.debounce(0); const stopper = cursor.find({b: {$gt: 4, $lte: 7}}).observe((result) => { calls > 0 && done(new Error()); calls += 1; }) stopper.stop(); stopper.then((result) => { expect(result).to.be.an('array'); return db.insert({b: 4.5}); }).then(() => { setTimeout(() => {done()}, 10); }); }); it('should observe in join and propagate update to upper observer', function (done) { var calls = 0; db.find({$or: [{f: 1}, {f: 2}]}) .join((doc) => { return db.find({b: 30}).observe(res => { doc.joined = res; }); }).observe(result => { if (calls === 0) { expect(result).to.be.an('array'); result.should.have.length(2); expect(result[0].joined).to.have.length(0); expect(result[1].joined).to.have.length(0); calls++; } else { expect(result[0].joined).to.have.length(1); expect(result[1].joined).to.have.length(1); done(); } }).then(() => { return db.insert({b: 30}); }); }); it('should stop observing previous join after upper join update', function (done) { var observerCalls = 0; var joinCalls = 0; db.find({$or: [{f: 1}, {f: 2}]}) .joinAll((docs) => { return db.find({b: 30}, {test: observerCalls}).observe(res => { if (res.length > 0) { joinCalls += 1; joinCalls.should.be.lte(2); } }); }) .batchSize(0) .debounce(0) .observe(result => { observerCalls.should.be.lte(2); observerCalls++; if (observerCalls === 2) { setTimeout(done, 60); } }).then(() => { return db.insert({f: 1}); }).then(() => { return db.insert({b: 30}); }); }); it('should update when join function call updater function', function (done) { var observerCalls = 0; db.find({$or: [{f: 1}, {f: 2}]}) .joinAll((docs, updated) => { setTimeout(() => { docs[0].updated = true; updated(); }, 10); }) .batchSize(0) .debounce(0) .observe(result => { observerCalls.should.be.lte(2); observerCalls++; if (observerCalls === 1) { expect(result[0].updated).to.be.undefined; } else if (observerCalls === 2) { result[0].updated.should.be.equals(true); done(); } }) }); it('should not update a cursor when updated dcc does not match a query', function (done) { var calls = 0; db.find({$or: [{f: 1}, {f: 2}]}).observe(result => { if (calls === 0) { expect(result).to.be.an('array'); result.should.have.length(2); calls++; } else { done(new Error('Called when document does not match query')); } }).then(() => { return db.update({f: 3}, {$set: {some: 'field'}}); }).then(() => { setTimeout(() => {done()}, 40); }); }); it('should not update a cursor when updated doc is equals to an old doc', function (done) { var calls = 0; db.find({$or: [{f: 1}, {f: 2}]}).observe(result => { if (calls === 0) { expect(result).to.be.an('array'); result.should.have.length(2); calls++; } else { done(new Error('Called when an updated doc is equals to an old doc')); } }).then(() => { return db.update({f: 1}, {$set: {b: 1}}); }).then(() => { setTimeout(() => {done()}, 40); }); }); it('should update a cursor when updatedAt is different', function (done) { var calls = 0; db.update({f: 1}, {$set: {updatedAt: new Date(0)}}).then(() => { return db.find({$or: [{f: 1}, {f: 2}]}).observe(result => { if (calls === 0) { expect(result).to.be.an('array'); result.should.have.length(2); calls++; } else { result[0].updatedAt.should.not.be.deep.equals(new Date(0)); result[0].updatedAt.should.be.deep.equals(new Date(1)); done(); } }).then(() => { return db.update({f: 1}, {$set: {updatedAt: new Date(1)}}); }); }); }); it('should NOT update a cursor when updatedAt is equals', function (done) { var calls = 0; db.update({f: 1}, {$set: {updatedAt: new Date(0)}}).then(() => { return db.find({$or: [{f: 1}, {f: 2}]}).observe(result => { if (calls === 0) { expect(result).to.be.an('array'); result.should.have.length(2); calls++; } else { done(new Error()); } }).then(() => { return db.update({f: 1}, {$set: {updatedAt: new Date(0)}}); }).then(() => { setTimeout(() => {done()}, 40); }); }); }); it('should update when not matching old doc will match by update', function (done) { var calls = 0; db.find({$or: [{f: 1}, {f: 2}]}).observe(result => { if (calls === 0) { expect(result).to.be.an('array'); result.should.have.length(2); calls++; } else { result.should.have.length(3); done(); } }).then(() => { return db.update({f: 20}, {$set: {f: 2}}); }); }); it('should be invoked before `then` callback', function (done) { var invoked = false; db.find({$or: [{f: 1}, {f: 2}]}).observe(result => { invoked.should.be.equals(false); invoked = true; }).then(() => { invoked.should.be.equals(true); done(); }); }); it('should observe `findOne` collection method', function (done) { var calls = 0; db.findOne({b: 8}).batchSize(1).debounce(0) .observe(result => { calls += 1; if (calls === 1) { expect(result).to.be.undefined; } else if (calls > 1) { result.b.should.be.equals(8); done(); } }).then(() => { db.insert({b: 8}); }); }); it('should stop previous execution with observed joins if cursor is updated', function () { const obspy = sinon.spy(); const cursor = db.find().join({j: db}, {observe: true}); cursor.observe(obspy); return resolvePromises(4).then(() => { return cursor.maybeUpdate(null, null); }).then(() => { obspy.should.have.callCount(2); obspy.getCall(0).args[0][0].j.should.be.deep.equal(dbDocs[1]); obspy.getCall(1).args[0][0].j.should.be.deep.equal(dbDocs[1]); }) }); }); describe('#maybeUpdate', function () { it('should update when no newDoc and oldDoc provided', function () { const cursor = new CursorObservable(db); cursor.update = sinon.spy(); cursor.maybeUpdate(null, null); cursor.update.should.have.callCount(1); }); it('should update when removed doc witihin previous result', function () { const cursor = new CursorObservable(db); cursor.update = sinon.spy(); cursor._latestResult = [{_id: '1'}]; cursor._updateLatestIds(); cursor.maybeUpdate(null, {_id: '1'}); cursor.update.should.have.callCount(1); cursor.maybeUpdate(null, {_id: '2'}); cursor.update.should.have.callCount(1); }); it('should update if match only old document', function() { const cursor = new CursorObservable(db); cursor.find({a: {$gt: 10}}); cursor.update = sinon.spy(); cursor.maybeUpdate({a: 9}, {a: 11}); cursor.update.should.have.callCount(1); cursor.maybeUpdate({a: 11}, {a: 9}); cursor.update.should.have.callCount(2); cursor.maybeUpdate({a: 12}, {a: 13}); cursor.update.should.have.callCount(3); cursor.maybeUpdate({a: 8}, {a: 9}); cursor.update.should.have.callCount(3); }); }); describe('#update', function () { it('should guarantee that only one _doUpdate invoked at one time', function () { const beforeUpdateCall = sinon.spy(); const doUpdateCall = sinon.spy(); class OtherCursor extends CursorObservable { _doUpdate(...args) { beforeUpdateCall(); return new Promise((resolve) => { setTimeout(resolve, 50); }).then(() => { return super._doUpdate(...args); }).then((res) => { doUpdateCall(); return res; }); } } const cursor = new OtherCursor(db); cursor.batchSize(2); cursor.debounce(50); const initUpd = cursor.update(true, true); const upd2 = cursor.update(); const upd3 = cursor.update(); //beforeUpdateCall.should.have.callCount(0); return Promise.resolve().then(() => { beforeUpdateCall.should.have.callCount(1); cursor.update(); cursor.update(); cursor.update(); const upd4 = cursor.update(); beforeUpdateCall.should.have.callCount(1); return initUpd.then(() => { beforeUpdateCall.should.have.callCount(1); return resolvePromises(1); }).then(() => { cursor.update(); cursor.update(); return upd4; }) .then(() => { beforeUpdateCall.should.have.callCount(2); doUpdateCall.should.have.callCount(2); }); }) }); }); describe('#_trackChildCursorPromise', function () { it('should stop observer throught not observable cursor', function () { const cursor_3 = new CursorObservable(db).find({a: 'c'}); const cursor_2 = new CursorObservable(db).find({a: 'b'}).join(() => cursor_3.observe()); const cursor_1 = new CursorObservable(db).find({a: 'a'}).join(() => cursor_2); return Promise.all([ cursor_1.observe() ]) .then(() => cursor_1.update()) .then(() => { cursor_3.listeners('update').should.have.length(1); cursor_2.listeners('update').should.have.length(0); cursor_1.listeners('update').should.have.length(1); }); }); it('should stop only useless observers', function () { const cursor_3 = new CursorObservable(db).find({a: 'c'}); const cursor_2 = new CursorObservable(db).find({a: 'b'}).join(() => cursor_3.observe()); const cursor_1 = new CursorObservable(db).find({a: 'a'}).join(() => cursor_2); return Promise.all([ cursor_1.observe(), cursor_3.observe(), ]) .then(() => { cursor_3.listeners('update').should.have.length(2); return cursor_1.update(); }) .then(() => { cursor_3.listeners('update').should.have.length(2); cursor_2.listeners('update').should.have.length(0); cursor_1.listeners('update').should.have.length(1); }); }); it('should not stop observer in parallel cursor tree', function () { const cursor_3 = new CursorObservable(db).find({a: 'c'}); const cursor_2 = new CursorObservable(db).find({a: 'b'}).join(() => cursor_3.observe()); const cursor_1 = new CursorObservable(db).find({a: 'a'}).join(() => cursor_2); const cursor_1_1 = new CursorObservable(db).find({a: 'a'}).join(() => cursor_3.observe()); return Promise.all([ cursor_1.observe(), cursor_1_1.observe(), ]) .then(() => { cursor_3.listeners('update').should.have.length(2); return cursor_1.update(); }) .then(() => { cursor_3.listeners('update').should.have.length(2); cursor_2.listeners('update').should.have.length(0); cursor_1.listeners('update').should.have.length(1); cursor_1_1.listeners('update').should.have.length(1); }); }); }); });