cachette 4.0.12 → 4.0.14
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.js +4 -2
- package/dist/src/lib/CacheInstance.js.map +1 -1
- package/dist/src/lib/LocalCache.js +10 -4
- package/dist/src/lib/LocalCache.js.map +1 -1
- package/dist/src/lib/RedisCache.js +23 -18
- 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 +8 -6
- package/src/lib/CacheClient.ts +42 -55
- package/src/lib/CacheInstance.ts +6 -10
- package/src/lib/LocalCache.ts +14 -10
- package/src/lib/RedisCache.ts +43 -38
- 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 +2 -8
- package/test/RedisCache_test.ts +20 -20
- package/test/WriteThroughCache_test.ts +34 -39
package/src/lib/LocalCache.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { LRUCache } from 'lru-cache';
|
|
|
3
3
|
import { CachableValue, CacheInstance } from './CacheInstance';
|
|
4
4
|
|
|
5
5
|
async function sleep(ms: number): Promise<void> {
|
|
6
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export class LocalCache extends CacheInstance {
|
|
10
|
-
|
|
11
10
|
public static DEFAULT_MAX_ITEMS = 5000;
|
|
12
11
|
// Default maximum age for the items, in MS.
|
|
13
12
|
public static DEFAULT_MAX_AGE: number = 30 * 60 * 1000;
|
|
@@ -126,12 +125,12 @@ export class LocalCache extends CacheInstance {
|
|
|
126
125
|
*/
|
|
127
126
|
public async lock(resource: string, ttlMs: number): Promise<any> {
|
|
128
127
|
let isLocked = true;
|
|
129
|
-
const startTimestamp = Date.now()
|
|
130
|
-
while(isLocked) {
|
|
128
|
+
const startTimestamp = Date.now();
|
|
129
|
+
while (isLocked) {
|
|
131
130
|
if (Date.now() - startTimestamp > LocalCache.LOCK_ACQUIRE_TIMEOUT) {
|
|
132
|
-
throw new Error(`Abandoning locking ${resource} , as timed out while waiting for other lock to be released.`)
|
|
131
|
+
throw new Error(`Abandoning locking ${resource} , as timed out while waiting for other lock to be released.`);
|
|
133
132
|
}
|
|
134
|
-
this.cache.purgeStale()
|
|
133
|
+
this.cache.purgeStale();
|
|
135
134
|
if (!this.cache.has(resource)) {
|
|
136
135
|
isLocked = false;
|
|
137
136
|
} else {
|
|
@@ -141,7 +140,9 @@ export class LocalCache extends CacheInstance {
|
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
142
|
this.cache.set(resource, 1, { ttl: ttlMs });
|
|
144
|
-
return new Promise(resolve => {
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
resolve(resource);
|
|
145
|
+
});
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
/**
|
|
@@ -149,7 +150,9 @@ export class LocalCache extends CacheInstance {
|
|
|
149
150
|
*/
|
|
150
151
|
public async unlock(lock: any): Promise<void> {
|
|
151
152
|
this.cache.delete(lock);
|
|
152
|
-
return new Promise(resolve => {
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
resolve();
|
|
155
|
+
});
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
/**
|
|
@@ -172,7 +175,8 @@ export class LocalCache extends CacheInstance {
|
|
|
172
175
|
found = true;
|
|
173
176
|
}
|
|
174
177
|
});
|
|
175
|
-
return new Promise((resolve) => {
|
|
178
|
+
return new Promise((resolve) => {
|
|
179
|
+
resolve(found);
|
|
180
|
+
});
|
|
176
181
|
}
|
|
177
|
-
|
|
178
182
|
}
|
package/src/lib/RedisCache.ts
CHANGED
|
@@ -18,7 +18,6 @@ export interface Lock {
|
|
|
18
18
|
* or the process might crash unexpectedly.
|
|
19
19
|
*/
|
|
20
20
|
export class RedisCache extends CacheInstance {
|
|
21
|
-
|
|
22
21
|
/**
|
|
23
22
|
* We cannot store null and booleans in Redis, so we store
|
|
24
23
|
* random values representing these values instead.
|
|
@@ -66,13 +65,15 @@ export class RedisCache extends CacheInstance {
|
|
|
66
65
|
// This prevents get/setValue calls from hanging if there is no active connection
|
|
67
66
|
enableOfflineQueue: false,
|
|
68
67
|
});
|
|
69
|
-
this.redlock = new Redlock([this.redisClient as unknown as Redlock.CompatibleRedisClient], {
|
|
68
|
+
this.redlock = new Redlock([this.redisClient as unknown as Redlock.CompatibleRedisClient], {
|
|
69
|
+
// Hack until Redlock 5.x is out of beta
|
|
70
70
|
driftFactor: RedisCache.REDLOCK_CLOCK_DRIFT_FACTOR,
|
|
71
71
|
retryCount: RedisCache.REDLOCK_RETRY_COUNT,
|
|
72
72
|
retryDelay: RedisCache.REDLOCK_RETRY_DELAY_MS,
|
|
73
73
|
retryJitter: RedisCache.REDLOCK_JITTER_MS,
|
|
74
74
|
});
|
|
75
|
-
this.redlockWithoutRetry = new Redlock([this.redisClient as unknown as Redlock.CompatibleRedisClient], {
|
|
75
|
+
this.redlockWithoutRetry = new Redlock([this.redisClient as unknown as Redlock.CompatibleRedisClient], {
|
|
76
|
+
// Hack until Redlock 5.x is out of beta
|
|
76
77
|
driftFactor: RedisCache.REDLOCK_CLOCK_DRIFT_FACTOR,
|
|
77
78
|
retryCount: 0,
|
|
78
79
|
retryDelay: 0,
|
|
@@ -95,7 +96,7 @@ export class RedisCache extends CacheInstance {
|
|
|
95
96
|
if (this.ready) {
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
|
-
return new Promise<void>(resolve => this.redisClient.on('ready', resolve));
|
|
99
|
+
return new Promise<void>((resolve) => this.redisClient.on('ready', resolve));
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/**
|
|
@@ -147,7 +148,6 @@ export class RedisCache extends CacheInstance {
|
|
|
147
148
|
*
|
|
148
149
|
*/
|
|
149
150
|
public static serializeValue(value: CachableValue): string {
|
|
150
|
-
|
|
151
151
|
if (value === null) {
|
|
152
152
|
return RedisCache.NULL_VALUE;
|
|
153
153
|
}
|
|
@@ -161,10 +161,13 @@ export class RedisCache extends CacheInstance {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
if (value instanceof Error) {
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
return (
|
|
165
|
+
RedisCache.ERROR_PREFIX +
|
|
166
|
+
JSON.stringify({
|
|
167
|
+
...value, // serialize potential Error metadata set as object properties
|
|
168
|
+
message: value.message,
|
|
169
|
+
})
|
|
170
|
+
);
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
if (typeof value === 'number') {
|
|
@@ -172,15 +175,18 @@ export class RedisCache extends CacheInstance {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
if (value instanceof Object) {
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
return (
|
|
179
|
+
RedisCache.JSON_PREFIX +
|
|
180
|
+
JSON.stringify(value, (key, value) => {
|
|
181
|
+
if (value instanceof Set) {
|
|
182
|
+
return { __dataType: 'Set', value: Array.from(value) };
|
|
183
|
+
} else if (value instanceof Map) {
|
|
184
|
+
return { __dataType: 'Map', value: Array.from(value) };
|
|
185
|
+
} else {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
);
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
return value;
|
|
@@ -199,7 +205,6 @@ export class RedisCache extends CacheInstance {
|
|
|
199
205
|
*
|
|
200
206
|
*/
|
|
201
207
|
public static deserializeValue(value: string | null): CachableValue {
|
|
202
|
-
|
|
203
208
|
if (value === null) {
|
|
204
209
|
// null means that the key was not present, which we interpret as undefined.
|
|
205
210
|
return undefined;
|
|
@@ -245,34 +250,28 @@ export class RedisCache extends CacheInstance {
|
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
return value;
|
|
248
|
-
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
/**
|
|
252
256
|
* @inheritdoc
|
|
253
257
|
*/
|
|
254
|
-
public async setValue(
|
|
255
|
-
key: string,
|
|
256
|
-
value: CachableValue,
|
|
257
|
-
ttl = 0,
|
|
258
|
-
): Promise<boolean> {
|
|
258
|
+
public async setValue(key: string, value: CachableValue, ttl = 0): Promise<boolean> {
|
|
259
259
|
try {
|
|
260
260
|
return await this.setValueInternal(key, value, ttl);
|
|
261
261
|
} catch (err) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
// Examples of things that may occur here:
|
|
263
|
+
// - A timeout, if the connection was broken during a value fetch.
|
|
264
|
+
// - A general error, e.g. if Redis is Out Of Memory.
|
|
265
265
|
const error = err as Error;
|
|
266
|
-
this.emit(
|
|
266
|
+
this.emit(
|
|
267
|
+
'warn',
|
|
268
|
+
`Error while setting Redis key ${key} with ttl ${ttl}: ${error.name} - ${error.message}\n${error.stack}`,
|
|
269
|
+
);
|
|
267
270
|
return false;
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
public async setValueInternal(
|
|
272
|
-
key: string,
|
|
273
|
-
value: CachableValue,
|
|
274
|
-
ttl: number,
|
|
275
|
-
): Promise<boolean> {
|
|
274
|
+
public async setValueInternal(key: string, value: CachableValue, ttl: number): Promise<boolean> {
|
|
276
275
|
this.emit('set', key, value);
|
|
277
276
|
|
|
278
277
|
if (value === undefined) {
|
|
@@ -387,7 +386,7 @@ export class RedisCache extends CacheInstance {
|
|
|
387
386
|
|
|
388
387
|
return {
|
|
389
388
|
value: internalLock.value,
|
|
390
|
-
unlock: async () => internalLock.unlock()
|
|
389
|
+
unlock: async () => internalLock.unlock(),
|
|
391
390
|
};
|
|
392
391
|
}
|
|
393
392
|
|
|
@@ -420,11 +419,18 @@ export class RedisCache extends CacheInstance {
|
|
|
420
419
|
public async hasLock(prefix: string): Promise<boolean> {
|
|
421
420
|
const redisPrefix = prefix.endsWith('*') ? prefix : `${prefix}*`;
|
|
422
421
|
let cursor = '';
|
|
423
|
-
while (cursor !== '0') {
|
|
422
|
+
while (cursor !== '0') {
|
|
423
|
+
// indicates Redis completed the scan
|
|
424
424
|
// Redis detail: we set the `count` option to a number (1000) greater than
|
|
425
425
|
// the default (10), to minimize the amount of network round-trips caused
|
|
426
426
|
// by incomplete scans needing more scanning from the returned cursor.
|
|
427
|
-
const [nextCursor, matchingKeys] = await this.redisClient.scan(
|
|
427
|
+
const [nextCursor, matchingKeys] = await this.redisClient.scan(
|
|
428
|
+
cursor || '0',
|
|
429
|
+
'MATCH',
|
|
430
|
+
redisPrefix,
|
|
431
|
+
'COUNT',
|
|
432
|
+
1000,
|
|
433
|
+
);
|
|
428
434
|
if (matchingKeys.length > 0) {
|
|
429
435
|
return true;
|
|
430
436
|
}
|
|
@@ -437,5 +443,4 @@ export class RedisCache extends CacheInstance {
|
|
|
437
443
|
public async quit(): Promise<void> {
|
|
438
444
|
await this.redisClient.quit();
|
|
439
445
|
}
|
|
440
|
-
|
|
441
446
|
}
|
|
@@ -2,7 +2,6 @@ import { CachableValue, CacheInstance } from './CacheInstance';
|
|
|
2
2
|
import { RedisCache } from './RedisCache';
|
|
3
3
|
import { LocalCache } from './LocalCache';
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* Write-through cache, using Redis and a local LRU cache with aligned TTLs.
|
|
8
7
|
*
|
|
@@ -31,7 +30,6 @@ import { LocalCache } from './LocalCache';
|
|
|
31
30
|
* "pub/sub traffic explosion" is a non-concern? To be evaluated :)
|
|
32
31
|
*/
|
|
33
32
|
export class WriteThroughCache extends CacheInstance {
|
|
34
|
-
|
|
35
33
|
private redisCacheForWriting: CacheInstance;
|
|
36
34
|
private redisCacheForReading: CacheInstance;
|
|
37
35
|
private localCache: CacheInstance;
|
|
@@ -59,26 +57,33 @@ export class WriteThroughCache extends CacheInstance {
|
|
|
59
57
|
const metricsPeriod = parseInt(process.env.CACHETTE_METRICS_PERIOD_MINUTES, 10);
|
|
60
58
|
if (Number.isInteger(metricsPeriod) && metricsPeriod > 0) {
|
|
61
59
|
this.metrics.enabled = true;
|
|
62
|
-
this.redisCacheForWriting.emit(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
this.redisCacheForWriting.emit(
|
|
61
|
+
'info',
|
|
62
|
+
`WriteThroughCache metrics enabled, will report every ${metricsPeriod} min`,
|
|
63
|
+
);
|
|
64
|
+
setInterval(
|
|
65
|
+
() => {
|
|
66
|
+
const total = this.metrics.localHits + this.metrics.redisHits + this.metrics.doubleMisses;
|
|
67
|
+
this.redisCacheForWriting.emit(
|
|
68
|
+
'info',
|
|
69
|
+
`WriteThroughCache metrics during last ${metricsPeriod} min - Total: ${total}, ` +
|
|
70
|
+
`Local hits: ${this.metrics.localHits} (${total && Math.floor((100 * this.metrics.localHits) / total)}%), ` +
|
|
71
|
+
`Redis hits: ${this.metrics.redisHits} (${total && Math.floor((100 * this.metrics.redisHits) / total)}%), ` +
|
|
72
|
+
`Double misses: ${this.metrics.doubleMisses} (${total && Math.floor((100 * this.metrics.doubleMisses) / total)}%).`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
this.metrics.localHits = 0;
|
|
76
|
+
this.metrics.redisHits = 0;
|
|
77
|
+
this.metrics.doubleMisses = 0;
|
|
78
|
+
},
|
|
79
|
+
metricsPeriod * 60 * 1000,
|
|
80
|
+
);
|
|
77
81
|
} else {
|
|
78
82
|
this.redisCacheForWriting.emit(
|
|
79
83
|
'warn',
|
|
80
84
|
'WriteThroughCache metrics activation impossible, CACHETTE_METRICS_PERIOD_MINUTES is invalid. ' +
|
|
81
|
-
|
|
85
|
+
`Must be a positive integer, but was ${process.env.CACHETTE_METRICS_PERIOD_MINUTES}`,
|
|
86
|
+
);
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
}
|
|
@@ -101,19 +106,15 @@ export class WriteThroughCache extends CacheInstance {
|
|
|
101
106
|
* @inheritdoc
|
|
102
107
|
*/
|
|
103
108
|
public async itemCount(): Promise<number> {
|
|
104
|
-
return await this.redisCacheForReading.itemCount() + await this.localCache.itemCount();
|
|
109
|
+
return (await this.redisCacheForReading.itemCount()) + (await this.localCache.itemCount());
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
/**
|
|
108
113
|
* @inheritdoc
|
|
109
114
|
*/
|
|
110
|
-
public async setValue(
|
|
111
|
-
key: string,
|
|
112
|
-
value: CachableValue,
|
|
113
|
-
ttl = 0,
|
|
114
|
-
): Promise<boolean> {
|
|
115
|
+
public async setValue(key: string, value: CachableValue, ttl = 0): Promise<boolean> {
|
|
115
116
|
const response = await this.localCache.setValue(key, value, ttl);
|
|
116
|
-
return await this.redisCacheForWriting.setValue(key, value, ttl) && response;
|
|
117
|
+
return (await this.redisCacheForWriting.setValue(key, value, ttl)) && response;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
/**
|
|
@@ -166,7 +167,7 @@ export class WriteThroughCache extends CacheInstance {
|
|
|
166
167
|
* @inheritdoc
|
|
167
168
|
*/
|
|
168
169
|
public async waitForReplication(replicas: number, timeout: number): Promise<number> {
|
|
169
|
-
this.emit('wait')
|
|
170
|
+
this.emit('wait');
|
|
170
171
|
await this.localCache.waitForReplication(replicas, timeout);
|
|
171
172
|
return this.redisCacheForWriting.waitForReplication(replicas, timeout);
|
|
172
173
|
}
|
|
@@ -196,5 +197,4 @@ export class WriteThroughCache extends CacheInstance {
|
|
|
196
197
|
public isLockingSupported(): boolean {
|
|
197
198
|
return false;
|
|
198
199
|
}
|
|
199
|
-
|
|
200
200
|
}
|
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
|
}
|