lemon-core
Version:
Lemon Serverless Micro-Service Platform
969 lines • 34.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fromTTL = exports.toTTL = exports.sleep = exports.DummyCacheService = exports.CacheService = void 0;
/**
* `cache-services.ts`
* - common service for remote cache
*
* @author Tim Hong <tim@lemoncloud.io>
* @date 2020-12-02 initial version
* @author Steve <steve@lemoncloud.io>
* @date 2022-04-01 optimized for `AbstractProxy`
*
* @copyright (C) lemoncloud.io 2020 - All Rights Reserved.
*/
const util_1 = require("util");
const node_cache_1 = __importDefault(require("node-cache"));
const memcached_1 = __importDefault(require("memcached"));
const ioredis_1 = __importDefault(require("ioredis"));
const engine_1 = require("../../engine");
const NS = engine_1.$U.NS('CCHS', 'green'); // NAMESPACE TO BE PRINTED.
/**
* class `CacheService`
* - common service to provide cache
*/
class CacheService {
/**
* Protected constructor -> use CacheService.create()
* WARN! - do not create directly.˜
*
* @param backend cache backend object
* @param params params to create service.
* @protected
*/
constructor(backend, params) {
if (!backend)
throw new Error(` (cache-backend) is required!`);
const ns = (params === null || params === void 0 ? void 0 : params.ns) || '';
(0, engine_1._inf)(NS, `! cache-service instantiated with [${backend.name}] backend. [ns=${ns}]`);
this.backend = backend;
this.ns = ns;
this.options = params.options;
this.maker = params === null || params === void 0 ? void 0 : params.maker;
}
/**
* Factory method
*
* @param options (optional) cache options
* @param maker (optional) custome cache-pkey generator.
* @static
*/
static create(options, maker) {
const type = (options === null || options === void 0 ? void 0 : options.type) || 'redis';
const endpoint = (options === null || options === void 0 ? void 0 : options.endpoint) || engine_1.$U.env(CacheService.ENV_CACHE_ENDPOINT);
const ns = (options === null || options === void 0 ? void 0 : options.ns) || '';
const defTimeout = engine_1.$U.N(options === null || options === void 0 ? void 0 : options.defTimeout, engine_1.$U.N(engine_1.$U.env(CacheService.ENV_CACHE_DEFAULT_TIMEOUT), CacheService.DEF_CACHE_DEFAULT_TIMEOUT));
options = Object.assign(Object.assign({}, options), { type, endpoint, ns, defTimeout });
(0, engine_1._log)(NS, `constructing [${type}] cache ...`);
(0, engine_1._log)(NS, ` > endpoint =`, endpoint);
(0, engine_1._log)(NS, ` > ns =`, ns);
(0, engine_1._log)(NS, ` > defTimeout =`, defTimeout);
let backend;
switch (type) {
case 'memcached':
backend = new MemcachedBackend(endpoint, defTimeout);
break;
case 'redis':
backend = new RedisBackend(endpoint, defTimeout);
break;
default:
throw new Error(` [${type}] is invalid - CacheService.create()`);
}
return new CacheService(backend, { ns, options, maker });
}
/**
* Say hello
*/
hello() {
return `cache-service:${this.backend.name}:${this.ns}`;
}
/**
* for convient, make another typed service.
* - it add `type` into key automatically.
*
* @param type model-type like 'test'
* @param delimiter (optional) delim bewteen type and key (default ':')
* @returns the typed CacheService
*/
cloneByType(type, delimiter = ':') {
const { backend, ns, options } = this;
const maker = (ns, delim, key) => this.asNamespacedKey(`${type}${delimiter}${key}`);
return new CacheService(backend, { ns, options, maker });
}
/**
* Close backend and connection
*/
close() {
return __awaiter(this, void 0, void 0, function* () {
yield this.backend.close();
});
}
/**
* Check whether the key is cached
*
* @return true if the key is cached
*/
exists(key) {
return __awaiter(this, void 0, void 0, function* () {
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.has(namespacedKey);
(0, engine_1._log)(NS, `.exists ${namespacedKey} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* List all keys
*
* @return list of keys
*/
keys() {
return __awaiter(this, void 0, void 0, function* () {
const namespacedKeys = yield this.backend.keys();
const ret = namespacedKeys.reduce((keys, namespacedKey) => {
const [ns, key] = namespacedKey.split(CacheService.NAMESPACE_DELIMITER);
if (ns === this.ns)
keys.push(key);
return keys;
}, []);
(0, engine_1._log)(NS, `.keys / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Store a key
*
* @param key
* @param val
* @param timeout (optional) TTL in seconds or Timeout object
* @return true on success
*/
set(key, val, timeout) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
if (val === undefined)
throw new Error(` (CacheValue) cannot be undefined.`);
const namespacedKey = this.asNamespacedKey(key);
const ttl = timeout && toTTL(timeout);
const ret = yield this.backend.set(namespacedKey, val, ttl);
(0, engine_1._log)(NS, `.set ${namespacedKey} ${val} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Store multiple keys
*
* @param entries
* @return true on success
*/
setMulti(entries) {
return __awaiter(this, void 0, void 0, function* () {
const param = entries.map(({ key, val, timeout }, idx) => {
if (!key)
throw new Error(`.key (CacheKey) is required (at [${idx}]).`);
if (val === undefined)
throw new Error(`.val (CacheValue) cannot be undefined (at [${idx}]).`);
return {
key: this.asNamespacedKey(key),
val,
ttl: timeout && toTTL(timeout),
};
});
const ret = yield this.backend.mset(param);
(0, engine_1._log)(NS, `.setMulti ${entries.map(entry => entry.key)} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Retrieve a key
*
* @param key
*/
get(key) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.get(namespacedKey);
(0, engine_1._log)(NS, `.get ${namespacedKey} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Get multiple keys
*
* @param keys
*/
getMulti(keys) {
return __awaiter(this, void 0, void 0, function* () {
const namespacedKeys = keys.map((key, idx) => {
if (!key)
throw new Error(` (CacheKey) is required (at [${idx}]).`);
return this.asNamespacedKey(key);
});
const map = yield this.backend.mget(namespacedKeys);
// Remove namespace prefix from keys
const ret = Object.entries(map).reduce((newMap, [namespacedKey, val]) => {
const key = namespacedKey.split(CacheService.NAMESPACE_DELIMITER)[1];
newMap[key] = val;
return newMap;
}, {});
(0, engine_1._log)(NS, `.getMulti ${namespacedKeys} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Increment the integer value of a key
*
* @param key
* @param inc number to increment
*/
increment(key, inc) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
if (inc === undefined)
throw new Error(` (number) cannot be undefined.`);
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.incr(namespacedKey, inc);
(0, engine_1._log)(NS, `.increment ${namespacedKey} ${inc} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* same as increment()
*/
inc(key, inc) {
return this.increment(key, inc);
}
/**
* Set the value of a key and return its old value
*/
getAndSet(key, val) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
if (val === undefined)
throw new Error(` (CacheValue) cannot be undefined.`);
const namespacedKey = this.asNamespacedKey(key);
let ret;
if (this.backend.getset) {
ret = yield this.backend.getset(namespacedKey, val);
}
else {
ret = yield this.backend.get(namespacedKey);
// Best effort to keep remaining TTL
let ttl = yield this.backend.ttl(namespacedKey);
if (ttl !== undefined) {
ttl = Math.ceil(ttl / 1000);
}
if (!(yield this.backend.set(namespacedKey, val, ttl)))
throw new Error(`getAndSet() failed`);
}
(0, engine_1._log)(NS, `.getAndSet ${namespacedKey} ${val} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Get and delete the key
*
* @param key
*/
getAndDelete(key) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
const namespacedKey = this.asNamespacedKey(key);
let ret;
if (this.backend.pop) {
ret = yield this.backend.pop(namespacedKey);
}
else {
ret = yield this.backend.get(namespacedKey);
yield this.backend.del(namespacedKey);
}
(0, engine_1._log)(NS, `.getAndDelete ${namespacedKey} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Delete a key
*
* @param key
* @return true on success
*/
delete(key) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.del(namespacedKey);
(0, engine_1._log)(NS, `.delete ${namespacedKey} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Delete multiple keys
*
* @param keys
* @return number of deleted entries
*/
deleteMulti(keys) {
return __awaiter(this, void 0, void 0, function* () {
const namespacedKeys = keys.map((key, idx) => {
if (!key)
throw new Error(` (CacheKey) is required (at [${idx}]).`);
return this.asNamespacedKey(key);
});
const promises = namespacedKeys.map(namespacedKey => this.backend.del(namespacedKey));
const ret = yield Promise.all(promises);
(0, engine_1._log)(NS, `.deleteMulti ${namespacedKeys} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Set or update the timeout of a key
*
* @param key
* @param timeout TTL in seconds or Timeout object
* @return true on success
*/
setTimeout(key, timeout) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.expire(namespacedKey, toTTL(timeout));
(0, engine_1._log)(NS, `.setTimeout ${namespacedKey} ${timeout} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Get remaining time to live in milliseconds
*
* @return
* - number of milliseconds to expire
* - undefined if the key does not exist
* - 0 if the key has no timeout
*/
getTimeout(key) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.ttl(namespacedKey);
(0, engine_1._log)(NS, `.getTimeout ${namespacedKey} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Remove the timeout from a key
*
* @param key
*/
removeTimeout(key) {
return __awaiter(this, void 0, void 0, function* () {
if (!key)
throw new Error(` (CacheKey) is required.`);
const namespacedKey = this.asNamespacedKey(key);
const ret = yield this.backend.expire(namespacedKey, 0);
(0, engine_1._log)(NS, `.removeTimeout ${namespacedKey} / ret =`, typeof ret === 'string' ? ret : engine_1.$U.json(ret));
return ret;
});
}
/**
* Get namespace prefixed cache key
*
* @param key
* @protected
*/
asNamespacedKey(key) {
const [ns, delim] = [this.ns, CacheService.NAMESPACE_DELIMITER];
if (this.maker)
return this.maker(ns, delim, key);
return `${ns}${delim}${key}`;
}
}
exports.CacheService = CacheService;
/**
* Environment variable name for cache server endpoint
* @static
*/
CacheService.ENV_CACHE_ENDPOINT = 'CACHE_ENDPOINT';
/**
* Environment variable name for default cache timeout
* @static
*/
CacheService.ENV_CACHE_DEFAULT_TIMEOUT = 'CACHE_DEFAULT_TIMEOUT';
/**
* Default cache timeout
* @static
*/
CacheService.DEF_CACHE_DEFAULT_TIMEOUT = 24 * 60 * 60; // 1-day
/**
* Namespace delimiter
* @private
* @static
*/
CacheService.NAMESPACE_DELIMITER = '::';
/**
* class `DummyCacheService`: use 'node-cache' library
*/
class DummyCacheService extends CacheService {
/**
* Factory method
*
* @param options (optional) cache options
* @static
*/
static create(options) {
const ns = (options === null || options === void 0 ? void 0 : options.ns) || '';
const defTimeout = engine_1.$U.N(options === null || options === void 0 ? void 0 : options.defTimeout, engine_1.$U.N(engine_1.$U.env(CacheService.ENV_CACHE_DEFAULT_TIMEOUT), CacheService.DEF_CACHE_DEFAULT_TIMEOUT));
options = Object.assign(Object.assign({}, options), { ns, defTimeout });
(0, engine_1._log)(NS, `constructing dummy cache ...`);
// NOTE: Use singleton backend instance
// because node-cache is volatile and client instance does not share keys with other instance
if (!DummyCacheService.backend)
DummyCacheService.backend = new NodeCacheBackend(defTimeout);
return new DummyCacheService(DummyCacheService.backend, { ns, options });
}
/**
* Say hello
*/
hello() {
return `dummy-${super.hello()}`;
}
}
exports.DummyCacheService = DummyCacheService;
/**
* function `sleep`
* @param ms duration in milliseconds
*/
function sleep(ms) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => setTimeout(resolve, ms));
});
}
exports.sleep = sleep;
/**
* Get TTL from timeout
* @param timeout timeout in seconds or Timeout object
* @return remaining time to live in seconds
*/
function toTTL(timeout) {
switch (typeof timeout) {
case 'number':
return timeout;
case 'object':
if (!timeout)
return 0;
const { expireIn, expireAt } = timeout;
if (typeof expireIn === 'number')
return expireIn;
if (typeof expireAt === 'number') {
const msTTL = timeout.expireAt - Date.now();
return Math.ceil(msTTL / 1000);
}
break;
}
throw new Error(` (number | Timeout) is invalid.`);
}
exports.toTTL = toTTL;
/**
* Get timestamp of expiration from TTL
* @param ttl remaining time to live in seconds
* @return timestamp in milliseconds since epoch
*/
function fromTTL(ttl) {
return ttl > 0 ? Date.now() + ttl * 1000 : 0;
}
exports.fromTTL = fromTTL;
/** ********************************************************************************************************************
* Internal Classes
** ********************************************************************************************************************/
/**
* class `NodeCacheBackend`: use 'node-cache' library
* @internal
*/
class NodeCacheBackend {
/**
* Public constructor
*/
constructor(defTTL = 0) {
/**
* backend type
*/
this.name = 'node-cache';
this.cache = new node_cache_1.default({ stdTTL: defTTL });
}
/**
* CacheBackend.set implementation
*/
set(key, val, ttl) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.set(key, val, ttl);
});
}
/**
* CacheBackend.get implementation
*/
get(key) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.get(key);
});
}
/**
* CacheBackend.mset implementation
*/
mset(entries) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.mset(entries);
});
}
/**
* CacheBackend.mget implementation
*/
mget(keys) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.mget(keys);
});
}
/**
* CacheBackend.pop implementation
*/
pop(key) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.take(key);
});
}
/**
* CacheBackend.incr implementation
*/
incr(key, increment) {
return __awaiter(this, void 0, void 0, function* () {
const org = this.cache.get(key);
if (typeof org !== 'number')
throw new Error(` [${key}] does not hold a number value.`);
const newVal = org + increment;
this.cache.set(key, newVal);
return newVal;
});
}
/**
* CacheBackend.keys implementation
*/
keys() {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.keys();
});
}
/**
* CacheBackend.has implementation
*/
has(key) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.has(key);
});
}
/**
* CacheBackend.del implementation
*/
del(key) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.del(key) === 1;
});
}
/**
* CacheBackend.expire implementation
*/
expire(key, ttl) {
return __awaiter(this, void 0, void 0, function* () {
return this.cache.ttl(key, ttl);
});
}
/**
* CacheBackend.ttl implementation
*/
ttl(key) {
return __awaiter(this, void 0, void 0, function* () {
const ts = this.cache.getTtl(key); // Timestamp in milliseconds
return ts && ts - Date.now();
});
}
/**
* CacheBackend.close implementation
*/
close() {
return __awaiter(this, void 0, void 0, function* () {
this.cache.close();
});
}
}
/**
* class `MemcachedBackend`
* @internal
*/
class MemcachedBackend {
/**
* Public constructor
*/
constructor(endpoint, defTTL = 0) {
/**
* backend type
*/
this.name = 'memcached';
const memcached = new memcached_1.default(endpoint || 'localhost:11211');
// Build promisified API map
this.api = {
get: (0, util_1.promisify)(memcached.get.bind(memcached)),
gets: (0, util_1.promisify)(memcached.gets.bind(memcached)),
getMulti: (0, util_1.promisify)(memcached.getMulti.bind(memcached)),
set: (0, util_1.promisify)(memcached.set.bind(memcached)),
cas: (0, util_1.promisify)(memcached.cas.bind(memcached)),
del: (0, util_1.promisify)(memcached.del.bind(memcached)),
items: (0, util_1.promisify)(memcached.items.bind(memcached)),
cachedump: (server, slabid, number) => {
return new Promise((resolve, reject) => {
memcached.cachedump(server, slabid, number, (err, cachedump) => {
if (err)
return reject(err);
if (!cachedump)
return resolve([]);
// Deep-copy를 안하면 데이터가 없어지는 이슈가 있음
resolve(Array.isArray(cachedump) ? [...cachedump] : [cachedump]);
});
});
},
end: memcached.end.bind(memcached),
};
// default TTL
this.defTTL = defTTL;
}
/**
* CacheBackend.set implementation
*/
set(key, val, ttl = this.defTTL) {
return __awaiter(this, void 0, void 0, function* () {
const entry = { val, exp: fromTTL(ttl) };
(0, engine_1._log)(NS, `[${this.name}-backend] storing to key [${key}] =`, engine_1.$U.json(entry));
return yield this.api.set(key, entry, ttl);
});
}
/**
* CacheBackend.get implementation
*/
get(key) {
return __awaiter(this, void 0, void 0, function* () {
const entry = yield this.api.get(key);
(0, engine_1._log)(NS, `[${this.name}-backend] entry fetched =`, engine_1.$U.json(entry));
return entry && entry.val;
});
}
/**
* CacheBackend.mset implementation
*/
mset(entries) {
return __awaiter(this, void 0, void 0, function* () {
(0, engine_1._log)(NS, `[${this.name}-backend] storing multiple keys ...`);
const promises = entries.map(({ key, val, ttl = this.defTTL }, idx) => {
const entry = { val, exp: fromTTL(ttl) };
(0, engine_1._log)(NS, ` ${idx}) key [${key}] =`, engine_1.$U.json(entry));
return this.api.set(key, entry, ttl);
});
const results = yield Promise.all(promises);
return results.every(result => result === true);
});
}
/**
* CacheBackend.mget implementation
*/
mget(keys) {
return __awaiter(this, void 0, void 0, function* () {
const map = yield this.api.getMulti(keys);
(0, engine_1._log)(NS, `[${this.name}-backend] entry map fetched =`, engine_1.$U.json(map));
Object.keys(map).forEach(key => {
const entry = map[key];
map[key] = entry.val;
});
return map;
});
}
/**
* CacheBackend.incr implementation
*/
incr(key, increment) {
return __awaiter(this, void 0, void 0, function* () {
// NOTE:
// Memcached는 음수에 대한 incr/decr를 지원하지 않으며 0 미만으로 decr 되지 않는다.
// 이런 이유로 sets & cas 조합을 이용해 직접 구현함
(0, engine_1._log)(NS, `[${this.name}-backend] incrementing (${increment}) to key [${key}] ...`);
// Use get/check-and-save + retry strategy for consistency
for (let retry = 0; retry < 5; yield sleep(10), retry++) {
const result = yield this.api.gets(key); // Get entry w/ CAS id
if (result === undefined) {
// Initialize to increment value if the key does not exist
if (!(yield this.set(key, increment, 0)))
break;
return increment;
}
else {
const { [key]: oldEntry, cas } = result;
if (typeof oldEntry.val !== 'number')
throw new Error(`.key [${key}] has non-numeric value.`);
// Preserve remaining lifetime w/ best effort strategy, not accurate
const now = Date.now();
const ttl = oldEntry.exp && Math.round((oldEntry.exp - now) / 1000);
const entry = {
val: oldEntry.val + increment,
exp: ttl && now + ttl * 1000,
};
if (yield this.api.cas(key, entry, cas, ttl))
return entry.val;
}
}
throw new Error(`[memcached] failed to increment key [${key}].`);
});
}
/**
* CacheBackend.keys implementation
*/
keys() {
return __awaiter(this, void 0, void 0, function* () {
// NOTE:
// memcached는 원래 keys 기능을 지원하지 않으며
// 아래와 같이 cachedump를 사용하여 가능하지만 set한 key가 dump 될 때 까지 상당한 시간이 소요되는 것으로 보인다.
// 따라서 이 operation의 결과를 신뢰하지 않도록 한다.
const item = (yield this.api.items())[0];
if (!item || Object.keys(item).length === 0)
return [];
const [server, slabid] = [item.server, Number(Object.keys(item)[0])];
const number = item[slabid].number;
const cachedump = yield this.api.cachedump(server, slabid, number);
return cachedump.map(({ key }) => key);
});
}
/**
* CacheBackend.has implementation
*/
has(key) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.api.get(key)) !== undefined;
});
}
/**
* CacheBackend.del implementation
*/
del(key) {
return __awaiter(this, void 0, void 0, function* () {
return yield this.api.del(key);
});
}
/**
* CacheBackend.expire implementation
*/
expire(key, ttl) {
return __awaiter(this, void 0, void 0, function* () {
let saved = false;
for (let retry = 0; !saved && retry < 5; yield sleep(10), retry++) {
const result = yield this.api.gets(key); // Get entry w/ CAS id
if (result === undefined)
break; // If key does not exist or already expired
// Refresh timeout
const { [key]: oldEntry, cas } = result;
const newEntry = {
val: oldEntry.val,
exp: ttl && Date.now() + ttl * 1000,
};
saved = yield this.api.cas(key, newEntry, cas, ttl);
}
return saved;
});
}
/**
* CacheBackend.ttl implementation
*/
ttl(key) {
return __awaiter(this, void 0, void 0, function* () {
const entry = yield this.api.get(key); // undefined if key does not exist
return (entry === null || entry === void 0 ? void 0 : entry.exp) && entry.exp - Date.now();
});
}
/**
* CacheBackend.close implementation
*/
close() {
return __awaiter(this, void 0, void 0, function* () {
this.api.end();
});
}
}
/**
* class `RedisBackend`
* @internal
*/
class RedisBackend {
/**
* Public constructor
*/
constructor(endpoint, defTTL = 0) {
/**
* backend type
*/
this.name = 'redis';
this.redis = new ioredis_1.default(endpoint || 'localhost:6379');
this.defTTL = defTTL;
}
/**
* CacheBackend.set implementation
*/
set(key, val, ttl = this.defTTL) {
return __awaiter(this, void 0, void 0, function* () {
const data = JSON.stringify(val); // Serialize
ttl > 0 ? yield this.redis.set(key, data, 'EX', ttl) : yield this.redis.set(key, data);
return true; // 'set' command always return OK
});
}
/**
* CacheBackend.get implementation
*/
get(key) {
return __awaiter(this, void 0, void 0, function* () {
const data = yield this.redis.get(key);
if (data !== null)
return JSON.parse(data); // Deserialize
});
}
/**
* CacheBackend.mset implementation
*/
mset(entries) {
return __awaiter(this, void 0, void 0, function* () {
// Create transaction pipeline
// -> MSET command를 사용할 수도 있으나 ttl 지정이 불가능하여 pipeline으로 구현함
const pipeline = entries.reduce((pipeline, { key, val, ttl = this.defTTL }) => {
const data = JSON.stringify(val); // Serialize
return ttl > 0 ? pipeline.set(key, data, 'EX', ttl) : pipeline.set(key, data);
}, this.redis.multi());
// Execute transaction
yield pipeline.exec(); // Always OK
return true;
});
}
/**
* CacheBackend.mget implementation
*/
mget(keys) {
return __awaiter(this, void 0, void 0, function* () {
const list = yield this.redis.mget(keys);
// Deserialize and map array into object
return list.reduce((map, data, idx) => {
if (data !== null) {
const key = keys[idx];
map[key] = JSON.parse(data); // Deserialize
}
return map;
}, {});
});
}
/**
* CacheBackend.getset implementation
*/
getset(key, val) {
return __awaiter(this, void 0, void 0, function* () {
const newData = JSON.stringify(val); // Serialize
const oldData = yield this.redis.getset(key, newData);
if (oldData !== null)
return JSON.parse(oldData); // Deserialize
});
}
/**
* CacheBackend.pop implementation
*/
pop(key) {
return __awaiter(this, void 0, void 0, function* () {
const [[err, data]] = yield this.redis
.multi()
.get(key) // read
.del(key) // and delete
.exec();
if (!err && data !== null)
return JSON.parse(data);
});
}
/**
* CacheBackend.incr implementation
*/
incr(key, increment) {
return __awaiter(this, void 0, void 0, function* () {
// Support both integer and floating point
const ret = yield this.redis.incrbyfloat(key, increment);
return Number(ret);
});
}
/**
* CacheBackend.keys implementation
*/
keys() {
return __awaiter(this, void 0, void 0, function* () {
return yield this.redis.keys('*');
});
}
/**
* CacheBackend.has implementation
*/
has(key) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.redis.exists(key)) > 0; // 1: exists / 0: does not exist
});
}
/**
* CacheBackend.del implementation
*/
del(key) {
return __awaiter(this, void 0, void 0, function* () {
return (yield this.redis.del(key)) === 1; // number of keys removed
});
}
/**
* CacheBackend.expire implementation
*/
expire(key, ttl) {
return __awaiter(this, void 0, void 0, function* () {
const ret = ttl > 0 ? yield this.redis.expire(key, ttl) : yield this.redis.persist(key);
return ret > 0; // 1: success / 0: key does not exist
});
}
/**
* CacheBackend.ttl implementation
*/
ttl(key) {
return __awaiter(this, void 0, void 0, function* () {
const ms = yield this.redis.pttl(key); // -2: key does not exist / -1: no timeout
if (ms >= 0)
return ms;
if (ms === -1)
return 0;
});
}
/**
* CacheBackend.close implementation
*/
close() {
return __awaiter(this, void 0, void 0, function* () {
yield this.redis.quit();
});
}
}
//# sourceMappingURL=cache-service.js.map