Skip to content

Commit ac264d7

Browse files
authored
chore: merge pull request #53 from gjovs/feature/request-with-cache-adapter
Feat: Add cache request adapter
2 parents 9f9d461 + 4d6e0f5 commit ac264d7

11 files changed

Lines changed: 276 additions & 6 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"acked",
44
"agendash",
55
"agentkeepalive",
6+
"checkperiod",
67
"codegen",
78
"datora",
89
"fastify",

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.4.0] - 2025-05-08
9+
10+
### Added
11+
12+
- Adds the `CacheRequestAdapter` wrapper of cache functionalities inside request adapter structure and interfaces
13+
814
## [2.3.0] - 2025-05-06
915

1016
### Added
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface GenericCacheService {
2+
get<T>(...args: unknown[]): Promise<T>;
3+
get<T>(...args: unknown[]): T;
4+
5+
set<T>(...args: unknown[]): T;
6+
}

src/data/protocols/cache/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './get';
22
export * from './set';
33
export * from './replace';
44
export * from './get-cache-key-by-context';
5+
export * from './generic-cache-service';

src/infra/cache/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './factory';
2+
export * from './node-cache';

src/infra/cache/node-cache.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import NodeCache from 'node-cache';
2+
3+
class NodeCacheSingleton {
4+
private static instance: NodeCache;
5+
private static readonly DEFAULT_TTL_IN_MINUTES = 60;
6+
private static readonly DEFAULT_TTL_IN_SECONDS =
7+
NodeCacheSingleton.DEFAULT_TTL_IN_MINUTES * 60;
8+
private static readonly DEFAULT_TIME_TO_CHECK_IN_SECONDS = 120;
9+
10+
private constructor() {}
11+
12+
public static getInstance(): NodeCache {
13+
if (!NodeCacheSingleton.instance) {
14+
NodeCacheSingleton.instance = new NodeCache({
15+
stdTTL: NodeCacheSingleton.DEFAULT_TTL_IN_SECONDS,
16+
checkperiod: NodeCacheSingleton.DEFAULT_TIME_TO_CHECK_IN_SECONDS,
17+
useClones: false
18+
});
19+
}
20+
return NodeCacheSingleton.instance;
21+
}
22+
}
23+
24+
export const makeNodeCache = () => NodeCacheSingleton.getInstance();

src/infra/db/mssql/util/knex/extensions/turbo-plugin/turbo-interceptor.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import { createHash } from 'node:crypto';
21
import k, { Knex } from 'knex';
32
import NodeCache from 'node-cache';
43

5-
import { logger } from '@/util';
64
import { makeCacheServer } from '@/infra/cache';
5+
import { generateHashKeyToMemJs, logger } from '@/util';
76

87
type Services = 'memjs' | 'node-cache';
98
type GenericObject = Record<string, unknown>;
109

11-
function generateHashKeyToMemJs(value: string): string {
12-
return createHash('sha256').update(value).digest('hex');
13-
}
14-
1510
const cache = new NodeCache({ stdTTL: 100, checkperiod: 120 });
1611
const memCache = makeCacheServer();
1712

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { GenericCacheService } from '@/data/protocols/cache';
2+
import type { Data, HttpClient, HttpResponse } from '@/data/protocols/http';
3+
import { generateHashKeyToMemJs, logger } from '@/util';
4+
5+
type Services = 'memjs' | 'node-cache';
6+
7+
const DEFAULT_CACHE_SERVICE: Services = 'node-cache';
8+
const DEFAULT_TTL_IN_MINUTES = 60;
9+
10+
const MAX_RESULT_SIZE = 1024 * 1024 * 1; // ~ 1 MB
11+
12+
export class CachedRequestAdapter implements HttpClient {
13+
constructor(
14+
private readonly httpClient: HttpClient,
15+
private readonly cacheService: GenericCacheService,
16+
private readonly options: {
17+
cacheService?: Services;
18+
headersFields?: string[];
19+
ttl?: number;
20+
} = {}
21+
) {}
22+
23+
async request(data: Data): Promise<HttpResponse> {
24+
const key = this.generateCacheKey(data);
25+
const cachedResponse = await this.getCachedResponse(key);
26+
27+
if (cachedResponse && Object.keys(cachedResponse).length > 1)
28+
return cachedResponse;
29+
30+
const response = await this.httpClient.request(data);
31+
32+
setImmediate(() => {
33+
this.saveToCache(key, response).catch((error) => {
34+
logger.log({
35+
message: `Cache save error: ${error.message}`,
36+
level: 'warn'
37+
});
38+
});
39+
});
40+
41+
return response;
42+
}
43+
44+
private generateCacheKey(data: Data): string {
45+
const method = data.method?.toUpperCase() || '';
46+
const { url } = data;
47+
const headers = this.getRelevantHeaders(data.headers);
48+
const body = this.normalizeBody(data.body);
49+
50+
const keyObject = { method, url, headers, body };
51+
return generateHashKeyToMemJs(JSON.stringify(keyObject));
52+
}
53+
54+
private getRelevantHeaders(
55+
headers?: Record<string, unknown>
56+
): Record<string, string> {
57+
if (!headers || !this.options.headersFields?.length) return {};
58+
59+
const relevantHeaders: Record<string, string> = {};
60+
61+
for (let i = 0; i < this.options.headersFields.length; i++) {
62+
const field = this.options.headersFields[i];
63+
if (headers[field] !== undefined) {
64+
relevantHeaders[field] = String(headers[field]);
65+
}
66+
}
67+
68+
return relevantHeaders;
69+
}
70+
71+
private normalizeBody(body: unknown): unknown {
72+
if (typeof body !== 'object' || body === null || body === undefined)
73+
return body;
74+
return JSON.parse(JSON.stringify(body));
75+
}
76+
77+
private async getCachedResponse(
78+
key: string
79+
): Promise<HttpResponse | undefined> {
80+
try {
81+
if (this.getCacheService() === 'node-cache') {
82+
return this.cacheService.get(key);
83+
}
84+
const buffer = await this.cacheService.get(key);
85+
return buffer ? JSON.parse(buffer.toString()) : undefined;
86+
} catch (error) {
87+
logger.log({
88+
message: `Cache read error: ${error.message}`,
89+
level: 'warn'
90+
});
91+
return undefined;
92+
}
93+
}
94+
95+
private async saveToCache(
96+
key: string,
97+
response: HttpResponse
98+
): Promise<void> {
99+
const responseString = JSON.stringify(response);
100+
101+
if (Buffer.byteLength(responseString) > MAX_RESULT_SIZE) {
102+
return;
103+
}
104+
105+
if (this.getCacheService() === 'node-cache') {
106+
this.cacheService.set(key, response, this.getTtl());
107+
} else {
108+
await this.cacheService.set({
109+
key,
110+
value: responseString,
111+
ttl: this.getTtl()
112+
});
113+
}
114+
}
115+
116+
private getCacheService(): Services {
117+
return this.options.cacheService || DEFAULT_CACHE_SERVICE;
118+
}
119+
120+
private getTtl(): number {
121+
return this.options.ttl || DEFAULT_TTL_IN_MINUTES;
122+
}
123+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './form-data-request-adapter';
22
export * from './request-adapter';
3+
export * from './cache-request-adapter';

src/util/cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { createHash } from 'node:crypto';
2+
13
import { ALLOWED_CONTEXT, CacheContexts } from '@/data/protocols/cache';
24

35
import { name } from '../../package.json';
@@ -18,3 +20,7 @@ export function getCacheKeyByContext(context: CacheContexts, meta?: string) {
1820
const metaValue = meta ? `.${meta}` : '';
1921
return `${applicationName}.${context}${metaValue}`;
2022
}
23+
24+
export function generateHashKeyToMemJs(value: string): string {
25+
return createHash('sha256').update(value).digest('hex');
26+
}

0 commit comments

Comments
 (0)