Source: index.js

/**
 * Module defining a simple interface for setting and getting context objects on a domain.
 * A connect middleware allows wrapping requests in a domain and setting/getting values
 * on the active domain object.
 * @module {Object} request-context
 * @requires domain
 */
'use strict';

const domain = require('domain');

/**
 * The ContextService provides the middleware and context accessor methods
 * @type {Object}
 */
exports = module.exports = {

	/**
	 * Wrap the request/response loop in a namespace wrapper (by using node's domain system).
	 * All following functions will be run in the created namespace. Returns a function
	 * function that can be used as connect middleware.
	 * @type {Function}
 	 * @params {String} name - The name of the namespace to create
	 *
	 */
	middleware: contextMiddleware,

	/**
	 * Set the context for a given name or path.
	 * @param {String} name - The name of the context
	 * @param {*} value - The value to set
	 */
	setContext: setContext,

	/**
	 * Alias for the setContext method.
	 * @param {String} name - The name of the context
	 * @param {*} value - The value to set
	 */
	set: setContext,

	/**
	 * Return the context or a context variable for a name or path.
	 * @param {String} name - The name or path of the context or context property to retrieve
	 * @returns {undefined|*} The context for the given specifier or null if no such name could be found
	 */
	getContext: getContext,

	/**
	 * Alias for the getContext method.
	 * @param {String} name - The name or path of the context or context property to retrieve
	 * @returns {undefined|*} The context for the given specifier or null if no such name could be found
	 */
	get: getContext
};

/**
 * Return the context for a name.
 * @api private
 * @param {String} name - The name of the context to retrieve
 * @param {domain} [current] - A domain object to retrieve the context object from
 * @returns {*} The context for the given name
 */
function getContext(name, current) {
	const context = getCurrent(current);
	if (!context) {
		return undefined;
	}

	return getPropertyForPath(context, name);
}

/**
 * Initiates the context object on the provided domain
 * @param domain - A domain object to initialize the context object on
 * @returns {Object|*}
 */
function initContext(domain) {
	if (!domain) {
		throw new Error('No domain found when initializing context');
	}

	domain.__$cntxt__ = Object.create(null);
	return domain.__$cntxt__;
}

/**
 * Set the context for a given name
 * @api private
 * @param {String} name - The name of the context
 * @param {*} value - The value to set
 * @param {domain} [current] - A domain object to retrieve the context object from
 * @throws Error
 */
function setContext(name, value, current) {
	const context = getCurrent(current);
	if (!context) {
		throw new Error('No active context found to set property ' + name);
	}

	setPropertyForPath(context, name, value);
}

/**
 * Wrap the request/response loop in a namespace wrapper (by using node's domain system).
 * @api private
 * @param {String} namespace - The name of the namespace to create
 * @returns {Function} A function that can be used as request middleware. Following functions
 * will be run in the created namespace.
 */
function contextMiddleware(namespace) {
	if (!namespace) {
		throw new Error('No namespace specified!');
	}

	return function runInContextMiddleware(req, res, next) {
		// We want multiple request-context consumers to use the same domain
		// context object rather than creating a bunch of nested domains.
		// Their namespaces should be sufficient to keep each consumer's
		// data separate from the others.
		if (domain.active && domain.active.__$cntxt__) {
			setContext(namespace, Object.create(null), domain.active);
			next();
			return;
		}

		const d = domain.create();
		d.add(req);
		d.add(res);
		d.on('error', handleError);

		initContext(d);
		setContext(namespace, Object.create(null), d);

		d.run(next);

		function handleError(err) {
			if (!res._header) {
				res.setHeader('Connection', 'close');
			}
			res.end();
			next(err);
		}
	};
}

/**
 * Get the current active domain context object
 * @api private
 * @param {domain} [current] - A domain object the context container should be looked upon
 * @returns {null|Object}
 */
function getCurrent(current) {
	if (!current) {
		current = domain.active;
	}

	// no active domain found
	if (!current || !current.__$cntxt__) {
		return null;
	}

	return current.__$cntxt__;
}

/**
 * Get the object property for a given path divided by dots
 * @api private
 * @param {Object} obj - The object to query
 * @param {String} path - The objects property path divided by dots
 * @returns {*}
 */
function getPropertyForPath(obj, path) {
	if (obj && path) {
		const arr = normalizePathArray(path);

		while (arr.length) {
			if (!(obj = obj[arr.shift()])) {
				break;
			}
		}
	}
	return obj;
}

/**
 * Set the object property for a given path divided by dots
 * @api private
 * @param {Object} obj - The object to modify
 * @param {String} path - The objects property path divided by dots
 * @param {*} value - The value to set on the objects path
 * @returns {*}
 */
function setPropertyForPath(obj, path, value) {
	const arr = normalizePathArray(path);
	const len = arr.length - 1;

	for (let i = 0; i < len; i += 1) {
		if (typeof obj[arr[i]] === 'undefined') {
			// create a new object container for undefined paths
			obj[arr[i]] = {};
		}
		obj = obj[arr[i]];
	}

	obj[arr[len]] = value;
}

/**
 * Normalize the namespace of a path by replacing all ':' to '.'.
 * @api private
 * @param {String} path - The context object property path divided by dots
 * @returns {*}
 */
function normalizePathArray(path) {
	return path.replace(':', '.').split('.');
}