Spaces:
Running
Running
; | |
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; | |