-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cache): Create
Cacheable
wrapper for values (#31108)
- Loading branch information
Showing
2 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { DateTime } from 'luxon'; | ||
import { Cacheable } from './cacheable'; | ||
|
||
describe('util/cache/package/cacheable', () => { | ||
it('constructs default value', () => { | ||
const res = Cacheable.empty(); | ||
expect(res.ttlMinutes).toBe(15); | ||
}); | ||
|
||
describe('TTL', () => { | ||
it('static method for minutes', () => { | ||
const res = Cacheable.forMinutes(123); | ||
expect(res.ttlMinutes).toBe(123); | ||
}); | ||
|
||
it('method for minutes', () => { | ||
const res = Cacheable.empty(); | ||
expect(res.forMinutes(42).ttlMinutes).toBe(42); | ||
}); | ||
|
||
it('setter for minutes', () => { | ||
const res = Cacheable.empty(); | ||
res.ttlMinutes = 42; | ||
expect(res.ttlMinutes).toBe(42); | ||
}); | ||
|
||
it('static method for hours', () => { | ||
const res = Cacheable.forHours(3); | ||
expect(res.ttlMinutes).toBe(180); | ||
}); | ||
|
||
it('method for hours', () => { | ||
const res = Cacheable.empty(); | ||
expect(res.forHours(3).ttlMinutes).toBe(180); | ||
}); | ||
|
||
it('setter for hours', () => { | ||
const res = Cacheable.empty(); | ||
res.ttlHours = 3; | ||
expect(res.ttlMinutes).toBe(180); | ||
}); | ||
|
||
it('static method for days', () => { | ||
const res = Cacheable.forDays(2); | ||
expect(res.ttlMinutes).toBe(2880); | ||
}); | ||
|
||
it('method for days', () => { | ||
const res = Cacheable.empty(); | ||
expect(res.forDays(2).ttlMinutes).toBe(2880); | ||
}); | ||
|
||
it('setter for days', () => { | ||
const res = Cacheable.empty(); | ||
res.ttlDays = 2; | ||
expect(res.ttlMinutes).toBe(2880); | ||
}); | ||
}); | ||
|
||
describe('public data', () => { | ||
it('via static method', () => { | ||
const res: Cacheable<number> = Cacheable.fromPublic(42); | ||
expect(res.value).toBe(42); | ||
expect(res.isPublic).toBeTrue(); | ||
expect(res.isPrivate).toBeFalse(); | ||
}); | ||
|
||
it('via method', () => { | ||
const res: Cacheable<number> = Cacheable.empty().asPublic(42); | ||
expect(res.value).toBe(42); | ||
expect(res.isPublic).toBeTrue(); | ||
expect(res.isPrivate).toBeFalse(); | ||
}); | ||
}); | ||
|
||
describe('private data', () => { | ||
it('via static method', () => { | ||
const res: Cacheable<number> = Cacheable.fromPrivate(42); | ||
expect(res.value).toBe(42); | ||
expect(res.isPublic).toBeFalse(); | ||
expect(res.isPrivate).toBeTrue(); | ||
}); | ||
|
||
it('via method', () => { | ||
const res: Cacheable<number> = Cacheable.empty().asPrivate(42); | ||
expect(res.value).toBe(42); | ||
expect(res.isPublic).toBeFalse(); | ||
expect(res.isPrivate).toBeTrue(); | ||
}); | ||
}); | ||
|
||
describe('timestamping', () => { | ||
function dateOf<T>(cacheableResult: Cacheable<T>): Date { | ||
return DateTime.fromISO(cacheableResult.cachedAt).toJSDate(); | ||
} | ||
|
||
it('handles dates automatically', () => { | ||
const t1 = new Date(); | ||
|
||
const empty = Cacheable.empty(); | ||
|
||
const t2 = new Date(); | ||
|
||
const a = Cacheable.fromPrivate(42); | ||
const b = Cacheable.fromPublic(42); | ||
const c = empty.asPrivate(42); | ||
const d = empty.asPublic(42); | ||
|
||
const t3 = new Date(); | ||
|
||
expect(dateOf(empty)).toBeAfterOrEqualTo(t1); | ||
expect(dateOf(empty)).toBeBeforeOrEqualTo(t2); | ||
|
||
expect(dateOf(a)).toBeAfterOrEqualTo(t2); | ||
expect(dateOf(a)).toBeBeforeOrEqualTo(t3); | ||
|
||
expect(dateOf(b)).toBeAfterOrEqualTo(t2); | ||
expect(dateOf(b)).toBeBeforeOrEqualTo(t3); | ||
|
||
expect(dateOf(c)).toBeAfterOrEqualTo(t2); | ||
expect(dateOf(c)).toBeBeforeOrEqualTo(t3); | ||
|
||
expect(dateOf(d)).toBeAfterOrEqualTo(t2); | ||
expect(dateOf(d)).toBeBeforeOrEqualTo(t3); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
import { DateTime } from 'luxon'; | ||
|
||
/** | ||
* Type that could be reliably cached, including `null` values. | ||
*/ | ||
type NotUndefined<T> = T extends undefined ? never : T; | ||
|
||
/** | ||
* Type used for partially initialized `Cacheable` values. | ||
*/ | ||
type MaybeUndefined<T> = NotUndefined<T> | undefined; | ||
|
||
/** | ||
* Default cache TTL. | ||
*/ | ||
const defaultTtlMinutes = 15; | ||
|
||
export class Cacheable<T> { | ||
private constructor( | ||
private _ttlMinutes: number, | ||
private _cachedAt: DateTime<true>, | ||
private _isPrivate: boolean, | ||
private _value: T, | ||
) {} | ||
|
||
/** | ||
* Constructs an empty instance for further modification. | ||
*/ | ||
public static empty<T>(): Cacheable<MaybeUndefined<T>> { | ||
return new Cacheable<MaybeUndefined<T>>( | ||
defaultTtlMinutes, | ||
DateTime.now(), | ||
true, | ||
undefined, | ||
); | ||
} | ||
|
||
/** | ||
* Returns the TTL in minutes. | ||
*/ | ||
get ttlMinutes(): number { | ||
return this._ttlMinutes; | ||
} | ||
|
||
/** | ||
* Set the TTL in minutes. | ||
*/ | ||
set ttlMinutes(minutes: number) { | ||
this._ttlMinutes = minutes; | ||
} | ||
|
||
/** | ||
* Set the TTL in hours. | ||
*/ | ||
set ttlHours(hours: number) { | ||
this._ttlMinutes = 60 * hours; | ||
} | ||
|
||
/** | ||
* Set the TTL in days. | ||
*/ | ||
set ttlDays(hours: number) { | ||
this._ttlMinutes = 24 * 60 * hours; | ||
} | ||
|
||
/** | ||
* Sets the cache TTL in minutes and returns the same object. | ||
*/ | ||
public forMinutes(minutes: number): Cacheable<T> { | ||
this.ttlMinutes = minutes; | ||
return this; | ||
} | ||
|
||
/** | ||
* Construct the empty `Cacheable` instance with pre-configured minutes of TTL. | ||
*/ | ||
public static forMinutes<T>(minutes: number): Cacheable<MaybeUndefined<T>> { | ||
return Cacheable.empty<MaybeUndefined<T>>().forMinutes(minutes); | ||
} | ||
|
||
/** | ||
* Sets the cache TTL in hours and returns the same object. | ||
*/ | ||
public forHours(hours: number): Cacheable<T> { | ||
return this.forMinutes(60 * hours); | ||
} | ||
|
||
/** | ||
* Construct the empty `Cacheable` instance with pre-configured hours of TTL. | ||
*/ | ||
public static forHours<T>(hours: number): Cacheable<MaybeUndefined<T>> { | ||
return Cacheable.empty<MaybeUndefined<T>>().forHours(hours); | ||
} | ||
|
||
/** | ||
* Sets the cache TTL in days and returns the same object. | ||
*/ | ||
public forDays(days: number): Cacheable<T> { | ||
return this.forHours(24 * days); | ||
} | ||
|
||
/** | ||
* Construct the empty `Cacheable` instance with pre-configured hours of TTL. | ||
*/ | ||
public static forDays<T>(days: number): Cacheable<MaybeUndefined<T>> { | ||
return Cacheable.empty<MaybeUndefined<T>>().forDays(days); | ||
} | ||
|
||
/** | ||
* Construct `Cacheable` instance that SHOULD be persisted and available publicly. | ||
* | ||
* @param value Data to cache | ||
* @returns New `Cacheable` instance with the `value` guaranteed to be defined. | ||
*/ | ||
public static fromPublic<T>( | ||
value: NotUndefined<T>, | ||
): Cacheable<NotUndefined<T>> { | ||
return new Cacheable<NotUndefined<T>>( | ||
defaultTtlMinutes, | ||
DateTime.now(), | ||
false, | ||
value, | ||
); | ||
} | ||
|
||
/** | ||
* Mark the partially initialized `Cacheable` instance as public, | ||
* for data that SHOULD be persisted and available publicly. | ||
* | ||
* @param value Data to cache | ||
* @returns New `Cacheable` instance with `value` guaranteed to be defined. | ||
*/ | ||
public asPublic<T>(value: NotUndefined<T>): Cacheable<NotUndefined<T>> { | ||
return new Cacheable<NotUndefined<T>>( | ||
this._ttlMinutes, | ||
this._cachedAt, | ||
false, | ||
value, | ||
); | ||
} | ||
|
||
/** | ||
* Construct `Cacheable` instance that MUST NOT be available publicly, | ||
* but still COULD be persisted in self-hosted setups. | ||
* | ||
* @param value Data to cache | ||
* @returns New `Cacheable` instance with `value` guaranteed to be defined. | ||
*/ | ||
public static fromPrivate<T>( | ||
value: NotUndefined<T>, | ||
): Cacheable<NotUndefined<T>> { | ||
return new Cacheable<NotUndefined<T>>( | ||
defaultTtlMinutes, | ||
DateTime.now(), | ||
true, | ||
value, | ||
); | ||
} | ||
|
||
/** | ||
* Mark the partially initialized `Cacheable` instance as private, | ||
* for data that MUST NOT be available publicly, | ||
* but still COULD be persisted in self-hosted setups. | ||
* | ||
* @param value Data to cache | ||
* @returns New `Cacheable` instance with `value` guaranteed to be defined. | ||
*/ | ||
public asPrivate<T>(value: NotUndefined<T>): Cacheable<NotUndefined<T>> { | ||
return new Cacheable<NotUndefined<T>>( | ||
this._ttlMinutes, | ||
this._cachedAt, | ||
true, | ||
value, | ||
); | ||
} | ||
|
||
/** | ||
* Check whether the instance is private. | ||
*/ | ||
get isPrivate(): boolean { | ||
return this._isPrivate; | ||
} | ||
|
||
/** | ||
* Check whether the instance is public. | ||
*/ | ||
get isPublic(): boolean { | ||
return !this._isPrivate; | ||
} | ||
|
||
/** | ||
* Cached value | ||
*/ | ||
get value(): T { | ||
return this._value; | ||
} | ||
|
||
/** | ||
* The creation date of the cached value, | ||
* which is set during `fromPrivate`, `asPrivate`, | ||
* `fromPublic`, or `asPublic` calls. | ||
*/ | ||
get cachedAt(): string { | ||
return this._cachedAt.toUTC().toISO(); | ||
} | ||
} |