cachette 4.0.13 → 4.0.15
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/.prettierignore +15 -0
- package/.prettierrc +8 -0
- package/dist/src/lib/CacheClient.js +10 -9
- package/dist/src/lib/CacheClient.js.map +1 -1
- package/dist/src/lib/CacheInstance.d.ts +14 -4
- package/dist/src/lib/CacheInstance.js +11 -6
- package/dist/src/lib/CacheInstance.js.map +1 -1
- package/dist/src/lib/LocalCache.d.ts +2 -7
- package/dist/src/lib/LocalCache.js +19 -24
- package/dist/src/lib/LocalCache.js.map +1 -1
- package/dist/src/lib/RedisCache.d.ts +1 -9
- package/dist/src/lib/RedisCache.js +26 -26
- package/dist/src/lib/RedisCache.js.map +1 -1
- package/dist/src/lib/WriteThroughCache.js +5 -5
- package/dist/src/lib/WriteThroughCache.js.map +1 -1
- package/package.json +4 -2
- package/src/lib/CacheClient.ts +42 -55
- package/src/lib/CacheInstance.ts +23 -15
- package/src/lib/LocalCache.ts +25 -29
- package/src/lib/RedisCache.ts +49 -53
- package/src/lib/WriteThroughCache.ts +27 -27
- package/test/CacheClient_test.ts +25 -44
- package/test/CacheInstance_test.ts +12 -40
- package/test/LocalCache_test.ts +102 -7
- package/test/RedisCache_test.ts +20 -20
- package/test/WriteThroughCache_test.ts +34 -39
package/test/CacheClient_test.ts
CHANGED
|
@@ -2,11 +2,8 @@ import { expect } from 'chai';
|
|
|
2
2
|
import { CacheClient } from '../src/lib/CacheClient';
|
|
3
3
|
import { LocalCache } from '../src/lib/LocalCache';
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
describe('CacheClient', () => {
|
|
7
|
-
|
|
8
6
|
describe('decorator cached()', () => {
|
|
9
|
-
|
|
10
7
|
interface Response {
|
|
11
8
|
variant: string;
|
|
12
9
|
value: number;
|
|
@@ -20,7 +17,7 @@ describe('CacheClient', () => {
|
|
|
20
17
|
@CacheClient.cached()
|
|
21
18
|
async fetchSomething(variant: string): Promise<Response> {
|
|
22
19
|
this.numCalled++;
|
|
23
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
24
21
|
return {
|
|
25
22
|
variant: variant,
|
|
26
23
|
value: 100 + parseInt(variant, 10),
|
|
@@ -33,7 +30,7 @@ describe('CacheClient', () => {
|
|
|
33
30
|
throw new Error('nope');
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
@CacheClient.cached(undefined, err => err['retryable'] === false) // custom error-caching function: caches only 'retryable' errors
|
|
33
|
+
@CacheClient.cached(undefined, (err) => err['retryable'] === false) // custom error-caching function: caches only 'retryable' errors
|
|
37
34
|
async throwingMachine2(): Promise<string> {
|
|
38
35
|
this.numCalled++;
|
|
39
36
|
// initially throws a retryable (to assert we don't cache),
|
|
@@ -45,10 +42,9 @@ describe('CacheClient', () => {
|
|
|
45
42
|
}
|
|
46
43
|
throw new Error('nope');
|
|
47
44
|
}
|
|
48
|
-
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
it
|
|
47
|
+
it('1. provides an error-caching function that caches all errors by default, 2. cohabits with the non-caching function', async () => {
|
|
52
48
|
const myObj = new MyClass();
|
|
53
49
|
const myObjThrowingMachine1WithErrorCaching = myObj.getErrorCachingFunction('throwingMachine1');
|
|
54
50
|
|
|
@@ -147,7 +143,7 @@ describe('CacheClient', () => {
|
|
|
147
143
|
|
|
148
144
|
const results = await Promise.all(jobs);
|
|
149
145
|
let numSuccess = 0;
|
|
150
|
-
results.forEach(x => {
|
|
146
|
+
results.forEach((x) => {
|
|
151
147
|
if (x.value === 100 + parseInt(x.variant, 10)) {
|
|
152
148
|
numSuccess++;
|
|
153
149
|
}
|
|
@@ -182,85 +178,70 @@ describe('CacheClient', () => {
|
|
|
182
178
|
cachedValue = await myObj.getCachedFunctionCall('fetchSomething', '123');
|
|
183
179
|
expect(cachedValue).not.to.exist;
|
|
184
180
|
});
|
|
185
|
-
|
|
186
181
|
});
|
|
187
182
|
|
|
188
183
|
describe('buildCacheKey', () => {
|
|
189
|
-
|
|
190
184
|
class MyCacheClient extends CacheClient {
|
|
191
185
|
cacheInstance = new LocalCache();
|
|
192
186
|
}
|
|
193
187
|
|
|
194
188
|
it('will add null or undefined to the key', async () => {
|
|
195
|
-
|
|
196
189
|
const cacheClient = new MyCacheClient();
|
|
197
190
|
const key = cacheClient['buildCacheKey']('functionName', [null, undefined, 'argument']);
|
|
198
191
|
expect(key).to.equal('functionName-null-undefined-argument');
|
|
199
|
-
|
|
200
192
|
});
|
|
201
193
|
|
|
202
194
|
it('will convert boolean values', async () => {
|
|
203
|
-
|
|
204
195
|
const cacheClient = new MyCacheClient();
|
|
205
196
|
const key = cacheClient['buildCacheKey']('functionName', ['argument', true, 'argument', false]);
|
|
206
197
|
expect(key).to.equal('functionName-argument-true-argument-false');
|
|
207
|
-
|
|
208
198
|
});
|
|
209
199
|
|
|
210
200
|
it('will convert number values', async () => {
|
|
211
|
-
|
|
212
201
|
const cacheClient = new MyCacheClient();
|
|
213
202
|
const key = cacheClient['buildCacheKey']('functionName', ['argument', 14, 'argument', 16]);
|
|
214
203
|
expect(key).to.equal('functionName-argument-14-argument-16');
|
|
215
|
-
|
|
216
204
|
});
|
|
217
205
|
|
|
218
206
|
it('will convert plain object values', async () => {
|
|
219
207
|
const cacheClient = new MyCacheClient();
|
|
220
|
-
const expectedKey =
|
|
208
|
+
const expectedKey =
|
|
209
|
+
'functionName-argument-property1-prop1-property2-prop2-property3-nestedProp1-nestedProp1-nestedProp2-nestedProp2';
|
|
221
210
|
|
|
222
211
|
const keyWithSortedObjectProperties = cacheClient['buildCacheKey']('functionName', [
|
|
223
212
|
'argument',
|
|
224
|
-
{
|
|
213
|
+
{
|
|
214
|
+
property1: 'prop1',
|
|
215
|
+
property2: 'prop2',
|
|
216
|
+
property3: { nestedProp1: 'nestedProp1', nestedProp2: 'nestedProp2' },
|
|
217
|
+
},
|
|
225
218
|
new Date(),
|
|
226
219
|
]);
|
|
227
220
|
expect(keyWithSortedObjectProperties).to.equal(expectedKey);
|
|
228
|
-
})
|
|
221
|
+
});
|
|
229
222
|
|
|
230
223
|
it('will convert array values', async () => {
|
|
231
224
|
const cacheClient = new MyCacheClient();
|
|
232
225
|
const expectedKey = 'functionName-prop1-propValue1-prop2-propValue2-value1-value2';
|
|
233
226
|
|
|
234
227
|
const keyWithArrayValues = cacheClient['buildCacheKey']('functionName', [
|
|
235
|
-
|
|
236
|
-
{ prop1: 'propValue1', prop2: 'propValue2' },
|
|
237
|
-
'value1',
|
|
238
|
-
'value2',
|
|
239
|
-
],
|
|
228
|
+
[{ prop1: 'propValue1', prop2: 'propValue2' }, 'value1', 'value2'],
|
|
240
229
|
]);
|
|
241
230
|
expect(keyWithArrayValues).to.equal(expectedKey);
|
|
242
|
-
})
|
|
231
|
+
});
|
|
243
232
|
|
|
244
233
|
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
234
|
const cacheClient = new MyCacheClient();
|
|
246
235
|
|
|
247
|
-
const keyWithSortedArrayValues= cacheClient['buildCacheKey']('functionName', [
|
|
248
|
-
[
|
|
249
|
-
{ prop1: 'propValue1', prop2: 'propValue2' },
|
|
250
|
-
'value1',
|
|
251
|
-
'value2',
|
|
252
|
-
],
|
|
236
|
+
const keyWithSortedArrayValues = cacheClient['buildCacheKey']('functionName', [
|
|
237
|
+
[{ prop1: 'propValue1', prop2: 'propValue2' }, 'value1', 'value2'],
|
|
253
238
|
]);
|
|
254
239
|
|
|
255
|
-
const keyWithUnsortedArrayValues= cacheClient['buildCacheKey']('functionName', [
|
|
256
|
-
[
|
|
257
|
-
'value1',
|
|
258
|
-
'value2',
|
|
259
|
-
{ prop1: 'propValue1', prop2: 'propValue2' },
|
|
260
|
-
],
|
|
240
|
+
const keyWithUnsortedArrayValues = cacheClient['buildCacheKey']('functionName', [
|
|
241
|
+
['value1', 'value2', { prop1: 'propValue1', prop2: 'propValue2' }],
|
|
261
242
|
]);
|
|
262
243
|
expect(keyWithSortedArrayValues).to.equal(keyWithUnsortedArrayValues);
|
|
263
|
-
})
|
|
244
|
+
});
|
|
264
245
|
|
|
265
246
|
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
247
|
const cacheClient = new MyCacheClient();
|
|
@@ -277,7 +258,7 @@ describe('CacheClient', () => {
|
|
|
277
258
|
['value2', 'value1'],
|
|
278
259
|
]);
|
|
279
260
|
expect(keyWithUnsortedObjectProperties).to.equal(keyWithSortedObjectProperties);
|
|
280
|
-
})
|
|
261
|
+
});
|
|
281
262
|
|
|
282
263
|
it('should throw if key is bigger than 1000', async () => {
|
|
283
264
|
const cacheClient = new MyCacheClient();
|
|
@@ -288,7 +269,7 @@ describe('CacheClient', () => {
|
|
|
288
269
|
}
|
|
289
270
|
|
|
290
271
|
expect(() => cacheClient['buildCacheKey']('functionName', [bigArray])).to.throw();
|
|
291
|
-
})
|
|
272
|
+
});
|
|
292
273
|
|
|
293
274
|
it('should detect circular reference in an object', async () => {
|
|
294
275
|
const cacheClient = new MyCacheClient();
|
|
@@ -301,18 +282,18 @@ describe('CacheClient', () => {
|
|
|
301
282
|
obj2.property1 = obj;
|
|
302
283
|
|
|
303
284
|
expect(() => cacheClient['buildCacheKey']('functionName', [obj2])).to.throw();
|
|
304
|
-
})
|
|
285
|
+
});
|
|
305
286
|
});
|
|
306
287
|
|
|
307
288
|
describe('Redlock maintenance reminder', () => {
|
|
308
|
-
|
|
309
289
|
it('is still on Redlock v4, or was carefully migrated to v5', () => {
|
|
310
290
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
311
291
|
const redlockVersion = require('../../package.json').dependencies.redlock;
|
|
312
292
|
if (redlockVersion !== '4.x') {
|
|
313
|
-
throw new Error(
|
|
293
|
+
throw new Error(
|
|
294
|
+
'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',
|
|
295
|
+
);
|
|
314
296
|
}
|
|
315
297
|
});
|
|
316
|
-
|
|
317
298
|
});
|
|
318
299
|
});
|
|
@@ -7,14 +7,13 @@ import { WriteThroughCache } from '../src/lib/WriteThroughCache';
|
|
|
7
7
|
import { CacheInstance, FetchingFunction } from '../src/lib/CacheInstance';
|
|
8
8
|
|
|
9
9
|
async function sleep(ms: number): Promise<void> {
|
|
10
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// set env var TEST_REDIS_URL (e.g. redis://localhost:6379) to enable running
|
|
14
14
|
// the tests with Redis
|
|
15
15
|
|
|
16
16
|
describe('CacheInstance', () => {
|
|
17
|
-
|
|
18
17
|
runTests('local', new LocalCache());
|
|
19
18
|
|
|
20
19
|
if (process.env.TEST_REDIS_URL) {
|
|
@@ -24,13 +23,11 @@ describe('CacheInstance', () => {
|
|
|
24
23
|
const writeThroughCache = new WriteThroughCache(process.env.TEST_REDIS_URL);
|
|
25
24
|
runTests('writeThrough', writeThroughCache);
|
|
26
25
|
}
|
|
27
|
-
|
|
28
26
|
});
|
|
29
27
|
|
|
30
28
|
function runTests(name: string, cache: CacheInstance): void {
|
|
31
|
-
|
|
32
29
|
const lockSupported = cache.isLockingSupported();
|
|
33
|
-
const ifLockIt =
|
|
30
|
+
const ifLockIt = cache && cache.isLockingSupported() ? it : it.skip;
|
|
34
31
|
let lockSpy: sinon.SinonSpy;
|
|
35
32
|
let unlockSpy: sinon.SinonSpy;
|
|
36
33
|
|
|
@@ -57,9 +54,7 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
57
54
|
}
|
|
58
55
|
});
|
|
59
56
|
|
|
60
|
-
|
|
61
57
|
describe(`getOrFetchValue - ${name}`, () => {
|
|
62
|
-
|
|
63
58
|
beforeEach(() => cache.clear());
|
|
64
59
|
|
|
65
60
|
it('does not fetch if value in cache', async () => {
|
|
@@ -94,11 +89,7 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
94
89
|
|
|
95
90
|
await cache.setValue('key2', 'value');
|
|
96
91
|
const fetchFunction = object.fetch.bind(object, 'newvalue');
|
|
97
|
-
const value = await cache.getOrFetchValue(
|
|
98
|
-
'key',
|
|
99
|
-
10,
|
|
100
|
-
fetchFunction,
|
|
101
|
-
);
|
|
92
|
+
const value = await cache.getOrFetchValue('key', 10, fetchFunction);
|
|
102
93
|
expect(value).to.eql('newvalue');
|
|
103
94
|
expect(numCalled).to.eql(1);
|
|
104
95
|
if (lockSupported) {
|
|
@@ -194,13 +185,8 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
194
185
|
const fetchFunction = object.fetchThatThrowsAfterThree.bind(object, 'newvalue');
|
|
195
186
|
|
|
196
187
|
let didThrow = false;
|
|
197
|
-
const getFromCache = async () =>
|
|
198
|
-
'key',
|
|
199
|
-
10,
|
|
200
|
-
fetchFunction,
|
|
201
|
-
undefined,
|
|
202
|
-
(err) => err.name !== 'NonCacheableError',
|
|
203
|
-
);
|
|
188
|
+
const getFromCache = async () =>
|
|
189
|
+
cache.getOrFetchValue('key', 10, fetchFunction, undefined, (err) => err.name !== 'NonCacheableError');
|
|
204
190
|
try {
|
|
205
191
|
await getFromCache();
|
|
206
192
|
} catch (err) {
|
|
@@ -258,11 +244,7 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
258
244
|
await cache.setValue('key2', 'value');
|
|
259
245
|
|
|
260
246
|
const fetchFunction = object.fetch.bind(object, 'newvalue');
|
|
261
|
-
const callGetOrFetch = () => cache.getOrFetchValue(
|
|
262
|
-
'key',
|
|
263
|
-
10,
|
|
264
|
-
fetchFunction,
|
|
265
|
-
);
|
|
247
|
+
const callGetOrFetch = () => cache.getOrFetchValue('key', 10, fetchFunction);
|
|
266
248
|
|
|
267
249
|
const calls: Promise<any>[] = [];
|
|
268
250
|
for (let i = 0; i < 100; i++) {
|
|
@@ -293,18 +275,14 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
293
275
|
|
|
294
276
|
const callGetOrFetch = (key, fn) => {
|
|
295
277
|
const fetchFunction = fn.bind(object, 'newvalue');
|
|
296
|
-
return cache.getOrFetchValue(
|
|
297
|
-
key,
|
|
298
|
-
10,
|
|
299
|
-
fetchFunction,
|
|
300
|
-
);
|
|
278
|
+
return cache.getOrFetchValue(key, 10, fetchFunction);
|
|
301
279
|
};
|
|
302
280
|
|
|
303
281
|
const calls: Promise<any>[] = [];
|
|
304
282
|
|
|
305
283
|
for (let i = 0; i < 100; i++) {
|
|
306
|
-
const fn =
|
|
307
|
-
const key =
|
|
284
|
+
const fn = i % 2 ? object.fetch1 : object.fetch2;
|
|
285
|
+
const key = i % 2 ? 'key1' : 'key2';
|
|
308
286
|
calls.push(callGetOrFetch(key, fn as FetchingFunction));
|
|
309
287
|
}
|
|
310
288
|
|
|
@@ -334,11 +312,7 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
334
312
|
},
|
|
335
313
|
};
|
|
336
314
|
|
|
337
|
-
const callGetOrFetch = () => cache.getOrFetchValue(
|
|
338
|
-
'key',
|
|
339
|
-
10,
|
|
340
|
-
object.fetch,
|
|
341
|
-
);
|
|
315
|
+
const callGetOrFetch = () => cache.getOrFetchValue('key', 10, object.fetch);
|
|
342
316
|
|
|
343
317
|
const calls: Promise<any>[] = [];
|
|
344
318
|
for (let i = 0; i < 10; i++) {
|
|
@@ -448,7 +422,7 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
448
422
|
};
|
|
449
423
|
|
|
450
424
|
const fetchFunction = object.fetch.bind(object, 'newvalue');
|
|
451
|
-
const value = await cache.getOrFetchValue(key, 10, fetchFunction, 1);
|
|
425
|
+
const value = await cache.getOrFetchValue(key, 10, fetchFunction, 1); // enable locking
|
|
452
426
|
|
|
453
427
|
expect(value).to.eql('newvalue');
|
|
454
428
|
expect(numCalled).to.eql(1);
|
|
@@ -479,10 +453,8 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
479
453
|
|
|
480
454
|
expect(value).to.eql('abcd');
|
|
481
455
|
expect(numCalled).to.eql(0);
|
|
482
|
-
sinon.assert.calledTwice(lockSpy);
|
|
456
|
+
sinon.assert.calledTwice(lockSpy); // includes our own call above
|
|
483
457
|
sinon.assert.calledTwice(unlockSpy);
|
|
484
458
|
});
|
|
485
|
-
|
|
486
459
|
});
|
|
487
|
-
|
|
488
460
|
}
|
package/test/LocalCache_test.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { expect } from 'chai';
|
|
2
|
+
import * as sinon from 'sinon';
|
|
2
3
|
|
|
3
4
|
import { LocalCache } from '../src/lib/LocalCache';
|
|
4
|
-
|
|
5
|
+
import { CacheInstance } from '../src/lib/CacheInstance';
|
|
5
6
|
|
|
6
7
|
describe('LocalCache', () => {
|
|
7
|
-
|
|
8
8
|
it('can set values', async () => {
|
|
9
9
|
const cache = new LocalCache();
|
|
10
10
|
const wasSet = await cache.setValue('key', 'value');
|
|
@@ -50,13 +50,12 @@ describe('LocalCache', () => {
|
|
|
50
50
|
|
|
51
51
|
it('can set values with expiry', async () => {
|
|
52
52
|
const cache = new LocalCache();
|
|
53
|
-
await cache.setValue('key', 'value', .2);
|
|
53
|
+
await cache.setValue('key', 'value', 0.2);
|
|
54
54
|
let value = await cache.getValue('key');
|
|
55
55
|
expect(value).to.equal('value');
|
|
56
56
|
await sleep(250);
|
|
57
57
|
value = await cache.getValue('key');
|
|
58
58
|
expect(value).to.equal(undefined);
|
|
59
|
-
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
it('can delete a value', async () => {
|
|
@@ -67,7 +66,6 @@ describe('LocalCache', () => {
|
|
|
67
66
|
await cache.delValue('key');
|
|
68
67
|
|
|
69
68
|
expect(await cache.getValue('key')).not.to.exist;
|
|
70
|
-
|
|
71
69
|
});
|
|
72
70
|
|
|
73
71
|
it('will not grow in size past the maximum size', async () => {
|
|
@@ -92,8 +90,7 @@ describe('LocalCache', () => {
|
|
|
92
90
|
|
|
93
91
|
// restore
|
|
94
92
|
LocalCache['DEFAULT_MAX_ITEMS'] = origMax;
|
|
95
|
-
|
|
96
|
-
})
|
|
93
|
+
});
|
|
97
94
|
|
|
98
95
|
it('can get the remaining TTL of an item', async () => {
|
|
99
96
|
const cache = new LocalCache();
|
|
@@ -121,6 +118,104 @@ describe('LocalCache', () => {
|
|
|
121
118
|
expect(cacheTtl).to.equal(0);
|
|
122
119
|
});
|
|
123
120
|
|
|
121
|
+
describe('lock', () => {
|
|
122
|
+
it('returns a Lock object with value property and unlock method', async () => {
|
|
123
|
+
const cache = new LocalCache();
|
|
124
|
+
const lock = await cache.lock('test-resource', 10000);
|
|
125
|
+
|
|
126
|
+
expect(lock).to.have.property('value');
|
|
127
|
+
expect(lock.value).to.be.a('string');
|
|
128
|
+
expect(lock.value).to.match(/^local-\d+-[a-z0-9]+$/);
|
|
129
|
+
|
|
130
|
+
expect(lock).to.have.property('unlock');
|
|
131
|
+
expect(lock.unlock).to.be.a('function');
|
|
132
|
+
|
|
133
|
+
await cache.unlock(lock);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('releases lock via the returned Lock object unlock method', async () => {
|
|
137
|
+
const cache = new LocalCache();
|
|
138
|
+
const resource = `test-resource-${Math.random()}`;
|
|
139
|
+
|
|
140
|
+
const lock = await cache.lock(resource, 10000);
|
|
141
|
+
expect(await cache.hasLock(resource)).to.be.true;
|
|
142
|
+
|
|
143
|
+
await lock.unlock();
|
|
144
|
+
expect(await cache.hasLock(resource)).to.be.false;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('releases lock via cache.unlock()', async () => {
|
|
148
|
+
const cache = new LocalCache();
|
|
149
|
+
const resource = `test-resource-${Math.random()}`;
|
|
150
|
+
|
|
151
|
+
const lock = await cache.lock(resource, 10000);
|
|
152
|
+
expect(await cache.hasLock(resource)).to.be.true;
|
|
153
|
+
|
|
154
|
+
await cache.unlock(lock);
|
|
155
|
+
expect(await cache.hasLock(resource)).to.be.false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('with retry = true, retries until lock is available', async () => {
|
|
159
|
+
// Fake only setTimeout to avoid conflicts with LRU cache's TTL (which uses Date)
|
|
160
|
+
const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const resource = 'test-resource';
|
|
164
|
+
const cache = new LocalCache();
|
|
165
|
+
const hasSpy = sinon.spy(cache['cache'], 'has');
|
|
166
|
+
|
|
167
|
+
const firstLock = await cache.lock(resource, 10000);
|
|
168
|
+
const hasCallsAfterFirstLock = hasSpy.callCount;
|
|
169
|
+
|
|
170
|
+
let secondLockAcquired = false;
|
|
171
|
+
const lockPromise = cache.lock(resource, 10000, true).then((lock) => {
|
|
172
|
+
secondLockAcquired = true;
|
|
173
|
+
return lock;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Advance time to trigger retry, but lock still held
|
|
177
|
+
await clock.tickAsync(CacheInstance.LOCK_RETRY_DELAY_MS);
|
|
178
|
+
expect(secondLockAcquired).to.be.false;
|
|
179
|
+
expect(hasSpy.callCount).to.be.greaterThan(hasCallsAfterFirstLock);
|
|
180
|
+
|
|
181
|
+
// Release first lock
|
|
182
|
+
await cache.unlock(firstLock);
|
|
183
|
+
|
|
184
|
+
// Advance time for next retry - should acquire now
|
|
185
|
+
await clock.tickAsync(CacheInstance.LOCK_RETRY_DELAY_MS);
|
|
186
|
+
const secondLock = await lockPromise;
|
|
187
|
+
|
|
188
|
+
expect(secondLockAcquired).to.be.true;
|
|
189
|
+
expect(secondLock).to.have.property('value');
|
|
190
|
+
|
|
191
|
+
hasSpy.restore();
|
|
192
|
+
await cache.unlock(secondLock);
|
|
193
|
+
} finally {
|
|
194
|
+
clock.restore();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('with retry = false, throws immediately if lock is held', async () => {
|
|
199
|
+
const cache = new LocalCache();
|
|
200
|
+
const resource = `test-resource-${Math.random()}`;
|
|
201
|
+
|
|
202
|
+
const firstLock = await cache.lock(resource, 10000);
|
|
203
|
+
expect(await cache.hasLock(resource)).to.be.true;
|
|
204
|
+
|
|
205
|
+
let error: Error | undefined;
|
|
206
|
+
try {
|
|
207
|
+
await cache.lock(resource, 10000, false);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
error = err as Error;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
expect(error).to.exist;
|
|
213
|
+
expect(error!.message).to.include('Failed to acquire lock');
|
|
214
|
+
expect(error!.message).to.include('after 1 attempts');
|
|
215
|
+
|
|
216
|
+
await cache.unlock(firstLock);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
124
219
|
});
|
|
125
220
|
|
|
126
221
|
function sleep(ms: number): Promise<void> {
|
package/test/RedisCache_test.ts
CHANGED
|
@@ -3,9 +3,7 @@ import * as sinon from 'sinon';
|
|
|
3
3
|
|
|
4
4
|
import { RedisCache, SIZE_THRESHOLD_WARNING_BYTES } from '../src/lib/RedisCache';
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
describe('RedisCache', () => {
|
|
8
|
-
|
|
9
7
|
describe('constructor', () => {
|
|
10
8
|
it('will not crash the application given an invalid Redis URL', async () => {
|
|
11
9
|
let cache = new RedisCache('redis://localhost:9999');
|
|
@@ -17,20 +15,15 @@ describe('RedisCache', () => {
|
|
|
17
15
|
});
|
|
18
16
|
|
|
19
17
|
it('will raise an error if given a Redis URL without protocol', async () => {
|
|
20
|
-
expect(
|
|
21
|
-
() => new RedisCache('rer17kq3qdwc5wmy.4gzf3f.ng.0001.use1.cache.amazonaws.com'),
|
|
22
|
-
).to.throw();
|
|
18
|
+
expect(() => new RedisCache('rer17kq3qdwc5wmy.4gzf3f.ng.0001.use1.cache.amazonaws.com')).to.throw();
|
|
23
19
|
});
|
|
24
20
|
|
|
25
21
|
it('will raise an error if given a Redis URL with an invalid protocol', async () => {
|
|
26
|
-
expect(
|
|
27
|
-
() => new RedisCache('potato://rer17kq3qdwc5wmy.4gzf3f.ng.0001.use1.cache.amazonaws.com'),
|
|
28
|
-
).to.throw();
|
|
22
|
+
expect(() => new RedisCache('potato://rer17kq3qdwc5wmy.4gzf3f.ng.0001.use1.cache.amazonaws.com')).to.throw();
|
|
29
23
|
});
|
|
30
24
|
});
|
|
31
25
|
|
|
32
26
|
describe('value serialization', () => {
|
|
33
|
-
|
|
34
27
|
it('can serialize the null value', () => {
|
|
35
28
|
let value = RedisCache.serializeValue(null);
|
|
36
29
|
expect(value).to.equal(RedisCache.NULL_VALUE);
|
|
@@ -58,12 +51,15 @@ describe('RedisCache', () => {
|
|
|
58
51
|
value = RedisCache.deserializeValue(value);
|
|
59
52
|
expect(value).to.deep.equal(obj);
|
|
60
53
|
});
|
|
61
|
-
|
|
54
|
+
|
|
62
55
|
it('can serialize an object with a nested map', () => {
|
|
63
|
-
const mapStructure: Map<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
const mapStructure: Map<
|
|
57
|
+
string,
|
|
58
|
+
{
|
|
59
|
+
checksum: number;
|
|
60
|
+
originCommentId?: string;
|
|
61
|
+
}
|
|
62
|
+
> = new Map();
|
|
67
63
|
mapStructure.set('key1', { checksum: 1, originCommentId: 'c1' });
|
|
68
64
|
mapStructure.set('key2', { checksum: 2, originCommentId: 'c2' });
|
|
69
65
|
const obj = {
|
|
@@ -83,7 +79,7 @@ describe('RedisCache', () => {
|
|
|
83
79
|
value = RedisCache.deserializeValue(value);
|
|
84
80
|
expect(value).to.deep.equal(obj);
|
|
85
81
|
});
|
|
86
|
-
|
|
82
|
+
|
|
87
83
|
it('can serialize an object with a nested set', () => {
|
|
88
84
|
const setStructure: Set<string> = new Set();
|
|
89
85
|
setStructure.add('key1');
|
|
@@ -124,7 +120,6 @@ describe('RedisCache', () => {
|
|
|
124
120
|
const value = RedisCache.deserializeValue(null);
|
|
125
121
|
expect(value).to.equal(undefined);
|
|
126
122
|
});
|
|
127
|
-
|
|
128
123
|
});
|
|
129
124
|
|
|
130
125
|
describe('setValue', async () => {
|
|
@@ -237,7 +232,10 @@ describe('RedisCache', () => {
|
|
|
237
232
|
const key = `emits-warning-on-large-value-${Math.random()}`;
|
|
238
233
|
await cache.setValue(key, 'a'.repeat(SIZE_THRESHOLD_WARNING_BYTES));
|
|
239
234
|
|
|
240
|
-
const warningsAfterSetLargeKey = warnSpy
|
|
235
|
+
const warningsAfterSetLargeKey = warnSpy
|
|
236
|
+
.getCalls()
|
|
237
|
+
.map((c) => c.firstArg)
|
|
238
|
+
.filter((msg) => msg.includes('Writing large value to Redis!'));
|
|
241
239
|
expect(warningsAfterSetLargeKey.length).to.equal(1);
|
|
242
240
|
});
|
|
243
241
|
|
|
@@ -254,13 +252,15 @@ describe('RedisCache', () => {
|
|
|
254
252
|
const key = `doesnt-emit-warning-on-small-value-${Math.random()}`;
|
|
255
253
|
await cache.setValue(key, 'a'.repeat(SIZE_THRESHOLD_WARNING_BYTES - 1));
|
|
256
254
|
|
|
257
|
-
const warningsAfterSetLargeKey = warnSpy
|
|
255
|
+
const warningsAfterSetLargeKey = warnSpy
|
|
256
|
+
.getCalls()
|
|
257
|
+
.map((c) => c.firstArg)
|
|
258
|
+
.filter((msg) => msg.includes('Writing large value to Redis!'));
|
|
258
259
|
expect(warningsAfterSetLargeKey.length).to.equal(0);
|
|
259
260
|
});
|
|
260
261
|
});
|
|
261
262
|
|
|
262
263
|
describe('itemCount', async () => {
|
|
263
|
-
|
|
264
264
|
it('can count the items in the redis cache.', async function (): Promise<void> {
|
|
265
265
|
if (!process.env.TEST_REDIS_URL) {
|
|
266
266
|
this.skip();
|
|
@@ -293,7 +293,7 @@ describe('RedisCache', () => {
|
|
|
293
293
|
await cache.clear();
|
|
294
294
|
|
|
295
295
|
await cache.setValue('test1', 'value1');
|
|
296
|
-
|
|
296
|
+
|
|
297
297
|
const replicationAcknowledged = await cache.waitForReplication(0, 50);
|
|
298
298
|
|
|
299
299
|
// No replicas so we expect 0. This test basically confirms that waitForReplication doesn't crash. 🤷♂️
|