feat: add websocket model

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

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"
}
}