@oada/oada-cache-overmind
Version:
390 lines (382 loc) • 16.1 kB
JavaScript
import Promise from 'bluebird';
import md5 from 'md5';
import url from 'url'
import _ from 'lodash';
const debug = require('debug')('oada-cache-overmind:actions');
var namespace = null;
debug('oada-cache-overmind Running...')
//TODO: Completely do away with plural requests array syntax...
let syncs = {};
function ns(context) {
return _.mapValues(context, (obj) => {
if (namespace == null) return obj;
return _.get(obj, namespace);
})
}
function urlToConnectionId(domainUrl) {
let domain = url.parse(domainUrl).hostname;
return domain.replace(/\./g, '_')
}
function findSyncMatches(string) {
return Object.keys(syncs).filter(key => string.startsWith(key))
}
function handleDelete(target, toRemove, parentPath) {
//Remove any null leaf nodes, set values of any non-null nodes
_.forEach(toRemove, (value, key) => {
const setPath = parentPath ? `${parentPath}.${key}` : key;
if (value == null) {
_.unset(target, setPath)
} else if (_.isObject(value)) {
const newParentPath = parentPath == null ? key : `${parentPath}.${key}`;
handleDelete(target, value, newParentPath);
} else {
_.set(target, setPath, value);
}
});
}
module.exports = {
actions: function(_namespace) {
namespace = _namespace;
return {
connect(context, props) {
const {state, effects} = ns(context);
const connection_id = (props.connection_id || urlToConnectionId(props.domain))
return effects.connect({
connection_id,
domain: props.domain,
options: props.options,
cache: props.cache,
token: props.token,
websocket: props.websocket,
}).then( (response) => {
if (!state.defaultConn) state.defaultConn = connection_id;
if (state[connection_id] == null) state[connection_id] = {};
state[connection_id].token = response._token;
state[connection_id].domain = props.domain;
//Clear bookmarks if exist
if (state[connection_id].bookmarks) state[connection_id].bookmarks = {};
state.isAuthenticated = true;
return {token: response.token, connection_id};
}).catch((error) => {
state.error = {error: error.message};
state.isAuthenticated = false;
return {error}
});
},
handleWatch(context, props) {
const {state, effects} = ns(context);
//Loop through all changes in the response
/*
No need for this as long as oada effects are now @oada/client rather than oada-cache
const changes = _.get(props, 'response.change') || [];
if (!_.isArray(changes)) {
console.warn('oada-cache-overmind: Watch response received from oada server was in a unrecognized format.', props);
debug('WARNING: response.change not an array')
return;
}
const watchPath = (props.path && props.path.length > 0) ? `${props.connection_id}.${props.path}` : props.connection_id;
_.forEach(changes, (change) => {
*/
let connection_id = props.connection_id || state.defaultConn;
let watchPath = props.payload.watchPath || '';
watchPath = watchPath.replace(/\/$/, '')
watchPath = watchPath.replace(/^\//, '')
watchPath = watchPath ? `.${watchPath}` : watchPath;
let path = props.path.replace(/^\//, '');
path = path.replace(/\/$/, '');
path = props.path.split('/').join('.');
const currentState = _.get(state, `${connection_id}${watchPath}${path}`)
if (props.type == 'merge') {
//Get the currentState at the change path
//Merge in changes
_.merge(currentState, props.body);
} else if (props.type == 'delete') {
//Delete every leaf node in change body that is null, merge in all others (_rev, etc.)
let parentPath = watchPath.replace(/^\//, '').split('/').join('.');
handleDelete(currentState, props.body, parentPath);
} else {
debug('WARNING: Unrecognized change type', props.type)
}
// })
},
get(context, props) {
const {state, effects, actions} = ns(context);
var hasRequests = props.requests ? true : false;
var requests = props.requests || [props];
const PromiseMap = (props.concurrent) ? Promise.map : Promise.mapSeries;
return PromiseMap(requests, (request, i) => {
if (request.complete) return
let _statePath = request.path.replace(/^\//, '').split('/').join('.')
let connection_id = request.connection_id || props.connection_id || state.defaultConn;
if (request.watch) {
let conn = state[connection_id];
if (conn) {
if (conn && conn.watches && conn.watches[request.path]) return
request.watch.actions = [actions.handleWatch, ...(request.watch.actions || [])];
request.watch.payload = request.watch.payload || {};
request.watch.payload.connection_id = connection_id;
request.watch.payload.watchPath = _statePath;
}
}
return effects.get({
connection_id,
url: request.url,
path: request.path || request,
headers: request.headers,
watch: request.watch,
tree: request.tree || props.tree,
}).then((response) => {
let _responseData = response.data;
//Build out path one object at a time.
var path = `${connection_id}.${_statePath}`;
//Set response
if (_responseData) _.set(state, path, _responseData);
if (request.watch) {
path = `${connection_id}.watches.${request.path}`;
_.set(state, path, true);
}
requests[i].complete = true;
return response;
}).catch((error) => {
return {error, ...error.response}
})
}).then((responses) => {
return hasRequests ? {responses, requests} : responses[0];
})
},
head(context, props) {
const {state, effects, actions} = ns(context);
var hasRequests = props.requests ? true : false;
var requests = props.requests || [props];
const PromiseMap = (props.concurrent) ? Promise.map : Promise.mapSeries;
return PromiseMap(requests, (request, i) => {
if (request.complete) return
let _statePath = request.path.replace(/^\//, '').split('/').join('.')
let connection_id = request.connection_id || props.connection_id || state.defaultConn;
return effects.head({
connection_id,
url: request.url,
path: request.path,
headers: request.headers,
}).then((response) => {
let _responseData = response.data;
//Build out path one object at a time.
var path = `${connection_id}.${_statePath}`;
//Set response
if (_responseData) _.set(state, path, _responseData);
if (request.watch) {
path = `${connection_id}.watches.${request.path}`;
_.set(state, path, true);
}
requests[i].complete = true;
return response;
}).catch((error) => {
return {error, ...error.response}
})
}).then((responses) => {
return hasRequests ? {responses, requests} : responses[0];
})
},
put(context, props) {
const {state, effects, actions} = ns(context);
var hasRequests = props.requests ? true : false;
var requests = props.requests || [props];
const PromiseMap = (props.concurrent) ? Promise.map : Promise.mapSeries;
return PromiseMap(requests, (request, i) => {
if (request.complete) return;
let connection_id = request.connection_id || props.connection_id || state.defaultConn;
return effects.put({
url: request.url, //props.domain + ((request.path[0] === '/') ? '':'/') + request.path,
path: request.path,
data: request.data,
type: request.type,
headers: request.headers,
tree: request.tree || props.tree,
connection_id,
}).then((response) => {
/*
var path = `${request.connection_id || props.connection_id}${request.path.split("/").join(".")}`;
var oldState = _.cloneDeep(_.get(state, path));
var newState = _.merge(oldState, request.data);
// Optimistic update
_.set(state, path, newState)
*/
requests[i].complete = true;
return response;
});
}).then((responses) => {
return hasRequests ? {responses, requests} : responses[0];
});
},
post(context, props) {
const {state, effects, actions} = ns(context);
var hasRequests = props.requests ? true : false;
var requests = props.requests || [props];
const PromiseMap = (props.concurrent) ? Promise.map : Promise.mapSeries;
return PromiseMap(requests, (request, i) => {
if (request.complete) return;
let connection_id = request.connection_id || props.connection_id || state.defaultConn;
return effects.post({
url: request.url, //props.domain + ((request.path[0] === '/') ? '':'/') + request.path,
path: request.path,
data: request.data,
type: request.type,
headers: request.headers,
tree: request.tree || props.tree,
connection_id,
})
.then((response) => {
/*
var id = response.headers.location; //TODO why is this here?
var path = `${request.connection_id || props.connection_id}${request.path.split("/").join(".")}`;
var oldState = _.cloneDeep(_.get(state, path));
var newState = _.merge(oldState, request.data);
_.set(state, path, newState)
*/
requests[i].complete = true;
return response;
});
}).then((responses) => {
return hasRequests ? {responses, requests} : responses[0];
});
},
delete(context, props) {
const {state, effects, actions} = ns(context);
var hasRequests = props.requests ? true : false;
var requests = props.requests || [props];
const PromiseMap = (props.concurrent) ? Promise.map : Promise.mapSeries;
return PromiseMap(requests, (request, i) => {
if (request.complete) return;
const connection_id = request.connection_id || props.connection_id || state.defaultConn;
let _statePath = request.path.replace(/^\//, "").split("/").join(".");
let conn = _.get(state, connection_id);
if (request.unwatch && conn && conn.watches) {
// Don't send the unwatch request if it isn't being watched already.
if (!conn.watches[request.path]) return;
}
return effects.delete({
connection_id,
url: request.url,
path: request.path,
headers: request.headers,
unwatch: request.unwatch,
type: request.type,
tree: request.tree || props.tree,
})
.then((response) => {
//Handle watches index and optimistically update
if (request.unwatch && conn && conn.watches) {
_.unset(state,`${connection_id}.watches.${request.path}`);
} /*else {
_.unset(state,`${connection_id}.${_statePath}`);
}*/
requests[i].complete = true;
return response;
});
}).then((responses) => {
return hasRequests ? {responses, requests} : responses[0];
});
},
disconnect(context, props) {
const {state, effects} = ns(context);
var requests = props.requests || [props];
return Promise.mapSeries(requests, (request, i) => {
const connection_id = request.connection_id || props.connection_id || state.defaultConn;
return effects.disconnect({ connection_id });
})
},
resetCache(context, props) {
//Currently oada-cache resets all of the cache, not just the db for a single connection_id
const {effects, state} = ns(context);
const connection_id = request.connection_id || props.connection_id || state.defaultConn;
return effects.resetCache({ connection_id });
},
ensurePath(context, props) {
const {state, actions, effects} = ns(context);
let { ensure, path, tree, watch } = props;
const connection_id = request.connection_id || props.connection_id || state.defaultConn;
return effects.head({
path,
tree,
connection_id,
}).catch(err => {
if (err.status === 404) return effects.put({
tree,
connection_id,
path,
data: {},
})
})
},
async sync(context, props) {
let { ensure, path, tree } = props;
const connection_id = request.connection_id || props.connection_id || state.defaultConn;
const {state, actions, effects} = ns(context);
let requests = [{
connection_id,
path,
tree,
watch: {
actions: props.actions || [],
},
}];
if (ensure !== false) await actions.ensurePath(_.clone(props));
let re = await actions.get({requests})
// register the sync with the handleSyncs function commenced on initialization of overmind
let p = 'oada.'+connection_id+'.'+(path).replace(/^\//, '').replace(/\/$/,'').split('/').join('.');
requests[0].path = p;
syncs[p] = requests[0];
debug(`sync set on path ${p}`)
return requests[0]
},
killSync(context, {}) {
const {state, actions, effects} = ns(context);
// unwatch,
},
test(context, {}) {
const {state, effects} = ns(context);
}
}
},
onInitialize: function(context, overmind) {
let {state, actions, effects} = ns(context);
function handleSyncs(mutation) {
if (!/^oada/.test(mutation.path)) return
//Find sync entries in which the path matches the mutation path
let keys = findSyncMatches(mutation.path);
// sync matches to oada
keys.forEach(async (key) => {
console.log('Sync match: ', key);
console.log('Mutation: ', mutation)
if (mutation.method === "set") {
console.log('Send put request:', {
connection_id: syncs[key].connection_id,
tree: syncs[key].tree,
data: mutation.args[0],
path: '/' + mutation.path.split('.').slice(2).join('/')
});
// TODO: Handle errors here regarding the optimistic update mutation that caused this.
await actions.put({requests: [{
connection_id: syncs[key].connection_id,
tree: syncs[key].tree,
data: mutation.args[0],
path: '/' + mutation.path.split('.').slice(2).join('/')
}]})
} else if (mutation.method === 'unset') {
console.log('Send delete request:', {
connection_id: syncs[key].connection_id,
tree: syncs[key].tree,
path: '/' + mutation.path.split('.').slice(2).join('/')
});
// TODO: Handle errors here regarding the optimistic update mutation that caused this.
await actions.delete({requests: [{
connection_id: syncs[key].connection_id,
tree: syncs[key].tree,
path: '/' + mutation.path.split('.').slice(2).join('/')
}]})
}
})
}
overmind.addMutationListener(handleSyncs)
},
}