| 'use strict'; |
| |
| const EventEmitter = require('events'); |
| const JSONB = require('json-buffer'); |
| |
| const loadStore = options => { |
| const adapters = { |
| redis: '@keyv/redis', |
| rediss: '@keyv/redis', |
| mongodb: '@keyv/mongo', |
| mongo: '@keyv/mongo', |
| sqlite: '@keyv/sqlite', |
| postgresql: '@keyv/postgres', |
| postgres: '@keyv/postgres', |
| mysql: '@keyv/mysql', |
| etcd: '@keyv/etcd', |
| offline: '@keyv/offline', |
| tiered: '@keyv/tiered', |
| }; |
| if (options.adapter || options.uri) { |
| const adapter = options.adapter || /^[^:+]*/.exec(options.uri)[0]; |
| return new (require(adapters[adapter]))(options); |
| } |
| |
| return new Map(); |
| }; |
| |
| const iterableAdapters = [ |
| 'sqlite', |
| 'postgres', |
| 'mysql', |
| 'mongo', |
| 'redis', |
| 'tiered', |
| ]; |
| |
| class Keyv extends EventEmitter { |
| constructor(uri, {emitErrors = true, ...options} = {}) { |
| super(); |
| this.opts = { |
| namespace: 'keyv', |
| serialize: JSONB.stringify, |
| deserialize: JSONB.parse, |
| ...((typeof uri === 'string') ? {uri} : uri), |
| ...options, |
| }; |
| |
| if (!this.opts.store) { |
| const adapterOptions = {...this.opts}; |
| this.opts.store = loadStore(adapterOptions); |
| } |
| |
| if (this.opts.compression) { |
| const compression = this.opts.compression; |
| this.opts.serialize = compression.serialize.bind(compression); |
| this.opts.deserialize = compression.deserialize.bind(compression); |
| } |
| |
| if (typeof this.opts.store.on === 'function' && emitErrors) { |
| this.opts.store.on('error', error => this.emit('error', error)); |
| } |
| |
| this.opts.store.namespace = this.opts.namespace; |
| |
| const generateIterator = iterator => async function * () { |
| for await (const [key, raw] of typeof iterator === 'function' |
| ? iterator(this.opts.store.namespace) |
| : iterator) { |
| const data = await this.opts.deserialize(raw); |
| if (this.opts.store.namespace && !key.includes(this.opts.store.namespace)) { |
| continue; |
| } |
| |
| if (typeof data.expires === 'number' && Date.now() > data.expires) { |
| this.delete(key); |
| continue; |
| } |
| |
| yield [this._getKeyUnprefix(key), data.value]; |
| } |
| }; |
| |
| // Attach iterators |
| if (typeof this.opts.store[Symbol.iterator] === 'function' && this.opts.store instanceof Map) { |
| this.iterator = generateIterator(this.opts.store); |
| } else if (typeof this.opts.store.iterator === 'function' && this.opts.store.opts |
| && this._checkIterableAdaptar()) { |
| this.iterator = generateIterator(this.opts.store.iterator.bind(this.opts.store)); |
| } |
| } |
| |
| _checkIterableAdaptar() { |
| return iterableAdapters.includes(this.opts.store.opts.dialect) |
| || iterableAdapters.findIndex(element => this.opts.store.opts.url.includes(element)) >= 0; |
| } |
| |
| _getKeyPrefix(key) { |
| return `${this.opts.namespace}:${key}`; |
| } |
| |
| _getKeyPrefixArray(keys) { |
| return keys.map(key => `${this.opts.namespace}:${key}`); |
| } |
| |
| _getKeyUnprefix(key) { |
| return key |
| .split(':') |
| .splice(1) |
| .join(':'); |
| } |
| |
| get(key, options) { |
| const {store} = this.opts; |
| const isArray = Array.isArray(key); |
| const keyPrefixed = isArray ? this._getKeyPrefixArray(key) : this._getKeyPrefix(key); |
| if (isArray && store.getMany === undefined) { |
| const promises = []; |
| for (const key of keyPrefixed) { |
| promises.push(Promise.resolve() |
| .then(() => store.get(key)) |
| .then(data => (typeof data === 'string') ? this.opts.deserialize(data) : (this.opts.compression ? this.opts.deserialize(data) : data)) |
| .then(data => { |
| if (data === undefined || data === null) { |
| return undefined; |
| } |
| |
| if (typeof data.expires === 'number' && Date.now() > data.expires) { |
| return this.delete(key).then(() => undefined); |
| } |
| |
| return (options && options.raw) ? data : data.value; |
| }), |
| ); |
| } |
| |
| return Promise.allSettled(promises) |
| .then(values => { |
| const data = []; |
| for (const value of values) { |
| data.push(value.value); |
| } |
| |
| return data; |
| }); |
| } |
| |
| return Promise.resolve() |
| .then(() => isArray ? store.getMany(keyPrefixed) : store.get(keyPrefixed)) |
| .then(data => (typeof data === 'string') ? this.opts.deserialize(data) : (this.opts.compression ? this.opts.deserialize(data) : data)) |
| .then(data => { |
| if (data === undefined || data === null) { |
| return undefined; |
| } |
| |
| if (isArray) { |
| return data.map((row, index) => { |
| if ((typeof row === 'string')) { |
| row = this.opts.deserialize(row); |
| } |
| |
| if (row === undefined || row === null) { |
| return undefined; |
| } |
| |
| if (typeof row.expires === 'number' && Date.now() > row.expires) { |
| this.delete(key[index]).then(() => undefined); |
| return undefined; |
| } |
| |
| return (options && options.raw) ? row : row.value; |
| }); |
| } |
| |
| if (typeof data.expires === 'number' && Date.now() > data.expires) { |
| return this.delete(key).then(() => undefined); |
| } |
| |
| return (options && options.raw) ? data : data.value; |
| }); |
| } |
| |
| set(key, value, ttl) { |
| const keyPrefixed = this._getKeyPrefix(key); |
| if (typeof ttl === 'undefined') { |
| ttl = this.opts.ttl; |
| } |
| |
| if (ttl === 0) { |
| ttl = undefined; |
| } |
| |
| const {store} = this.opts; |
| |
| return Promise.resolve() |
| .then(() => { |
| const expires = (typeof ttl === 'number') ? (Date.now() + ttl) : null; |
| if (typeof value === 'symbol') { |
| this.emit('error', 'symbol cannot be serialized'); |
| } |
| |
| value = {value, expires}; |
| return this.opts.serialize(value); |
| }) |
| .then(value => store.set(keyPrefixed, value, ttl)) |
| .then(() => true); |
| } |
| |
| delete(key) { |
| const {store} = this.opts; |
| if (Array.isArray(key)) { |
| const keyPrefixed = this._getKeyPrefixArray(key); |
| if (store.deleteMany === undefined) { |
| const promises = []; |
| for (const key of keyPrefixed) { |
| promises.push(store.delete(key)); |
| } |
| |
| return Promise.allSettled(promises) |
| .then(values => values.every(x => x.value === true)); |
| } |
| |
| return Promise.resolve() |
| .then(() => store.deleteMany(keyPrefixed)); |
| } |
| |
| const keyPrefixed = this._getKeyPrefix(key); |
| return Promise.resolve() |
| .then(() => store.delete(keyPrefixed)); |
| } |
| |
| clear() { |
| const {store} = this.opts; |
| return Promise.resolve() |
| .then(() => store.clear()); |
| } |
| |
| has(key) { |
| const keyPrefixed = this._getKeyPrefix(key); |
| const {store} = this.opts; |
| return Promise.resolve() |
| .then(async () => { |
| if (typeof store.has === 'function') { |
| return store.has(keyPrefixed); |
| } |
| |
| const value = await store.get(keyPrefixed); |
| return value !== undefined; |
| }); |
| } |
| |
| disconnect() { |
| const {store} = this.opts; |
| if (typeof store.disconnect === 'function') { |
| return store.disconnect(); |
| } |
| } |
| } |
| |
| module.exports = Keyv; |