paint-brush
Intercept It: How to Implement Different Logic For Different Functionsby@anatolii
851 reads
851 reads

Intercept It: How to Implement Different Logic For Different Functions

by Anatolii KabanovJune 8th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This article will be about the Interceptor design pattern and its usage via Ts.ED. In brief, the pattern injects some logical functionality before and/or after function execution. It has the same nature as the Middleware, but instead of working with a request pipeline, the interceptor handle function invocation and can repeat function execution with different parameters. Let’s assume the scenario where it could be helpful. I have a service that sends requests to some black box. And the black box is not so friendly and in case of exception very possible, there will be only an HTTP status code without any explanation.

Company Mentioned

Mention Thumbnail
featured image - Intercept It: How to Implement Different Logic For Different Functions
Anatolii Kabanov HackerNoon profile picture


This article will be about the Interceptor design pattern and its usage via Ts.ED.


In brief, the pattern injects some logical functionality before and/or after function execution.


It has the same nature as the Middleware, but instead of working with a request pipeline, the interceptor handle function invocation and can repeat function execution with different parameters.


Let’s assume the scenario where it could be helpful. I have a service that sends requests to some black box. And the black box is not so friendly and in case of exception very possible, there will be only an HTTP status code without any explanation.


In case of receiving a 403 status code most probably need to try re-authenticate the user and repeat the request to the service. So basic retry mechanism with the request will not suit this scenario, because there could be multiple different endpoints.


It could look like this:


async createPermamentCell(shapeId: number): Promise<Cell> {
    try {
      return await this.createCell(`shape-${shapeId}`, true);
    } catch (error: any) {
       // try auth again
       if (error.status === 403) {
          const userEmail = httpContext.get(REQUEST_USER);
          const token = await this.authenticate(userEmail);
          httpContext.set(CLIENT,
            new Client({
              token: token,
            })
          );
          return await this.createCell(`shape-${shapeId}`, true);
       }
    }
}

// ...As well for other functions same try catch


Or like this:


async retryOn403(functionToCall: (...args: any[]) => any, ...args: any[]): Promise<Cell> {
    try {
      await functionToCall(...args);
    } catch (error: any) {
       // try auth again
       if (error.status === 403) {
          const userEmail = httpContext.get(REQUEST_USER);
          const token = await this.authenticate(userEmail);
          httpContext.set(CLIENT,
            new Client({
              token: token,
            })
          );
          return await functionToCall(...args);
       }
    }
}

// The function will be wrapped 


But these things can get out of control and start to live their own life. In other words, such code will be more difficult to maintain.


Changes in part of the code will cause a change in other parts of the code, or vice versa changing in one place reshapes the code that is expected not to be touched.


Let’s implement the same logic via the Interceptor approach. And for the example, I will use Ts.ED framework. Of course, other frameworks also contain this mechanism out of the box like in Nestjs or .Net, e.t.c.


I require to create a new class and implement the interface InterceptorMethods and use decorator @Interceptor().


@Interceptor()
export class ReAuthInterceptor implements InterceptorMethods {
    constructor() {}

    async intercept(context: InterceptorContext<any>, next: InterceptorNext) {
        try {
            // It's possible to do somthing before call
            const result = await next();
            // Or right after the call
            return result;
        } catch (error: any) {
            if (error.status === 403) {
                try {
                    const userEmail = httpContext.get(REQUEST_USER);
                    const token = await this.authenticate(userEmail);
                    httpContext.set(CLIENT,
                      new Client({
                        token: token,
                      })
                    );
                    return next();
                } catch (error: any) {
                    _log.warn('Failed re auth', error);
                }
                // In case of exception on re auth return original exception
                return next(error);
            }
            // All other cases immediately return error
            return next(error);
        }
    }
}


And any service or particular function can be decorated with the new interceptor @Intercept(ReAuthInterceptor).


@Intercept(ReAuthInterceptor)
export class CellService implements CellService {
    constructor(@Inject(ClientProvider) private clientProvider: ClientProvider) {}

    // ... Many fucntions 

}

    


In the context parameter of interceptor you can find arguments or options (the second parameter that you may use in the decorator @Intercept(ReAuthInterceptor, ‘My opts’) . It could help to implement different logic for different functions.


There is one specific feature when you will try to use the interceptor in Ts.ED on the whole class with async and sync functions, e.g.:


@Intercept(ReAuthInterceptor)
export class CellService implements CellService {
    constructor(@Inject(ClientProvider) private clientProvider: ClientProvider) {}

    async createCell() {
      // ...
    } 

    getCurrentCell(): string {
      // ...
      return `some str`;
    }

}


The actual result of the second function getCurrentCell() will be not the string, instead of this type, it will be Promise<string>.


But otherwise, the code looks more understandable and robust via Intercept approach, all things in one place, you can still process the functions differently, and test logic in one place.