seneca-promisified-core/src/index.js

/* jshint esversion: 6 */

import Promise from 'any-promise';

const completesPromise = (resolve, reject) => {
	return (err, res) => {
		if(err) return reject(err);
		resolve(res);
	};
};

/**
 * Automatically patched on the SenecaPromisified instance.
 * @memberof SenecaPromisified.prototype
 * @param {Object} args - Is the args object to pass to the next handler
 *
 * @example
 * seneca.add({ foo: 'bar' }, (args) => {
 *   return { response: 1 };
 * });
 * seneca.add({ foo: 'bar' }, (args, seneca) => {
 *   return seneca.prior(args).then(({ response }) => {
 *     return {
 *       response: response + 1
 *     };
 *   });
 * });
 *
 * seneca.act({ foo: 'bar' }).then(console.log); // => `{ response: 2 }`
 */
const prior = function(args) {
	const seneca = this._seneca;
	return new Promise((resolve, reject) => {
		seneca.prior(args, completesPromise(resolve, reject));
	});
};

/**
 * Meant to wrap the global seneca instance.
 *
 * @class SenecaPromisified
 */
class SenecaPromisified {
	constructor(seneca) {
		Object.defineProperties(this, {
			_seneca: {
				value: seneca
			},
			log: {
				value: seneca.log
			}
		});
	}

	/**
	 * Calls a seneca handler.
	 *
	 * @public
	 * @returns {Promise}
	 * @example
	 * // These all do the same thing...
	 * seneca.act('foo:true,bar:false').then(console.log);
	 * seneca.act({ foo: true, bar: false }).then(console.log);
	 * seneca.act('foo:true', { bar: false }).then(console.log);
	 */
	act(...args) {
		return new Promise((resolve, reject) => {
			this._seneca.act.apply(
				this._seneca,
				args.concat(completesPromise(resolve, reject))
			);
		});
	}

	/**
	 * This is only there to allow classes which inherit from this one to 
	 * override what the methods return. If you extend this class you should
	 * override this method to return a new instance of this one for the
	 * internals to work properly.
	 *
	 * @public
	 * @param {Object} seneca - Is the callback-base seneca instance.
	 * @returns {SenecaPromisified}
	 */
	create(seneca) {
		return new SenecaPromisified(seneca);
	}

	/**
	 * @private
	 */
	_handleResult(res, done) {
		if(typeof res.then === 'function') {
			res.then(done.bind(null, null), done);
		} else if(res instanceof Error) {
			done(err);
		} else {
			done(null, res);
		}
	}
	/**
	 * Adds a handler to the internal seneca instance.
	 *
	 * @public
	 * @example
	 * this.add({ cmd: 'foobar' }, function(args) {
	 *   return Promise.resolve('FOOBAR');
	 * });
	 */
	add(pat, ...rest) {
		const handler = rest.length > 1 ? rest[1] : rest[0];

		const handleResult = this._handleResult;
		const create = this.create;
		const wrappedHandler = function(args, done) {
			const seneca = this;
			const wrapped = create(seneca);
			wrapped.prior = prior;
			const res = handler.call(wrapped, args, wrapped);
			handleResult(res, done);
		};
		if(rest.length > 1) {
			this._seneca.add(pat, rest[0], wrappedHandler);
		} else {
			this._seneca.add(pat, wrappedHandler);
		}
	}

	/**
	 * @public
	 * @returns {Undefined}
	 * @example
	 * seneca.use((seneca) => {
	 * 	seneca.add({ cmd: 'ping' }, (args, seneca) => {
	 * 		return seneca.act({ cmd: '' });
	 * 	});
	 * });
	 *
	 * Or:
	 * seneca.use(function() {
	 * 	this.add({ cmd: 'pong' }, function(args) {
	 * 		return this.act({ cmd: 'ping' });
	 * 	});
	 * });
	 */
	use(...args) {

		// For now call the regular context...
		if(typeof args[0] === 'string') {
			return this._seneca.use.apply(this._seneca, args);
		}

		const plugin = typeof args[0] === 'string' ? args[1] : args[0];
		const create = this.create;
		const loader = function() {
			const seneca = this;
			const wrapped = create(seneca);
			plugin.call(wrapped, wrapped);
		};
		
		if(args.length > 1) {
			this._seneca.use(args[0], loader);
		} else {
			this._seneca.use(loader);
		}
	}

	/**
	 * Returns a new seneca object which will automatically add the given
	 * properties to the object you send.
	 *
	 * @public
	 * @param {Object} opts - The properties to automatically add.
	 * @returns {SenecaPromisified}
	 * @example
	 * const delegated = seneca.delegate({ safe: false });
	 * // The object submitted will also have the `safe` property.
	 * delegated.act({ cmd: 'ping' });
	 */
	delegate(opts) {
		const del = this._seneca.delegate(opts);
		return this.create(del);
	}

	/**
	 * Closes the connection established by `listen`.
	 *
	 * @public
	 * @returns {Promise}
	 */
	close() {
		return new Promise((resolve, reject) => {
			this._seneca.close((err) => err ? reject(err) : resolve());
		});
	}

	/**
	 * Similar to delegate.
	 *
	 * @public
	 * @returns {Object}
	 *
	 * @example
	 * seneca.add({ cmd: 'save', entity: 'person' }, (args) => {
	 *   return { saved: true };
	 * });
	 * seneca.add({ cmd: 'load', entity: 'person' }, (args) => {
	 *   return { loaded: true };
	 * });
	 *
	 * const pin = seneca.pin({ cmd: '*', entity: 'person' });
	 *
	 * pin.load({}).then(console.log); // => `{ loaded: true }`
	 * pin.save({}).then(console.log); // => `{ saved: true }`
	 */
	pin(pat) {
		const pinned = this._seneca.pin(pat);
		return Object.keys(pinned).reduce((accu, key) => {
			accu[key] = (args) => {
				return new Promise((resolve, reject) => {
					pinned[key](args, completesPromise(resolve, reject));
				});
			};
			return accu;
		}, {});
	}

	/**
	 * Opens a connection.
	 * @public
	 * @returns {Undefined}
	 */
	listen(opts) {
		this._seneca.listen(opts);
	}

	/**
	 * Returns a promise which will be resolved when seneca is loaded.
	 * @public
	 * @returns {Promise}
	 */
	ready() {
		return new Promise((resolve, reject) => {
			this._seneca.ready((err) => err ? reject(err) : resolve());
		});
	}

	/**
	 * Modifies the prototype to add new methods.
	 *
	 * @public
	 * @static
	 * @param {Function} fn - Side effecting function which changes the
	 * prototype.
	 */
	static use(fn) {
		return fn(SenecaPromisified.prototype);
	}

	/**
	 * Just an alternate way to instantiate the class...
	 * @static
	 */
	static create(seneca) {
		return new SenecaPromisified(seneca);
	}
}

module.exports = SenecaPromisified;