Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 38 additions & 72 deletions public/index.html
Original file line number Diff line number Diff line change
@@ -1,73 +1,39 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>@loopback/example-todo</title>

<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/x-icon" href="https://loopback.io/favicon.ico">

<style>
h3 {
margin-left: 25px;
text-align: center;
}

a, a:visited {
color: #3f5dff;
}

h3 a {
margin-left: 10px;
}

a:hover, a:focus, a:active {
color: #001956;
}

.power {
position: absolute;
bottom: 25px;
left: 50%;
transform: translateX(-50%);
}

.info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%)
}

.info h1 {
text-align: center;
margin-bottom: 0
}

.info p {
text-align: center;
margin-bottom: 3em;
margin-top: 1em;
}
</style>
</head>

<body>
<div class="info">
<h1>@loopback/example-todo</h1>

<h3>OpenAPI spec: <a href="/openapi.json">/openapi.json</a></h3>
<h3>API Explorer: <a href="/explorer">/explorer</a></h3>
<h3>Chats: <a href="/chats.html">/chats.html</a></h3>
</div>

<footer class="power">
<a href="https://loopback.io" target="_blank">
<img src="https://loopback.io/images/branding/powered-by-loopback/blue/powered-by-loopback-sm.png" />
</a>
</footer>
</body>

<!doctype html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
#messages { margin-bottom: 40px }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
$(function () {
var socket = io('/chats/1');
$('form').submit(function(){
socket.emit('chat message', $('#m').val());
$('#m').val('');
return false;
});
socket.on('chat message', function(msg){
$('#messages').append($('<li>').text(msg));
window.scrollTo(0, document.body.scrollHeight);
});
});
</script>
</body>
</html>
108 changes: 43 additions & 65 deletions src/application.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,54 @@
// Copyright IBM Corp. 2018,2020. All Rights Reserved.
// Node module: @loopback/example-todo
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import {Application, ApplicationConfig} from '@loopback/core';
import {HttpServer} from '@loopback/http-server';
import * as express from 'express';
import * as path from 'path';
import {WebSocketController} from './controllers';
import {WebSocketServer} from './websocket.server';

import { BootMixin } from '@loopback/boot';
import { ApplicationConfig } from '@loopback/core';
import { RepositoryMixin } from '@loopback/repository';
import { Request, Response } from '@loopback/rest';
import { RestExplorerComponent } from '@loopback/rest-explorer';
import { ServiceMixin } from '@loopback/service-proxy';
import morgan from 'morgan';
import path from 'path';
import { MySequence } from './sequence';
import { WebsocketApplication } from "./websockets/websocket.application";
import { WebsocketControllerBooter } from "./websockets/websocket.booter";
// tslint:disable:no-any

export { ApplicationConfig };
export class WebSocketDemoApplication extends Application {
readonly httpServer: HttpServer;
readonly wsServer: WebSocketServer;

export class TodoListApplication extends BootMixin(
ServiceMixin(RepositoryMixin(WebsocketApplication)),
) {
constructor(options: ApplicationConfig = {}) {
super(options);

// Set up the custom sequence
this.sequence(MySequence);

// Set up default home page
this.static('/', path.join(__dirname, '../public'));

this.component(RestExplorerComponent);

this.booters(WebsocketControllerBooter);

this.projectRoot = __dirname;
// Customize @loopback/boot Booter Conventions here
this.bootOptions = {
controllers: {
// Customize ControllerBooter Conventions here
dirs: ['controllers'],
extensions: ['.controller.js'],
nested: true,
},
websocketControllers: {
dirs: ['controllers'],
extensions: ['.controller.ws.js'],
nested: true,
},
};

this.setupLogging();
/**
* Create an Express app to serve the home page
*/
const expressApp = express();
const root = path.resolve(__dirname, '../../public');
expressApp.use('/', express.static(root));

// Create an http server backed by the Express app
this.httpServer = new HttpServer(expressApp, options.websocket);

// Create ws server from the http server
const wsServer = new WebSocketServer(this.httpServer);
this.bind('servers.websocket.server1').to(wsServer);
wsServer.use((socket, next) => {
console.log('Global middleware - socket:', socket.id);
next();
});
// Add a route
const ns = wsServer.route(WebSocketController, /^\/chats\/\d+$/);
ns.use((socket, next) => {
console.log(
'Middleware for namespace %s - socket: %s',
socket.nsp.name,
socket.id,
);
next();
});
this.wsServer = wsServer;
}

private setupLogging() {
// Register `morgan` express middleware
// Create a middleware factory wrapper for `morgan(format, options)`
const morganFactory = (config?: morgan.Options<Request, Response>) => {
this.debug('Morgan configuration', config);
return morgan('combined', config);
};
start() {
return this.wsServer.start();
}

// Print out logs using `debug`
const defaultConfig: morgan.Options<Request, Response> = {
stream: {
write: str => {
this._debug(str);
},
},
};
this.expressMiddleware(morganFactory, defaultConfig, {
injectConfiguration: 'watch',
key: 'middleware.morgan',
});
stop() {
return this.wsServer.stop();
}
}
8 changes: 1 addition & 7 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
// Copyright IBM Corp. 2018,2020. All Rights Reserved.
// Node module: @loopback/example-todo
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './todo.controller';
export * from './chat.controller.ws';
export * from './websocket.controller';
52 changes: 52 additions & 0 deletions src/controllers/websocket.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {Socket} from 'socket.io';
import {ws} from '../decorators/websocket.decorator';

/**
* A demo controller for websocket
*/
@ws('/chats')
export class WebSocketController {
constructor(
@ws.socket() // Equivalent to `@inject('ws.socket')`
private socket: Socket,
) {}

/**
* The method is invoked when a client connects to the server
* @param socket
*/
@ws.connect()
connect(socket: Socket) {
console.log('Client connected: %s', this.socket.id);
socket.join('room 1');
}

/**
* Register a handler for 'chat message' events
* @param msg
*/
@ws.subscribe('chat message')
// @ws.emit('namespace' | 'requestor' | 'broadcast')
handleChatMessage(msg: unknown) {
console.log('Chat message: %s', msg);
this.socket.nsp.emit('chat message', `[${this.socket.id}] ${msg}`);
}

/**
* Register a handler for all events
* @param msg
*/
@ws.subscribe(/.+/)
logMessage(...args: unknown[]) {
console.log('Message: %s', args);
}

/**
* The method is invoked when a client disconnects from the server
* @param socket
*/
@ws.disconnect()
disconnect() {
console.log('Client disconnected: %s', this.socket.id);
}
}
77 changes: 77 additions & 0 deletions src/decorators/websocket.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
ClassDecoratorFactory,
Constructor,
MetadataAccessor,
MetadataInspector,
MethodDecoratorFactory,
inject,
} from '@loopback/context';

export interface WebSocketMetadata {
namespace?: string | RegExp;
}

export const WEBSOCKET_METADATA = MetadataAccessor.create<
WebSocketMetadata,
ClassDecorator
>('websocket');

/**
* Decorate a websocket controller class to specify the namespace
* For example,
* ```ts
* @ws({namespace: '/chats'})
* export class WebSocketController {}
* ```
* @param spec A namespace or object
*/
export function ws(spec: WebSocketMetadata | string | RegExp = {}) {
if (typeof spec === 'string' || spec instanceof RegExp) {
spec = {namespace: spec};
}
return ClassDecoratorFactory.createDecorator(WEBSOCKET_METADATA, spec);
}

export function getWebSocketMetadata(controllerClass: Constructor<unknown>) {
return MetadataInspector.getClassMetadata(
WEBSOCKET_METADATA,
controllerClass,
);
}

export namespace ws {
export function socket() {
return inject('ws.socket');
}

/**
* Decorate a method to subscribe to websocket events.
* For example,
* ```ts
* @ws.subscribe('chat message')
* async function onChat(msg: string) {
* }
* ```
* @param messageTypes
*/
export function subscribe(...messageTypes: (string | RegExp)[]) {
return MethodDecoratorFactory.createDecorator(
'websocket:subscribe',
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('websocket:connect', true);
}
}
Loading