prototypes/server/extend/request-session.js

const _cluster = require('cluster');
/**
 * @class
 * @name SGAppsServerRequestSession
 * @param {SGAppsServerRequest} request
 * @param {SGAppsSessionManagerOptions} options
 */
function SGAppsServerRequestSessionBuilder(request, options) {
	/**
	 * @typedef {object} SGAppsSessionManagerOptions
	 * @property {number} [SESSION_LIFE=600] 
	 * @property {string} [cookie='ssiddyn'] 
	 */

	/**
	 * @typedef {object} SGAppsServerRequestSessionCache
	 * @property {number} expire
	 * @property {object} data
	 */

	/**
	 * @memberof SGAppsServerRequestSession#
	 * @name _created
	 * @type {number}
	 */
	this._created = new Date().valueOf();
	var _this = this;

	/**
	 * @memberof SGAppsServerRequestSession#
	 * @name _ip
	 * @type {string}
	 */
	this._ip = (
		(
			request.request ? (
				Array.isArray(request.request.headers['x-forwarded-for']) ? (
					request.request.headers['x-forwarded-for'][0]
				) : request.request.headers['x-forwarded-for']
			) : null
		) || (
			( request.request && request.request.connection )
			? request.request.connection.remoteAddress
			: null
		) || (
			( request.request && request.request.socket )
			? request.request.socket.remoteAddress
			: null
		) || ''
	);

	/**
	 * Session was received from previously saved cookie
	 * @memberof SGAppsServerRequestSession#
	 * @name _confirmed
	 * @type {boolean}
	 */
	this._confirmed = false;

	/**
	 * @memberof SGAppsServerRequestSession#
	 * @name _id
	 * @type {string}
	 */
	this._id = request.cookies ? (
		request.cookies.get(
			options.cookie,
			{ secure: true }
		) || request.cookies.get(
			options.cookie,
			{ secure: false }
		) || null
	) : null;

	if (this._id === null) {
		this._id = `sess-${
			Math.floor(1E6 + 1E6 * Math.random()).toString(36)
			}-${
				new Date().valueOf().toString(36)
			}`;

		request.cookies.set(
			options.cookie,
			this._id,
			{
				secure: true
			},
			true
		);
		request.cookies.set(
			options.cookie,
			this._id,
			{
				secure: false
			},
			true
		);
	} else {
		this._confirmed = true;
	}
	
	/**
	 * @memberof SGAppsServerRequestSession#
	 * @name _options
	 * @type {SGAppsSessionManagerOptions}
	 */
	this._options = Object.assign(
		{ SESSION_LIFE: 600, cookie: 'ssiddyn' },
		options || {}
	);

	/**
	 * @memberof SGAppsServerRequestSession#
	 * @name data
	 * @type {object}
	 */
	this.data = null;

	/**
	 * @memberof SGAppsServerRequestSession#
	 * @method destroy
	 */
	this.destroy = function () {
		_this.data = null;
		_this._id = null;
		_this._ip = null;
		_this._options = null;
		delete _this.destroy;
	};

	return this;
}

/**
 * @class
 * @name SGAppsSessionManager
 * @param {SGAppsServer} server
 * @param {SGAppsSessionManagerOptions} [options]
 */
function SGAppsSessionManager(server, options) {
	/**
	 * @memberof SGAppsSessionManager#
	 * @name _options
	 * @type {SGAppsSessionManagerOptions}
	 */
	this._options = Object.assign(
		{ SESSION_LIFE: 600, cookie: 'ssiddyn', workersSyncMaxDelay: 200 },
		options || {}
	);


	/**
	 * @memberof SGAppsSessionManager#
	 * @name _enabled
	 * @type {boolean}
	 */
	this._enabled = true;

	/**
	 * @memberof SGAppsSessionManager#
	 * @name _sessions
	 * @type {Object<string,SGAppsServerRequestSessionCache>}
	 */
	this._sessions = {};

	return this;
};

/**
 * @memberof SGAppsSessionManager#
 * @method removeExpiredSessions
 */
SGAppsSessionManager.prototype.removeExpiredSessions = function () {
	const time = new Date().valueOf();
	let sessionId;
	for (sessionId in this._sessions) {
		if (this._sessions[sessionId].expire <= time) {
			this._sessions[sessionId].data = null;
			delete this._sessions[sessionId];
		}
	}
};

/**
 * @memberof SGAppsSessionManager#
 * @method handleRequest
 * @param {SGAppsServerRequest} request
 */
SGAppsSessionManager.prototype.handleRequest = function (request) {
	request.session = new SGAppsServerRequestSessionBuilder(
		request,
		this._options
	);

	const SessionManager = this;

	if (request.session._id in SessionManager._sessions) {
		request.session.data = SessionManager._sessions[request.session._id].data;
		SessionManager._sessions[request.session._id].expire = request.session._created + SessionManager._options.SESSION_LIFE * 1000;
	} else {
		SessionManager._sessions[request.session._id] = {
			expire: request.session._created + SessionManager._options.SESSION_LIFE * 1000,
			data: {}
		};
		request.session.data = SessionManager._sessions[request.session._id].data;
	}
};



/**
 * @param {SGAppsServerRequest} request 
 * @param {SGAppsServerResponse} response 
 * @param {SGAppsServer} server 
 * @param {function} callback 
 */
function RequestSessionDecorator(request, response, server, callback) {
	if (
		request === null
		&&
		response === null
	) {
		/**
		 * @memberof SGAppsServer#
		 * @name SessionManager
		 * @type {SGAppsSessionManager}
		 */
		server.SessionManager = new SGAppsSessionManager(server);

		if (_cluster.isPrimary || _cluster.isMaster) {
			let workerId;
			const decorateWorker = (worker) => {
				worker.on('message', function (message) {
					if (
						message
						&& typeof(message) === "object"
						&& ( 'type' in message )
						&& typeof(message.type) === "string"
						&& typeof(message.pid) === "number" && message.pid
						&& typeof(message.sessionId) === "string" && message.sessionId
					) {
						if (
							message.type === "sgapps-server:session-manager:worker-data-request"
						) {
							if (
								(message.sessionId in server.SessionManager._sessions)
								&& typeof(server.SessionManager._sessions[message.sessionId].data) === "object"
								&& server.SessionManager._sessions[message.sessionId].data
							) {
								server.SessionManager._sessions[message.sessionId].expire = new Date().valueOf() + server.SessionManager._options.SESSION_LIFE * 1000;
								worker.send({
									type: "sgapps-server:session-manager:worker-data-response",
									sessionId: message.sessionId,
									data: server.SessionManager._sessions[message.sessionId].data,
									pid: process.pid
								});
							} else {
								worker.send({
									type: "sgapps-server:session-manager:worker-data-response",
									sessionId: message.sessionId,
									data: {},
									pid: process.pid
								});
							}
						} else if (
							message.type === "sgapps-server:session-manager:worker-data-store"
							&& typeof(message.data) === "object" && message.data
						) {
							if (
								(message.sessionId in server.SessionManager._sessions)
								&& typeof(server.SessionManager._sessions[message.sessionId].data) === "object"
								&& server.SessionManager._sessions[message.sessionId].data
							) {
								Object.assign(
									server.SessionManager._sessions[message.sessionId].data,
									message.data
								);
								server.SessionManager._sessions[message.sessionId].expire = new Date().valueOf() + server.SessionManager._options.SESSION_LIFE * 1000;
							} else {
								server.SessionManager._sessions[message.sessionId] = {
									expire: new Date().valueOf() + server.SessionManager._options.SESSION_LIFE * 1000,
									data: Object.assign({}, message.data)
								};
							}
						}
					}
				});
			};
			_cluster.on('fork', (worker) => {
				decorateWorker(worker);
			});
			for (workerId in _cluster.workers) {
				let worker = _cluster.workers[workerId];
				decorateWorker(worker);
			}
		}

		setInterval(() => {
			//@ts-ignore
			server.SessionManager.removeExpiredSessions();
		}, 5000);
		callback();
		return;
	}

	if (!server.SessionManager._enabled) {
		callback();
		return;
	}

	/**
	 * @memberof SGAppsServerRequest#
	 * @name session
	 * @type {SGAppsServerRequestSession}
	 */
	server.SessionManager.handleRequest(request);
	
	if (_cluster.isPrimary || _cluster.isMaster) {
		response._destroy.push(function () {
			request.session.destroy();
			delete request.session;
		});

		callback();
	} else {
		response._destroy.unshift(function () {
			process.send({
				type: 'sgapps-server:session-manager:worker-data-store',
				sessionId: request.session._id,
				data: request.session.data,
				pid: process.pid
			});
		});
		response._destroy.push(function () {
			request.session.destroy();
			delete request.session;
		});

		process.send({
			type: 'sgapps-server:session-manager:worker-data-request',
			sessionId: request.session._id,
			pid: process.pid
		});

		let callbackSent   = false;
		const callbackOnce = () => {
			if (callbackSent) return false;
			callbackSent = true;
			callback();
			return true;
		}

		process.on('message', function (message) {
			if (
				message
				&& typeof(message) === "object"
				&& request.session
				&& ( 'type' in message )
				&& typeof(message.type) === "string"
				&& ( 'pid' in message )
				&& typeof(message.pid) === "number"
				&& message.type === "sgapps-server:session-manager:worker-data-response"
				&& typeof(message.sessionId) === "string" && message.sessionId === request.session._id
				&& typeof(message.data) === "object" && message.data
			) {
				request.session.data = Object.assign(request.session.data, message.data);
				callbackOnce();
			}
		});

		setTimeout(function () {
			if (callbackOnce()) server.logger.warn(
				process.pid,
				' SessionSync between worker and master skipped after ' + server.SessionManager._options.workersSyncMaxDelay + 'ms'
			);
		}, server.SessionManager._options.workersSyncMaxDelay);
	}
};


module.exports = RequestSessionDecorator;