Skip to content

Commit

Permalink
Merge pull request #147 from rackspace/issue/146-expiring-value-failure
Browse files Browse the repository at this point in the history
Issue #146 ExpiringValue caches failures
  • Loading branch information
kernwig authored Sep 19, 2024
2 parents 9bcc485 + 80040f8 commit b39cc70
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 53 deletions.
10 changes: 9 additions & 1 deletion docs/types/expiring-value.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export declare class ExpiringValue<T> {
private factoryFn;
private ttl;
private options;
/** Cached value */
private value;
/** Epoch millisecond time of when the current value expires */
Expand All @@ -14,8 +15,13 @@ export declare class ExpiringValue<T> {
*
* @param factoryFn factory to lazy-load the value
* @param ttl milliseconds the value is good for, after which it is reloaded.
* @param options optional options to change behavior
* @param options.cacheError set to true to cache for TTL a Promise rejection from factoryFn.
* By default, a rejection is not cached and factoryFn will be retried upon the next call.
*/
constructor(factoryFn: (() => Promise<T>), ttl: number);
constructor(factoryFn: (() => Promise<T>), ttl: number, options?: {
cacheError: boolean;
});
/**
* Get value; lazy-load from factory if not yet loaded or if expired.
*/
Expand All @@ -29,4 +35,6 @@ export declare class ExpiringValue<T> {
* Is the value expired (or not set)
*/
isExpired(): boolean;
/** Reset the value expiration to TTL past now */
private extendExpiration;
}
45 changes: 41 additions & 4 deletions expiring-value/lib/expiring-value.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {ExpiringValue} from "./expiring-value";
import * as MockDate from "mockdate";
import {ExpiringValue} from "./expiring-value";


describe('ExpiringValue',() => {
describe('ExpiringValue', () => {
const baseDate = Date.now();

beforeEach(() => {
Expand All @@ -18,7 +18,7 @@ describe('ExpiringValue',() => {

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

// First GET - lazy created
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('ExpiringValue',() => {

// Clear content..
sut.clear();
expect(sut['value']).toBeFalsy();
expect(sut['value']).toBeUndefined();
expect(sut['expiration']).toEqual(0);

// Fourth GET - factory called again
Expand All @@ -64,4 +64,41 @@ describe('ExpiringValue',() => {
await expect(sut['value']).resolves.toBe('world!');
expect(sut['expiration']).toEqual(baseDate + 1000);
});

test("doesn't cache failure", async () => {
let factoryResponse: Promise<string> = Promise.reject(new Error());

// Initialize
let sut = new ExpiringValue<string>(() => factoryResponse, 1000);
expect(sut['value']).toBeUndefined();
expect(sut['expiration']).toEqual(0);

// First GET - rejects - still expired
await expect(sut.get()).rejects.toThrow();
expect(sut['expiration']).toEqual(0);

// Second GET - calls again, success this time
factoryResponse = Promise.resolve("yay");
const v2 = await sut.get();
expect(v2).toBe('yay');
await expect(sut['value']).resolves.toBe('yay');
expect(sut['expiration']).toEqual(baseDate + 1000);
});

test("does cache failure when option selected", async () => {
let factoryResponse: Promise<string> = Promise.reject(new Error());

// Initialize
let sut = new ExpiringValue<string>(() => factoryResponse, 1000,{cacheError: true});
expect(sut['value']).toBeUndefined();
expect(sut['expiration']).toEqual(0);

// First GET - rejects - still expired
await expect(sut.get()).rejects.toThrow();
expect(sut['expiration']).toEqual(baseDate + 1000);

// Second GET - calls again, uses cached failure
factoryResponse = Promise.resolve("yay");
await expect(sut.get()).rejects.toThrow();
});
});
24 changes: 21 additions & 3 deletions expiring-value/lib/expiring-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
export class ExpiringValue<T> {
/** Cached value */
private value: Promise<T>|undefined;
private value: Promise<T> | undefined;

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

/**
Expand All @@ -24,7 +31,13 @@ export class ExpiringValue<T> {
get(): Promise<T> {
if (this.isExpired()) {
this.value = this.factoryFn();
this.expiration = Date.now() + this.ttl;

if (this.options.cacheError) {
this.extendExpiration();
} else {
// Update expiration, only upon success
this.value.then(() => this.extendExpiration());
}
}

return this.value!;
Expand All @@ -45,4 +58,9 @@ export class ExpiringValue<T> {
isExpired(): boolean {
return Date.now() > this.expiration;
}

/** Reset the value expiration to TTL past now */
private extendExpiration(): void {
this.expiration = Date.now() + this.ttl;
}
}
135 changes: 94 additions & 41 deletions expiring-value/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b39cc70

Please sign in to comment.