Skip to content

Commit 8c5de13

Browse files
authored
Merge pull request #29 from nebarf/http-cache-store
Http cache store
2 parents a5c56c9 + 51801f0 commit 8c5de13

10 files changed

+378
-224
lines changed

README.md

Lines changed: 52 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ Below the complete set of options you can provide to the `HttpClientConfigProvid
121121
|responseParser|A function that maps the native fetch response. The default parser transform the fetch response stream into a json (https://developer.mozilla.org/en-US/docs/Web/API/Response/json)|[httpResponseParser](src/config/response-parser.ts)
122122
|requestBodySerializer|A function used to serialize request body. The default serializer take into account a wide range of data types to figure out which type of serialization to perform|[serializeRequestBody](src/config/request-body-serializer.ts)
123123
|reqOptions|The default request option that will be carried by any request dispatched by the client. See [HttpRequestOptions](src/client/types.ts)|```{ headers: { 'Content-Type': 'application/json' } }```
124-
|cache|A service used to cache http responses. [HttpCache](src/cache/http-cache.ts) can be used as reference to provide your own implementation. By default it uses an in-memory cache.|[HttpInMemoryCacheService](src/cache/http-in-memory-cache.ts)
124+
|cacheStore|The store for cached http responses. By default an in-memory cache store is used.|[HttpInMemoryCacheService](src/cache/http-in-memory-cache.ts)
125+
|cacheStorePrefix|The prefix concatenated to any cached entry.|`rfh`
126+
|cacheStoreSeparator|Separates the store prefix and the cached entry identifier|`__`
125127

126128
<br>
127129

@@ -501,134 +503,90 @@ function App() {
501503
export default App;
502504
```
503505

504-
By default the http client uses an in-memory cache, so it will be flushed everytime a full app refresh is performed. You can override the default caching strategy by providing your own cache. The example below shows a http cache based on session storage:
506+
By default the http client uses an in-memory cache, so it will be flushed everytime a full app refresh is performed. You can override the default caching strategy by providing your own cache store. The example below shows a http cache store based on session storage:
505507

506508
```js
507509
import React from 'react';
508-
import { HttpClientConfigProvider, HttpCache, useHttpRequest } from 'react-http-fetch';
510+
import { useHttpRequest, HttpClientConfigProvider } from 'react-http-fetch';
509511

510-
export class HttpSessionStorageCacheService extends HttpCache {
512+
export class HttpSessionStorageCacheStore {
511513
/**
512514
* The local cache providing for a request identifier
513-
* the corresponding parsed response.
515+
* the corresponding cached entry.
514516
*/
515-
store = window.sessionStorage;
517+
_store = window.sessionStorage;
516518

517519
/**
518-
* Gets the unique key used as idenitifier to store
519-
* a cached response for the given http request.
520+
* @inheritdoc
520521
*/
521-
_getRequestIdentifier(request) {
522-
const fullUrl = request.urlWithParams;
523-
return fullUrl;
524-
}
525-
526-
/**
527-
* Tells if a cached entry is expired.
528-
*/
529-
_isEntryExpired(entry) {
530-
const nowTime = new Date().getTime();
531-
const cachedAt = entry.cachedAt instanceof Date ? entry.cachedAt : new Date(entry.cachedAt);
532-
const cachedTime = cachedAt.getTime();
533-
return cachedTime + entry.maxAge < nowTime;
534-
}
535-
536-
/**
537-
* Gets the cached entry associated with the request.
538-
*/
539-
_getEntry(request) {
540-
const reqIdentifier = this._getRequestIdentifier(request);
541-
const storedEntry = this.store.getItem(reqIdentifier);
522+
get(identifier) {
523+
const stringifiedEntry = this._store.getItem(identifier);
524+
if (!stringifiedEntry) {
525+
return;
526+
}
542527

543528
try {
544-
const parsedEntry = JSON.parse(storedEntry);
529+
const parsedEntry = JSON.parse(stringifiedEntry);
545530
return parsedEntry;
546-
} catch(err) {
547-
return null;
531+
} catch (err) {
532+
return;
548533
}
549534
}
550535

551536
/**
552-
* Removes a cached entry.
537+
* @inheritdoc
553538
*/
554-
_removeEntry(entry) {
555-
this.store.removeItem(entry.identifier);
539+
put(identifier, entry) {
540+
try {
541+
const stringifiedEntry = JSON.stringify(entry);
542+
this._store.setItem(identifier, stringifiedEntry);
543+
544+
return () => this.delete(identifier);
545+
} catch (err) {
546+
return () => {};
547+
}
556548
}
557549

558550
/**
559-
* Determines if for the given request is available a cached response.
551+
* @inheritdoc
560552
*/
561-
_has(request) {
562-
const key = this._getRequestIdentifier(request);
563-
return this.store.hasOwnProperty(key);
553+
has(identifier) {
554+
return this._store.has(identifier);
564555
}
565556

566557
/**
567-
* Tells if the cached request is expired or not.
558+
* @inheritdoc
568559
*/
569-
_isExpired(request) {
570-
const cachedEntry = this._getEntry(request);
571-
if (!cachedEntry) {
572-
return true;
573-
}
574-
575-
return this._isEntryExpired(cachedEntry);
560+
delete(identifier) {
561+
this._store.removeItem(identifier);
576562
}
577563

578564
/**
579-
* Gets the cached entry in the map for the given request.
565+
* Gets all entry keys.
580566
*/
581-
get(request) {
582-
const cachedEntry = this._getEntry(request);
583-
if (!cachedEntry) {
584-
return undefined;
585-
}
586-
587-
const isExpired = this._isEntryExpired(cachedEntry);
588-
return isExpired ? undefined : cachedEntry.response;
567+
_keys() {
568+
return Object.keys(this._store);
589569
}
590570

591571
/**
592-
* Puts a new cached response for the given request.
572+
* Gets all stored entries.
593573
*/
594-
put(request, response) {
595-
if (!request.maxAge) {
596-
return;
597-
}
598-
599-
const reqKey = this._getRequestIdentifier(request);
600-
const entry = {
601-
response,
602-
identifier: reqKey,
603-
cachedAt: new Date(),
604-
maxAge: request.maxAge,
605-
};
606-
607-
// Update and flush the cache.
608-
this.store.setItem(reqKey, JSON.stringify(entry));
609-
610-
// Remove the entry from the cache once expired.
611-
const timerRef = setTimeout(() => {
612-
this._removeEntry(entry);
613-
clearTimeout(timerRef);
614-
}, request.maxAge);
574+
entries() {
575+
return this._keys()
576+
.map(entryKey => this._store.getItem(entryKey));
615577
}
616578

617579
/**
618-
* Founds all expired entry and deletes them from the cache.
580+
* @inheritdoc
619581
*/
620582
flush() {
621-
this.store.forEach((entry) => {
622-
const isEntryExpired = this._isEntryExpired(entry);
623-
624-
if (isEntryExpired) {
625-
this._removeEntry(entry);
626-
}
583+
this._keys().forEach((entry) => {
584+
this.delete(entry.identifier);
627585
});
628586
}
629587
}
630588

631-
const httpCache = new HttpSessionStorageCacheService();
589+
const httpCacheStore = new HttpSessionStorageCacheStore();
632590

633591
function Child() {
634592
const [state, request] = useHttpRequest({
@@ -641,7 +599,7 @@ function Child() {
641599

642600
const fetchTodo = () => {
643601
const { reqResult } = request();
644-
reqResult.then(res => console.log('Request res: ', res))
602+
reqResult.then(res => console.log('Request response: ', res))
645603
};
646604

647605
return (
@@ -658,7 +616,13 @@ function Child() {
658616

659617

660618
function App() {
661-
const httpReqConfig = { cache: httpCache };
619+
const httpReqConfig = {
620+
cacheStore: httpCacheStore,
621+
// "prefix" and "separator" are not mandatory,
622+
// if not provided the default ones will be used.
623+
cacheStorePrefix: 'customPrefix',
624+
cacheStoreSeparator: '-'
625+
};
662626

663627
return (
664628
<HttpClientConfigProvider config={httpReqConfig}>

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 interface HttpCacheStore {
4+
/**
5+
* Gets the cached entry for the given identifier.
6+
*/
7+
get<T>(identifier: string): HttpCacheEntry<T> | undefined;
8+
9+
/**
10+
* Stores the entry.
11+
*/
12+
put<T>(identifier: string, entry: HttpCacheEntry<T>): () => void;
13+
14+
/**
15+
* Determines if the entry is in the store.
16+
*/
17+
has(identifier: string): boolean;
18+
19+
/**
20+
* Deletes the cached entry for the given identifier.
21+
*/
22+
delete(identifier: string): void;
23+
24+
/**
25+
* Gets all stored entries.
26+
*/
27+
entries(): HttpCacheEntry<unknown>[];
28+
29+
/**
30+
* Flushes the store by deleting all entries.
31+
*/
32+
flush(): void;
33+
}

src/cache/http-cache.ts

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,125 @@
11
import { HttpRequest } from '../client';
2+
import { HttpCacheEntry } from './types';
3+
import { HttpCacheStorePrefixDecorator } from './prefix-decorator';
4+
5+
export class HttpCacheService {
6+
private prefixedStore: HttpCacheStorePrefixDecorator;
7+
8+
constructor(store: HttpCacheStorePrefixDecorator) {
9+
this.prefixedStore = store;
10+
}
11+
12+
/**
13+
* Gets the unique key used as idenitifier to store
14+
* a cached response for the given http request.
15+
*/
16+
private getRequestIdentifier(request: HttpRequest): string {
17+
const fullUrl = request.urlWithParams;
18+
return fullUrl;
19+
}
20+
21+
/**
22+
* Tells if a cached entry is expired.
23+
*/
24+
private isEntryExpired<T>(entry: HttpCacheEntry<T>): boolean {
25+
const nowTime = new Date().getTime();
26+
const cachedAtDate = entry.cachedAt instanceof Date ? entry.cachedAt : new Date(entry.cachedAt);
27+
const cachedTime = cachedAtDate.getTime();
28+
return cachedTime + entry.maxAge < nowTime;
29+
}
30+
31+
/**
32+
* Gets the cached entry associated with the request.
33+
*/
34+
private getEntry<T>(request: HttpRequest): HttpCacheEntry<T> | undefined {
35+
const reqIdentifier = this.getRequestIdentifier(request);
36+
return this.prefixedStore.get(reqIdentifier) as HttpCacheEntry<T>;
37+
}
38+
39+
/**
40+
* Removes a cached entry.
41+
*/
42+
private removeEntry<T>(entry: HttpCacheEntry<T>): void {
43+
this.prefixedStore.delete(entry.identifier);
44+
}
45+
46+
/**
47+
* Determines if for the given request is available a cached response.
48+
*/
49+
has(request: HttpRequest): boolean {
50+
const key = this.getRequestIdentifier(request);
51+
return this.prefixedStore.has(key);
52+
}
53+
54+
/**
55+
* Tells if the cached request is expired or not.
56+
*/
57+
isExpired(request: HttpRequest): boolean {
58+
const cachedEntry = this.getEntry(request);
59+
if (!cachedEntry) {
60+
return true;
61+
}
62+
63+
return this.isEntryExpired(cachedEntry);
64+
}
65+
66+
/**
67+
* Gets the cached entry in the map for the given request.
68+
*/
69+
get<T>(request: HttpRequest): T | undefined {
70+
const cachedEntry = this.getEntry(request);
71+
if (!cachedEntry) {
72+
return undefined;
73+
}
74+
75+
const isExpired = this.isEntryExpired(cachedEntry);
76+
return isExpired ? undefined : (cachedEntry.response as T);
77+
}
78+
79+
/**
80+
* Puts a new cached response for the given request.
81+
*/
82+
put<T>(request: HttpRequest, response: T): void {
83+
if (!request.maxAge) {
84+
return;
85+
}
86+
87+
const reqKey = this.getRequestIdentifier(request);
88+
const entry: HttpCacheEntry<T> = {
89+
response,
90+
identifier: reqKey,
91+
cachedAt: new Date(),
92+
maxAge: request.maxAge,
93+
};
94+
95+
// Update the store.
96+
this.prefixedStore.put(entry.identifier, entry);
97+
98+
// Remove the entry from the cache once expired.
99+
const timerRef = setTimeout(() => {
100+
this.removeEntry(entry);
101+
clearTimeout(timerRef);
102+
}, request.maxAge);
103+
}
2104

3-
export abstract class HttpCache {
4105
/**
5-
* Gets the cached parsed response for the given request.
106+
* Founds all expired entry and deletes them from the cache.
6107
*/
7-
abstract get<T>(request: HttpRequest): T | undefined;
108+
prune(): void {
109+
const entries = this.prefixedStore.entries();
110+
entries.forEach((entry) => {
111+
const isEntryExpired = this.isEntryExpired(entry);
112+
113+
if (isEntryExpired) {
114+
this.removeEntry(entry);
115+
}
116+
});
117+
}
8118

9119
/**
10-
* Caches the parsed response for the given request.
120+
* Flush the cache by removing all entries.
11121
*/
12-
abstract put<T>(request: HttpRequest, response: T): void;
122+
flush(): void {
123+
this.prefixedStore.flush();
124+
}
13125
}

0 commit comments

Comments
 (0)