Repository: pgte/pouch-redux-middleware Branch: master Commit: d22968c7c77f Files: 16 Total size: 28.7 KB Directory structure: gitextract_9uhaz__j/ ├── .babelrc ├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── README.md ├── index.js ├── lib/ │ └── index.js ├── package.json ├── src/ │ └── index.js └── test/ ├── _action_types.js ├── reducers/ │ ├── index.js │ ├── todos.js │ └── todosobject.js ├── standalone.js ├── with-redux-object.js └── with-redux.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "es2015", "stage-0" ] } ================================================ FILE: .gitignore ================================================ node_modules coverage ================================================ FILE: .istanbul.yml ================================================ instrumentation: excludes: ['test', 'node_modules'] check: global: lines: 100 branches: 100 statements: 100 functions: 100 ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 4 - 5 ================================================ FILE: README.md ================================================ # pouch-redux-middleware [![By](https://img.shields.io/badge/made%20by-yld!-32bbee.svg?style=flat)](http://yld.io/contact?source=github-pouch-redux-middleware) [![Build Status](https://secure.travis-ci.org/pgte/pouch-redux-middleware.svg?branch=master)](http://travis-ci.org/pgte/pouch-redux-middleware?branch=master) Redux Middleware for syncing state and a PouchDB database. Propagates changes made to a state into PouchDB. Propagates changes made to PouchDB into the state. ## Install ``` $ npm install pouch-redux-middleware --save ``` ## Overview pouch-redux-middleware will automatically populate a part of the store, specified by `path`, with the documents using the specified actions. This "sub-state" will be a list of the documents straight out of the database. When the database is modified by a 3rd party (e.g. by replication) a Redux action will be dispatched to update the "sub-state". Conversely, if you alter a document within the "sub-state", then the document will be updated in the database. * If a new document is created in the database (e.g. by replication or directly using `db.post`), then the corresponding `insert` action will be dispatched. If a document is updated in the database (e.g. by replication or directly), then the corresponding `update` action will be dispatched. If a document is deleted in the database (e.g. by replication or directly), then the corresponding `remove` action will be dispatched. * If you add a document to the "sub-state", then the document will be added to the database automatically (you should specify keys such as `_id`). If you alter a document in the "sub-state", the document will be updated in the database automatically. If you remove a document from the "sub-state", the document will be removed from the database automatically. * You may specify that that only a subset of the database's documents should populate the store by using `changeFilter` which effectively filters the documents under consideration. ## Example Example of configuring a store: ```js import * as types from '../constants/ActionTypes' import PouchMiddleware from 'pouch-redux-middleware' import { createStore, applyMiddleware } from 'redux' import rootReducer from '../reducers' import PouchDB from 'pouchdb' export default function configureStore() { const db = new PouchDB('todos'); const pouchMiddleware = PouchMiddleware({ path: '/todos', db, actions: { remove: doc => { return { type: types.DELETE_TODO, id: doc._id } }, insert: doc => { return { type: types.INSERT_TODO, todo: doc } }, batchInsert: docs => { return { type: types.BATCH_INSERT_TODOS, todos: docs } } update: doc => { return { type: types.UPDATE_TODO, todo: doc } }, } }) const store = createStore( rootReducer, undefined, applyMiddleware(pouchMiddleware) ) return store } ``` ## API ### PouchMiddleware(paths) * `paths`: path or array containing path specs A path spec is an object describing the behaviour of a sub-tree of the state it has the following attributes: * `path`: a JsonPath path where the documents will stored in the state as an array * `db`: a PouchDB database * `actions`: an object describing the actions to perform when initially inserting items and when a change occurs in the db. It's an object with keys containing a function that returns an action for each of the events (`remove`, `insert`, `batchInsert` and `update`) * `changeFilter`: a filtering function that receives a changed document, and if it returns false, the document will be ignored for the path. This is useful when you have multiple paths in a single database that are differentiated through an attribute (like `type`). * `handleResponse` a function that is invoked with the direct response of the database, which is useful when metadata is needed or errors need custom handling. Arguments are `error, data, callback`. `callback` must be invoked with a potential error after custom handling is done. * `initialBatchDispatched` a function that is invoked once the initial set of data has been read from pouchdb and dispatched to the redux store. This comes handy if you want skip the initial updates to a store subscriber by delaying the subscription to the redux store until the initial state is present. For example, when your application is first loaded you may wish to delay rendering until the store is updated. Example of a path spec: ```js { path: '/todos', db, actions: { remove: doc => { return { type: types.DELETE_TODO, id: doc._id } }, insert: doc => { return { type: types.INSERT_TODO, todo: doc } }, batchInsert: docs => { return { type: types.BATCH_INSERT_TODOS, todos: docs } } update: doc => { return { type: types.UPDATE_TODO, todo: doc } }, } } ``` ## License ISC ================================================ FILE: index.js ================================================ module.exports = require('./lib'); ================================================ FILE: lib/index.js ================================================ 'use strict'; var jPath = require('json-path'); var Queue = require('async-function-queue'); var extend = require('xtend'); var equal = require('deep-equal'); module.exports = createPouchMiddleware; function createPouchMiddleware(_paths) { var paths = _paths || []; if (!Array.isArray(paths)) { paths = [paths]; } if (!paths.length) { throw new Error('PouchMiddleware: no paths'); } var defaultSpec = { path: '.', remove: scheduleRemove, insert: scheduleInsert, propagateDelete: propagateDelete, propagateUpdate: propagateUpdate, propagateInsert: propagateInsert, propagateBatchInsert: propagateBatchInsert, handleResponse: function handleResponse(err, data, cb) { cb(err); }, queue: Queue(1), docs: {}, actions: { remove: defaultAction('remove'), update: defaultAction('update'), insert: defaultAction('insert'), batchInsert: defaultAction('batchInsert') } }; paths = paths.map(function (path) { var spec = extend({}, defaultSpec, path); spec.actions = extend({}, defaultSpec.actions, spec.actions); spec.docs = {}; if (!spec.db) { throw new Error('path ' + path.path + ' needs a db'); } return spec; }); function listen(path, dispatch, initialBatchDispatched) { path.db.allDocs({ include_docs: true }).then(function (rawAllDocs) { var allDocs = rawAllDocs.rows.map(function (doc) { return doc.doc; }); var filteredAllDocs = allDocs; if (path.changeFilter) { filteredAllDocs = allDocs.filter(path.changeFilter); } allDocs.forEach(function (doc) { path.docs[doc._id] = doc; }); path.propagateBatchInsert(filteredAllDocs, dispatch); initialBatchDispatched(); var changes = path.db.changes({ live: true, include_docs: true, since: 'now' }); changes.on('change', function (change) { onDbChange(path, change, dispatch); }); }); } function processNewStateForPath(path, state) { var docs = jPath.resolve(state, path.path); /* istanbul ignore else */ if (docs && docs.length) { docs.forEach(function (docs) { var diffs = differences(path.docs, docs); diffs.new.concat(diffs.updated).forEach(function (doc) { return path.insert(doc); }); diffs.deleted.forEach(function (doc) { return path.remove(doc); }); }); } } function write(data, responseHandler) { return function (done) { data.db[data.type](data.doc, function (err, resp) { responseHandler(err, { response: resp, doc: data.doc, type: data.type }, function (err2) { done(err2, resp); }); }); }; } function scheduleInsert(doc) { this.docs[doc._id] = doc; this.queue.push(write({ type: 'put', doc: doc, db: this.db }, this.handleResponse)); } function scheduleRemove(doc) { delete this.docs[doc._id]; this.queue.push(write({ type: 'remove', doc: doc, db: this.db }, this.handleResponse)); } function propagateDelete(doc, dispatch) { dispatch(this.actions.remove(doc)); } function propagateInsert(doc, dispatch) { dispatch(this.actions.insert(doc)); } function propagateUpdate(doc, dispatch) { dispatch(this.actions.update(doc)); } function propagateBatchInsert(docs, dispatch) { dispatch(this.actions.batchInsert(docs)); } return function (options) { paths.forEach(function (path) { listen(path, options.dispatch, function (err) { if (path.initialBatchDispatched) { path.initialBatchDispatched(err); } }); }); return function (next) { return function (action) { var returnValue = next(action); var newState = options.getState(); paths.forEach(function (path) { return processNewStateForPath(path, newState); }); return returnValue; }; }; }; } function differences(oldDocs, newDocs) { var result = { new: [], updated: [], deleted: Object.keys(oldDocs).map(function (oldDocId) { return oldDocs[oldDocId]; }) }; var checkDoc = function checkDoc(newDoc) { var id = newDoc._id; /* istanbul ignore next */ if (!id) { warn('doc with no id'); } result.deleted = result.deleted.filter(function (doc) { return doc._id !== id; }); var oldDoc = oldDocs[id]; if (!oldDoc) { result.new.push(newDoc); } else if (!equal(oldDoc, newDoc)) { result.updated.push(newDoc); } }; if (Array.isArray(newDocs)) { newDocs.forEach(function (doc) { checkDoc(doc); }); } else { var keys = Object.keys(newDocs); for (var key in newDocs) { checkDoc(newDocs[key]); } } return result; } function onDbChange(path, change, dispatch) { var changeDoc = change.doc; if (path.changeFilter && !path.changeFilter(changeDoc)) { return; } if (changeDoc._deleted) { if (path.docs[changeDoc._id]) { delete path.docs[changeDoc._id]; path.propagateDelete(changeDoc, dispatch); } } else { var oldDoc = path.docs[changeDoc._id]; path.docs[changeDoc._id] = changeDoc; if (oldDoc) { path.propagateUpdate(changeDoc, dispatch); } else { path.propagateInsert(changeDoc, dispatch); } } } /* istanbul ignore next */ function warn(what) { var fn = console.warn || console.log; if (fn) { fn.call(console, what); } } /* istanbul ignore next */ function defaultAction(action) { return function () { throw new Error('no action provided for ' + action); }; } ================================================ FILE: package.json ================================================ { "name": "pouch-redux-middleware", "version": "1.1.0", "description": "PouchDB Redux Middleware", "main": "lib/index.js", "scripts": { "test": "node --harmony node_modules/istanbul/lib/cli.js cover -- lab -vl && istanbul check-coverage", "prepublish": "npm run build", "build": "babel ./src -d lib" }, "repository": { "type": "git", "url": "git+https://github.com/pgte/pouch-redux-middleware.git" }, "keywords": [ "pouchdb", "redux", "react", "middleware" ], "author": "pgte", "license": "ISC", "bugs": { "url": "https://github.com/pgte/pouch-redux-middleware/issues" }, "homepage": "https://github.com/pgte/pouch-redux-middleware#readme", "dependencies": { "async-function-queue": "^1.0.0", "deep-equal": "^1.0.1", "json-path": "^0.1.3", "xtend": "^4.0.1" }, "devDependencies": { "async": "^2.1.4", "code": "^4.0.0", "istanbul": "^0.4.5", "lab": "^12.1.0", "memdown": "^1.2.4", "pouchdb": "^6.1.2", "pre-commit": "^1.2.2", "redux": "^3.6.0", "babel-cli": "^6.22.2", "babel-core": "^6.22.1", "babel-preset-es2015": "^6.22.0", "babel-preset-stage-0": "^6.22.0" }, "pre-commit": [ "test" ] } ================================================ FILE: src/index.js ================================================ var jPath = require('json-path'); var Queue = require('async-function-queue'); var extend = require('xtend'); var equal = require('deep-equal'); module.exports = createPouchMiddleware; function createPouchMiddleware(_paths) { var paths = _paths || []; if (!Array.isArray(paths)) { paths = [paths]; } if (!paths.length) { throw new Error('PouchMiddleware: no paths'); } var defaultSpec = { path: '.', remove: scheduleRemove, insert: scheduleInsert, propagateDelete, propagateUpdate, propagateInsert, propagateBatchInsert, handleResponse: function(err, data, cb) { cb(err); }, queue: Queue(1), docs: {}, actions: { remove: defaultAction('remove'), update: defaultAction('update'), insert: defaultAction('insert'), batchInsert: defaultAction('batchInsert'), } } paths = paths.map(function(path) { var spec = extend({}, defaultSpec, path); spec.actions = extend({}, defaultSpec.actions, spec.actions); spec.docs = {}; if (! spec.db) { throw new Error('path ' + path.path + ' needs a db'); } return spec; }); function listen(path, dispatch, initialBatchDispatched) { path.db.allDocs({ include_docs: true }).then((rawAllDocs) => { var allDocs = rawAllDocs.rows.map((doc) => doc.doc); var filteredAllDocs = allDocs; if (path.changeFilter) { filteredAllDocs = allDocs.filter(path.changeFilter); } allDocs.forEach((doc) => { path.docs[doc._id] = doc; }); path.propagateBatchInsert(filteredAllDocs, dispatch); initialBatchDispatched(); var changes = path.db.changes({ live: true, include_docs: true, since: 'now', }); changes.on('change', change => { onDbChange(path, change, dispatch); }); }); } function processNewStateForPath(path, state) { var docs = jPath.resolve(state, path.path); /* istanbul ignore else */ if (docs && docs.length) { docs.forEach(function(docs) { var diffs = differences(path.docs, docs); diffs.new.concat(diffs.updated).forEach(doc => path.insert(doc)) diffs.deleted.forEach(doc => path.remove(doc)); }); } } function write(data, responseHandler) { return function(done) { data.db[data.type](data.doc, function(err, resp) { responseHandler( err, { response: resp, doc: data.doc, type: data.type }, function(err2) { done(err2, resp); } ); }); }; } function scheduleInsert(doc) { this.docs[doc._id] = doc; this.queue.push(write( { type: 'put', doc: doc, db: this.db }, this.handleResponse )); } function scheduleRemove(doc) { delete this.docs[doc._id]; this.queue.push(write( { type: 'remove', doc: doc, db: this.db }, this.handleResponse )); } function propagateDelete(doc, dispatch) { dispatch(this.actions.remove(doc)); } function propagateInsert(doc, dispatch) { dispatch(this.actions.insert(doc)); } function propagateUpdate(doc, dispatch) { dispatch(this.actions.update(doc)); } function propagateBatchInsert(docs, dispatch) { dispatch(this.actions.batchInsert(docs)); } return function(options) { paths.forEach((path) => { listen(path, options.dispatch, (err) => { if (path.initialBatchDispatched) { path.initialBatchDispatched(err); } }); }); return function(next) { return function(action) { var returnValue = next(action); var newState = options.getState(); paths.forEach(path => processNewStateForPath(path, newState)); return returnValue; } } } } function differences(oldDocs, newDocs) { var result = { new: [], updated: [], deleted: Object.keys(oldDocs).map(oldDocId => oldDocs[oldDocId]), }; var checkDoc = function(newDoc) { var id = newDoc._id; /* istanbul ignore next */ if (! id) { warn('doc with no id'); } result.deleted = result.deleted.filter(doc => doc._id !== id); var oldDoc = oldDocs[id]; if (! oldDoc) { result.new.push(newDoc); } else if (!equal(oldDoc, newDoc)) { result.updated.push(newDoc); } }; if (Array.isArray(newDocs)){ newDocs.forEach(function (doc) { checkDoc(doc) }); } else{ var keys = Object.keys(newDocs); for (var key in newDocs){ checkDoc(newDocs[key]) } } return result; } function onDbChange(path, change, dispatch) { var changeDoc = change.doc; if(path.changeFilter && (! path.changeFilter(changeDoc))) { return; } if (changeDoc._deleted) { if (path.docs[changeDoc._id]) { delete path.docs[changeDoc._id]; path.propagateDelete(changeDoc, dispatch); } } else { var oldDoc = path.docs[changeDoc._id]; path.docs[changeDoc._id] = changeDoc; if (oldDoc) { path.propagateUpdate(changeDoc, dispatch); } else { path.propagateInsert(changeDoc, dispatch); } } } /* istanbul ignore next */ function warn(what) { var fn = console.warn || console.log; if (fn) { fn.call(console, what); } } /* istanbul ignore next */ function defaultAction(action) { return function() { throw new Error('no action provided for ' + action); }; } ================================================ FILE: test/_action_types.js ================================================ [ 'ERROR', 'ADD_TODO', 'INSERT_TODO', 'BATCH_INSERT_TODOS', 'DELETE_TODO', 'EDIT_TODO', 'UPDATE_TODO', 'COMPLETE_TODO', 'COMPLETE_ALL', 'CLEAR_COMPLETED' ].forEach(function(type) { exports[type] = type; }); ================================================ FILE: test/reducers/index.js ================================================ var redux = require('redux'); var todos = require('./todos'); var todosobject = require('./todosobject'); module.exports = redux.combineReducers({ todos: todos, todosobject: todosobject }); ================================================ FILE: test/reducers/todos.js ================================================ var actionTypes = require('../_action_types'); const initialState = [] module.exports = function todos(state, action) { if (! state) { state = []; } switch (action.type) { case actionTypes.ADD_TODO: return [ { _id: action.id || id(), completed: false, text: action.text }, ...state ] case actionTypes.INSERT_TODO: return [ action.todo, ...state ] case actionTypes.BATCH_INSERT_TODOS: return [...state, ...action.todos] case actionTypes.DELETE_TODO: return state.filter(todo => todo._id !== action.id ) case actionTypes.EDIT_TODO: return state.map(todo => todo._id === action.id ? Object.assign({}, todo, { text: action.text }) : todo ) case actionTypes.UPDATE_TODO: return state.map(todo => todo._id === action.todo._id ? action.todo : todo ) case actionTypes.COMPLETE_TODO: return state.map(todo => todo._id === action.id ? Object.assign({}, todo, { completed: !todo.completed }) : todo ) case actionTypes.COMPLETE_ALL: const areAllMarked = state.every(todo => todo.completed) return state.map(todo => Object.assign({}, todo, { completed: !areAllMarked })) case actionTypes.CLEAR_COMPLETED: return state.filter(todo => todo.completed === false) default: return state } } function id() { return Math.random().toString(36).substring(7); } ================================================ FILE: test/reducers/todosobject.js ================================================ var actionTypes = require('../_action_types'); const initialState = [] module.exports = function todosobject(state, action) { if (! state) { state = {}; } switch (action.type) { case actionTypes.ADD_TODO: { var todo = { _id: action.id || id(), completed: false, text: action.text }; return Object.assign(state, {[todo._id]: todo}); } case actionTypes.INSERT_TODO: return Object.assign(state, { [action.todo._id]: action.todo }); case actionTypes.DELETE_TODO: var newState = Object.assign({}, state); delete newState[action.id]; return newState; case actionTypes.UPDATE_TODO: return Object.assign(state, { [action.todo._id]: action.todo }); default: return state } } function id() { return Math.random().toString(36).substring(7); } ================================================ FILE: test/standalone.js ================================================ var Lab = require('lab'); var lab = exports.lab = Lab.script(); var describe = lab.experiment; var before = lab.before; var after = lab.after; var it = lab.it; var Code = require('code'); var expect = Code.expect; var PouchMiddleware = require('../src/'); var PouchDB = require('pouchdb'); var db = new PouchDB('todos', { db: require('memdown'), }); describe('Pouch Redux Middleware', function() { var pouchMiddleware; var store; it('cannot be created with no paths', function(done) { expect(function() { PouchMiddleware(); }).to.throw('PouchMiddleware: no paths'); done(); }); it('requires db in path', function(done) { expect(function() { PouchMiddleware([{}]); }).to.throw('path undefined needs a db'); done(); }); }); ================================================ FILE: test/with-redux-object.js ================================================ var Lab = require('lab'); var lab = exports.lab = Lab.script(); var describe = lab.experiment; var before = lab.before; var after = lab.after; var it = lab.it; var Code = require('code'); var expect = Code.expect; var actionTypes = require('./_action_types'); var rootReducer = require('./reducers'); var timers = require('timers'); var async = require('async'); var PouchDB = require('pouchdb'); var db = new PouchDB('todosobject', { db: require('memdown'), }); var redux = require('redux'); var PouchMiddleware = require('../src/'); describe('Pouch Redux Middleware with Objects', function() { var pouchMiddleware; var store; it('todosmaps can be created', function(done) { pouchMiddleware = PouchMiddleware({ path: '/todosobject', db: db, actions: { remove: (doc) => { return {type: actionTypes.DELETE_TODO, id: doc._id} }, insert: (doc) => { return {type: actionTypes.INSERT_TODO, todo: doc} }, batchInsert: (docs) => { return {type: actionTypes.BATCH_INSERT_TODOS, todos: docs} }, update: (doc) => { return {type: actionTypes.UPDATE_TODO, todo: doc} } }, changeFilter: doc => !doc.filter }); done(); }); it('can be used to create a store', function(done) { var createStoreWithMiddleware = redux.applyMiddleware(pouchMiddleware)(redux.createStore); store = createStoreWithMiddleware(rootReducer); done(); }); it('accepts a few inserts', function(done) { store.dispatch({type: actionTypes.ADD_TODO, text: 'do laundry', id: 'a'}); store.dispatch({type: actionTypes.ADD_TODO, text: 'wash dishes', id: 'b'}); timers.setTimeout(done, 100); }); it('saves changes in pouchdb', function(done) { async.map(['a', 'b'], db.get.bind(db), function(err, results) { if (err) return done(err); expect(results.length).to.equal(2); expect(results[0].text).to.equal('do laundry'); expect(results[1].text).to.equal('wash dishes'); done(); }); }); it('accepts an removal', function(done) { store.dispatch({type: actionTypes.DELETE_TODO, id: 'a'}); timers.setTimeout(done, 100); }); it('saves changes in pouchdb', function(done) { db.get('a', function(err) { expect(err).to.be.an.object(); expect(err.message).to.equal('missing'); done(); }); }); }); ================================================ FILE: test/with-redux.js ================================================ var Lab = require('lab'); var lab = exports.lab = Lab.script(); var describe = lab.experiment; var before = lab.before; var after = lab.after; var it = lab.it; var Code = require('code'); var expect = Code.expect; var actionTypes = require('./_action_types'); var rootReducer = require('./reducers'); var timers = require('timers'); var async = require('async'); var PouchDB = require('pouchdb'); var db = new PouchDB('todos', { db: require('memdown'), }); var redux = require('redux'); var PouchMiddleware = require('../src/'); describe('Pouch Redux Middleware', function() { var pouchMiddleware; var store; it('can be created', function(done) { pouchMiddleware = PouchMiddleware({ path: '/todos', db: db, actions: { remove: (doc) => { return {type: actionTypes.DELETE_TODO, id: doc._id} }, insert: (doc) => { return {type: actionTypes.INSERT_TODO, todo: doc} }, batchInsert: (docs) => { return {type: actionTypes.BATCH_INSERT_TODOS, todos: docs} }, update: (doc) => { return {type: actionTypes.UPDATE_TODO, todo: doc} } }, changeFilter: doc => !doc.filter }); done(); }); it('can be used to create a store', function(done) { var createStoreWithMiddleware = redux.applyMiddleware(pouchMiddleware)(redux.createStore); store = createStoreWithMiddleware(rootReducer); done(); }); it('accepts a few inserts', function(done) { store.dispatch({type: actionTypes.ADD_TODO, text: 'do laundry', id: 'a'}); store.dispatch({type: actionTypes.ADD_TODO, text: 'wash dishes', id: 'b'}); timers.setTimeout(done, 100); }); it('saves changes in pouchdb', function(done) { async.map(['a', 'b'], db.get.bind(db), function(err, results) { if (err) return done(err); expect(results.length).to.equal(2); expect(results[0].text).to.equal('do laundry'); expect(results[1].text).to.equal('wash dishes'); done(); }); }); it('accepts an edit', function(done) { store.dispatch({type: actionTypes.EDIT_TODO, text: 'wash all the dishes', id: 'b'}); timers.setTimeout(done, 100); }); it('saves changes in pouchdb', function(done) { async.map(['a', 'b'], db.get.bind(db), function(err, results) { if (err) return done(err); expect(results.length).to.equal(2); expect(results[0].text).to.equal('do laundry'); expect(results[1].text).to.equal('wash all the dishes'); done(); }); }); it('accepts an removal', function(done) { store.dispatch({type: actionTypes.DELETE_TODO, id: 'a'}); timers.setTimeout(done, 100); }); it('saves changes in pouchdb', function(done) { db.get('a', function(err) { expect(err).to.be.an.object(); expect(err.message).to.equal('missing'); done(); }); }); it('making changes in pouchdb...', function(done) { db.get('b', function(err, doc) { expect(err).to.equal(null); doc.text = 'wash some of the dishes'; db.put(doc, done); }); }); it('waiting a bit', function(done) { timers.setTimeout(done, 100); }); it('...propagates update from pouchdb', function(done) { expect(store.getState().todos.filter(function(doc) { return doc._id == 'b'; })[0].text).to.equal('wash some of the dishes'); done(); }); it('making removal in pouchdb...', function(done) { db.get('b', function(err, doc) { expect(err).to.equal(null); db.remove(doc, done); }); }); it('waiting a bit', function(done) { timers.setTimeout(done, 100); }); it('...propagates update from pouchdb', function(done) { expect(store.getState().todos.filter(function(doc) { return doc._id == 'b'; }).length).to.equal(0); done(); }); it('making insert in pouchdb...', function(done) { db.post({ _id: 'c', text: 'pay bills', }, done); }); it('waiting a bit', function(done) { timers.setTimeout(done, 100); }); it('...propagates update from pouchdb', function(done) { expect(store.getState().todos.filter(function(doc) { return doc._id == 'c'; })[0].text).to.equal('pay bills'); done(); }); it('...inserts filtered document', function(done) { db.post({ _id: 'd', filter: true, }).then(() => done()).catch(done); }); it('waiting a bit', function(done) { timers.setTimeout(done, 100); }); it('...filters documents', function(done) { expect(store.getState().todos.filter(function(doc) { return doc._id == 'd'; }).length).to.equal(0); done(); }); it('calles initialBatchDispatched', (done) => { const anotherMiddleware = PouchMiddleware({ path: '/todos', db: db, actions: { remove: (doc) => { return {type: actionTypes.DELETE_TODO, id: doc._id} }, insert: (doc) => { return {type: actionTypes.INSERT_TODO, todo: doc} }, batchInsert: (docs) => { return {type: actionTypes.BATCH_INSERT_TODOS, todos: docs} }, update: (doc) => { return {type: actionTypes.UPDATE_TODO, todo: doc} } }, initialBatchDispatched(err) { if (err) { return done(err); } var called = false; store.subscribe(() => { if (called) { done(new Error('expect subscribe to only be called once')); } called = true; expect(store.getState().todos.length).to.equal(1); timers.setTimeout(done, 100); }); expect(store.getState().todos.length).to.equal(2); store.dispatch({type: actionTypes.DELETE_TODO, id: 'c'}); } }); const store = redux.applyMiddleware(anotherMiddleware)(redux.createStore)(rootReducer); expect(store.getState().todos.length).to.equal(0); }); });