UNPKG

alchemymvc

Version:
666 lines (545 loc) 14.2 kB
/** * See if the given object is a stream * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.0.5 * * @return {boolean} */ function isStream(obj) { return obj && (typeof obj._read == 'function' || typeof obj._write == 'function') && typeof obj.on === 'function'; }; /** * The Linkup class * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.10 * * @param {string} type */ var Linkup = Blast.Collection.Function.inherits('Informer', function ClientLinkup(client, type, data) { var server_object, id; if (type && typeof type == 'object' && type.id && data == null) { server_object = type; id = server_object.id; type = server_object.type; data = server_object.data; } else { id = type + '-' + Blast.Classes.Crypto.pseudoHex(); } // The identifier this.id = id; // The typename of the link this.type = type; // The initial submitted data this.initialData = data; // Make the linkup store itself client.linkups[this.id] = this; // The parent server this.client = client; if (server_object) { this.submit('ready'); } else { // Establish the link client._submit('linkup', {type: type, id: this.id, data: data}); // The server will send the `connected_to_server` event this.on('connected_to_server', () => { this.emit('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() { if (Blast.isBrowser) { return hawkejs.scene?.scene_id; } return this.conduit?.scene_id; }); /** * Submit a message to the server on this link * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.10 * * @param {string} type * @param {Object} data * @param {Function} callback */ Linkup.setAfterMethod('ready', function submit(type, data, stream, callback) { this.client.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.client.createStream(); }); /** * Destroy this linkup (and tell the server) * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.10 */ Linkup.setMethod(function destroy() { this.submit('__destroy__'); this._destroy(); 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() { delete this.client.linkups[this.id]; this.emit('destroyed'); }); /** * Actually make the socket.io connection, * this requires the socket.io js to be loaded * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.0.1 * @version 0.2.0 * * @param {string} address Address to connect to * @param {Object} data Announcement data * @param {Function} callback */ var Client = Blast.Collection.Function.inherits('Informer', function ClientSocket() { var that = this; // Connected is false this.connected = false; // The packet queue this.queue = []; // The callbacks this.callbacks = {}; // Established linkups this.linkups = {}; // The client message counter this.counter = 0; // The server object this.server = null; // The server stream this.serverstream = null; // The timesync offset this.offset = 0; // The connection latency this.latency = 0; // Enable auto reconnect this.reconnect = true; // Server-linkup listeners this.server_linkup_listeners = {}; this.emitPacket = function emitPacket(packet) { var stream; if (packet.stream) { stream = packet.stream; delete packet.stream; that.serverstream.emit('payload', stream, packet); } else { that.server.emit('payload', packet); } }; }); /** * Get an offset-corrected timestamp * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.1 * @version 0.2.1 * * @return {number} */ Client.setMethod(function now() { return Date.now() + (this.offset || 0); }); /** * Low level socket emit * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.1.0 */ Client.setMethod(function _submit() { var that = this, args = Array.cast(arguments); this.afterOnce('connected', function connected() { that.server.emit.apply(that.server, args); }); }); /** * Submit method * * @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 */ Client.setMethod(function submit(type, data, stream, callback) { var packet = {}, regular_stream; if (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 { packet.data = data; } packet.id = 'c' + (++this.counter); packet.stream = stream; if (typeof callback == 'function') { this.callbacks[packet.id] = callback; packet.respond = true; } if (this.connected) { this.emitPacket(packet); } else { this.queue.push(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 */ Client.setMethod(function createStream() { var stream; if (Blast.isNode) { stream = alchemy.use('socket.io-stream').createStream(); } else { stream = ss.createStream(); } return stream; }); /** * Make the actual connection * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.12 * * @param {Function} callback */ Client.setMethod(function connect(address, data, callback) { // @TODO: Actual disconnects will cause problems // (Though the library reconnects behind the scenes) if (this.connected || this.hasBeenSeen('connecting')) { this.afterOnce('connected', callback); return; } let that = this, io_client, serverstream, config = {}, server; if (typeof address == 'function') { callback = address; data = {}; address = null; } else if (typeof data == 'function') { callback = data; data = {}; } if (!data || typeof data != 'object') { data = {}; } if (typeof io != 'undefined') { io_client = io; } else if (typeof alchemy?.use == 'function') { io_client = alchemy.use('socket.io-client'); } else { return callback(new Error('Could not find socket.io client library')); } // The address to connect to this.address = address; if (Blast.isNode) { data.connection_type = 'node'; data.discovery = alchemy.discovery_id; } else { data.connection_type = 'browser'; data.scene = hawkejs.scene.sceneId; data.last_update = alchemy.last_update; } if (!this.reconnect) { config.reconnection = false; } if (Blast.isNode) { let msgpack_parser = alchemy.use('socket.io-msgpack-parser'); if (msgpack_parser) { config.parser = msgpack_parser; } } // Create the connection to the server if (address) { server = io_client(address, config); } else { server = io_client(config); } if (Blast.isNode) { serverstream = alchemy.use('socket.io-stream')(server); } else { serverstream = ss(server); } this.server = server; this.serverstream = serverstream; // Announce ourselves when we've connected server.on('connect', function onConnect() { server.emit('announce', data); }); // Emit the close event once we get disconnected from the server server.on('disconnect', function closed() { that.connected = false; that.emit('close'); }); // Listen to timesync commands server.on('timesync', function gotTimesync(data) { // When offset is not defined, // the server is actually requesting our timestamp if (data.offset == null) { data.client_time = Date.now(); server.emit('timesync', data); return; } // If the offset property is set, // the timesync procedure has finished // Set the values in this object that.offset = data.offset || 0; that.latency = data.latency || 0; // Emit it as an event, too that.emit('timesynced', that.offset, that.latency); }); // Listen for the ready event server.on('ready', function onReady() { that.connected = true; if (callback) { callback(); } that.emit('connected'); // Emit all the queued packets that.queue.forEach(that.emitPacket); // Request a timesync that.server.emit('timesync', {start: Date.now()}); // Reset the queue that.queue.length = 0; }); server.on('error', function(err) { console.log('Socket error:', err); }); // Listen for cookies server.on('alchemy-set-cookie', function setCookie(data) { if (Blast.isNode) { // @TODO: set node cookie? } else { hawkejs.scene.cookie(data.name, data.value, data.options); } }); // Listen for server initiated linkups server.on('linkup', function gotLinkup(config) { var linkup = new Linkup(that, config), i; // Look through the server-linkup callbacks if (that.server_linkup_listeners[config.type]) { for (i = 0; i < that.server_linkup_listeners[config.type].length; i++) { that.server_linkup_listeners[config.type][i].call(that, linkup, config.data); } } // Emit it on the client itself that.emit(linkup.type, linkup, null); }); // Listen for payloads server.on('payload', onPacket); // Listen for payloads with streams serverstream.on('payload', function onPayload(stream, packet) { packet.stream = stream; onPacket(packet); }); // Listen for responses with streams serverstream.on('response', function onStreamingResponse(stream, packet) { packet.stream = stream; onResponse(packet); }); // Listen for responses server.on('response', onResponse); // The function that handles responses function onResponse(packet) { if (typeof that.callbacks[packet.respond_to] === 'function') { if (packet.err && typeof packet.err == 'object') { try { packet.err = JSON.undry(packet.err); } catch (err) { console.log('Error undrying error:', err, packet); } } try { if (packet.data && typeof packet.data == 'object') { packet.data = JSON.undry(packet.data); } } catch (err) { console.log('ERROR UNDRYING PACKET:', err, packet); return; } if (packet.noData) { that.callbacks[packet.respond_to](packet.err, packet.stream); } else if (packet.stream) { that.callbacks[packet.respond_to](packet.err, packet.data, packet.stream); } else { that.callbacks[packet.respond_to](packet.err, packet.data); } } delete that.callbacks[packet.respond_to]; } // The function that handles packets function onPacket(packet) { var respond; try { if (packet.data && typeof packet.data == 'object') { packet.data = JSON.undry(packet.data); } } catch (err) { console.log('ERROR UNDRYING PACKET:', err, packet); return; } if (packet.respond) { respond = function respond(err, data) { var responsePacket = {}; responsePacket.err = err; responsePacket.respond_to = packet.id; responsePacket.data = data; server.emit('response', responsePacket); }; } // See if this is for a specific linkup if (packet.link) { if (that.linkups[packet.link]) { if (packet.stream) { that.linkups[packet.link].emit(packet.type, packet.data, packet.stream, respond, null); } else { that.linkups[packet.link].emit(packet.type, packet.data, respond, null); } } return; } if (packet.stream) { that.emit(packet.type, packet.data, packet.stream, respond, null); } else { that.emit(packet.type, packet.data, respond, null); } } }); /** * Create a namespace and inform the server * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} type The typename of link to create * @param {Object} data The initial data to submit * @param {Function} cb Called when link isready * * @return {Linkup} */ Client.setMethod(function linkup(type, data, cb) { if (typeof data == 'function') { cb = data; data = {}; } let link = new Linkup(this, type, data); if (cb) { link.once('ready', function whenReady() { cb.call(link, link); }); } return link; }); /** * Listen for specific server-initiated linkups * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} type The typename of link to listen to * @param {Function} callback */ Client.setMethod(function onLinkup(type, callback) { if (this.server_linkup_listeners[type] == null) { this.server_linkup_listeners[type] = []; } this.server_linkup_listeners[type].push(callback); });