redis-dataloader
Version:
DataLoader Using Redis as a Cache
166 lines (148 loc) • 4.82 kB
JavaScript
const _ = require('lodash');
const DataLoader = require('dataloader');
const stringify = require('json-stable-stringify');
const mapPromise = (promise, fn) => Promise.all(promise.map(fn));
module.exports = fig => {
const redis = fig.redis;
const isIORedis = redis.constructor.name !== 'RedisClient';
const parse = (resp, opt) =>
new Promise((resolve, reject) => {
try {
if (resp === '' || resp === null) {
resolve(resp);
} else if (opt.deserialize) {
resolve(opt.deserialize(resp));
} else {
resolve(JSON.parse(resp));
}
} catch (err) {
reject(err);
}
});
const toString = (val, opt) => {
if (val === null) {
return Promise.resolve('');
} else if (opt.serialize) {
return Promise.resolve(opt.serialize(val));
} else if (_.isObject(val)) {
return Promise.resolve(JSON.stringify(val));
} else {
return Promise.reject(new Error('Must be Object or Null'));
}
};
const makeKey = (keySpace, key, cacheKeyFn) =>
`${keySpace ? keySpace + ':' : ''}${cacheKeyFn(key)}`;
const rSetAndGet = (keySpace, key, rawVal, opt) =>
toString(rawVal, opt).then(
val =>
new Promise((resolve, reject) => {
const fullKey = makeKey(keySpace, key, opt.cacheKeyFn);
const multi = redis.multi();
multi.set(fullKey, val);
if (opt.expire) {
multi.expire(fullKey, opt.expire);
}
multi.get(fullKey);
multi.exec((err, replies) => {
const lastReply = isIORedis
? _.last(_.last(replies))
: _.last(replies);
return err ? reject(err) : parse(lastReply, opt).then(resolve);
});
})
);
const rGet = (keySpace, key, opt) =>
new Promise((resolve, reject) =>
redis.get(
makeKey(keySpace, key, opt.cacheKeyFn),
(err, result) => (err ? reject(err) : parse(result, opt).then(resolve))
)
);
const rMGet = (keySpace, keys, opt) =>
new Promise((resolve, reject) =>
redis.mget(
_.map(keys, k => makeKey(keySpace, k, opt.cacheKeyFn)),
(err, results) => {
return err
? reject(err)
: mapPromise(results, r => parse(r, opt)).then(resolve);
}
)
);
const rDel = (keySpace, key, opt) =>
new Promise((resolve, reject) =>
redis.del(
makeKey(keySpace, key, opt.cacheKeyFn),
(err, resp) => (err ? reject(err) : resolve(resp))
)
);
return class RedisDataLoader {
constructor(ks, userLoader, opt) {
const customOptions = [
'expire',
'serialize',
'deserialize',
'cacheKeyFn',
];
this.opt = _.pick(opt, customOptions) || {};
this.opt.cacheKeyFn =
this.opt.cacheKeyFn || (k => (_.isObject(k) ? stringify(k) : k));
this.keySpace = ks;
this.loader = new DataLoader(
keys =>
rMGet(this.keySpace, keys, this.opt).then(results =>
mapPromise(results, (v, i) => {
if (v === '') {
return Promise.resolve(null);
} else if (v === null) {
return userLoader
.load(keys[i])
.then(resp =>
rSetAndGet(this.keySpace, keys[i], resp, this.opt)
)
.then(r => (r === '' ? null : r));
} else {
return Promise.resolve(v);
}
})
),
_.chain(opt)
.omit(customOptions)
.extend({ cacheKeyFn: this.opt.cacheKeyFn })
.value()
);
}
load(key) {
return key
? Promise.resolve(this.loader.load(key))
: Promise.reject(new TypeError('key parameter is required'));
}
loadMany(keys) {
return keys
? Promise.resolve(this.loader.loadMany(keys))
: Promise.reject(new TypeError('keys parameter is required'));
}
prime(key, val) {
if (!key) {
return Promise.reject(new TypeError('key parameter is required'));
} else if (val === undefined) {
return Promise.reject(new TypeError('value parameter is required'));
} else {
return rSetAndGet(this.keySpace, key, val, this.opt).then(r => {
this.loader.clear(key).prime(key, r === '' ? null : r);
});
}
}
clear(key) {
return key
? rDel(this.keySpace, key, this.opt).then(() => this.loader.clear(key))
: Promise.reject(new TypeError('key parameter is required'));
}
clearAllLocal() {
return Promise.resolve(this.loader.clearAll());
}
clearLocal(key) {
return Promise.resolve(this.loader.clear(key));
}
};
};