|
'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]; |
|
} |
|
}; |
|
|
|
|
|
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; |
|
|