Skip to content

Commit d51d465

Browse files
author
nebarf
committed
Allow to provide a custom http cache store in provider config in place of cache service override
1 parent a5c56c9 commit d51d465

File tree

7 files changed

+207
-126
lines changed

7 files changed

+207
-126
lines changed

src/cache/http-cache-store.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { HttpCacheEntry } from './types';
2+
3+
export abstract class HttpCacheStore<Identifier = string> {
4+
/**
5+
* Gets the cached entry for the given identifier.
6+
*/
7+
abstract get<T>(identifier: Identifier): HttpCacheEntry<T> | undefined;
8+
9+
/**
10+
* Stores the entry.
11+
*/
12+
abstract put<T>(entry: HttpCacheEntry<T>): () => void;
13+
14+
/**
15+
* Determines if the entry is in the store.
16+
*/
17+
abstract has(idenitifier: Identifier): boolean;
18+
19+
/**
20+
* Deletes the cached entry for the given identifier.
21+
*/
22+
abstract delete(idenitifier: Identifier): void;
23+
24+
/**
25+
* Gets all stored entries.
26+
*/
27+
abstract entries(): HttpCacheEntry<unknown>[];
28+
29+
/**
30+
* Flushes the store by deleting all entries.
31+
*/
32+
abstract flush(): void;
33+
}

src/cache/http-cache.ts

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,121 @@
1+
import { HttpCacheStore } from './http-cache-store';
12
import { HttpRequest } from '../client';
3+
import { HttpCacheEntry } from './types';
4+
5+
export class HttpCacheService {
6+
constructor(private store: HttpCacheStore) {}
7+
8+
/**
9+
* Gets the unique key used as idenitifier to store
10+
* a cached response for the given http request.
11+
*/
12+
private getRequestIdentifier(request: HttpRequest): string {
13+
const fullUrl = request.urlWithParams;
14+
return fullUrl;
15+
}
16+
17+
/**
18+
* Tells if a cached entry is expired.
19+
*/
20+
private isEntryExpired<T>(entry: HttpCacheEntry<T>): boolean {
21+
const nowTime = new Date().getTime();
22+
const cachedAtDate = entry.cachedAt instanceof Date ? entry.cachedAt : new Date(entry.cachedAt);
23+
const cachedTime = cachedAtDate.getTime();
24+
return cachedTime + entry.maxAge < nowTime;
25+
}
26+
27+
/**
28+
* Gets the cached entry associated with the request.
29+
*/
30+
private getEntry<T>(request: HttpRequest): HttpCacheEntry<T> | undefined {
31+
const reqIdentifier = this.getRequestIdentifier(request);
32+
return this.store.get(reqIdentifier) as HttpCacheEntry<T>;
33+
}
34+
35+
/**
36+
* Removes a cached entry.
37+
*/
38+
private removeEntry<T>(entry: HttpCacheEntry<T>): void {
39+
this.store.delete(entry.identifier);
40+
}
41+
42+
/**
43+
* Determines if for the given request is available a cached response.
44+
*/
45+
has(request: HttpRequest): boolean {
46+
const key = this.getRequestIdentifier(request);
47+
return this.store.has(key);
48+
}
249

3-
export abstract class HttpCache {
450
/**
5-
* Gets the cached parsed response for the given request.
51+
* Tells if the cached request is expired or not.
652
*/
7-
abstract get<T>(request: HttpRequest): T | undefined;
53+
isExpired(request: HttpRequest): boolean {
54+
const cachedEntry = this.getEntry(request);
55+
if (!cachedEntry) {
56+
return true;
57+
}
58+
59+
return this.isEntryExpired(cachedEntry);
60+
}
61+
62+
/**
63+
* Gets the cached entry in the map for the given request.
64+
*/
65+
get<T>(request: HttpRequest): T | undefined {
66+
const cachedEntry = this.getEntry(request);
67+
if (!cachedEntry) {
68+
return undefined;
69+
}
70+
71+
const isExpired = this.isEntryExpired(cachedEntry);
72+
return isExpired ? undefined : (cachedEntry.response as T);
73+
}
74+
75+
/**
76+
* Puts a new cached response for the given request.
77+
*/
78+
put<T>(request: HttpRequest, response: T): void {
79+
if (!request.maxAge) {
80+
return;
81+
}
82+
83+
const reqKey = this.getRequestIdentifier(request);
84+
const entry: HttpCacheEntry<T> = {
85+
response,
86+
identifier: reqKey,
87+
cachedAt: new Date(),
88+
maxAge: request.maxAge,
89+
};
90+
91+
// Update the store.
92+
this.store.put(entry);
93+
94+
// Remove the entry from the cache once expired.
95+
const timerRef = setTimeout(() => {
96+
this.removeEntry(entry);
97+
clearTimeout(timerRef);
98+
}, request.maxAge);
99+
}
100+
101+
/**
102+
* Founds all expired entry and deletes them from the cache.
103+
*/
104+
prune(): void {
105+
const entries = this.store.entries();
106+
entries.forEach((entry) => {
107+
const isEntryExpired = this.isEntryExpired(entry);
108+
109+
if (isEntryExpired) {
110+
this.removeEntry(entry);
111+
}
112+
});
113+
}
8114

9115
/**
10-
* Caches the parsed response for the given request.
116+
* Flush the cache by removing all entries.
11117
*/
12-
abstract put<T>(request: HttpRequest, response: T): void;
118+
flush(): void {
119+
this.store.flush();
120+
}
13121
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { HttpCacheEntry } from './types';
2+
import { HttpCacheStore } from './http-cache-store';
3+
4+
export class HttpInMemoryCacheStore extends HttpCacheStore {
5+
/**
6+
* The local cache providing for a request identifier
7+
* the corresponding cached entry.
8+
*/
9+
private readonly store = new Map<string, HttpCacheEntry<unknown>>();
10+
11+
/**
12+
* @inheritdoc
13+
*/
14+
get<T>(identifier: string): HttpCacheEntry<T> | undefined {
15+
return this.store.get(identifier) as HttpCacheEntry<T>;
16+
}
17+
18+
/**
19+
* @inheritdoc
20+
*/
21+
put<T>(entry: HttpCacheEntry<T>): () => void {
22+
this.store.set(entry.identifier, entry);
23+
return () => this.delete(entry.identifier);
24+
}
25+
26+
/**
27+
* @inheritdoc
28+
*/
29+
has(identifier: string): boolean {
30+
return this.store.has(identifier);
31+
}
32+
33+
/**
34+
* @inheritdoc
35+
*/
36+
delete(identifier: string): void {
37+
this.store.delete(identifier);
38+
}
39+
40+
/**
41+
* Gets all stored entries.
42+
*/
43+
entries(): HttpCacheEntry<unknown>[] {
44+
return Array.from(this.store.values());
45+
}
46+
47+
/**
48+
* @inheritdoc
49+
*/
50+
flush(): void {
51+
this.store.forEach((entry) => {
52+
this.delete(entry.identifier);
53+
});
54+
}
55+
}

src/cache/http-in-memory-cache.ts

Lines changed: 0 additions & 116 deletions
This file was deleted.

src/cache/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './http-cache';
2-
export * from './http-in-memory-cache';
2+
export * from './http-cache-store';
3+
export * from './http-in-memory-cache-store';
34
export * from './types';

src/config/defaults.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { HttpClientContextProps, HttpClientConfig } from './types';
22
import { httpResponseParser } from './response-parser';
33
import { serializeRequestBody } from './request-body-serializer';
4-
import { HttpInMemoryCacheService } from '../cache';
4+
import { HttpCacheService, HttpInMemoryCacheStore } from '../cache';
55

66
export const defaultHttpReqConfig: HttpClientConfig = {
77
baseUrl: '',
@@ -12,7 +12,7 @@ export const defaultHttpReqConfig: HttpClientConfig = {
1212
'Content-Type': 'application/json',
1313
},
1414
},
15-
cache: new HttpInMemoryCacheService(),
15+
cache: new HttpCacheService(new HttpInMemoryCacheStore()),
1616
};
1717

1818
export const defaultClientProps: HttpClientContextProps = {

src/config/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { HttpCache } from '../cache';
1+
import { HttpCacheService } from '../cache';
22
import { ReactElement } from 'react';
33
import { HttpRequestOptions, HttpResponseParser } from '../client';
44

@@ -16,7 +16,7 @@ export interface HttpClientConfig {
1616
baseUrl: string;
1717
responseParser: HttpResponseParser;
1818
requestBodySerializer: HttpRequestBodySerializer;
19-
cache: HttpCache;
19+
cache: HttpCacheService;
2020
}
2121

2222
export type HttpInterceptor = (request: Promise<Response>) => Promise<void>;

0 commit comments

Comments
 (0)