Skip to content

Commit

Permalink
Add support for clocks (#77)
Browse files Browse the repository at this point in the history
Co-authored-by: Renan Oliveira <[email protected]>
  • Loading branch information
Fryuni and renan628 authored Jun 16, 2023
1 parent dc6309b commit 1901030
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 25 deletions.
52 changes: 52 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,56 @@ module.exports = {
parserOptions: {
project: ['./tsconfig.json'],
},
overrides: [
{
files: ['src/**/*.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'CallExpression[arguments.length=0] '
+ "> MemberExpression[object.name='Instant'][property.name='now']",
message: 'Do not use Instant.now without a clock.',
},
{
selector: 'CallExpression[arguments.length=0] '
+ "> MemberExpression[object.name='LocalDateTime'][property.name='now']",
message: 'Do not use LocalDateTime.now without a clock.',
},
{
selector: 'CallExpression[arguments.length=1] '
+ "> MemberExpression[object.name='LocalDateTime'][property.name='nowIn']",
message: 'Do not use LocalDateTime.nowIn without a clock.',
},
{
selector: 'CallExpression[arguments.length=0] '
+ "> MemberExpression[object.name='Date'][property.name='now']",
message: 'Do not use Date.now, use a clock.',
},
{
selector: "NewExpression[callee.name='Date']",
message: 'Do not create a Date instance, use a clock.',
},
],
},
},
{
files: ['test/**/*.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
selector: "CallExpression[callee.object.name='jest'][callee.property.name='spyOn'] "
+ "> Identifier[name='Instant'] ~ Literal[value='now']",
message: 'Do not mock Instant.now, use a FixedClock.',
},
{
selector: "CallExpression[callee.object.name='jest'][callee.property.name='spyOn'] "
+ "> Identifier[name='LocalDateTime'] ~ Literal[value='now']",
message: 'Do not mock LocalDateTime.now, use a FixedClock.',
},
],
},
},
],
};
13 changes: 9 additions & 4 deletions src/holdWhileRevalidate.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import {Instant} from '@croct-tech/time';
import {Clock, Instant} from '@croct-tech/time';
import {DefaultClockProvider} from '@croct-tech/time/defaultClockProvider';
import {CacheLoader, CacheProvider} from './cacheProvider';
import {TimestampedCacheEntry} from './timestampedCacheEntry';

type Configuration<K, V> = {
cacheProvider: CacheProvider<K, TimestampedCacheEntry<V>>,
maxAge: number,
clock?: Clock,
};

export class HoldWhileRevalidateCache<K, V> implements CacheProvider<K, V> {
private readonly cacheProvider: Configuration<K, V>['cacheProvider'];

private readonly maxAge: number;

public constructor({cacheProvider, maxAge}: Configuration<K, V>) {
private readonly clock: Clock;

public constructor({cacheProvider, maxAge, clock}: Configuration<K, V>) {
this.cacheProvider = cacheProvider;
this.maxAge = maxAge;
this.clock = clock ?? DefaultClockProvider.getClock();
}

public async get(key: K, loader: CacheLoader<K, V>): Promise<V> {
const now = Instant.now();
const now = Instant.now(this.clock);

const retrieveAndSave = async (): Promise<TimestampedCacheEntry<V>> => {
const entry: TimestampedCacheEntry<V> = {
Expand All @@ -45,7 +50,7 @@ export class HoldWhileRevalidateCache<K, V> implements CacheProvider<K, V> {
public set(key: K, value: V): Promise<void> {
return this.cacheProvider.set(key, {
value: value,
timestamp: Instant.now(),
timestamp: Instant.now(this.clock),
});
}

Expand Down
11 changes: 8 additions & 3 deletions src/staleWhileRevalidate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {Instant} from '@croct-tech/time';
import {Clock, Instant} from '@croct-tech/time';
import {DefaultClockProvider} from '@croct-tech/time/defaultClockProvider';
import {CacheLoader, CacheProvider} from './cacheProvider';
import {TimestampedCacheEntry} from './timestampedCacheEntry';

type Configuration<K, V> = {
cacheProvider: CacheProvider<K, TimestampedCacheEntry<V>>,
freshPeriod: number,
clock?: Clock,

/**
* Handler for background revalidation errors
Expand All @@ -17,16 +19,19 @@ export class StaleWhileRevalidateCache<K, V> implements CacheProvider<K, V> {

private readonly freshPeriod: number;

private readonly clock: Clock;

private readonly errorHandler: (error: Error) => void;

public constructor(config: Configuration<K, V>) {
this.cacheProvider = config.cacheProvider;
this.freshPeriod = config.freshPeriod;
this.clock = config.clock ?? DefaultClockProvider.getClock();
this.errorHandler = config.errorHandler ?? ((): void => { /* noop */ });
}

public async get(key: K, loader: CacheLoader<K, V>): Promise<V> {
const now = Instant.now();
const now = Instant.now(this.clock);

const retrieveAndSave = async (): Promise<TimestampedCacheEntry<V>> => {
const entry: TimestampedCacheEntry<V> = {
Expand All @@ -52,7 +57,7 @@ export class StaleWhileRevalidateCache<K, V> implements CacheProvider<K, V> {
public set(key: K, value: V): Promise<void> {
return this.cacheProvider.set(key, {
value: value,
timestamp: Instant.now(),
timestamp: Instant.now(this.clock),
});
}

Expand Down
19 changes: 10 additions & 9 deletions test/holdWhileRevalidate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Instant} from '@croct-tech/time';
import {Instant, TimeZone} from '@croct-tech/time';
import {FixedClock} from '@croct-tech/time/clock/fixedClock';
import {CacheProvider, HoldWhileRevalidateCache, TimestampedCacheEntry} from '../src';

describe('A cache provider that holds while revalidating the cache', () => {
Expand All @@ -10,16 +11,16 @@ describe('A cache provider that holds while revalidating the cache', () => {

it('should hold while saving a non-cached value', async () => {
const now = Instant.ofEpochMilli(12345);
const clock = FixedClock.of(now, TimeZone.UTC);

mockCache.get.mockImplementation((key, loader) => loader(key));
mockCache.set.mockResolvedValue();

jest.spyOn(Instant, 'now').mockReturnValue(now);

const loader = jest.fn().mockResolvedValue('loaderValue');

const cache = new HoldWhileRevalidateCache({
cacheProvider: mockCache,
clock: clock,
maxAge: 10,
});

Expand All @@ -39,6 +40,7 @@ describe('A cache provider that holds while revalidating the cache', () => {

it('should hold while saving over an expired value', async () => {
const now = Instant.ofEpochMilli(12345);
const clock = FixedClock.of(now, TimeZone.UTC);

const cachedEntry: TimestampedCacheEntry<string> = {
value: 'cachedValue',
Expand All @@ -48,12 +50,11 @@ describe('A cache provider that holds while revalidating the cache', () => {
mockCache.get.mockResolvedValue(cachedEntry);
mockCache.set.mockResolvedValue();

jest.spyOn(Instant, 'now').mockReturnValue(now);

const loader = jest.fn().mockResolvedValue('loaderValue');

const cache = new HoldWhileRevalidateCache({
cacheProvider: mockCache,
clock: clock,
maxAge: 10,
});

Expand All @@ -73,6 +74,7 @@ describe('A cache provider that holds while revalidating the cache', () => {

it('should return the non-expired cached value', async () => {
const now = Instant.ofEpochMilli(12345);
const clock = FixedClock.of(now, TimeZone.UTC);

const entry: TimestampedCacheEntry<string> = {
value: 'cachedValue',
Expand All @@ -81,12 +83,11 @@ describe('A cache provider that holds while revalidating the cache', () => {

mockCache.get.mockResolvedValue(entry);

jest.spyOn(Instant, 'now').mockReturnValue(now);

const loader = jest.fn();

const cache = new HoldWhileRevalidateCache({
cacheProvider: mockCache,
clock: clock,
maxAge: 10,
});

Expand All @@ -101,12 +102,12 @@ describe('A cache provider that holds while revalidating the cache', () => {
mockCache.set.mockResolvedValue();

const now = Instant.ofEpochMilli(12345);

jest.spyOn(Instant, 'now').mockReturnValue(now);
const clock = FixedClock.of(now, TimeZone.UTC);

const cache = new HoldWhileRevalidateCache({
cacheProvider: mockCache,
maxAge: 10,
clock: clock,
});

await cache.set('key', 'value');
Expand Down
19 changes: 10 additions & 9 deletions test/staleWhileRevalidate.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Instant} from '@croct-tech/time';
import {Instant, TimeZone} from '@croct-tech/time';
import {FixedClock} from '@croct-tech/time/clock/fixedClock';
import {CacheProvider, StaleWhileRevalidateCache, TimestampedCacheEntry} from '../src';

describe('A cache provider that uses stale values while revalidating the cache', () => {
Expand All @@ -10,17 +11,17 @@ describe('A cache provider that uses stale values while revalidating the cache',

it('should hold while saving a non-cached value', async () => {
const now = Instant.ofEpochMilli(12345);
const clock = FixedClock.of(now, TimeZone.UTC);

mockCache.get.mockImplementation((key, loader) => loader(key));
mockCache.set.mockResolvedValue();

jest.spyOn(Instant, 'now').mockReturnValue(now);

const loader = jest.fn().mockResolvedValue('loaderValue');

const cache = new StaleWhileRevalidateCache({
cacheProvider: mockCache,
freshPeriod: 10,
clock: clock,
});

await expect(cache.get('key', loader)).resolves.toBe('loaderValue');
Expand All @@ -39,6 +40,7 @@ describe('A cache provider that uses stale values while revalidating the cache',

it('should return the stale value while revalidating expired entries', async () => {
const now = Instant.ofEpochMilli(12345);
const clock = FixedClock.of(now, TimeZone.UTC);

const cachedEntry: TimestampedCacheEntry<string> = {
value: 'cachedValue',
Expand All @@ -56,8 +58,6 @@ describe('A cache provider that uses stale values while revalidating the cache',
return setPromise;
});

jest.spyOn(Instant, 'now').mockReturnValue(now);

let resolveloader: (value: string) => any = jest.fn();

const loader = jest.fn().mockReturnValueOnce(
Expand All @@ -67,6 +67,7 @@ describe('A cache provider that uses stale values while revalidating the cache',
const cache = new StaleWhileRevalidateCache({
cacheProvider: mockCache,
freshPeriod: 10,
clock: clock,
});

await expect(cache.get('key', loader)).resolves.toBe('cachedValue');
Expand All @@ -91,6 +92,7 @@ describe('A cache provider that uses stale values while revalidating the cache',

it('should returned non-expired entries', async () => {
const now = Instant.ofEpochMilli(12345);
const clock = FixedClock.of(now, TimeZone.UTC);

const entry: TimestampedCacheEntry<string> = {
value: 'cachedValue',
Expand All @@ -99,13 +101,12 @@ describe('A cache provider that uses stale values while revalidating the cache',

mockCache.get.mockResolvedValue(entry);

jest.spyOn(Instant, 'now').mockReturnValue(now);

const loader = jest.fn();

const cache = new StaleWhileRevalidateCache({
cacheProvider: mockCache,
freshPeriod: 10,
clock: clock,
});

await expect(cache.get('key', loader)).resolves.toBe('cachedValue');
Expand All @@ -119,12 +120,12 @@ describe('A cache provider that uses stale values while revalidating the cache',
mockCache.set.mockResolvedValue();

const now = Instant.ofEpochMilli(12345);

jest.spyOn(Instant, 'now').mockReturnValue(now);
const clock = FixedClock.of(now, TimeZone.UTC);

const cache = new StaleWhileRevalidateCache({
cacheProvider: mockCache,
freshPeriod: 10,
clock: clock,
});

await cache.set('key', 'value');
Expand Down

0 comments on commit 1901030

Please sign in to comment.