feat: add websocket model

Signed-off-by: MiaoWoo <admin@yumc.pw>
This commit is contained in:
MiaoWoo 2019-06-28 15:30:46 +08:00
parent c93c5c3fbb
commit d9e3cad8a1
23 changed files with 483 additions and 37 deletions

View File

@ -1,7 +1,7 @@
{ {
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"version": "0.3.3", "version": "independent",
"packages": [ "packages": [
"packages/*" "packages/*"
], ],
@ -14,4 +14,4 @@
"registry": "https://repo.yumc.pw/repository/npm-hosted/" "registry": "https://repo.yumc.pw/repository/npm-hosted/"
} }
} }
} }

View File

@ -14,7 +14,7 @@
"url": "git+https://github.com/502647092/cc-server-parent.git" "url": "git+https://github.com/502647092/cc-server-parent.git"
}, },
"scripts": { "scripts": {
"dev": "npx ts-node src/index.ts", "watch": "npx tsc --watch",
"build": "rimraf dist && npx tsc", "build": "rimraf dist && npx tsc",
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },

View File

@ -1,4 +1,3 @@
export * from './decorators' export * from './decorators'
export * from './constants'; export * from './activation'
export * from './utils' export * from 'inversify-express-utils'
export * from './activation'

View File

@ -23,15 +23,17 @@
"@cc-server/binding": "^0.3.3", "@cc-server/binding": "^0.3.3",
"@cc-server/db-mongo": "^0.3.3", "@cc-server/db-mongo": "^0.3.3",
"@cc-server/ioc": "^0.3.3", "@cc-server/ioc": "^0.3.3",
"@cc-server/ws": "^0.3.3",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"inversify": "^5.0.1", "inversify": "^5.0.1",
"inversify-express-utils": "^6.3.2", "inversify-express-utils": "^6.3.2",
"prettyjson": "^1.2.1", "prettyjson": "^1.2.1",
"reflect-metadata": "^0.1.13" "socket.io": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/body-parser": "^1.17.0", "@types/body-parser": "^1.17.0",
"@types/express": "^4.17.0", "@types/express": "^4.17.0",
"@types/socket.io": "^2.1.2",
"@types/prettyjson": "^0.0.29", "@types/prettyjson": "^0.0.29",
"mocha": "^6.1.4", "mocha": "^6.1.4",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
@ -40,4 +42,4 @@
"typescript": "^3.5.1" "typescript": "^3.5.1"
}, },
"gitHead": "7d84393a3cb6be6be9ed51d71f12677d2d7d0728" "gitHead": "7d84393a3cb6be6be9ed51d71f12677d2d7d0728"
} }

View File

@ -0,0 +1,55 @@
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@2.2.0/dist/socket.io.js"> </script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@3.12.2/dist/xterm.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@3.12.2/dist/addons/fullscreen/fullscreen.css">
<script src="https://cdn.jsdelivr.net/npm/xterm@3.12.2/dist/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@3.12.2/dist/addons/fit/fit.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@3.12.2/dist/addons/attach/attach.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@3.12.2/dist/addons/fullscreen/fullscreen.js"></script>
<style>
#terminal-container .terminal.xterm {
height: 100%;
}
#terminal-container .xterm-viewport {
height: 100% !important;
}
</style>
</head>
<body>
<div id="terminal" style="height: 100%;"></div>
<script type="text/javascript">
Terminal.applyAddon(fit);
Terminal.applyAddon(attach);
Terminal.applyAddon(fullscreen);
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.toggleFullScreen();
term.fit();
var socket = io('',{
path: '/ws'
});
socket.on('connect', function(){
term.writeln('connect')
});
term.on('data', (data) => {
if (data =='\r') {
term.writeln(data)
}
socket.send(data);
});
socket.on('message', function(data){
term.write(data.toString());
});
socket.on('disconnect', function(){
term.reset();
term.writeln('disconnect')
});
</script>
</body>
</html>

View File

@ -1,8 +1,6 @@
import { import { inject, postConstruct } from '@cc-server/ioc';
controller, response, requestBody, httpGet, httpPost, queryParam, requestParam import { Vaild, NotBlank, NotNull, controller, requestBody, httpGet, httpPost, requestParam } from '@cc-server/binding'
} from 'inversify-express-utils'; import { namespace, listener, interfaces, io, getSocketContext } from '@cc-server/ws'
import { inject, postConstruct } from 'inversify';
import { Vaild, NotBlank, NotNull } from '@cc-server/binding'
import { DBClient } from '@cc-server/db' import { DBClient } from '@cc-server/db'
import '@cc-server/db-mongo' import '@cc-server/db-mongo'
@ -21,9 +19,7 @@ class ExampleModel {
email: string; email: string;
} }
type Model = ExampleModel @controller('/example')
@controller('')
export class Controller { export class Controller {
@inject(DBClient) @inject(DBClient)
private client: DBClient private client: DBClient
@ -34,14 +30,14 @@ export class Controller {
} }
@httpGet('/') @httpGet('/')
public async list(): Promise<Model[]> { public async list(): Promise<ExampleModel[]> {
return this.client.find({}); return this.client.find({});
} }
@httpGet('/:id') @httpGet('/:id')
public async get( public async get(
@requestParam('id') id: string @requestParam('id') id: string
): Promise<Model> { ): Promise<ExampleModel> {
return this.client.findOneById(id); return this.client.findOneById(id);
} }
@ -56,8 +52,40 @@ export class Controller {
@httpPost('/:id') @httpPost('/:id')
public async update( public async update(
@requestParam('id') id: string, @requestParam('id') id: string,
@requestBody() model: Model @requestBody() model: ExampleModel
): Promise<boolean> { ): Promise<boolean> {
return this.client.updateById(id, model); return this.client.updateById(id, model);
} }
} }
@namespace('/', (socket: io.Socket, next: (err?: any) => void) => {
console.log(socket.nsp.name, socket.id, 'before connection');
next();
})
export class Namespace extends interfaces.Namespace {
private cache: { [key: string]: string } = {};
public async connection(socket: io.Socket) {
console.log(this.nsp.name, socket.id, 'connection');
return `Welcome to Websocket Chat Room Now: ${Date.now()} Your ID: ${socket.id}! \n`;
}
public async disconnect(socket: io.Socket) {
console.log(this.nsp.name, socket.id, 'disconnect');
}
@listener('message', (socket: io.Socket, packet: io.Packet, next: (err?: any) => void) => {
console.log(socket.nsp.name, socket.id, 'listener middleware', [...packet]);
next();
})
public async message(socket: io.Socket, data: any) {
console.log(this.nsp.name, socket.id, 'message', data)
this.cache[socket.id] = (this.cache[socket.id] || '') + data;
if (data == '\r' && this.cache[socket.id] !== "") {
let result = this.broadcast(this.cache[socket.id] + '\n')
this.cache[socket.id] = '';
return result;
}
return data;
}
}

View File

@ -1,48 +1,76 @@
import 'reflect-metadata'; import 'reflect-metadata';
import * as http from 'http'
import * as express from "express"; import * as express from "express";
import { InversifyExpressServer, interfaces, getRouteInfo } from 'inversify-express-utils';
import * as bodyParser from 'body-parser';
import { buildProviderModule } from '@cc-server/ioc';
import { rebuildServer } from '@cc-server/binding'
import * as prettyjson from "prettyjson"; import * as prettyjson from "prettyjson";
import { Container } from 'inversify'; import * as bodyParser from 'body-parser';
import { buildWebSocket, io } from '@cc-server/ws'
import { buildProviderModule, Container } from '@cc-server/ioc';
import { InversifyExpressServer, interfaces, getRouteInfo, rebuildServer } from '@cc-server/binding'
export { io, http, express }
export class CcServerBoot { export class CcServerBoot {
private _container: Container; private _container: Container;
private _server: InversifyExpressServer; private _server: http.Server;
private _serverInstance: express.Application; private _serverInstance: express.Application;
private _serverInversify: InversifyExpressServer;
private _wsServer: io.Server;
constructor(container?: Container) { constructor(container?: Container) {
this._container = container || new Container(); this._container = container || new Container();
this._container.load(buildProviderModule()); this._serverInstance = express();
this._server = http.createServer(this._serverInstance);
// start the server // start the server
this._server = new InversifyExpressServer(this._container); this._serverInversify = new InversifyExpressServer(this._container, null, null, this._serverInstance);
this._server.setConfig((app) => { this._serverInversify.setConfig((app) => {
app.use(bodyParser.urlencoded({ app.use(bodyParser.urlencoded({
extended: true extended: true
})); }));
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.raw()); app.use(bodyParser.raw());
}); });
this._wsServer = io(this._server, {
path: '/ws',
serveClient: false,
})
}
get server() {
return this._server;
}
get express() {
return this._serverInstance;
}
get inversify() {
return this._serverInversify;
}
get websocket() {
return this._wsServer;
} }
public setConfig(fn: interfaces.ConfigFunction) { public setConfig(fn: interfaces.ConfigFunction) {
this._server.setConfig(fn) this._serverInversify.setConfig(fn)
} }
public setErrorConfig(fn: interfaces.ConfigFunction) { public setErrorConfig(fn: interfaces.ConfigFunction) {
this._server.setErrorConfig(fn) this._serverInversify.setErrorConfig(fn)
} }
public build() { public build() {
this._serverInstance = this._server.build(); this._container.load(buildProviderModule());
this._serverInstance = this._serverInversify.build();
rebuildServer(this._container); rebuildServer(this._container);
buildWebSocket(this._container, this._wsServer);
return this._serverInstance; return this._serverInstance;
} }
public start(port: number = 80) { public start(port: number = 80) {
const routeInfo = getRouteInfo(this._container); const routeInfo = getRouteInfo(this._container);
console.log(prettyjson.render({ routes: routeInfo })); console.log(prettyjson.render({ routes: routeInfo }));
this._serverInstance.listen(port); this._server.listen(port);
console.log(`Server started on port ${port} :)`); console.log(`Server started on port ${port} :)`);
} }
} }

View File

@ -1,8 +1,10 @@
import { CcServerBoot } from './index' import { CcServerBoot, express } from './index'
import './function/handle'; import './function/handle';
let server = new CcServerBoot(); let server = new CcServerBoot();
server.express.use(express.static('public'));
server.build(); server.build();
server.start(); server.start();

View File

@ -10,6 +10,7 @@
"registry": "https://repo.yumc.pw/repository/npm-hosted/" "registry": "https://repo.yumc.pw/repository/npm-hosted/"
}, },
"scripts": { "scripts": {
"watch": "npx tsc --watch",
"build": "rimraf dist && npx tsc", "build": "rimraf dist && npx tsc",
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },

View File

@ -14,6 +14,7 @@
"url": "git+https://github.com/502647092/cc-server-parent.git" "url": "git+https://github.com/502647092/cc-server-parent.git"
}, },
"scripts": { "scripts": {
"watch": "npx tsc --watch",
"build": "rimraf dist && npx tsc", "build": "rimraf dist && npx tsc",
"test": "echo \"Error: run tests from root\" && exit 1" "test": "echo \"Error: run tests from root\" && exit 1"
}, },

View File

@ -20,8 +20,7 @@
"dependencies": { "dependencies": {
"inversify": "^5.0.1", "inversify": "^5.0.1",
"inversify-binding-decorators": "^4.0.0", "inversify-binding-decorators": "^4.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13"
"ts-node-dev": "^1.0.0-pre.40"
}, },
"devDependencies": { "devDependencies": {
"mocha": "^6.1.4", "mocha": "^6.1.4",

View File

@ -1,5 +1,5 @@
import "reflect-metadata"; import "reflect-metadata";
import { Container, inject, interfaces } from 'inversify'; import { Container, inject, interfaces, injectable, postConstruct } from 'inversify';
import { autoProvide, provide, fluentProvide, buildProviderModule } from 'inversify-binding-decorators'; import { autoProvide, provide, fluentProvide, buildProviderModule } from 'inversify-binding-decorators';
const provideNamed = (identifier: interfaces.ServiceIdentifier<any>, name: string) => { const provideNamed = (identifier: interfaces.ServiceIdentifier<any>, name: string) => {
@ -14,4 +14,4 @@ const provideSingleton = (identifier: interfaces.ServiceIdentifier<any>) => {
.done(); .done();
}; };
export { autoProvide, provide, provideNamed, provideSingleton, inject, buildProviderModule }; export { autoProvide, provide, provideNamed, provideSingleton, Container, inject, injectable, postConstruct, buildProviderModule };

4
packages/ws/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules
/dist
/package-lock.json
/yarn.lock

22
packages/ws/.npmignore Normal file
View File

@ -0,0 +1,22 @@
src
test
typings
bundled
build
coverage
docs
wiki
gulpfile.js
bower.json
karma.conf.js
tsconfig.json
typings.json
CONTRIBUTING.md
ISSUE_TEMPLATE.md
PULL_REQUEST_TEMPLATE.md
tslint.json
wallaby.js
.travis.yml
.gitignore
.vscode
type_definitions

11
packages/ws/README.md Normal file
View File

@ -0,0 +1,11 @@
# `cc-server-ws`
> TODO: description
## Usage
```
const ccServerIoc = require('cc-server-ws');
// TODO: DEMONSTRATE API
```

33
packages/ws/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "@cc-server/ws",
"version": "0.3.3",
"description": "> TODO: description",
"author": "MiaoWoo <admin@yumc.pw>",
"homepage": "https://faas.yumc.pw",
"license": "ISC",
"main": "dist/index.js",
"publishConfig": {
"registry": "https://repo.yumc.pw/repository/npm-hosted/"
},
"scripts": {
"watch": "npx tsc --watch",
"build": "rimraf dist && npx tsc",
"test": "echo \"Error: run tests from root\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/502647092/cc-server-parent.git"
},
"dependencies": {
"inversify": "^5.0.1",
"reflect-metadata": "^0.1.13",
"socket.io": "^2.2.0"
},
"devDependencies": {
"@types/socket.io": "^2.1.2",
"mocha": "^6.1.4",
"rimraf": "^2.6.3",
"typescript": "^3.5.1"
},
"gitHead": "7d84393a3cb6be6be9ed51d71f12677d2d7d0728"
}

View File

@ -0,0 +1,92 @@
import "reflect-metadata";
import { Container } from 'inversify'
import { interfaces, BroadcastMessage } from './interfaces'
import { TYPE } from './constants'
import { getNamespaces, getNamespaceMetadata, getNamespaceListenerMetadata } from './utils'
import * as io from 'socket.io'
export function buildWebSocket(container: Container, server: io.Server) {
let constructors = getNamespaces();
if (!constructors.length) { return; }
registryNamespace(container, constructors);
// get all namespaces
let namespaces = container.getAll<interfaces.Namespace>(TYPE.Namespace)
for (const namespace of namespaces) {
let namespaceMetadata = getNamespaceMetadata(namespace);
let namespaceEventMetadata = getNamespaceListenerMetadata(namespace);
let ns = server.of(namespaceMetadata.name);
namespace.constructor.prototype.nsp = ns;
applyNamespaceMiddleware(namespaceMetadata, ns);
ns.on('connection', async (socket: io.Socket) => {
let namespaceInstance = container.getNamed<interfaces.Namespace>(TYPE.Namespace, namespace.constructor.name);
await applyEvent(namespaceInstance, socket);
await applyMiddlewares(namespaceEventMetadata, socket);
await applyListeners(namespaceEventMetadata, socket, namespaceInstance);
})
}
}
function registryNamespace(container: Container, constructors: any[]) {
constructors.forEach((constructor) => {
const name = constructor.name;
if (container.isBoundNamed(TYPE.Namespace, name)) {
throw new Error(`DUPLICATED_NAMESPACE(${name})`);
}
container.bind(TYPE.Namespace)
.to(constructor)
.whenTargetNamed(name);
});
}
function applyNamespaceMiddleware(namespaceMetadata: interfaces.NamespaceMetadata, ns: io.Namespace) {
for (const middleware of namespaceMetadata.middleware) {
ns.use(middleware);
}
}
function flatten(arr: Array<any>) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
function applyMiddlewares(namespaceEventMetadata: interfaces.ListenerMetadata[], socket: io.Socket) {
// socket.use((packet: io.Packet, next: (err?: any) => void) => {
// Reflect.defineMetadata(TYPE.SocketContext, socket, packet);
// next();
// })
let middlewares = [...new Set(flatten(namespaceEventMetadata.map((data) => data.middleware)))];
for (const middleware of middlewares) {
socket.use((packet: io.Packet, next: (err?: any) => void) => { middleware(socket, packet, next); });
}
}
async function applyEvent(namespaceInstance: interfaces.Namespace, socket: io.Socket) {
if (namespaceInstance.connection) {
let result = await namespaceInstance.connection(socket);
if (result != undefined) {
socket.send(result);
}
}
if (namespaceInstance.disconnect) {
socket.on('disconnect', async () => await namespaceInstance.disconnect(socket));
}
}
function applyListeners(namespaceEventMetadata: interfaces.ListenerMetadata[], socket: io.Socket, namespaceInstance: interfaces.Namespace) {
for (const event of namespaceEventMetadata) {
socket.on(event.name, async data => {
let result = await namespaceInstance[event.key](socket, data);
if (result != undefined) {
if (result instanceof BroadcastMessage) {
socket.broadcast.emit(event.name, result.message);
}
else {
socket.emit(event.name, result);
}
}
});
}
}

View File

@ -0,0 +1,9 @@
export const TYPE = {
Namespace: Symbol.for('namespace'),
SocketContext: Symbol.for('context')
}
export const METADATA_KEY = {
namespace: "@cc-server/ws:namespace",
listener: "@cc-server/ws:listener"
};

View File

@ -0,0 +1,40 @@
import { inject, injectable, decorate } from "inversify";
import { interfaces } from './interfaces'
import { METADATA_KEY } from './constants'
import { getNamespaceListenerMetadata, getNamespacesMetadata } from './utils'
/**
* Socket.io Namespace
* @param name namespace name default is '/'
* @param middleware middleware array
*/
export function namespace(name?: string, ...middleware: interfaces.Middleware[]) {
return function(target: any) {
let currentMetadata: interfaces.NamespaceMetadata = {
name: name || '/',
middleware: middleware,
target: target
};
decorate(injectable(), target);
Reflect.defineMetadata(METADATA_KEY.namespace, currentMetadata, target);
const previousMetadata: interfaces.NamespaceMetadata[] = getNamespacesMetadata();
Reflect.defineMetadata(METADATA_KEY.namespace, [currentMetadata, ...previousMetadata], Reflect);
};
}
/**
* Socket.io listner
* @param name event name
*/
export function listener(name?: string, ...middleware: interfaces.ListenerMiddleware[]) {
return function(target: any, key: string, value: any) {
let currentMetadata: interfaces.ListenerMetadata = {
name: name || key,
middleware: middleware,
key: key,
target: target
};
const previousMetadata: interfaces.ListenerMetadata[] = getNamespaceListenerMetadata(target)
Reflect.defineMetadata(METADATA_KEY.listener, [currentMetadata, ...previousMetadata], target.constructor);
};
}

7
packages/ws/src/index.ts Normal file
View File

@ -0,0 +1,7 @@
import * as io from 'socket.io'
export * from './builder'
export * from './decorators'
export * from './interfaces'
export { getSocketContext } from './utils'
export { io }

View File

@ -0,0 +1,64 @@
import * as io from 'socket.io'
import { injectable } from 'inversify';
export class Message {
constructor(public message: any) { }
}
export class BroadcastMessage {
constructor(public message: any) { }
}
export namespace interfaces {
@injectable()
export class Namespace {
/**
* @see io.Namespace
*/
public nsp?: io.Namespace;
/**
* The event fired when we get a new connection
* @param socket socket
* @return return data will send use socket.send(data)
*/
public connection?(socket: io.Socket): any;
/**
* The event fired when socket is close
* @param socket socket
*/
public disconnect?(socket: io.Socket): void;
/**
* broadcast message on this namespace
*/
public broadcast(message: any): BroadcastMessage {
return new BroadcastMessage(message);
}
/**
* Event Listener
* @param data event data
* @return return data will send use socket.emit(key, data)
*/
[key: string]: ((data: any, socket: io.Socket) => any) | any;
}
export interface Middleware {
(socket: io.Socket, next: (err?: any) => void): void;
}
export interface ListenerMiddleware {
(socket: io.Socket, packet: io.Packet, next: (err?: any) => void): void;
}
export interface NamespaceMetadata {
name: string;
middleware?: Middleware[];
target: any;
}
export interface ListenerMetadata {
name: string;
key: string;
/**
* Socket Listener Middleware will share all event listener
*/
middleware?: ListenerMiddleware[];
target: any;
}
}

42
packages/ws/src/utils.ts Normal file
View File

@ -0,0 +1,42 @@
import { METADATA_KEY, TYPE } from './constants'
import { interfaces } from './interfaces'
function getNamespaces() {
return getNamespacesMetadata().map((target) => target.target);
}
function getNamespacesMetadata() {
let namespaceMetadata: interfaces.NamespaceMetadata[] = Reflect.getMetadata(
METADATA_KEY.namespace,
Reflect
) || [];
return namespaceMetadata;
}
function getNamespaceMetadata(target: any) {
let namespaceMetadata: interfaces.NamespaceMetadata = Reflect.getMetadata(
METADATA_KEY.namespace,
target.constructor
) || {};
return namespaceMetadata;
}
function getNamespaceListenerMetadata(target: any) {
let eventMetadata: interfaces.ListenerMetadata[] = Reflect.getMetadata(
METADATA_KEY.listener,
target.constructor
) || [];
return eventMetadata;
}
function getSocketContext(packet: any) {
return Reflect.getMetadata(TYPE.SocketContext, packet);
}
export {
getNamespaces,
getNamespaceMetadata,
getNamespacesMetadata,
getNamespaceListenerMetadata,
getSocketContext
}

View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "src",
"outDir": "dist"
}
}