Skip to content

Commit

Permalink
feat(cache): Create Cacheable wrapper for values (#31108)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Aug 31, 2024
1 parent de01497 commit fad5e98
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 0 deletions.
127 changes: 127 additions & 0 deletions lib/util/cache/package/cacheable.spec.ts
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);
});
});
});
206 changes: 206 additions & 0 deletions lib/util/cache/package/cacheable.ts
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();
}
}

0 comments on commit fad5e98

Please sign in to comment.