diff --git a/CODEOWNERS b/CODEOWNERS
index 661b10cd..aebfa74b 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 f728cf62..84adfe3f 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 13c6536f..1e66d888 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/experimental/socketio/LICENSE b/experimental/socketio/LICENSE
new file mode 100644
index 00000000..b56c195b
--- /dev/null
+++ b/experimental/socketio/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) IBM Corp. 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/experimental/socketio/README.md b/experimental/socketio/README.md
new file mode 100644
index 00000000..f55d72b1
--- /dev/null
+++ b/experimental/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/experimental/socketio/docs.json b/experimental/socketio/docs.json
new file mode 100644
index 00000000..d2f75808
--- /dev/null
+++ b/experimental/socketio/docs.json
@@ -0,0 +1,7 @@
+{
+ "content": [
+ "index.ts",
+ "src/**/*.ts"
+ ],
+ "codeSectionDepth": 4
+}
diff --git a/experimental/socketio/index.d.ts b/experimental/socketio/index.d.ts
new file mode 100644
index 00000000..369063c3
--- /dev/null
+++ b/experimental/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/experimental/socketio/index.js b/experimental/socketio/index.js
new file mode 100644
index 00000000..cb208b17
--- /dev/null
+++ b/experimental/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/experimental/socketio/index.ts b/experimental/socketio/index.ts
new file mode 100644
index 00000000..0171a2c2
--- /dev/null
+++ b/experimental/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/experimental/socketio/package-lock.json b/experimental/socketio/package-lock.json
new file mode 100644
index 00000000..614fec04
--- /dev/null
+++ b/experimental/socketio/package-lock.json
@@ -0,0 +1,386 @@
+{
+ "name": "@loopback/socketio",
+ "version": "1.0.0-1",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@types/debug": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.4.tgz",
+ "integrity": "sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ==",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "10.14.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.6.tgz",
+ "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==",
+ "dev": true
+ },
+ "@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="
+ },
+ "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=="
+ },
+ "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="
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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="
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+ },
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ },
+ "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="
+ },
+ "p-event": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz",
+ "integrity": "sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA==",
+ "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"
+ }
+ },
+ "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="
+ }
+ }
+ },
+ "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="
+ },
+ "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/experimental/socketio/package.json b/experimental/socketio/package.json
new file mode 100644
index 00000000..d29a61ab
--- /dev/null
+++ b/experimental/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": {
+ "@loopback/context": "^1.14.0",
+ "@loopback/core": "^1.6.3",
+ "@loopback/http-server": "^1.2.2",
+ "debug": "^4.0.1",
+ "socket.io": "^2.1.1"
+ },
+ "devDependencies": {
+ "@loopback/build": "^1.5.3",
+ "@loopback/testlab": "^1.2.8",
+ "@loopback/tslint-config": "^2.0.4",
+ "@types/debug": "^4.1.4",
+ "@types/node": "^10.14.6",
+ "@types/socket.io": "^2.1.0",
+ "@types/socket.io-client": "^1.4.32",
+ "p-event": "^4.1.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/experimental/socketio/src/__tests__/acceptance/application.ts b/experimental/socketio/src/__tests__/acceptance/application.ts
new file mode 100644
index 00000000..23cfc03f
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/__tests__/acceptance/controllers/index.ts b/experimental/socketio/src/__tests__/acceptance/controllers/index.ts
new file mode 100644
index 00000000..47d35e91
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/__tests__/acceptance/controllers/socketio.controller.ts b/experimental/socketio/src/__tests__/acceptance/controllers/socketio.controller.ts
new file mode 100644
index 00000000..f8464b9a
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/__tests__/acceptance/socketio.server.acceptance.ts b/experimental/socketio/src/__tests__/acceptance/socketio.server.acceptance.ts
new file mode 100644
index 00000000..1cb60972
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/decorators/socketio.decorator.ts b/experimental/socketio/src/decorators/socketio.decorator.ts
new file mode 100644
index 00000000..61f6cd58
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/index.ts b/experimental/socketio/src/index.ts
new file mode 100644
index 00000000..6276a27b
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/keys.ts b/experimental/socketio/src/keys.ts
new file mode 100644
index 00000000..3b582885
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/socketio-controller-factory.ts b/experimental/socketio/src/socketio-controller-factory.ts
new file mode 100644
index 00000000..15228caa
--- /dev/null
+++ b/experimental/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/experimental/socketio/src/socketio.server.ts b/experimental/socketio/src/socketio.server.ts
new file mode 100644
index 00000000..9739ca8c
--- /dev/null
+++ b/experimental/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