UNPKG

landlord

Version:

Multi-entry transaction control for distributed KV stores.

335 lines (262 loc) 7.71 kB
'use strict'; const elv = require('elv'); const Promise = require('bluebird'); const errors = require('./errors'); const Lease = require('./lease'); const msg = { optionsPojo: 'Arg "options" must be a non-Array, non-Date object', noStore: 'No store defined', storeAsyncFunc: 'Store method must be a function: ', storeInsertFunc: 'Stores must have a method called "insert()"', storeRemoveFunc: 'Stores must have a method called "remove()"', storeTouchFunc: 'Stores must have a method called "touch()"', docsNotPojoMap: 'Arg "docs" must be a non-Array, non-Date object or a Map', ttlNum: 'Arg "options.ttl" must be a number', ttlNegative: 'Arg "optoins.ttl" cannot be negative', callbackFunc: 'Arg "callback" must be undefined or a function', keysArraySet: 'Arg "keys" must be an array or Set', }; class Underwriter { constructor(options) { options = this._assertOptions(options); this.store = options.store; this.ttl = options.ttl; } _isPojo(val) { return (typeof val === 'object' && !(val instanceof Map) && !(val instanceof Set) && !Array.isArray(val) && !(val instanceof Date)); } _toMap(obj) { const m = new Map(); const keys = Object.keys(obj); for (let i = 0; i < keys.length; i++) { const key = keys[i]; m.set(key, obj[key]); } return m; } _promisify(store, func) { const afunc = `${func}Async`; if (!elv(store[afunc])) { store[afunc] = Promise.promisify(store[func]); return; } if (typeof store[afunc] !== 'function') throw new TypeError(msg.storeAsyncFunc + afunc); }; _assertOptions(options) { if (!this._isPojo(options)) throw new TypeError(msg.optionsPojo); const store = options.store; if (!elv(store)) throw new TypeError(msg.noStore); if (typeof store.insert !== 'function') throw new TypeError(msg.storeInsertFunc); if (typeof store.remove !== 'function') throw new TypeError(msg.storeRemoveFunc); if (typeof store.touch !== 'function') throw new TypeError(msg.storeTouchFunc); if (elv(options.ttl)) { if (typeof options.ttl !== 'number') throw new TypeError(msg.ttlNum); if (options.ttl < 0) throw new TypeError(msg.ttlNegative); } this._promisify(store, 'insert'); this._promisify(store, 'remove'); this._promisify(store, 'touch'); return { store: store, ttl: elv.coalesce(options.ttl, 5000) }; } _assertDocuments(docs) { if (this._isPojo(docs)) return this._toMap(docs); if (!(docs instanceof Map)) throw new TypeError(msg.docsNotPojoMap); return docs; } _assertCallback(callback) { if (typeof callback !== 'function') throw new TypeError(msg.callbackFunc); } _assertKeys(keys) { if (!Array.isArray(keys) && !(keys instanceof Set)) throw new TypeError(msg.keysArraySet); } _noResultError() { return new errors.StoreError({ message: 'The store did not respond with a result' }); } _createLease(res, docs) { const leaseDocs = new Map(); const successful = []; const collided = []; let success = true; let internal = undefined; for (let key of res.keys()) { const value = res.get(key); if (!value.success) { success = false; if (value.isCollision) collided.push(key); else internal = value.err; continue; } successful.push(key); leaseDocs.set(key, { etag: value.etag, value: docs.get(key) }); } let lease = undefined; let error = undefined; if (success) { lease = new Lease(this, leaseDocs); } else { error = (elv(internal)) ? new errors.StoreError(internal) : new errors.CollisionError(collided); } return { value: lease, success: success, collided: collided, successful: successful, err: error }; } _createSummary(res) { const successful = []; let success = true; let internal = false; for (let key of res.keys()) { const value = res.get(key); if (!value.success) { success = false; if (!value.isMissing) internal = value.err; continue; } successful.push(key); } let err; if (!success) { err = (internal) ? new errors.StoreError(internal) : new errors.ExpiredError(); } return { success: success, err: err, successful: successful }; } _cleanup(keys, error, callback) { const e = error; const cb = callback; this.store.remove(Array.from(keys), (err, res) => { cb(e); }); } _cleanupAsync(keys, error) { const e = error; return this.store.removeAsync(Array.from(keys)) .then(() => { throw e; }); } insert(docs, callback) { const docsm = this._assertDocuments(docs); this._assertCallback(callback); const self = this; const cb = callback; this.store.insert(docsm, { ttl: this.ttl }, (err, res) => { if (elv(err) || !elv(res)) { const e = (elv(err)) ? new errors.StoreError(err) : self._noResultError(); return self._cleanup(docsm.keys(), e, cb); } const lease = self._createLease(res, docsm); if (!lease.success) return self._cleanup(lease.successful, lease.err, cb); cb(undefined, lease.value); }); } insertAsync(docs) { const docsm = this._assertDocuments(docs); const self = this; return this.store.insertAsync(docsm, { ttl: this.ttl }) .then((res) => { if (!elv(res)) throw self._noResultError(); const lease = self._createLease(res, docsm); if (!lease.success) return self._cleanupAsync(lease.successful, lease.err); return lease.value; }) .catch((err) => { if (errors.isKnown(err)) throw err; throw new errors.StoreError(err); }); } remove(keys, callback) { this._assertKeys(keys); this._assertCallback(callback); const cb = callback; this.store.remove(keys, (err, res) => { if (elv(err)) return cb(new errors.StoreError(err)); cb(undefined, true); }) } removeAsync(keys) { this._assertKeys(keys); return this.store.removeAsync(keys) .catch((err) => { if (errors.isKnown(err)) throw err; throw new errors.StoreError(err); }); } touch(keys, callback) { this._assertKeys(keys); this._assertCallback(callback); const self = this; const cb = callback; const k = keys; this.store.touch(keys, { ttl: 0 }, (err, res) => { if (elv(err) || !elv(res)) { const e = (elv(err)) ? new errors.StoreError(err) : self._noResultError(); return self._cleanup(k, e, cb); } const summary = self._createSummary(res); if (!summary.success) return self._cleanup(summary.successful, summary.err, cb); cb(undefined, res); }); } touchAsync(keys) { this._assertKeys(keys); const self = this; const k = keys; return this.store.touchAsync(keys, { ttl: 0 }) .then((res) => { if (!elv(res)) throw self._noResultError(); const summary = self._createSummary(res); if (!summary.success) return self._cleanupAsync(summary.successful, summary.err); return res; }) .catch((err) => { if (errors.isKnown(err)) throw err; throw new errors.StoreError(err); }); } } module.exports = Underwriter;