From 4477c0f0ab050050a20d8f040ad4e2a84dfd7be4 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 17 Mar 2019 10:06:02 -0700 Subject: [PATCH] feat(socketio): add support for Socket.IO to expose WebSocket endpoints --- CODEOWNERS | 1 + docs/apidocs.html | 1 + docs/site/MONOREPO.md | 1 + greenkeeper.json | 4 +- packages/socketio/LICENSE | 25 + packages/socketio/README.md | 66 ++ packages/socketio/docs.json | 7 + packages/socketio/index.d.ts | 6 + packages/socketio/index.js | 6 + packages/socketio/index.ts | 8 + packages/socketio/package-lock.json | 862 ++++++++++++++++++ packages/socketio/package.json | 59 ++ .../src/__tests__/acceptance/application.ts | 51 ++ .../__tests__/acceptance/controllers/index.ts | 6 + .../controllers/socketio.controller.ts | 60 ++ .../acceptance/socketio.server.acceptance.ts | 40 + .../src/decorators/socketio.decorator.ts | 95 ++ packages/socketio/src/index.ts | 9 + packages/socketio/src/keys.ts | 17 + .../src/socketio-controller-factory.ts | 101 ++ packages/socketio/src/socketio.server.ts | 213 +++++ packages/socketio/tsconfig.build.json | 8 + 22 files changed, 1644 insertions(+), 2 deletions(-) create mode 100644 packages/socketio/LICENSE create mode 100644 packages/socketio/README.md create mode 100644 packages/socketio/docs.json create mode 100644 packages/socketio/index.d.ts create mode 100644 packages/socketio/index.js create mode 100644 packages/socketio/index.ts create mode 100644 packages/socketio/package-lock.json create mode 100644 packages/socketio/package.json create mode 100644 packages/socketio/src/__tests__/acceptance/application.ts create mode 100644 packages/socketio/src/__tests__/acceptance/controllers/index.ts create mode 100644 packages/socketio/src/__tests__/acceptance/controllers/socketio.controller.ts create mode 100644 packages/socketio/src/__tests__/acceptance/socketio.server.acceptance.ts create mode 100644 packages/socketio/src/decorators/socketio.decorator.ts create mode 100644 packages/socketio/src/index.ts create mode 100644 packages/socketio/src/keys.ts create mode 100644 packages/socketio/src/socketio-controller-factory.ts create mode 100644 packages/socketio/src/socketio.server.ts create mode 100644 packages/socketio/tsconfig.build.json diff --git a/CODEOWNERS b/CODEOWNERS index 661b10cd4dd2..aebfa74bff68 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -23,6 +23,7 @@ packages/repository/* @raymondfeng packages/repository-json-schema/* @bajtos packages/rest/* @bajtos @raymondfeng packages/service-proxy/* @raymondfeng +packages/socketio/* @raymondfeng packages/testlab/* @bajtos examples/todo/* @bajtos @hacksparrow examples/express-composition/* @nabdelgadir diff --git a/docs/apidocs.html b/docs/apidocs.html index f728cf62ad00..84adfe3f9785 100644 --- a/docs/apidocs.html +++ b/docs/apidocs.html @@ -32,6 +32,7 @@

List of packages

  • @loopback/repository-json-schema
  • @loopback/rest
  • @loopback/service-proxy
  • +
  • @loopback/socketio
  • @loopback/testlab
  • diff --git a/docs/site/MONOREPO.md b/docs/site/MONOREPO.md index 13c6536f6fef..1e66d888b37d 100644 --- a/docs/site/MONOREPO.md +++ b/docs/site/MONOREPO.md @@ -33,6 +33,7 @@ The [loopback-next](https://github.com/strongloop/loopback-next) repository uses | [repository](https://github.com/strongloop/loopback-next/tree/master/packages/repository) | @loopback/repository | Define and implement a common set of interfaces for interacting with databases | | [rest](https://github.com/strongloop/loopback-next/tree/master/packages/rest) | @loopback/rest | Expose controllers as REST endpoints and route REST API requests to controller methods | | [service-proxy](https://github.com/strongloop/loopback-next/tree/master/packages/service-proxy) | @loopback/service-proxy | A common set of interfaces for interacting with service oriented backends such as REST APIs, SOAP Web Services, and gRPC microservices | +| [socketio](https://github.com/strongloop/loopback-next/tree/master/packages/socketio) | @loopback/socketio | Expose controllers as Socket.IO endpoints | | [testlab](https://github.com/strongloop/loopback-next/tree/master/packages/testlab) | @loopback/testlab | A collection of test utilities we use to write LoopBack tests | | [tslint-config](https://github.com/strongloop/loopback-next/tree/master/packages/tslint-config) | @loopback/tslint-config | Shared TSLint config to enforce a consistent code style for LoopBack development | diff --git a/greenkeeper.json b/greenkeeper.json index c8e2197408f2..556e5412e28d 100644 --- a/greenkeeper.json +++ b/greenkeeper.json @@ -31,11 +31,11 @@ "packages/rest-explorer/package.json", "packages/rest/package.json", "packages/service-proxy/package.json", + "packages/socketio/package.json", "packages/testlab/package.json", "packages/tslint-config/package.json", "sandbox/example/package.json" ] } - }, - "ignore": ["@types/node"] + } } diff --git a/packages/socketio/LICENSE b/packages/socketio/LICENSE new file mode 100644 index 000000000000..6f1548e5d805 --- /dev/null +++ b/packages/socketio/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2017,2019. All Rights Reserved. +Node module: @loopback/socketio +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/socketio/README.md b/packages/socketio/README.md new file mode 100644 index 000000000000..f55d72b18cc5 --- /dev/null +++ b/packages/socketio/README.md @@ -0,0 +1,66 @@ +# @loopback/socketio + +This module uses [socket.io](http://socket.io) to expose controllers as +WebSocket friendly endpoints. + +## Use Cases + +1. A real time application would like to use WebSocket friendly APIs to manage + subscriptions, participants (rooms) and message exchanges. + +2. Make it easy for API developers to expose WebSocket friendly APIs without + learning a lot of low level concepts. + +3. Use it as a step stone to explore messaging oriented API paradigms and + programming models, such as pub/sub, eventing, streaming, and reactive. + +## High Level Design + +The package will provide the following key constructs: + +- SocketIOServer: A new server type that listens on incoming WebSocket + connections and dispatches messages to controllers that subscribe to the + namespace. Each server is attached to an http/https endpoint. + +- SocketIO controller: A controller class that is decorated with SocketIO + related metadata, including: + + - Map to a namespace + - Connect/disconnect events + - Subscribe/consume messages + - Publish/produce messages + +- SocketIO middleware or sequence + - Allow common logic to intercept/process WebSocket messages + +## Basic Use + +1. Create a SocketIOServer +2. Define a controller to handle socket.io events/messages +3. Register the controller +4. Discover socket.io controllers and mount them to the SocketIOServer + namespaces. + +## Installation + +```sh +npm install --save @loopback/socketio +``` + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/socketio/docs.json b/packages/socketio/docs.json new file mode 100644 index 000000000000..d2f75808ee7f --- /dev/null +++ b/packages/socketio/docs.json @@ -0,0 +1,7 @@ +{ + "content": [ + "index.ts", + "src/**/*.ts" + ], + "codeSectionDepth": 4 +} diff --git a/packages/socketio/index.d.ts b/packages/socketio/index.d.ts new file mode 100644 index 000000000000..369063c3c4f9 --- /dev/null +++ b/packages/socketio/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/socketio/index.js b/packages/socketio/index.js new file mode 100644 index 000000000000..cb208b17f4a1 --- /dev/null +++ b/packages/socketio/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/socketio/index.ts b/packages/socketio/index.ts new file mode 100644 index 000000000000..0171a2c2a15b --- /dev/null +++ b/packages/socketio/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/socketio/package-lock.json b/packages/socketio/package-lock.json new file mode 100644 index 000000000000..6749f3eb99c9 --- /dev/null +++ b/packages/socketio/package-lock.json @@ -0,0 +1,862 @@ +{ + "name": "@loopback/socketio", + "version": "1.0.0-1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/debug": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.2.tgz", + "integrity": "sha512-jkf6UiWUjcOqdQbatbvOm54/YbCdjt3JjiAzT/9KS2XtMmOkYHdKsI5u8fulhbuTUuiqNBfa6J5GSDiwjK+zLA==", + "dev": true + }, + "@types/express": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", + "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.1.tgz", + "integrity": "sha512-QgbIMRU1EVRry5cIu1ORCQP4flSYqLM1lS5LYyGWfKnFT3E58f0gKto7BR13clBFVrVZ0G0rbLZ1hUpSkgQQOA==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/node": { + "version": "10.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.1.tgz", + "integrity": "sha512-Rymt08vh1GaW4vYB6QP61/5m/CFLGnFZP++bJpWbiNxceNa6RBipDmb413jvtSf/R1gg5a/jQVl2jY4XVRscEA==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/socket.io": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.2.tgz", + "integrity": "sha512-Ind+4qMNfQ62llyB4IMs1D8znMEBsMKohZBPqfBUIXqLQ9bdtWIbNTBWwtdcBWJKnokMZGcmWOOKslatni5vtA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/socket.io-client": { + "version": "1.4.32", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.32.tgz", + "integrity": "sha512-Vs55Kq8F+OWvy1RLA31rT+cAyemzgm0EWNeax6BWF8H7QiiOYMJIdcwSDdm5LVgfEkoepsWkS+40+WNb7BUMbg==", + "dev": true + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", + "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "dev": true, + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "engine.io": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz", + "integrity": "sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w==", + "requires": { + "accepts": "~1.3.4", + "base64id": "1.0.0", + "cookie": "0.3.1", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.0", + "ws": "~6.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "engine.io-client": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz", + "integrity": "sha512-y0CPINnhMvPuwtqXfsGuWE8BB66+B6wTtCofQDRecMQPYX3MYUZXFNKDhdrSe3EVjgOu4V3rxdeqN/Tr91IgbQ==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "~3.1.0", + "engine.io-parser": "~2.1.1", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "engine.io-parser": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", + "integrity": "sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", + "dev": true + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "p-event": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.0.0.tgz", + "integrity": "sha512-5VKoUdeAfIlAXmASmre9vGlKjnwGwbr5Zwd3GZU2KmkaRgxEOeUUS5Oyh0qYMx8Y+0VTvvNjNIlqIvMpI/DKSw==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "dev": true + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dev": true, + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "socket.io": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz", + "integrity": "sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w==", + "requires": { + "debug": "~4.1.0", + "engine.io": "~3.3.1", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.2.0", + "socket.io-parser": "~3.3.0" + } + }, + "socket.io-adapter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", + "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" + }, + "socket.io-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz", + "integrity": "sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "engine.io-client": "~3.3.1", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "dev": true + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/packages/socketio/package.json b/packages/socketio/package.json new file mode 100644 index 000000000000..3306d8f9c8a2 --- /dev/null +++ b/packages/socketio/package.json @@ -0,0 +1,59 @@ +{ + "name": "@loopback/socketio", + "version": "1.0.0-1", + "description": "LoopBack's WebSocket server based on socket.io", + "engines": { + "node": ">=8.9" + }, + "scripts": { + "acceptance": "lb-mocha \"dist/__tests__/acceptance/**/*.js\"", + "build:apidocs": "lb-apidocs", + "build": "lb-tsc es2017 --outDir dist", + "clean": "lb-clean loopback-socketio*.tgz dist package api-docs", + "pretest": "npm run build", + "test": "lb-mocha --allow-console-logs \"dist/__tests__/**/*.js\"", + "unit": "lb-mocha \"dist/__tests__/unit/**/*.js\"", + "verify": "npm pack && tar xf loopback-socketio*.tgz && tree package && npm run clean" + }, + "author": "IBM Corp.", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "debug": "^4.0.1", + "@loopback/context": "^1.1.0", + "@loopback/core": "^1.1.0", + "@loopback/http-server": "^1.0.2", + "socket.io": "^2.1.1" + }, + "devDependencies": { + "@loopback/build": "^1.3.2", + "@loopback/testlab": "^1.1.0", + "@loopback/tslint-config": "^2.0.2", + "@types/debug": "^4.1.0", + "@types/node": "^10.11.2", + "@types/socket.io": "^2.1.0", + "@types/socket.io-client": "^1.4.32", + "p-event": "^4.0.0", + "socket.io-client": "^2.1.1" + }, + "keywords": [ + "LoopBack", + "Socket.IO", + "WebSocket" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist", + "src", + "!*/__tests__" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/socketio/src/__tests__/acceptance/application.ts b/packages/socketio/src/__tests__/acceptance/application.ts new file mode 100644 index 000000000000..23cfc03f09b4 --- /dev/null +++ b/packages/socketio/src/__tests__/acceptance/application.ts @@ -0,0 +1,51 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, ApplicationConfig} from '@loopback/core'; +import {SocketIOServer} from '../..'; +import {SocketIOController} from './controllers'; + +// const debug = debugFactory('loopback:socketio:application'); + +// tslint:disable:no-any + +export class SocketIODemoApplication extends Application { + readonly ioServer: SocketIOServer; + + constructor(options: ApplicationConfig = {}) { + super(options); + + // Create ws server from the http server + const server = new SocketIOServer({httpServerOptions: options.socketio}); + this.bind('servers.socketio.SocketIOServer').to(server); + /* + server.use((socket, next) => { + debug('Global middleware - socket:', socket.id); + next(); + }); + // Add a route + const ns = server.route(SocketIOController, /^\/chats\/.+$/); + ns.use((socket, next) => { + debug( + 'Middleware for namespace %s - socket: %s', + socket.nsp.name, + socket.id, + ); + next(); + }); + */ + server.controller(SocketIOController); + server.discoverAndRegister(); + this.ioServer = server; + } + + start() { + return this.ioServer.start(); + } + + stop() { + return this.ioServer.stop(); + } +} diff --git a/packages/socketio/src/__tests__/acceptance/controllers/index.ts b/packages/socketio/src/__tests__/acceptance/controllers/index.ts new file mode 100644 index 000000000000..47d35e910023 --- /dev/null +++ b/packages/socketio/src/__tests__/acceptance/controllers/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './socketio.controller'; diff --git a/packages/socketio/src/__tests__/acceptance/controllers/socketio.controller.ts b/packages/socketio/src/__tests__/acceptance/controllers/socketio.controller.ts new file mode 100644 index 000000000000..f8464b9aca6a --- /dev/null +++ b/packages/socketio/src/__tests__/acceptance/controllers/socketio.controller.ts @@ -0,0 +1,60 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import * as debugFactory from 'debug'; +import {Socket} from 'socket.io'; +import {socketio} from '../../..'; + +const debug = debugFactory('loopback:socketio:controller'); + +/** + * A demo controller for socket.io + */ +@socketio(/^\/chats\/.+$/) +export class SocketIOController { + constructor( + @socketio.socket() // Equivalent to `@inject('ws.socket')` + private socket: Socket, + ) {} + + /** + * The method is invoked when a client connects to the server + * @param socket + */ + @socketio.connect() + connect(socket: Socket) { + debug('Client connected: %s', this.socket.id); + socket.join('room 1'); + } + + /** + * Register a handler for 'chat message' events + * @param msg + */ + @socketio.subscribe('chat message') + // @socketio.emit('namespace' | 'requestor' | 'broadcast') + handleChatMessage(msg: unknown) { + debug('Chat message: %s', msg); + this.socket.nsp.emit('chat message', `[${this.socket.id}] ${msg}`); + } + + /** + * Register a handler for all events + * @param msg + */ + @socketio.subscribe(/.+/) + logMessage(...args: unknown[]) { + debug('Message: %s', args); + } + + /** + * The method is invoked when a client disconnects from the server + * @param socket + */ + @socketio.disconnect() + disconnect() { + debug('Client disconnected: %s', this.socket.id); + } +} diff --git a/packages/socketio/src/__tests__/acceptance/socketio.server.acceptance.ts b/packages/socketio/src/__tests__/acceptance/socketio.server.acceptance.ts new file mode 100644 index 000000000000..1cb60972f5b0 --- /dev/null +++ b/packages/socketio/src/__tests__/acceptance/socketio.server.acceptance.ts @@ -0,0 +1,40 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import pEvent from 'p-event'; +import * as io from 'socket.io-client'; +import {SocketIODemoApplication} from './application'; + +describe('SocketIOServer', () => { + let app: SocketIODemoApplication; + + before(givenApplication); + + after(async () => { + await app.stop(); + }); + + it('connects to socketio controller', async () => { + const url = app.ioServer.url + '/chats/group1'; + const socket = io(url); + socket.emit('chat message', 'Hello'); + const msg = await pEvent(socket, 'chat message'); + expect(msg).to.match(/\[\/chats\/group1#.+\] Hello/); + await socket.disconnect(); + }); + + async function givenApplication() { + app = new SocketIODemoApplication({ + socketio: { + host: '127.0.0.1', + port: 0, + }, + }); + await app.start(); + + return app; + } +}); diff --git a/packages/socketio/src/decorators/socketio.decorator.ts b/packages/socketio/src/decorators/socketio.decorator.ts new file mode 100644 index 000000000000..61f6cd58dd3b --- /dev/null +++ b/packages/socketio/src/decorators/socketio.decorator.ts @@ -0,0 +1,95 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + ClassDecoratorFactory, + Constructor, + inject, + MetadataAccessor, + MetadataInspector, + MethodDecoratorFactory, +} from '@loopback/context'; + +export interface SocketIOMetadata { + namespace?: string | RegExp; +} + +export const SOCKET_IO_METADATA = MetadataAccessor.create< + SocketIOMetadata, + ClassDecorator +>('socketio'); + +export const SOCKET_IO_SUBSCRIBE_METADATA = MetadataAccessor.create< + (string | RegExp)[], + MethodDecorator +>('socketio:subscribe'); + +export const SOCKET_IO_CONNECT_METADATA = MetadataAccessor.create< + boolean, + MethodDecorator +>('socketio:connect'); + +/** + * Decorate a socketio controller class to specify the namespace + * For example, + * ```ts + * @socketio({namespace: '/chats'}) + * export class SocketIOController {} + * ``` + * @param spec A namespace or object + */ +export function socketio(spec: SocketIOMetadata | string | RegExp = {}) { + if (typeof spec === 'string' || spec instanceof RegExp) { + spec = {namespace: spec}; + } + return ClassDecoratorFactory.createDecorator(SOCKET_IO_METADATA, spec); +} + +export function getSocketIOMetadata(controllerClass: Constructor) { + return MetadataInspector.getClassMetadata( + SOCKET_IO_METADATA, + controllerClass, + ); +} + +export namespace socketio { + export function socket() { + return inject('socketio.socket'); + } + + /** + * Decorate a method to subscribe to socketio events. + * For example, + * ```ts + * @socketio.subscribe('chat message') + * async function onChat(msg: string) { + * } + * ``` + * @param messageTypes + */ + export function subscribe(...messageTypes: (string | RegExp)[]) { + return MethodDecoratorFactory.createDecorator( + SOCKET_IO_SUBSCRIBE_METADATA, + messageTypes, + ); + } + + /** + * Decorate a controller method for `disconnect` + */ + export function disconnect() { + return subscribe('disconnect'); + } + + /** + * Decorate a controller method for `connect` + */ + export function connect() { + return MethodDecoratorFactory.createDecorator( + SOCKET_IO_CONNECT_METADATA, + true, + ); + } +} diff --git a/packages/socketio/src/index.ts b/packages/socketio/src/index.ts new file mode 100644 index 000000000000..6276a27bc6fb --- /dev/null +++ b/packages/socketio/src/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './decorators/socketio.decorator'; +export * from './keys'; +export * from './socketio-controller-factory'; +export * from './socketio.server'; diff --git a/packages/socketio/src/keys.ts b/packages/socketio/src/keys.ts new file mode 100644 index 000000000000..3b582885e94b --- /dev/null +++ b/packages/socketio/src/keys.ts @@ -0,0 +1,17 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey} from '@loopback/core'; +import {SocketIOServerOptions} from './socketio.server'; + +export namespace SocketIOBindings { + export const CONFIG = BindingKey.create( + 'socketio.server.options', + ); +} + +export namespace SocketIOTags { + export const SOCKET_IO = 'socketio'; +} diff --git a/packages/socketio/src/socketio-controller-factory.ts b/packages/socketio/src/socketio-controller-factory.ts new file mode 100644 index 000000000000..15228caa1c0a --- /dev/null +++ b/packages/socketio/src/socketio-controller-factory.ts @@ -0,0 +1,101 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor, invokeMethod, MetadataInspector} from '@loopback/context'; +import {CoreBindings} from '@loopback/core'; +import {SocketIORequestContext} from './socketio.server'; + +export class SocketIOControllerFactory { + private controller: {[method: string]: Function}; + + constructor( + private reqCtx: SocketIORequestContext, + private controllerClass: Constructor, + ) {} + + async create() { + // Instantiate the controller instance + this.controller = await this.reqCtx.get<{[method: string]: Function}>( + CoreBindings.CONTROLLER_CURRENT, + ); + await this.setup(); + return this.controller; + } + + async connect() { + const connectMethods = + MetadataInspector.getAllMethodMetadata( + 'socketio:connect', + this.controllerClass.prototype, + ) || {}; + for (const m in connectMethods) { + await invokeMethod(this.controller, m, this.reqCtx, [this.reqCtx.socket]); + } + } + + registerSubscribeMethods() { + const regexpEventHandlers = new Map< + RegExp[], + (...args: unknown[]) => Promise + >(); + const subscribeMethods = + MetadataInspector.getAllMethodMetadata<(string | RegExp)[]>( + 'socketio:subscribe', + this.controllerClass.prototype, + ) || {}; + for (const m in subscribeMethods) { + for (const t of subscribeMethods[m]) { + const regexps: RegExp[] = []; + if (typeof t === 'string') { + this.reqCtx.socket.on(t, async (...args: unknown[]) => { + await invokeMethod(this.controller, m, this.reqCtx, args); + }); + } else if (t instanceof RegExp) { + regexps.push(t); + } + if (regexps.length) { + // Build a map of regexp based message handlers + regexpEventHandlers.set(regexps, async (...args: unknown[]) => { + await invokeMethod(this.controller, m, this.reqCtx, args); + }); + } + } + } + return regexpEventHandlers; + } + + /** + * Set up the controller for the given socket + * @param socket + */ + async setup() { + // Invoke connect handlers + await this.connect(); + + // Register event handlers + const regexpHandlers = this.registerSubscribeMethods(); + + // Register event handlers with regexp + if (regexpHandlers.size) { + // Use a socket middleware to match event names with regexp + this.reqCtx.socket.use(async (packet, next) => { + const eventName = packet[0]; + for (const e of regexpHandlers.entries()) { + if (e[0].some(re => !!eventName.match(re))) { + const handler = e[1]; + const args = [packet[1]]; + if (packet[2]) { + // TODO: Should we auto-ack? + // Ack callback + args.push(packet[2]); + } + await handler(args); + } + } + next(); + }); + } + } +} diff --git a/packages/socketio/src/socketio.server.ts b/packages/socketio/src/socketio.server.ts new file mode 100644 index 000000000000..9739ca8c5d1c --- /dev/null +++ b/packages/socketio/src/socketio.server.ts @@ -0,0 +1,213 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/socketio +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + BindingFilter, + BindingScope, + Constructor, + Context, + ContextView, + createBindingFromClass, + inject, + MetadataInspector, +} from '@loopback/context'; +import {CoreBindings, CoreTags} from '@loopback/core'; +import {HttpServer, HttpServerOptions} from '@loopback/http-server'; +import * as debugFactory from 'debug'; +import {Server, ServerOptions, Socket} from 'socket.io'; +import { + getSocketIOMetadata, + SOCKET_IO_CONNECT_METADATA, + SOCKET_IO_METADATA, + SOCKET_IO_SUBSCRIBE_METADATA, +} from './decorators/socketio.decorator'; +import {SocketIOBindings, SocketIOTags} from './keys'; +import {SocketIOControllerFactory} from './socketio-controller-factory'; +import SocketIO = require('socket.io'); + +const debug = debugFactory('loopback:socketio:server'); + +// tslint:disable:no-any +export type SockIOMiddleware = ( + socket: Socket, + fn: (err?: any) => void, +) => void; + +export const socketIOControllers: BindingFilter = binding => { + if (!binding.tagNames.includes(CoreTags.CONTROLLER)) return false; + if (binding.tagNames.includes('socketio')) return true; + if (binding.valueConstructor) { + const cls = binding.valueConstructor; + const classMeta = MetadataInspector.getClassMetadata( + SOCKET_IO_METADATA, + cls, + ); + if (classMeta != null) { + debug('SocketIO metadata found at class %s', cls.name); + return true; + } + const subscribeMeta = MetadataInspector.getAllMethodMetadata( + SOCKET_IO_SUBSCRIBE_METADATA, + cls.prototype, + ); + if (subscribeMeta != null) { + debug('SocketIO subscribe metadata found at methods of %s', cls.name); + return true; + } + + const connectMeta = MetadataInspector.getAllMethodMetadata( + SOCKET_IO_CONNECT_METADATA, + cls.prototype, + ); + if (connectMeta != null) { + debug('SocketIO connect metadata found at methods of %s', cls.name); + return true; + } + } + return false; +}; + +export interface SocketIOServerOptions { + httpServerOptions?: HttpServerOptions; + socketIOOptions?: ServerOptions; +} + +/** + * A socketio server + */ +export class SocketIOServer extends Context { + private controllers: ContextView; + private httpServer: HttpServer; + private io: Server; + + constructor( + @inject(SocketIOBindings.CONFIG, {optional: true}) + private options: SocketIOServerOptions = {}, + ) { + super(); + this.io = SocketIO(options); + this.controllers = this.createView(socketIOControllers); + } + + /** + * Register a sock.io middleware function + * @param fn + */ + use(fn: SockIOMiddleware) { + return this.io.use(fn); + } + + get url() { + return this.httpServer && this.httpServer.url; + } + + /** + * Register a socketio controller + * @param controllerClass + * @param namespace + */ + route(controllerClass: Constructor, namespace?: string | RegExp) { + if (namespace == null) { + const meta = getSocketIOMetadata(controllerClass); + namespace = meta && meta.namespace; + } + + const nsp = namespace ? this.io.of(namespace) : this.io; + nsp.on('connection', socket => + this.createSocketHandler(controllerClass)(socket), + ); + return nsp; + } + + /** + * Create socket handler from the controller class + * @param controllerClass + */ + private createSocketHandler( + controllerClass: Constructor, + ): (socket: Socket) => void { + return async socket => { + debug( + 'Websocket connected: id=%s namespace=%s', + socket.id, + socket.nsp.name, + ); + // Create a request context + const reqCtx = new SocketIORequestContext(socket, this); + // Bind socketio + reqCtx.bind('socketio.socket').to(socket); + reqCtx.bind(CoreBindings.CONTROLLER_CLASS).to(controllerClass); + reqCtx + .bind(CoreBindings.CONTROLLER_CURRENT) + .toClass(controllerClass) + .inScope(BindingScope.SINGLETON); + // Instantiate the controller instance + await new SocketIOControllerFactory(reqCtx, controllerClass).create(); + }; + } + + /** + * Register a socket.io controller + * @param controllerClass + */ + controller(controllerClass: Constructor) { + debug('Adding controller %s', controllerClass.name); + const binding = createBindingFromClass(controllerClass, { + namespace: 'socketio.controllers', + defaultScope: BindingScope.TRANSIENT, + }).tag(SocketIOTags.SOCKET_IO, CoreTags.CONTROLLER); + this.add(binding); + debug('Controller binding: %j', binding); + return binding; + } + + /** + * Discover all socket.io controllers and register routes + */ + discoverAndRegister() { + const bindings = this.controllers.bindings; + for (const binding of bindings) { + if (binding.valueConstructor) { + debug( + 'Controller binding found: %s %s', + binding.key, + binding.valueConstructor.name, + ); + this.route(binding.valueConstructor as Constructor); + } + } + } + + /** + * Start the socketio server + */ + async start() { + this.httpServer = new HttpServer(() => {}, this.options.httpServerOptions); + await this.httpServer.start(); + this.io.attach(this.httpServer.server, this.options.socketIOOptions); + } + + /** + * Stop the socketio server + */ + async stop() { + const closePromise = new Promise((resolve, reject) => { + this.io.close(() => { + resolve(); + }); + }); + await closePromise; + if (this.httpServer) await this.httpServer.stop(); + } +} + +/** + * Request context for a socket.io request + */ +export class SocketIORequestContext extends Context { + constructor(public readonly socket: Socket, parent: Context) { + super(parent); + } +} diff --git a/packages/socketio/tsconfig.build.json b/packages/socketio/tsconfig.build.json new file mode 100644 index 000000000000..6e15e4be4f6f --- /dev/null +++ b/packages/socketio/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +}