cachette 3.0.1 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +2 -3
- package/src/index.ts +5 -0
- package/src/lib/CacheClient.ts +145 -0
- package/src/lib/CacheInstance.ts +222 -0
- package/src/lib/LocalCache.ts +178 -0
- package/src/lib/RedisCache.ts +431 -0
- package/src/lib/WriteThroughCache.ts +200 -0
- package/test/CacheClient_test.ts +318 -0
- package/test/CacheInstance_test.ts +488 -0
- package/test/LocalCache_test.ts +128 -0
- package/test/RedisCache_test.ts +303 -0
- package/test/WriteThroughCache_test.ts +306 -0
- package/test/mocharc-ci.js +12 -0
- package/test/mocharc.js +10 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { CacheClient } from '../src/lib/CacheClient';
|
|
3
|
+
import { LocalCache } from '../src/lib/LocalCache';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('CacheClient', () => {
|
|
7
|
+
|
|
8
|
+
describe('decorator cached()', () => {
|
|
9
|
+
|
|
10
|
+
interface Response {
|
|
11
|
+
variant: string;
|
|
12
|
+
value: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class MyClass extends CacheClient {
|
|
16
|
+
numCalled = 0;
|
|
17
|
+
|
|
18
|
+
cacheInstance = new LocalCache();
|
|
19
|
+
|
|
20
|
+
@CacheClient.cached()
|
|
21
|
+
async fetchSomething(variant: string): Promise<Response> {
|
|
22
|
+
this.numCalled++;
|
|
23
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
24
|
+
return {
|
|
25
|
+
variant: variant,
|
|
26
|
+
value: 100 + parseInt(variant, 10),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@CacheClient.cached() // default error-caching function: caches all errors
|
|
31
|
+
async throwingMachine1(): Promise<string> {
|
|
32
|
+
this.numCalled++;
|
|
33
|
+
throw new Error('nope');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@CacheClient.cached(undefined, err => err['retryable'] === false) // custom error-caching function: caches only 'retryable' errors
|
|
37
|
+
async throwingMachine2(): Promise<string> {
|
|
38
|
+
this.numCalled++;
|
|
39
|
+
// initially throws a retryable (to assert we don't cache),
|
|
40
|
+
// then switch to a non-retryable (to assert we do cache from that point)
|
|
41
|
+
if (this.numCalled > 1) {
|
|
42
|
+
const nonRetryableError = new Error('nope');
|
|
43
|
+
nonRetryableError['retryable'] = false;
|
|
44
|
+
throw nonRetryableError;
|
|
45
|
+
}
|
|
46
|
+
throw new Error('nope');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it ('1. provides an error-caching function that caches all errors by default, 2. cohabits with the non-caching function', async () => {
|
|
52
|
+
const myObj = new MyClass();
|
|
53
|
+
const myObjThrowingMachine1WithErrorCaching = myObj.getErrorCachingFunction('throwingMachine1');
|
|
54
|
+
|
|
55
|
+
// 1. Initial calls with *NO* caching
|
|
56
|
+
let didThrow1 = false;
|
|
57
|
+
try {
|
|
58
|
+
await myObj.throwingMachine1();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
didThrow1 = true;
|
|
61
|
+
}
|
|
62
|
+
expect(didThrow1).to.be.true;
|
|
63
|
+
expect(myObj.numCalled).to.equal(1); // initial call -> increase
|
|
64
|
+
|
|
65
|
+
let didThrow2 = false;
|
|
66
|
+
try {
|
|
67
|
+
await myObj.throwingMachine1();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
didThrow2 = true;
|
|
70
|
+
}
|
|
71
|
+
expect(didThrow2).to.be.true;
|
|
72
|
+
expect(myObj.numCalled).to.equal(2); // no caching -> increase
|
|
73
|
+
|
|
74
|
+
// 2. Interleaving calls *WITH* caching
|
|
75
|
+
let didThrow3WithCaching = false;
|
|
76
|
+
try {
|
|
77
|
+
await myObjThrowingMachine1WithErrorCaching(true);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
didThrow3WithCaching = true;
|
|
80
|
+
}
|
|
81
|
+
expect(didThrow3WithCaching).to.be.true;
|
|
82
|
+
expect(myObj.numCalled).to.equal(3); // first call with caching -> increase
|
|
83
|
+
|
|
84
|
+
let didThrow4WithCaching = false;
|
|
85
|
+
try {
|
|
86
|
+
await myObjThrowingMachine1WithErrorCaching(true);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
didThrow4WithCaching = true;
|
|
89
|
+
}
|
|
90
|
+
expect(didThrow4WithCaching).to.be.true;
|
|
91
|
+
expect(myObj.numCalled).to.equal(3); // second call with caching -> NO increase
|
|
92
|
+
|
|
93
|
+
// 3. Back to calls with *NO* caching
|
|
94
|
+
let didThrow5 = false;
|
|
95
|
+
try {
|
|
96
|
+
await myObj.throwingMachine1();
|
|
97
|
+
} catch (err) {
|
|
98
|
+
didThrow5 = true;
|
|
99
|
+
}
|
|
100
|
+
expect(didThrow5).to.be.true;
|
|
101
|
+
expect(myObj.numCalled).to.equal(4); // back to no caching -> increase
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('honors the shouldCacheError callback letting users specify which errors to cache', async () => {
|
|
105
|
+
const myObj = new MyClass();
|
|
106
|
+
const myObjThrowingMachine2WithErrorCaching = myObj.getErrorCachingFunction('throwingMachine2');
|
|
107
|
+
|
|
108
|
+
// 1. Initial call *WITH* caching, which will throw a retryable error (not cached)
|
|
109
|
+
let didThrow1 = false;
|
|
110
|
+
try {
|
|
111
|
+
await myObjThrowingMachine2WithErrorCaching();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
didThrow1 = true;
|
|
114
|
+
}
|
|
115
|
+
expect(didThrow1).to.be.true;
|
|
116
|
+
expect(myObj.numCalled).to.equal(1); // initial call -> increase
|
|
117
|
+
|
|
118
|
+
// 2. Second call *WITH* caching, which will this time throw a non-retryable error (cached)
|
|
119
|
+
let didThrow2 = false;
|
|
120
|
+
try {
|
|
121
|
+
await myObjThrowingMachine2WithErrorCaching();
|
|
122
|
+
} catch (err) {
|
|
123
|
+
didThrow2 = true;
|
|
124
|
+
}
|
|
125
|
+
expect(didThrow2).to.be.true;
|
|
126
|
+
expect(myObj.numCalled).to.equal(2); // it got called again -> increase
|
|
127
|
+
|
|
128
|
+
// 3. Third call *WITH* caching, now fetched from cache
|
|
129
|
+
let didThrow3 = false;
|
|
130
|
+
try {
|
|
131
|
+
await myObjThrowingMachine2WithErrorCaching();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
didThrow3 = true;
|
|
134
|
+
}
|
|
135
|
+
expect(didThrow3).to.be.true;
|
|
136
|
+
expect(myObj.numCalled).to.equal(2); // from cache -> NO increase
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('protect against concurrent fetches', async () => {
|
|
140
|
+
const myObj = new MyClass();
|
|
141
|
+
const jobs: Promise<any>[] = [];
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < 100; i++) {
|
|
144
|
+
const variant = i % 10;
|
|
145
|
+
jobs.push(myObj.fetchSomething(variant.toString()));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const results = await Promise.all(jobs);
|
|
149
|
+
let numSuccess = 0;
|
|
150
|
+
results.forEach(x => {
|
|
151
|
+
if (x.value === 100 + parseInt(x.variant, 10)) {
|
|
152
|
+
numSuccess++;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Number time fetched
|
|
157
|
+
expect(myObj.numCalled).to.eql(10);
|
|
158
|
+
// Number successes
|
|
159
|
+
expect(numSuccess).to.eql(100);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('can use the uncached version of a cached function', async () => {
|
|
163
|
+
const myObj = new MyClass();
|
|
164
|
+
|
|
165
|
+
await myObj.fetchSomething('123');
|
|
166
|
+
await myObj.fetchSomething('123');
|
|
167
|
+
expect(myObj.numCalled).to.eql(1);
|
|
168
|
+
|
|
169
|
+
await myObj.getUncachedFunction('fetchSomething')('123');
|
|
170
|
+
expect(myObj.numCalled).to.eql(2);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('can clear the value of a function that was cached using the decorator.', async () => {
|
|
174
|
+
const myObj = new MyClass();
|
|
175
|
+
|
|
176
|
+
await myObj.fetchSomething('123');
|
|
177
|
+
expect(myObj.numCalled).to.eql(1);
|
|
178
|
+
let cachedValue = await myObj.getCachedFunctionCall('fetchSomething', '123');
|
|
179
|
+
expect(cachedValue).to.exist;
|
|
180
|
+
|
|
181
|
+
await myObj.clearCachedFunctionCall('fetchSomething', '123');
|
|
182
|
+
cachedValue = await myObj.getCachedFunctionCall('fetchSomething', '123');
|
|
183
|
+
expect(cachedValue).not.to.exist;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('buildCacheKey', () => {
|
|
189
|
+
|
|
190
|
+
class MyCacheClient extends CacheClient {
|
|
191
|
+
cacheInstance = new LocalCache();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
it('will add null or undefined to the key', async () => {
|
|
195
|
+
|
|
196
|
+
const cacheClient = new MyCacheClient();
|
|
197
|
+
const key = cacheClient['buildCacheKey']('functionName', [null, undefined, 'argument']);
|
|
198
|
+
expect(key).to.equal('functionName-null-undefined-argument');
|
|
199
|
+
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('will convert boolean values', async () => {
|
|
203
|
+
|
|
204
|
+
const cacheClient = new MyCacheClient();
|
|
205
|
+
const key = cacheClient['buildCacheKey']('functionName', ['argument', true, 'argument', false]);
|
|
206
|
+
expect(key).to.equal('functionName-argument-true-argument-false');
|
|
207
|
+
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('will convert number values', async () => {
|
|
211
|
+
|
|
212
|
+
const cacheClient = new MyCacheClient();
|
|
213
|
+
const key = cacheClient['buildCacheKey']('functionName', ['argument', 14, 'argument', 16]);
|
|
214
|
+
expect(key).to.equal('functionName-argument-14-argument-16');
|
|
215
|
+
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('will convert plain object values', async () => {
|
|
219
|
+
const cacheClient = new MyCacheClient();
|
|
220
|
+
const expectedKey = 'functionName-argument-property1-prop1-property2-prop2-property3-nestedProp1-nestedProp1-nestedProp2-nestedProp2';
|
|
221
|
+
|
|
222
|
+
const keyWithSortedObjectProperties = cacheClient['buildCacheKey']('functionName', [
|
|
223
|
+
'argument',
|
|
224
|
+
{ property1: 'prop1', property2: 'prop2', property3: { nestedProp1: 'nestedProp1', nestedProp2: 'nestedProp2' } },
|
|
225
|
+
new Date(),
|
|
226
|
+
]);
|
|
227
|
+
expect(keyWithSortedObjectProperties).to.equal(expectedKey);
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('will convert array values', async () => {
|
|
231
|
+
const cacheClient = new MyCacheClient();
|
|
232
|
+
const expectedKey = 'functionName-prop1-propValue1-prop2-propValue2-value1-value2';
|
|
233
|
+
|
|
234
|
+
const keyWithArrayValues = cacheClient['buildCacheKey']('functionName', [
|
|
235
|
+
[
|
|
236
|
+
{ prop1: 'propValue1', prop2: 'propValue2' },
|
|
237
|
+
'value1',
|
|
238
|
+
'value2',
|
|
239
|
+
],
|
|
240
|
+
]);
|
|
241
|
+
expect(keyWithArrayValues).to.equal(expectedKey);
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('will convert array values and the result should be the same key if two array own the same properties but not in the same order', async () => {
|
|
245
|
+
const cacheClient = new MyCacheClient();
|
|
246
|
+
|
|
247
|
+
const keyWithSortedArrayValues= cacheClient['buildCacheKey']('functionName', [
|
|
248
|
+
[
|
|
249
|
+
{ prop1: 'propValue1', prop2: 'propValue2' },
|
|
250
|
+
'value1',
|
|
251
|
+
'value2',
|
|
252
|
+
],
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const keyWithUnsortedArrayValues= cacheClient['buildCacheKey']('functionName', [
|
|
256
|
+
[
|
|
257
|
+
'value1',
|
|
258
|
+
'value2',
|
|
259
|
+
{ prop1: 'propValue1', prop2: 'propValue2' },
|
|
260
|
+
],
|
|
261
|
+
]);
|
|
262
|
+
expect(keyWithSortedArrayValues).to.equal(keyWithUnsortedArrayValues);
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('will convert plain object values and the result should be the same key if two objects have the same properties but not in the same order', async () => {
|
|
266
|
+
const cacheClient = new MyCacheClient();
|
|
267
|
+
|
|
268
|
+
const keyWithSortedObjectProperties = cacheClient['buildCacheKey']('functionName', [
|
|
269
|
+
'argument',
|
|
270
|
+
{ property1: { nestedProp1: 'nestedProp1', nestedProp2: 'nestedProp2' }, property2: 'prop2' },
|
|
271
|
+
['value1', 'value2'],
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
const keyWithUnsortedObjectProperties = cacheClient['buildCacheKey']('functionName', [
|
|
275
|
+
'argument',
|
|
276
|
+
{ property2: 'prop2', property1: { nestedProp1: 'nestedProp1', nestedProp2: 'nestedProp2' } },
|
|
277
|
+
['value2', 'value1'],
|
|
278
|
+
]);
|
|
279
|
+
expect(keyWithUnsortedObjectProperties).to.equal(keyWithSortedObjectProperties);
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should throw if key is bigger than 1000', async () => {
|
|
283
|
+
const cacheClient = new MyCacheClient();
|
|
284
|
+
|
|
285
|
+
const bigArray: string[] = [];
|
|
286
|
+
while (bigArray.length < 1000) {
|
|
287
|
+
bigArray.push('myValue');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
expect(() => cacheClient['buildCacheKey']('functionName', [bigArray])).to.throw();
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('should detect circular reference in an object', async () => {
|
|
294
|
+
const cacheClient = new MyCacheClient();
|
|
295
|
+
const obj2: any = {};
|
|
296
|
+
const obj: any = {
|
|
297
|
+
property1: 'hello',
|
|
298
|
+
property2: obj2,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
obj2.property1 = obj;
|
|
302
|
+
|
|
303
|
+
expect(() => cacheClient['buildCacheKey']('functionName', [obj2])).to.throw();
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('Redlock maintenance reminder', () => {
|
|
308
|
+
|
|
309
|
+
it('is still on Redlock v4, or was carefully migrated to v5', () => {
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
311
|
+
const redlockVersion = require('../../package.json').dependencies.redlock;
|
|
312
|
+
if (redlockVersion !== '4.x') {
|
|
313
|
+
throw new Error('Migrating Redlock to v5? This breaking test is a reminder to:\n 1. Migrate v4 handling of `clientError` events into v5 `error` events\n 2. Review error handling: Redlock v5 throws at many places, while Redlock v4 only threw in its constructor\n 3. Review other breaking changes provided by the upgrade guide, if Redlock maintainers provide one when shipping v5');
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
});
|
|
318
|
+
});
|