From 3715ce38764c1ac6003ec50273fe5a0807b5f76a Mon Sep 17 00:00:00 2001 From: Serhii Sol Date: Mon, 24 Apr 2017 16:28:32 +0200 Subject: [PATCH] Socket 2.0.0 (#43) * feat(socket): Refactoring for 2.0.0 * chore(socket): Fixed empty middleware issue && adjusted example accordingly * chore(socket): Small polishing, plus added WrapperClass option for IO Decorator --- package.json | 7 +- playground/socket/index.ts | 55 ++-- playground/socket/package.json | 5 +- socket/CHANGELOG.md | 14 +- socket/README.md | 74 ++--- socket/index.ts | 19 -- socket/package.json | 12 +- socket/src/decorators/class.ts | 76 ------ socket/src/decorators/controller.ts | 22 ++ socket/src/decorators/events.ts | 89 ++++++ socket/src/decorators/index.ts | 7 +- socket/src/decorators/middleware.ts | 39 +++ socket/src/decorators/param.ts | 60 ---- socket/src/decorators/params.ts | 61 +++++ socket/src/decorators/property.ts | 70 ----- socket/src/index.ts | 8 +- socket/src/interface.ts | 140 +++++++--- socket/src/meta.ts | 29 -- socket/src/socket.ts | 407 +++++++++++++++++----------- socket/src/utils.ts | 77 ++++++ socket/tsconfig.json | 22 ++ tsconfig.json | 3 +- tslint.json | 6 + 23 files changed, 758 insertions(+), 544 deletions(-) delete mode 100644 socket/index.ts delete mode 100644 socket/src/decorators/class.ts create mode 100644 socket/src/decorators/controller.ts create mode 100644 socket/src/decorators/events.ts create mode 100644 socket/src/decorators/middleware.ts delete mode 100644 socket/src/decorators/param.ts create mode 100644 socket/src/decorators/params.ts delete mode 100644 socket/src/decorators/property.ts delete mode 100644 socket/src/meta.ts create mode 100644 socket/src/utils.ts create mode 100644 socket/tsconfig.json diff --git a/package.json b/package.json index 7ebc77c..09314a7 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,15 @@ "description": "node decorators", "main": "index.js", "scripts": { - "compile": "tsc" + "compile": "tsc", + "clean-tsc": "find . -name \"*.js\" -type f -not -path \"./node_modules/*\" -delete && find . -name \"*.js.map\" -type f -not -path \"./node_modules/*\" -delete && find . -name \"*.d.ts\" -type f -not -path \"./node_modules/*\" -not -path \"./types/*\" -delete" }, "dependencies": { "co": "4.6.0", - "express": "4.14.0", - "socket.io": "1.7.2" + "express": "4.14.0" }, "devDependencies": { "@types/express": "4.0.34", - "@types/socket.io": "1.4.27", "typescript": "2.1.1" }, "keywords": [ diff --git a/playground/socket/index.ts b/playground/socket/index.ts index 46069a6..cceca98 100644 --- a/playground/socket/index.ts +++ b/playground/socket/index.ts @@ -1,55 +1,32 @@ import { listen } from 'socket.io'; import { - Middleware, - GlobalMiddleware, - ServerMiddleware, Event, + attachControllers, + Controller, + Socket, Args, - bootstrapSocketIO, - Namespace, - Socket + IO, + Ack } from '@decorators/socket'; const server = listen(3000); -class SocketWrapper { - constructor(private socket: SocketIO.Socket) {} - - log() { - console.log('Log'); +class Wrapper { + constructor(data: any) { + console.log('Wrapper'); } } -@ServerMiddleware((io, socket, next) => { - console.log('Global Server Middleware'); - next(); -}) -@GlobalMiddleware((io, socket, packet, next) => { - console.log('Global socket middleware'); - next(); -}) -@Middleware((io, socket, packet, next) => { - console.log('Controller based middleware'); - next(); -}) -@Namespace('/messaging') -class FirstController { - - constructor(...args) { - console.log(args); - } +@Controller('/') +class MessagingController { - @Event('message', (io, socket, packet, next) => { - console.log('Message middleware'); - next(); - }) - onMessage(@Args() message, @Socket(SocketWrapper) socket: SocketWrapper) { - socket.log(); - console.log(`Message: ${message}`); + @Event('message') + onMessage(@Args() message: string) { + console.log( + `Message: ${message}` + ); } } -bootstrapSocketIO(server, [ - { provide: FirstController, deps: [1, 2, 3]} -]); +attachControllers(server, [ MessagingController ]); diff --git a/playground/socket/package.json b/playground/socket/package.json index 66a1a5b..8b650b4 100644 --- a/playground/socket/package.json +++ b/playground/socket/package.json @@ -1,10 +1,11 @@ { "name": "@decorators/playground-socket", - "version": "1.3.3", + "version": "2.0.0", "description": "node decorators", "main": "index.js", "dependencies": { - "@decorators/socket": "../../socket" + "@decorators/socket": "../../socket", + "@types/socket.io": "1.4.29" }, "keywords": [ "nodejs", diff --git a/socket/CHANGELOG.md b/socket/CHANGELOG.md index 7596a01..c9b6c2d 100644 --- a/socket/CHANGELOG.md +++ b/socket/CHANGELOG.md @@ -1,3 +1,15 @@ +# Socket#2.0.0 +* Features + * Added wrap option for **@IO(WrapperClass?)** decorator, see **@Socket()** decorator for details +* Renamed + * **@Callback()** to **@Ack()** + * **@GlobalMiddleware()** for socket global middleware to **@Middleware()** + * Renamed **@Namespace()** to **@Controller(namespace: string, middleware?: Function | Function[])** +* Removed + * **@Middleware(middleware: Function | Function[])** - use Controller based middleware + * deprecated **bootstrapSocketIO(io: SocketIO.Server, Controllers)** - Attaches controllers to IO server - use **attachControllers()** instead + * deprecated **attachControllerToSocket(io: SocketIO.Server, socket: SocketIO.Socket, Controllers)** - Attaches controllers to Socket - use **attachControllersToSocket()** instead + # Socket#1.4.0 * Controller DI ```typescript @@ -71,7 +83,7 @@ class SocketWrapper { * **bootstrapSocketIO(rootController: Controller)** ##### Class * **@Connect(serverOrPort: number | string | HttpServer, opts?: any)** - * **@Middleware(fn: Function)** + * **@Middleware(fn: Function)** ##### Method * **@OnIO(event: string)** * **@OnConnect()** diff --git a/socket/README.md b/socket/README.md index e010eaf..3ce899f 100644 --- a/socket/README.md +++ b/socket/README.md @@ -1,27 +1,16 @@ ![Node Decorators](https://github.com/serhiisol/node-decorators/blob/master/decorators.png?raw=true) -Project implements decorators for modern tools for NodeJS like: -- [Socket.IO] +Project implements decorators for [Socket.IO] ### Installation ``` npm install @decorators/socket --save ``` ### API -#### Functions -* **attachControllers(io: SocketIO.Server, Controller[] || Injectable[])** - Attaches controllers to IO server -* **attachControllersToSocket(io: SocketIO.Server, socket: SocketIO.Socket, Controller[] || Injectable[])** - Attaches controllers to Socket -where Injectable: -```typescript -{ provide: UserController, deps: [UserService] } -``` -* **bootstrapSocketIO(io: SocketIO.Server, Controllers)** - Attaches controllers to IO server - **_[deprecated, use attachControllers - will be removed in v2.0.0 of this library]_** -* **attachControllerToSocket(io: SocketIO.Server, socket: SocketIO.Socket, Controllers)** - Attaches controllers to Socket - **_[deprecated, use attachControllersToSocket - will be removed in v2.0.0 of this library]_** - #### Decorators ##### Class -* **@Namespace(namespace: string)** - registers controller for namespace +* **@Controller(namespace?: string, middleware?: Function | Function[])** - registers controller for controller * **@ServerMiddleware(middleware: Function | Function[])** - registers global server (io) middleware ```typescript @@ -32,9 +21,7 @@ function middleware( ) {} ``` -* **@GlobalMiddleware(middleware: Function | Function[]) => {})** - registers socket global middleware -* **@Middleware(middleware: Function | Function[])** - registers controller-based middleware, -will handle only socket events registered in controller +* **@Middleware(middleware: Function | Function[]) => {})** - registers socket global middleware ```typescript function middleware( io: SocketIO.Server | SocketIO.Namespace, @@ -45,11 +32,11 @@ function middleware( ``` ##### Method -* **@GlobalEvent(event: string)** - register global event (**io.on**) * **@Connection()** - register **connection** listener (**io.on('connection', fn)**) * **@Disconnect()** - register disconnect socket event (**socket.on('disconnect', fn)**) +* **@GlobalEvent(event: string)** - register global event (**io.on**) -* **@Event(event: string, middleware || \[middleware\])** - register socket event (**socket.on**), +* **@Event(event: string, middleware?: Function | Function[])** - register socket event (**socket.on**), where middleware is a function which accepts four parameters: ```typescript function middleware( @@ -61,45 +48,60 @@ function middleware( ``` ##### Parameter -* **@IO()** - returns server itself -* **@Socket(WrapperClass?: Class)** - returns socket, if **WrapperClass** provided, returns instance -of **WrapperClass**, passes **socket** as dependency into **WrapperClass** +* **@IO(WrapperClass?: Class)** - returns server itself +* **@Socket(WrapperClass?: Class)** - returns socket + +If **WrapperClass** provided, returns instance +of **WrapperClass**, passes **socket** or **server** as dependency into **WrapperClass** + ```typescript class SocketWrapper { - constructor(private socket: SocketIO.Socket) {} + constructor(private ioSock: SocketIO.Server|SocketIO.Namespace|SocketIO.Socket) {} } ``` -* **@Args()** - returns event arguments (excluding callback)(if it exists) -* **@Callback()** - returns callback function (if it exists) + +* **@Args()** - returns event arguments (excluding callback, if it exists) +* **@Ack()** - returns ack callback function (if it exists) + +#### Helper Functions +* **attachControllers(io: SocketIO.Server, Controller[] || Injectable[])** - Attaches controllers to IO server +* **attachControllersToSocket(io: SocketIO.Server, socket: SocketIO.Socket, Controller[] || Injectable[])** - Attaches controllers to Socket + +where Injectable: +```typescript +{ provide: UserController, deps: [UserService] } +``` ### Details #### Middleware The middleware order : -* Global Server Middleware -* Global Socket middleware -* Controller based middleware -* Event based middleware -Additionally to this order depends on the order how you've registered appropriate types of middleware +* Global Server Middleware (**io.use(...)**) +* Global Socket middleware (**socket.use(...)**) +* Controller based middleware (**@Controller(...)**) +* Event based middleware (**@Event(...)**) + +Additionally to this order depends on the order how you've registered appropriate types of middleware ### Quick Example: ```typescript import { listen } from 'socket.io'; -import { Event, Args, bootstrapSocketIO, Namespace } from '@decorators/socket'; +import { Event, Args, attachControllers, Controller } from '@decorators/socket'; const server = listen(3000); -@Namespace('/messaging') +@Controller('/') class MessageController { + @Event('message') onMessage(@Args() message) { - console.log(`Message: ${message}`); + console.log( + `Message: ${message}` + ); } + } -bootstrapSocketIO(server, [ MessageController ]); +attachControllers(server, [ MessageController ]); ``` -### License -MIT - [Socket.IO]:http://socket.io/ diff --git a/socket/index.ts b/socket/index.ts deleted file mode 100644 index befc343..0000000 --- a/socket/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { - Namespace, - ServerMiddleware, - GlobalMiddleware, - Middleware, - Connection, - Disconnect, - GlobalEvent, - Event, - IO, - Socket, - Args, - Callback, - attachControllers, - attachControllersToSocket -} from './src'; - -/** @deprecated */ -export { bootstrapSocketIO, attachControllerToSocket } from './src'; diff --git a/socket/package.json b/socket/package.json index 00bf29b..c9737d7 100644 --- a/socket/package.json +++ b/socket/package.json @@ -1,10 +1,14 @@ { "name": "@decorators/socket", - "version": "1.4.0", - "description": "node decorators", - "main": "index.js", + "version": "2.0.0", + "description": "node decorators - decorators for socket.io library", + "main": "src/index.js", "dependencies": { - "socket.io": "1.7.2" + "socket.io": ">=1.7.3" + }, + "devDependencies": { + "@types/socket.io": "1.4.29", + "typescript": "2.2.2" }, "keywords": [ "nodejs", diff --git a/socket/src/decorators/class.ts b/socket/src/decorators/class.ts deleted file mode 100644 index 5383773..0000000 --- a/socket/src/decorators/class.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { SocketIOMeta } from '../interface'; -import { getMeta } from '../meta'; - -/** - * Defines namespace for the controller - * @param {string} namespace - */ -export let Namespace = (namespace: string): ClassDecorator => { - return (target: Function): void => { - let meta: SocketIOMeta = getMeta(target.prototype); - meta.namespace = namespace; - } -}; - -/** - * Registers global middleware - * @param {Function|Function[]} fn - * @returns {(target:Function)=>void} - * @constructor - */ -export let ServerMiddleware = (fn: Function|Function[]): ClassDecorator => { - return (target: Function): void => { - let meta: SocketIOMeta = getMeta(target.prototype); - let middleware: Function[]; - - if (typeof fn === 'function') { - middleware = [fn]; - } else if (Array.isArray(fn)){ - middleware = (fn).filter(md => typeof md === 'function'); - } - - meta.middleware.io.push(...middleware); - } -}; - -/** - * Registers socket middleware - * @param {Function} fn - * @returns {(target:Function)=>void} - * @constructor - */ -export let GlobalMiddleware = (fn: Function|Function[]): ClassDecorator => { - return (target: Function): void => { - let meta: SocketIOMeta = getMeta(target.prototype); - let middleware: Function[]; - - if (typeof fn === 'function') { - middleware = [fn]; - } else if (Array.isArray(fn)){ - middleware = (fn).filter(md => typeof md === 'function'); - } - - meta.middleware.socket.push(...middleware); - } -}; - -/** - * Registers socket middleware - * @param {Function} fn - * @returns {(target:Function)=>void} - * @constructor - */ -export let Middleware = (fn: Function|Function[]): ClassDecorator => { - return (target: Function): void => { - let meta: SocketIOMeta = getMeta(target.prototype); - let middleware: Function[]; - - if (typeof fn === 'function') { - middleware = [fn]; - } else if (Array.isArray(fn)){ - middleware = (fn).filter(md => typeof md === 'function'); - } - - meta.middleware.controller.push(...middleware); - } -}; diff --git a/socket/src/decorators/controller.ts b/socket/src/decorators/controller.ts new file mode 100644 index 0000000..531b68a --- /dev/null +++ b/socket/src/decorators/controller.ts @@ -0,0 +1,22 @@ +import { SocketMeta, MiddlewareType } from '../interface'; +import { getMeta, prepareMiddleware } from '../utils'; + +/** + * Defines namespace for the controller and controller-based middleware + * + * @param {string} [ns = '/'] + * @param {Function|Function[]} [fn] + */ +export const Controller = (ns = '/', fn?: Function|Function[]): ClassDecorator => { + return (target: Function): void => { + const meta: SocketMeta = getMeta(target.prototype); + const middleware: Function[] = prepareMiddleware(fn); + + meta.ns = ns; + + meta.middleware.push({ + middleware, + type: MiddlewareType.Controller + }); + }; +}; diff --git a/socket/src/decorators/events.ts b/socket/src/decorators/events.ts new file mode 100644 index 0000000..7ac17c7 --- /dev/null +++ b/socket/src/decorators/events.ts @@ -0,0 +1,89 @@ +import { SocketIOClass, SocketMeta, EventType } from '../interface'; +import { getMeta, prepareMiddleware } from '../utils'; + +/** + * Add listener to metadata + * @param target Target class + * @param {string} key Function name + * @param descriptor Function descriptor + * @param {string} type Listener type: io or socket + * @param {string} event Event name + * @param {Function | Function[]} middleware Event middleware + * @returns {Object} + */ +function addListener( + target: SocketIOClass, + method: string | symbol, + type: EventType, + event: string, + middleware: Function | Function[] +) { + const meta: SocketMeta = getMeta(target); + + meta.listeners.push({ + event, + type, + method, + middleware: prepareMiddleware(middleware) + }); +} + +/** + * Listener decorator factory, creates listener decorator + * @param {string} type Listener type: io or socket + * @param {string} event Event name + * @param {Function | Function[]} middleware Event middleware + */ +function makeDecorator( + type: EventType, + event: string, + middleware?: Function | Function[] +): MethodDecorator { + return (target: SocketIOClass, key: string | symbol, descriptor?: any) => { + addListener(target, key, type, event, middleware); + + return descriptor; + }; +} + +/** + * Register **connection** event + * + * @see **io.on('connection', fn)** + * @param {Function | Function[]} middleware Event middleware + */ +export function Connection(middleware?: Function | Function[]): MethodDecorator { + return makeDecorator(EventType.IO, 'connection', middleware); +} + +/** + * Registers global event + * + * @see **io.on** + * @param {string} event + * @param {Function | Function[]} middleware Event middleware + */ +export function GlobalEvent(event: string, middleware?: Function | Function[]): MethodDecorator { + return makeDecorator(EventType.IO, event, middleware); +} + +/** + * Registers **disconnect** listener + * + * @see **socket.on('disconnect', fn)** + * @param {Function | Function[]} middleware Event middleware + */ +export function Disconnect(middleware?: Function | Function[]): MethodDecorator { + return makeDecorator(EventType.Socket, 'disconnect', middleware); +} + +/** + * Registers socket event + * + * @see **socket.on** + * @param {string} event + * @param {Function | Function[]} middleware Event middleware + */ +export function Event(event: string, middleware?: Function | Function[]): MethodDecorator { + return makeDecorator(EventType.Socket, event, middleware); +}; diff --git a/socket/src/decorators/index.ts b/socket/src/decorators/index.ts index 2ec076c..9f35e7d 100644 --- a/socket/src/decorators/index.ts +++ b/socket/src/decorators/index.ts @@ -1,3 +1,4 @@ -export { Namespace, ServerMiddleware, GlobalMiddleware, Middleware } from './class'; -export { Connection, Disconnect, GlobalEvent, Event } from './property'; -export { IO, Socket, Args, Callback } from './param'; +export { Controller } from './controller'; +export { ServerMiddleware, Middleware } from './middleware'; +export { Connection, Disconnect, GlobalEvent, Event } from './events'; +export { IO, Socket, Args, Ack } from './params'; diff --git a/socket/src/decorators/middleware.ts b/socket/src/decorators/middleware.ts new file mode 100644 index 0000000..2ef085e --- /dev/null +++ b/socket/src/decorators/middleware.ts @@ -0,0 +1,39 @@ +import { SocketMeta, MiddlewareType } from '../interface'; +import { getMeta, prepareMiddleware } from '../utils'; + +/** + * Registers global middleware + * + * @param {Function|Function[]} fn + * @returns {(target:Function)=>void} + * @constructor + */ +export const ServerMiddleware = (fn: Function|Function[]): ClassDecorator => { + return (target: Function): void => { + const meta: SocketMeta = getMeta(target.prototype); + const middleware: Function[] = prepareMiddleware(fn); + + meta.middleware.push({ + middleware, + type: MiddlewareType.IO + }); + }; +}; + +/** + * Registers socket middleware + * @param {Function} fn + * @returns {(target:Function)=>void} + * @constructor + */ +export let Middleware = (fn: Function|Function[]): ClassDecorator => { + return (target: Function): void => { + const meta: SocketMeta = getMeta(target.prototype); + const middleware: Function[] = prepareMiddleware(fn); + + meta.middleware.push({ + middleware, + type: MiddlewareType.Socket + }); + }; +}; diff --git a/socket/src/decorators/param.ts b/socket/src/decorators/param.ts deleted file mode 100644 index 9c04cdb..0000000 --- a/socket/src/decorators/param.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ParameterType, SocketIOClass, ParameterConfiguration } from '../interface'; -import { getMeta } from '../meta'; - -/** - * Add parameter to metadata - * @param target Target Class - * @param propertyKey Function name, parameters owner - * @param { {index: number, type: number} } config Parameter configuration - */ -function addParameterMeta(target: SocketIOClass, propertyKey: string | symbol, config: ParameterConfiguration) { - let meta = getMeta(target); - if (!meta.params[propertyKey]) { - meta.params[propertyKey] = []; - } - meta.params[propertyKey].push(config); -} - -/** - * Parameter decorator factory, creates parameter decorator - * @param parameterType Parameter Type - * @returns { () => ParameterDecorator } - */ -function parameterDecoratorFactory(parameterType: ParameterType): () => ParameterDecorator { - return function(): ParameterDecorator { - return function(target: SocketIOClass, propertyKey: string | symbol, index: number) { - addParameterMeta(target, propertyKey, {index, type: parameterType}); - }; - }; -} - -/** - * Returns server itself - * @type { () => ParameterDecorator } - */ -export const IO = parameterDecoratorFactory(ParameterType.IO); - -/** - * Returns socket - * @param WrapperClass Class, that will get plain socket object as dependency to add new functionality on top of standard one - * @type { (WrapperClass?: any) => ParameterDecorator } - */ -export const Socket = function(WrapperClass?: any): ParameterDecorator { - return function(target: SocketIOClass, propertyKey: string | symbol, index: number) { - addParameterMeta(target, propertyKey, { - index, type: ParameterType.Socket, data: WrapperClass - }); - }; -}; - -/** - * Returns event arguments (excluding callback)(if it exists) - * @type { () => ParameterDecorator } - */ -export const Args = parameterDecoratorFactory(ParameterType.Args); - -/** - * Returns callback function (if it exists) - * @type { () => ParameterDecorator } - */ -export const Callback = parameterDecoratorFactory(ParameterType.Callback); diff --git a/socket/src/decorators/params.ts b/socket/src/decorators/params.ts new file mode 100644 index 0000000..829330f --- /dev/null +++ b/socket/src/decorators/params.ts @@ -0,0 +1,61 @@ +import { SocketMeta, ParameterType, SocketIOClass } from '../interface'; +import { getMeta } from '../utils'; + +/** + * Add parameter to metadata + * + * @param {SocketIOClass} target Target Class + * @param {string|symbol} key Function name, parameters owner + * @param {ParameterType} type + * @param {number} index + * @param {*} [data] + */ +function addParameter( + target: SocketIOClass, + method: string | symbol, + type: ParameterType, + index: number, + data?: any +) { + const meta: SocketMeta = getMeta(target); + + meta.params.push({ type, method, index, data }); +} + +/** + * Parameter decorator factory, creates parameter decorator + * @param parameterType Parameter Type + * @returns { (WrapperClass?: any) => ParameterDecorator } + */ +function makeDecorator(parameterType: ParameterType): (WrapperClass?: any) => ParameterDecorator { + return (WrapperClass?: any): ParameterDecorator => { + return (target: SocketIOClass, key: string | symbol, index: number) => { + addParameter(target, key, parameterType, index, WrapperClass); + }; + }; +} + +/** + * Returns server itself + * @type { () => ParameterDecorator } + */ +export const IO = makeDecorator(ParameterType.IO); + +/** + * Returns socket + * @param WrapperClass Class, that will get plain socket object as dependency to add new functionality on top of standard one + * @type {(WrapperClass?: any) => ParameterDecorator} + */ +export const Socket = makeDecorator(ParameterType.Socket); + +/** + * Returns event arguments (excluding callback)(if it exists) + * @type {() => ParameterDecorator} + */ +export const Args = makeDecorator(ParameterType.Args); + +/** + * Returns ack callback function (if it exists) + * @type {() => ParameterDecorator} + */ +export const Ack = makeDecorator(ParameterType.Ack); diff --git a/socket/src/decorators/property.ts b/socket/src/decorators/property.ts deleted file mode 100644 index e512549..0000000 --- a/socket/src/decorators/property.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { SocketIOClass, SocketIOMeta } from '../interface'; -import { getMeta } from '../meta'; - -/** - * Add listener to metadata - * @param target Target class - * @param {string} key Function name - * @param descriptor Function descriptor - * @param {string} type Listener type: io or socket - * @param {string} event Event name - * @param {Function | Function[]} middleware Event middleware - * @returns {Object} - */ -function addListenerMeta( - target: SocketIOClass, - key: string | symbol, - descriptor: any, - type: string, - event: string, - middleware: Function | Function[] -): Object { - let meta: SocketIOMeta = getMeta(target); - meta.listeners[type][key] = { - event, - middleware: typeof middleware === 'function' ? [middleware] : middleware || [] - }; - if (type === 'socket') { - meta.listeners.all.push(event); - } - return descriptor; -} - -/** - * Listener decorator factory, creates listener decorator - * @param {string} type Listener type: io or socket - * @param {string} event Event name - * @param {Function | Function[]} middleware Event middleware - */ -let listenerDecoratorFactory = (type: string, event: string, middleware?: Function | Function[]): MethodDecorator => { - return (target: SocketIOClass, key: string | symbol, descriptor?: any) => { - return addListenerMeta(target, key, descriptor, type, event, middleware) - }; -}; - -/** - * @alias {OnConnect} - * @type {()=>MethodDecorator} - */ -export let Connection = (): MethodDecorator => listenerDecoratorFactory('io', 'connection'); - -/** - * register **disconnect** listener (**socket.on('disconnect', fn)**) - * @constructor - */ -export let Disconnect = (): MethodDecorator => listenerDecoratorFactory('socket', 'disconnect'); - -/** - * Register global event (**io.on**) - * @param {string} event - * @constructor - */ -export let GlobalEvent = (event: string): MethodDecorator => listenerDecoratorFactory('io', event); - -/** - * Register socket event (**socket.on**); - * @param {string} event - * @param {Function | Function[]} middleware Event middleware - * @constructor - */ -export let Event = (event: string, middleware?: Function | Function[]): MethodDecorator => listenerDecoratorFactory('socket', event, middleware); diff --git a/socket/src/index.ts b/socket/src/index.ts index 116ec7c..d8d99f3 100644 --- a/socket/src/index.ts +++ b/socket/src/index.ts @@ -1,7 +1,6 @@ export { - Namespace, + Controller, ServerMiddleware, - GlobalMiddleware, Middleware, Connection, Disconnect, @@ -10,9 +9,6 @@ export { IO, Socket, Args, - Callback + Ack } from './decorators'; export { attachControllers, attachControllersToSocket } from './socket'; - -/** @deprecated */ -export { bootstrapSocketIO, attachControllerToSocket } from './socket'; diff --git a/socket/src/interface.ts b/socket/src/interface.ts index 112e3e8..c316c8c 100644 --- a/socket/src/interface.ts +++ b/socket/src/interface.ts @@ -1,57 +1,135 @@ -export interface ParameterConfiguration { - index: number; - type: any; - name?: string; - data?: any; +/** + * All possible middleware types + * + * @export + * @enum {number} + */ +export enum MiddlewareType { + IO, + Socket, + Controller } -export interface Params { - [key: string]: ParameterConfiguration[]; +/** + * Middleware + * + * @export + * @interface Middleware + */ +export interface Middleware { + type: MiddlewareType; + middleware: Function[]; } /** * Parameter types enum + * + * @export + * @enum {number} */ export enum ParameterType { IO, Socket, Args, - Callback + Ack +} + +/** + * Registered params + * + * @export + * @interface Param + */ +export interface Param { + method: string | symbol; + type: ParameterType; + index: number; + data?: any; +} + +/** + * Event types + * + * @export + * @enum {number} + */ +export enum EventType { + IO, + Socket } +/** + * Event listener + * + * @export + * @interface Listener + */ export interface Listener { - [key: string]: { - event: string; - middleware: Function[]; - }; + event: string; + type: EventType; + method: string | symbol; + middleware: Function[]; } -export interface SocketIOMeta { - serverOrPort: any; - options: any; - namespace: string; +/** + * Metadata class + * + * @export + * @class Meta + */ +export class SocketMeta { + /** + * Namespace + */ + ns: string; - middleware: { - io: Function[]; - socket: Function[]; - controller: Function[]; - } + /** + * Middleware + */ + middleware: Middleware[] = []; - listeners: { - all: string[], - io: Listener; - socket: Listener; - }; + /** + * Event listeners + */ + listeners: Listener[] = []; + + /** + * Event listener params + */ + params: Param[] = []; +} + +/** + * Socket IO Class + * + * @export + * @interface SocketIOClass + * @extends {Object} + */ +export interface SocketIOClass extends Object { + __socket_meta__: SocketMeta; - params: Params; + new (...deps: any[]); } +/** + * Injectable + * + * @export + * @interface Injectable + */ export interface Injectable { - provide: Function; + provide: SocketIOClass; deps: any[]; } -export interface SocketIOClass extends Object { - __meta__: SocketIOMeta; - new (...deps: any[]); +/** + * Artifacts + * + * @export + * @interface DecoratorsArtifacts + */ +export interface DecoratorsArtifacts { + controller: SocketIOClass; + meta: SocketMeta; } diff --git a/socket/src/meta.ts b/socket/src/meta.ts deleted file mode 100644 index c616de1..0000000 --- a/socket/src/meta.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { SocketIOClass, SocketIOMeta } from './interface'; - -/** - * Get or initiate metadata on target - * @param target - * @returns {SocketIOMeta} - */ -export function getMeta(target: SocketIOClass): SocketIOMeta { - if (!target.__meta__) { - target.__meta__ = { - serverOrPort: undefined, - options: undefined, - namespace: '/', - middleware: { - io: [], - socket: [], - controller: [] - }, - listeners: { - all: [], - io: {}, - socket: {} - }, - params: {} - }; - } - - return target.__meta__; -} diff --git a/socket/src/socket.ts b/socket/src/socket.ts index b804787..ac18d68 100644 --- a/socket/src/socket.ts +++ b/socket/src/socket.ts @@ -1,238 +1,321 @@ import { + MiddlewareType, + Middleware, ParameterType, + Param, + EventType, Listener, - SocketIOMeta, + SocketMeta, Injectable, SocketIOClass, - ParameterConfiguration, - Params + DecoratorsArtifacts } from './interface'; +import { getMeta, noop, loopFns } from './utils'; /** - * Dummy function to ensure, that callback exists + * Attaches controllers to IO server + * + * @param {SocketIO.Server} io + * @param {Array} injectables */ -function noop() {} +export function attachControllers( + io: SocketIO.Server, + injectables: any[] +) { + injectables.forEach((injectable: Injectable | SocketIOClass) => { + const controller: SocketIOClass = + (injectable).provide || injectable; + const deps: any[] = (injectable).deps || []; + + attachController(io, controller, deps); + }); +} /** - * Get original socket, or create instance of passed WrapperClass (data) - * @param {ParameterConfiguration} item + * Attach Controller to already existed socket io server + * With this approach you can't define global middleware, it's up to you. + * + * @param {SocketIO.Server|SocketIO.Namespace} io * @param {SocketIO.Socket} socket - * @returns {SocketIO.Socket} + * @param {Array} injectables */ -function getSocket(item: ParameterConfiguration, socket: SocketIO.Socket) { - return item.data ? new item.data(socket) : socket; +export function attachControllersToSocket( + io: SocketIO.Server|SocketIO.Namespace, + socket: SocketIO.Socket, + injectables: any[] +) { + injectables.forEach((injectable: Injectable | SocketIOClass) => { + const Controller: SocketIOClass = + (injectable).provide || injectable; + const deps: any[] = (injectable).deps || []; + + attachControllerToSocket(io, socket, getArtifacts(Controller, deps)); + }); } /** - * Extract parameters for new handler - * @param io - * @param socket - * @param params Meta parameters - * @param eventArgs incoming arguments - * @returns {Array} returns arguments array, or io and socket, event args (default) by default + * Attach Controller + * + * @param {SocketIO.Server} IO + * @param {SocketIOClass} Controller + * @param {any[]} deps */ -function extractParameters( - io: SocketIO.Server | SocketIO.Namespace, - socket: SocketIO.Socket, - params: ParameterConfiguration[], - eventArgs -): any[] { - let args = []; +function attachController( + IO: SocketIO.Server, + Controller: SocketIOClass, + deps: any[] +) { + const artifacts = getArtifacts(Controller, deps); + const io: SocketIO.Namespace = IO.of(artifacts.meta.ns); + const controller: SocketIOClass = artifacts.controller; + /** - * if no parameters provided, return io and socket, and event arguments (which came for default handler) + * Filter all registered middleware and find IO middleware + * Apply all middleware functions to io */ - if (!params || !params.length) { - return [io, socket, ...eventArgs]; - } + findMiddleware(artifacts, MiddlewareType.IO) + .forEach((fn: Function) => + io.use((...args) => fn.call(controller, io, ...args)) + ); + + /** + * Apply io based events + */ + applyEvents(io, null, artifacts, EventType.IO); /** - * Callback function - * @type {Function} + * Apply local listeners (socket based) */ - let callback: Function; + io.on('connection', (socket: SocketIO.Socket) => { + attachControllerToSocket(io, socket, artifacts); + }); +} + +/** + * Attach controller to socket + * + * @param {SocketIO.Server|SocketIO.Namespace} io + * @param {SocketIO.Socket} socket + * @param {DecoratorsArtifacts} artifacts + */ +function attachControllerToSocket( + io: SocketIO.Server|SocketIO.Namespace, + socket: SocketIO.Socket, + artifacts: DecoratorsArtifacts +) { + const controller: SocketIOClass = artifacts.controller; + /** - * extract callback function, it it exists + * Apply all registered global socket based middleware (@Middleware) */ - if (eventArgs.length && typeof eventArgs[eventArgs.length - 1] === 'function') { - callback = eventArgs.pop(); - } + findMiddleware(artifacts, MiddlewareType.Socket) + .forEach((fn: Function) => { + (socket).use((...args) => fn.call(controller, io, socket, ...args)); + }); /** - * loop through all params and put them into correct order + * Apply all registered controller-based and event-based middlewares to socket + */ + (socket).use(socketMiddlewareHandler(io, socket, artifacts)); + + /** + * Apply socket based events */ - for (let item of params) { - switch (item.type) { - default: args[item.index] = getSocket(item, socket); break; // socket - case ParameterType.IO: args[item.index] = io; break; - case ParameterType.Args: args[item.index] = eventArgs.pop(); break; - case ParameterType.Callback: args[item.index] = callback || noop; break; + applyEvents(io, socket, artifacts, EventType.Socket); +} + +/** + * Handler for all registered controller based middleware + * + * @param {SocketIO.Server|SocketIO.Namespace} io + * @param {SocketIO.Socket} socket + * @param {DecoratorsArtifacts} artifacts + * @returns {(packet, next) => void} + */ +function socketMiddlewareHandler( + io: SocketIO.Server | SocketIO.Namespace, + socket: SocketIO.Socket, + artifacts: DecoratorsArtifacts +): (packet, next) => void { + const socketMds = findMiddleware(artifacts, MiddlewareType.Controller); + + return (packet, next) => { + const [event]: [string] = packet; + const args = [io, socket, packet]; + /** + * Find listener that serves this event + */ + const listener = artifacts.meta.listeners + .find((lst: Listener) => lst.event === event); + + /** + * And if listener exists, loop through controller middlewares + * and after that event (registered in listener) middlewares + */ + if (listener) { + return loopFns(socketMds, args) + .then(() => loopFns(listener.middleware, args)) + .then(() => next()) + .catch(err => next(err)); } - } - return args; + next(); + }; } /** * Apply listeners to socket or io - * @param io - * @param socket - * @param controller - * @param {Listener} listeners object with registered listeners - * @param {Params} params object with registered params for specific listener + * + * @param {SocketIO.Server|SocketIO.Namespace} io + * @param {SocketIO.Socket} socket + * @param {DecoratorsArtifacts} artifacts + * @param {EventType} type */ -function applyListeners( +function applyEvents( io: SocketIO.Server | SocketIO.Namespace, socket: SocketIO.Socket, - controller, - listeners: Listener, - params: Params + artifacts: DecoratorsArtifacts, + type: EventType ) { - for (let listener of Object.keys(listeners)) { - if (socket) { - listeners[listener].middleware.forEach((middleware: Function) => { - (socket).use((packet, next) => { - if (packet[0] === listeners[listener].event) { - return middleware(io, socket, packet, next); - } - next(); - }); - }); - } + const ioSock: any = socket || io; - (socket || io).on(listeners[listener].event, (...args) => { - let handlerArgs = extractParameters(io, (socket || args[0]), params[listener], args); - return controller[listener].apply(controller, handlerArgs); + artifacts.meta.listeners + .filter((listener: Listener) => listener.type === type) + .forEach((listener: Listener) => { + ioSock.on(listener.event, makeEventListener(io, socket, artifacts, listener.method)); }); - } } /** - * Get artifacts, instantiates controller and extract meta data - * @param Controller - * @param deps - * @returns { {controller, meta: SocketIOMeta, listeners: {io: Listener, socket: Listener}, params: Params} } + * Makes new event listeners + * + * @param {SocketIO.Server|SocketIO.Namespace} io + * @param {SocketIO.Socket} Socket + * @param {DecoratorsArtifacts} artifacts + * @param {string|symbol} method Handler name + * + * @returns {Function} Returns new handler, which serves event, extracts + * proper arguments and executes original function */ -function getArtifacts(Controller, deps) { - const controller = new Controller(...deps); - const meta: SocketIOMeta = controller.__meta__; - const namespace: string = meta.namespace; - const listeners = meta.listeners; - const params = meta.params; +function makeEventListener( + io: SocketIO.Server | SocketIO.Namespace, + socket: SocketIO.Socket, + artifacts: DecoratorsArtifacts, + method: string | symbol +): Function { + const controller: SocketIOClass = artifacts.controller; + const params: Param[] = artifacts.meta.params + .filter((param: Param) => param.method === method); - return { controller, meta, listeners, params, namespace }; + return function(...args) { + const newArgs: any[] = mapArguments(io, socket, params, args); + + return controller[method].apply(controller, newArgs); + }; } /** - * Attach controller to socket - * @param io - * @param socket - * @param artifacts + * Map parameters for new handler + * + * @param {SocketIO.Server|SocketIO.Namespace} io + * @param {SocketIO.Socket} socket + * @param {Param[]} params Meta parameters + * @param {any[]} args Event handler arguments + * @returns {Array} returns arguments array, or io and socket, event args by default */ -function _attachControllerToSocket(io, socket, artifacts) { +function mapArguments( + io: SocketIO.Server | SocketIO.Namespace, + socket: SocketIO.Socket, + params: Param[], + args: any[] +): any[] { /** - * Apply all registered middleware to socket + * if no parameters provided, return io and socket, and event arguments + * (which came for default handler) */ - artifacts.meta.middleware.socket.forEach(middleware => { - (socket).use((...args) => middleware.apply(middleware, [io, socket, ...args])); - }); + if (!params || !params.length) { + return [io, socket, ...args]; + } + + const ack: Function = getAck(args); /** - * Apply all registered controller-based middleware to socket + * loop through all params and put them into correct order */ - artifacts.meta.middleware.controller.forEach(middleware => { - (socket).use((packet, next) => { - if (artifacts.meta.listeners.all.indexOf(packet[0]) !== -1) { - return middleware.apply(middleware, [io, socket, packet, next]); + return params + .sort((param: Param) => param.index) + .map((param: Param) => { + switch (param.type) { + case ParameterType.IO: return getWrapper(param, io); + case ParameterType.Socket: return getWrapper(param, socket); + case ParameterType.Args: return getArgs(args); + default: return ack; } - next(); }); - }); - - /** - * Apply socket listeners (socket based) - */ - applyListeners( - io, - socket, - artifacts.controller, - artifacts.listeners.socket, - artifacts.params - ); } /** - * Attach Controller - * @param io - * @param Controller + * Iteratee through middleware and find proper ones + * + * @param {Middleware[]} middleware + * @param {MiddlewareType} type + * @returns {Function[]} */ -function attachController(io: SocketIO.Server, Controller, deps) { - const artifacts = getArtifacts(Controller, deps); - const _io: SocketIO.Namespace = io.of(artifacts.namespace); - - /** - * Apply all registered global middleware to io - */ - artifacts.meta.middleware.io.forEach(middleware => { - _io.use((...args) => middleware.apply(middleware, [_io, ...args])); - }); - - /** - * Apply global listeners (io based) - */ - applyListeners(_io, null, artifacts.controller, artifacts.listeners.io, artifacts.params); - - /** - * Apply local listeners (socket based) - */ - _io.on('connection', (socket) => _attachControllerToSocket(_io, socket, artifacts)); +function findMiddleware( + artifacts: DecoratorsArtifacts, + type: MiddlewareType +): Function[] { + return artifacts.meta.middleware + .filter((md: Middleware) => md.type === type) + .reduce((acc: Function[], md: Middleware) => + [...acc, ...md.middleware], [] + ); } /** - * Attaches controllers to IO server - * @param {SocketIO.Server} io - * @param {Array} injectables + * Get artifacts, instantiates controller and extract meta data + * + * @param {SocketIOClass} Controller + * @param {any[]} deps + * @returns {DecoratorsArtifacts} */ -export function attachControllers(io: SocketIO.Server, injectables: Array) { - injectables - .forEach((injectable: Injectable | SocketIOClass) => { - const controller = (injectable).provide || injectable; - const deps = (injectable).deps || []; +function getArtifacts(Controller: SocketIOClass, deps: any[]): DecoratorsArtifacts { + const controller = new Controller(...deps); + const meta: SocketMeta = getMeta(controller); - attachController(io, controller, deps); - }); + return { controller, meta }; } /** - * Attach Controller to already existed socket io server - * With this approach you can't define global middleware, it's up to you. - * @param {SocketIO.Server} io - * @param {SocketIO.Socket} socket - * @param {Array} injectables + * Get ack callback function + * + * @description extract callback function, it it exists + * @param {any[]} args Event arguments, passed to handler function */ -export function attachControllersToSocket( - io: SocketIO.Server, - socket: SocketIO.Socket, - injectables: Array -) { - injectables - .forEach((injectable: Injectable | SocketIOClass) => { - const controller = (injectable).provide || injectable; - const deps = (injectable).deps || []; +function getAck(args: any[]) { + const ackExists: boolean = typeof args[args.length - 1] === 'function'; - _attachControllerToSocket(io, socket, getArtifacts(controller, deps)); - }); + return ackExists ? args.pop() : noop; } /** - * @alias - * @see attachControllers - * @deprecated + * Get original socket or io server, or create instance of passed WrapperClass (data) + * + * @param {ParameterConfiguration} item + * @param {SocketIO.Server|SocketIO.Namespace|SocketIO.Socket} ioSock + * @returns {SocketIO.Socket} */ -export let bootstrapSocketIO = attachControllers; +function getWrapper(item: Param, ioSock: SocketIO.Server|SocketIO.Namespace|SocketIO.Socket) { + return item.data ? new item.data(ioSock) : ioSock; +} /** - * @alias - * @see attachControllersToSocket - * @deprecated + * Get proper message data + * + * @param {any[]} args + * @returns {*} */ -export let attachControllerToSocket = attachControllersToSocket; +function getArgs(args: any[]): any { + return typeof args[args.length - 1] === 'function' ? + args[args.length - 2] : args[args.length - 1]; +} diff --git a/socket/src/utils.ts b/socket/src/utils.ts new file mode 100644 index 0000000..9d07ace --- /dev/null +++ b/socket/src/utils.ts @@ -0,0 +1,77 @@ +import { SocketMeta, SocketIOClass } from './interface'; + +/** + * Get or initiate metadata on target + * @param target + * @returns {SocketIOMeta} + */ +export function getMeta(target: SocketIOClass): SocketMeta { + if (!target.__socket_meta__) { + target.__socket_meta__ = new SocketMeta(); + } + + return target.__socket_meta__; +} + +/** + * Check middleware and return array of middlewares by default + * + * @export + * @param {(Function|Function[])} fn + * @returns {Function[]} + */ +export function prepareMiddleware(fn: Function|Function[]): Function[] { + if (typeof fn === 'function') { + return [ fn ]; + } else if (Array.isArray(fn)) { + return (fn).filter(md => typeof md === 'function'); + } + + return []; +} + +/** + * Loops through all registered middlewares + * + * @description middleware approach + * @param {Function[]} fns Array of middleware functions + * @param {any[]} [args = []] Arguments to pass in + * @returns {Promise} + */ +export function loopFns(fns: Function[], args: any[] = []): Promise { + function iteratee(done: (err: Error) => void, i = 0) { + const fn = fns[i]; + + try { + fn(...args, (err, res) => { + if (err) { + return done(err); + } + + if (i === fns.length - 1) { + return done(null); + } + + iteratee(done, ++i); + }); + } catch(e) { + done(e); + } + + } + + return new Promise((resolve, reject) => { + + if (!Array.isArray(fns) || !fns.length) { + return resolve(); + } + + iteratee((err: Error) => err ? reject(err) : resolve()); + + }); +} + +/** + * Dummy empty function, to ensure that callback exists + */ +export function noop() {} diff --git a/socket/tsconfig.json b/socket/tsconfig.json new file mode 100644 index 0000000..1099156 --- /dev/null +++ b/socket/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": false, + "rootDir": ".", + "sourceMap": true, + "target": "es6", + "inlineSources": true, + "typeRoots": ["node_modules/@types"], + "pretty": true + }, + "exclude": ["node_modules"], + "files": [ + "src/index.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 284c2b5..eba20ae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,6 @@ ], "files": [ "co/index.ts", - "express/index.ts", - "socket/index.ts" + "express/index.ts" ] } diff --git a/tslint.json b/tslint.json index d3f6324..59d0f1c 100644 --- a/tslint.json +++ b/tslint.json @@ -2,6 +2,9 @@ "extends": "tslint:latest", "rules": { "class-name": true, + "max-classes-per-file": [ + 0 + ], "comment-format": [ true, "check-space" @@ -45,6 +48,9 @@ "no-switch-case-fall-through": true, "no-trailing-whitespace": true, "no-unused-expression": true, + "no-unused-variable": [ + true + ], "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false,