cachette 4.0.15 → 4.0.16

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.
@@ -108,6 +108,7 @@ export declare abstract class CacheInstance extends EventEmitter {
108
108
  * simultaneous requests to the same resource in parallel.
109
109
  */
110
110
  private activeFetches;
111
+ private getValueWithErrorHandling;
111
112
  /**
112
113
  * Get or fetch a value
113
114
  *
@@ -51,6 +51,18 @@ class CacheInstance extends node_events_1.EventEmitter {
51
51
  * simultaneous requests to the same resource in parallel.
52
52
  */
53
53
  activeFetches = {};
54
+ async getValueWithErrorHandling(key, shouldCacheError) {
55
+ const cached = await this.getValue(key);
56
+ if (cached instanceof Error) {
57
+ if (shouldCacheError && shouldCacheError(cached)) {
58
+ throw cached;
59
+ }
60
+ else {
61
+ return undefined;
62
+ }
63
+ }
64
+ return cached;
65
+ }
54
66
  /**
55
67
  * Get or fetch a value
56
68
  *
@@ -66,15 +78,7 @@ class CacheInstance extends node_events_1.EventEmitter {
66
78
  */
67
79
  async getOrFetchValue(key, ttl, fetchFunction, lockTtl, shouldCacheError) {
68
80
  // already cached?
69
- let cached = await this.getValue(key);
70
- if (cached instanceof Error) {
71
- if (shouldCacheError) {
72
- throw cached;
73
- }
74
- else {
75
- cached = undefined;
76
- }
77
- }
81
+ let cached = await this.getValueWithErrorHandling(key, shouldCacheError);
78
82
  if (cached !== undefined) {
79
83
  return cached;
80
84
  }
@@ -83,54 +87,61 @@ class CacheInstance extends node_events_1.EventEmitter {
83
87
  if (currentFetch) {
84
88
  return currentFetch;
85
89
  }
86
- // I'm the one fetching.
87
- let lock;
88
- try {
89
- // get the lock if needed
90
- const lockName = `lock__${key}`;
91
- if (lockTtl && this.isLockingSupported()) {
92
- lock = await this.lock(lockName, lockTtl * 1000);
93
- // check if the value has been populated while we were locking
94
- let cachedValue = await this.getValue(key);
95
- if (cachedValue instanceof Error) {
96
- if (shouldCacheError) {
97
- throw cachedValue;
90
+ // I'm the one fetching - immediately register the fetch promise to prevent race conditions
91
+ this.activeFetches[key] = (async () => {
92
+ let lock;
93
+ let lockErrorMessage;
94
+ try {
95
+ // get the lock if needed
96
+ if (lockTtl && this.isLockingSupported()) {
97
+ const lockName = `lock__${key}`;
98
+ try {
99
+ lock = await this.lock(lockName, lockTtl * 1000);
98
100
  }
99
- else {
100
- cachedValue = undefined;
101
+ catch (lockError) {
102
+ lockErrorMessage = lockError instanceof Error ? lockError.message : String(lockError);
101
103
  }
102
104
  }
105
+ // check gain if the value has been populated while we previously checked
106
+ const cachedValue = await this.getValueWithErrorHandling(key, shouldCacheError);
103
107
  if (cachedValue !== undefined) {
104
108
  return cachedValue;
105
109
  }
110
+ if (lockErrorMessage) {
111
+ throw new Error(`Failed to acquire lock for key ${key}. No active fetch or cached value found: ${lockErrorMessage}`);
112
+ }
113
+ // fetch!
114
+ let error;
115
+ let result;
116
+ try {
117
+ result = await fetchFunction();
118
+ }
119
+ catch (err) {
120
+ error = err;
121
+ }
122
+ // cache! results: always, errors: only if satisfying user assertion
123
+ if (error && shouldCacheError && shouldCacheError(error)) {
124
+ await this.setValue(key, error, ttl);
125
+ }
126
+ else if (result !== undefined) {
127
+ await this.setValue(key, result, ttl);
128
+ }
129
+ if (error) {
130
+ throw error;
131
+ }
132
+ return result;
106
133
  }
107
- // fetch!
108
- let error;
109
- let result;
110
- try {
111
- const fetchPromise = (this.activeFetches[key] = fetchFunction());
112
- result = await fetchPromise;
113
- }
114
- catch (err) {
115
- error = err;
116
- }
117
- // cache! results: always, errors: only if satisfying user assertion
118
- if (error && shouldCacheError && shouldCacheError(error)) {
119
- await this.setValue(key, error, ttl);
120
- }
121
- else if (result !== undefined) {
122
- await this.setValue(key, result, ttl);
123
- }
124
- if (error) {
125
- throw error;
134
+ finally {
135
+ if (lock) {
136
+ await this.unlock(lock);
137
+ }
126
138
  }
127
- return result;
139
+ })();
140
+ try {
141
+ return await this.activeFetches[key];
128
142
  }
129
143
  finally {
130
144
  delete this.activeFetches[key];
131
- if (lock) {
132
- await this.unlock(lock);
133
- }
134
145
  }
135
146
  }
136
147
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CacheInstance.js","sourceRoot":"","sources":["../../../src/lib/CacheInstance.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CacheInstance.js","sourceRoot":"","sources":["../../../src/lib/CacheInstance.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cachette",
3
- "version": "4.0.15",
3
+ "version": "4.0.16",
4
4
  "engines": {
5
5
  "node": ">=20",
6
6
  "npm": ">=10"
@@ -139,6 +139,19 @@ export abstract class CacheInstance extends EventEmitter {
139
139
  */
140
140
  private activeFetches: { [key: string]: Promise<CachableValue> } = {};
141
141
 
142
+ private async getValueWithErrorHandling(key: string, shouldCacheError?: (err: Error) => boolean): Promise<CachableValue> {
143
+ const cached = await this.getValue(key);
144
+ if (cached instanceof Error) {
145
+ if (shouldCacheError && shouldCacheError(cached)) {
146
+ throw cached;
147
+ } else {
148
+ return undefined;
149
+ }
150
+ }
151
+
152
+ return cached;
153
+ }
154
+
142
155
  /**
143
156
  * Get or fetch a value
144
157
  *
@@ -160,14 +173,7 @@ export abstract class CacheInstance extends EventEmitter {
160
173
  shouldCacheError?: (err: Error) => boolean,
161
174
  ): Promise<ReturnType<F>> {
162
175
  // already cached?
163
- let cached = await this.getValue(key);
164
- if (cached instanceof Error) {
165
- if (shouldCacheError) {
166
- throw cached;
167
- } else {
168
- cached = undefined;
169
- }
170
- }
176
+ let cached = await this.getValueWithErrorHandling(key, shouldCacheError);
171
177
  if (cached !== undefined) {
172
178
  return cached;
173
179
  }
@@ -178,53 +184,63 @@ export abstract class CacheInstance extends EventEmitter {
178
184
  return currentFetch;
179
185
  }
180
186
 
181
- // I'm the one fetching.
182
- let lock: Lock | undefined;
183
- try {
184
- // get the lock if needed
185
- const lockName = `lock__${key}`;
186
- if (lockTtl && this.isLockingSupported()) {
187
- lock = await this.lock(lockName, lockTtl * 1000);
188
- // check if the value has been populated while we were locking
189
- let cachedValue = await this.getValue(key);
190
- if (cachedValue instanceof Error) {
191
- if (shouldCacheError) {
192
- throw cachedValue;
193
- } else {
194
- cachedValue = undefined;
187
+ // I'm the one fetching - immediately register the fetch promise to prevent race conditions
188
+ this.activeFetches[key] = (async () => {
189
+ let lock: Lock | undefined;
190
+ let lockErrorMessage: string | undefined;
191
+
192
+ try {
193
+ // get the lock if needed
194
+ if (lockTtl && this.isLockingSupported()) {
195
+ const lockName = `lock__${key}`;
196
+ try {
197
+ lock = await this.lock(lockName, lockTtl * 1000);
198
+ } catch (lockError) {
199
+ lockErrorMessage = lockError instanceof Error ? lockError.message : String(lockError);
195
200
  }
196
201
  }
202
+
203
+ // check gain if the value has been populated while we previously checked
204
+ const cachedValue = await this.getValueWithErrorHandling(key, shouldCacheError);
197
205
  if (cachedValue !== undefined) {
198
206
  return cachedValue;
199
207
  }
200
- }
201
208
 
202
- // fetch!
203
- let error: Error | undefined;
204
- let result: any;
205
- try {
206
- const fetchPromise = (this.activeFetches[key] = fetchFunction());
207
- result = await fetchPromise;
208
- } catch (err) {
209
- error = err;
210
- }
209
+ if (lockErrorMessage) {
210
+ throw new Error(`Failed to acquire lock for key ${key}. No active fetch or cached value found: ${lockErrorMessage}`);
211
+ }
211
212
 
212
- // cache! results: always, errors: only if satisfying user assertion
213
- if (error && shouldCacheError && shouldCacheError(error)) {
214
- await this.setValue(key, error, ttl);
215
- } else if (result !== undefined) {
216
- await this.setValue(key, result, ttl);
217
- }
213
+ // fetch!
214
+ let error: Error | undefined;
215
+ let result: any;
216
+ try {
217
+ result = await fetchFunction();
218
+ } catch (err) {
219
+ error = err;
220
+ }
218
221
 
219
- if (error) {
220
- throw error;
222
+ // cache! results: always, errors: only if satisfying user assertion
223
+ if (error && shouldCacheError && shouldCacheError(error)) {
224
+ await this.setValue(key, error, ttl);
225
+ } else if (result !== undefined) {
226
+ await this.setValue(key, result, ttl);
227
+ }
228
+
229
+ if (error) {
230
+ throw error;
231
+ }
232
+ return result;
233
+ } finally {
234
+ if (lock) {
235
+ await this.unlock(lock);
236
+ }
221
237
  }
222
- return result;
238
+ })();
239
+
240
+ try {
241
+ return await this.activeFetches[key];
223
242
  } finally {
224
243
  delete this.activeFetches[key];
225
- if (lock) {
226
- await this.unlock(lock);
227
- }
228
244
  }
229
245
  }
230
246
  }
@@ -132,6 +132,50 @@ describe('CacheClient', () => {
132
132
  expect(myObj.numCalled).to.equal(2); // from cache -> NO increase
133
133
  });
134
134
 
135
+ it('correctly evaluates shouldCacheError callback when retrieving cached errors', async () => {
136
+ const cache = new LocalCache();
137
+ const key = 'test-key';
138
+
139
+ // Manually cache an error with a specific property
140
+ const cachedError = new Error('cached error');
141
+ cachedError['errorType'] = 'temporary';
142
+ await cache.setValue(key, cachedError, 60);
143
+
144
+ // Try to retrieve with a shouldCacheError that should reject this error
145
+ const result = await cache.getOrFetchValue(
146
+ key,
147
+ 60,
148
+ async () => 'fresh value',
149
+ undefined,
150
+ (err) => err['errorType'] === 'permanent', // Only cache permanent errors
151
+ );
152
+
153
+ // Should return 'fresh value' because the cached error doesn't match the callback criteria
154
+ expect(result).to.equal('fresh value');
155
+
156
+ // Now try with a callback that accepts the cached error
157
+ const cachedError2 = new Error('cached error 2');
158
+ cachedError2['errorType'] = 'permanent';
159
+ await cache.setValue(key, cachedError2, 60);
160
+
161
+ let didThrow = false;
162
+ try {
163
+ await cache.getOrFetchValue(
164
+ key,
165
+ 60,
166
+ async () => 'fresh value',
167
+ undefined,
168
+ (err) => err['errorType'] === 'permanent', // Only cache permanent errors
169
+ );
170
+ } catch (err) {
171
+ didThrow = true;
172
+ expect(err.message).to.equal('cached error 2');
173
+ }
174
+
175
+ // Should throw because the cached error matches the callback criteria
176
+ expect(didThrow).to.be.true;
177
+ });
178
+
135
179
  it('protect against concurrent fetches', async () => {
136
180
  const myObj = new MyClass();
137
181
  const jobs: Promise<any>[] = [];
@@ -456,5 +456,63 @@ function runTests(name: string, cache: CacheInstance): void {
456
456
  sinon.assert.calledTwice(lockSpy); // includes our own call above
457
457
  sinon.assert.calledTwice(unlockSpy);
458
458
  });
459
+
460
+ ifLockIt('returns cached value if lock acquisition fails and value was populated meanwhile', async () => {
461
+ const key = `key${Math.random()}`;
462
+ let numCalled = 0;
463
+ const object = {
464
+ fetch: async (v) => {
465
+ numCalled++;
466
+ return v;
467
+ },
468
+ };
469
+
470
+ const fetchFunction = object.fetch.bind(object, 'newvalue');
471
+
472
+ // Restore the spy temporarily so we can stub it
473
+ lockSpy.restore();
474
+
475
+ const lockStub = sinon.stub(cache, 'lock').callsFake(async () => {
476
+ // Before failing, simulate the cache being populated
477
+ await cache.setValue(key, 'from-cache');
478
+ throw new Error('Lock acquisition failed');
479
+ });
480
+
481
+ const value = await cache.getOrFetchValue(key, 10, fetchFunction, 1);
482
+
483
+ expect(value).to.eql('from-cache');
484
+ expect(numCalled).to.eql(0); // never called fetch, got from cache
485
+
486
+ lockStub.restore();
487
+ lockSpy = sinon.spy(cache, 'lock');
488
+ });
489
+
490
+ ifLockIt('throws detailed error if lock fails with no activeFetch or cached value', async () => {
491
+ const key = `key${Math.random()}`;
492
+ const object = {
493
+ fetch: async (v) => v,
494
+ };
495
+
496
+ const fetchFunction = object.fetch.bind(object, 'newvalue');
497
+
498
+ // Restore the spy temporarily so we can stub it
499
+ lockSpy.restore();
500
+
501
+ const lockStub = sinon.stub(cache, 'lock').rejects(new Error('Lock acquisition failed'));
502
+
503
+ let thrownError: Error | undefined;
504
+ try {
505
+ await cache.getOrFetchValue(key, 10, fetchFunction, 1);
506
+ } catch (err) {
507
+ thrownError = err as Error;
508
+ }
509
+
510
+ expect(thrownError).to.exist;
511
+ expect(thrownError!.message).to.include(`Failed to acquire lock for key ${key}. No active fetch or cached value found: Lock acquisition failed`);
512
+
513
+ // Restore and recreate the spy
514
+ lockStub.restore();
515
+ lockSpy = sinon.spy(cache, 'lock');
516
+ });
459
517
  });
460
518
  }