Passed
Push — main ( 238ec8...50243d )
by Lorenzo
03:03
created

Cached.ts ➔ Cached   C

Complexity

Conditions 8

Size

Total Lines 73
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 56
dl 0
loc 73
c 0
b 0
f 0
ccs 33
cts 33
cp 1
rs 6.5733
cc 8
crap 8

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1 9
import { createHash } from 'node:crypto';
2
import { Cache } from '@/ExpressBeansTypes';
3 9
import { logger, registeredMethods } from '@/core';
4
import type { Request, Response } from 'express';
5 9
import { Executor } from '@/core/executor';
6
7
type BeanFunction = (...args: any) => any
8
type CacheEntry = {
9
  data: ReturnType<BeanFunction>,
10
  expiration: number,
11
}
12
13 15
const createKey = (obj: any) => createHash('sha224')
14
  .update(JSON.stringify(obj))
15
  .digest('hex');
16
17 9
const getCachedData = (cache: Map<string, CacheEntry>, key: string) => {
18 15
  const cached = cache.get(key);
19 15
  if (!cached) {
20 8
    throw new Error(`Key ${key} not found in cache`);
21
  }
22 7
  if (cached.expiration < Date.now()) {
23 1
    cache.delete(key);
24 1
    throw new Error(`Key ${key} expired in cache`);
25
  }
26 6
  return cached;
27
};
28
29 9
const isRoute = (args: unknown[]): args is [Request, Response] => {
30 15
  if (args.length !== 2) {
31 13
    return false;
32
  }
33 2
  const potentialReq = args[0] as any;
34 2
  const potentialRes = args[1] as any;
35
36 2
  return (
37
    potentialReq &&
38
    typeof potentialReq === 'object' &&
39
    'url' in potentialReq &&
40
    'method' in potentialReq &&
41
    'params' in potentialReq &&
42
    'query' in potentialReq &&
43
    potentialRes &&
44
    typeof potentialRes === 'object' &&
45
    'send' in potentialRes &&
46
    'json' in potentialRes
47
  );
48
};
49
50
/**
51
 * Caches the result of a method
52
 * or the response of a route handler using Response.send
53
 * @param options {Cache}
54
 * @decorator
55
 */
56 9
export function Cached<This>(
57
  options: Cache = { duration: 60_000, type: 'memory' },
58
) {
59 6
  return (
60
    method: BeanFunction,
61
    context: ClassMethodDecoratorContext<This, BeanFunction>,
62
  ) => {
63 6
    logger.debug(`Initializing cache for method ${String(context.name)}`);
64
65 6
    const cache = new Map<string, CacheEntry>();
66 6
    Executor.setExecution('init', () => {
67 16
      const bean = registeredMethods.get(method);
68 16
      bean?._interceptors.set(context.name as string, (target: any, _prop: string) => {
69
70 15
        return (...args: any[]) => {
71 15
          let keyObj: any = { args };
72 15
          if (isRoute(args)) {
73 2
            const [req, res] = args;
74 2
            keyObj = {
75
              url: req.url,
76
              method: req.method,
77
              params: req.params,
78
              query: req.query,
79
              body: req.body,
80
            };
81 2
            const key = createKey(keyObj);
82 2
            try {
83 2
              const result = getCachedData(cache, key);
84 1
              logger.debug(`Returning cached data for ${key}`);
85 1
              return res.send(result.data);
86
            } catch (error) {
87 1
              logger.debug(error);
88
89 1
              const originalSend = res.send.bind(res);
90 1
              res.send = function (body: any) {
91 1
                cache.set(key, {
92
                  data: body,
93
                  expiration: Date.now() + options.duration,
94
                });
95 1
                return originalSend(body);
96
              };
97
98 1
              return method.call(target, ...args);
99
            }
100
          } else {
101 13
            const key = createKey(keyObj);
102 13
            try {
103 13
              const cached = getCachedData(cache, key);
104 5
              logger.debug(`Cache hit for method ${String(context.name)} with key ${key}`);
105 5
              return cached.data;
106
            } catch {
107 8
              logger.debug(`Cache miss for method ${String(context.name)} with key ${key}`);
108 8
              const result = method.apply(target, args);
109 8
              cache.set(key, {
110
                data: result,
111
                expiration: Date.now() + options.duration,
112
              });
113 8
              return result;
114
            }
115
          }
116
        };
117
118
119
      });
120
    });
121 6
    return method;
122
  };
123
}
124