Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,13 +487,13 @@ You can enable scoped controllers by providing a `scopedContainerGetter` functio

You will get a new instance for each event in the controller.

The `scopedContainerGetter` function receives a parameter which contains the socket, socket.io instance, event type, event name, namespace parameters and the message arguments if they are applicable.
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, ScopedContainerGetterParams } from 'socket-controllers';
import { SocketControllers, SocketEventContext } from 'socket-controllers';
import { Container, ContainerInstance, Token } from "typedi";

const myDiToken = new Token();
Expand All @@ -502,7 +502,7 @@ const myDiToken = new Token();
const server = new SocketControllers({
port: 3000,
container: Container,
scopedContainerGetter: (args: ScopedContainerGetterParams) => {
scopedContainerGetter: (args: SocketEventContext) => {
const container = Container.of(YOUR_REQUEST_CONTEXT);
container.set(myDiToken, 'MY_VALUE');
return container;
Expand All @@ -515,6 +515,56 @@ const server = new SocketControllers({
});
```

## 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<Message[]> {
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 |
Expand Down
48 changes: 39 additions & 9 deletions src/SocketControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import { TransformOptions } from './types/TransformOptions';
import { defaultTransformOptions } from './types/constants/defaultTransformOptions';
import { ActionTransformOptions } from './types/ActionTransformOptions';
import { instanceToPlain, plainToInstance } from 'class-transformer';
import { ScopedContainerGetterParams } from './types/ScopedContainerGetterParams';
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<T>(someClass: { new (...args: any[]): T } | Function): T };
Expand Down Expand Up @@ -201,18 +203,44 @@ export class SocketControllers {
data?: any[],
ack?: Function | null
) {
const parameters = this.resolveParameters(socket, controller.metadata, action.parameters || [], data, ack);
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(
this.collectScopedContainerParams(socket, action.type, eventName, data, controller.metadata.namespace)
);
container = this.options.scopedContainerGetter(eventContext);
}

try {
const controllerInstance: any = container.get(controller.target);
const actionResult = controllerInstance[action.methodName](...parameters);

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) {
Expand Down Expand Up @@ -347,20 +375,22 @@ export class SocketControllers {
return value;
}

private collectScopedContainerParams(
private resolveEventContext(
socket: Socket,
eventType: SocketEventType,
eventName?: string,
messageBody?: any[],
namespace?: string | RegExp
): ScopedContainerGetterParams {
namespace?: string | RegExp,
ack?: Function | null
): SocketEventContext {
return {
eventType,
eventName,
socket,
socketIo: this.io,
nspParams: this.extractNamespaceParameters(socket, namespace),
messageArgs: messageBody,
ack,
};
}

Expand Down
20 changes: 20 additions & 0 deletions src/decorators/UseInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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);
}
}
};
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ 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';
1 change: 1 addition & 0 deletions src/types/ActionMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface ActionMetadata {
options: any;
parameters: ParameterMetadata[];
results: ResultMetadata[];
interceptors: Function[];
}
5 changes: 5 additions & 0 deletions src/types/InterceptorInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SocketEventContext } from './SocketEventContext';

export interface InterceptorInterface {
use(context: SocketEventContext, next: () => any): any;
}
4 changes: 2 additions & 2 deletions src/types/SocketControllersOptions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Server } from 'socket.io';
import { TransformOptions } from './TransformOptions';
import { ScopedContainerGetterParams } from './ScopedContainerGetterParams';
import { SocketEventContext } from './SocketEventContext';

export interface SocketControllersOptions {
container: { get<T>(someClass: { new (...args: any[]): T } | Function): T };

scopedContainerGetter?: (params: ScopedContainerGetterParams) => {
scopedContainerGetter?: (context: SocketEventContext) => {
get<T>(someClass: { new (...args: any[]): T } | Function): T;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { SocketEventType } from './enums/SocketEventType';
import { Server, Socket } from 'socket.io';

export interface ScopedContainerGetterParams {
export interface SocketEventContext {
socketIo: Server;
socket: Socket;
eventType: SocketEventType;
eventName?: string;
messageArgs?: any[];
nspParams?: Record<string, string>;
ack?: Function | null;
}
21 changes: 21 additions & 0 deletions src/util/add-interceptor-to-action-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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<any, ControllerMetadata>(target);
(Reflect as any).defineMetadata(
SOCKET_CONTROLLER_META_KEY,
{
...existingMetadata,
actions: {
...existingMetadata?.actions,
[methodName]: {
...existingMetadata?.actions?.[methodName],
interceptors: [interceptor, ...(existingMetadata?.actions?.[methodName]?.interceptors || [])],
},
},
},
target
);
};
11 changes: 11 additions & 0 deletions src/util/chain-execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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();
}
11 changes: 5 additions & 6 deletions test/functional/scoped-controllers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ 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';
import { ScopedContainerGetterParams } from '../../src/types/ScopedContainerGetterParams';
import { EmitOnSuccess, OnMessage, SocketEventContext } from '../../src';

describe('Scoped controllers', () => {
const PORT = 8080;
Expand Down Expand Up @@ -73,7 +72,7 @@ describe('Scoped controllers', () => {
io: wsApp,
container: Container,
controllers: [TestController],
scopedContainerGetter: (args: ScopedContainerGetterParams) => {
scopedContainerGetter: (args: SocketEventContext) => {
return Container.of(Math.random().toString());
},
});
Expand Down Expand Up @@ -112,7 +111,7 @@ describe('Scoped controllers', () => {
io: wsApp,
container: Container,
controllers: [TestController],
scopedContainerGetter: (args: ScopedContainerGetterParams) => {
scopedContainerGetter: (args: SocketEventContext) => {
return Container.of(Math.random().toString());
},
});
Expand Down Expand Up @@ -154,7 +153,7 @@ describe('Scoped controllers', () => {
io: wsApp,
container: Container,
controllers: [TestController],
scopedContainerGetter: (args: ScopedContainerGetterParams) => {
scopedContainerGetter: (args: SocketEventContext) => {
const container = Container.of(counter.toString());
container.set(token, counter);
counter++;
Expand Down Expand Up @@ -189,7 +188,7 @@ describe('Scoped controllers', () => {
io: wsApp,
container: Container,
controllers: [TestController],
scopedContainerGetter: (args: ScopedContainerGetterParams) => {
scopedContainerGetter: (args: SocketEventContext) => {
testResult.push(args);
return Container.of('');
},
Expand Down
Loading