UNPKG

alchemymvc

Version:
684 lines (549 loc) 14.2 kB
var iostream = alchemy.use('socket.io-stream'), libstream = require('stream'); /** * The Socket Conduit Class * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.10 * * @param {Socker} socket * @param {Object} announcement */ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket, announcement) { var that = this; // Indicate this is a websocket this.websocket = socket; this.socket = socket; // Store the headers this.headers = socket.handshake.headers; // Store the announcement data this.announcement = announcement; // Detect node clients if (this.headers['user-agent'] == 'node-XMLHttpRequest') { this.isNodeClient = true; } // Cookies to send to the client this.new_cookies = {}; this.new_cookie_header = []; // The amount of submissions this.counter = 0; // Collection of callbacks this.callbacks = {}; // Create a stream instance this.stream = iostream(socket); // Linkup storage this.linkups = {}; this.parseAnnouncement(); if (!this.isNodeClient) this.parseRequest(); // Listen for payloads socket.on('payload', function onPayload(packet) { that.onPayload(packet); }); // Listen for responses socket.on('response', function onResponse(packet) { that.onResponse(packet); }); // Listen for linkups socket.on('linkup', function onLinkup(packet) { that.onLinkup(packet); }); // Listen for disconnects socket.on('disconnect', function onDisconnect() { let key; that.emit('disconnect'); for (key in that.linkups) { that.linkups[key].destroy(); } }); // Listen for payloads on the stream socket this.stream.on('payload', function onStreamPayload(stream, packet) { packet.stream = stream; that.onPayload(packet); }); // Listen for responses on the stream socket this.stream.on('response', function onStreamResponse(stream, data) { packet.stream = stream; that.onPayload(packet); }); // Listen to data subscriptions this.on('subscribe-data', function gotSubscriptionRequest(arr) { if (!Array.isArray(arr)) { return; } that.registerBindings(arr); }); }); /** * Return the client IP address * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.1 * @version 1.3.1 */ SocketConduit.setProperty(function ip() { let handshake = this.socket?.handshake; if (!handshake) { return null; } if (handshake.headers) { let forwarded_for = handshake.headers['x-forwarded-for'] || handshake.headers['x-real-ip']; if (forwarded_for) { // Forwarded for can contain multiple ip addresses, // return the first one if (forwarded_for.indexOf(',') > -1) { forwarded_for = forwarded_for.before(','); } return forwarded_for; } } return handshake.address || null; }); /** * Is this client still connected? * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.16 * @version 1.3.16 * * @type {boolean} */ SocketConduit.setProperty(function is_connected() { return this.socket?.connected || false; }); /** * Parse the request, get information from the url * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ SocketConduit.setMethod(function parseRequest() { this.parseShortcuts(); this.parseLanguages(); }); /** * Handle a client announcement * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.4.0 * * @param {Object} data */ SocketConduit.setMethod(function parseAnnouncement() { var connections, data = this.announcement; // Store the scene information this.connection_type = data.connection_type; this.last_update = data.last_update || Date.now(); this.scene_id = data.scene; // Register the connection in the user's session this.getSession().registerConnection(this); // Tell the client we're ready this.websocket.emit('ready'); if (alchemy.settings.debugging.debug) { log.info('Established websocket connection to', this.ip, 'using', this.useragent.family, this.useragent.major); } }); /** * Handle an incoming linkup * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.4.0 * * @param {Object} packet */ SocketConduit.setMethod(function onLinkup(packet) { var controller, instance, section, linkup, router, action, split, type, temp, fnc, id; temp = packet.type; // Make sure a type is defined if (!temp) { return; } id = packet.id; // Split by at symbol to get the optional sub section temp = temp.split('@'); if (temp.length == 2) { section = temp[0]; type = temp[1]; router = Router.subSections[section]; if (!router) { return; } } else { type = temp[0]; router = Router; } // See if any socket routes have been set if (router.linkupRoutes[type]) { fnc = router.linkupRoutes[type]; // Create the new linkup instance linkup = new Linkup(this, type, id, packet.data); linkup.submit('connected_to_server'); if (typeof fnc === 'function') { fnc.call(this, this, linkup); } else if (typeof fnc === 'string') { // Strings like 'StaticController#index' split = fnc.split('#'); controller = split[0]; action = split[1]; instance = Controller.get(controller, this); instance.packetType = packet.type; instance.doAction(action, [this, linkup, packet.data]); } } // Emit it on the socket itself this.emit(packet.type, linkup, null); }); /** * Handle a client packet * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.1.4 * * @param {Object} packet */ SocketConduit.setMethod(function onPayload(packet) { var that = this, controller, instance, respond, action, linkup, route, split, args, fnc; if (packet.respond) { respond = function respondToPayload(err, data, stream) { var responsePacket = { err : null, respond_to : packet.id, noData : false, data : null }; if (err) { err = JSON.clone(err, 'toHawkejs'); responsePacket.err = JSON.toDryObject(err); } if (data && data.constructor.name == 'IOStream') { responsePacket.noData = true; stream = data; } else { // Make sure the data is converted to client-side friendly code data = JSON.clone(data, 'toHawkejs'); // Now create a DRY object responsePacket.data = JSON.toDryObject(data); } if (stream && stream.constructor.name == 'IOStream') { return that.stream.emit('response', stream, responsePacket); } that.socket.emit('response', responsePacket); }; } // See if this is for a specific linkup if (packet.link) { linkup = this.linkups[packet.link]; // Do NOT allow packets of the 'linkup_packet' type, // we use that internally for the entire packet if (packet.type == 'linkup_packet') { return; } if (linkup) { let test = linkup.simpleListeners.get(packet.type); if (!test) { console.error('Linkup', linkup, 'has no listener for', packet.type); } if (packet.stream) { linkup.emit(packet.type, packet.data, packet.stream, respond, null); } else { linkup.emit(packet.type, packet.data, respond, null); } // Also emit as a linkup_packet linkup.emit('linkup_packet', packet, respond, null); } return; } // Normalize data-server-events if (packet.type == 'data-server-event') { // See if data is valid, otherwise do nothing if (!packet.data || !packet.data[0]) { return; } // Get the real type packet.type = packet.data[0]; // Store the rest as arguments packet.args = packet.data.slice(1); } // See if any socket routes have been set if (Router.socketRoutes[packet.type]) { route = Router.socketRoutes[packet.type]; if (packet.args) { args = packet.args; // Add "respond" callback to the bottom args.push(respond); } else if (packet.stream) { args = [packet.data, packet.stream, respond]; } else { args = [packet.data, respond]; } route.callHandler(this, args); } // Emit it on the socket itself if (packet.stream) { that.emit(packet.type, packet.data, packet.stream, respond, null); } else { that.emit(packet.type, packet.data, respond, null); } }); /** * Handle a client response * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {Object} packet */ SocketConduit.setMethod(function onResponse(packet) { if (typeof this.callbacks[packet.respond_to] === 'function') { this.callbacks[packet.respond_to](packet.err, packet.data); } delete this.callbacks[packet.respond_to]; }); /** * Submit a message to the client * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.0.1 * @version 1.0.5 * * @param {string} type * @param {Object} data * @param {IOStream} stream * @param {Function} callback */ SocketConduit.setMethod(function submit(type, data, stream, callback) { var packet = {}, regular_stream; if (alchemy.isStream(data)) { callback = stream; stream = data; data = undefined; } else if (typeof data === 'function') { callback = data; data = undefined; } if (!stream || typeof stream == 'function') { callback = stream; stream = undefined; } else if (stream && stream.constructor.name != 'IOStream') { // Keep the regular stream regular_stream = stream; // Create an IOStream stream = this.createStream(); // Pipe the regular stream into the IOStream regular_stream.pipe(stream); } if (Array.isArray(type)) { packet.link = type[0]; packet.type = type[1]; } else { packet.type = type; } if (data && data.constructor.name == 'IOStream') { stream = data; packet.noData = true; } else { // Make sure the data is converted to client-side friendly code data = JSON.clone(data, 'toHawkejs'); // Now create a DRY object packet.data = JSON.toDryObject(data); } packet.id = 's' + (++this.counter); if (typeof callback == 'function') { this.callbacks[packet.id] = callback; packet.respond = true; } if (stream) { // Emit on the IOStream socket return this.stream.emit('payload', stream, packet); } this.socket.emit('payload', packet); }); /** * Create a stream we can send through a websocket connection * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ SocketConduit.setMethod(function createStream() { return iostream.createStream(); }); /** * Create a linkup to the client * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ SocketConduit.setMethod(function linkup(type, data, cb) { var client, id; if (typeof data == 'function') { cb = data; data = {}; } id = type + '-' + Crypto.pseudoHex(); client = new Linkup(this, type, id, data); this.socket.emit('linkup', {type: type, id: id, data: data}); if (cb) { client.once('ready', function whenReady() { cb.call(client, client); }); } return client; }); /** * The Linkup class * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {SocketConduit} conduit * @param {string} type * @param {string} id * @param {Object} data */ var Linkup = Function.inherits('Informer', function Linkup(conduit, type, id, data) { // The socket conduit this.conduit = conduit; // The typename this.type = type; // The unique identifier, as created by the client this.id = id; // The initial data this.initialData = data; // Make it store itself conduit.linkups[id] = this; // Tell the client it's ready this.submit('ready'); this.on('__destroy__', () => { this._destroy(); }); }); /** * Add a reference to its scene_id * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.10 * @version 1.3.10 */ Linkup.setProperty(function scene_id() { return this.conduit?.scene_id; }); /** * Destroy this link * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.10 */ Linkup.setMethod(function destroy() { // Tell the client this.submit('__destroy__'); // Actually do the destroying this._destroy(); // Remove all listeners this.removeAllListeners(); }); /** * Make sure the linkup is removed * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.10 * @version 1.3.10 */ Linkup.setMethod(function _destroy() { // Remove it from the linkups list delete this.conduit.linkups[this.id]; this.emit('destroyed'); }); /** * Submit a message to the client * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} type * @param {Object} data * @param {IOStream} stream * @param {Function} callback */ Linkup.setMethod(function submit(type, data, stream, callback) { this.conduit.submit([this.id, type], data, stream, callback); }); /** * Submit a message to the server on this link and return a promise * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.1.2 * @version 1.1.2 * * @param {string} type * @param {Object} data */ Linkup.setMethod(function demand(type, data, stream) { const that = this; let pledge = new Classes.Pledge(), args = [type, data]; if (stream) { args.push(stream); } this.submit(...args, function done(err, result) { if (err) { return pledge.reject(err); } pledge.resolve(result); }); return pledge; }); /** * Create a stream * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 */ Linkup.setMethod(function createStream() { return this.conduit.createStream(); }); /** * Send an error to the client * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {Error} err * @param {Function} callback */ Linkup.setMethod(function error(err, callback) { console.log('Error:', err); this.submit('error', {stack: err.stack, message: err.message}, callback); });