const BaseService = require('lib/services/BaseService.js'); const Mutex = require('async-mutex').Mutex; class KvStore extends BaseService { static instance() { if (this.instance_) return this.instance_; this.instance_ = new KvStore(); return this.instance_; } constructor() { super(); this.incMutex_ = new Mutex(); } setDb(v) { this.db_ = v; } db() { if (!this.db_) throw new Error('Accessing DB before it has been set!'); return this.db_; } typeFromValue_(value) { if (typeof value === 'string') return KvStore.TYPE_TEXT; if (typeof value === 'number') return KvStore.TYPE_INT; throw new Error(`Unsupported value type: ${typeof value}`); } formatValues_(kvs) { const output = []; for (const kv of kvs) { kv.value = this.formatValue_(kv.value, kv.type); output.push(kv); } return output; } formatValue_(value, type) { if (type === KvStore.TYPE_INT) return Number(value); if (type === KvStore.TYPE_TEXT) return `${value}`; throw new Error(`Unknown type: ${type}`); } async value(key) { const r = await this.db().selectOne('SELECT `value`, `type` FROM key_values WHERE `key` = ?', [key]); if (!r) return null; return this.formatValue_(r.value, r.type); } async setValue(key, value) { const t = Date.now(); await this.db().exec('INSERT OR REPLACE INTO key_values (`key`, `value`, `type`, `updated_time`) VALUES (?, ?, ?, ?)', [key, value, this.typeFromValue_(value), t]); } async deleteValue(key) { await this.db().exec('DELETE FROM key_values WHERE `key` = ?', [key]); } async clear() { await this.db().exec('DELETE FROM key_values'); } async all() { return this.formatValues_(await this.db().selectAll('SELECT * FROM key_values')); } // Note: atomicity is done at application level so two difference instances // accessing the db at the same time could mess up the increment. async incValue(key, inc = 1) { const release = await this.incMutex_.acquire(); try { const result = await this.db().selectOne('SELECT `value`, `type` FROM key_values WHERE `key` = ?', [key]); const newValue = result ? this.formatValue_(result.value, result.type) + inc : inc; await this.setValue(key, newValue); release(); return newValue; } catch (error) { release(); throw error; } } async searchByPrefix(prefix) { let results = await this.db().selectAll('SELECT `key`, `value`, `type` FROM key_values WHERE `key` LIKE ?', [`${prefix}%`]); return this.formatValues_(results); } async countKeys() { const r = await this.db().selectOne('SELECT count(*) as total FROM key_values'); return r.total ? r.total : 0; } } KvStore.TYPE_INT = 1; KvStore.TYPE_TEXT = 2; module.exports = KvStore;