UNPKG

lemon-core

Version:
969 lines 34.6 kB
"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(`@backend (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 [${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(`@key (CacheKey) is required.`); if (val === undefined) throw new Error(`@val (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 @entries[${idx}]).`); if (val === undefined) throw new Error(`.val (CacheValue) cannot be undefined (at @entries[${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(`@key (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(`@key (CacheKey) is required (at @keys[${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(`@key (CacheKey) is required.`); if (inc === undefined) throw new Error(`@inc (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(`@key (CacheKey) is required.`); if (val === undefined) throw new Error(`@val (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(`@key (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(`@key (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(`@key (CacheKey) is required (at @keys[${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(`@key (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(`@key (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(`@key (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(`@timeout (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 [${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