From d9e3cad8a121f5b256c702175ff4c4b95c5f8df7 Mon Sep 17 00:00:00 2001 From: MiaoWoo Date: Fri, 28 Jun 2019 15:30:46 +0800 Subject: [PATCH] feat: add websocket model Signed-off-by: MiaoWoo --- lerna.json | 4 +- packages/binding/package.json | 2 +- packages/binding/src/index.ts | 5 +- packages/core/package.json | 6 +- packages/core/public/index.html | 55 +++++++++++++++++ packages/core/src/function/handle.ts | 50 +++++++++++---- packages/core/src/index.ts | 54 ++++++++++++---- packages/core/src/server.ts | 4 +- packages/db-mongo/package.json | 1 + packages/db/package.json | 1 + packages/ioc/package.json | 3 +- packages/ioc/src/index.ts | 4 +- packages/ws/.gitignore | 4 ++ packages/ws/.npmignore | 22 +++++++ packages/ws/README.md | 11 ++++ packages/ws/package.json | 33 ++++++++++ packages/ws/src/builder.ts | 92 ++++++++++++++++++++++++++++ packages/ws/src/constants.ts | 9 +++ packages/ws/src/decorators.ts | 40 ++++++++++++ packages/ws/src/index.ts | 7 +++ packages/ws/src/interfaces.ts | 64 +++++++++++++++++++ packages/ws/src/utils.ts | 42 +++++++++++++ packages/ws/tsconfig.json | 7 +++ 23 files changed, 483 insertions(+), 37 deletions(-) create mode 100644 packages/core/public/index.html create mode 100644 packages/ws/.gitignore create mode 100644 packages/ws/.npmignore create mode 100644 packages/ws/README.md create mode 100644 packages/ws/package.json create mode 100644 packages/ws/src/builder.ts create mode 100644 packages/ws/src/constants.ts create mode 100644 packages/ws/src/decorators.ts create mode 100644 packages/ws/src/index.ts create mode 100644 packages/ws/src/interfaces.ts create mode 100644 packages/ws/src/utils.ts create mode 100644 packages/ws/tsconfig.json diff --git a/lerna.json b/lerna.json index 7aee3d4..150f0bc 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,7 @@ { "npmClient": "yarn", "useWorkspaces": true, - "version": "0.3.3", + "version": "independent", "packages": [ "packages/*" ], @@ -14,4 +14,4 @@ "registry": "https://repo.yumc.pw/repository/npm-hosted/" } } -} +} \ No newline at end of file diff --git a/packages/binding/package.json b/packages/binding/package.json index 55e63de..8d5b919 100644 --- a/packages/binding/package.json +++ b/packages/binding/package.json @@ -14,7 +14,7 @@ "url": "git+https://github.com/502647092/cc-server-parent.git" }, "scripts": { - "dev": "npx ts-node src/index.ts", + "watch": "npx tsc --watch", "build": "rimraf dist && npx tsc", "test": "echo \"Error: run tests from root\" && exit 1" }, diff --git a/packages/binding/src/index.ts b/packages/binding/src/index.ts index a4b91ad..c963185 100644 --- a/packages/binding/src/index.ts +++ b/packages/binding/src/index.ts @@ -1,4 +1,3 @@ export * from './decorators' -export * from './constants'; -export * from './utils' -export * from './activation' \ No newline at end of file +export * from './activation' +export * from 'inversify-express-utils' diff --git a/packages/core/package.json b/packages/core/package.json index 442d4cb..1eb2710 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,15 +23,17 @@ "@cc-server/binding": "^0.3.3", "@cc-server/db-mongo": "^0.3.3", "@cc-server/ioc": "^0.3.3", + "@cc-server/ws": "^0.3.3", "body-parser": "^1.19.0", "inversify": "^5.0.1", "inversify-express-utils": "^6.3.2", "prettyjson": "^1.2.1", - "reflect-metadata": "^0.1.13" + "socket.io": "^2.2.0" }, "devDependencies": { "@types/body-parser": "^1.17.0", "@types/express": "^4.17.0", + "@types/socket.io": "^2.1.2", "@types/prettyjson": "^0.0.29", "mocha": "^6.1.4", "rimraf": "^2.6.3", @@ -40,4 +42,4 @@ "typescript": "^3.5.1" }, "gitHead": "7d84393a3cb6be6be9ed51d71f12677d2d7d0728" -} +} \ No newline at end of file diff --git a/packages/core/public/index.html b/packages/core/public/index.html new file mode 100644 index 0000000..1f74a22 --- /dev/null +++ b/packages/core/public/index.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/packages/core/src/function/handle.ts b/packages/core/src/function/handle.ts index a9183eb..166d273 100644 --- a/packages/core/src/function/handle.ts +++ b/packages/core/src/function/handle.ts @@ -1,8 +1,6 @@ -import { - controller, response, requestBody, httpGet, httpPost, queryParam, requestParam -} from 'inversify-express-utils'; -import { inject, postConstruct } from 'inversify'; -import { Vaild, NotBlank, NotNull } from '@cc-server/binding' +import { inject, postConstruct } from '@cc-server/ioc'; +import { Vaild, NotBlank, NotNull, controller, requestBody, httpGet, httpPost, requestParam } from '@cc-server/binding' +import { namespace, listener, interfaces, io, getSocketContext } from '@cc-server/ws' import { DBClient } from '@cc-server/db' import '@cc-server/db-mongo' @@ -21,9 +19,7 @@ class ExampleModel { email: string; } -type Model = ExampleModel - -@controller('') +@controller('/example') export class Controller { @inject(DBClient) private client: DBClient @@ -34,14 +30,14 @@ export class Controller { } @httpGet('/') - public async list(): Promise { + public async list(): Promise { return this.client.find({}); } @httpGet('/:id') public async get( @requestParam('id') id: string - ): Promise { + ): Promise { return this.client.findOneById(id); } @@ -56,8 +52,40 @@ export class Controller { @httpPost('/:id') public async update( @requestParam('id') id: string, - @requestBody() model: Model + @requestBody() model: ExampleModel ): Promise { 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; + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 08c1d9b..babb142 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,48 +1,76 @@ import 'reflect-metadata'; +import * as http from 'http' 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 { 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 { private _container: Container; - private _server: InversifyExpressServer; + private _server: http.Server; private _serverInstance: express.Application; + private _serverInversify: InversifyExpressServer; + private _wsServer: io.Server; + constructor(container?: Container) { this._container = container || new Container(); - this._container.load(buildProviderModule()); + this._serverInstance = express(); + this._server = http.createServer(this._serverInstance); // start the server - this._server = new InversifyExpressServer(this._container); - this._server.setConfig((app) => { + this._serverInversify = new InversifyExpressServer(this._container, null, null, this._serverInstance); + this._serverInversify.setConfig((app) => { app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); 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) { - this._server.setConfig(fn) + this._serverInversify.setConfig(fn) } public setErrorConfig(fn: interfaces.ConfigFunction) { - this._server.setErrorConfig(fn) + this._serverInversify.setErrorConfig(fn) } public build() { - this._serverInstance = this._server.build(); + this._container.load(buildProviderModule()); + this._serverInstance = this._serverInversify.build(); rebuildServer(this._container); + buildWebSocket(this._container, this._wsServer); return this._serverInstance; } public start(port: number = 80) { const routeInfo = getRouteInfo(this._container); console.log(prettyjson.render({ routes: routeInfo })); - this._serverInstance.listen(port); + this._server.listen(port); console.log(`Server started on port ${port} :)`); } } diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 7fd94ce..328f251 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -1,8 +1,10 @@ -import { CcServerBoot } from './index' +import { CcServerBoot, express } from './index' import './function/handle'; let server = new CcServerBoot(); +server.express.use(express.static('public')); + server.build(); server.start(); diff --git a/packages/db-mongo/package.json b/packages/db-mongo/package.json index 85edade..0cecefc 100644 --- a/packages/db-mongo/package.json +++ b/packages/db-mongo/package.json @@ -10,6 +10,7 @@ "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" }, diff --git a/packages/db/package.json b/packages/db/package.json index a39c700..6fb31d6 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -14,6 +14,7 @@ "url": "git+https://github.com/502647092/cc-server-parent.git" }, "scripts": { + "watch": "npx tsc --watch", "build": "rimraf dist && npx tsc", "test": "echo \"Error: run tests from root\" && exit 1" }, diff --git a/packages/ioc/package.json b/packages/ioc/package.json index ad3d9ea..04f848e 100644 --- a/packages/ioc/package.json +++ b/packages/ioc/package.json @@ -20,8 +20,7 @@ "dependencies": { "inversify": "^5.0.1", "inversify-binding-decorators": "^4.0.0", - "reflect-metadata": "^0.1.13", - "ts-node-dev": "^1.0.0-pre.40" + "reflect-metadata": "^0.1.13" }, "devDependencies": { "mocha": "^6.1.4", diff --git a/packages/ioc/src/index.ts b/packages/ioc/src/index.ts index cb22dd1..c5ad337 100644 --- a/packages/ioc/src/index.ts +++ b/packages/ioc/src/index.ts @@ -1,5 +1,5 @@ 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'; const provideNamed = (identifier: interfaces.ServiceIdentifier, name: string) => { @@ -14,4 +14,4 @@ const provideSingleton = (identifier: interfaces.ServiceIdentifier) => { .done(); }; -export { autoProvide, provide, provideNamed, provideSingleton, inject, buildProviderModule }; +export { autoProvide, provide, provideNamed, provideSingleton, Container, inject, injectable, postConstruct, buildProviderModule }; diff --git a/packages/ws/.gitignore b/packages/ws/.gitignore new file mode 100644 index 0000000..d856d44 --- /dev/null +++ b/packages/ws/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/dist +/package-lock.json +/yarn.lock diff --git a/packages/ws/.npmignore b/packages/ws/.npmignore new file mode 100644 index 0000000..b0eede3 --- /dev/null +++ b/packages/ws/.npmignore @@ -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 \ No newline at end of file diff --git a/packages/ws/README.md b/packages/ws/README.md new file mode 100644 index 0000000..4c9d928 --- /dev/null +++ b/packages/ws/README.md @@ -0,0 +1,11 @@ +# `cc-server-ws` + +> TODO: description + +## Usage + +``` +const ccServerIoc = require('cc-server-ws'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/ws/package.json b/packages/ws/package.json new file mode 100644 index 0000000..29e5f5f --- /dev/null +++ b/packages/ws/package.json @@ -0,0 +1,33 @@ +{ + "name": "@cc-server/ws", + "version": "0.3.3", + "description": "> TODO: description", + "author": "MiaoWoo ", + "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" +} \ No newline at end of file diff --git a/packages/ws/src/builder.ts b/packages/ws/src/builder.ts new file mode 100644 index 0000000..0ed3f37 --- /dev/null +++ b/packages/ws/src/builder.ts @@ -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(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(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) { + 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); + } + } + }); + } +} \ No newline at end of file diff --git a/packages/ws/src/constants.ts b/packages/ws/src/constants.ts new file mode 100644 index 0000000..0c63629 --- /dev/null +++ b/packages/ws/src/constants.ts @@ -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" +}; diff --git a/packages/ws/src/decorators.ts b/packages/ws/src/decorators.ts new file mode 100644 index 0000000..3fae20f --- /dev/null +++ b/packages/ws/src/decorators.ts @@ -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); + }; +} diff --git a/packages/ws/src/index.ts b/packages/ws/src/index.ts new file mode 100644 index 0000000..cf52c6d --- /dev/null +++ b/packages/ws/src/index.ts @@ -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 } diff --git a/packages/ws/src/interfaces.ts b/packages/ws/src/interfaces.ts new file mode 100644 index 0000000..c58007a --- /dev/null +++ b/packages/ws/src/interfaces.ts @@ -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; + } +} diff --git a/packages/ws/src/utils.ts b/packages/ws/src/utils.ts new file mode 100644 index 0000000..ab741a7 --- /dev/null +++ b/packages/ws/src/utils.ts @@ -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 +} diff --git a/packages/ws/tsconfig.json b/packages/ws/tsconfig.json new file mode 100644 index 0000000..7aae5d2 --- /dev/null +++ b/packages/ws/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "src", + "outDir": "dist" + } +} \ No newline at end of file