3box
Version:
Interact with user data
219 lines (195 loc) • 6.37 kB
JavaScript
const { throwIfUndefined, throwIfNotEqualLenArrays } = require('./utils/index')
class KeyValueStore {
/**
* Please use **box.public** or **box.private** to get the instance of this class
*/
constructor (name, replicator, threeId) {
this._name = name
this._replicator = replicator
if (this._name.startsWith('3box.space.')) {
this._space = this._name.split('.')[2]
}
this._3id = threeId
}
/**
* Get the value and optionally metadata of the given key
*
* @param {String} key the key
* @param {Object} opts optional parameters
* @param {Boolean} opts.metadata return both value and metadata
* @return {String|{value: String, timestamp: Number}} the value associated with the key, undefined if there's no such key
*/
async get (key, opts = {}) {
const x = await this._get(key)
if (!x) {
return x
}
if (opts.metadata) {
const metadata = this._extractMetadata(x)
return {
...metadata,
value: x.value
}
}
return x.value
}
/**
* Get metadata for for a given key
*
* @param {String} key the key
* @return {Metadata} Metadata for the key, undefined if there's no such key
*/
async getMetadata (key) {
const x = await this._get(key)
if (!x) {
return x
}
return this._extractMetadata(x)
}
/**
* Set a value for the given key
*
* @param {String} key the key
* @param {String} value the value
* @return {Boolean} true if successful
*/
async set (key, value) {
throwIfUndefined(key, 'key')
this._requireLoad()
this._replicator.ensureConnected(this._db.address.toString())
const timeStamp = new Date().getTime()
await this._db.put(key, { value, timeStamp })
return true
}
/**
* Set multiple values for multiple keys
*
* @param {Array<String>} keys the keys
* @param {Array<String>} values the values
* @return {Boolean} true if successful, throw error if not
*/
async setMultiple (keys, values) {
throwIfNotEqualLenArrays(keys, values)
this._requireLoad()
this._replicator.ensureConnected(this._db.address.toString())
try {
await keys.reduce(async (previousPromise, nextKey, index) => {
await previousPromise
throwIfUndefined(nextKey, 'key')
const timeStamp = new Date().getTime()
return this._db.put(nextKey, { value: values[index], timeStamp })
}, Promise.resolve())
return true
} catch (error) {
throw new Error(error)
}
}
/**
* Remove the value for the given key
*
* @param {String} key the key
* @return {Boolean} true if successful
*/
async remove (key) {
throwIfUndefined(key, 'key')
this._requireLoad()
this._replicator.ensureConnected(this._db.address.toString())
await this._db.del(key)
return true
}
/**
* Extract metadata from store object
* @private
* @param x {Object} data from store
* @return {Metadata} store metadata
*/
_extractMetadata (x) {
// ms -> seconds, see issue #396 for details
const timestamp = Math.floor(x.timeStamp / 1000)
return { timestamp }
}
/**
* Get the raw value of the given key
* @private
*
* @param {String} key the key
* @return {String} the value associated with the key
*/
async _get (key) {
this._requireLoad()
return this._db.get(key)
}
async _sync () {
this._requireLoad()
await this._replicator.syncDB(this._db)
return this._db.address.toString()
}
async _load (threeId) {
this._3id = threeId || this._3id
const odbAddress = this._replicator.listStoreAddresses().find(odbAddress => odbAddress.includes(this._name))
if (odbAddress) {
this._db = await this._replicator.getStore(odbAddress)
} else {
const key = (await this._3id.getPublicKeys(this._space, true)).signingKey
this._db = await this._replicator.addKVStore(this._name, key, Boolean(this._space), this._3id.getSubDID(this._space))
}
// when this._space is undefined it will use the root identity
const odbIdentity = await this._3id.getOdbId(this._space)
this._db.setIdentity(odbIdentity)
return this._db.address.toString()
}
_requireLoad () {
if (!this._db) throw new Error('_load must be called before interacting with the store')
}
async close () {
this._requireLoad()
await this._db.close()
}
/**
* Get all values and optionally metadata
*
* @param {Object} opts optional parameters
* @param {Boolean} opts.metadata return both values and metadata
* @return {Array<String|{value: String, timestamp: Number}>} the values
*/
async all (opts = {}) {
this._requireLoad()
const entries = this._db.all
const allSimple = {}
Object.keys(entries).map(key => {
const entry = entries[key]
if (opts.metadata) {
allSimple[key] = {
...this._extractMetadata(entry),
value: entry.value
}
} else {
allSimple[key] = entry.value
}
})
return allSimple
}
/**
* Returns array of underlying log entries. In linearized order according to their Lamport clocks.
* Useful for generating a complete history of all operations on store.
*
* @example
* const log = store.log
* const entry = log[0]
* console.log(entry)
* // { op: 'PUT', key: 'Name', value: 'Botbot', timeStamp: '1538575416068' }
*
* @return {Array<Object>} Array of ordered log entry objects
*/
async log () {
return this._db._oplog.values.map(obj => {
return {
op: obj.payload.op,
key: obj.payload.key,
value: obj.payload.value ? obj.payload.value.value : null,
timeStamp: obj.payload.value ? obj.payload.value.timeStamp : null
}
})
}
}
module.exports = KeyValueStore