-
Notifications
You must be signed in to change notification settings - Fork 150
/
caching.ts
159 lines (140 loc) · 5.26 KB
/
caching.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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import EventEmitter from 'eventemitter3';
import {coalesceAsync} from 'promise-coalesce';
import {
type MemoryCache, type MemoryConfig, type MemoryStore, memoryStore,
} from './stores/index.js';
export type Config = {
ttl?: Milliseconds;
refreshThreshold?: Milliseconds;
isCacheable?: (value: unknown) => boolean;
onBackgroundRefreshError?: (error: unknown) => void;
};
export type Milliseconds = number;
/**
* @deprecated will remove after 5.2.0. Use Milliseconds instead
*/
export type Ttl = Milliseconds;
export type Store = {
get<T>(key: string): Promise<T | undefined>;
set<T>(key: string, data: T, ttl?: Milliseconds): Promise<void>;
del(key: string): Promise<void>;
reset(): Promise<void>;
mset(arguments_: Array<[string, unknown]>, ttl?: Milliseconds): Promise<void>;
mget(...arguments_: string[]): Promise<unknown[]>;
mdel(...arguments_: string[]): Promise<void>;
keys(pattern?: string): Promise<string[]>;
ttl(key: string): Promise<number>;
};
export type StoreConfig = Config;
export type FactoryConfig<T> = T & Config;
export type FactoryStore<S extends Store, T extends Record<string, unknown> = never> = (
config?: FactoryConfig<T>,
) => S | Promise<S>;
export type Stores<S extends Store, T extends Record<string, unknown>> =
| 'memory'
| Store
| FactoryStore<S, T>;
export type CachingConfig<T> = MemoryConfig | StoreConfig | FactoryConfig<T>;
// eslint-disable-next-line @typescript-eslint/naming-convention
export type WrapTTL<T> = Milliseconds | ((v: T) => Milliseconds);
export type Cache<S extends Store = Store> = {
store: S;
set: (key: string, value: unknown, ttl?: Milliseconds) => Promise<void>;
get: <T>(key: string) => Promise<T | undefined>;
del: (key: string) => Promise<void>;
reset: () => Promise<void>;
on: (event: 'error', handler: (error: Error) => void) => void;
wrap<T>(key: string, function_: () => Promise<T>, ttl?: WrapTTL<T>, refreshThreshold?: Milliseconds): Promise<T>;
};
export async function caching(
name: 'memory',
arguments_?: MemoryConfig,
): Promise<MemoryCache>;
export async function caching<S extends Store>(store: S): Promise<Cache<S>>;
export async function caching<S extends Store, T extends Record<string, unknown> = never>(
factory: FactoryStore<S, T>,
arguments_?: FactoryConfig<T>,
): Promise<Cache<S>>;
/**
* Generic caching interface that wraps any caching library with a compatible interface.
*/
export async function caching<S extends Store, T extends Record<string, unknown> = never>(
factory: Stores<S, T>,
arguments_?: CachingConfig<T>,
): Promise<Cache<S> | Cache | MemoryCache> {
if (factory === 'memory') {
const store = memoryStore(arguments_ as MemoryConfig);
return createCache(store, arguments_ as MemoryConfig);
}
if (typeof factory === 'function') {
const store = await factory(arguments_ as FactoryConfig<T>);
return createCache(store, arguments_);
}
const store = factory;
return createCache(store, arguments_);
}
export function createCache(
store: MemoryStore,
arguments_?: MemoryConfig,
): MemoryCache;
export function createCache(store: Store, arguments_?: Config): Cache;
/**
* Create cache instance by store (non-async).
*/
export function createCache<S extends Store, C extends Config>(
store: S,
arguments_?: C,
): Cache<S> {
const eventEmitter = new EventEmitter();
return {
/**
* Wraps a function in cache. I.e., the first time the function is run,
* its results are stored in cache so subsequent calls retrieve from cache
* instead of calling the function.
* @example
* const result = await cache.wrap('key', () => Promise.resolve(1));
*
*/
async wrap<T>(key: string, function_: () => Promise<T>, ttl?: WrapTTL<T>, refreshThreshold?: Milliseconds) {
const refreshThresholdConfig = refreshThreshold ?? arguments_?.refreshThreshold ?? 0;
return coalesceAsync(key, async () => {
const value = await store.get<T>(key).catch(error => {
eventEmitter.emit('error', error);
});
if (value === undefined) {
const result = await function_();
const cacheTtl = typeof ttl === 'function' ? ttl(result) : ttl;
await store.set<T>(key, result, cacheTtl).catch(error => eventEmitter.emit('error', error));
return result;
}
if (refreshThresholdConfig) {
const cacheTtl = typeof ttl === 'function' ? ttl(value) : ttl;
const remainingTtl = await store.ttl(key);
if (remainingTtl !== -1 && remainingTtl < refreshThresholdConfig) {
coalesceAsync(`+++${key}`, function_)
.then(async result => store.set<T>(key, result, cacheTtl))
.catch(async error => {
eventEmitter.emit('error', error);
eventEmitter.emit('onBackgroundRefreshError', error);
if (arguments_?.onBackgroundRefreshError) {
arguments_.onBackgroundRefreshError(error);
} else {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw error;
}
});
}
}
return value;
});
},
store,
del: async (key: string) => store.del(key),
get: async <T>(key: string) => store.get<T>(key),
set: async (key: string, value: unknown, ttl?: Milliseconds) =>
store.set(key, value, ttl),
reset: async () => store.reset(),
on: (event: 'error', handler: (error: Error) => void) =>
eventEmitter.on('error', handler),
};
}