/
hybridCache.ts
102 lines (89 loc) · 3.77 KB
/
hybridCache.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { createLogger } from '@unly/utils-simple-logger';
import deepmerge from 'deepmerge';
import getTimestampsElapsedTime from '../time/getTimestampsElapsedTime';
import { CachedItem, HybridCacheStorage, StorageOptions } from './hybridCacheStorage';
const fileLabel = 'utils/cache/cache';
const logger = createLogger({ // eslint-disable-line no-unused-vars,@typescript-eslint/no-unused-vars
label: fileLabel,
});
type HybridCacheOptions = {
ttl?: number; // Default: 30 (seconds). If 0 then cache won't be invalidated (no TTL)
storage?: {
type: 'memory'; // Default
} | {
type: 'disk';
options: {
filename: string;
};
};
enabled: boolean; // Enabled by default
}
const defaultHybridCacheOptions: Required<HybridCacheOptions> = {
ttl: 30,
storage: {
type: 'memory',
},
enabled: true,
};
/**
* Hybrid cache, can use in-memory storage or on-disk storage.
*
* You can select the kind of storage you want through the options.
*
* Algorithm:
* - Check if cache exist for key, if not then call the dataResolver and cache the result.
* - If cache exists and record age is lower than TTL (default: 30 seconds), then return cached data. Otherwise, call the dataResolver and cache the result.
*
* Special considerations:
* - If options.ttl is 0, then TTL is disabled and data will live indefinitely
* - Memory cache storage is selected by default
* - Memory cache storage will not work if you want to cache data at build time, when building the pages (typically: SSG). You must use the disk for this kind of usage.
* - Disk cache storage works on Vercel, you need a writable file system
*
* @example With default (in-memory, TTL 30 secs)
* const stuff = await hybridCache('CustomerTable', async () => await fetchMyStuff());
*
* @example Using disk storage with no TTL, meant to cache stuff once for all pages during server initial build (for SSG, typically - IS_SERVER_INITIAL_BUILD comes from next.config.js)
* const stuff = await hybridCache('CustomerTable', async () => await fetchMyStuff(), { enabled: !!process.env.IS_SERVER_INITIAL_BUILD && process.env.NODE_ENV !== 'development', storage: {type: 'disk', options: {filename: 'my-stuff'}} });
*
* @param keyResolver string | Function Key used to store the data in the cache
* @param dataResolver Function Has to be implemented by the caller and is supposed to take care of fetching the data
* @param options HybridCacheOptions
*/
const hybridCache = async <T>(keyResolver: string | (() => string), dataResolver: () => T, options: Partial<HybridCacheOptions> = defaultHybridCacheOptions): Promise<T> => {
const { ttl, enabled, storage } = deepmerge(defaultHybridCacheOptions, options);
if (!enabled) { // Bypasses cache completely
// eslint-disable-next-line no-console
console.debug('Cache is disabled, bypassing');
return dataResolver();
}
let cacheStorage: HybridCacheStorage;
let storageOptions: StorageOptions = {};
if (storage.type === 'memory') {
cacheStorage = require('./memoryCacheStorage');
} else {
cacheStorage = require('./diskCacheStorage');
const { options } = storage;
storageOptions = options;
}
let key: string;
if (typeof keyResolver === 'function') {
key = keyResolver();
} else {
key = keyResolver;
}
const cachedItem: CachedItem = await cacheStorage.get(key, storageOptions);
if (typeof cachedItem !== 'undefined') {
const { value, timestamp } = cachedItem;
if (timestamp === 0 || getTimestampsElapsedTime(timestamp, +new Date()) < ttl) {
return value;
} else {
logger.debug('Cache key has expired');
}
} else {
logger.debug('Cache key does not exist');
}
const unMemoizedResult: T = await dataResolver();
return cacheStorage.set(key, unMemoizedResult, storageOptions);
};
export default hybridCache;