Skip to content

Commit b39cc70

Browse files
authored
Merge pull request #147 from rackspace/issue/146-expiring-value-failure
Issue #146 ExpiringValue caches failures
2 parents 9bcc485 + 80040f8 commit b39cc70

File tree

5 files changed

+169
-53
lines changed

5 files changed

+169
-53
lines changed

docs/types/expiring-value.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
export declare class ExpiringValue<T> {
66
private factoryFn;
77
private ttl;
8+
private options;
89
/** Cached value */
910
private value;
1011
/** Epoch millisecond time of when the current value expires */
@@ -14,8 +15,13 @@ export declare class ExpiringValue<T> {
1415
*
1516
* @param factoryFn factory to lazy-load the value
1617
* @param ttl milliseconds the value is good for, after which it is reloaded.
18+
* @param options optional options to change behavior
19+
* @param options.cacheError set to true to cache for TTL a Promise rejection from factoryFn.
20+
* By default, a rejection is not cached and factoryFn will be retried upon the next call.
1721
*/
18-
constructor(factoryFn: (() => Promise<T>), ttl: number);
22+
constructor(factoryFn: (() => Promise<T>), ttl: number, options?: {
23+
cacheError: boolean;
24+
});
1925
/**
2026
* Get value; lazy-load from factory if not yet loaded or if expired.
2127
*/
@@ -29,4 +35,6 @@ export declare class ExpiringValue<T> {
2935
* Is the value expired (or not set)
3036
*/
3137
isExpired(): boolean;
38+
/** Reset the value expiration to TTL past now */
39+
private extendExpiration;
3240
}

expiring-value/lib/expiring-value.test.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {ExpiringValue} from "./expiring-value";
21
import * as MockDate from "mockdate";
2+
import {ExpiringValue} from "./expiring-value";
33

44

5-
describe('ExpiringValue',() => {
5+
describe('ExpiringValue', () => {
66
const baseDate = Date.now();
77

88
beforeEach(() => {
@@ -18,7 +18,7 @@ describe('ExpiringValue',() => {
1818

1919
// Initialize
2020
let sut = new ExpiringValue<string>(() => Promise.resolve(factoryValue), 1000);
21-
expect(sut['value']).toBeFalsy();
21+
expect(sut['value']).toBeUndefined();
2222
expect(sut['expiration']).toEqual(0);
2323

2424
// First GET - lazy created
@@ -52,7 +52,7 @@ describe('ExpiringValue',() => {
5252

5353
// Clear content..
5454
sut.clear();
55-
expect(sut['value']).toBeFalsy();
55+
expect(sut['value']).toBeUndefined();
5656
expect(sut['expiration']).toEqual(0);
5757

5858
// Fourth GET - factory called again
@@ -64,4 +64,41 @@ describe('ExpiringValue',() => {
6464
await expect(sut['value']).resolves.toBe('world!');
6565
expect(sut['expiration']).toEqual(baseDate + 1000);
6666
});
67+
68+
test("doesn't cache failure", async () => {
69+
let factoryResponse: Promise<string> = Promise.reject(new Error());
70+
71+
// Initialize
72+
let sut = new ExpiringValue<string>(() => factoryResponse, 1000);
73+
expect(sut['value']).toBeUndefined();
74+
expect(sut['expiration']).toEqual(0);
75+
76+
// First GET - rejects - still expired
77+
await expect(sut.get()).rejects.toThrow();
78+
expect(sut['expiration']).toEqual(0);
79+
80+
// Second GET - calls again, success this time
81+
factoryResponse = Promise.resolve("yay");
82+
const v2 = await sut.get();
83+
expect(v2).toBe('yay');
84+
await expect(sut['value']).resolves.toBe('yay');
85+
expect(sut['expiration']).toEqual(baseDate + 1000);
86+
});
87+
88+
test("does cache failure when option selected", async () => {
89+
let factoryResponse: Promise<string> = Promise.reject(new Error());
90+
91+
// Initialize
92+
let sut = new ExpiringValue<string>(() => factoryResponse, 1000,{cacheError: true});
93+
expect(sut['value']).toBeUndefined();
94+
expect(sut['expiration']).toEqual(0);
95+
96+
// First GET - rejects - still expired
97+
await expect(sut.get()).rejects.toThrow();
98+
expect(sut['expiration']).toEqual(baseDate + 1000);
99+
100+
// Second GET - calls again, uses cached failure
101+
factoryResponse = Promise.resolve("yay");
102+
await expect(sut.get()).rejects.toThrow();
103+
});
67104
});

expiring-value/lib/expiring-value.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
export class ExpiringValue<T> {
66
/** Cached value */
7-
private value: Promise<T>|undefined;
7+
private value: Promise<T> | undefined;
88

99
/** Epoch millisecond time of when the current value expires */
1010
private expiration: number = 0;
@@ -14,8 +14,15 @@ export class ExpiringValue<T> {
1414
*
1515
* @param factoryFn factory to lazy-load the value
1616
* @param ttl milliseconds the value is good for, after which it is reloaded.
17+
* @param options optional options to change behavior
18+
* @param options.cacheError set to true to cache for TTL a Promise rejection from factoryFn.
19+
* By default, a rejection is not cached and factoryFn will be retried upon the next call.
1720
*/
18-
constructor(private factoryFn: (() => Promise<T>), private ttl: number) {
21+
constructor(
22+
private factoryFn: (() => Promise<T>),
23+
private ttl: number,
24+
private options = {cacheError: false}
25+
) {
1926
}
2027

2128
/**
@@ -24,7 +31,13 @@ export class ExpiringValue<T> {
2431
get(): Promise<T> {
2532
if (this.isExpired()) {
2633
this.value = this.factoryFn();
27-
this.expiration = Date.now() + this.ttl;
34+
35+
if (this.options.cacheError) {
36+
this.extendExpiration();
37+
} else {
38+
// Update expiration, only upon success
39+
this.value.then(() => this.extendExpiration());
40+
}
2841
}
2942

3043
return this.value!;
@@ -45,4 +58,9 @@ export class ExpiringValue<T> {
4558
isExpired(): boolean {
4659
return Date.now() > this.expiration;
4760
}
61+
62+
/** Reset the value expiration to TTL past now */
63+
private extendExpiration(): void {
64+
this.expiration = Date.now() + this.ttl;
65+
}
4866
}

expiring-value/package-lock.json

Lines changed: 94 additions & 41 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)