Repository: typestack/socket-controllers Branch: develop Commit: efb981d1d9d0 Files: 119 Total size: 158.7 KB Directory structure: gitextract_1if_z9rq/ ├── .github/ │ ├── dependabot.yml │ ├── semantic.yml │ └── workflows/ │ ├── auto-approve-dependabot-workflow.yml │ ├── continuous-deployment-workflow.yml │ ├── continuous-integration-workflow.yml │ └── lock-closed-issues-workflow.yml ├── .gitignore ├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── eslint.config.mjs ├── jest.config.js ├── jest.setup.js ├── package.json ├── sample/ │ ├── sample1-simple-controller/ │ │ ├── Message.ts │ │ ├── MessageController.ts │ │ ├── app.ts │ │ └── index.html │ ├── sample2-use-created-socket-io/ │ │ ├── Message.ts │ │ ├── MessageController.ts │ │ ├── app.ts │ │ └── index.html │ ├── sample3-namespaces/ │ │ ├── Message.ts │ │ ├── MessageController.ts │ │ ├── app.ts │ │ └── index.html │ ├── sample4-emitters/ │ │ ├── Message.ts │ │ ├── MessageController.ts │ │ ├── app.ts │ │ └── index.html │ ├── sample5-middlewares/ │ │ ├── AuthenticationMiddleware.ts │ │ ├── Message.ts │ │ ├── MessageController.ts │ │ ├── app.ts │ │ └── index.html │ └── sample6-dynamic-namespaces/ │ ├── Message.ts │ ├── MessageController.ts │ ├── app.ts │ └── index.html ├── src/ │ ├── SocketControllers.ts │ ├── decorators/ │ │ ├── ConnectedSocket.ts │ │ ├── EmitOnFail.ts │ │ ├── EmitOnSuccess.ts │ │ ├── MessageAck.ts │ │ ├── MessageBody.ts │ │ ├── Middleware.ts │ │ ├── NspParam.ts │ │ ├── NspParams.ts │ │ ├── OnConnect.ts │ │ ├── OnDisconnect.ts │ │ ├── OnDisconnecting.ts │ │ ├── OnMessage.ts │ │ ├── SkipEmitOnEmptyResult.ts │ │ ├── SocketController.ts │ │ ├── SocketIO.ts │ │ ├── SocketId.ts │ │ ├── SocketQueryParam.ts │ │ ├── SocketRequest.ts │ │ ├── SocketRooms.ts │ │ └── UseInterceptor.ts │ ├── index.ts │ ├── types/ │ │ ├── ActionMetadata.ts │ │ ├── ActionTransformOptions.ts │ │ ├── ControllerMetadata.ts │ │ ├── HandlerMetadata.ts │ │ ├── InterceptorInterface.ts │ │ ├── MiddlewareInterface.ts │ │ ├── MiddlewareMetadata.ts │ │ ├── ParameterMetadata.ts │ │ ├── ResultMetadata.ts │ │ ├── SocketControllerMetaKey.ts │ │ ├── SocketControllersOptions.ts │ │ ├── SocketEventContext.ts │ │ ├── TransformOptions.ts │ │ ├── constants/ │ │ │ └── defaultTransformOptions.ts │ │ └── enums/ │ │ ├── HandlerType.ts │ │ ├── ParameterType.ts │ │ ├── ResultType.ts │ │ └── SocketEventType.ts │ └── util/ │ ├── add-action-to-controller-metadata.ts │ ├── add-controller-metadata.ts │ ├── add-interceptor-to-action-metadata.ts │ ├── add-middleware-metadata.ts │ ├── add-parameter-to-action-metadata.ts │ ├── add-result-to-action-metadata.ts │ ├── chain-execute.ts │ └── get-metadata.ts ├── test/ │ ├── functional/ │ │ ├── connected-socket.spec.ts │ │ ├── controllers/ │ │ │ ├── test.controller.ts │ │ │ └── test2.controller.ts │ │ ├── create-socket-server.spec.ts │ │ ├── emit-on-fail.spec.ts │ │ ├── emit-on-success.spec.ts │ │ ├── load-controllers-from-directory.spec.ts │ │ ├── middlewares.spec.ts │ │ ├── multiple-controllers-on-same-namespace.spec.ts │ │ ├── nsp-param.spec.ts │ │ ├── nsp-params.spec.ts │ │ ├── on-disconnect.spec.ts │ │ ├── on-disconnecting.spec.ts │ │ ├── parameter-transformation.spec.ts │ │ ├── scoped-controllers.spec.ts │ │ ├── skip-emit-on-empty-result.spec.ts │ │ ├── socket-id.spec.ts │ │ ├── socket-io.spec.ts │ │ ├── socket-message-ack.spec.ts │ │ ├── socket-message-body.spec.ts │ │ ├── socket-query-param.spec.ts │ │ ├── socket-request.spec.ts │ │ ├── socket-rooms.spec.ts │ │ └── use-interceptor.spec.ts │ └── utilities/ │ ├── testSocketConnection.ts │ ├── waitForEvent.ts │ └── waitForTime.ts ├── tsconfig.json ├── tsconfig.prod.json └── tsconfig.spec.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: "10:00" timezone: Europe/Budapest open-pull-requests-limit: 5 versioning-strategy: increase commit-message: prefix: build include: scope ignore: - dependency-name: "husky" - dependency-name: "socket.io" - dependency-name: "socket.io-client" ================================================ FILE: .github/semantic.yml ================================================ titleAndCommits: true allowMergeCommits: false scopes: - deps - deps-dev types: - feat - fix - docs - style - refactor - perf - test - build - ci - chore - revert - merge ================================================ FILE: .github/workflows/auto-approve-dependabot-workflow.yml ================================================ name: Dependabot auto-merge on: pull_request_target jobs: dependabot: runs-on: ubuntu-latest if: github.actor == 'dependabot[bot]' steps: - name: 'Auto approve PR by Dependabot' uses: hmarr/auto-approve-action@v2.0.0 with: github-token: "${{ secrets.TYPESTACK_BOT_TOKEN }}" - name: 'Comment merge command' uses: actions/github-script@v3 with: github-token: ${{secrets.TYPESTACK_BOT_TOKEN }} script: | await github.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: '@dependabot squash and merge' }) ================================================ FILE: .github/workflows/continuous-deployment-workflow.yml ================================================ name: CD on: release: types: [created] jobs: publish: name: Publish to NPM runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 'lts/*' registry-url: https://registry.npmjs.org - run: npm ci --ignore-scripts - run: npm run prettier:check - run: npm run lint:check - run: npm run test:ci - run: npm run build - run: cp LICENSE build/LICENSE - run: cp README.md build/README.md - run: jq 'del(.devDependencies) | del(.scripts)' package.json > build/package.json - run: npm publish ./build env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} ================================================ FILE: .github/workflows/continuous-integration-workflow.yml ================================================ name: CI on: [push, pull_request] jobs: checks: name: Linters runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 'lts/*' - run: npm ci --ignore-scripts - run: npm run prettier:check - run: npm run lint:check tests: name: Tests runs-on: ubuntu-latest strategy: matrix: node-version: ['lts/*', 'current'] fail-fast: false steps: - uses: actions/checkout@v3 - name: Setting up Node.js (v${{ matrix.node-version }}.x) uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm ci --ignore-scripts - run: npm run test:ci - run: npm install codecov -g if: ${{ matrix.node-version == 'current' }} - run: codecov -f ./coverage/clover.xml -t ${{ secrets.CODECOV_TOKEN }} --commit=$GITHUB_SHA --branch=${GITHUB_REF##*/} if: ${{ matrix.node-version == 'current' }} build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 'lts/*' - run: npm ci --ignore-scripts - run: npm run build ================================================ FILE: .github/workflows/lock-closed-issues-workflow.yml ================================================ name: 'Lock inactive threads' on: schedule: - cron: '0 0 * * *' jobs: lock: name: Lock closed issues runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v2 with: github-token: ${{ github.token }} issue-lock-inactive-days: 30 pr-lock-inactive-days: 30 issue-lock-comment: > This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. pr-lock-comment: > This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. ================================================ FILE: .gitignore ================================================ # Log files logs *.log *.tmp *.tmp.* log.txt npm-debug.log* # Testing output lib-cov/** coverage/** # Environment files .env # Dependency directories node_modules # MacOS related files *.DS_Store .AppleDouble .LSOverride ._* UserInterfaceState.xcuserstate # Windows related files Thumbs.db Desktop.ini $RECYCLE.BIN/ # IDE - Sublime *.sublime-project *.sublime-workspace # IDE - VSCode .vscode/** !.vscode/tasks.json !.vscode/launch.json # IDE - IntelliJ .idea # Compilation output folders dist/ build/ tmp/ out-tsc/ temp # Files for playing around locally playground.ts playground.js ================================================ FILE: .prettierrc.yml ================================================ printWidth: 120 tabWidth: 2 useTabs: false semi: true singleQuote: true trailingComma: es5 bracketSpacing: true arrowParens: avoid ================================================ FILE: CHANGELOG.md ================================================ # Changelog _This changelog follows the [keep a changelog][keep-a-changelog]_ format to maintain a human readable changelog. ## [0.3.1](https://github.com/typestack/socket-controllers/compare/v0.3.0...v0.3.1) (2024-04-21) ### Changed - Added missing `@OnDisconnecting()` export to index ## [0.3.0](https://github.com/typestack/socket-controllers/compare/v0.2.0...v0.3.0) (2024-04-21) ### Breaking Changes - Middlewares without a defined namespace are registered on all namespaces ### Changed - `glob` package updated from `10.0.0` to `10.3.12` - `path-to-regexp` package updated from `6.2.1` to `6.2.2` - `reflect-metadata` package updated from `0.1.13` to `0.2.2` - `socket.io` package updated from `4.5.4` to `4.7.5` ## [0.2.0](https://github.com/typestack/socket-controllers/compare/v0.1.2...v0.2.0) (2023-04-10) ### Breaking Changes - Replaced `ScopedContainerGetterParams` with `SocketEventContext` BEFORE: ```ts scopedContainerGetter: (args: ScopedContainerGetterParams) => { // ... } ``` AFTER: ```ts scopedContainerGetter: (args: SocketEventContext) => { // ... } ``` Note: The new interface contains all properties of the previous ### Added - Added scoped container dispose support - Added interceptor support - Added ack support ### Changed - `glob` package updated from `8.1.0` to `10.0.0` ## [0.1.2](https://github.com/typestack/socket-controllers/compare/v0.1.1...v0.1.2) (2023-01-30) ### Added - Added scoped controller support ```typescript // create and run socket server const server = new SocketControllers({ ... scopedContainerGetter: (args: ScopedContainerGetterParams) => { // Return a container instance to be used to instantiate // the controllers and their dependencies on each event } }); ``` ## [0.1.1](https://github.com/typestack/socket-controllers/compare/v0.1.0...v0.1.1) (2023-01-27) ### Added - Added `@OnDisconnecting()` decorator - Added error type filter option to `@EmitOnFail()` decorator Example: `@EmitOnFail('message', {errorType: TypeError})` - Added `index` option to `@MessageBody()` decorator to be able to get multiple event arguments Note: If you don't specify the index it will return the first - Added support to use the same namespace for multiple controllers Note: The namespaces must match exactly, providing a differnet pattern will not work due to a socket.io limitation ### Changed - `glob` package updated from `8.0.3` to `8.1.0` ## [0.1.0](https://github.com/typestack/socket-controllers/compare/v0.0.5...v0.1.0) (2023-01-18) ### Breaking Changes - Removed `createSocketServer()` in favor of constructor initialization BEFORE: ```ts import { useSocketServer } from 'socket-controllers'; import { Server } from 'socket.io'; const io = new Server(PORT); useSocketServer(io); ``` AFTER: ```ts import { SocketControllers } from "socket-controllers"; import { Server } from "socket.io"; const io = new Server(PORT); new SocketControllers({io: io, container: YOUR_DI_CONTAINER}); ``` - Removed `createSocketServer()` in favor of constructor initialization BEFORE: ```ts import { createSocketServer } from 'socket-controllers'; const io = createSocketServer(PORT); ``` AFTER: ```ts import { SocketControllers } from "socket-controllers"; const server = new SocketControllers({port: PORT, container: YOUR_DI_CONTAINER}); const io = server.io; ``` - Removed `useContainer()` in favor of constructor initialization BEFORE: ```ts import { useContainer } from 'socket-controllers'; import { Container } from 'typedi'; useContainer(Container); ``` AFTER: ```ts import { SocketControllers } from "socket-controllers"; import { Container } from 'typedi'; const server = new SocketControllers({port: PORT, container: Container}); ``` > Note: DI container is not included anymore, you have to provide your own. - Changed initialization parameters Before: ```typescript interface SocketControllersOptions { controllers?: Function[] | string[]; middlewares?: Function[] | string[]; useClassTransformer?: boolean; classToPlainTransformOptions?: ClassTransformOptions; plainToClassTransformOptions?: ClassTransformOptions; } ``` After: ```typescript interface SocketControllersOptions { container: { get(someClass: { new (...args: any[]): T } | Function): T }; io?: Server; port?: number; controllers?: Function[] | string[]; middlewares?: Function[] | string[]; transformOption?: Partial<{ transform?: boolean; parameterTransformOptions?: ClassTransformOptions; resultTransformOptions?: ClassTransformOptions; }>; } ``` - Changed class-transformer property name in decorators that support class-transformer Before: `classTransformOptions?: ClassTransformOptions` After: `transformOptions?: ClassTransformOptions` ### Added - Namespace scope support for middlewares - `transform: boolean` option to decorators that support class-transformer ### Changed - `class-transformer` package updated from `0.1.6` to `0.5.1` - `path-to-regexp` package updated from `3.0.0` to `6.2.1` - `reflect-metadata` package updated from `0.1.10` to `0.1.13` - `socket.io` package updated from `2.0.1` to `4.5.4` - updated various dev dependencies ### [0.0.5][v0.0.5] - 2020-02-04 #### Added - Added support dynamic namespace - Added `NspParams`, `NspParam` decorators to handle dynamic namespace name params - Allowed use function array for controllers and middlewares #### Fixed - Import middlewares from directory [v0.0.5]: https://github.com/typestack/socket-controllers/compare/v0.0.4...v0.0.5 [keep-a-changelog]: https://keepachangelog.com/en/1.0.0/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-2020 TypeStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # socket-controllers ![Build Status](https://github.com/typestack/socket-controllers/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/typestack/socket-controllers/branch/develop/graph/badge.svg)](https://codecov.io/gh/typestack/socket-controllers) [![npm version](https://badge.fury.io/js/socket-controllers.svg)](https://badge.fury.io/js/socket-controllers) Use class-based controllers to handle websocket events. Helps to organize your code using websockets in classes. ## Installation 1. Install `socket-controllers`: ``` npm install socket-controllers ``` 2. Install `reflect-metadata` shim: ``` npm install reflect-metadata ``` and make sure to import it in a global place, like app.ts: ```typescript import 'reflect-metadata'; ``` 3. Install a DI container, for example `typedi`; ``` npm install typedi ``` ## Example of usage 1. Create a file `MessageController.ts` ```typescript import { OnConnect, SocketController, ConnectedSocket, OnDisconnect, MessageBody, OnMessage, } from 'socket-controllers'; import {Service} from 'typedi'; // Only if you are using typedi @SocketController() @Service() // Only if you are using typedi export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') save(@ConnectedSocket() socket: any, @MessageBody() message: any) { console.log('received message:', message); console.log('setting id to the message and sending it back to the client'); message.id = 1; socket.emit('message_saved', message); } } ``` 2. Create a file `app.ts` ```typescript import 'es6-shim'; // this shim is optional if you are using old version of node import 'reflect-metadata'; // this shim is required import { SocketControllers } from 'socket-controllers'; import { MessageController } from './MessageController'; import {Container} from 'typedi'; // Only if you are using typedi new SocketControllers({ port: 3001, container: Container, controllers: [MessageController], }); ``` 3. Now you can send `save` websocket message using websocket-client. ## More usage examples #### Run code on socket client connect / disconnect / disconnecting Controller action marked with `@OnConnect()` decorator is called once new client connected. Controller action marked with `@OnDisconnect()` decorator is called once client disconnected. Controller action marked with `@OnDisconnecting()` decorator is called when the client is disconnecting, before the disconnect event. ```typescript import { SocketController, OnConnect, OnDisconnect, OnDisconnecting } from 'socket-controllers'; @SocketController() export class MessageController { @OnConnect() save() { console.log('client connected'); } @OnDisconnect() save() { console.log('client disconnected'); } @OnDisconnecting() save() { console.log('client is disconnecting'); } } ``` #### `@ConnectedSocket()` decorator To get connected socket instance you need to use `@ConnectedSocket()` decorator. ```typescript import { SocketController, OnMessage, ConnectedSocket } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@ConnectedSocket() socket: any) { socket.emit('save_success'); } } ``` #### `@MessageBody()` decorator To get received message body use `@MessageBody()` decorator: ```typescript import { SocketController, OnMessage, MessageBody } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@MessageBody() message: any) { console.log('received message: ', message); } } ``` If you specify a class type to parameter that is decorated with `@MessageBody()`, socket-controllers will use [class-transformer][1] to create instance of the given class type with the data received in the message. To disable this behaviour you need to specify `{ transformOption: { transform: false ] }` in SocketControllerOptions when creating a server. You can define an index to get multiple parameters from the socket event. ```typescript import { SocketController, OnMessage, MessageBody } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@MessageBody({index: 0}) param1: any, @MessageBody({index: 1}) param2: any) { console.log('received message: ', message1); console.log('received message: ', message2); } } ``` #### `@MessageAck()` decorator To get received message ack use `@MessageAck()` decorator: ```typescript import { SocketController, OnMessage, MessageAck, MessageBody } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@MessageBody() message: any, @MessageAck() ack: Function) { console.log('received message: ', message); ack('callback message'); } } ``` > note: ack must be the last parameter in `emit`, otherwise it will be `null` #### `@SocketQueryParam()` decorator To get received query parameter use `@SocketQueryParam()` decorator. ```typescript import { SocketController, OnMessage, MessageBody } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@SocketQueryParam('token') token: string) { console.log('authorization token from query parameter: ', token); } } ``` #### Get socket client id using `@SocketId()` decorator To get connected client id use `@SocketId()` decorator. ```typescript import { SocketController, OnMessage, MessageBody } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@SocketId() id: string) {} } ``` #### Get access to using socket.io instance using `@SocketIO()` decorator ```typescript import { SocketController, OnMessage, MessageBody } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') save(@SocketIO() io: any) { // now you can broadcast messages to specific rooms or namespaces using io instance } } ``` #### Send message back to client after method execution You can use `@EmitOnSuccess` decorator: ```typescript import { SocketController, OnMessage, EmitOnSuccess } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') @EmitOnSuccess('save_successfully') save() { // after this controller executed "save_successfully" message will be emitted back to the client } } ``` If you return something, it will be returned in the emitted message data: ```typescript import { SocketController, OnMessage, EmitOnSuccess } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') @EmitOnSuccess('save_successfully') save() { // after this controller executed "save_successfully" message will be emitted back to the client with message object return { id: 1, text: 'new message', }; } } ``` You can also control what message will be emitted if there is error/exception during execution: ```typescript import { SocketController, OnMessage, EmitOnSuccess, EmitOnFail } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('save') @EmitOnSuccess('save_successfully') @EmitOnFail('save_error_range', {errorType: RangeError}) @EmitOnFail('save_error_type', {errorType: TypeError}) @EmitOnFail('save_error') save() { if (1 === 1) { throw new Error('One is equal to one! Fatal error!'); } return { id: 1, text: 'new message', }; } } ``` In this case `save_error` message will be sent to the client with `One is equal to one! Fatal error!` error message. The order is important when defining multiple `@EmitOnFail()` decorators, the first matching errorType will be served Sometimes you may want to not emit success/error message if returned result is null or undefined. In such cases you can use `@SkipEmitOnEmptyResult()` decorator. ```typescript import { SocketController, OnMessage, EmitOnSuccess, EmitOnFail, SkipEmitOnEmptyResult } from 'socket-controllers'; @SocketController() export class MessageController { @OnMessage('get') @EmitOnSuccess('get_success') @SkipEmitOnEmptyResult() get(): Promise { return this.messageRepository.findAll(); } } ``` In this case if findAll will return undefined, `get_success` message will not be emitted. If findAll will return array of messages, they will be emitted back to the client in the `get_success` message. This example also demonstrates Promises support. If promise returned by controller action, message will be emitted only after promise will be resolved. #### Using exist server instead of creating a new one If you need to create and configure socket.io server manually, you can pass it to the `SocketControllers` constructor. Here is example of creating socket.io server and configuring it with express: ```typescript import 'reflect-metadata'; // this shim is required import { SocketControllers } from 'socket-controllers'; import { Server } from 'socket.io'; import { Container } from 'typedi'; // Only if you are using typedi const app = require('express')(); const server = require('http').Server(app); const io = new Server(server); server.listen(3001); app.get('/', function (req: any, res: any) { res.send('hello express'); }); io.use((socket: any, next: Function) => { console.log('Custom middleware'); next(); }); new SocketControllers({io, container: Container}); ``` #### Load all controllers from the given directory You can load all controllers in once from specific directories, by specifying array of directories via options in `createSocketServer` or `useSocketServer`: ```typescript import 'reflect-metadata'; // this shim is required import { SocketControllers } from 'socket-controllers'; import { Container } from 'typedi'; // Only if you are using typedi new SocketControllers({ port: 3000, container: Container, controllers: [__dirname + '/controllers/*.js'], }); // registers all given controllers ``` #### Using socket.io namespaces To listen to messages only of the specific namespace you can mark a controller with namespace: ```typescript @SocketController('/messages') export class MessageController { // ... } ``` Also you can use dynamic namespace, like `express router` patterns: ```typescript @SocketController('/messages/:userId') export class MessageController { // ... } ``` ## Using middlewares Middlewares are the functions passed to the `socketIo.use` method. Middlewares allows you to define a logic that will be executed each time client connected to the server. To create your middlewares use `@Middleware` decorator: ```typescript import { Middleware, MiddlewareInterface } from 'socket-controllers'; @Middleware() export class CompressionMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any) { console.log('do something, for example get authorization token and check authorization'); next(); } } ``` You can limit middlewares to namespaces providing either a `string`, `RegExp` or `Array` to the `namespace` parameter: ```typescript import { Middleware, MiddlewareInterface } from 'socket-controllers'; @Middleware({namespace: '/test'}) export class CompressionMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any) { console.log('do something, for example get authorization token and check authorization'); next(); } } ``` ## Don't forget to load your controllers and middlewares Controllers and middlewares should be loaded: ```typescript import 'reflect-metadata'; import { SocketControllers } from 'socket-controllers'; import { MessageController } from './MessageController'; import { MyMiddleware } from './MyMiddleware'; // here we import it import { Container } from 'typedi'; // Only if you are using typedi const server = new SocketControllers({ port: 3000, container: Container, controllers: [MessageController], middlewares: [MyMiddleware], }); ``` Also you can load them from directories. Also you can use glob patterns: ```typescript import 'reflect-metadata'; import { SocketControllers } from 'socket-controllers'; import { Container } from 'typedi'; // Only if you are using typedi const server = new SocketControllers({ port: 3000, container: Container, controllers: [__dirname + '/controllers/**/*.js'], middlewares: [__dirname + '/middlewares/**/*.js'], }); ``` ## Using DI container `socket-controllers` supports a DI container out of the box. You can inject your services into your controllers and middlewares. Container must be setup during application bootstrap. Here is example how to integrate socket-controllers with [typedi](https://github.com/pleerock/typedi): ```typescript import 'reflect-metadata'; import { SocketControllers } from 'socket-controllers'; import { Container } from 'typedi'; // create and run socket server const server = new SocketControllers({ port: 3000, container: Container, controllers: [__dirname + '/controllers/*.js'], middlewares: [__dirname + '/middlewares/*.js'], }); ``` That's it, now you can inject your services into your controllers: ```typescript @Service() @SocketController() export class MessageController { constructor(private messageRepository: MessageRepository) {} // ... controller actions } ``` > Note: TypeDI won't create instances for unknown classes since 0.9.0, you have to decorate your Class as a `Service()` as well. ### Scoped controllers You can enable scoped controllers by providing a `scopedContainerGetter` function in SocketServerOptions. This function should return a new container that will be used to instantiate the controller and its dependencies. You will get a new instance for each event in the controller. The `scopedContainerGetter` function receives the `SocketEventContext`. The `scopedContainerDisposer` function receives the container instance you created with `scopedContainerGetter` after the socket action is finished. Use this function to dispose the container if needed. ```typescript import 'reflect-metadata'; import { SocketControllers, SocketEventContext } from 'socket-controllers'; import { Container, ContainerInstance, Token } from "typedi"; const myDiToken = new Token(); // create and run socket server const server = new SocketControllers({ port: 3000, container: Container, scopedContainerGetter: (args: SocketEventContext) => { const container = Container.of(YOUR_REQUEST_CONTEXT); container.set(myDiToken, 'MY_VALUE'); return container; }, scopedContainerDisposer: (container: ContainerInstance) => { container.dispose(); }, controllers: [__dirname + '/controllers/*.js'], middlewares: [__dirname + '/middlewares/*.js'], }); ``` ## Interceptors Interceptors allow you to wrap your event handlers in higher order functions. With interceptors you can add logging or modify the incoming or outgoing data for event handlers. ```typescript import { SocketController, OnMessage, EmitOnSuccess, EmitOnFail, SkipEmitOnEmptyResult, UseInterceptor, MessageBody } from 'socket-controllers'; const interceptor: InterceptorInterface = { use: (ctx: SocketEventContext, next: () => any) => { ctx.messageArgs[0] = 'modified message from controller - ' + ctx.messageArgs[0]; const resp = next(); return 'modified response from controller - ' + resp; // modified response from controller - modified response from method - reponse }, }; @Service() class Interceptor implements InterceptorInterface { async use(ctx: SocketEventContext, next: () => any) { ctx.messageArgs[0] = 'modified message from method - ' + ctx.messageArgs[0]; const resp = await next(); return 'modified response from method - ' + resp; // modified response from method - reponse } } @SocketController() @UseInterceptor(interceptor) export class MessageController { @OnMessage('get') @EmitOnSuccess('get_success') @SkipEmitOnEmptyResult() @UseInterceptor(Interceptor) get(@MessageBody() message: string): Promise { console.log(message); // modified message from controller - modified message from method - original message return 'response'; } } ``` Interceptors are executed in order of definition, starting with the controller interceptors. ## Decorators Reference | Signature | Description | |----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `@SocketController(namespace?: string\ | Regex)` | Registers a class to be a socket controller that can listen to websocket events and respond to them. | | `@OnMessage(messageName: string)` | Registers controller's action to be executed when socket receives message with given name. | | `@OnConnect()` | Registers controller's action to be executed when client connects to the socket. | | `@OnDisconnect()` | Registers controller's action to be executed when client disconnects from the socket. | | `@OnDisconnecting()` | Registers controller's action to be executed when client is disconnecting from the socket. | | `@ConnectedSocket()` | Injects connected client's socket object to the controller action. | | `@SocketIO()` | Injects socket.io object that initialized a connection. | | `@MessageBody()` | Injects received message body. | | `@SocketQueryParam(paramName: string)` | Injects query parameter from the received socket request. | | `@SocketId()` | Injects socket id from the received request. | | `@SocketRequest()` | Injects request object received by socket. | | `@SocketRooms()` | Injects rooms of the connected socket client. | | `@NspParams()` | Injects dynamic namespace params. | | `@NspParam(paramName: string)` | Injects param from the dynamic namespace. | | `@Middleware()` | Registers a new middleware to be registered in the socket.io. | | `@EmitOnSuccess(messageName: string)` | If this decorator is set then after controller action will emit message with the given name after action execution. It will emit message only if controller succeed without errors. If result is a Promise then it will wait until promise is resolved and emit a message. | | `@EmitOnFail(messageName: string)` | If this decorator is set then after controller action will emit message with the given name after action execution. It will emit message only if controller throw an exception. If result is a Promise then it will wait until promise throw an error and emit a message. | | `@SkipEmitOnEmptyResult()` | Used in conjunction with @EmitOnSuccess and @EmitOnFail decorators. If result returned by controller action is null or undefined then messages will not be emitted by @EmitOnSuccess or @EmitOnFail decorators. | | ## Samples Take a look on samples in [./sample](https://github.com/pleerock/socket-controllers/tree/master/sample) for more examples of usage. ## Related projects - If you are interested to create controller-based express or koa server use [routing-controllers](https://github.com/pleerock/routing-controllers) module. - If you need to use dependency injection in use [typedi](https://github.com/pleerock/typedi) module. [1]: https://github.com/pleerock/class-transformer ================================================ FILE: codecov.yml ================================================ coverage: range: 70..100 round: down precision: 2 status: project: default: threshold: 0% paths: - src/**/*.ts comment: off ignore: - testing/**/*.ts - src/**/*.interface.ts ================================================ FILE: eslint.config.mjs ================================================ import typescriptEslint from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import js from '@eslint/js'; import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all, }); export default [...compat.extends( 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:jest/recommended', 'prettier', ), { plugins: { '@typescript-eslint': typescriptEslint, }, languageOptions: { parser: tsParser, ecmaVersion: 2018, sourceType: 'module', parserOptions: { project: ['./tsconfig.json', './tsconfig.spec.json'], }, }, rules: { '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/no-angle-bracket-type-assertion': 'off', '@typescript-eslint/no-parameter-properties': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/member-delimiter-style': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/member-ordering': 'error', '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', '@typescript-eslint/no-wrapper-object-types': 'off', }, }]; ================================================ FILE: jest.config.js ================================================ module.exports = { preset: 'ts-jest', testEnvironment: 'node', collectCoverageFrom: ['src/**/*.ts', '!src/**/index.ts', '!src/**/*.interface.ts'], globals: {}, setupFilesAfterEnv: ["./jest.setup.js"], transform: { '^.+\\.tsx?$': [ 'ts-jest', {tsconfig: './tsconfig.spec.json'}, ], } }; ================================================ FILE: jest.setup.js ================================================ jest.setTimeout(30000); require("reflect-metadata"); ================================================ FILE: package.json ================================================ { "name": "socket-controllers", "version": "0.3.1", "description": "Use class-based controllers to handle websocket events.", "license": "MIT", "main": "index.js", "author": { "name": "TypeStack contributors" }, "repository": { "type": "git", "url": "https://github.com/pleerock/socket-controllers.git" }, "keywords": [ "websocket", "typescript", "typescript-websocket", "socket-controllers", "socket.io", "socket-server" ], "scripts": { "build": "rimraf build && tsc --project tsconfig.prod.json", "prettier:fix": "prettier --write \"**/*.ts\"", "prettier:check": "prettier --check \"**/*.ts\"", "lint:fix": "eslint --max-warnings 0 --fix src/**/*.ts", "lint:check": "eslint --max-warnings 0 src/**/*.ts", "test": "jest --coverage --verbose", "test:watch": "jest --watch", "test:ci": "jest --runInBand --no-cache --coverage --verbose" }, "dependencies": { "class-transformer": "^0.5.1", "glob": "^11.0.0", "path-to-regexp": "^8.1.0", "reflect-metadata": "^0.2.2", "socket.io": "^4.7.5" }, "devDependencies": { "@types/glob": "^8.1.0", "@types/jest": "^29.5.13", "@types/node": "^20.14.11", "@types/path-to-regexp": "^1.7.0", "@types/socket.io": "^3.0.2", "@typescript-eslint/eslint-plugin": "^8.6.0", "@typescript-eslint/parser": "^8.6.0", "eslint": "^9.10.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^28.8.3", "express": "^4.21.0", "husky": "^9.1.6", "jest": "^29.7.0", "lint-staged": "^15.2.10", "prettier": "^3.3.3", "rimraf": "6.0.1", "socket.io-client": "^4.7.5", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typedi": "^0.10.0", "typescript": "^5.6.2" } } ================================================ FILE: sample/sample1-simple-controller/Message.ts ================================================ export class Message { id: number; text: string; } ================================================ FILE: sample/sample1-simple-controller/MessageController.ts ================================================ import { Message } from './Message'; import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController() export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') save(@ConnectedSocket() socket: any, @MessageBody() message: Message) { console.log('received message:', message); console.log('setting id to the message and sending it back to the client'); message.id = 1; socket.emit('message_saved', message); } } ================================================ FILE: sample/sample1-simple-controller/app.ts ================================================ import 'reflect-metadata'; import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; import { Container } from 'typedi'; new SocketControllers({ port: 3001, container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there console.log('Socket.io is up and running on port 3001. Send messages via socket-io client.'); ================================================ FILE: sample/sample1-simple-controller/index.html ================================================ Watch console for events.
================================================ FILE: sample/sample2-use-created-socket-io/Message.ts ================================================ export class Message { id: number; text: string; } ================================================ FILE: sample/sample2-use-created-socket-io/MessageController.ts ================================================ import { Message } from './Message'; import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController() export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') save(@ConnectedSocket() socket: any, @MessageBody() message: Message) { console.log('received message:', message); console.log('setting id to the message and sending it back to the client'); message.id = 1; socket.emit('message_saved', message); } } ================================================ FILE: sample/sample2-use-created-socket-io/app.ts ================================================ import 'reflect-metadata'; import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; import { Server } from 'socket.io'; import { Container } from 'typedi'; const app = require('express')(); const server = require('http').Server(app); const io = new Server(server); server.listen(3001); app.get('/', function (req: any, res: any) { res.send('hello express'); }); io.use((socket: any, next: Function) => { console.log('Custom middleware'); next(); }); new SocketControllers({ io, container: Container, controllers: [MessageController], }); console.log('Socket.io is up and running on port 3001. Send messages via socket-io client.'); ================================================ FILE: sample/sample2-use-created-socket-io/index.html ================================================ Watch console for events.
================================================ FILE: sample/sample3-namespaces/Message.ts ================================================ export class Message { id: number; text: string; } ================================================ FILE: sample/sample3-namespaces/MessageController.ts ================================================ import { Message } from './Message'; import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController('/messages') export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') save(@ConnectedSocket() socket: any, @MessageBody() message: Message) { console.log('received message:', message); console.log('setting id to the message and sending it back to the client'); message.id = 1; socket.emit('message_saved', message); } } ================================================ FILE: sample/sample3-namespaces/app.ts ================================================ import 'reflect-metadata'; import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; import { Container } from 'typedi'; new SocketControllers({ port: 3001, container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there console.log('Socket.io is up and running on port 3001. Send messages via socket-io client.'); ================================================ FILE: sample/sample3-namespaces/index.html ================================================ Watch console for events.
================================================ FILE: sample/sample4-emitters/Message.ts ================================================ export class Message { id: number; text: string; } ================================================ FILE: sample/sample4-emitters/MessageController.ts ================================================ import { Message } from './Message'; import { ConnectedSocket, EmitOnFail, EmitOnSuccess, MessageBody, OnConnect, OnDisconnect, OnMessage, SkipEmitOnEmptyResult, SocketController, } from '../../src'; @SocketController() export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') @EmitOnSuccess('message_save_success') @EmitOnFail('message_save_failed') @SkipEmitOnEmptyResult() save(@ConnectedSocket() socket: any, @MessageBody() message: Message) { console.log('received message:', message); console.log('setting id to the message and sending it back to the client'); message.id = 1; return message; } @OnMessage('try_to_save') @EmitOnSuccess('message_save_success') @EmitOnFail('message_save_failed') @SkipEmitOnEmptyResult() trySave(@ConnectedSocket() socket: any, @MessageBody() message: Message) { console.log('received message:', message); throw new Error('No, cannot save =('); } } ================================================ FILE: sample/sample4-emitters/app.ts ================================================ import 'reflect-metadata'; import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; import { Container } from 'typedi'; new SocketControllers({ port: 3001, container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there console.log('Socket.io is up and running on port 3001. Send messages via socket-io client.'); ================================================ FILE: sample/sample4-emitters/index.html ================================================ Watch console for events.

================================================ FILE: sample/sample5-middlewares/AuthenticationMiddleware.ts ================================================ import { Middleware, MiddlewareInterface } from '../../src'; @Middleware() export class AuthenticationMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { console.log('authentication...'); next(); } } ================================================ FILE: sample/sample5-middlewares/Message.ts ================================================ export class Message { id: number; text: string; } ================================================ FILE: sample/sample5-middlewares/MessageController.ts ================================================ import { Message } from './Message'; import { ConnectedSocket, MessageBody, OnConnect, OnDisconnect, OnMessage, SocketController } from '../../src'; @SocketController() export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') save(@ConnectedSocket() socket: any, @MessageBody() message: Message) { console.log('received message:', message); console.log('setting id to the message and sending it back to the client'); message.id = 1; socket.emit('message_saved', message); } } ================================================ FILE: sample/sample5-middlewares/app.ts ================================================ import 'reflect-metadata'; import { SocketControllers } from '../../src/index'; import { AuthenticationMiddleware } from './AuthenticationMiddleware'; import { MessageController } from './MessageController'; import { Container } from 'typedi'; new SocketControllers({ port: 3001, container: Container, controllers: [MessageController], middlewares: [AuthenticationMiddleware], }); // creates socket.io server and registers all controllers and middlewares there console.log('Socket.io is up and running on port 3001. Send messages via socket-io client.'); ================================================ FILE: sample/sample5-middlewares/index.html ================================================ Watch console for events.
================================================ FILE: sample/sample6-dynamic-namespaces/Message.ts ================================================ export class Message { id: number; text: string; } ================================================ FILE: sample/sample6-dynamic-namespaces/MessageController.ts ================================================ import { Message } from './Message'; import { ConnectedSocket, MessageBody, NspParams, OnConnect, OnDisconnect, OnMessage, SocketController, } from '../../src'; @SocketController('/messages/:id') export class MessageController { @OnConnect() connection(@ConnectedSocket() socket: any) { console.log('client connected'); } @OnDisconnect() disconnect(@ConnectedSocket() socket: any) { console.log('client disconnected'); } @OnMessage('save') async save(@ConnectedSocket() socket: any, @MessageBody() message: Message, @NspParams() params: any[]) { console.log('received message:', message); console.log('namespace params:', params); console.log('setting id to the message and sending it back to the client'); message.id = 1; socket.emit('message_saved', message); } } ================================================ FILE: sample/sample6-dynamic-namespaces/app.ts ================================================ import 'reflect-metadata'; import { SocketControllers } from '../../src/index'; import { MessageController } from './MessageController'; import { Container } from 'typedi'; new SocketControllers({ port: 3001, container: Container, controllers: [MessageController], }); // creates socket.io server and registers all controllers there console.log('Socket.io is up and running on port 3001. Send messages via socket-io client.'); ================================================ FILE: sample/sample6-dynamic-namespaces/index.html ================================================ Watch console for events.
================================================ FILE: src/SocketControllers.ts ================================================ import { Namespace, Server, Socket } from 'socket.io'; import { sync } from 'glob'; import { normalize } from 'path'; import { SOCKET_CONTROLLER_META_KEY } from './types/SocketControllerMetaKey'; import { pathToRegexp } from 'path-to-regexp'; import { HandlerMetadata } from './types/HandlerMetadata'; import { HandlerType } from './types/enums/HandlerType'; import { SocketControllersOptions } from './types/SocketControllersOptions'; import { ControllerMetadata } from './types/ControllerMetadata'; import { MiddlewareMetadata } from './types/MiddlewareMetadata'; import { SocketEventType } from './types/enums/SocketEventType'; import { ActionMetadata } from './types/ActionMetadata'; import { ParameterMetadata } from './types/ParameterMetadata'; import { ParameterType } from './types/enums/ParameterType'; import { ResultType } from './types/enums/ResultType'; import { getMetadata } from './util/get-metadata'; import { TransformOptions } from './types/TransformOptions'; import { defaultTransformOptions } from './types/constants/defaultTransformOptions'; import { ActionTransformOptions } from './types/ActionTransformOptions'; import { instanceToPlain, plainToInstance } from 'class-transformer'; import { MiddlewareInterface } from './types/MiddlewareInterface'; import { InterceptorInterface } from './types/InterceptorInterface'; import { chainExecute } from './util/chain-execute'; import { SocketEventContext } from './types/SocketEventContext'; export class SocketControllers { public container: { get(someClass: { new (...args: any[]): T } | Function): T }; public controllers: HandlerMetadata[]; public middlewares: HandlerMetadata[]; public io: Server; public transformOptions: TransformOptions; constructor(private options: SocketControllersOptions) { this.container = options.container; this.io = options.io || new Server(options.port); this.transformOptions = { ...defaultTransformOptions, ...options.transformOption, }; this.controllers = this.loadHandlers(options.controllers || [], HandlerType.CONTROLLER); this.middlewares = this.loadHandlers(options.middlewares || [], HandlerType.MIDDLEWARE); this.registerMiddlewares(); this.registerControllers(); } private loadHandlers(handlers: Array, type: HandlerType): HandlerMetadata[] { const loadedHandlers: Function[] = []; for (const handler of handlers) { if (typeof handler === 'string') { loadedHandlers.push(...this.loadHandlersFromPath(handler, type)); } else { loadedHandlers.push(handler); } } return loadedHandlers.map(handler => { return { metadata: getMetadata(handler), target: handler, }; }); } private loadHandlersFromPath(path: string, handlerType: HandlerType): Function[] { const files = sync(normalize(path).replace(/\\/g, '/')); return files .map(file => require(file)) .reduce((loadedFiles: Function[], loadedFile: Record) => { const handlersInFile = Object.values(loadedFile).filter(fileEntry => { if (typeof fileEntry !== 'function') { return false; } if (!(Reflect as any).hasMetadata(SOCKET_CONTROLLER_META_KEY, fileEntry as Function)) { return false; } return (Reflect as any).getMetadata(SOCKET_CONTROLLER_META_KEY, fileEntry as Function).type === handlerType; }); loadedFiles.push(...(handlersInFile as Function[])); return loadedFiles; }, []); } private registerMiddlewares() { const middlewares = this.middlewares.slice().sort((middleware1, middleware2) => { return (middleware1.metadata.priority || 0) - (middleware2.metadata.priority || 0); }); const middlewaresWithoutNamespace = middlewares.filter(middleware => !middleware.metadata.namespace); for (const middleware of middlewaresWithoutNamespace) { this.registerMiddleware(this.io as unknown as Namespace, middleware); } this.io.on('new_namespace', (namespace: Namespace) => { for (const middleware of middlewares) { const middlewareNamespaces = Array.isArray(middleware.metadata.namespace) ? middleware.metadata.namespace : [middleware.metadata.namespace]; const shouldApply = middlewareNamespaces.some(nsp => { // Register middlewares without namespace too if (nsp == null) { return true; } const nspRegexp = nsp instanceof RegExp ? nsp : pathToRegexp(nsp).regexp; return nspRegexp.test(namespace.name); }); if (shouldApply) { this.registerMiddleware(namespace, middleware); } } }); } private registerControllers() { const controllersWithoutNamespace = this.controllers.filter(controller => !controller.metadata.namespace); const controllersWithNamespace = this.controllers.filter(controller => !!controller.metadata.namespace); this.io.on('connection', (socket: Socket) => { for (const controller of controllersWithoutNamespace) { this.registerController(socket, controller); } }); const controllerNamespaceMap: Record[]> = {}; const controllerNamespaceRegExpMap: Record = {}; for (const controller of controllersWithNamespace) { const nsp = controller.metadata.namespace as string; if (!controllerNamespaceMap[nsp]) { controllerNamespaceMap[nsp] = []; } controllerNamespaceMap[nsp].push(controller); controllerNamespaceRegExpMap[nsp] = nsp; } for (const [nsp, controllers] of Object.entries(controllerNamespaceMap)) { const namespace = controllerNamespaceRegExpMap[nsp]; this.io .of(namespace instanceof RegExp ? namespace : pathToRegexp(namespace).regexp) .on('connection', (socket: Socket) => { for (const controller of controllers) { this.registerController(socket, controller); } }); } } private registerController(socket: Socket, controller: HandlerMetadata) { const connectedAction = Object.values(controller.metadata.actions || {}).find( action => action.type === SocketEventType.CONNECT ); const disconnectedAction = Object.values(controller.metadata.actions || {}).find( action => action.type === SocketEventType.DISCONNECT ); const disconnectingAction = Object.values(controller.metadata.actions || {}).find( action => action.type === SocketEventType.DISCONNECTING ); const messageActions = Object.values(controller.metadata.actions || {}).filter( action => action.type === SocketEventType.MESSAGE ); if (connectedAction) { void this.executeAction(socket, controller, connectedAction); } if (disconnectedAction) { socket.on('disconnect', () => { void this.executeAction(socket, controller, disconnectedAction); }); } if (disconnectingAction) { socket.on('disconnecting', () => { void this.executeAction(socket, controller, disconnectingAction); }); } for (const messageAction of messageActions) { socket.on(messageAction.options.name, (...args: any[]) => { const messages: any[] = args.slice(0, -1); let ack: Function | null = args[args.length - 1]; if (!(ack instanceof Function)) { messages.push(ack); ack = null; } void this.executeAction(socket, controller, messageAction, messageAction.options.name as string, messages, ack); }); } } private async executeAction( socket: Socket, controller: HandlerMetadata, action: ActionMetadata, eventName?: string, data?: any[], ack?: Function | null ) { const eventContext = this.resolveEventContext( socket, action.type, eventName, data, controller.metadata.namespace, ack ); let container = this.container; if (this.options.scopedContainerGetter) { container = this.options.scopedContainerGetter(eventContext); } try { const controllerInstance: any = container.get(controller.target); const actions = [ ...(action.interceptors || []).map(interceptor => { return ( ((interceptor as any) instanceof Function ? container.get(interceptor) : interceptor) as InterceptorInterface ).use.bind(interceptor); }), (context: SocketEventContext) => { const parameters = this.resolveParameters( socket, controller.metadata, action.parameters || [], context.messageArgs, ack ); return controllerInstance[action.methodName](...parameters); }, ]; const actionResult = chainExecute(eventContext, actions); const result = await Promise.resolve(actionResult); this.handleActionResult(socket, action, result, ResultType.EMIT_ON_SUCCESS); } catch (error: any) { this.handleActionResult(socket, action, error, ResultType.EMIT_ON_FAIL); } if (this.options.scopedContainerDisposer) { this.options.scopedContainerDisposer(container); } } private handleActionResult(socket: Socket, action: ActionMetadata, result: any, resultType: ResultType) { const allOnResultActions = action.results?.filter(result => result.type === resultType) || []; const skipOnEmpty = action.results?.some(result => result.type === ResultType.SKIP_EMIT_ON_EMPTY_RESULT); if (result == null && skipOnEmpty) { return; } let onResultActions = allOnResultActions; if (onResultActions.some(action => action.options.errorType)) { const firstFittingAction = allOnResultActions.find( action => action.options.errorType && result instanceof (action.options.errorType as Function) ); if (!firstFittingAction) { onResultActions = allOnResultActions.filter(action => !action.options.errorType); } else { onResultActions = [firstFittingAction]; } } for (const onResultAction of onResultActions) { const transformedValue = result instanceof Error ? result.message : this.transformActionValue(result as never, null, onResultAction.options, 'result'); socket.emit(onResultAction.options.messageName as never, transformedValue); } } private registerMiddleware(namespace: Namespace, middleware: HandlerMetadata) { namespace.use((socket: Socket, next: (err?: any) => void) => { const instance: MiddlewareInterface = this.container.get(middleware.target); instance.use(socket, next); }); } private resolveParameters( socket: Socket, controllerMetadata: ControllerMetadata, parameterMetadatas: ParameterMetadata[], data?: any[], ack?: Function | null ) { const parameters = []; for (const metadata of parameterMetadatas) { const parameterValue = this.resolveParameter(socket, controllerMetadata, metadata, data, ack) as never; parameters[metadata.index] = this.transformActionValue( parameterValue, metadata.reflectedType as never, metadata.options, 'parameter' ); } return parameters; } private resolveParameter( socket: Socket, controller: ControllerMetadata, parameter: ParameterMetadata, data?: any[], ack?: Function | null ) { switch (parameter.type) { case ParameterType.CONNECTED_SOCKET: return socket; case ParameterType.SOCKET_ID: return socket.id; case ParameterType.SOCKET_IO: return this.io; case ParameterType.SOCKET_ROOMS: return socket.rooms; case ParameterType.MESSAGE_BODY: return data?.[(parameter.options.index as number) || 0]; case ParameterType.MESSAGE_ACK: return ack; case ParameterType.SOCKET_QUERY_PARAM: return socket.handshake.query[parameter.options.name as string]; case ParameterType.SOCKET_REQUEST: return socket.request; case ParameterType.NAMESPACE_PARAMS: return this.extractNamespaceParameters(socket, controller.namespace, parameter); case ParameterType.NAMESPACE_PARAM: return this.extractNamespaceParameters(socket, controller.namespace, parameter)?.[ parameter.options.name as string ]; } } private transformActionValue( value: never, reflectedType: unknown, options: ActionTransformOptions, transformType: 'parameter' | 'result' ) { const transformOptions: TransformOptions = { transform: options.transform ?? this.transformOptions.transform, parameterTransformOptions: options.transformOptions ?? this.transformOptions.parameterTransformOptions, resultTransformOptions: options.transformOptions ?? this.transformOptions.resultTransformOptions, }; if (!transformOptions.transform) { return value; } if (typeof value !== 'object' || Array.isArray(value) || value == null) { return value; } if (transformType === 'parameter') { return plainToInstance(reflectedType as never, value, transformOptions.parameterTransformOptions); } if (transformType === 'result') { return instanceToPlain(value, transformOptions.resultTransformOptions); } return value; } private resolveEventContext( socket: Socket, eventType: SocketEventType, eventName?: string, messageBody?: any[], namespace?: string | RegExp, ack?: Function | null ): SocketEventContext { return { eventType, eventName, socket, socketIo: this.io, nspParams: this.extractNamespaceParameters(socket, namespace), messageArgs: messageBody, ack, }; } private extractNamespaceParameters( socket: Socket, namespace: string | RegExp | undefined, parameterMetadata?: ParameterMetadata ) { let keys: any[]; let regexp: RegExp; if (namespace instanceof RegExp) { regexp = namespace; keys = []; } else { const pathToRegexpResult = pathToRegexp(namespace || '/'); regexp = pathToRegexpResult.regexp; keys = pathToRegexpResult.keys; } const parts: any[] = regexp.exec(socket.nsp.name) || []; const params: Record = {}; keys.forEach((key: any, index: number) => { params[key.name as string] = parameterMetadata?.options?.transform ? this.transformActionValue( parts[index + 1] as never, parameterMetadata.reflectedType, parameterMetadata.options, 'parameter' ) : parts[index + 1]; }); return params; } } ================================================ FILE: src/decorators/ConnectedSocket.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function ConnectedSocket() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.CONNECTED_SOCKET, options: { transform: false, }, }); }; } ================================================ FILE: src/decorators/EmitOnFail.ts ================================================ import { addResultToActionMetadata } from '../util/add-result-to-action-metadata'; import { ResultType } from '../types/enums/ResultType'; import { ActionTransformOptions } from '../types/ActionTransformOptions'; export function EmitOnFail(messageName: string, options?: ActionTransformOptions & { errorType: unknown }): Function { return function (object: Object, methodName: string) { addResultToActionMetadata(object.constructor, methodName, { type: ResultType.EMIT_ON_FAIL, options: { messageName, ...options, }, }); }; } ================================================ FILE: src/decorators/EmitOnSuccess.ts ================================================ import { addResultToActionMetadata } from '../util/add-result-to-action-metadata'; import { ResultType } from '../types/enums/ResultType'; import { ActionTransformOptions } from '../types/ActionTransformOptions'; export function EmitOnSuccess(messageName: string, options?: ActionTransformOptions): Function { return function (object: Object, methodName: string) { addResultToActionMetadata(object.constructor, methodName, { type: ResultType.EMIT_ON_SUCCESS, options: { messageName, ...options, }, }); }; } ================================================ FILE: src/decorators/MessageAck.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function MessageAck() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.MESSAGE_ACK, options: {}, }); }; } ================================================ FILE: src/decorators/MessageBody.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; import { ActionTransformOptions } from '../types/ActionTransformOptions'; export function MessageBody(options?: ActionTransformOptions & { index?: number }) { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.MESSAGE_BODY, options: { ...options, }, }); }; } ================================================ FILE: src/decorators/Middleware.ts ================================================ import { HandlerType } from '../types/enums/HandlerType'; import { addMiddlewareMetadata } from '../util/add-middleware-metadata'; export function Middleware(options?: { priority?: number; namespace?: string | RegExp | Array; }): Function { return function (object: Function) { addMiddlewareMetadata(object, { type: HandlerType.MIDDLEWARE, namespace: options?.namespace, priority: options?.priority, }); }; } ================================================ FILE: src/decorators/NspParam.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function NspParam(name: string) { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.NAMESPACE_PARAM, options: { name, transform: false }, }); }; } ================================================ FILE: src/decorators/NspParams.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function NspParams() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.NAMESPACE_PARAMS, options: { transform: false }, }); }; } ================================================ FILE: src/decorators/OnConnect.ts ================================================ import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; import { SocketEventType } from '../types/enums/SocketEventType'; export function OnConnect(): Function { return function (object: Object, methodName: string) { addActionToControllerMetadata(object.constructor, { methodName, type: SocketEventType.CONNECT, options: {}, }); }; } ================================================ FILE: src/decorators/OnDisconnect.ts ================================================ import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; import { SocketEventType } from '../types/enums/SocketEventType'; export function OnDisconnect(): Function { return function (object: Object, methodName: string) { addActionToControllerMetadata(object.constructor, { methodName, type: SocketEventType.DISCONNECT, options: {}, }); }; } ================================================ FILE: src/decorators/OnDisconnecting.ts ================================================ import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; import { SocketEventType } from '../types/enums/SocketEventType'; export function OnDisconnecting(): Function { return function (object: Object, methodName: string) { addActionToControllerMetadata(object.constructor, { methodName, type: SocketEventType.DISCONNECTING, options: {}, }); }; } ================================================ FILE: src/decorators/OnMessage.ts ================================================ import { addActionToControllerMetadata } from '../util/add-action-to-controller-metadata'; import { SocketEventType } from '../types/enums/SocketEventType'; export function OnMessage(name?: string): Function { return function (object: Object, methodName: string) { addActionToControllerMetadata(object.constructor, { methodName, type: SocketEventType.MESSAGE, options: { name }, }); }; } ================================================ FILE: src/decorators/SkipEmitOnEmptyResult.ts ================================================ import { addResultToActionMetadata } from '../util/add-result-to-action-metadata'; import { ResultType } from '../types/enums/ResultType'; export function SkipEmitOnEmptyResult(): Function { return function (object: Object, methodName: string) { addResultToActionMetadata(object.constructor, methodName, { type: ResultType.SKIP_EMIT_ON_EMPTY_RESULT, options: {}, }); }; } ================================================ FILE: src/decorators/SocketController.ts ================================================ import { HandlerType } from '../types/enums/HandlerType'; import { addControllerMetadata } from '../util/add-controller-metadata'; export function SocketController(namespace?: string | RegExp) { return function (object: Function) { addControllerMetadata(object, { namespace, type: HandlerType.CONTROLLER }); }; } ================================================ FILE: src/decorators/SocketIO.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function SocketIO() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.SOCKET_IO, options: { transform: false }, }); }; } ================================================ FILE: src/decorators/SocketId.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function SocketId() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.SOCKET_ID, options: { transform: false }, }); }; } ================================================ FILE: src/decorators/SocketQueryParam.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function SocketQueryParam(name?: string) { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.SOCKET_QUERY_PARAM, options: { name, transform: false }, }); }; } ================================================ FILE: src/decorators/SocketRequest.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function SocketRequest() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.SOCKET_REQUEST, options: { transform: false }, }); }; } ================================================ FILE: src/decorators/SocketRooms.ts ================================================ import { addParameterToActionMetadata } from '../util/add-parameter-to-action-metadata'; import { ParameterType } from '../types/enums/ParameterType'; export function SocketRooms() { return function (object: Object, methodName: string, index: number) { const format = (Reflect as any).getMetadata('design:paramtypes', object, methodName)[index]; addParameterToActionMetadata(object.constructor, methodName, { index, reflectedType: format, type: ParameterType.SOCKET_ROOMS, options: { transform: false }, }); }; } ================================================ FILE: src/decorators/UseInterceptor.ts ================================================ import { addInterceptorToActionMetadata } from '../util/add-interceptor-to-action-metadata'; import { getMetadata } from '../util/get-metadata'; import { ControllerMetadata } from '../types/ControllerMetadata'; export function UseInterceptor(...interceptors: any[]): Function { return function (object: Function | Object, methodName?: string) { for (const interceptor of interceptors) { if (object instanceof Function) { // Class interceptor const existingMetadata: ControllerMetadata = getMetadata(object); for (const key of Object.keys(existingMetadata?.actions || {})) { addInterceptorToActionMetadata(object, key, interceptor as Function); } } else { // Method interceptor addInterceptorToActionMetadata(object.constructor, methodName as string, interceptor as Function); } } }; } ================================================ FILE: src/index.ts ================================================ export * from './decorators/ConnectedSocket'; export * from './decorators/EmitOnFail'; export * from './decorators/EmitOnSuccess'; export * from './decorators/MessageBody'; export * from './decorators/MessageAck'; export * from './decorators/Middleware'; export * from './decorators/NspParam'; export * from './decorators/NspParams'; export * from './decorators/OnConnect'; export * from './decorators/OnDisconnect'; export * from './decorators/OnDisconnecting'; export * from './decorators/OnMessage'; export * from './decorators/SkipEmitOnEmptyResult'; export * from './decorators/SocketController'; export * from './decorators/SocketId'; export * from './decorators/SocketIO'; export * from './decorators/SocketQueryParam'; export * from './decorators/SocketRequest'; export * from './decorators/SocketRooms'; export * from './types/MiddlewareInterface'; export * from './types/InterceptorInterface'; export * from './types/TransformOptions'; export * from './types/SocketControllersOptions'; export * from './types/enums/SocketEventType'; export * from './types/SocketEventContext'; export * from './SocketControllers'; ================================================ FILE: src/types/ActionMetadata.ts ================================================ import { ParameterMetadata } from './ParameterMetadata'; import { ResultMetadata } from './ResultMetadata'; import { SocketEventType } from './enums/SocketEventType'; export interface ActionMetadata { type: SocketEventType; methodName: string; options: any; parameters: ParameterMetadata[]; results: ResultMetadata[]; interceptors: Function[]; } ================================================ FILE: src/types/ActionTransformOptions.ts ================================================ import { ClassTransformOptions } from 'class-transformer'; export interface ActionTransformOptions { transform?: boolean; transformOptions?: ClassTransformOptions; } ================================================ FILE: src/types/ControllerMetadata.ts ================================================ import { HandlerType } from './enums/HandlerType'; import { ActionMetadata } from './ActionMetadata'; export interface ControllerMetadata { namespace?: string | RegExp; type: HandlerType.CONTROLLER; actions: { [methodName: string]: ActionMetadata; }; } ================================================ FILE: src/types/HandlerMetadata.ts ================================================ export interface HandlerMetadata { metadata: T; target: Function; } ================================================ FILE: src/types/InterceptorInterface.ts ================================================ import { SocketEventContext } from './SocketEventContext'; export interface InterceptorInterface { use(context: SocketEventContext, next: () => any): any; } ================================================ FILE: src/types/MiddlewareInterface.ts ================================================ import { Socket } from 'socket.io'; export interface MiddlewareInterface { use(socket: Socket, next: (err?: any) => any): any; } ================================================ FILE: src/types/MiddlewareMetadata.ts ================================================ import { HandlerType } from './enums/HandlerType'; export interface MiddlewareMetadata { namespace?: string | RegExp | Array; priority?: number; type: HandlerType.MIDDLEWARE; } ================================================ FILE: src/types/ParameterMetadata.ts ================================================ import { ParameterType } from './enums/ParameterType'; import { ActionTransformOptions } from './ActionTransformOptions'; export interface ParameterMetadata { type: ParameterType; index: number; reflectedType: any; options: ActionTransformOptions & Record; } ================================================ FILE: src/types/ResultMetadata.ts ================================================ import { ResultType } from './enums/ResultType'; import { ActionTransformOptions } from './ActionTransformOptions'; export interface ResultMetadata { type: ResultType; options: ActionTransformOptions & Record; } ================================================ FILE: src/types/SocketControllerMetaKey.ts ================================================ export const SOCKET_CONTROLLER_META_KEY = Symbol('SocketControllerMetaKey'); ================================================ FILE: src/types/SocketControllersOptions.ts ================================================ import { Server } from 'socket.io'; import { TransformOptions } from './TransformOptions'; import { SocketEventContext } from './SocketEventContext'; export interface SocketControllersOptions { container: { get(someClass: { new (...args: any[]): T } | Function): T }; scopedContainerGetter?: (context: SocketEventContext) => { get(someClass: { new (...args: any[]): T } | Function): T; }; scopedContainerDisposer?: (container: { get(someClass: { new (...args: any[]): T } | Function): T }) => void; io?: Server; port?: number; controllers?: Function[] | string[]; middlewares?: Function[] | string[]; transformOption?: Partial; } ================================================ FILE: src/types/SocketEventContext.ts ================================================ import { SocketEventType } from './enums/SocketEventType'; import { Server, Socket } from 'socket.io'; export interface SocketEventContext { socketIo: Server; socket: Socket; eventType: SocketEventType; eventName?: string; messageArgs?: any[]; nspParams?: Record; ack?: Function | null; } ================================================ FILE: src/types/TransformOptions.ts ================================================ import { ClassTransformOptions } from 'class-transformer'; export interface TransformOptions { transform?: boolean; parameterTransformOptions?: ClassTransformOptions; resultTransformOptions?: ClassTransformOptions; } ================================================ FILE: src/types/constants/defaultTransformOptions.ts ================================================ import { TransformOptions } from '../TransformOptions'; export const defaultTransformOptions: TransformOptions = { transform: true, }; ================================================ FILE: src/types/enums/HandlerType.ts ================================================ export enum HandlerType { CONTROLLER, MIDDLEWARE, } ================================================ FILE: src/types/enums/ParameterType.ts ================================================ export enum ParameterType { CUSTOM, CONNECTED_SOCKET, MESSAGE_BODY, MESSAGE_ACK, SOCKET_QUERY_PARAM, SOCKET_IO, SOCKET_ID, SOCKET_REQUEST, SOCKET_ROOMS, NAMESPACE_PARAMS, NAMESPACE_PARAM, } ================================================ FILE: src/types/enums/ResultType.ts ================================================ export enum ResultType { EMIT_ON_SUCCESS, EMIT_ON_FAIL, SKIP_EMIT_ON_EMPTY_RESULT, } ================================================ FILE: src/types/enums/SocketEventType.ts ================================================ export enum SocketEventType { MESSAGE, CONNECT, DISCONNECT, DISCONNECTING, } ================================================ FILE: src/util/add-action-to-controller-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; import { ActionMetadata } from '../types/ActionMetadata'; import { getMetadata } from './get-metadata'; import { ControllerMetadata } from '../types/ControllerMetadata'; export const addActionToControllerMetadata = ( target: Function, actionMetadata: Pick ) => { const existingMetadata = getMetadata(target); (Reflect as any).defineMetadata( SOCKET_CONTROLLER_META_KEY, { ...existingMetadata, actions: { ...existingMetadata?.actions, [actionMetadata.methodName]: { ...existingMetadata?.actions?.[actionMetadata.methodName], ...actionMetadata, }, }, }, target ); }; ================================================ FILE: src/util/add-controller-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; import { ControllerMetadata } from '../types/ControllerMetadata'; import { getMetadata } from './get-metadata'; export const addControllerMetadata = (target: Function, metadata: Pick) => { const existingMetadata = getMetadata(target); (Reflect as any).defineMetadata( SOCKET_CONTROLLER_META_KEY, { ...existingMetadata, ...metadata, }, target ); }; ================================================ FILE: src/util/add-interceptor-to-action-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; import { getMetadata } from './get-metadata'; import { ControllerMetadata } from '../types/ControllerMetadata'; export const addInterceptorToActionMetadata = (target: Function, methodName: string, interceptor: Function) => { const existingMetadata = getMetadata(target); (Reflect as any).defineMetadata( SOCKET_CONTROLLER_META_KEY, { ...existingMetadata, actions: { ...existingMetadata?.actions, [methodName]: { ...existingMetadata?.actions?.[methodName], interceptors: [interceptor, ...(existingMetadata?.actions?.[methodName]?.interceptors || [])], }, }, }, target ); }; ================================================ FILE: src/util/add-middleware-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; import { MiddlewareMetadata } from '../types/MiddlewareMetadata'; export const addMiddlewareMetadata = (target: Function, metadata: MiddlewareMetadata) => { (Reflect as any).defineMetadata(SOCKET_CONTROLLER_META_KEY, metadata, target); }; ================================================ FILE: src/util/add-parameter-to-action-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; import { ParameterMetadata } from '../types/ParameterMetadata'; import { getMetadata } from './get-metadata'; import { ControllerMetadata } from '../types/ControllerMetadata'; export const addParameterToActionMetadata = (target: Function, methodName: string, args: ParameterMetadata) => { const existingMetadata = getMetadata(target); (Reflect as any).defineMetadata( SOCKET_CONTROLLER_META_KEY, { ...existingMetadata, actions: { ...existingMetadata?.actions, [methodName]: { ...existingMetadata?.actions?.[methodName], parameters: [args, ...(existingMetadata?.actions?.[methodName]?.parameters || [])], }, }, }, target ); }; ================================================ FILE: src/util/add-result-to-action-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; import { ResultMetadata } from '../types/ResultMetadata'; import { getMetadata } from './get-metadata'; import { ControllerMetadata } from '../types/ControllerMetadata'; export const addResultToActionMetadata = (target: Function, methodName: string, args: ResultMetadata) => { const existingMetadata = getMetadata(target); (Reflect as any).defineMetadata( SOCKET_CONTROLLER_META_KEY, { ...existingMetadata, actions: { ...existingMetadata?.actions, [methodName]: { ...existingMetadata?.actions?.[methodName], results: [args, ...(existingMetadata?.actions?.[methodName]?.results || [])], }, }, }, target ); }; ================================================ FILE: src/util/chain-execute.ts ================================================ export function chainExecute(context: any, chain: Function[]) { function next() { const middleware: Function = chain.shift() as Function; if (middleware && typeof middleware === 'function') { return middleware(context, next); } } return next(); } ================================================ FILE: src/util/get-metadata.ts ================================================ import { SOCKET_CONTROLLER_META_KEY } from '../types/SocketControllerMetaKey'; export const getMetadata = (target: T): U => { return (Reflect as any).getMetadata(SOCKET_CONTROLLER_META_KEY, target); }; ================================================ FILE: test/functional/connected-socket.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; describe('ConnectedSocket', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Connected socket is retrieved correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { testResult = socket.id; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(wsClient.id).toEqual(testResult); }); }); ================================================ FILE: test/functional/controllers/test.controller.ts ================================================ import { ConnectedSocket, OnConnect, SocketController } from '../../../src'; import { Service } from 'typedi'; @SocketController() @Service() export class TestController { @OnConnect() connected(@ConnectedSocket() socket: any) { socket.emit('connected'); } } ================================================ FILE: test/functional/controllers/test2.controller.ts ================================================ import { ConnectedSocket, OnMessage, SocketController } from '../../../src'; import { Service } from 'typedi'; @SocketController() @Service() export class Test2Controller { @OnMessage('test') connected(@ConnectedSocket() socket: any) { socket.emit('response'); } } ================================================ FILE: test/functional/create-socket-server.spec.ts ================================================ import { SocketController, SocketControllers } from '../../src'; import { testConnection } from '../utilities/testSocketConnection'; import { Container, Service } from 'typedi'; describe('Create socket server', () => { let wsApp: SocketControllers; const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; afterEach(() => { return new Promise(resolve => { if (wsApp) return wsApp.io.close(() => { resolve(null); }); resolve(null); }); }); it('should create socket server without options', async () => { expect.assertions(1); wsApp = new SocketControllers({ port: PORT, container: Container }); expect(await testConnection(PATH_FOR_CLIENT)).toEqual(0); }); it('should create socket server with empty controllers array in options', async () => { expect.assertions(1); wsApp = new SocketControllers({ port: PORT, controllers: [], container: Container }); expect(await testConnection(PATH_FOR_CLIENT)).toEqual(0); }); it('should create socket server with controllers array in options', async () => { expect.assertions(1); @SocketController() @Service() class TestController {} wsApp = new SocketControllers({ port: PORT, container: Container, controllers: [TestController] }); expect(await testConnection(PATH_FOR_CLIENT)).toEqual(0); }); }); ================================================ FILE: test/functional/emit-on-fail.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { EmitOnFail, OnMessage } from '../../src'; import { waitForTime } from '../utilities/waitForTime'; describe('EmitOnFail', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Emit defined event on failing sync execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() @EmitOnFail('fail') connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnFail('fail') testEvent() { throw new Error('error string'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const errors = []; wsClient.on('fail', data => { errors.push(data); }); await waitForEvent(wsClient, 'connected'); expect(errors.length).toEqual(0); wsClient.emit('request'); await waitForEvent(wsClient, 'fail'); expect(errors[0]).toEqual('error string'); expect(errors.length).toEqual(1); }); it('Emit defined event on failing async execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() @EmitOnFail('fail') connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnFail('fail') async testEvent() { throw new Error('error string'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const errors = []; wsClient.on('fail', data => { errors.push(data); }); await waitForEvent(wsClient, 'connected'); expect(errors.length).toEqual(0); wsClient.emit('request'); await waitForEvent(wsClient, 'fail'); expect(errors[0]).toEqual('error string'); expect(errors.length).toEqual(1); }); it('Emit defined event on failing with specific error type', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnFail('fail1', { errorType: RangeError }) @EmitOnFail('fail2', { errorType: TypeError }) @EmitOnFail('fail3') async testEvent() { throw new RangeError('range error'); } @OnMessage('request2') @EmitOnFail('fail1', { errorType: RangeError }) @EmitOnFail('fail2', { errorType: TypeError }) @EmitOnFail('fail3') async testEvent2() { throw new TypeError('type error'); } @OnMessage('request3') @EmitOnFail('fail1', { errorType: RangeError }) @EmitOnFail('fail2', { errorType: TypeError }) @EmitOnFail('fail3') async testEvent3() { throw new Error('test error'); } @OnMessage('request4') @EmitOnFail('fail1', { errorType: Error }) @EmitOnFail('fail2', { errorType: TypeError }) @EmitOnFail('fail3') async testEvent4() { throw new TypeError('type error 2'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const errors = { fail1: [], fail2: [], fail3: [] }; wsClient.on('fail1', data => { errors.fail1.push(data); }); wsClient.on('fail2', data => { errors.fail2.push(data); }); wsClient.on('fail3', data => { errors.fail3.push(data); }); await waitForEvent(wsClient, 'connected'); wsClient.emit('request'); wsClient.emit('request2'); wsClient.emit('request3'); wsClient.emit('request4'); await waitForTime(1000); expect(errors).toEqual({ fail1: ['range error', 'type error 2'], fail2: ['type error'], fail3: ['test error'], }); }); }); ================================================ FILE: test/functional/emit-on-success.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { EmitOnSuccess, OnMessage } from '../../src'; describe('EmitOnSuccess', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Emit defined event on successful sync execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnSuccess('response') testEvent() { throw new Error('error string'); } @OnMessage('request2') @EmitOnSuccess('response') testEvent2() { return 'data'; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const responses = []; wsClient.on('response', data => { responses.push(data); }); await waitForEvent(wsClient, 'connected'); expect(responses.length).toEqual(0); wsClient.emit('request'); wsClient.emit('request2'); await waitForEvent(wsClient, 'response'); expect(responses[0]).toEqual('data'); expect(responses.length).toEqual(1); }); it('Emit defined event on successful async execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnSuccess('response') async testEvent() { throw new Error('error string'); } @OnMessage('request2') @EmitOnSuccess('response') async testEvent2() { return 'data'; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const responses = []; wsClient.on('response', data => { responses.push(data); }); await waitForEvent(wsClient, 'connected'); expect(responses.length).toEqual(0); wsClient.emit('request'); wsClient.emit('request2'); await waitForEvent(wsClient, 'response'); expect(responses[0]).toEqual('data'); expect(responses.length).toEqual(1); }); }); ================================================ FILE: test/functional/load-controllers-from-directory.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container } from 'typedi'; import { waitForEvent } from '../utilities/waitForEvent'; import path from 'path'; describe('Load controllers from directory', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Load controllers from directory', async () => { socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [path.join(__dirname, './controllers/**.*')], }); wsClient = io(PATH_FOR_CLIENT, { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); await waitForEvent(wsClient, 'response'); expect(true).toEqual(true); }); }); ================================================ FILE: test/functional/middlewares.spec.ts ================================================ import { Server } from 'socket.io'; import { Socket, io } from 'socket.io-client'; import { Container, Service } from 'typedi'; import { waitForEvent } from '../utilities/waitForEvent'; import { createServer, Server as HttpServer } from 'http'; import { Middleware } from '../../src/decorators/Middleware'; import { MiddlewareInterface } from '../../src/types/MiddlewareInterface'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { SocketControllers } from '../../src/SocketControllers'; describe('Middlewares', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult: string[] = []; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = []; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('no namespace', async () => { @Middleware() @Service() class GlobalMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('global middleware'); next(); } } @SocketController() @Service() class Controller { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [GlobalMiddleware], controllers: [Controller], }); wsClient = io(PATH_FOR_CLIENT, { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['global middleware']); }); describe('string namespace', () => { it('correct namespace', async () => { @Middleware({ namespace: '/string' }) @Service() class StringNamespaceMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('string middleware'); next(); } } @Middleware() @Service() class MiddlewareWithoutNamespace implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('middleware without namespace'); next(); } } @SocketController('/string') @Service() class StringNamespaceController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [StringNamespaceMiddleware, MiddlewareWithoutNamespace], controllers: [StringNamespaceController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['string middleware', 'middleware without namespace']); }); it('incorrect namespace', async () => { @Middleware({ namespace: '/string' }) @Service() class StringNamespaceMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('string middleware'); next(); } } @Middleware() @Service() class MiddlewareWithoutNamespace implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('middleware without namespace'); next(); } } @SocketController('/string2') @Service() class String2NamespaceController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [StringNamespaceMiddleware, MiddlewareWithoutNamespace], controllers: [String2NamespaceController], }); wsClient = io(PATH_FOR_CLIENT + '/string2', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['middleware without namespace']); }); }); describe('regexp namespace', () => { it('correct namespace', async () => { @Middleware({ namespace: /^\/dynamic-\d+$/ }) @Service() class RegexpNamespaceMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push(socket.nsp.name as string); next(); } } @Middleware() @Service() class MiddlewareWithoutNamespace implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('middleware without namespace'); next(); } } @SocketController(/^\/dynamic-\d+$/) @Service() class RegexpNamespaceController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [RegexpNamespaceMiddleware, MiddlewareWithoutNamespace], controllers: [RegexpNamespaceController], }); wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['/dynamic-1', 'middleware without namespace']); }); it('incorrect namespace', async () => { @Middleware({ namespace: /^\/dynamic-\s+$/ }) @Service() class RegexpNamespaceMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push(socket.nsp.name as string); next(); } } @Middleware() @Service() class MiddlewareWithoutNamespace implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('middleware without namespace'); next(); } } @SocketController(/^\/dynamic-\d+$/) @Service() class RegexpNamespaceController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [RegexpNamespaceMiddleware, MiddlewareWithoutNamespace], controllers: [RegexpNamespaceController], }); wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['middleware without namespace']); }); }); describe('array namespace', () => { it('correct namespace', async () => { @Middleware({ namespace: [/^\/dynamic-\d+$/] }) @Service() class RegexpArrayNamespaceMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push(socket.nsp.name as string); next(); } } @Middleware() @Service() class MiddlewareWithoutNamespace implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('middleware without namespace'); next(); } } @SocketController(/^\/dynamic-\d+$/) @Service() class RegexpNamespaceController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [RegexpArrayNamespaceMiddleware, MiddlewareWithoutNamespace], controllers: [RegexpNamespaceController], }); wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['/dynamic-1', 'middleware without namespace']); }); it('incorrect namespace', async () => { @Middleware({ namespace: [/^\/dynamic-\s+$/] }) @Service() class RegexpArrayNamespaceMiddleware implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push(socket.nsp.name as string); next(); } } @Middleware() @Service() class MiddlewareWithoutNamespace implements MiddlewareInterface { use(socket: any, next: (err?: any) => any): any { testResult.push('middleware without namespace'); next(); } } @SocketController(/^\/dynamic-\d+$/) @Service() class RegexpNamespaceController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, middlewares: [RegexpArrayNamespaceMiddleware, MiddlewareWithoutNamespace], controllers: [RegexpNamespaceController], }); wsClient = io(PATH_FOR_CLIENT + '/dynamic-1', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual(['middleware without namespace']); }); }); }); ================================================ FILE: test/functional/multiple-controllers-on-same-namespace.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { ConnectedSocket, OnConnect, SocketController, SocketControllers } from '../../src'; import { Container, Service } from 'typedi'; import { waitForTime } from '../utilities/waitForTime'; describe('Multiple controllers with same namespace', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('using string namespace', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { testResult = [...(testResult || []), '1']; socket.emit('connected'); } } @SocketController('/string') @Service() class TestController2 { @OnConnect() connected(@ConnectedSocket() socket: Socket) { testResult = [...(testResult || []), '2']; socket.emit('connected2'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController, TestController2], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForTime(100); expect(testResult).toContain('1'); expect(testResult).toContain('2'); }); }); ================================================ FILE: test/functional/nsp-param.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { NspParam } from '../../src'; describe('NspParam', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Namespace param is retrieved correctly', async () => { @SocketController('/:first/:second') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @NspParam('second') parameter: string) { testResult = parameter; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/test1/test2', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual('test2'); }); }); ================================================ FILE: test/functional/nsp-params.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { NspParams } from '../../src'; describe('NspParams', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Namespace params are retrieved correctly', async () => { @SocketController('/:first/:second') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @NspParams() parameters: { first: string; second: string }) { testResult = parameters; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/test1/test2', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual({ first: 'test1', second: 'test2' }); }); }); ================================================ FILE: test/functional/on-disconnect.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { OnDisconnect } from '../../src'; import { waitForTime } from '../utilities/waitForTime'; describe('OnDisconnect', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('OnDisconnect is called correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnDisconnect() disconnected() { testResult = 'disconnected'; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.disconnect(); await waitForTime(1000); expect(testResult).toEqual('disconnected'); }); }); ================================================ FILE: test/functional/on-disconnecting.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { OnDisconnect } from '../../src'; import { waitForTime } from '../utilities/waitForTime'; import { OnDisconnecting } from '../../src/decorators/OnDisconnecting'; describe('OnDisconnecting', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult = []; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = []; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('OnDisconnect is called correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnDisconnect() disconnected() { testResult.push('disconnected'); } @OnDisconnecting() disconnecting() { testResult.push('disconnecting'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.disconnect(); await waitForTime(1000); expect(testResult).toEqual(['disconnecting', 'disconnected']); }); }); ================================================ FILE: test/functional/parameter-transformation.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { MessageBody, OnMessage } from '../../src'; import { Expose } from 'class-transformer'; describe('Parameter transformation', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Parameters are converted correctly with the given options', async () => { class Body { @Expose() prop1: string; @Expose() prop2: number; } @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { testResult = socket.id; socket.emit('connected'); } @OnMessage('test') test( @ConnectedSocket() socket: Socket, @MessageBody({ transform: true, transformOptions: { excludeExtraneousValues: true, enableImplicitConversion: true }, }) body: Body ) { testResult = body; socket.emit('result'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test', { prop1: 'test', prop2: '2', prop3: 10 }); await waitForEvent(wsClient, 'result'); expect(testResult).toBeInstanceOf(Body); expect(testResult.prop1).toEqual('test'); expect(testResult.prop2).toEqual(2); expect(testResult.prop3).toEqual(undefined); }); }); ================================================ FILE: test/functional/scoped-controllers.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, ContainerInstance, Inject, Service, Token } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { EmitOnSuccess, OnMessage, SocketEventContext } from '../../src'; describe('Scoped controllers', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult = []; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = []; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('two instances should be different', async () => { @Service() class TestService {} @SocketController('/string') @Service() class TestController { constructor(private testService: TestService) {} @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('test') @EmitOnSuccess('done') test() { testResult.push(this.testService); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], scopedContainerGetter: (args: SocketEventContext) => { return Container.of(Math.random().toString()); }, }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); expect(testResult[0]).not.toBe(testResult[1]); }); it('two global instances should be the same', async () => { @Service({ global: true }) class TestService {} @SocketController('/string') @Service() class TestController { constructor(private testService: TestService) {} @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('test') @EmitOnSuccess('done') test() { testResult.push(this.testService); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], scopedContainerGetter: (args: SocketEventContext) => { return Container.of(Math.random().toString()); }, }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); expect(testResult[0]).toBe(testResult[1]); }); it('additional injectables should be retrievable', async () => { const token = new Token('ADDITIONAL'); let counter = 0; @Service({ global: true }) class TestService {} @SocketController('/string') @Service() class TestController { constructor( private testService: TestService, @Inject(token) public myAdditional: number ) {} @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('test') @EmitOnSuccess('done') test() { testResult.push(this.myAdditional); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], scopedContainerGetter: (args: SocketEventContext) => { const container = Container.of(counter.toString()); container.set(token, counter); counter++; return container; }, }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); expect(testResult[0]).toBe(1); expect(testResult[1]).toBe(2); }); it('arguments should be provided correctly to getter', async () => { @SocketController('/:test') @Service() class TestController { @OnConnect() @EmitOnSuccess('connected') connected(@ConnectedSocket() socket: Socket) {} @OnMessage('test') @EmitOnSuccess('done') test() {} } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], scopedContainerGetter: (args: SocketEventContext) => { testResult.push(args); return Container.of(''); }, }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test', 'args1'); await waitForEvent(wsClient, 'done'); expect(testResult[1].socket.id).toEqual(wsClient.id); expect(testResult[1].socketIo).toBe(socketControllers.io); expect(testResult[1].eventName).toBe('test'); expect(testResult[1].messageArgs).toEqual(['args1']); expect(testResult[1].nspParams).toEqual({ test: 'string' }); }); it('container should be disposed', async () => { const token = new Token('ADDITIONAL'); @Service({ global: true }) class TestService {} @SocketController('/string') @Service() class TestController { constructor( private testService: TestService, @Inject(token) public myAdditional: number ) {} @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('test') @EmitOnSuccess('done') test() { testResult.push(this.myAdditional); } } let container; socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], scopedContainerGetter: () => { container = Container.of('test'); container.set(token, 'test'); return container; }, scopedContainerDisposer: (scopedContainer: ContainerInstance) => { scopedContainer.reset({ strategy: 'resetServices' }); }, }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); await waitForEvent(wsClient, 'done'); expect(Container.has(TestService)).toBe(true); expect(container.has(token)).toBe(false); expect(container.has(TestController)).toBe(false); }); }); ================================================ FILE: test/functional/skip-emit-on-empty-result.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { EmitOnFail, EmitOnSuccess, OnMessage, SkipEmitOnEmptyResult } from '../../src'; describe('SkipEmitOnEmptyResult', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Skip emit of defined event on successful sync execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnSuccess('response') @SkipEmitOnEmptyResult() testEvent() { return { data: true }; } @OnMessage('request2') @EmitOnSuccess('response') @SkipEmitOnEmptyResult() testEvent2() { return null; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const responses = []; wsClient.on('response', data => { responses.push(data); }); await waitForEvent(wsClient, 'connected'); expect(responses.length).toEqual(0); wsClient.emit('request2'); wsClient.emit('request'); await waitForEvent(wsClient, 'response'); expect(responses.length).toEqual(1); expect(responses[0]).toEqual({ data: true }); }); it('Skip emit of defined event on successful async execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() async connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnSuccess('response') @SkipEmitOnEmptyResult() async testEvent() { return { data: true }; } @OnMessage('request2') @EmitOnSuccess('response') @SkipEmitOnEmptyResult() async testEvent2() { return null; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const responses = []; wsClient.on('response', data => { responses.push(data); }); await waitForEvent(wsClient, 'connected'); expect(responses.length).toEqual(0); wsClient.emit('request2'); wsClient.emit('request'); await waitForEvent(wsClient, 'response'); expect(responses.length).toEqual(1); expect(responses[0]).toEqual({ data: true }); }); it('Skip emit of defined event on failing sync execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnFail('response') @SkipEmitOnEmptyResult() testEvent() { throw new Error('error string'); } @OnMessage('request2') @EmitOnFail('response') @SkipEmitOnEmptyResult() testEvent2() { return Promise.reject(null); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const responses = []; wsClient.on('response', data => { responses.push(data); }); await waitForEvent(wsClient, 'connected'); expect(responses.length).toEqual(0); wsClient.emit('request2'); wsClient.emit('request'); await waitForEvent(wsClient, 'response'); expect(responses.length).toEqual(1); expect(responses[0]).toEqual('error string'); }); it('Skip emit of defined event on failing async execution', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() async connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('request') @EmitOnFail('response') @SkipEmitOnEmptyResult() async testEvent() { throw new Error('error string'); } @OnMessage('request2') @EmitOnFail('response2') @SkipEmitOnEmptyResult() async testEvent2(@ConnectedSocket() socket: Socket) { return Promise.reject(null); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); const responses = []; wsClient.on('response', data => { responses.push(data); }); await waitForEvent(wsClient, 'connected'); expect(responses.length).toEqual(0); wsClient.emit('request2'); wsClient.emit('request'); await waitForEvent(wsClient, 'response'); expect(responses.length).toEqual(1); expect(responses[0]).toEqual('error string'); }); }); ================================================ FILE: test/functional/socket-id.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { SocketId } from '../../src'; describe('SocketId', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Connected socket id is retrieved correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketId() socketId: string) { testResult = socketId; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(wsClient.id).toEqual(testResult); }); }); ================================================ FILE: test/functional/socket-io.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { SocketIO } from '../../src'; describe('SocketIo', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Socket.io instance is retrieved correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketIO() socketIO: Server) { testResult = socketIO; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(socketControllers.io).toEqual(testResult); }); }); ================================================ FILE: test/functional/socket-message-ack.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { MessageAck, MessageBody, OnMessage, SocketId } from '../../src'; describe('MessageAck', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let testAckResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; testAckResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Event ack is retrieved correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketId() socketId: string) { testResult = socketId; socket.emit('connected'); } @OnMessage('test') test(@MessageBody() data: any, @ConnectedSocket() socket: Socket, @MessageAck() ack: Function) { testResult = data; testAckResult = ack; socket.emit('return'); } @OnMessage('test2') test2( @MessageAck() ack: Function, @MessageBody({ index: 1 }) data1: any, @MessageBody({ index: 0 }) data0: any, @ConnectedSocket() socket: Socket ) { testResult = { data1, data0 }; ack?.('test ack2'); socket.emit('return2'); } @OnMessage('test3') test3( @ConnectedSocket() socket: Socket, @MessageAck() ack: Function, @MessageBody({ index: 1 }) data1: any, @MessageBody({ index: 0 }) data0: any ) { testResult = { data1, data0 }; testAckResult = ack; socket.emit('return3'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); const ack = (ack: any) => (testAckResult = ack); wsClient.emit('test', 'test data'); await waitForEvent(wsClient, 'return'); expect(testResult).toEqual('test data'); expect(testAckResult).toBeNull(); wsClient.emit('test2', 'test data 0', 'test data 1', 'test data 2', ack); await waitForEvent(wsClient, 'return2'); expect(testResult).toEqual({ data0: 'test data 0', data1: 'test data 1' }); expect(testAckResult).toEqual('test ack2'); // ack should be the last parameter wsClient.emit('test3', 'test data 0', 'test data 1', ack, 'test data 2'); await waitForEvent(wsClient, 'return3'); expect(testResult).toEqual({ data0: 'test data 0', data1: 'test data 1' }); expect(testAckResult).toBeNull(); }); }); ================================================ FILE: test/functional/socket-message-body.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { MessageBody, OnMessage, SocketId } from '../../src'; describe('MessageBody', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Event body is retrieved correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketId() socketId: string) { testResult = socketId; socket.emit('connected'); } @OnMessage('test') test(@MessageBody() data: any, @ConnectedSocket() socket: Socket) { testResult = data; socket.emit('return'); } @OnMessage('test2') test2( @MessageBody({ index: 1 }) data1: any, @MessageBody({ index: 0 }) data0: any, @ConnectedSocket() socket: Socket ) { testResult = { data1, data0 }; socket.emit('return2'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test', 'test data'); await waitForEvent(wsClient, 'return'); expect(testResult).toEqual('test data'); wsClient.emit('test2', 'test data 0', 'test data 1', 'test data 2', ack => { console.log(ack); }); await waitForEvent(wsClient, 'return2'); expect(testResult).toEqual({ data0: 'test data 0', data1: 'test data 1' }); }); }); ================================================ FILE: test/functional/socket-query-param.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { ConnectedSocket, OnConnect, SocketController, SocketControllers, SocketQueryParam } from '../../src'; import { Container, Service } from 'typedi'; import { waitForEvent } from '../utilities/waitForEvent'; describe('SocketQueryParam', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Socket query param is retrieved correctly', async () => { @SocketController() @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketQueryParam('testParam') parameter: string) { testResult = parameter; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '?testParam=testValue', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult).toEqual('testValue'); }); }); ================================================ FILE: test/functional/socket-request.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { ConnectedSocket, OnConnect, SocketController, SocketControllers, SocketQueryParam, SocketRequest, } from '../../src'; import { Container, Service } from 'typedi'; import { waitForEvent } from '../utilities/waitForEvent'; describe('SocketRequest', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Socket request is retrieved correctly', async () => { @SocketController() @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketRequest() request: any) { testResult = request; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '?testParam=testValue', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect(testResult.url).toContain('/socket.io/?testParam=testValue&EIO='); }); }); ================================================ FILE: test/functional/socket-rooms.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { SocketRooms } from '../../src'; describe('SocketRooms', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = undefined; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('Socket rooms set is retrieved correctly', async () => { @SocketController('/string') @Service() class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @SocketRooms() rooms: any) { testResult = rooms; socket.emit('connected'); } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); expect([...(testResult as Set).keys()][0]).toEqual(wsClient.id); }); }); ================================================ FILE: test/functional/use-interceptor.spec.ts ================================================ import { createServer, Server as HttpServer } from 'http'; import { Server } from 'socket.io'; import { io, Socket } from 'socket.io-client'; import { SocketControllers } from '../../src/SocketControllers'; import { Container, Service } from 'typedi'; import { SocketController } from '../../src/decorators/SocketController'; import { OnConnect } from '../../src/decorators/OnConnect'; import { ConnectedSocket } from '../../src/decorators/ConnectedSocket'; import { waitForEvent } from '../utilities/waitForEvent'; import { EmitOnSuccess, MessageBody, OnMessage, SocketEventType } from '../../src'; import { UseInterceptor } from '../../src/decorators/UseInterceptor'; import { InterceptorInterface } from '../../src/types/InterceptorInterface'; import { SocketEventContext } from '../../src/types/SocketEventContext'; describe('UseInterceptor', () => { const PORT = 8080; const PATH_FOR_CLIENT = `ws://localhost:${PORT}`; let httpServer: HttpServer; let wsApp: Server; let wsClient: Socket; let testResult = []; let socketControllers: SocketControllers; beforeEach(done => { httpServer = createServer(); wsApp = new Server(httpServer, { cors: { origin: '*', }, }); httpServer.listen(PORT, () => { done(); }); }); afterEach(() => { testResult = []; Container.reset(); wsClient.close(); wsClient = null; socketControllers = null; return new Promise(resolve => { if (wsApp) return wsApp.close(() => { resolve(null); }); resolve(null); }); }); it('interceptor should be executed in the correct order', async () => { @Service() class testInterceptor implements InterceptorInterface { use(ctx: SocketEventContext, next: any) { testResult.push('testInterceptor start'); const resp = next(); testResult.push('testInterceptor end'); return resp; } } const plain: InterceptorInterface = { use: (ctx: SocketEventContext, next: () => any) => { testResult.push('plain start'); const resp = next(); testResult.push('plain end'); return resp; }, }; @SocketController('/string') @Service() @UseInterceptor(plain) class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('test') @UseInterceptor(testInterceptor) @EmitOnSuccess('finished') test() { testResult.push('action'); return 'test'; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); const response = await waitForEvent(wsClient, 'finished'); expect(testResult).toEqual([ 'plain start', 'plain end', 'plain start', 'testInterceptor start', 'action', 'testInterceptor end', 'plain end', ]); expect(response).toEqual('test'); }); it('interceptor should be able to skip further actions', async () => { @Service() class testInterceptor implements InterceptorInterface { use(ctx: SocketEventContext, next: any) { testResult.push('testInterceptor start'); const response = next(); testResult.push('testInterceptor end'); return response; } } const plain: InterceptorInterface = { use: (ctx: SocketEventContext, next: () => any) => { testResult.push('plain start'); if (ctx.eventType === SocketEventType.CONNECT) { next(); } testResult.push('plain end'); return 'plain response'; }, }; @SocketController('/string') @Service() @UseInterceptor(plain) class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket) { socket.emit('connected'); } @OnMessage('test') @UseInterceptor(testInterceptor) @EmitOnSuccess('finished') test() { testResult.push('action'); return 'test'; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test'); const response = await waitForEvent(wsClient, 'finished'); expect(testResult).toEqual(['plain start', 'plain end', 'plain start', 'plain end']); expect(response).toEqual('plain response'); }); it('interceptor should be able to mutate the context', async () => { @Service() class testInterceptor implements InterceptorInterface { use(ctx: SocketEventContext, next: any) { testResult.push(ctx.messageArgs?.[0]); ctx.messageArgs = ['testInterceptor']; return next(); } } const plain: InterceptorInterface = { use: (ctx: SocketEventContext, next: () => any) => { testResult.push(ctx.messageArgs?.[0]); ctx.messageArgs = ['plain']; return next(); }, }; @SocketController('/string') @Service() @UseInterceptor(plain) class TestController { @OnConnect() connected(@ConnectedSocket() socket: Socket, @MessageBody() message: any) { testResult.push(message); socket.emit('connected'); } @OnMessage('test') @UseInterceptor(testInterceptor) @EmitOnSuccess('finished') test(@MessageBody() message: any) { testResult.push(message); return 'test'; } } socketControllers = new SocketControllers({ io: wsApp, container: Container, controllers: [TestController], }); wsClient = io(PATH_FOR_CLIENT + '/string', { reconnection: false, timeout: 5000, forceNew: true }); await waitForEvent(wsClient, 'connected'); wsClient.emit('test', 'my body'); const response = await waitForEvent(wsClient, 'finished'); expect(testResult).toEqual([undefined, 'plain', 'my body', 'plain', 'testInterceptor']); expect(response).toEqual('test'); }); }); ================================================ FILE: test/utilities/testSocketConnection.ts ================================================ import socketio from 'socket.io-client'; export async function testConnection(path: string) { return await new Promise((resolve, reject) => { const socket = socketio(path, { reconnection: false, timeout: 5000 }); socket.on('connect', () => socket.disconnect()); socket.on('connect_error', reject); socket.on('disconnect', () => { resolve(0); }); }); } ================================================ FILE: test/utilities/waitForEvent.ts ================================================ import { Socket } from 'socket.io-client'; import { Server } from 'socket.io'; export const waitForEvent = (socket: Socket | Server, event: string): Promise => { return new Promise(resolve => { socket.on(event, data => { resolve(data); }); }); }; ================================================ FILE: test/utilities/waitForTime.ts ================================================ export const waitForTime = (time: number): Promise => { return new Promise(resolve => { setTimeout(() => { resolve(null); }, time); }); }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "target": "es2018", "lib": ["es2018"], "outDir": "build", "rootDir": "./src", "strict": true, "sourceMap": true, "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "forceConsistentCasingInFileNames": true }, "exclude": ["node_modules", "sample", "**/*.spec.ts", "testing/**", "test/**"], } ================================================ FILE: tsconfig.prod.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "strict": false, "sourceMap": true, "removeComments": false, "declaration": true, }, } ================================================ FILE: tsconfig.spec.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "strict": false, "strictPropertyInitialization": false, "sourceMap": true, "inlineSourceMap": true, "removeComments": true, "noImplicitAny": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, "rootDir": "./" }, "exclude": ["node_modules"] }