'use strict';
// Dependencies
const level = require('level');
const merge = require('lodash.merge');
const monitor = require('fast-json-patch');
const pointer = require('json-pointer');
// Fabric Types
const Actor = require('./actor');
const Codec = require('./codec');
const Message = require('./message');
const Tree = require('./tree');
const Key = require('./key');
/**
* Provides an encrypted datastore for generic object storage.
*/
class Keystore extends Actor {
/**
* Create an instance of the Store.
* @param {FabricStoreConfiguration} [configuration] Settings to use.
* @param {String} [configuration.name="DefaultStore"] Name of the Store.
* @returns {Keystore} Instance of the store.
*/
constructor (settings = {}) {
super(settings);
if (!settings.seed) settings.seed = (process) ? process.env.FABRIC_SEED || null : null;
this.settings = merge({
name: 'DefaultStore',
type: 'EncryptedFabricStore',
path: './stores/keystore',
mode: 'aes-256-cbc',
key: null,
version: 0
}, this.settings, settings);
this.tree = new Tree();
this.level = null;
this.db = null;
this.codec = new Codec({
key: this.settings.key,
mode: this.settings.mode,
version: this.settings.version
});
this._state = {
status: 'initialized',
version: this.settings.version,
keys: [],
content: {}
};
this.observer = monitor.observe(this._state.content, this._handleStateChange.bind(this));
return this;
}
get states () {
return [
'initialized',
'starting',
'opening',
'open',
'started',
'writing',
'closing',
'closed',
'deleting',
'deleted',
'stopping',
'stopped'
];
}
get status () {
return this._state.status;
}
set status (value) {
if (!value) throw new Error('Cannot set status to empty value.');
if (!this.states.includes(value)) throw new Error(`Status value "${value}" is not one of ${this.states.length} valid states: ${JSON.stringify(this.states)}`);
this._state.status = value;
return this.status;
}
get state () {
return Object.assign({}, this._state.content);
}
async commit () {
const changes = monitor.generate(this.observer, true);
if (changes) {
const actor = new Actor(changes);
this.emit('changes', changes);
this.emit('message', Message.fromVector(['StateChange', {
changes: changes,
signature: actor.sign().signature
}]));
}
return this;
}
async open () {
const keystore = this;
const promise = new Promise((resolve, reject) => {
if (['open', 'writing'].includes(keystore.status)) return resolve(keystore);
keystore.status = 'opening';
async function _handleDiskOpen (err, db) {
if (err) return this.emit('error', `Could not open: ${err}`);
this.status = 'open';
let state = null;
try {
state = await this._getState();
} catch (exception) {
this.emit('warning', `Could not retrieve state`);
}
if (state) {
// TODO: recursively cast { type, data } tuples as Buffer
// where type === 'Buffer' && data
await this._setState(state);
await this.commit();
}
return this;
}
try {
keystore.db = level(keystore.settings.path, {
keyEncoding: keystore.codec,
valueEncoding: keystore.codec
}, _handleDiskOpen.bind(keystore));
keystore.status = 'open';
resolve(keystore);
} catch (exception) {
keystore.status = 'closed';
reject(new Error(`Could not open store: ${exception}`));
}
});
return promise;
}
async close () {
this.status = 'closing';
if (this.db) await this.db.close();
this.status = 'closed';
return this;
}
async batch (ops) {
await this._batch(ops);
return this;
}
async wipe () {
if (this.status !== 'open') return this.emit('error', `Status not open: ${this.status}`);
this.status = 'deleting';
this._state.content = null;
await this.db.clear();
this.status = 'deleted';
return this;
}
async get (path = '*') {
return this._get(path);
}
async _handleStateChange (changes) {
// console.log('changes:', changes);
}
async _applyChanges (changes) {
monitor.applyPatch(this._state.content, changes);
await this.commit();
return this._get();
}
async _get (key = '*') {
if (key === '*') return Object.assign({}, this._state.value);
try {
const result = pointer.get(this._state.value, `/${key}`);
return result;
} catch (exception) {
return null;
}
}
async _set (key, value) {
if (!['open', 'deleting'].includes(this.status)) throw new Error(`Cannot write while status === ${this.status}`);
this.status = 'writing';
this._state.content[key] = value;
await this.db.put(key, value);
this.status = 'open';
return this._get(key);
}
async _batch (ops) {
await this.db.batch(ops);
return this;
}
async _getState () {
const keystore = this;
const promise = new Promise((resolve, reject) => {
async function loadStateFromDisk () {
try {
const result = await keystore.db.get('/');
// TODO: actor.deserialize();
return JSON.parse(result);
} catch (exception) {
return null;
}
}
loadStateFromDisk().then(resolve).catch(reject);
});
return promise;
}
/**
* Saves an Object to the store.
* @param {Object} state State to store.
* @returns {Actor} The local instance of the provided State's {@link Actor}.
*/
async _setState (state) {
if (!state) throw new Error('State must be provided.');
if (!['open', 'deleting'].includes(this.status)) throw new Error(`Store is not writable. Currently: ${this.status}`);
const keystore = this;
const promise = new Promise((resolve, reject) => {
for (const key in state) {
if (Object.prototype.hasOwnProperty.call(state, key)) {
keystore._state.keys.push(key);
keystore._state.content[key] = state[key];
}
}
this._syncStateToDisk().then(resolve).catch(reject);
});
return promise;
}
async _syncStateToDisk () {
if (!['open', 'deleting'].includes(this.status)) throw new Error(`Store is not writable. Currently: ${this.status}`);
const keystore = this;
const promise = new Promise((resolve, reject) => {
const actor = new Actor(this.state);
const serialized = actor.serialize();
if (!serialized) throw new Error(`Could not serialize: ${JSON.stringify(this.state, null, ' ')}`)
if (keystore.db) {
keystore.db.put('/', serialized).then(resolve).catch(reject);
}
});
return promise;
}
async start () {
this.status = 'starting';
await this.open();
this.status = 'started';
return this;
}
async stop () {
this.status = 'stopping';
await this.close();
this.status = 'stopped';
return this;
}
}
module.exports = Keystore;