Skip to content

Commit 3024adf

Browse files
committed
make cache classes async, add RedisCache class
1 parent 4b0e4b4 commit 3024adf

File tree

18 files changed

+241
-81
lines changed

18 files changed

+241
-81
lines changed

src/MemoryCache.ts

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

src/lib/cache/Cache.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* eslint-disable no-unused-vars */
2+
3+
export abstract class Cache {
4+
public prefix = '';
5+
6+
public destroy() {
7+
//
8+
}
9+
public abstract has(key: string): Promise<boolean>;
10+
11+
public abstract get(key: string): Promise<any | null>;
12+
13+
public abstract put(key: string, value: any, ttlSeconds: number): void;
14+
15+
public abstract purge();
16+
17+
public abstract clear();
18+
}

src/lib/cache/MemoryCache.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Cache } from './Cache';
2+
3+
export interface CacheItem {
4+
ts: number;
5+
expires: number;
6+
value: any;
7+
}
8+
9+
export interface CacheMap {
10+
[key: string]: CacheItem;
11+
}
12+
13+
export class MemoryCache extends Cache {
14+
public map: CacheMap = {};
15+
16+
constructor(keyPrefix = '') {
17+
super();
18+
19+
if (keyPrefix.length) {
20+
keyPrefix += ':';
21+
}
22+
23+
this.prefix = keyPrefix;
24+
}
25+
26+
protected makeKey(key: string): string {
27+
return `${this.prefix}${key}`;
28+
}
29+
30+
public async has(key: string): Promise<boolean> {
31+
this.purge();
32+
33+
return new Promise(resolve => resolve(typeof this.map[this.makeKey(key)] !== 'undefined'));
34+
}
35+
36+
public async get(key: string): Promise<any | null> {
37+
this.purge();
38+
39+
const result = this.map[this.makeKey(key)]?.value ?? null;
40+
41+
return new Promise(resolve => resolve(result));
42+
}
43+
44+
public put(key: string, value: any, ttlSeconds: number) {
45+
this.map[this.makeKey(key)] = {
46+
ts: new Date().getTime(),
47+
expires: new Date().getTime() + ttlSeconds * 1000,
48+
value: value,
49+
};
50+
}
51+
52+
public purge() {
53+
for (const prop in this.map) {
54+
if (new Date().getTime() >= this.map[prop].expires) {
55+
delete this.map[prop];
56+
continue;
57+
}
58+
}
59+
}
60+
61+
public clear() {
62+
//
63+
}
64+
}

src/lib/cache/RedisCache.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Cache } from './Cache';
2+
const Redis = require('ioredis');
3+
4+
export class RedisCache extends Cache {
5+
public redis: typeof Redis;
6+
7+
constructor(keyPrefix = '') {
8+
super();
9+
10+
if (keyPrefix.length) {
11+
keyPrefix += ':';
12+
}
13+
14+
this.prefix = keyPrefix;
15+
16+
this.redis = new Redis({ keyPrefix: keyPrefix });
17+
}
18+
19+
public destroy() {
20+
this.redis.disconnect();
21+
}
22+
23+
public async has(key: string): Promise<boolean> {
24+
const result = await this.redis.exists(key);
25+
26+
return new Promise(resolve => resolve(result === 1));
27+
}
28+
29+
public async get(key: string): Promise<any | null> {
30+
let result = await this.redis.get(key);
31+
32+
if (result !== null) {
33+
result = JSON.parse(result);
34+
}
35+
36+
return new Promise(resolve => resolve(result));
37+
}
38+
39+
public put(key: string, value: any, ttlSeconds: number): void {
40+
this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
41+
}
42+
43+
public purge() {
44+
//
45+
}
46+
47+
public clear() {
48+
//
49+
}
50+
}

src/lib/resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MemoryCache } from '@/MemoryCache';
1+
import { MemoryCache } from '@/lib/cache/MemoryCache';
22
import { getResolver as getCommentsResolver } from '@/resolvers/comments';
33
import { getResolver as getJobsResolver } from '@/resolvers/jobs';
44
import { getResolver as getStoriesResolver } from '@/resolvers/stories';

src/resolvers/Story/author.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import axios from 'axios';
33
export function getResolver(cache: any): any {
44
const authorResolver = async parent => {
55
const userCacheKey = `user:${parent.by}`;
6+
const hasKey = await cache.has(userCacheKey);
67

7-
if (cache.has(userCacheKey)) {
8+
if (hasKey) {
89
return cache.get(userCacheKey);
910
}
1011

@@ -13,7 +14,7 @@ export function getResolver(cache: any): any {
1314

1415
cache.put(userCacheKey, resp.data, 3600);
1516

16-
return new Promise(resolve => resolve(cache.get(userCacheKey)));
17+
return new Promise(resolve => resolve(resp.data));
1718
};
1819

1920
return authorResolver;

src/resolvers/comments.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export function getResolver(cache: any): any {
1313
const commentIds = parent.kids.slice(0, first);
1414

1515
for (const id of commentIds) {
16-
if (!cache.has(`comment:${id}`)) {
16+
const hasKey = await cache.has(`comment:${id}`);
17+
18+
if (!hasKey) {
1719
// console.log(`getting comment ${id} from URL...`);
1820

1921
const commentResp = await axios.get(`${process.env.HACKERNEWS_API_URL}/item/${id}.json`);
@@ -22,7 +24,7 @@ export function getResolver(cache: any): any {
2224
cache.put(`comment:${id}`, commentData, 3600);
2325
}
2426

25-
comments.push(cache.get(`comment:${id}`));
27+
comments.push(await cache.get(`comment:${id}`));
2628
}
2729

2830
return new Promise(resolve => resolve(comments));

src/resolvers/jobs.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MemoryCache } from '@/MemoryCache';
1+
import { MemoryCache } from '@/lib/cache/MemoryCache';
22
import axios from 'axios';
33

44
export function getResolver(storyIds, stories, cache: MemoryCache): any {
@@ -11,22 +11,23 @@ export function getResolver(storyIds, stories, cache: MemoryCache): any {
1111
const ids: number[] = [];
1212
const cacheKey = `jobstoryids:${first}-${skipText ? 'skipText' : 'dontSkipText'}`;
1313

14-
if (!cache.has(cacheKey)) {
14+
const hasKey = await cache.has(cacheKey);
15+
if (!hasKey) {
1516
const resp = await axios.get(`${process.env.HACKERNEWS_API_URL}/jobstories.json?limitToFirst=${first + 5}&orderBy="$key"`);
1617
const data: number[] = resp.data;
1718

1819
cache.put(cacheKey, data, 600);
1920
ids.push(...data);
2021
}
2122

22-
const cacheData = cache.get(cacheKey);
23+
const cacheData = await cache.get(cacheKey);
2324

2425
ids.push(...cacheData);
2526
ids.splice(first);
2627

2728
const storyDataPromises = ids
28-
.map(id => {
29-
if (cache.has(`${kind}story:${id}`)) {
29+
.map(async id => {
30+
if (await cache.has(`${kind}story:${id}`)) {
3031
return null;
3132
}
3233

@@ -42,7 +43,7 @@ export function getResolver(storyIds, stories, cache: MemoryCache): any {
4243
});
4344

4445
for (const id of ids) {
45-
const storyItem = cache.get(`${kind}story:${id}`);
46+
const storyItem = await cache.get(`${kind}story:${id}`);
4647

4748
if (!storyItem.text || !skipText) {
4849
stories.push(storyItem);

src/resolvers/stories.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export function getResolver(topStoryIds, topStories, cache: any): any {
1010
const ids: number[] = [];
1111
const topstoryCacheKey = `${kind}storyids:${first}`;
1212

13-
if (!cache.has(topstoryCacheKey)) {
13+
const hasTopStoryKey = await cache.has(topstoryCacheKey);
14+
if (!hasTopStoryKey) {
1415
// console.log(`getting ${kind}storyids from URL...`);
1516

1617
const resp = await axios.get(`${process.env.HACKERNEWS_API_URL}/${kind}stories.json?limitToFirst=${first}&orderBy="$key"`);
@@ -20,14 +21,14 @@ export function getResolver(topStoryIds, topStories, cache: any): any {
2021
ids.push(...data);
2122
}
2223

23-
const cacheData = cache.get(topstoryCacheKey);
24+
const cacheData = await cache.get(topstoryCacheKey);
2425

2526
ids.push(...cacheData);
2627
ids.splice(first);
2728

2829
const storyDataPromises = ids
29-
.map(id => {
30-
if (cache.has(`${kind}story:${id}`)) {
30+
.map(async id => {
31+
if (await cache.has(`${kind}story:${id}`)) {
3132
return null;
3233
}
3334

@@ -45,7 +46,7 @@ export function getResolver(topStoryIds, topStories, cache: any): any {
4546
});
4647

4748
for (const id of ids) {
48-
topStories.push(cache.get(`${kind}story:${id}`));
49+
topStories.push(await cache.get(`${kind}story:${id}`));
4950
}
5051

5152
return new Promise(resolve => resolve(topStories));

src/resolvers/user.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/* eslint-disable no-unused-vars */
22

3-
import { MemoryCache } from '../MemoryCache';
3+
import { MemoryCache } from '../lib/cache/MemoryCache';
44
import axios from 'axios';
55

66
export function getResolver(cache: MemoryCache): any {
77
const userResolver = async (_, { id }) => {
8-
if (cache.has(`user:${id}`)) {
9-
return cache.get(`user:${id}`);
8+
const hasKey = await cache.has(`user:${id}`);
9+
10+
if (hasKey) {
11+
return await cache.get(`user:${id}`);
1012
}
1113

1214
const resp = await axios.get(`${process.env.HACKERNEWS_API_URL}/user/${id}.json`);
1315

1416
cache.put(`user:${id}`, resp.data, 3600);
1517

16-
return cache.get(`user:${id}`);
18+
return resp.data;
1719
};
1820

1921
return userResolver;

0 commit comments

Comments
 (0)