├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── example ├── index.html └── server.js ├── index.js ├── package.json └── spec ├── attach.test.js ├── connection.test.js ├── handlers.test.js ├── helpers ├── connect.js └── utils.js ├── index.js └── stack.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | coverage 4 | .idea 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | script: 6 | - npm test 7 | after_script: 8 | - npm run travis 9 | matrix: 10 | fast_finish: true 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 1.1.0 - 05.09.2019 3 | 4 | * _update_ promoted Socket.IO for better API exposure 5 | * _update_ improved async passthrough handling 6 | 7 | ## 1.0.17 - 05.12.2017 8 | 9 | * _update_ improved and re-added bug fix for promise chaining (credit: ihwbox) 10 | 11 | ## 1.0.16 - 03.12.2017 12 | 13 | * _update_ revert bug fix for promise chaining, tests failing 14 | 15 | ## 1.0.15 - 03.12.2017 16 | 17 | * _update_ bug fix for promise chaining 18 | 19 | ## 1.0.14 - 23.11.2017 20 | 21 | * _update_ bug fix for multi-node adapter support (credit: ihwbox) 22 | 23 | ## 1.0.13 - 23.06.2017 24 | 25 | * _update_ room broadcast functionality 26 | 27 | ## 1.0.12 - 14.06.2017 28 | 29 | * _add_ socket ack functionality 30 | 31 | ## 1.0.11 - 13.06.2017 32 | 33 | * _update_ documentation 34 | 35 | ## 1.0.10 - 08.06.2017 36 | 37 | * _add_ multi-node clustering adapter support 38 | 39 | ## 1.0.9 - 07.06.2017 40 | 41 | * _remove_ babel support 42 | * _remove_ co wrapper support 43 | * _update_ examples to NodeJS v7 44 | * _update_ documentation 45 | * _update_ tests 46 | 47 | ## 1.0.8 - 06.06.2017 48 | 49 | * _update_ minor bug fixes for room support 50 | 51 | ## 1.0.7 - 06.06.2017 52 | 53 | * _update_ documentation 54 | * _add_ room list 55 | 56 | ## 1.0.6 - 06.06.2017 57 | 58 | * _update_ improved room management 59 | 60 | ## 1.0.5 - 05.06.2017 61 | 62 | * _add_ room join/leave support 63 | 64 | ## 1.0.4 - 02.06.2017 65 | 66 | * _update_ documentation 67 | 68 | ## 1.0.3 - 18.05.2017 69 | 70 | * _update_ documentation 71 | 72 | ## 1.0.2 - 15.05.2017 73 | 74 | * _update_ breaking change - broadcast functionality now works as in the native method 75 | * _add_ volatile message support 76 | * _add_ compress message support 77 | 78 | ## 1.0.1 - 12.05.2017 79 | 80 | * _add_ built-in HTTPS support 81 | 82 | ## 1.0.0 - 11.05.2017 83 | 84 | * _update_ upgraded socket.io version to >= 2.0.1 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ambelovsky/koa-socket-2.svg?branch=master)](https://travis-ci.org/ambelovsky/koa-socket-2) 2 | [![NPM Version](https://img.shields.io/npm/v/koa-socket-2.svg)](https://npmjs.com/packages/koa-socket-2) 3 | 4 | # Koa-socket-2 5 | 6 | > Sugar for connecting socket.io to a Koa instance 7 | 8 | **Koa-socket-2 uses socket.io v3. It is recommended that you connect to a koa-socket-2 server with a socket.io v3 client.** 9 | 10 | Koa-socket-2 is only compatible with Koa v2 style of middleware (where context is passed as a parameter). 11 | 12 | Koa-socket-2 requires Node v7.0.0 or higher. 13 | 14 | ## Interested in GoLang? 15 | 16 | This project helps you start a SocketIO server using Koa and NodeJS. However, Google's Go language -- or GoLang -- allows you to write code that compiles down to binary. It can be a very good way to take your SocketIO server to the next level by running faster and requiring less overhead than runtime environments like NodeJS. 17 | 18 | If you're interested in building a SocketIO server in GoLang, take a look at [gosf.io](http://gosf.io) or [GOSF on GitHub](https://github.com/ambelovsky/gosf), the GoLang SocketIO Framework for building SocketIO API servers. 19 | 20 | ## Installation 21 | 22 | ```sh 23 | npm i -S koa-socket-2 24 | ``` 25 | 26 | ## HTTP Example 27 | 28 | Please make the world a better place and stop using unsecure channels. If you 29 | absolutely must, however, then the following will get you started. 30 | 31 | ```js 32 | const Koa = require('koa'); 33 | const IO = require('koa-socket-2'); 34 | 35 | const app = new Koa(); 36 | const io = new IO(); 37 | 38 | app.use( ... ); 39 | 40 | io.attach(app); 41 | 42 | io.on('message', (ctx, data) => { 43 | console.log('client sent data to message endpoint', data); 44 | }); 45 | 46 | app.listen( process.env.PORT || 3000 ); 47 | ``` 48 | 49 | ## HTTPS Example 50 | 51 | ```js 52 | const Koa = require('koa'); 53 | const IO = require('koa-socket-2'); 54 | const fs = require('fs'); 55 | 56 | // If you want to access the HTTPS server from a local JS client for 57 | // development, then try this simple plugin: 58 | app.use(async (ctx, next) => { 59 | ctx.set('Access-Control-Allow-Origin', 'null'); 60 | ctx.set('Access-Control-Allow-Credentials', 'true'); 61 | await next(); 62 | }); 63 | 64 | const app = new Koa(); 65 | const io = new IO(); 66 | 67 | app.use( ... ); 68 | 69 | // Replace the "..." placeholders below with your own SSL certificate files 70 | io.attach(app, true, { 71 | key: fs.readFileSync(...), 72 | cert: fs.readFileSync(...), 73 | ca: fs.readFileSync(...) 74 | }); 75 | 76 | console.log('Server: HTTPS/TLS Enabled.'); 77 | 78 | io.on('message', (ctx, data) => { 79 | console.log('client sent data to message endpoint', data); 80 | }); 81 | 82 | app.listen(process.env.PORT || 3000); 83 | ``` 84 | 85 | ## Features 86 | 87 | * Attach socket.io to existing koa projects 88 | * Attach koa-style middleware to socket.io events 89 | * Supports koa v2 style of passing context along the response chain 90 | 91 | 92 | ## Attaching to existing projects 93 | 94 | 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. 95 | 96 | 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. 97 | 98 | ```js 99 | const Koa = require( 'koa' ); 100 | const IO = require( 'koa-socket-2' ); 101 | 102 | const app = new Koa(); 103 | const io = new IO(); 104 | 105 | // Attach the socket to the application 106 | io.attach( app ); 107 | 108 | // Socket is now available as app.io if you prefer 109 | app.io.on( event, eventHandler ); 110 | 111 | // The raw socket.io instance is attached as app._io if you need it 112 | app._io.on( 'connection', sock => { 113 | // ... 114 | }); 115 | 116 | // *If* you had manually attached an `app.server` yourself, you should do: 117 | app.listen = function() { 118 | app.server.listen.apply(app.server, arguments); 119 | return app.server; 120 | } 121 | 122 | // app.listen is mapped to app.server.listen, so you can just do: 123 | app.listen( process.env.PORT || 3000 ); 124 | ``` 125 | 126 | ## Middleware and event handlers 127 | 128 | Middleware can be added in much the same way as it can be added to any regular koa instance. 129 | 130 | ### Example with *async* functions 131 | 132 | ```js 133 | io.use( async ( ctx, next ) => { 134 | let start = new Date(); 135 | await next(); 136 | console.log( `response time: ${ new Date() - start }ms` ); 137 | }) 138 | ``` 139 | 140 | 141 | ### Example with generator functions 142 | 143 | Don't use generator functions. Get with the times, and upgrade to Node >= 7.X.X. 144 | 145 | 146 | ### Plain example 147 | 148 | Whilst slightly unwieldy, the standalone method also works 149 | 150 | ```js 151 | io.use( ( ctx, next ) => { 152 | let start = new Date() 153 | return next().then( () => { 154 | console.log( `response time: ${ new Date() - start }ms` ) 155 | }) 156 | }) 157 | ``` 158 | 159 | 160 | ## Passed Context 161 | 162 | ```js 163 | let ctx = { 164 | event: listener.event, 165 | data: data, 166 | socket: Socket, 167 | acknowledge: cb 168 | } 169 | ``` 170 | 171 | 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. 172 | 173 | 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. 174 | 175 | 176 | ```js 177 | io.use( async ( ctx, next ) => { 178 | ctx.process = process.pid 179 | await next() 180 | }) 181 | 182 | io.use( async ( ctx, next ) => { 183 | // ctx is passed along so ctx.process is now available 184 | console.log( ctx.process ) 185 | }) 186 | 187 | io.on( 'event', ( ctx, data ) => { 188 | // ctx is passed all the way through to the end point 189 | console.log( ctx.process ) 190 | }) 191 | ``` 192 | 193 | 194 | ## Namespaces 195 | 196 | 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. 197 | 198 | ```js 199 | const app = new Koa() 200 | const chat = new IO({ 201 | namespace: 'chat' 202 | }); 203 | 204 | chat.attach( app ); 205 | 206 | chat.on( 'message', ctx => { 207 | console.log( ctx.data ); 208 | chat.broadcast( 'response', ... ); 209 | }); 210 | ``` 211 | 212 | Namespaces also attach themselves to the `app` instance, throwing an error if the property name already exists. 213 | 214 | ```js 215 | const app = new Koa(); 216 | const chat = new IO({ 217 | namespace: 'chat' 218 | }); 219 | 220 | chat.attach( app ); 221 | 222 | app.chat.use( ... ); 223 | app.chat.on( ... ); 224 | app.chat.broadcast( ... ); 225 | ``` 226 | 227 | The attachment is configurable if you don’t want to muddy the `app` object with all your namespaces. 228 | 229 | ```js 230 | const chat = new IO({ 231 | namespace: 'chat', 232 | hidden: true 233 | }); 234 | 235 | chat.use( ... ); 236 | chat.on( ... ); 237 | ``` 238 | 239 | 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`. 240 | 241 | ```js 242 | const chat = new IO( 'chat' ); 243 | ``` 244 | 245 | 246 | ## IO API 247 | 248 | ### .attach( `Koa app` ) 249 | 250 | Attaches to a koa application 251 | 252 | ```js 253 | io.attach( app ); 254 | app.listen( process.env.PORT ); 255 | ``` 256 | 257 | ### .use( `Function callback` ) 258 | 259 | Applies middleware to the stack. 260 | 261 | Middleware are executed each time an event is reacted to and before the callback is triggered for an event. 262 | 263 | Middleware with generators should use `co.wrap`. 264 | 265 | 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`. 266 | 267 | 268 | ```js 269 | io.use( async ( ctx, next ) { 270 | console.log( 'Upstream' ); 271 | await next(); 272 | console.log( 'Downstream' ); 273 | }) 274 | ``` 275 | 276 | ### .on( `String event`, `Function callback` ) 277 | 278 | Attaches a callback to an event. 279 | 280 | 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. 281 | 282 | ```js 283 | io.on( 'message', ( ctx, data ) => { 284 | console.log( data ); 285 | console.log( ctx.data, data ); 286 | }); 287 | ``` 288 | 289 | ### .off( `String event`, `Function callback` ) 290 | 291 | Removes a callback from an event. 292 | 293 | If the `event` is omitted then it will remove all listeners from the instance. 294 | 295 | If the `callback` is omitted then all callbacks for the supplied event will be removed. 296 | 297 | ```js 298 | io.off( 'message', onChat ); 299 | io.off( 'message' ); 300 | io.off(); 301 | ``` 302 | 303 | ### .broadcast.emit( `String event`, `data` ) 304 | 305 | Sends a message to all connections. 306 | 307 | 308 | ### .to( `String room` ).emit( `String event`, `data` ) 309 | 310 | Sends data to all connections in a room. 311 | 312 | ```js 313 | io.to( 'some_room' ).emit( 'message', { hello: 'world' } ); 314 | ``` 315 | 316 | 317 | ### .adapter( `Object adapter` ) 318 | 319 | ```js 320 | const redis = require('socket.io-redis'); 321 | io.adapter(redis({ host: 'localhost', port: 6379 })); 322 | ``` 323 | 324 | 325 | ## Socket Connection API 326 | 327 | ### .rooms 328 | 329 | A list of rooms that this connection is associated with. 330 | 331 | ```js 332 | io.on( 'message', ( ctx, data ) => { 333 | console.log(ctx.socket.rooms); 334 | }); 335 | ``` 336 | 337 | 338 | ### .join( `String room` ) 339 | 340 | Associates the connection with a room. 341 | 342 | ```js 343 | io.on( 'message', ( ctx, data ) => { 344 | ctx.socket.join('some_room'); 345 | }); 346 | ``` 347 | 348 | 349 | ### .leave( `String room` ) 350 | 351 | Disassociates the connection with a room. 352 | 353 | ```js 354 | io.on( 'message', ( ctx, data ) => { 355 | ctx.socket.leave( 'some_room' ); 356 | }); 357 | ``` 358 | 359 | 360 | ### .broadcast.emit( `String event`, `data` ) 361 | 362 | Sends a message to all active connections except the current connection. 363 | 364 | ```js 365 | io.on( 'message', ( ctx, data ) => { 366 | ctx.socket.broadcast.emit( 'message', { hello: 'world' } ); 367 | }); 368 | ``` 369 | 370 | 371 | ### .broadcast.to(`String room`).emit( `String event`, `data` ) 372 | 373 | Sends a message to all active connections in a room except the current connection. 374 | 375 | ```js 376 | io.on( 'message', ( ctx, data ) => { 377 | ctx.socket.broadcast.to('some_room').emit( 'message', { hello: 'world' } ); 378 | }); 379 | ``` 380 | 381 | 382 | ### .volatile.emit( `String event`, `data` ) 383 | 384 | Sends a message without ensuring delivery. 385 | 386 | ```js 387 | io.on( 'message', ( ctx, data ) => { 388 | ctx.socket.volatile.emit( 'message', { hello: 'world' } ); 389 | }); 390 | ``` 391 | 392 | 393 | ### .compress(true).emit( `String event`, `data` ) 394 | 395 | Activates per-message compression. 396 | 397 | ```js 398 | io.on( 'message', ( ctx, data ) => { 399 | ctx.socket.compress(true).emit( 'message', { hello: 'world' } ); 400 | }); 401 | ``` 402 | 403 | 404 | ## Running tests 405 | 406 | ```sh 407 | npm test 408 | ``` 409 | 410 | 411 | ## Maintainer/Contributor 412 | 413 | - [Aaron Belovsky](https://github.com/ambelovsky) 414 | - [Jonas Lieb](https://github.com/jojonas) 415 | 416 | 417 | ## Original Author 418 | 419 | - [Matt Styles](https://github.com/mattstyles) 420 | 421 | 422 | ## License 423 | 424 | MIT 425 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 8 | const app = new Koa(); 9 | const io = new IO(); 10 | const chat = new IO( 'chat' ); 11 | 12 | io.attach( app ); 13 | chat.attach( app ); 14 | 15 | /** 16 | * Koa Middlewares 17 | */ 18 | app.use( async ( ctx, next ) => { 19 | const start = new Date; 20 | await next(); 21 | const ms = new Date - start; 22 | console.log( `${ ctx.method } ${ ctx.url } - ${ ms }ms` ); 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 | * Socket middlewares 35 | */ 36 | io.use( async ( ctx, next ) => { 37 | console.log( 'Socket middleware' ); 38 | const start = new Date; 39 | await next(); 40 | const ms = new Date - start; 41 | console.log( `WS ${ ms }ms` ); 42 | }); 43 | 44 | io.use( async ( ctx, next ) => { 45 | ctx.teststring = 'test'; 46 | await next(); 47 | }); 48 | 49 | /** 50 | * Socket handlers 51 | */ 52 | io.on( 'connection', ctx => { 53 | console.log( 'Join event', ctx.id ); 54 | io.broadcast( 'connections', { 55 | numConnections: io.connections.size 56 | }); 57 | 58 | ctx.on( 'disconnect', () => { 59 | console.log( 'leave event', ctx.id ); 60 | io.broadcast( 'connections', { 61 | numConnections: io.connections.size 62 | }); 63 | }); 64 | }); 65 | 66 | io.on( 'data', ( ctx, data ) => { 67 | console.log( 'data event', data ); 68 | console.log( 'ctx:', ctx.event, ctx.data, ctx.id ); 69 | console.log( 'ctx.teststring:', ctx.teststring ); 70 | ctx.socket.emit( 'response', { 71 | message: 'response from server' 72 | }); 73 | }); 74 | 75 | io.on( 'ack', ( ctx, data ) => { 76 | console.log( 'data event with acknowledgement', data ); 77 | ctx.acknowledge( 'received' ); 78 | }); 79 | 80 | io.on( 'numConnections', packet => { 81 | console.log( `Number of connections: ${ io.connections.size }` ); 82 | }); 83 | 84 | /** 85 | * Chat handlers 86 | */ 87 | chat.on( 'connection', ctx => { 88 | console.log( 'Joining chat namespace', ctx.id ); 89 | }); 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.emit( 'message', 'ok connections:chat:broadcast' ); 99 | 100 | // Emits to just this socket 101 | ctx.socket.emit( 'message', 'ok connections:chat:emit' ); 102 | }); 103 | 104 | chat.use( async ( ctx, next ) => { 105 | ctx.teststring = 'chattest'; 106 | console.log(`ctx.socket =>`) 107 | console.dir(ctx.socket, {colors:true, depth:2}) 108 | console.log(`ctx.socket.nsp =>`, ctx.socket.nsp) 109 | await next(); 110 | }); 111 | 112 | const PORT = 3000; 113 | app.listen( 3000, () => { 114 | console.log( `Listening on ${ PORT }` ); 115 | } ); 116 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | "use strict"; 3 | 4 | const socketIO = require( 'socket.io' ); 5 | const compose = require( 'koa-compose' ); 6 | 7 | /** 8 | * Main IO class that handles the socket.io connections 9 | * @class 10 | */ 11 | module.exports = class IO { 12 | /** 13 | * @constructs 14 | * @param namespace namespace identifier 15 | */ 16 | constructor( opts ) { 17 | if ( opts && !(typeof opts !== 'string' || opts && typeof opts !== 'object' ) ) { 18 | throw new Error( 'Incorrect argument passed to koaSocket constructor' ); 19 | } 20 | 21 | // app._io reference 22 | this._io = null; 23 | 24 | /** 25 | * List of middlewares, these are composed into an execution chain and 26 | * evaluated with each event 27 | * @type 28 | */ 29 | this.middleware = []; 30 | 31 | /** 32 | * Composed middleware stack 33 | * @type 34 | */ 35 | this.composed = null; 36 | 37 | /** 38 | * All of the listeners currently added to the IO instance 39 | * event:callback 40 | * @type 41 | */ 42 | this.listeners = new Map(); 43 | 44 | /** 45 | * All active connections 46 | * id:Socket 47 | * @type 48 | */ 49 | this.connections = new Map(); 50 | 51 | /** 52 | * Configuration options 53 | * @type 54 | */ 55 | if ( typeof opts === 'string' ) { 56 | opts = { 57 | namespace: opts 58 | }; 59 | } 60 | this.opts = Object.assign({ 61 | /** 62 | * Namespace id 63 | * @type 64 | * @default null 65 | */ 66 | namespace: null, 67 | 68 | /** 69 | * Hidden instances do not append to the koa app, but still require attachment 70 | * @type 71 | * @default false 72 | */ 73 | hidden: false, 74 | 75 | /** 76 | * Options to pass when instantiating socket.io 77 | * @type 78 | * @default {} 79 | */ 80 | ioOptions: {} 81 | }, opts ); 82 | 83 | /** 84 | * Holds the socketIO connection 85 | * @type 86 | */ 87 | this.socket = null; 88 | 89 | // Bind handlers 90 | this.onConnection = this.onConnection.bind( this ); 91 | this.onDisconnect = this.onDisconnect.bind( this ); 92 | } 93 | 94 | /** 95 | * Attach to a koa application 96 | * @param app the koa app to use 97 | * @param https whether to activate HTTPS 98 | */ 99 | attach( app, https, opts ) { 100 | let http = https ? require('https') : require('http'); 101 | 102 | if ( app.server && app.server.constructor.name != 'Server' ) { 103 | throw new Error( 'app.server already exists but it\'s not an http server' ); 104 | } 105 | 106 | if ( !app.server ) { 107 | // Create a server if it doesn't already exists 108 | app.server = https ? http.createServer(opts || {}, app.callback()) : http.createServer(app.callback()); 109 | 110 | // Patch `app.listen()` to call `app.server.listen()` 111 | app.listen = function listen(){ 112 | app.server.listen.apply( app.server, arguments ); 113 | return app.server; 114 | } 115 | } 116 | 117 | if ( app._io ) { 118 | // Without a namespace we’ll use the default, but .io already exists meaning 119 | // the default is taken already 120 | if ( !this.opts.namespace ) { 121 | throw new Error( 'Socket failed to initialise::Instance may already exist' ); 122 | } 123 | 124 | this.attachNamespace( app, this.opts.namespace ); 125 | return; 126 | } 127 | 128 | if ( this.opts.hidden && !this.opts.namespace ) { 129 | throw new Error( 'Default namespace can not be hidden' ); 130 | } 131 | 132 | app._io = socketIO( app.server, this.opts.ioOptions ); 133 | 134 | if ( this.opts.namespace ) { 135 | this.attachNamespace( app, this.opts.namespace ); 136 | return; 137 | } 138 | 139 | // Local aliases / passthrough socket.io functionality 140 | this.adapter = app._io.adapter.bind(app._io); 141 | 142 | // Attach default namespace 143 | app.io = this; 144 | 145 | // If there is no namespace then connect using the default 146 | this.socket = app._io; 147 | this.socket.on( 'connection', this.onConnection ); 148 | } 149 | 150 | /** 151 | * Attaches the namespace to the server 152 | * @param app the koa app to use 153 | * @param id namespace identifier 154 | */ 155 | attachNamespace( app, id ) { 156 | if ( !app._io ) { 157 | throw new Error( 'Namespaces can only be attached once a socketIO instance has been attached' ); 158 | } 159 | 160 | this.socket = app._io.of( id ); 161 | this.socket.on( 'connection', this.onConnection ); 162 | 163 | if ( this.opts.hidden ) { 164 | return; 165 | } 166 | 167 | if ( app[ id ] ) { 168 | throw new Error( 'Namespace ' + id + ' already attached to koa instance' ); 169 | } 170 | 171 | app[ id ] = this; 172 | } 173 | 174 | /** 175 | * Pushes a middleware on to the stack 176 | * @param fn the middleware function to execute 177 | */ 178 | use( fn ) { 179 | this.middleware.push( fn ); 180 | this.composed = compose( this.middleware ); 181 | 182 | this.updateConnections(); 183 | 184 | return this; 185 | } 186 | 187 | /** 188 | * Adds a new listener to the stack 189 | * @param event the event id 190 | * @param handler the callback to execute 191 | * @return this 192 | */ 193 | on( event, handler ) { 194 | if(['connect', 'connection'].includes(event)) { 195 | this.socket.on(event, handler); 196 | return this; 197 | } 198 | 199 | let listeners = this.listeners.get( event ); 200 | 201 | // If this is a new event then just set it 202 | if ( !listeners ) { 203 | this.listeners.set( event, [ handler ] ); 204 | this.updateConnections(); 205 | return this; 206 | } 207 | 208 | listeners.push( handler ) 209 | this.listeners.set( event, listeners ); 210 | this.updateConnections(); 211 | return this; 212 | } 213 | 214 | /** 215 | * Removes a listener from the event 216 | * @param event if omitted will remove all listeners 217 | * @param handler if omitted will remove all from the event 218 | * @return this 219 | */ 220 | off( event, handler ) { 221 | if ( !event ) { 222 | this.listeners = new Map(); 223 | this.updateConnections(); 224 | return this; 225 | } 226 | 227 | if ( !handler ) { 228 | this.listeners.delete( event ); 229 | this.updateConnections(); 230 | return this; 231 | } 232 | 233 | let listeners = this.listeners.get( event ); 234 | let i = listeners.length - 1; 235 | while( i ) { 236 | if ( listeners[ i ] === handler ) { 237 | break; 238 | } 239 | i--; 240 | } 241 | listeners.splice( i, 1 ); 242 | 243 | this.updateConnections(); 244 | return this; 245 | } 246 | 247 | /** 248 | * Broadcasts an event to all connections 249 | * @param event 250 | * @param data 251 | */ 252 | broadcast( event, data ) { 253 | this.connections.forEach( ( socket, id ) => socket.emit( event, data ) ); 254 | } 255 | 256 | /** 257 | * Perform an action on a room 258 | * @param room 259 | * @return socket 260 | */ 261 | to( room ) { 262 | return this.socket.to(room); 263 | } 264 | 265 | /** 266 | * Triggered for each new connection 267 | * Creates a new Socket instance and adds that to the stack and sets up the 268 | * disconnect event 269 | * @param sock 270 | * @private 271 | */ 272 | onConnection( sock ) { 273 | /** 274 | * Adds a specific event and callback to this socket 275 | * @param event 276 | * @param data 277 | */ 278 | sock._on = ( event, handler ) => sock.on( event, ( data, cb ) => { 279 | let packet = { 280 | event: event, 281 | data: data, 282 | socket: sock, 283 | acknowledge: cb 284 | }; 285 | 286 | if ( !this.composed ) { 287 | handler( packet, data ); 288 | return; 289 | } 290 | 291 | this.composed( packet, () => 292 | handler( packet, data ) 293 | ); 294 | }); 295 | 296 | /** 297 | * Registers the new list of listeners and middleware composition 298 | * @param listeners map of events and callbacks 299 | * @param middleware the composed middleware 300 | */ 301 | sock.update = ( listeners ) => { 302 | sock.removeAllListeners(); 303 | 304 | listeners.forEach( ( handlers, event ) => { 305 | if ( event === 'connection' ) { 306 | return; 307 | } 308 | 309 | handlers.forEach( handler => sock._on( event, handler ) ); 310 | }) 311 | }; 312 | 313 | // Append listeners and composed middleware function 314 | sock.update( this.listeners ); 315 | 316 | this.connections.set( sock.id, sock ); 317 | sock.on( 'disconnect', () => this.onDisconnect( sock ) ); 318 | 319 | // Trigger the connection event if attached to the socket listener map 320 | let handlers = this.listeners.get( 'connection' ); 321 | if ( handlers ) { 322 | handlers.forEach( handler => handler({ 323 | event: 'connection', 324 | data: sock, 325 | socket: sock 326 | }, sock.id ) ); 327 | } 328 | } 329 | 330 | /** 331 | * Fired when the socket disconnects, simply reflects stack in the connections 332 | * stack 333 | * @param sock 334 | * @private 335 | */ 336 | onDisconnect( sock ) { 337 | this.connections.delete( sock.id ); 338 | } 339 | 340 | /** 341 | * Updates all existing connections with current listeners and middleware 342 | * @private 343 | */ 344 | updateConnections() { 345 | this.connections.forEach( connection => connection.update( this.listeners, this.composed ) ); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-socket-2", 3 | "version": "2.0.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": "nyc npm run test", 9 | "travis": "npm run cover && nyc report --reporter=text-lcov | coveralls", 10 | "example": "node example/server" 11 | }, 12 | "keywords": [ 13 | "koa", 14 | "koa v2", 15 | "koa2", 16 | "koa version 2", 17 | "socket.io", 18 | "socket.io 2", 19 | "socket io", 20 | "socket io 2", 21 | "web sockets", 22 | "websockets", 23 | "websocket", 24 | "web socket" 25 | ], 26 | "engines": { 27 | "node": ">= 4" 28 | }, 29 | "repository": "ambelovsky/koa-socket-2", 30 | "author": "Aaron Belovsky (Originally: Matt Styles)", 31 | "license": "MIT", 32 | "dependencies": { 33 | "koa-compose": "^4.1.0", 34 | "socket.io": "^3.0.2" 35 | }, 36 | "devDependencies": { 37 | "coveralls": "^3.0.0", 38 | "glob": "^7.1.6", 39 | "koa": "^2.7.0", 40 | "minimist": "^1.2.0", 41 | "nyc": "^15.1.0", 42 | "socket.io-client": "^3.0.2", 43 | "tape": "^4.6.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /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 | tape( 'Attaching a namespace should be done via an options object', t => { 107 | t.plan( 2 ); 108 | 109 | const app = new Koa(); 110 | const chat = new IO({ 111 | namespace: 'chat' 112 | }); 113 | 114 | t.doesNotThrow( () => { 115 | chat.attach( app ); 116 | t.ok( app.chat, 'the chat namespace has been attached to the app' ); 117 | }, null, 'Attaching only a namespace via options hash is fine' ); 118 | }); 119 | 120 | tape( 'Attaching a namespace will attach the IO class', t => { 121 | t.plan( 2 ); 122 | 123 | const app = new Koa(); 124 | const chat = new IO( 'chat' ); 125 | 126 | chat.attach( app ); 127 | t.ok( app.chat, 'the chat namespace has been attached to the app' ); 128 | t.ok( app.chat instanceof IO, 'an IO instance has been attached' ); 129 | }); 130 | 131 | tape( 'Namespaces can be hidden from the app object', t => { 132 | t.plan( 2 ); 133 | 134 | const app = new Koa(); 135 | const chat = new IO({ 136 | namespace: 'chat', 137 | hidden: true 138 | }); 139 | 140 | chat.attach( app ); 141 | 142 | const srv = app.server.listen(); 143 | const client = ioc( 'ws://localhost:' + srv.address().port + '/chat', { 144 | transports: [ 'websocket' ] 145 | }); 146 | 147 | client.on( 'disconnect', () => { 148 | srv.close(); 149 | }); 150 | client.on( 'connect', () => { 151 | client.disconnect(); 152 | }); 153 | 154 | chat.on( 'connection', ctx => { 155 | t.notOk( app.chat, 'chat should exist but not be available on the app' ); 156 | t.ok( true, 'Client can connect to the chat namespace even though it is not available on the app' ); 157 | }); 158 | }); 159 | 160 | tape( 'The default namespace can not be hidden, app.io must be attached to app', t => { 161 | t.plan( 1 ); 162 | 163 | const app = new Koa(); 164 | const io = new IO({ 165 | hidden: true 166 | }); 167 | 168 | t.throws( () => { 169 | io.attach( app ); 170 | }, null, 'Attaching a hidden default instance will throw' ); 171 | }); 172 | 173 | tape( 'Calling app.listen calls app.server.listen', t => { 174 | t.plan( 2 ); 175 | 176 | const app = new Koa(); 177 | const io = new IO(); 178 | 179 | io.attach( app ); 180 | 181 | app.server.listen = function() { 182 | t.pass( 'Calling app.listen called app.server.listen' ); 183 | }; 184 | 185 | t.doesNotThrow( () => { 186 | var srv = app.listen( () => { 187 | srv.close(); 188 | }) 189 | }, 'Calling app.listen does not throw' ); 190 | }); 191 | -------------------------------------------------------------------------------- /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.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 io = new IO(); 90 | const app = application( io ); 91 | 92 | app._io.on( 'connection', sock => { 93 | t.equal( io.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( io.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 = io.connections.get( client.id ); 107 | sock.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.disconnect(); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /spec/handlers.test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const tape = require( 'tape' ); 5 | const IO = require( '../' ); 6 | 7 | const application = require( './helpers/utils' ).application; 8 | const connection = require( './helpers/utils' ).connection; 9 | 10 | 11 | tape( 'An event handler can be associated with an event', t => { 12 | t.plan( 1 ); 13 | 14 | const io = new IO(); 15 | const app = application( io ); 16 | const client = connection( app.server ); 17 | 18 | io.on( 'req', ctx => { 19 | t.pass( 'The event handler has been triggered' ); 20 | client.disconnect(); 21 | }); 22 | 23 | client.emit( 'req' ); 24 | }); 25 | 26 | tape( 'Multiple events can be set listening', t => { 27 | t.plan( 1 ); 28 | 29 | const io = new IO(); 30 | const app = application( io ); 31 | const client = connection( app.server ); 32 | 33 | var count = 0; 34 | 35 | io.on( 'req', ctx => { 36 | count++; 37 | }); 38 | io.on( 'req2', ctx => { 39 | count++; 40 | t.equal( count, 2, 'Both events were triggered' ); 41 | client.disconnect(); 42 | }); 43 | 44 | client.emit( 'req' ); 45 | client.emit( 'req2' ); 46 | }); 47 | 48 | tape( 'Multiple handlers can be connected to an event', t => { 49 | t.plan( 1 ); 50 | 51 | const io = new IO(); 52 | const app = application( io ); 53 | const client = connection( app.server ); 54 | 55 | var count = 0; 56 | 57 | io.on( 'req', ctx => { 58 | // First handler 59 | count++; 60 | }); 61 | io.on( 'req', ctx => { 62 | // Second handler 63 | count++; 64 | }); 65 | io.on( 'end', ctx => { 66 | t.equal( count, 2, 'Both handlers should have been triggered' ); 67 | client.disconnect(); 68 | }); 69 | 70 | client.emit( 'req' ); 71 | client.emit( 'end' ); 72 | }); 73 | 74 | tape( 'A handler can be removed', t => { 75 | t.plan( 1 ); 76 | 77 | const io = new IO(); 78 | const app = application( io ); 79 | const client = connection( app.server ); 80 | 81 | var count = 0; 82 | 83 | function add() { 84 | count++; 85 | } 86 | 87 | io.on( 'req', add ); 88 | client.emit( 'req' ); 89 | 90 | setTimeout( () => { 91 | io.off( 'req', add ); 92 | client.emit( 'req' ); 93 | 94 | setTimeout( () => { 95 | t.equal( count, 1, 'Add function is called only once' ); 96 | client.disconnect(); 97 | }, 500 ); 98 | }, 500 ); 99 | }); 100 | 101 | tape( 'A handler can be removed from a multiple handler event', t => { 102 | t.plan( 2 ); 103 | 104 | const io = new IO(); 105 | const app = application( io ); 106 | const client = connection( app.server ); 107 | 108 | var count = 0; 109 | 110 | function add() { 111 | count++; 112 | } 113 | function plus() { 114 | count++; 115 | } 116 | 117 | io.on( 'req', add ); 118 | io.on( 'req', plus ); 119 | client.emit( 'req' ); 120 | 121 | setTimeout( () => { 122 | t.equal( count, 2, 'Both handlers should have been called' ); 123 | io.off( 'req', add ); 124 | client.emit( 'req' ); 125 | 126 | setTimeout( () => { 127 | t.equal( count, 3, 'After removal only one handler will have been triggered' ); 128 | client.disconnect(); 129 | }, 500 ); 130 | }, 500 ); 131 | }); 132 | 133 | tape( 'A specific handler can be removed from an event - front', t => { 134 | t.plan( 2 ); 135 | 136 | const io = new IO(); 137 | const app = application( io ); 138 | const client = connection( app.server ); 139 | 140 | var count1 = 0; 141 | var count2 = 0; 142 | 143 | function add() { 144 | count1++; 145 | } 146 | function plus() { 147 | count2++; 148 | } 149 | 150 | io.on( 'req', add ); 151 | io.on( 'req', plus ); 152 | client.emit( 'req' ); 153 | 154 | setTimeout( () => { 155 | t.ok( count1 === 1 && count2 === 1, 'Both handlers should have been called' ); 156 | io.off( 'req', add ); 157 | client.emit( 'req' ); 158 | 159 | setTimeout( () => { 160 | t.ok( count1 === 1 && count2 === 2, 'A specific handler has been removed from the start of the list' ); 161 | client.disconnect(); 162 | }, 500 ); 163 | }, 500 ); 164 | }); 165 | 166 | tape( 'A specific handler can be removed from an event - last', t => { 167 | t.plan( 2 ); 168 | 169 | const io = new IO(); 170 | const app = application( io ); 171 | const client = connection( app.server ); 172 | 173 | var count1 = 0; 174 | var count2 = 0; 175 | 176 | function add() { 177 | count1++; 178 | } 179 | function plus() { 180 | count2++; 181 | } 182 | 183 | io.on( 'req', add ); 184 | io.on( 'req', plus ); 185 | client.emit( 'req' ); 186 | 187 | setTimeout( () => { 188 | t.ok( count1 === 1 && count2 === 1, 'Both handlers should have been called' ); 189 | io.off( 'req', plus ); 190 | client.emit( 'req' ); 191 | 192 | setTimeout( () => { 193 | t.ok( count1 === 2 && count2 === 1, 'A specific handler has been removed from the end of the list' ); 194 | client.disconnect(); 195 | }, 500 ); 196 | }, 500 ); 197 | }); 198 | 199 | 200 | tape( 'All handlers can be removed from an event', t => { 201 | t.plan( 2 ); 202 | 203 | const io = new IO(); 204 | const app = application( io ); 205 | const client = connection( app.server ); 206 | 207 | var count = 0; 208 | 209 | function add() { 210 | count++; 211 | } 212 | function plus() { 213 | count++; 214 | } 215 | 216 | io.on( 'req', add ); 217 | io.on( 'req', plus ); 218 | client.emit( 'req' ); 219 | 220 | setTimeout( () => { 221 | t.equal( count, 2, 'Both handlers should have been called' ); 222 | io.off( 'req' ); 223 | client.emit( 'req' ); 224 | 225 | setTimeout( () => { 226 | t.equal( count, 2, 'All handlers have been removed from the event' ); 227 | client.disconnect(); 228 | }, 500 ); 229 | }, 500 ); 230 | }); 231 | 232 | tape( 'All handlers can be removed from a socket instance', t => { 233 | t.plan( 2 ); 234 | 235 | const io = new IO(); 236 | const app = application( io ); 237 | const client = connection( app.server ); 238 | 239 | var count = 0; 240 | 241 | function add() { 242 | count++; 243 | } 244 | function plus() { 245 | count++; 246 | } 247 | 248 | io.on( 'req1', add ); 249 | io.on( 'req2', plus ); 250 | client.emit( 'req1' ); 251 | client.emit( 'req2' ); 252 | 253 | setTimeout( () => { 254 | t.equal( count, 2, 'Both handlers should have been called' ); 255 | io.off(); 256 | client.emit( 'req1' ); 257 | client.emit( 'req2' ); 258 | 259 | setTimeout( () => { 260 | t.equal( count, 2, 'All handlers have been removed from the event' ); 261 | client.disconnect(); 262 | }, 500 ); 263 | }, 500 ); 264 | }); 265 | 266 | tape( 'Middleware is run before listeners', t => { 267 | t.plan( 1 ); 268 | 269 | const io = new IO(); 270 | const app = application( io ); 271 | const client = connection( app.server ); 272 | 273 | var count = 0; 274 | 275 | io.use( async ( ctx, next ) => { 276 | count++; 277 | await next(); 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( async ( ctx, next ) => { 295 | ctx.foo = true 296 | await next() 297 | }); 298 | io.on( 'req', ctx => { 299 | t.ok( ctx.foo, 'Context can be manipulated' ) 300 | client.disconnect() 301 | }); 302 | 303 | client.emit( 'req' ); 304 | }); 305 | 306 | tape( 'Middleware can be traversed', t => { 307 | t.plan( 2 ); 308 | 309 | const io = new IO(); 310 | const app = application( io ); 311 | const client = connection( app.server ); 312 | 313 | io.use( async ( ctx, next ) => { 314 | ctx.count = 0; 315 | await next(); 316 | t.equal( ctx.count, 1, 'Downstream middleware manipulated the context' ); 317 | ctx.count++; 318 | }); 319 | io.use( async ( ctx, next ) => { 320 | ctx.count++; 321 | await next(); 322 | }); 323 | io.on( 'req', ctx => { 324 | t.equal( ctx.count, 1, 'Middleware upstream and downstream have executed' ); 325 | client.disconnect(); 326 | }); 327 | 328 | client.emit( 'req' ); 329 | }); 330 | -------------------------------------------------------------------------------- /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 | 21 | return client; 22 | }; 23 | 24 | exports.application = function( instance ) { 25 | const app = new Koa(); 26 | const io = instance || new IO(); 27 | io.attach( app ); 28 | return app; 29 | }; 30 | -------------------------------------------------------------------------------- /spec/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const minimist = require('minimist'); 3 | const glob = require('glob'); 4 | 5 | const argv = minimist( process.argv.slice( 2 ) ); 6 | 7 | argv._.forEach( file => { 8 | glob(`./${file}`, function (er, files) { 9 | for(let i in files) { 10 | require(path.resolve(files[i])); 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /spec/stack.test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const tape = require( 'tape' ); 5 | const IO = require( '../' ); 6 | 7 | const application = require( './helpers/utils' ).application; 8 | const connection = require( './helpers/utils' ).connection; 9 | 10 | 11 | tape( 'Listeners can be added during runtime to connected clients', t => { 12 | t.plan( 2 ); 13 | 14 | const io = new IO(); 15 | const app = application( io ); 16 | 17 | const client = connection( app.server ); 18 | 19 | client.on( 'connect', () => { 20 | var called = false; 21 | client.on( 'response', ctx => { 22 | called = true 23 | }); 24 | 25 | client.emit( 'request' ); 26 | 27 | // Wait for a response and see if called turns true 28 | setTimeout( () => { 29 | t.notOk( called, 'Called should remain false' ); 30 | 31 | io.on( 'request', ctx => { 32 | ctx.socket.emit( 'response' ); 33 | }); 34 | 35 | client.emit( 'request' ); 36 | 37 | setTimeout( () => { 38 | t.ok( called, 'IO should now respond to the event and called should be true' ); 39 | client.disconnect(); 40 | }, 500 ); 41 | }, 500 ); 42 | }); 43 | }); 44 | 45 | tape( 'Middleware can be added during runtime to connected clients', t => { 46 | t.plan( 2 ); 47 | 48 | const io = new IO(); 49 | const app = application( io ); 50 | 51 | const client = connection( app.server ); 52 | 53 | io.on( 'req1', ctx => { 54 | ctx.socket.emit( 'res1', ctx.foo ); 55 | }); 56 | io.on( 'req2', ctx => { 57 | ctx.socket.emit( 'res2', ctx.foo ); 58 | }); 59 | 60 | client.on( 'connect', () => { 61 | client.on( 'res1', data => { 62 | t.notOk( data, 'Middleware did not fire and attach additional prop' ); 63 | 64 | io.use( async ( ctx, next ) => { 65 | ctx.foo = 'foo' 66 | await next() 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 | --------------------------------------------------------------------------------