UNPKG

koa-socket-2

Version:
348 lines (293 loc) 8.45 kB
"use strict"; const socketIO = require( 'socket.io' ); const compose = require( 'koa-compose' ); /** * Main IO class that handles the socket.io connections * @class */ module.exports = class IO { /** * @constructs * @param namespace <String> namespace identifier */ constructor( opts ) { if ( opts && !(typeof opts !== 'string' || opts && typeof opts !== 'object' ) ) { throw new Error( 'Incorrect argument passed to koaSocket constructor' ); } // app._io reference this._io = null; /** * List of middlewares, these are composed into an execution chain and * evaluated with each event * @type <Array:Function> */ this.middleware = []; /** * Composed middleware stack * @type <Function> */ this.composed = null; /** * All of the listeners currently added to the IO instance * event:callback * @type <Map> */ this.listeners = new Map(); /** * All active connections * id:Socket * @type <Map> */ this.connections = new Map(); /** * Configuration options * @type <Object> */ if ( typeof opts === 'string' ) { opts = { namespace: opts }; } this.opts = Object.assign({ /** * Namespace id * @type <String> * @default null */ namespace: null, /** * Hidden instances do not append to the koa app, but still require attachment * @type <Boolean> * @default false */ hidden: false, /** * Options to pass when instantiating socket.io * @type <Object> * @default {} */ ioOptions: {} }, opts ); /** * Holds the socketIO connection * @type <Socket.IO> */ this.socket = null; // Bind handlers this.onConnection = this.onConnection.bind( this ); this.onDisconnect = this.onDisconnect.bind( this ); } /** * Attach to a koa application * @param app <Koa app> the koa app to use * @param https <Boolean> whether to activate HTTPS */ attach( app, https, opts ) { let http = https ? require('https') : require('http'); if ( app.server && app.server.constructor.name != 'Server' ) { throw new Error( 'app.server already exists but it\'s not an http server' ); } if ( !app.server ) { // Create a server if it doesn't already exists app.server = https ? http.createServer(opts || {}, app.callback()) : http.createServer(app.callback()); // Patch `app.listen()` to call `app.server.listen()` app.listen = function listen(){ app.server.listen.apply( app.server, arguments ); return app.server; } } if ( app._io ) { // Without a namespace we’ll use the default, but .io already exists meaning // the default is taken already if ( !this.opts.namespace ) { throw new Error( 'Socket failed to initialise::Instance may already exist' ); } this.attachNamespace( app, this.opts.namespace ); return; } if ( this.opts.hidden && !this.opts.namespace ) { throw new Error( 'Default namespace can not be hidden' ); } app._io = socketIO( app.server, this.opts.ioOptions ); if ( this.opts.namespace ) { this.attachNamespace( app, this.opts.namespace ); return; } // Local aliases / passthrough socket.io functionality this.adapter = app._io.adapter.bind(app._io); // Attach default namespace app.io = this; // If there is no namespace then connect using the default this.socket = app._io; this.socket.on( 'connection', this.onConnection ); } /** * Attaches the namespace to the server * @param app <Koa app> the koa app to use * @param id <String> namespace identifier */ attachNamespace( app, id ) { if ( !app._io ) { throw new Error( 'Namespaces can only be attached once a socketIO instance has been attached' ); } this.socket = app._io.of( id ); this.socket.on( 'connection', this.onConnection ); if ( this.opts.hidden ) { return; } if ( app[ id ] ) { throw new Error( 'Namespace ' + id + ' already attached to koa instance' ); } app[ id ] = this; } /** * Pushes a middleware on to the stack * @param fn <Function> the middleware function to execute */ use( fn ) { this.middleware.push( fn ); this.composed = compose( this.middleware ); this.updateConnections(); return this; } /** * Adds a new listener to the stack * @param event <String> the event id * @param handler <Function> the callback to execute * @return this */ on( event, handler ) { if(['connect', 'connection'].includes(event)) { this.socket.on(event, handler); return this; } let listeners = this.listeners.get( event ); // If this is a new event then just set it if ( !listeners ) { this.listeners.set( event, [ handler ] ); this.updateConnections(); return this; } listeners.push( handler ) this.listeners.set( event, listeners ); this.updateConnections(); return this; } /** * Removes a listener from the event * @param event <String> if omitted will remove all listeners * @param handler <Function> if omitted will remove all from the event * @return this */ off( event, handler ) { if ( !event ) { this.listeners = new Map(); this.updateConnections(); return this; } if ( !handler ) { this.listeners.delete( event ); this.updateConnections(); return this; } let listeners = this.listeners.get( event ); let i = listeners.length - 1; while( i ) { if ( listeners[ i ] === handler ) { break; } i--; } listeners.splice( i, 1 ); this.updateConnections(); return this; } /** * Broadcasts an event to all connections * @param event <String> * @param data <?> */ broadcast( event, data ) { this.connections.forEach( ( socket, id ) => socket.emit( event, data ) ); } /** * Perform an action on a room * @param room <String> * @return socket <Object> */ to( room ) { return this.socket.to(room); } /** * Triggered for each new connection * Creates a new Socket instance and adds that to the stack and sets up the * disconnect event * @param sock <Socket.io Socket> * @private */ onConnection( sock ) { /** * Adds a specific event and callback to this socket * @param event <String> * @param data <?> */ sock._on = ( event, handler ) => sock.on( event, ( data, cb ) => { let packet = { event: event, data: data, socket: sock, acknowledge: cb }; if ( !this.composed ) { handler( packet, data ); return; } this.composed( packet, () => handler( packet, data ) ); }); /** * Registers the new list of listeners and middleware composition * @param listeners <Map> map of events and callbacks * @param middleware <Function> the composed middleware */ sock.update = ( listeners ) => { sock.removeAllListeners(); listeners.forEach( ( handlers, event ) => { if ( event === 'connection' ) { return; } handlers.forEach( handler => sock._on( event, handler ) ); }) }; // Append listeners and composed middleware function sock.update( this.listeners ); this.connections.set( sock.id, sock ); sock.on( 'disconnect', () => this.onDisconnect( sock ) ); // Trigger the connection event if attached to the socket listener map let handlers = this.listeners.get( 'connection' ); if ( handlers ) { handlers.forEach( handler => handler({ event: 'connection', data: sock, socket: sock }, sock.id ) ); } } /** * Fired when the socket disconnects, simply reflects stack in the connections * stack * @param sock <Socket.io Socket> * @private */ onDisconnect( sock ) { this.connections.delete( sock.id ); } /** * Updates all existing connections with current listeners and middleware * @private */ updateConnections() { this.connections.forEach( connection => connection.update( this.listeners, this.composed ) ); } }