Skip to content

Instantly share code, notes, and snippets.

@devel-pa
Last active May 11, 2017 07:13
Show Gist options
  • Select an option

  • Save devel-pa/e104d518b07dda068b55f9a549112cca to your computer and use it in GitHub Desktop.

Select an option

Save devel-pa/e104d518b07dda068b55f9a549112cca to your computer and use it in GitHub Desktop.
Redux middleware for GunDB. Low perfomance. Supports arrays.
/**
* Created by Paul on 10/7/2016.
* GunDB v0.3
* inspired by https://github.com/pgte/pouch-redux-middleware
* TODO: issue: stores duplicates in an object path.docs for removing redundant operations
* - can be implemented with redux history, but same lot of data
* - checking with gun, don't know how, yet
* - another method is eventually to store timestamps (updated_at) in redux and only that in path.docs
* TODO: issue: not quite async as it should be in certain areas
*/
/*global Gun*/
/*eslint no-undef: "off"*/
import jPath from 'json-path';
import equal from 'deep-equal';
import extend from 'xtend';
export default function GunMiddleware(gun, _options) {
// let count = 1;
// let countMap = 1;
// let countState = 1;
let paths = _options || [];
if (! paths.length) {
throw new Error('GunMiddleware: no paths');
}
const defaultSpec = {
path: '.',
remove,
put,
reduxRemove,
reduxUpdate,
reduxInsert,
docs: {},
actions: {
remove: defaultAction('remove'),
update: defaultAction('update'),
insert: defaultAction('insert')
}
};
let gunner = {};
paths = paths.map(function(path) {
let spec = extend({}, defaultSpec, path);
spec.actions = extend({}, defaultSpec.actions, spec.actions);
spec.docs = {};
gunner[path.path] = gun.get(path.path.slice(1));
return spec;
});
function toRedux(val, id, db, k) {
let ret = id ? { id } : {};
//for nested objects I don't need an ID
if(k) id = k;
//reentering in Gun
let base = Gun.is(val) ? val : db.path(id);
//can't use `base` as I can't get values. Why?
let keys = Object.keys(val);
keys.shift();
if(keys[0] === "[") {
keys.shift();
let arr = [];
keys.forEach(function (key) {
let current = val[key];
if (current['#']) {
base.path(key).val(function (value) {
arr.push(toRedux(value, void 0, base, key));
});
}
});
return arr;
}
else {
keys.forEach(function (key) {
let current = val[key];
if (current && current['#']) {
base.path(key).val(function (value) {
ret[key] = toRedux(value, void 0, base, key);
});
}
else {
ret[key] = current;
}
});
}
return ret;
}
function array2object(arr) {
var obj = { "[": true };
Gun.list.map(arr, function (v, f, t) {
if (Gun.list.is(v)) {
obj[Gun.text.random()] = array2object(v);
return;
}
if (Gun.obj.is(v) && v !== null) {
obj[Gun.text.random()] = toGun(v);
return;
}
if(v === false) {
ret[key] = v;
return;
}
obj[Gun.text.random()] = v || null;
});
return obj;
}
function toGun(val) {
// let ret = extend({}, val);
let ret = {};
let keys = Object.keys(val);
keys.forEach(function (key) {
let crt = val[key];
if(Gun.list.is(crt)) {
ret[key] = array2object(crt);
return;
}
if(Gun.obj.is(crt) && crt !== null) {
ret[key] = toGun(crt);
return;
}
if(crt === false) {
ret[key] = crt;
return;
}
ret[key] = crt || null;
});
delete ret.id;
return ret;
}
function listen(path, dispatch) {
let db = gunner[path.path];
db.map(function (val, id) {
// console.log('map: ', count++, countMap++);
if(! val) {
if(path.docs[id]) {
// delete path.docs[id]; //useful? or in fact generates issues?
return path.reduxRemove({ id }, dispatch);
}
else return void 0;
}
return onDbChange(path, toRedux(val, id, db), dispatch)
});
}
function processNewStateForPath(path, state) {
let db = gunner[path.path];
let docs = jPath.resolve(state, path.path);
if (docs && docs.length) {
docs.forEach(function(docs) {
// console.log('each: ', count++, countState++);
let diffs = differences(path.docs, docs);
diffs.put.forEach(doc => path.put(doc, db, path));
diffs.deleted.forEach(doc => path.remove(doc, db, path));
});
}
}
function put(doc, db, path) {
path.docs[doc.id] = extend({}, doc );
db.path(doc.id).put(toGun(doc));
}
function remove(doc, db, path) {
delete path.docs[doc.id];
db.path(doc.id).put(null);
}
function reduxRemove(doc, dispatch) {
dispatch(this.actions.remove(doc));
}
function reduxInsert(doc, dispatch) {
dispatch(this.actions.insert(doc));
}
function reduxUpdate(doc, dispatch) {
dispatch(this.actions.update(doc));
}
const middleware = store => {
paths.forEach((path) => listen(path, store.dispatch));
return next => action => {
let returnValue = next(action);
let newState = store.getState();
paths.forEach(path => processNewStateForPath(path, newState));
return returnValue;
}
};
return middleware;
}
function differences(oldDocs, newDocs) {
let result = {
put: [],
deleted: Object.keys(oldDocs).map(oldDocId => oldDocs[oldDocId]),
};
newDocs.forEach(function(newDoc) {
let id = newDoc.id;
if (! id) {
warn('doc with no id');
}
result.deleted = result.deleted.filter(doc => doc.id !== id);
let oldDoc = oldDocs[id];
if (! oldDoc || ! equal(oldDoc, newDoc)) {
result.put.push(newDoc);
}
});
return result;
}
function onDbChange(path, changed, dispatch) {
let changeDoc = changed;
if(path.changeFilter && (! path.changeFilter(changeDoc))) {
return;
}
let oldDoc = path.docs[changeDoc.id];
path.docs[changeDoc.id] = extend({}, changeDoc );
if (oldDoc) {
if(! equal(oldDoc, changeDoc)) {
path.reduxUpdate(changeDoc, dispatch);
}
} else {
path.reduxInsert(changeDoc, dispatch);
}
}
function warn(what) {
let fn = console.warn || console.log;
if (fn) {
fn.call(console, what);
}
}
function defaultAction(action) {
return function() {
throw new Error('no action provided for ' + action);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment