index.js

/**
 * @class
 * @name WindowSocket
 * @param {Window} [_window] specify other window to listen
 */
var WindowSocket = function (_window) {
	if (!_window) _window = window;

	var app = new ApplicationPrototype();
	var _uniqIndex = 0;
	var _uniqPrefix = new Date().valueOf().toString(36);
	var _uniq = function () {
		_uniqIndex++;
		return 'cb-index' + ':' + _uniqPrefix + ':' + _uniqIndex.toString(36);
	};

	var _config = {
		origins: [],
		handler: null,
		handlerError: null,
		encoders: [],
		clients: [],
		reservedEvents: [
			"message",
			"connect",
			"disconnect",
			"message:raw",
			"event"
		]
	};

	var WindowSocketDataEncode = function (arg, client) {
		switch (typeof(arg)) {
			case "undefined":
				return {
					type: "undefined"
				};
			case "number":
				return {
					type: "number",
					value: arg
				};
			case "bigint":
				return {
					type: "bigint",
					value: arg
				};
			case "boolean":
				return {
					type: "boolean",
					value: arg
				};
			case "string":
				return {
					type: "string",
					value: arg
				};
			case "object":
				if (arg === null || !arg) {
					return {
						type: "null"
					};
				} else if (Array.isArray(arg)) {
					return {
						type: "array",
						value: arg.map(function (item) {
							return WindowSocketDataEncode(item, client);
						})
					};
				} else if (arg instanceof Error) {
					return {
						type: "error",
						value: {
							name : arg.name,
							stack: arg.stack,
							message: arg.message
						}
					};
				} else if (arg instanceof ArrayBuffer) {
					return {
						type: "arraybuffer",
						value: arg
					};
				} else {
					var data = {}, prop;
					for (prop in arg) {
						data[prop] = WindowSocketDataEncode(arg[prop], client);
					}
					return {
						type: "object",
						value: data
					};
				}
			break;
			case "function":
				var func = function () {
					arg.apply({}, arguments);
				};
				func.__callId = func.__callId || _uniq();
				func.__callDestroy = function () {
					app.off("message:callback:" + func.__callId);
				};
				func.__callClient = client;
				app.on("message:callback:" + func.__callId, function () {
					func.apply(
						{},
						arguments
					);
				});
				return {
					type: "function",
					callId: func.__callId
				};
			default:
				console.warn('Unable to encode value');
				return null;
		}
	};
	
	var WindowSocketDataDecode = function (arg, client) {
		if (!arg || typeof(arg) !== "object") {
			return null;
		}

		switch (arg.type) {
			case "undefined":
				return undefined;
			case "number":
			case "bigint":
			case "boolean":
			case "string":
			case "arraybuffer":
				return arg.value;
			case "null":
				return null;
			case "array":
				return arg.value.map(function (item) {
					return WindowSocketDataDecode(item, client);
				});
			case "object":
				var prop, data = {};
				for (prop in arg.value) {
					data[prop] = WindowSocketDataDecode(arg.value[prop], client);
				}
				return data;
			case "error":
				var err = Error(arg.value.message);
				err.name = arg.value.name;
				err.stack = arg.value.stack;
				return err;
			case "function":
				var func = function () {
					func.__callClient.message({
						type: "callback",
						callId: arg.callId,
						args: Array.prototype.slice.call(
							arguments
						)
					});
				};
				func.__callClient = client;
				func.__callId = arg.callId;
				func.__callDestroy = function () {
					func.__callClient.message({
						type: "callback:destroy",
						callId: arg.callId
					});
				};
				return func;
		}
	};

	/**
	 * @class
	 * @name WindowSocket.Client
	 * @param {MessageEvent} event
	 */
	app.Client = function (event) {
		var client = new ApplicationPrototype();

		client.bind(
			/**
			 * @method source
			 * @memberof WindowSocket.Client
			 * @returns {MessageEventSource}
			 */
			function source() {
				return event.source;
			}
		);

		client.bind(
			/**
			 * @method target
			 * @memberof WindowSocket.Client
			 * @returns {WindowProxy}
			 */
			function target() {
				return event.target;
			}
		);

		client.bind(
			/**
			 * @method isSameSource
			 * @param {MessageEventSource} source
			 * @memberof WindowSocket.Client
			 * @returns {boolean}
			 */
			function isSameSource(source) {
				return source === event.source;
			}
		);

		client.bind(
			/**
			 * @method isSameTarget
			 * @param {WindowProxy} target
			 * @memberof WindowSocket.Client
			 * @returns {boolean}
			 */
			function isSameTarget(target) {
				return target === event.target;
			}
		);

		client.bind(
			/**
			 * @method message
			 * @param {...any[]} args
			 * @memberof WindowSocket.Client
			 * @returns {WindowSocket.Client}
			 */
			function message() {
				client.source().postMessage(
					WindowSocketDataEncode(
						Array.prototype.slice.call(
							arguments
						),
						client
					),
					"*"
				);
				return client;
			}
		);

		client.bind(
			/**
			 * @method fire
			 * @memberof WindowSocket.Client
			 * @param {string} eventName
			 * @param {...any} args
			 * @returns {WindowSocket.Client}
			 */
			function fire() {
				var args = Array.prototype.slice.call(arguments);
				client.message(
					{
						type: "event",
						name: args.shift(),
						args: args
					}
				);
				return client;
			}
		);

		client.bind(
			/**
			 * @method disconnect
			 * @memberof WindowSocket.Client
			 */
			function disconnect() {
				_config.clients = _config.clients.filter(
					function (item) {
						return item !== client;
					}
				);
			}
		);

		return client;
	};

	app.bind(
		/**
		 * @method origins
		 * @param {string} origin
		 * @returns {WindowSocket}
		 */

		/**
		 * @method origins
		 * @returns {string[]}
		 */
		function origins(origin) {
			if (
				typeof(origin) === "string" && origin
			) {
				_config.origins.push(origin);
				return app;
			} else {
				return _config.origins;
			}
		}
	);

	app.bind(
		/**
		 * @method stop
		 * @memberof WindowSocket
		 * @returns {WindowSocket}
		 */
		function stop() {
			if (_config.handler) {
				_window.removeEventListener("message", _config.handler);
				_config.handler = null;
			}

			if (_config.handlerError) {
				_window.removeEventListener("messageerror", _config.handlerError);
				_config.handlerError = null;
			}
			return app;
		}
	);

	app.bind(
		/**
		 * @method start
		 * @memberof WindowSocket
		 * @returns {WindowSocket}
		 */
		function start() {
			app.stop();
			_config.handler = _window.addEventListener('message', function (event) {
				/**
				 * @event message:raw
				 * @memberof WindowSocket
				 * @type {MessageEvent}
				 */
				app.emit('message:raw', [event]);
			});
		
			_config.handlerError = _window.addEventListener('messageerror', function (event) {
				/**
				 * @event message:error
				 * @memberof WindowSocket
				 * @type {MessageEvent}
				 */
				app.emit('message:error', [event]);
			});

			return app;
		}
	);

	app.on('message:raw', function (event) {
		var client = app.validateSource(
			event
		);
		if (
			client
		) {
			var data = WindowSocketDataDecode(event.data, client);
			console.log("DecodeData", event.data, data);
			if (Array.isArray(data)) {
				/**
				 * @event message
				 * @memberof WindowSocket
				 * @type {object}
				 */
				app.emit('message', data);
			}
		}
	});

	app.on("message", function (data) {
		if (typeof(data) === "object" && data) {
			switch (data.type) {
				case "callback":
					if (
						data.callId
						&&
						typeof(data.callId) === "string"
						&&
						Array.isArray(
							data.args
						)
					) {
						app.emit(
							"message:callback:" + data.callId, data.args
						);
					} else {
						console.warn("Incorrect callback structure", data);
					}
				break;
				case "callback:destroy":
						if (
							data.callId
							&&
							typeof(data.callId) === "string"
						) {
							app.off("message:callback:" + data.callId);
						} else {
							console.warn("Incorrect callback structure", data);
						}
					break;
				case "event":
					if (
						typeof(data.name) === "string"
						&&
						data.name
						&&
						_config.reservedEvents.indexOf(data.name) === -1
						&&
						Array.isArray(data.args)
					) {
						app.emit(data.name, data.args);
					} else {
						console.warn("Incorrect Event Structure");
					}
				break;
			}
		}
	});

	app.bind(
		/**
		 * @method broadcast
		 * @param {...any[]} args
		 * @memberof WindowSocket
		 * @returns {WindowSocket}
		 */
		function broadcast() {
			var args = Array.prototype.slice.call(
				arguments
			);
			_config.clients.forEach(function (client) {
				client.message(args);
			});
			return app;
		}
	);

	app.bind(
		/**
		 * @method fire
		 * @param {...any[]} args
		 * @memberof WindowSocket
		 * @returns {WindowSocket}
		 */
		function fire() {
			var args = Array.prototype.slice.call(arguments);
			_config.clients.forEach(
				function (client) {
					client.fire.apply(
						client,
						args
					);
				}
			);
		}
	);

	app.bind(
		/**
		 * Validate Message Event Source
		 * @method validateSource
		 * @memberof WindowSocket
		 * @param {MessageEvent} event 
		 * @returns {WindowSocket.Client}
		 */
		function validateSource(event) {
			// event.origin
			// event.source
			// event.data
			// _config.origins
			// _config.encoders

			var client = _config.clients.find(
				/**
				 * @param {WindowSocket.Client} client 
				 */
				function (client) {
					return !client.isSameSource(event.source);
				}
			);

			if (client) {
				return client;
			}

			client = new app.Client(event);

			_config.clients.push(client);


			/**
			 * @event connect
			 * @memberof WindowSocket
			 * @param {WindowSocket.Client}
			 */
			app.emit('connect', [client]);

			return client;
		}
	);


	app.bind(
		/**
		 * get current encoders
		 * @method encoders
		 * @memberof WindowSocket
		 * @returns {WindowSocket.Encoder[]}
		 */

		/**
		 * update encoders
		 * @method encoders
		 * @memberof WindowSocket
		 * @param {WindowSocket.Encoder[]} encoders 
		 * @returns {WindowSocket}
		 */
		function encoders(encoders) {
			if (Array.isArray(encoders)) {
				_config.encoders = encoders;
				return app;
			}
			return _config.encoders;
		}
	);

	app.bind(
		/**
		 * @method clients
		 * @memberof WindowSocket
		 * @returns {WindowSocket.Client[]}
		 */
		function clients() {
			return _config.clients.map(function (client) {
				return client;
			});
		}
	);


	return app;
};

if (typeof(module) === "object" && module) {
	module.exports = WindowSocket;
}