├── .gitignore ├── .travis.yml ├── spec ├── index.js ├── helpers │ ├── connect.js │ └── utils.js ├── stack.test.js ├── connection.test.js ├── attach.test.js └── handlers.test.js ├── package.json ├── example ├── babelasync.js ├── server.js └── index.html ├── lib └── socket.js ├── CHANGELOG.md ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | script: 7 | - npm test 8 | after_script: 9 | - npm run travis 10 | matrix: 11 | fast_finish: true 12 | -------------------------------------------------------------------------------- /spec/index.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require( 'path' ) 3 | const minimist = require( 'minimist' ) 4 | 5 | const argv = minimist( process.argv.slice( 2 ) ) 6 | 7 | argv._.forEach( file => { 8 | require( path.resolve( file ) ) 9 | }) 10 | -------------------------------------------------------------------------------- /spec/helpers/connect.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const minimist = require( 'minimist' ) 5 | const ioc = require( 'socket.io-client' ) 6 | 7 | const argv = minimist( process.argv.slice( 2 ) ) 8 | 9 | const client = ioc( 'ws://0.0.0.0:' + argv.port, { 10 | transports: [ 'websocket' ] 11 | }) 12 | 13 | client.on( 'disconnect', () => { 14 | process.exit( 0 ) 15 | }) 16 | 17 | process.on( 'message', msg => { 18 | if ( msg.action === 'disconnect' ) { 19 | client.disconnect() 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /spec/helpers/utils.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const Koa = require( 'koa' ) 5 | const ioc = require( 'socket.io-client' ) 6 | const IO = require( '../../' ) 7 | 8 | exports.connection = function( srv, opts ) { 9 | opts = Object.assign({ 10 | transports: [ 'websocket' ] 11 | }, opts ) 12 | let addr = srv.address() 13 | if ( !addr ) { 14 | addr = srv.listen().address() 15 | } 16 | let client = ioc( 'ws://0.0.0.0:' + addr.port, opts ) 17 | client.on( 'disconnect', () => { 18 | srv.close() 19 | }) 20 | return client 21 | } 22 | 23 | exports.application = function( instance ) { 24 | const app = new Koa() 25 | const io = instance || new IO() 26 | io.attach( app ) 27 | return app 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-socket", 3 | "version": "4.4.0", 4 | "description": "Koa meets socket.io connected socket", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node spec/index.js spec/*.test.js", 8 | "cover": "istanbul cover --report lcovonly --print detail spec/index.js spec/*.test.js", 9 | "travis": "npm run cover && cat coverage/lcov.info | coveralls", 10 | "example": "node example/server", 11 | "example-babel": "babel-node --plugins transform-async-to-generator example/babelasync" 12 | }, 13 | "keywords": [ 14 | "koa", 15 | "koa v2", 16 | "socket.io", 17 | "web sockets", 18 | "websocket" 19 | ], 20 | "engines": { 21 | "node": ">= 4" 22 | }, 23 | "repository": "mattstyles/koa-socket", 24 | "author": "Matt Styles", 25 | "license": "MIT", 26 | "dependencies": { 27 | "koa-compose": "3.1.0", 28 | "socket.io": "1.4.5" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "6.6.4", 32 | "babel-plugin-transform-async-to-generator": "6.7.0", 33 | "co": "4.6.0", 34 | "coveralls": "2.11.6", 35 | "istanbul": "0.4.2", 36 | "koa": "^2.0.0-alpha.3", 37 | "minimist": "^1.2.0", 38 | "socket.io-client": "1.4.4", 39 | "tape": "4.5.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/babelasync.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require( 'fs' ) 3 | const path = require( 'path' ) 4 | 5 | const Koa = require( 'koa' ) 6 | const IO = require( '../' ) 7 | const co = require( 'co' ) 8 | 9 | const app = new Koa() 10 | const io = new IO() 11 | 12 | io.attach( app ) 13 | 14 | /** 15 | * Koa Middlewares 16 | */ 17 | app.use( async ( ctx, next ) => { 18 | const start = new Date 19 | await next() 20 | const ms = new Date - start 21 | console.log( `${ ctx.method } ${ ctx.url } - ${ ms }ms` ) 22 | }) 23 | 24 | 25 | /** 26 | * App handlers 27 | */ 28 | app.use( ctx => { 29 | ctx.type = 'text/html' 30 | ctx.body = fs.createReadStream( path.join( __dirname, 'index.html' ) ) 31 | }) 32 | 33 | /** 34 | * io middlewares 35 | */ 36 | io.use( async ( ctx, next ) => { 37 | console.log( 'io middleware' ) 38 | const start = new Date 39 | await next() 40 | const ms = new Date - start 41 | console.log( `WS ${ ms }ms` ) 42 | }) 43 | io.use( async ( ctx, next ) => { 44 | ctx.teststring = 'test' 45 | await next() 46 | }) 47 | 48 | /** 49 | * io handlers 50 | */ 51 | io.on( 'connection', ctx => { 52 | console.log( 'Join event', ctx.socket.id ) 53 | io.broadcast( 'connections', { 54 | numConnections: io.connections.size 55 | }) 56 | }) 57 | 58 | io.on( 'disconnect', ctx => { 59 | console.log( 'leave event', ctx.io.id ) 60 | io.broadcast( 'connections', { 61 | numConnections: io.connections.size 62 | }) 63 | }) 64 | io.on( 'data', ( ctx, data ) => { 65 | console.log( 'data event', data ) 66 | console.log( 'ctx:', ctx.event, ctx.data, ctx.socket.id ) 67 | console.log( 'ctx.teststring:', ctx.teststring ) 68 | ctx.socket.emit( 'response', { 69 | message: 'response from server' 70 | }) 71 | }) 72 | io.on( 'numConnections', packet => { 73 | console.log( `Number of connections: ${ io.connections.size }` ) 74 | }) 75 | 76 | 77 | const PORT = 3000 78 | app.listen( 3000, () => { 79 | console.log( `Listening on ${ PORT }` ) 80 | } ) 81 | -------------------------------------------------------------------------------- /spec/stack.test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const tape = require( 'tape' ) 5 | const co = require( 'co' ) 6 | const IO = require( '../' ) 7 | 8 | const application = require( './helpers/utils' ).application 9 | const connection = require( './helpers/utils' ).connection 10 | 11 | 12 | tape( 'Listeners can be added during runtime to connected clients', t => { 13 | t.plan( 2 ) 14 | 15 | const io = new IO() 16 | const app = application( io ) 17 | 18 | const client = connection( app.server ) 19 | 20 | client.on( 'connect', () => { 21 | var called = false 22 | client.on( 'response', ctx => { 23 | called = true 24 | }) 25 | 26 | client.emit( 'request' ) 27 | 28 | // Wait for a response and see if called turns true 29 | setTimeout( () => { 30 | t.notOk( called, 'Called should remain false' ) 31 | 32 | io.on( 'request', ctx => { 33 | ctx.socket.emit( 'response' ) 34 | }) 35 | 36 | client.emit( 'request' ) 37 | 38 | setTimeout( () => { 39 | t.ok( called, 'IO should now respond to the event and called should be true' ) 40 | client.disconnect() 41 | }, 500 ) 42 | }, 500 ) 43 | }) 44 | }) 45 | 46 | tape( 'Middleware can be added during runtime to connected clients', t => { 47 | t.plan( 2 ) 48 | 49 | const io = new IO() 50 | const app = application( io ) 51 | 52 | const client = connection( app.server ) 53 | 54 | io.on( 'req1', ctx => { 55 | ctx.socket.emit( 'res1', ctx.foo ) 56 | }) 57 | io.on( 'req2', ctx => { 58 | ctx.socket.emit( 'res2', ctx.foo ) 59 | }) 60 | 61 | client.on( 'connect', () => { 62 | client.on( 'res1', data => { 63 | t.notOk( data, 'Middleware did not fire and attach additional prop' ) 64 | 65 | io.use( co.wrap( function *( ctx, next ) { 66 | ctx.foo = 'foo' 67 | })) 68 | 69 | client.emit( 'req2' ) 70 | }) 71 | 72 | client.on( 'res2', data => { 73 | t.ok( data, 'Middleware has fired and attached prop' ) 74 | client.disconnect() 75 | }) 76 | 77 | client.emit( 'req1' ) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /lib/socket.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict"; 3 | 4 | /** 5 | * @class 6 | */ 7 | module.exports = class Socket { 8 | /** 9 | * Socket constructor. 10 | * Called when a socket gets connected and attaches any listeners and middleware to the event chain. 11 | * @param socket } 12 | * @param listeners list of events and handlers 13 | * @param middleware composed middleware function 14 | */ 15 | constructor( socket, listeners, middleware ) { 16 | this.socket = socket 17 | 18 | // The composed middleware function 19 | this.middleware = null 20 | 21 | // Append listeners and composed middleware function 22 | this.update( listeners, middleware ) 23 | } 24 | 25 | /** 26 | * Adds a specific event and callback to this socket 27 | * @param event 28 | * @param data 29 | */ 30 | on( event, handler ) { 31 | this.socket.on( event, ( data, cb ) => { 32 | let packet = { 33 | event: event, 34 | data: data, 35 | socket: this, 36 | acknowledge: cb 37 | } 38 | 39 | if ( !this.middleware ) { 40 | handler( packet, data ) 41 | return 42 | } 43 | 44 | this.middleware( packet ) 45 | .then( () => { 46 | handler( packet, data ) 47 | }) 48 | }) 49 | } 50 | 51 | /** 52 | * Registers the new list of listeners and middleware composition 53 | * @param listeners map of events and callbacks 54 | * @param middleware the composed middleware 55 | */ 56 | update( listeners, middleware ) { 57 | this.socket.removeAllListeners() 58 | this.middleware = middleware 59 | 60 | listeners.forEach( ( handlers, event ) => { 61 | if ( event === 'connection' ) { 62 | return 63 | } 64 | 65 | handlers.forEach( handler => { 66 | this.on( event, handler ) 67 | }) 68 | }) 69 | } 70 | 71 | /** 72 | * Getter for the socket id 73 | * @type 74 | */ 75 | get id() { 76 | return this.socket.id 77 | } 78 | 79 | /** 80 | * Helper through to the socket 81 | * @param event 82 | * @param packet 83 | */ 84 | emit( event, packet ) { 85 | this.socket.emit( event, packet ) 86 | } 87 | 88 | /** 89 | * Helper through to broadcasting 90 | * @param event 91 | * @param packet 92 | */ 93 | broadcast( event, packet ) { 94 | this.socket.broadcast.emit( event, packet ) 95 | } 96 | 97 | /** 98 | * Disconnect helper 99 | */ 100 | disconnect() { 101 | this.socket.disconnect() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require( 'fs' ) 3 | const path = require( 'path' ) 4 | 5 | const Koa = require( 'koa' ) 6 | const IO = require( '../' ) 7 | const co = require( 'co' ) 8 | 9 | const app = new Koa() 10 | const io = new IO() 11 | const chat = new IO( 'chat' ) 12 | 13 | io.attach( app ) 14 | chat.attach( app ) 15 | 16 | /** 17 | * Koa Middlewares 18 | */ 19 | app.use( co.wrap( function *( ctx, next ) { 20 | const start = new Date 21 | yield next() 22 | const ms = new Date - start 23 | console.log( `${ ctx.method } ${ ctx.url } - ${ ms }ms` ) 24 | })) 25 | 26 | 27 | /** 28 | * App handlers 29 | */ 30 | app.use( ctx => { 31 | ctx.type = 'text/html' 32 | ctx.body = fs.createReadStream( path.join( __dirname, 'index.html' ) ) 33 | }) 34 | 35 | /** 36 | * Socket middlewares 37 | */ 38 | io.use( co.wrap( function *( ctx, next ) { 39 | console.log( 'Socket middleware' ) 40 | const start = new Date 41 | yield next() 42 | const ms = new Date - start 43 | console.log( `WS ${ ms }ms` ) 44 | })) 45 | io.use( co.wrap( function *( ctx, next ) { 46 | ctx.teststring = 'test' 47 | yield next() 48 | })) 49 | 50 | /** 51 | * Socket handlers 52 | */ 53 | io.on( 'connection', ctx => { 54 | console.log( 'Join event', ctx.socket.id ) 55 | io.broadcast( 'connections', { 56 | numConnections: io.connections.size 57 | }) 58 | // app.io.broadcast( 'connections', { 59 | // numConnections: socket.connections.size 60 | // }) 61 | }) 62 | 63 | io.on( 'disconnect', ctx => { 64 | console.log( 'leave event', ctx.socket.id ) 65 | io.broadcast( 'connections', { 66 | numConnections: io.connections.size 67 | }) 68 | }) 69 | io.on( 'data', ( ctx, data ) => { 70 | console.log( 'data event', data ) 71 | console.log( 'ctx:', ctx.event, ctx.data, ctx.socket.id ) 72 | console.log( 'ctx.teststring:', ctx.teststring ) 73 | ctx.socket.emit( 'response', { 74 | message: 'response from server' 75 | }) 76 | }) 77 | io.on( 'ack', ( ctx, data ) => { 78 | console.log( 'data event with acknowledgement', data ) 79 | ctx.acknowledge( 'received' ) 80 | }) 81 | io.on( 'numConnections', packet => { 82 | console.log( `Number of connections: ${ io.connections.size }` ) 83 | }) 84 | 85 | /** 86 | * Chat handlers 87 | */ 88 | chat.on( 'connection', ctx => { 89 | console.log( 'Joining chat namespace', ctx.socket.id ) 90 | }) 91 | chat.on( 'message', ctx => { 92 | console.log( 'chat message received', ctx.data ) 93 | 94 | // Broadcasts to everybody, including this connection 95 | app.chat.broadcast( 'message', 'yo connections, lets chat' ) 96 | 97 | // Broadcasts to all other connections 98 | ctx.socket.broadcast( 'message', 'ok connections:chat:broadcast' ) 99 | 100 | // Emits to just this socket 101 | ctx.socket.emit( 'message', 'ok connections:chat:emit' ) 102 | }) 103 | 104 | const PORT = 3000 105 | app.listen( 3000, () => { 106 | console.log( `Listening on ${ PORT }` ) 107 | } ) 108 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Koa-Socket Example 6 | 7 | 8 | 58 | 59 | 60 | 61 | 62 | 63 |
1
64 | 65 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 4.4.0 - 23.05.2016 3 | 4 | * _update_ return server instance [farmisen](https://github.com/farmisen) 5 | * _update_ examples 6 | 7 | ## 4.3.0 - 29.03.2016 8 | 9 | * _add_ `socket.broadcast` helper for broadcast emit 10 | 11 | ## 4.2.0 - 29.02.2016 12 | 13 | * _update_ patch `app.listen` [laggingreflex](https://github.com/laggingreflex) 14 | 15 | ## 4.1.0 16 | 17 | * _update_ use an http server if one already exists [laggingreflex](https://github.com/laggingreflex) 18 | 19 | ## 4.0.0 - 18.01.2016 20 | 21 | * _add_ allow passing options to socket.io instance 22 | * _update_ attach raw socket.io instance 23 | * _update_ attach IO instance for default namespace 24 | * _fix_ use new socket.io identifier type 25 | * _update_ dependencies 26 | * _fix_ attach IO instance and not the raw socket for namespaced instances 27 | 28 | ### Breaking changes 29 | 30 | * koaSocket now attaches instances of itself to the `app`, whereas previously it attaches the socket.io instance. This facilitates accessing `koaSocket` instances from any `app` middleware without having to pass it around as a dependency, the raw socket.io instance is still available as `koaSocket.socket`. 31 | * The raw socket.io instance is now attached as `._io`, whilst the default namespace becomes `.io` if it exists. 32 | * Made it explicit that `koaSocket` only supports node v4 as its lowest version. 33 | 34 | 35 | ## 3.2.0 - 18.11.2015 36 | 37 | * _add_ namespace support 38 | * _add_ configure namespaces attaching to the app using `hidden` 39 | * _add_ 1-arity constructor 40 | 41 | ## 3.1.0 - 16.11.2015 42 | 43 | * _add_ allow runtime updates to middleware and listeners 44 | * _update_ test coverage 45 | * _add_ remove event listeners 46 | 47 | ## 3.0.0 - 16.11.2015 48 | 49 | * _update_ use class notation 50 | * _update_ performance improvements by composing middleware in the socket constructor rather than every event 51 | * _add_ broadcast to all connections 52 | * _add_ test sling 53 | * _update_ throw error on failure to attach socket to koa 54 | 55 | ### Breaking changes 56 | 57 | * koaSocket exposes an IO class, which must be instantiated. This is the same as instantiating a koa v2 app. 58 | * `start` is renamed to `attach`. `Start` is misleading, more so than `attach` which at least does attach socket.io to a koa-callback-powered listener. 59 | * The context packet returned with each event now contains a Socket instance, rather than just the raw socket.io socket 60 | 61 | 62 | ## 2.0.0 - 11.11.2015 63 | 64 | * _add_ koa v2 compatibility 65 | * _update_ middleware composition 66 | 67 | ### Breaking changes 68 | 69 | * Middleware should now be passed `co` wrapped generators, similar to one method of using koa. 70 | * Context is shifted from `this` to the `ctx` parameter, which is passed through middleware to event listeners and is mutable. 71 | 72 | 73 | ## 0.4.0 - 30.04.2015 74 | 75 | * _add_ - event to data packet - *[git-jiby-me](https://github.com/git-jiby-me)* 76 | 77 | ## 0.3.0 - 20.09.2014 78 | 79 | * _add_ - explicit on and off connection callbacks 80 | * _fix_ - disconnect handler only once 81 | 82 | ## 0.2.0 - 19.09.2014 83 | 84 | * _add_ - list of open connections 85 | * _add_ - remove a connection 86 | * _add_ - allow creation of middleware chain 87 | * _add_ - start server convenience function 88 | * _add_ - expose connected socket 89 | 90 | ## 0.1.0 - 18.09.2014 91 | 92 | * _add_ - wrapper around attaching listeners to a socket instance 93 | -------------------------------------------------------------------------------- /spec/connection.test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const fork = require( 'child_process' ).fork 5 | const tape = require( 'tape' ) 6 | const IO = require( '../' ) 7 | 8 | const application = require( './helpers/utils' ).application 9 | const connection = require( './helpers/utils' ).connection 10 | 11 | function forkConnection( srv ) { 12 | return fork( __dirname + '/helpers/connect', [ 13 | '--port', srv.address().port 14 | ]) 15 | } 16 | 17 | 18 | tape( 'Client connects to server', t => { 19 | t.plan( 1 ) 20 | 21 | const socket = new IO() 22 | const client = connection( application( socket ).server ) 23 | 24 | client.on( 'connect', () => { 25 | client.disconnect() 26 | }) 27 | socket.on( 'disconnect', ctx => { 28 | t.pass( 'connect-disconnect cleanly' ) 29 | }) 30 | }) 31 | 32 | tape( 'Number of connections should update when a client connects', t => { 33 | t.plan( 3 ) 34 | 35 | const socket = new IO() 36 | const app = application( socket ) 37 | const client = connection( app.server ) 38 | 39 | t.equal( socket.connections.size, 0, 'socket connections should start at 0' ) 40 | 41 | socket.on( 'connection', ctx => { 42 | t.equal( socket.connections.size, 1, 'one connections should be one connection' ) 43 | ctx.socket.disconnect() 44 | }) 45 | client.on( 'disconnect', ctx => { 46 | t.equal( socket.connections.size, 0, 'after a disconnect there should be 0 again' ) 47 | }) 48 | }) 49 | 50 | tape( 'Number of connections should reflect multiple connectees', t => { 51 | t.plan( 2 ) 52 | 53 | const socket = new IO() 54 | const app = application( socket ) 55 | 56 | app.server.listen() 57 | 58 | t.equal( socket.connections.size, 0, 'socket connections should start at 0' ) 59 | 60 | const c1 = forkConnection( app.server ) 61 | const c2 = forkConnection( app.server ) 62 | 63 | // Give them 500ms to connect, that'll be more than enough and makes life simpler 64 | setTimeout( () => { 65 | t.equal( socket.connections.size, 2, '2 connectors should mean 2 number of connections' ) 66 | c1.send({ action: 'disconnect' }) 67 | c2.send({ action: 'disconnect' }) 68 | app.server.close() 69 | }, 500 ) 70 | }) 71 | 72 | tape( 'A specific connection can be picked from the list of active connections', t => { 73 | t.plan( 1 ) 74 | 75 | const socket = new IO() 76 | const app = application( socket ) 77 | 78 | app._io.on( 'connection', sock => { 79 | t.equal( socket.connections.has( sock.id ), true, 'The socket ID is contained in the connections map' ) 80 | sock.disconnect() 81 | }) 82 | 83 | const client = connection( app.server ) 84 | }) 85 | 86 | tape( 'The connection list can be used to boot a client', t => { 87 | t.plan( 2 ) 88 | 89 | const socket = new IO() 90 | const app = application( socket ) 91 | 92 | app._io.on( 'connection', sock => { 93 | t.equal( socket.connections.size, 1, 'The connected client is registered' ) 94 | }) 95 | 96 | const client = connection( app.server ) 97 | 98 | client.on( 'disconnect', ctx => { 99 | t.equal( socket.connections.size, 0, 'The client has been booted' ) 100 | }) 101 | 102 | 103 | // Do it some time in the future, and do it away from the connection socket instance 104 | setTimeout( () => { 105 | // use /# as id's are socket.io ids are now namespace + '#' + clientID 106 | let sock = socket.connections.get( '/#' + client.id ) 107 | sock.socket.disconnect() 108 | }, 500 ) 109 | }) 110 | 111 | tape( 'A connection handler can be applied to the koaIO instance', t => { 112 | t.plan( 1 ) 113 | 114 | const socket = new IO() 115 | const app = application( socket ) 116 | const srv = app.server.listen() 117 | 118 | const client = connection( srv ) 119 | 120 | socket.on( 'connection', ctx => { 121 | t.pass( 'The socket connection handler is fired' ) 122 | ctx.socket.disconnect() 123 | }) 124 | 125 | }) 126 | -------------------------------------------------------------------------------- /spec/attach.test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const http = require( 'http' ) 5 | 6 | const tape = require( 'tape' ) 7 | const Koa = require( 'koa' ) 8 | const ioc = require( 'socket.io-client' ) 9 | const socketIO = require( 'socket.io' ) 10 | const IO = require( '../' ) 11 | 12 | const application = require( './helpers/utils' ).application 13 | const connection = require( './helpers/utils' ).connection 14 | 15 | tape( 'socket.start alters the app to include socket.io', t => { 16 | t.plan( 2 ) 17 | 18 | const app = new Koa() 19 | const socket = new IO() 20 | socket.attach( app ) 21 | 22 | t.ok( app.io, 'socket is attached to koa app' ) 23 | t.ok( app.server, 'server created linking socket and the koa callback' ) 24 | }) 25 | 26 | tape( 'should not alter a koa app that already has ._io unless called with a namespace', t => { 27 | t.plan( 1 ) 28 | 29 | const app = new Koa() 30 | const socket = new IO() 31 | app._io = {} 32 | 33 | t.throws( () => { 34 | socket.attach( app ) 35 | }, null, 'calling .attach throws an error when ._io already exists without a namespace' ) 36 | }) 37 | 38 | tape( 'should work with koa app that already has .server', t => { 39 | t.plan( 1 ) 40 | 41 | const app = new Koa() 42 | const socket = new IO() 43 | app.server = http.createServer() 44 | socket.attach( app ) 45 | 46 | t.ok( app.io, 'socket is attached to koa app' ) 47 | }) 48 | 49 | tape( 'shouldn\'t work if app.server exists but it\'s not an http server', t => { 50 | t.plan( 1 ) 51 | 52 | const app = new Koa() 53 | const socket = new IO() 54 | app.server = {} 55 | 56 | t.throws( () => { 57 | socket.attach( app ) 58 | }, null, 'calling .attach throws an error when .server already exists but it\'s not an http server' ) 59 | }) 60 | 61 | tape( 'Attaching a namespace to a koa app with socket.io existing is all cool', t => { 62 | t.plan( 2 ) 63 | 64 | const app = new Koa() 65 | const socket = new IO() 66 | const chat = new IO( 'chat' ) 67 | 68 | socket.attach( app ) 69 | 70 | t.doesNotThrow( () => { 71 | chat.attach( app ) 72 | 73 | t.ok( app.chat, 'the chat namespace has been attached to the app' ) 74 | }, null, 'Attaching a new namespace works great' ) 75 | }) 76 | 77 | tape( 'Attaching a namespace to a \'clean\' koa app is fine', t => { 78 | t.plan( 3 ) 79 | 80 | const app = new Koa() 81 | const chat = new IO( 'chat' ) 82 | 83 | t.doesNotThrow( () => { 84 | chat.attach( app ) 85 | 86 | t.ok( app.chat, 'the chat namespace has been attached to the app' ) 87 | t.ok( app._io, 'io will be attached, it just isnt listening' ) 88 | }, null, 'Attaching only a namespace is fine' ) 89 | }) 90 | 91 | tape( 'Manually creating the socketIO instance and attaching namespaces without a default is fine', t => { 92 | t.plan( 1 ) 93 | 94 | const app = new Koa() 95 | const chat = new IO( 'chat' ) 96 | 97 | const server = http.createServer( app.callback() ) 98 | const io = socketIO( server ) 99 | app._io = io 100 | 101 | t.doesNotThrow( () => { 102 | chat.attach( app ) 103 | }, null, 'Attaching a namespace is fine' ) 104 | 105 | }) 106 | 107 | tape( 'Attaching a namespace should be done via an options object', t => { 108 | t.plan( 2 ) 109 | 110 | const app = new Koa() 111 | const chat = new IO({ 112 | namespace: 'chat' 113 | }) 114 | 115 | t.doesNotThrow( () => { 116 | chat.attach( app ) 117 | t.ok( app.chat, 'the chat namespace has been attached to the app' ) 118 | }, null, 'Attaching only a namespace via options hash is fine' ) 119 | }) 120 | 121 | tape( 'Attaching a namespace will attach the IO class', t => { 122 | t.plan( 2 ) 123 | 124 | const app = new Koa() 125 | const chat = new IO( 'chat' ) 126 | 127 | chat.attach( app ) 128 | t.ok( app.chat, 'the chat namespace has been attached to the app' ) 129 | t.ok( app.chat instanceof IO, 'an IO instance has been attached' ) 130 | }) 131 | 132 | tape( 'Namespaces can be hidden from the app object', t => { 133 | t.plan( 2 ) 134 | 135 | const app = new Koa() 136 | const chat = new IO({ 137 | namespace: 'chat', 138 | hidden: true 139 | }) 140 | 141 | chat.attach( app ) 142 | 143 | const srv = app.server.listen() 144 | const client = ioc( 'ws://localhost:' + srv.address().port + '/chat', { 145 | transports: [ 'websocket' ] 146 | }) 147 | 148 | client.on( 'disconnect', () => { 149 | srv.close() 150 | }) 151 | client.on( 'connect', () => { 152 | client.disconnect() 153 | }) 154 | 155 | chat.on( 'connection', ctx => { 156 | t.notOk( app.chat, 'chat should exist but not be available on the app' ) 157 | t.ok( true, 'Client can connect to the chat namespace even though it is not available on the app' ) 158 | }) 159 | }) 160 | 161 | tape( 'The default namespace can not be hidden, app.io must be attached to app', t => { 162 | t.plan( 1 ) 163 | 164 | const app = new Koa() 165 | const io = new IO({ 166 | hidden: true 167 | }) 168 | 169 | t.throws( () => { 170 | io.attach( app ) 171 | }, null, 'Attaching a hidden default instance will throw' ) 172 | }) 173 | 174 | tape( 'Calling app.listen calls app.server.listen', t => { 175 | t.plan( 2 ) 176 | 177 | const app = new Koa() 178 | const io = new IO() 179 | 180 | io.attach( app ) 181 | 182 | app.server.listen = function() { 183 | t.pass( 'Calling app.listen called app.server.listen' ) 184 | } 185 | 186 | t.doesNotThrow( () => { 187 | var srv = app.listen( () => { 188 | srv.close() 189 | }) 190 | }, 'Calling app.listen does not throw' ) 191 | }) 192 | -------------------------------------------------------------------------------- /spec/handlers.test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const tape = require( 'tape' ) 5 | const co = require( 'co' ) 6 | const IO = require( '../' ) 7 | 8 | const application = require( './helpers/utils' ).application 9 | const connection = require( './helpers/utils' ).connection 10 | 11 | 12 | tape( 'An event handler can be associated with an event', t => { 13 | t.plan( 1 ) 14 | 15 | const io = new IO() 16 | const app = application( io ) 17 | const client = connection( app.server ) 18 | 19 | io.on( 'req', ctx => { 20 | t.pass( 'The event handler has been triggered' ) 21 | client.disconnect() 22 | }) 23 | 24 | client.emit( 'req' ) 25 | }) 26 | 27 | tape( 'Multiple events can be set listening', t => { 28 | t.plan( 1 ) 29 | 30 | const io = new IO() 31 | const app = application( io ) 32 | const client = connection( app.server ) 33 | 34 | var count = 0 35 | 36 | io.on( 'req', ctx => { 37 | count++ 38 | }) 39 | io.on( 'req2', ctx => { 40 | count++ 41 | t.equal( count, 2, 'Both events were triggered' ) 42 | client.disconnect() 43 | }) 44 | 45 | client.emit( 'req' ) 46 | client.emit( 'req2' ) 47 | }) 48 | 49 | tape( 'Multiple handlers can be connected to an event', t => { 50 | t.plan( 1 ) 51 | 52 | const io = new IO() 53 | const app = application( io ) 54 | const client = connection( app.server ) 55 | 56 | var count = 0 57 | 58 | io.on( 'req', ctx => { 59 | // First handler 60 | count++ 61 | }) 62 | io.on( 'req', ctx => { 63 | // Second handler 64 | count++ 65 | }) 66 | io.on( 'end', ctx => { 67 | t.equal( count, 2, 'Both handlers should have been triggered' ) 68 | client.disconnect() 69 | }) 70 | 71 | client.emit( 'req' ) 72 | client.emit( 'end' ) 73 | }) 74 | 75 | tape( 'A handler can be removed', t => { 76 | t.plan( 1 ) 77 | 78 | const io = new IO() 79 | const app = application( io ) 80 | const client = connection( app.server ) 81 | 82 | var count = 0 83 | 84 | function add() { 85 | count++ 86 | } 87 | 88 | io.on( 'req', add ) 89 | client.emit( 'req' ) 90 | 91 | setTimeout( () => { 92 | io.off( 'req', add ) 93 | client.emit( 'req' ) 94 | 95 | setTimeout( () => { 96 | t.equal( count, 1, 'Add function is called only once' ) 97 | client.disconnect() 98 | }, 500 ) 99 | }, 500 ) 100 | }) 101 | 102 | tape( 'A handler can be removed from a multiple handler event', t => { 103 | t.plan( 2 ) 104 | 105 | const io = new IO() 106 | const app = application( io ) 107 | const client = connection( app.server ) 108 | 109 | var count = 0 110 | 111 | function add() { 112 | count++ 113 | } 114 | function plus() { 115 | count++ 116 | } 117 | 118 | io.on( 'req', add ) 119 | io.on( 'req', plus ) 120 | client.emit( 'req' ) 121 | 122 | setTimeout( () => { 123 | t.equal( count, 2, 'Both handlers should have been called' ) 124 | io.off( 'req', add ) 125 | client.emit( 'req' ) 126 | 127 | setTimeout( () => { 128 | t.equal( count, 3, 'After removal only one handler will have been triggered' ) 129 | client.disconnect() 130 | }, 500 ) 131 | }, 500 ) 132 | }) 133 | 134 | tape( 'A specific handler can be removed from an event - front', t => { 135 | t.plan( 2 ) 136 | 137 | const io = new IO() 138 | const app = application( io ) 139 | const client = connection( app.server ) 140 | 141 | var count1 = 0 142 | var count2 = 0 143 | 144 | function add() { 145 | count1++ 146 | } 147 | function plus() { 148 | count2++ 149 | } 150 | 151 | io.on( 'req', add ) 152 | io.on( 'req', plus ) 153 | client.emit( 'req' ) 154 | 155 | setTimeout( () => { 156 | t.ok( count1 === 1 && count2 === 1, 'Both handlers should have been called' ) 157 | io.off( 'req', add ) 158 | client.emit( 'req' ) 159 | 160 | setTimeout( () => { 161 | t.ok( count1 === 1 && count2 === 2, 'A specific handler has been removed from the start of the list' ) 162 | client.disconnect() 163 | }, 500 ) 164 | }, 500 ) 165 | }) 166 | 167 | tape( 'A specific handler can be removed from an event - last', t => { 168 | t.plan( 2 ) 169 | 170 | const io = new IO() 171 | const app = application( io ) 172 | const client = connection( app.server ) 173 | 174 | var count1 = 0 175 | var count2 = 0 176 | 177 | function add() { 178 | count1++ 179 | } 180 | function plus() { 181 | count2++ 182 | } 183 | 184 | io.on( 'req', add ) 185 | io.on( 'req', plus ) 186 | client.emit( 'req' ) 187 | 188 | setTimeout( () => { 189 | t.ok( count1 === 1 && count2 === 1, 'Both handlers should have been called' ) 190 | io.off( 'req', plus ) 191 | client.emit( 'req' ) 192 | 193 | setTimeout( () => { 194 | t.ok( count1 === 2 && count2 === 1, 'A specific handler has been removed from the end of the list' ) 195 | client.disconnect() 196 | }, 500 ) 197 | }, 500 ) 198 | }) 199 | 200 | 201 | tape( 'All handlers can be removed from an event', t => { 202 | t.plan( 2 ) 203 | 204 | const io = new IO() 205 | const app = application( io ) 206 | const client = connection( app.server ) 207 | 208 | var count = 0 209 | 210 | function add() { 211 | count++ 212 | } 213 | function plus() { 214 | count++ 215 | } 216 | 217 | io.on( 'req', add ) 218 | io.on( 'req', plus ) 219 | client.emit( 'req' ) 220 | 221 | setTimeout( () => { 222 | t.equal( count, 2, 'Both handlers should have been called' ) 223 | io.off( 'req' ) 224 | client.emit( 'req' ) 225 | 226 | setTimeout( () => { 227 | t.equal( count, 2, 'All handlers have been removed from the event' ) 228 | client.disconnect() 229 | }, 500 ) 230 | }, 500 ) 231 | }) 232 | 233 | tape( 'All handlers can be removed from a socket instance', t => { 234 | t.plan( 2 ) 235 | 236 | const io = new IO() 237 | const app = application( io ) 238 | const client = connection( app.server ) 239 | 240 | var count = 0 241 | 242 | function add() { 243 | count++ 244 | } 245 | function plus() { 246 | count++ 247 | } 248 | 249 | io.on( 'req1', add ) 250 | io.on( 'req2', plus ) 251 | client.emit( 'req1' ) 252 | client.emit( 'req2' ) 253 | 254 | setTimeout( () => { 255 | t.equal( count, 2, 'Both handlers should have been called' ) 256 | io.off() 257 | client.emit( 'req1' ) 258 | client.emit( 'req2' ) 259 | 260 | setTimeout( () => { 261 | t.equal( count, 2, 'All handlers have been removed from the event' ) 262 | client.disconnect() 263 | }, 500 ) 264 | }, 500 ) 265 | }) 266 | 267 | tape( 'Middleware is run before listeners', t => { 268 | t.plan( 1 ) 269 | 270 | const io = new IO() 271 | const app = application( io ) 272 | const client = connection( app.server ) 273 | 274 | var count = 0 275 | 276 | io.use( co.wrap( function *( ctx, next ) { 277 | count++ 278 | })) 279 | io.on( 'req', ctx => { 280 | t.equal( count, 1, 'Middleware runs before listeners' ) 281 | client.disconnect() 282 | }) 283 | 284 | client.emit( 'req' ) 285 | }) 286 | 287 | tape( 'Middleware can manipulate the context', t => { 288 | t.plan( 1 ) 289 | 290 | const io = new IO() 291 | const app = application( io ) 292 | const client = connection( app.server ) 293 | 294 | io.use( co.wrap( function *( ctx, next ) { 295 | ctx.foo = true 296 | })) 297 | io.on( 'req', ctx => { 298 | t.ok( ctx.foo, 'Context can be manipulated' ) 299 | client.disconnect() 300 | }) 301 | 302 | client.emit( 'req' ) 303 | }) 304 | 305 | tape( 'Middleware can be traversed', t => { 306 | t.plan( 2 ) 307 | 308 | const io = new IO() 309 | const app = application( io ) 310 | const client = connection( app.server ) 311 | 312 | io.use( co.wrap( function *( ctx, next ) { 313 | ctx.count = 0 314 | yield next() 315 | t.equal( ctx.count, 1, 'Downstream middleware manipulated the context' ) 316 | ctx.count++ 317 | })) 318 | io.use( co.wrap( function *( ctx, next ) { 319 | ctx.count++ 320 | })) 321 | io.on( 'req', ctx => { 322 | t.equal( ctx.count, 2, 'Middleware upstream and downstream have executed' ) 323 | client.disconnect() 324 | }) 325 | 326 | client.emit( 'req' ) 327 | }) 328 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict"; 3 | 4 | const http = require( 'http' ) 5 | const socketIO = require( 'socket.io' ) 6 | const compose = require( 'koa-compose' ) 7 | 8 | const Socket = require( './lib/socket' ) 9 | 10 | 11 | /** 12 | * Main IO class that handles the socket.io connections 13 | * @class 14 | */ 15 | module.exports = class IO { 16 | /** 17 | * @constructs 18 | * @param namespace namespace identifier 19 | */ 20 | constructor( opts ) { 21 | if ( opts && !(typeof opts !== 'string' || opts && typeof opts !== 'object' ) ) { 22 | throw new Error( 'Incorrect argument passed to koaSocket constructor' ) 23 | } 24 | 25 | /** 26 | * List of middlewares, these are composed into an execution chain and 27 | * evaluated with each event 28 | * @type 29 | */ 30 | this.middleware = [] 31 | 32 | /** 33 | * Composed middleware stack 34 | * @type 35 | */ 36 | this.composed = null 37 | 38 | /** 39 | * All of the listeners currently added to the IO instance 40 | * event:callback 41 | * @type 42 | */ 43 | this.listeners = new Map() 44 | 45 | /** 46 | * All active connections 47 | * id:Socket 48 | * @type 49 | */ 50 | this.connections = new Map() 51 | 52 | /** 53 | * Configuration options 54 | * @type 55 | */ 56 | if ( typeof opts === 'string' ) { 57 | opts = { 58 | namespace: opts 59 | } 60 | } 61 | this.opts = Object.assign({ 62 | /** 63 | * Namespace id 64 | * @type 65 | * @default null 66 | */ 67 | namespace: null, 68 | 69 | /** 70 | * Hidden instances do not append to the koa app, but still require attachment 71 | * @type 72 | * @default false 73 | */ 74 | hidden: false, 75 | 76 | /** 77 | * Options to pass when instantiating socket.io 78 | * @type 79 | * @default {} 80 | */ 81 | ioOptions: {} 82 | }, opts ) 83 | 84 | /** 85 | * Holds the socketIO connection 86 | * @type 87 | */ 88 | this.socket = null 89 | 90 | // Bind handlers 91 | this.onConnection = this.onConnection.bind( this ) 92 | this.onDisconnect = this.onDisconnect.bind( this ) 93 | } 94 | 95 | /** 96 | * Attach to a koa application 97 | * @param app the koa app to use 98 | */ 99 | attach( app ) { 100 | 101 | if ( app.server && app.server.constructor.name != 'Server' ) { 102 | throw new Error( 'app.server already exists but it\'s not an http server' ); 103 | } 104 | 105 | if ( !app.server ) { 106 | // Create a server if it doesn't already exists 107 | app.server = http.createServer( app.callback() ) 108 | 109 | // Patch `app.listen()` to call `app.server.listen()` 110 | app.listen = function listen(){ 111 | app.server.listen.apply( app.server, arguments ) 112 | return app.server 113 | } 114 | } 115 | 116 | if ( app._io ) { 117 | // Without a namespace we’ll use the default, but .io already exists meaning 118 | // the default is taken already 119 | if ( !this.opts.namespace ) { 120 | throw new Error( 'Socket failed to initialise::Instance may already exist' ) 121 | } 122 | 123 | this.attachNamespace( app, this.opts.namespace ) 124 | return 125 | } 126 | 127 | if ( this.opts.hidden && !this.opts.namespace ) { 128 | throw new Error( 'Default namespace can not be hidden' ) 129 | } 130 | 131 | app._io = new socketIO( app.server, this.opts.ioOptions ) 132 | 133 | if ( this.opts.namespace ) { 134 | this.attachNamespace( app, this.opts.namespace ) 135 | return 136 | } 137 | 138 | // Attach default namespace 139 | app.io = this 140 | 141 | // If there is no namespace then connect using the default 142 | this.socket = app._io 143 | this.socket.on( 'connection', this.onConnection ) 144 | } 145 | 146 | /** 147 | * Attaches the namespace to the server 148 | * @param app the koa app to use 149 | * @param id namespace identifier 150 | */ 151 | attachNamespace( app, id ) { 152 | if ( !app._io ) { 153 | throw new Error( 'Namespaces can only be attached once a socketIO instance has been attached' ) 154 | } 155 | 156 | this.socket = app._io.of( id ) 157 | this.socket.on( 'connection', this.onConnection ) 158 | 159 | if ( this.opts.hidden ) { 160 | return 161 | } 162 | 163 | if ( app[ id ] ) { 164 | throw new Error( 'Namespace ' + id + ' already attached to koa instance' ) 165 | } 166 | 167 | app[ id ] = this 168 | } 169 | 170 | /** 171 | * Pushes a middleware on to the stack 172 | * @param fn the middleware function to execute 173 | */ 174 | use( fn ) { 175 | this.middleware.push( fn ) 176 | this.composed = compose( this.middleware ) 177 | 178 | this.updateConnections() 179 | 180 | return this 181 | } 182 | 183 | /** 184 | * Adds a new listeners to the stack 185 | * @param event the event id 186 | * @param handler the callback to execute 187 | * @return this 188 | */ 189 | on( event, handler ) { 190 | let listeners = this.listeners.get( event ) 191 | 192 | // If this is a new event then just set it 193 | if ( !listeners ) { 194 | this.listeners.set( event, [ handler ] ) 195 | this.updateConnections() 196 | return this 197 | } 198 | 199 | listeners.push( handler ) 200 | this.listeners.set( event, listeners ) 201 | this.updateConnections() 202 | return this 203 | } 204 | 205 | /** 206 | * Removes a listener from the event 207 | * @param event if omitted will remove all listeners 208 | * @param handler if omitted will remove all from the event 209 | * @return this 210 | */ 211 | off( event, handler ) { 212 | if ( !event ) { 213 | this.listeners = new Map() 214 | this.updateConnections() 215 | return this 216 | } 217 | 218 | if ( !handler ) { 219 | this.listeners.delete( event ) 220 | this.updateConnections() 221 | return this 222 | } 223 | 224 | let listeners = this.listeners.get( event ) 225 | let i = listeners.length - 1 226 | while( i ) { 227 | if ( listeners[ i ] === handler ) { 228 | break 229 | } 230 | i-- 231 | } 232 | listeners.splice( i, 1 ) 233 | 234 | this.updateConnections() 235 | return this 236 | } 237 | 238 | /** 239 | * Broadcasts an event to all connections 240 | * @param event 241 | * @param data 242 | */ 243 | broadcast( event, data ) { 244 | this.connections.forEach( ( socket, id ) => { 245 | socket.emit( event, data ) 246 | }) 247 | } 248 | 249 | /** 250 | * Triggered for each new connection 251 | * Creates a new Socket instance and adds that to the stack and sets up the 252 | * disconnect event 253 | * @param sock 254 | * @private 255 | */ 256 | onConnection( sock ) { 257 | // let instance = new Socket( sock, this.listeners, this.middleware ) 258 | let instance = new Socket( sock, this.listeners, this.composed ) 259 | this.connections.set( sock.id, instance ) 260 | sock.on( 'disconnect', () => { 261 | this.onDisconnect( sock ) 262 | }) 263 | 264 | // Trigger the connection event if attached to the socket listener map 265 | let handlers = this.listeners.get( 'connection' ) 266 | if ( handlers ) { 267 | handlers.forEach( handler => { 268 | handler({ 269 | event: 'connection', 270 | data: instance, 271 | socket: instance.socket 272 | }, instance.id ) 273 | }) 274 | } 275 | } 276 | 277 | /** 278 | * Fired when the socket disconnects, simply reflects stack in the connections 279 | * stack 280 | * @param sock 281 | * @private 282 | */ 283 | onDisconnect( sock ) { 284 | this.connections.delete( sock.id ) 285 | } 286 | 287 | /** 288 | * Updates all existing connections with current listeners and middleware 289 | * @private 290 | */ 291 | updateConnections() { 292 | this.connections.forEach( connection => { 293 | connection.update( this.listeners, this.composed ) 294 | }) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/mattstyles/koa-socket.svg?branch=master)](https://travis-ci.org/mattstyles/koa-socket) 3 | [![npm version](https://badge.fury.io/js/koa-socket.svg)](https://badge.fury.io/js/koa-socket) 4 | [![Coverage Status](https://coveralls.io/repos/mattstyles/koa-socket/badge.svg?branch=master&service=github)](https://coveralls.io/github/mattstyles/koa-socket?branch=master) 5 | [![Dependency Status](https://david-dm.org/mattstyles/koa-socket.svg)](https://david-dm.org/mattstyles/koa-socket.svg) 6 | 7 | # Koa-socket 8 | 9 | > Sugar for connecting socket.io to a koa instance 10 | 11 | **Koa-socket** is now compatible with koa v2 style of middleware (where context is passed as a parameter), v0.4.0 of koa-socket is the last version to support the old style of middleware. 12 | 13 | As such, koa-socket now requires **node v4.0.0** or higher although koa-socket simply attaches to the server instance so will be compatible with a koa v1 powered app. 14 | 15 | 16 | ## Installation 17 | 18 | ```sh 19 | npm i -S koa-socket 20 | ``` 21 | 22 | ## Example 23 | 24 | ```js 25 | const Koa = require( 'koa' ) 26 | const IO = require( 'koa-socket' ) 27 | 28 | const app = new Koa() 29 | const io = new IO() 30 | 31 | app.use( ... ) 32 | 33 | io.attach( app ) 34 | 35 | io.on( 'join', ( ctx, data ) => { 36 | console.log( 'join event fired', data ) 37 | }) 38 | 39 | app.listen( process.env.PORT || 3000 ) 40 | ``` 41 | 42 | ## Features 43 | 44 | * Attach socket.io to existing koa projects 45 | * Attach koa-style middleware to socket.io events 46 | * Supports koa v2 style of passing context along the response chain 47 | 48 | 49 | ## Attaching to existing projects 50 | 51 | The `attach` function is used to attach the `IO` instance to the application, this adds `server`\* and `io` properties to the koa application and should happen before the app starts listening on a port. 52 | 53 | It also re-maps `app.listen` to `app.server.listen`, so you could simply do `app.listen()`. However if you already had an `app.server` attached, it uses it instead and expects you to do `app.server.listen()` yourself. 54 | 55 | ```js 56 | const Koa = require( 'koa' ) 57 | const IO = require( 'koa-socket' ) 58 | 59 | const app = new Koa() 60 | const io = new IO() 61 | 62 | // Attach the socket to the application 63 | io.attach( app ) 64 | 65 | // Socket is now available as app.io if you prefer 66 | app.io.on( event, eventHandler ) 67 | 68 | // The raw socket.io instance is attached as app._io if you need it 69 | app._io.on( 'connection', sock => { 70 | // ... 71 | }) 72 | 73 | // app.listen is mapped to app.server.listen, so you can just do: 74 | app.listen( process.env.PORT || 3000 ) 75 | 76 | // *If* you had manually attached an `app.server` yourself, you should do: 77 | app.server.listen( process.env.PORT || 3000 ) 78 | ``` 79 | 80 | ## Middleware and event handlers 81 | 82 | Middleware can be added in much the same way as it can be added to any regular koa instance. 83 | 84 | ### Example with *async* functions (transpilation required) 85 | 86 | ```js 87 | io.use( async ( ctx, next ) => { 88 | let start = new Date() 89 | await next() 90 | console.log( `response time: ${ new Date() - start }ms` ) 91 | }) 92 | ``` 93 | 94 | There is an example in the `examples` folder, use `npm run example-babel` to fire it up. The npm script relies on the `babel` require hook, which is not recommended in production. 95 | 96 | 97 | ### Example with generator functions 98 | 99 | Koa v2 no longer supports generators so if you are using v2 then you must use `co.wrap` to have access to the generator style. 100 | 101 | ```js 102 | const Koa = require( 'koa' ) 103 | const IO = require( 'koa-socket' ) 104 | const co = require( 'co' ) 105 | 106 | const app = new Koa() 107 | const io = new IO() 108 | 109 | app.use( ... ) 110 | 111 | io.use( co.wrap( function *( ctx, next ) { 112 | let start = new Date() 113 | yield next() 114 | console.log( `response time: ${ new Date() - start }ms` ) 115 | })) 116 | 117 | io.use( ... ); 118 | 119 | io.on( 'message', ( ctx, data ) => { 120 | console.log( `message: ${ data }` ) 121 | }) 122 | 123 | io.attach( app ) 124 | app.listen( 3000 ); 125 | ``` 126 | 127 | ### Plain example 128 | 129 | Whilst slightly unwieldy, the standalone method also works 130 | 131 | ```js 132 | io.use( ( ctx, next ) => { 133 | let start = new Date() 134 | return next().then( () => { 135 | console.log( `response time: ${ new Date() - start }ms` ) 136 | }) 137 | }) 138 | ``` 139 | 140 | 141 | ## Passed Context 142 | 143 | ```js 144 | let ctx = { 145 | event: listener.event, 146 | data: data, 147 | socket: Socket, 148 | acknowledge: cb 149 | } 150 | ``` 151 | 152 | The context passed to each socket middleware and handler begins the chain with the event that triggered the response, the data sent with that event and the socket instance that is handling the event. There is also a shorthand for firing an acknowledgement back to the client. 153 | 154 | As the context is passed to each function in the response chain it is fair game for mutation at any point along that chain, it is up to you to decide whether this is an anti-pattern or not. There was much discussion around this topic for koa v2. 155 | 156 | 157 | ```js 158 | io.use( async ( ctx, next ) => { 159 | ctx.process = process.pid 160 | await next() 161 | }) 162 | 163 | io.use( async ( ctx, next ) => { 164 | // ctx is passed along so ctx.process is now available 165 | console.log( ctx.process ) 166 | }) 167 | 168 | io.on( 'event', ( ctx, data ) => { 169 | // ctx is passed all the way through to the end point 170 | console.log( ctx.process ) 171 | }) 172 | ``` 173 | 174 | 175 | ## Namespaces 176 | 177 | Namespaces can be defined simply by instantiating a new instance of `koaSocket` and passing the namespace id in the constructor. All other functionality works the same, it’ll just be constrained to the single namespace. 178 | 179 | ```js 180 | const app = new Koa() 181 | const chat = new IO({ 182 | namespace: 'chat' 183 | }) 184 | 185 | chat.attach( app ) 186 | 187 | chat.on( 'message', ctx => { 188 | console.log( ctx.data ) 189 | chat.broadcast( 'response', ... ) 190 | }) 191 | ``` 192 | 193 | Namespaces also attach themselves to the `app` instance, throwing an error if the property name already exists. 194 | 195 | ```js 196 | const app = new Koa() 197 | const chat = new IO({ 198 | namespace: 'chat' 199 | }) 200 | 201 | chat.attach( app ) 202 | 203 | app.chat.use( ... ) 204 | app.chat.on( ... ) 205 | app.chat.broadcast( ... ) 206 | ``` 207 | 208 | The attachment is configurable if you don’t want to muddy the `app` object with all your namespaces. 209 | 210 | ```js 211 | const chat = new IO({ 212 | namespace: 'chat', 213 | hidden: true 214 | }) 215 | 216 | chat.use( ... ) 217 | chat.on( ... ) 218 | ``` 219 | 220 | Namespaces are fairly ubiquitous so they get a dirty shorthand for creating them, note that if you want to add any additional options you’ll need to use the longhand object parameter to instantiate `koaSocket`. 221 | 222 | ```js 223 | const chat = new IO( 'chat' ) 224 | ``` 225 | 226 | 227 | ## API 228 | 229 | ### .attach( `Koa app` ) 230 | 231 | Attaches to a koa application 232 | 233 | ```js 234 | io.attach( app ) 235 | app.listen( process.env.PORT ) 236 | ``` 237 | 238 | ### .use( `Function callback` ) 239 | 240 | Applies middleware to the stack. 241 | 242 | Middleware are executed each time an event is reacted to and before the callback is triggered for an event. 243 | 244 | Middleware with generators should use `co.wrap`. 245 | 246 | Middleware functions are called with `ctx` and `next`. The context is passed through each middleware and out to the event listener callback. `next` allows the middleware chain to be traversed. Under the hood `koa-compose` is used to follow functionality with `koa`. 247 | 248 | 249 | ```js 250 | io.use( async ( ctx, next ) { 251 | console.log( 'Upstream' ) 252 | await next() 253 | console.log( 'Downstream' ) 254 | }) 255 | ``` 256 | 257 | ### .on( `String event`, `Function callback` ) 258 | 259 | Attaches a callback to an event. 260 | 261 | The callback is fired after any middleware that are attached to the instance and is called with the `ctx` object and the `data` that triggered the event. The `data` can also be found on the `ctx`, the only potential difference is that `data` is the raw `data` emitted with the event trigger whilst `ctx.data` could have been mutated within the middleware stack. 262 | 263 | ```js 264 | io.on( 'join', ( ctx, data ) => { 265 | console.log( data ) 266 | console.log( ctx.data, data ) 267 | }) 268 | ``` 269 | 270 | ### .off( `String event`, `Function callback` ) 271 | 272 | Removes a callback from an event. 273 | 274 | If the `event` is omitted then it will remove all listeners from the instance. 275 | 276 | If the `callback` is omitted then all callbacks for the supplied event will be removed. 277 | 278 | ```js 279 | io.off( 'join', onJoin ) 280 | io.off( 'join' ) 281 | io.off() 282 | ``` 283 | 284 | ### .broadcast( `String event`, `data` ) 285 | 286 | Sends a message to all connections. 287 | 288 | 289 | ## Running tests 290 | 291 | ```sh 292 | npm test 293 | ``` 294 | 295 | ## License 296 | 297 | MIT 298 | --------------------------------------------------------------------------------