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.
@@ -0,0 +1,488 @@
1
+ import { expect } from 'chai';
2
+ import * as sinon from 'sinon';
3
+
4
+ import { LocalCache } from '../src/lib/LocalCache';
5
+ import { RedisCache } from '../src/lib/RedisCache';
6
+ import { WriteThroughCache } from '../src/lib/WriteThroughCache';
7
+ import { CacheInstance, FetchingFunction } from '../src/lib/CacheInstance';
8
+
9
+ async function sleep(ms: number): Promise<void> {
10
+ return new Promise(resolve => setTimeout(resolve, ms));
11
+ }
12
+
13
+ // set env var TEST_REDIS_URL (e.g. redis://localhost:6379) to enable running
14
+ // the tests with Redis
15
+
16
+ describe('CacheInstance', () => {
17
+
18
+ runTests('local', new LocalCache());
19
+
20
+ if (process.env.TEST_REDIS_URL) {
21
+ const redisCache = new RedisCache(process.env.TEST_REDIS_URL);
22
+ runTests('redis', redisCache);
23
+
24
+ const writeThroughCache = new WriteThroughCache(process.env.TEST_REDIS_URL);
25
+ runTests('writeThrough', writeThroughCache);
26
+ }
27
+
28
+ });
29
+
30
+ function runTests(name: string, cache: CacheInstance): void {
31
+
32
+ const lockSupported = cache.isLockingSupported();
33
+ const ifLockIt = (cache && cache.isLockingSupported()) ? it : it.skip;
34
+ let lockSpy: sinon.SinonSpy;
35
+ let unlockSpy: sinon.SinonSpy;
36
+
37
+ before(() => {
38
+ if (lockSupported) {
39
+ lockSpy = sinon.spy(cache, 'lock');
40
+ unlockSpy = sinon.spy(cache, 'unlock');
41
+ }
42
+ });
43
+
44
+ beforeEach(() => {
45
+ if (lockSupported) {
46
+ lockSpy.resetHistory();
47
+ unlockSpy.resetHistory();
48
+ }
49
+ });
50
+
51
+ after(() => {
52
+ if (lockSpy) {
53
+ lockSpy.restore();
54
+ }
55
+ if (unlockSpy) {
56
+ unlockSpy.restore();
57
+ }
58
+ });
59
+
60
+
61
+ describe(`getOrFetchValue - ${name}`, () => {
62
+
63
+ beforeEach(() => cache.clear());
64
+
65
+ it('does not fetch if value in cache', async () => {
66
+ const key = `key${Math.random()}`;
67
+ let numCalled = 0;
68
+ const object = {
69
+ fetch: async (v) => {
70
+ numCalled++;
71
+ return v;
72
+ },
73
+ };
74
+
75
+ await cache.setValue(key, 'value');
76
+ const fetchFunction: () => Promise<string> = object.fetch.bind(object, 'newvalue');
77
+ const value = await cache.getOrFetchValue(key, 10, fetchFunction);
78
+ expect(value).to.eql('value');
79
+ expect(numCalled).to.eql(0);
80
+ if (lockSupported) {
81
+ sinon.assert.notCalled(lockSpy);
82
+ sinon.assert.notCalled(unlockSpy);
83
+ }
84
+ });
85
+
86
+ it('fetches if value not in cache', async () => {
87
+ let numCalled = 0;
88
+ const object = {
89
+ fetch: async (v) => {
90
+ numCalled++;
91
+ return v;
92
+ },
93
+ };
94
+
95
+ await cache.setValue('key2', 'value');
96
+ const fetchFunction = object.fetch.bind(object, 'newvalue');
97
+ const value = await cache.getOrFetchValue(
98
+ 'key',
99
+ 10,
100
+ fetchFunction,
101
+ );
102
+ expect(value).to.eql('newvalue');
103
+ expect(numCalled).to.eql(1);
104
+ if (lockSupported) {
105
+ sinon.assert.notCalled(lockSpy);
106
+ sinon.assert.notCalled(unlockSpy);
107
+ }
108
+ });
109
+
110
+ it('does not cache exceptions by default', async () => {
111
+ let numCalled = 0;
112
+ const object = {
113
+ fetchThatThrows: async () => {
114
+ numCalled++;
115
+ throw new Error(`nope ${numCalled}`);
116
+ },
117
+ };
118
+
119
+ const fetchFunction = object.fetchThatThrows.bind(object, 'newvalue');
120
+ try {
121
+ await cache.getOrFetchValue('key', 10, fetchFunction);
122
+ } catch (err) {
123
+ expect(err.message).to.equal('nope 1');
124
+ }
125
+ expect(numCalled).to.equal(1);
126
+
127
+ try {
128
+ await cache.getOrFetchValue('key', 10, fetchFunction);
129
+ } catch (err) {
130
+ expect(err.message).to.equal('nope 2'); // <- no caching happened
131
+ }
132
+ expect(numCalled).to.equal(2);
133
+ });
134
+
135
+ it('caches exceptions if asked to', async () => {
136
+ let numCalled = 0;
137
+ const object = {
138
+ fetchThatThrows: async () => {
139
+ numCalled++;
140
+ const error = new Error(`nope ${numCalled}`);
141
+ error.name = 'MyCustomError';
142
+ // Some people enrich their errors objects with metadata.
143
+ // We ensure to preserve these.
144
+ error['myStringProperty'] = 'foo';
145
+ error['myBooleanProperty'] = true;
146
+ error['myNumberProperty'] = 1789.1789;
147
+ throw error;
148
+ },
149
+ };
150
+ const fetchFunction = object.fetchThatThrows.bind(object, 'newvalue');
151
+
152
+ let didThrow = false;
153
+ const getFromCache = async () => cache.getOrFetchValue('key', 10, fetchFunction, undefined, () => true);
154
+ try {
155
+ await getFromCache();
156
+ } catch (err) {
157
+ // initial throw, without cache
158
+ didThrow = true;
159
+ expect(err.message).to.equal('nope 1');
160
+ expect(err.name).to.equal('MyCustomError');
161
+ expect(err.myStringProperty).to.equal('foo');
162
+ expect(err.myBooleanProperty).to.equal(true);
163
+ expect(err.myNumberProperty).to.equal(1789.1789);
164
+ }
165
+ expect(didThrow).to.be.true;
166
+ expect(numCalled).to.equal(1);
167
+
168
+ didThrow = false;
169
+ try {
170
+ await getFromCache();
171
+ } catch (err) {
172
+ // second throw, cached
173
+ didThrow = true;
174
+ expect(err.message).to.equal('nope 1'); // <-- from cache; didn't increase
175
+ expect(err.name).to.equal('MyCustomError');
176
+ expect(err.myStringProperty).to.equal('foo');
177
+ expect(err.myBooleanProperty).to.equal(true);
178
+ expect(err.myNumberProperty).to.equal(1789.1789);
179
+ }
180
+ expect(didThrow).to.be.true;
181
+ expect(numCalled).to.equal(1); // <-- from cache; didn't increase
182
+ });
183
+
184
+ it('honors the shouldCacheError callback to determine whether to cache or not to cache -- (?, that is the question)', async () => {
185
+ let numCalled = 0;
186
+ const object = {
187
+ fetchThatThrowsAfterThree: async () => {
188
+ numCalled++;
189
+ const error = new Error(`nope ${numCalled}`);
190
+ error.name = numCalled >= 3 ? 'CacheableError' : 'NonCacheableError';
191
+ throw error;
192
+ },
193
+ };
194
+ const fetchFunction = object.fetchThatThrowsAfterThree.bind(object, 'newvalue');
195
+
196
+ let didThrow = false;
197
+ const getFromCache = async () => cache.getOrFetchValue(
198
+ 'key',
199
+ 10,
200
+ fetchFunction,
201
+ undefined,
202
+ (err) => err.name !== 'NonCacheableError',
203
+ );
204
+ try {
205
+ await getFromCache();
206
+ } catch (err) {
207
+ didThrow = true;
208
+ expect(err.message).to.equal('nope 1');
209
+ expect(err.name).to.equal('NonCacheableError');
210
+ }
211
+ expect(didThrow).to.be.true;
212
+ expect(numCalled).to.equal(1);
213
+
214
+ didThrow = false;
215
+ try {
216
+ await getFromCache();
217
+ } catch (err) {
218
+ didThrow = true;
219
+ expect(err.message).to.equal('nope 2');
220
+ expect(err.name).to.equal('NonCacheableError');
221
+ }
222
+ expect(didThrow).to.be.true;
223
+ expect(numCalled).to.equal(2); // <-- from actuall call, did increase
224
+
225
+ // next calls (after third call) will produce a cacheable error
226
+ didThrow = false;
227
+ try {
228
+ await getFromCache();
229
+ } catch (err) {
230
+ didThrow = true;
231
+ expect(err.message).to.equal('nope 3');
232
+ expect(err.name).to.equal('CacheableError');
233
+ }
234
+ expect(didThrow).to.be.true;
235
+ expect(numCalled).to.equal(3); // <-- from actual call, did increase
236
+
237
+ didThrow = false;
238
+ try {
239
+ await getFromCache();
240
+ } catch (err) {
241
+ didThrow = true;
242
+ expect(err.message).to.equal('nope 3');
243
+ expect(err.name).to.equal('CacheableError');
244
+ }
245
+ expect(didThrow).to.be.true;
246
+ expect(numCalled).to.equal(3); // <-- from cache, didn't increase
247
+ });
248
+
249
+ it('fetches once if multiple simultaneous requests', async () => {
250
+ let numCalled = 0;
251
+ const object = {
252
+ fetch: async (value) => {
253
+ numCalled++;
254
+ return value;
255
+ },
256
+ };
257
+
258
+ await cache.setValue('key2', 'value');
259
+
260
+ const fetchFunction = object.fetch.bind(object, 'newvalue');
261
+ const callGetOrFetch = () => cache.getOrFetchValue(
262
+ 'key',
263
+ 10,
264
+ fetchFunction,
265
+ );
266
+
267
+ const calls: Promise<any>[] = [];
268
+ for (let i = 0; i < 100; i++) {
269
+ calls.push(callGetOrFetch());
270
+ }
271
+
272
+ const values = await Promise.all(calls);
273
+ expect(values.length).to.eql(100);
274
+ for (const value of values) {
275
+ expect(value).to.eql('newvalue');
276
+ }
277
+ expect(numCalled).to.eql(1);
278
+ });
279
+
280
+ it('fetches once each if multiple simultaneous of two requests', async () => {
281
+ let numCalled1 = 0;
282
+ let numCalled2 = 0;
283
+ const object = {
284
+ fetch1: async (value) => {
285
+ numCalled1++;
286
+ return value;
287
+ },
288
+ fetch2: async (value) => {
289
+ numCalled2++;
290
+ return `${value}bis`;
291
+ },
292
+ };
293
+
294
+ const callGetOrFetch = (key, fn) => {
295
+ const fetchFunction = fn.bind(object, 'newvalue');
296
+ return cache.getOrFetchValue(
297
+ key,
298
+ 10,
299
+ fetchFunction,
300
+ );
301
+ };
302
+
303
+ const calls: Promise<any>[] = [];
304
+
305
+ for (let i = 0; i < 100; i++) {
306
+ const fn = (i % 2) ? object.fetch1 : object.fetch2;
307
+ const key = (i % 2) ? 'key1' : 'key2';
308
+ calls.push(callGetOrFetch(key, fn as FetchingFunction));
309
+ }
310
+
311
+ const values = await Promise.all(calls);
312
+ expect(values.length).to.eql(100);
313
+ let count1 = 0;
314
+ let count2 = 0;
315
+ for (const value of values) {
316
+ if (value === 'newvalue') {
317
+ count1++;
318
+ } else if (value === 'newvaluebis') {
319
+ count2++;
320
+ } else {
321
+ expect(value).to.eql('newvalue');
322
+ }
323
+ }
324
+ expect(numCalled1).to.eql(1);
325
+ expect(numCalled2).to.eql(1);
326
+ expect(count1).to.eql(50);
327
+ expect(count2).to.eql(50);
328
+ });
329
+
330
+ it('handles errors during simultaneous requests', async () => {
331
+ const object = {
332
+ fetch: async (): Promise<number> => {
333
+ throw new Error('basta');
334
+ },
335
+ };
336
+
337
+ const callGetOrFetch = () => cache.getOrFetchValue(
338
+ 'key',
339
+ 10,
340
+ object.fetch,
341
+ );
342
+
343
+ const calls: Promise<any>[] = [];
344
+ for (let i = 0; i < 10; i++) {
345
+ calls.push(callGetOrFetch());
346
+ }
347
+
348
+ let numExceptions = 0;
349
+ for (const call of calls) {
350
+ await call.catch(() => numExceptions++);
351
+ }
352
+
353
+ expect(numExceptions).to.eql(10);
354
+ });
355
+
356
+ ifLockIt('creates locks that expire', async () => {
357
+ const prefix = `prefix_${Math.random()}`;
358
+ await cache.lock(`${prefix}_sublock1`, 50);
359
+ const locksExist = await cache.hasLock(prefix);
360
+ expect(locksExist).to.be.true;
361
+
362
+ await sleep(51);
363
+ const locksExistAfterSleeping = await cache.hasLock(prefix);
364
+ expect(locksExistAfterSleeping).to.be.false;
365
+ });
366
+
367
+ ifLockIt('waits for expiry of existing locks', async () => {
368
+ const key = `prefix_${Math.random()}`;
369
+ const timeBeforeFirstLock = Date.now();
370
+ const firstLockTTL = 50;
371
+ await cache.lock(key, firstLockTTL);
372
+ const locksExist = await cache.hasLock(key);
373
+ expect(locksExist).to.be.true;
374
+
375
+ await cache.lock(key, 1000);
376
+ const timeAfterFirstLock = Date.now();
377
+ const locksStillExist = await cache.hasLock(key);
378
+ expect(locksStillExist).to.be.true;
379
+
380
+ expect(timeAfterFirstLock - timeBeforeFirstLock).to.be.greaterThanOrEqual(firstLockTTL);
381
+ });
382
+
383
+ ifLockIt('finds no lock if pattern does not match', async () => {
384
+ await cache.lock(`lock__${Math.random()}`, 10000);
385
+ await cache.lock(`otherlock__${Math.random()}`, 10000);
386
+
387
+ const locksExist = await cache.hasLock('whatever');
388
+ expect(locksExist).to.be.false;
389
+ });
390
+
391
+ ifLockIt('finds locks if a lock matches pattern', async () => {
392
+ const prefix = `prefix_${Math.random()}`;
393
+ await cache.lock(`${prefix}_sublock1`, 10000);
394
+
395
+ const locksExist = await cache.hasLock(prefix);
396
+ expect(locksExist).to.be.true;
397
+ });
398
+
399
+ ifLockIt('finds locks if a lock matches pattern, with already a star at the end', async () => {
400
+ const prefix = `prefix_${Math.random()}`;
401
+ await cache.lock(`${prefix}_sublock1`, 10000);
402
+
403
+ const locksExist = await cache.hasLock(`${prefix}*`);
404
+ expect(locksExist).to.be.true;
405
+ });
406
+
407
+ ifLockIt('finds no lock if a lock matched pattern, but was unlocked', async () => {
408
+ const prefix = `prefix_${Math.random()}`;
409
+ const lock = await cache.lock(`${prefix}_sublock1`, 10000);
410
+ await cache.unlock(lock);
411
+
412
+ const locksExist = await cache.hasLock(prefix);
413
+ expect(locksExist).to.be.false;
414
+ });
415
+
416
+ ifLockIt('finds locks if several locks match pattern', async () => {
417
+ const prefix = `prefix_${Math.random()}`;
418
+ await cache.lock(`${prefix}_sublock1`, 10000);
419
+ await cache.lock(`${prefix}_sublock2`, 10000);
420
+
421
+ const locksExist = await cache.hasLock(prefix);
422
+ expect(locksExist).to.be.true;
423
+ });
424
+
425
+ ifLockIt('returns no locks only when all the matching locks are cleared', async () => {
426
+ const prefix = `prefix_${Math.random()}`;
427
+ const lock1 = await cache.lock(`${prefix}_sublock1`, 10000);
428
+ expect(await cache.hasLock(prefix)).to.be.true;
429
+
430
+ const lock2 = await cache.lock(`${prefix}_sublock2`, 10000);
431
+ expect(await cache.hasLock(prefix)).to.be.true;
432
+
433
+ await cache.unlock(lock1);
434
+ expect(await cache.hasLock(prefix)).to.be.true;
435
+
436
+ await cache.unlock(lock2);
437
+ expect(await cache.hasLock(prefix)).to.be.false;
438
+ });
439
+
440
+ ifLockIt('locks before fetching if value not in cache', async () => {
441
+ const key = `key${Math.random()}`;
442
+ let numCalled = 0;
443
+ const object = {
444
+ fetch: async (v) => {
445
+ numCalled++;
446
+ return v;
447
+ },
448
+ };
449
+
450
+ const fetchFunction = object.fetch.bind(object, 'newvalue');
451
+ const value = await cache.getOrFetchValue(key, 10, fetchFunction, 1); // enable locking
452
+
453
+ expect(value).to.eql('newvalue');
454
+ expect(numCalled).to.eql(1);
455
+ sinon.assert.calledOnce(lockSpy);
456
+ sinon.assert.calledOnce(unlockSpy);
457
+ });
458
+
459
+ ifLockIt('does not fetch if value already in cache after lock', async () => {
460
+ const key = `key${Math.random()}`;
461
+ // steal the lock
462
+ const lock = await cache.lock(`lock__${key}`, 1000);
463
+
464
+ let numCalled = 0;
465
+ const object = {
466
+ fetch: async (v) => {
467
+ numCalled++;
468
+ return v;
469
+ },
470
+ };
471
+
472
+ const fetchFunction = object.fetch.bind(object, 'newvalue');
473
+
474
+ setTimeout(async () => {
475
+ await cache.setValue(key, 'abcd');
476
+ await cache.unlock(lock);
477
+ }, 40);
478
+ const value = await cache.getOrFetchValue(key, 10, fetchFunction, 1);
479
+
480
+ expect(value).to.eql('abcd');
481
+ expect(numCalled).to.eql(0);
482
+ sinon.assert.calledTwice(lockSpy); // includes our own call above
483
+ sinon.assert.calledTwice(unlockSpy);
484
+ });
485
+
486
+ });
487
+
488
+ }
@@ -0,0 +1,128 @@
1
+ import { expect } from 'chai';
2
+
3
+ import { LocalCache } from '../src/lib/LocalCache';
4
+
5
+
6
+ describe('LocalCache', () => {
7
+
8
+ it('can set values', async () => {
9
+ const cache = new LocalCache();
10
+ const wasSet = await cache.setValue('key', 'value');
11
+ expect(wasSet).to.be.true;
12
+ expect(await cache.itemCount()).to.equal(1);
13
+ const value = await cache.getValue('key');
14
+ expect(value).to.equal('value');
15
+ expect(await cache.itemCount()).to.equal(1);
16
+ });
17
+
18
+ it('can set/get numbers', async function (): Promise<void> {
19
+ const cache = new LocalCache();
20
+
21
+ await cache.setValue('numZero', 0);
22
+ expect(await cache.getValue('numZero')).to.equal(0);
23
+
24
+ await cache.setValue('numFloat', 123.456);
25
+ expect(await cache.getValue('numFloat')).to.equal(123.456);
26
+
27
+ await cache.setValue('numNegative', -99);
28
+ expect(await cache.getValue('numNegative')).to.equal(-99);
29
+
30
+ await cache.setValue('numMax', Number.MAX_SAFE_INTEGER);
31
+ expect(await cache.getValue('numMax')).to.equal(Number.MAX_SAFE_INTEGER);
32
+
33
+ await cache.setValue('numInfinity', Infinity);
34
+ expect(await cache.getValue('numInfinity')).to.equal(Infinity);
35
+
36
+ await cache.setValue('numBarf', 0.1 + 0.2); // 0.30000000000000004, IEEE754
37
+ expect(await cache.getValue('numBarf')).to.equal(0.1 + 0.2);
38
+ });
39
+
40
+ it('will not throw if we set a value of undefined', async () => {
41
+ const cache = new LocalCache();
42
+ expect(await cache.itemCount()).to.equal(0);
43
+ const wasSet = await cache.setValue('key', undefined);
44
+ expect(await cache.itemCount()).to.equal(0);
45
+ expect(wasSet).to.be.false;
46
+ const value = await cache.getValue('key');
47
+ expect(await cache.itemCount()).to.equal(0);
48
+ expect(value).not.to.exist;
49
+ });
50
+
51
+ it('can set values with expiry', async () => {
52
+ const cache = new LocalCache();
53
+ await cache.setValue('key', 'value', .2);
54
+ let value = await cache.getValue('key');
55
+ expect(value).to.equal('value');
56
+ await sleep(250);
57
+ value = await cache.getValue('key');
58
+ expect(value).to.equal(undefined);
59
+
60
+ });
61
+
62
+ it('can delete a value', async () => {
63
+ const cache = new LocalCache();
64
+ await cache.setValue('key', 'value');
65
+
66
+ expect(await cache.getValue('key')).to.equal('value');
67
+ await cache.delValue('key');
68
+
69
+ expect(await cache.getValue('key')).not.to.exist;
70
+
71
+ });
72
+
73
+ it('will not grow in size past the maximum size', async () => {
74
+ const origMax = LocalCache['DEFAULT_MAX_ITEMS'];
75
+ LocalCache['DEFAULT_MAX_ITEMS'] = 5;
76
+
77
+ const cache = new LocalCache();
78
+ await cache.setValue('1', '1');
79
+ await cache.setValue('2', '2');
80
+ await cache.setValue('3', '3');
81
+ await cache.setValue('4', '4');
82
+ await cache.setValue('5', '5');
83
+ await cache.setValue('6', '6');
84
+ await cache.setValue('7', '7');
85
+ await cache.setValue('8', '8');
86
+
87
+ const cacheSize = await cache.itemCount();
88
+ expect(cacheSize).to.equal(5);
89
+
90
+ const oldestValue = await cache.getValue('1');
91
+ expect(oldestValue).to.equal(undefined);
92
+
93
+ // restore
94
+ LocalCache['DEFAULT_MAX_ITEMS'] = origMax;
95
+
96
+ })
97
+
98
+ it('can get the remaining TTL of an item', async () => {
99
+ const cache = new LocalCache();
100
+ const wasSet = await cache.setValue('key', 'value', 10 * 60);
101
+ const cacheTtl = await cache.getTtl('key'); // returns ttl in ms
102
+
103
+ expect(wasSet).to.be.true;
104
+ expect(cacheTtl).to.exist;
105
+ expect(cacheTtl).to.be.within(9 * 60 * 1000, 10 * 60 * 1000);
106
+ });
107
+
108
+ it('returns undefined when we call getTtl if the item does not exist in the cache', async () => {
109
+ const cache = new LocalCache();
110
+ const cacheTtl = await cache.getTtl('key');
111
+
112
+ expect(cacheTtl).to.be.undefined;
113
+ });
114
+
115
+ it('returns 0 when we call getTtl if the item does not expire', async () => {
116
+ const cache = new LocalCache();
117
+ const wasSet = await cache.setValue('key', 'value');
118
+ const cacheTtl = await cache.getTtl('key');
119
+
120
+ expect(wasSet).to.be.true;
121
+ expect(cacheTtl).to.equal(0);
122
+ });
123
+
124
+ });
125
+
126
+ function sleep(ms: number): Promise<void> {
127
+ return new Promise((resolve) => setTimeout(resolve, ms));
128
+ }