@house-agency/brewstore
Version:
The Brewery Storage
176 lines (155 loc) • 4.51 kB
JavaScript
/**
* Handles Key/Value database storage ~= Redis
*/
const _ = require('lodash');
const conf = require('@house-agency/brewtils/config');
const errors = require('./errors');
const format = require('util').format;
const log = require('@house-agency/brewtils/log');
const q = require('q');
const redis = require('redis');
const rx = require('rx');
const listeners = {};
/**
* Creates a client for the key-value database
* It takes database connections info from config
* and starts listen for incoming calls and errors
*
* @return {object} Key-value database client (redis)
*/
function get_client() {
return conf('storage.keyvalue')
.then(props => {
log('debug', 'Connecting to key/value storage', props);
const deferred = q.defer();
const client = redis.createClient(
props.port,
props.host
);
// Selects which database to use
client.select(props.db);
// Listen for ready and error events.
rx.Observable.merge(
rx.Observable.fromEvent(client, 'ready')
.first(),
rx.Observable.fromEvent(client, 'error')
.map(error => {
throw new Error(error);
})
)
.subscribe(
() => {
deferred.resolve(client);
},
error => {
log('fatal', 'Redis error', error);
deferred.reject(error);
}
);
return deferred.promise;
});
}
/**
* Creates a new client, starts listen on messages and
* returns an obvervable.
*
* @return {object} Promise with database client
*/
function get_listener() {
return get_client()
.then(db => {
return q.all([
db,
rx.Observable.fromEvent(
db,
'message',
(channel, message) => {
log('debug', 'Publishing', channel, message);
return {
db: db,
channel: channel,
message: message
};
}
)
]);
});
}
/**
* Wraps database calls to make the normalized
* and throws error when nothing found.
*
* @param {object} client
* @param {string} key Identification key string
* @param {string} method Method to run at database
* @param {array} args Method arguments
* @return {object} Results or throws error if empty results
*/
function run(client, key, method, args) {
if (!args) {
args = [];
}
if (!_.isArray(args)) {
args = _.toArray(arguments).slice(3);
}
return client
.then(db => {
args.unshift(key);
return q.npost(db, method, args);
})
.catch(error => {
log('fatal', 'Redis error while executing', method, 'with', key, 'for', args, error);
return q.reject(error);
})
.then(val => {
if (val === null) {
throw new errors.EmptyRecordError(format('%s not found', key));
}
return val;
});
}
/**
* Creates or finds an already existing listener as a rx observable
* returned in a promise.
* If creating new clients and observables for every listeners, the
* number or file descriptors will run out!
*
* @param {object} client Client to use for listening, created and wrapped in wrapper()
* @param {string} channel Channel to listen to
* @return {object} Promise with an observable
*/
function listen(client, channel) {
return client
.spread((db, listener) => {
log('debug', 'Starts listen to', channel);
// Check for already created listeners
if (!listeners[channel]) {
log('debug', 'Created new listener for', channel);
listeners[channel] = listener
// Filter for specified channel
.filter(obj => {
return obj.channel === channel;
})
// Map returned data to message
.map(obj => {
return obj.message;
});
// And then subscribe on channel
db.subscribe(channel);
}
return listeners[channel];
});
}
/**
* Wraps client with runner and listener functions
*/
const client = get_client();
const wrapper = Object.assign(
client,
{
get_client: get_client,
run:_.wrap(client, run),
listen:_.wrap(get_listener(), listen)
}
);
module.exports = wrapper;