Skip to content

Commit

Permalink
feat: storage abstractions (#52)
Browse files Browse the repository at this point in the history
* refactor: better support for different storages

* test: updated tests

* feat: export AxiosStorage

* style: fix linting
  • Loading branch information
arthurfiorette authored Nov 11, 2021
1 parent 76a8af7 commit b35ae3e
Show file tree
Hide file tree
Showing 14 changed files with 121 additions and 141 deletions.
9 changes: 5 additions & 4 deletions src/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { Method } from 'axios';
import type { Deferred } from 'typed-core/dist/promises/deferred';
import type { HeadersInterpreter } from '../header/types';
import type { AxiosInterceptor } from '../interceptors/types';
import type { CachedResponse, CacheStorage } from '../storage/types';
import type { AxiosStorage } from '../storage/storage';
import type { CachedResponse } from '../storage/types';
import type { CachePredicate, KeyGenerator } from '../util/types';
import type { CacheUpdater } from '../util/update-cache';
import type { CacheAxiosResponse, CacheRequestConfig } from './axios';
Expand Down Expand Up @@ -54,7 +55,7 @@ export type CacheProperties = {
* The id used is the same as the id on `CacheRequestConfig['id']`,
* auto-generated or not.
*
* @default
* @default {}
*/
update: Record<string, CacheUpdater>;
};
Expand All @@ -63,9 +64,9 @@ export interface CacheInstance {
/**
* The storage to save the cache data.
*
* @default new MemoryStorage()
* @default new MemoryAxiosStorage()
*/
storage: CacheStorage;
storage: AxiosStorage;

/**
* The function used to create different keys for each request.
Expand Down
4 changes: 2 additions & 2 deletions src/cache/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { defaultHeaderInterpreter } from '../header/interpreter';
import { CacheRequestInterceptor } from '../interceptors/request';
import { CacheResponseInterceptor } from '../interceptors/response';
import { MemoryStorage } from '../storage/memory';
import { MemoryAxiosStorage } from '../storage/memory';
import { defaultKeyGenerator } from '../util/key-generator';
import type { AxiosCacheInstance } from './axios';
import type { CacheInstance, CacheProperties } from './cache';
Expand All @@ -28,7 +28,7 @@ export function useCache(
): AxiosCacheInstance {
const axiosCache = axios as AxiosCacheInstance;

axiosCache.storage = storage || new MemoryStorage();
axiosCache.storage = storage || new MemoryAxiosStorage({});
axiosCache.generateKey = generateKey || defaultKeyGenerator;
axiosCache.waiting = waiting || {};
axiosCache.headerInterpreter = headerInterpreter || defaultHeaderInterpreter;
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './cache/cache';
export * from './cache/create';
export * from './header/types';
export * from './interceptors/types';
export * from './storage/storage';
export * from './storage/types';
export * from './util/types';
44 changes: 44 additions & 0 deletions src/storage/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AxiosStorage } from './storage';
import type { EmptyStorageValue, StorageValue } from './types';

export class BrowserAxiosStorage extends AxiosStorage {
public static DEFAULT_KEY_PREFIX = 'a-c-i';

/**
* @param storage any browser storage, like sessionStorage or localStorage
* @param prefix the key prefix to use on all keys.
*/
constructor(
readonly storage: Storage,
readonly prefix: string = BrowserAxiosStorage.DEFAULT_KEY_PREFIX
) {
super();
}

public get = (key: string): StorageValue => {
const prefixedKey = `${this.prefix}:${key}`;

const json = this.storage.getItem(prefixedKey);

if (!json) {
return { state: 'empty' };
}

const parsed = JSON.parse(json);

if (!AxiosStorage.isValid(parsed)) {
this.storage.removeItem(prefixedKey);
return { state: 'empty' };
}

return parsed;
};

public set = (key: string, value: Exclude<StorageValue, EmptyStorageValue>): void => {
return this.storage.setItem(`${this.prefix}:${key}`, JSON.stringify(value));
};

public remove = (key: string): void | Promise<void> => {
return this.storage.removeItem(`${this.prefix}:${key}`);
};
}
24 changes: 13 additions & 11 deletions src/storage/memory.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
import type { CacheStorage, StorageValue } from './types';
import { isCacheValid } from './util';
import { AxiosStorage } from './storage';
import type { CachedStorageValue, LoadingStorageValue, StorageValue } from './types';

export class MemoryStorage implements CacheStorage {
private readonly storage: Map<string, StorageValue> = new Map();
export class MemoryAxiosStorage extends AxiosStorage {
constructor(readonly storage: Record<string, StorageValue> = {}) {
super();
}

get = async (key: string): Promise<StorageValue> => {
const value = this.storage.get(key);
public get = (key: string): StorageValue => {
const value = this.storage[key];

if (!value) {
return { state: 'empty' };
}

if (isCacheValid(value) === false) {
if (!AxiosStorage.isValid(value)) {
this.remove(key);
return { state: 'empty' };
}

return value;
};

set = async (key: string, value: StorageValue): Promise<void> => {
this.storage.set(key, value);
public set = (key: string, value: CachedStorageValue | LoadingStorageValue): void => {
this.storage[key] = value;
};

remove = async (key: string): Promise<void> => {
this.storage.delete(key);
public remove = (key: string): void => {
delete this.storage[key];
};
}
38 changes: 38 additions & 0 deletions src/storage/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { EmptyStorageValue, StorageValue } from './types';

export abstract class AxiosStorage {
/**
* Returns the cached value for the given key. Must handle cache
* miss and staling by returning a new `StorageValue` with `empty` state.
*
* @see {AxiosStorage#isValid}
*/
public abstract get: (key: string) => Promise<StorageValue> | StorageValue;

/**
* Sets a new value for the given key
*
* Use CacheStorage.remove(key) to define a key to 'empty' state.
*/
public abstract set: (
key: string,
value: Exclude<StorageValue, EmptyStorageValue>
) => Promise<void> | void;

/**
* Removes the value for the given key
*/
public abstract remove: (key: string) => Promise<void> | void;

/**
* Returns true if a storage value can still be used by checking his
* createdAt and ttl values.
*/
static isValid = (value?: StorageValue): boolean | 'unknown' => {
if (value?.state === 'cached') {
return value.createdAt + value.ttl > Date.now();
}

return true;
};
}
20 changes: 0 additions & 20 deletions src/storage/types.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,3 @@
export interface CacheStorage {
/**
* Returns the cached value for the given key. Must handle cache
* miss and staling by returning a new `StorageValue` with `empty` state.
*/
get: (key: string) => Promise<StorageValue>;

/**
* Sets a new value for the given key
*
* Use CacheStorage.remove(key) to define a key to 'empty' state.
*/
set: (key: string, value: LoadingStorageValue | CachedStorageValue) => Promise<void>;

/**
* Removes the value for the given key
*/
remove: (key: string) => Promise<void>;
}

export type CachedResponse = {
data?: any;
headers: Record<string, string>;
Expand Down
17 changes: 0 additions & 17 deletions src/storage/util.ts

This file was deleted.

66 changes: 0 additions & 66 deletions src/storage/web.ts

This file was deleted.

9 changes: 3 additions & 6 deletions src/util/update-cache.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import type {
CachedStorageValue,
CacheStorage,
EmptyStorageValue
} from '../storage/types';
import type { AxiosStorage } from '../storage/storage';
import type { CachedStorageValue, EmptyStorageValue } from '../storage/types';

export type CacheUpdater =
| 'delete'
Expand All @@ -12,7 +9,7 @@ export type CacheUpdater =
) => CachedStorageValue | void);

export async function updateCache<T = any>(
storage: CacheStorage,
storage: AxiosStorage,
data: T,
entries: Record<string, CacheUpdater>
): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions test/storage/common.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MemoryStorage } from '../../src/storage/memory';
import { MemoryAxiosStorage } from '../../src/storage/memory';
import { testStorage } from './storages';

describe('tests common storages', () => {
testStorage('memory', () => new MemoryStorage());
testStorage('memory', () => new MemoryAxiosStorage());
});
4 changes: 2 additions & 2 deletions test/storage/storages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CacheStorage } from '../../src/storage/types';
import type { AxiosStorage } from '../../src/storage/storage';
import { EMPTY_RESPONSE } from '../constants';

export function testStorage(name: string, Storage: () => CacheStorage): void {
export function testStorage(name: string, Storage: () => AxiosStorage): void {
it(`tests ${name} storage methods`, async () => {
const storage = Storage();

Expand Down
16 changes: 8 additions & 8 deletions test/storage/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { isCacheValid } from '../../src/storage/util';
import { AxiosStorage } from '../../src/storage/storage';

describe('tests common storages', () => {
it('tests isCacheValid with empty state', () => {
const invalid = isCacheValid({ state: 'empty' });
const invalid = AxiosStorage.isValid({ state: 'empty' });

expect(invalid).toBe('unknown');
expect(invalid).toBe(true);
});

it('tests isCacheValid with loading state', () => {
const invalid = isCacheValid({ state: 'loading' });
const invalid = AxiosStorage.isValid({ state: 'loading' });

expect(invalid).toBe('unknown');
expect(invalid).toBe(true);
});

it('tests isCacheValid with overdue cached state', () => {
const isValid = isCacheValid({
const isValid = AxiosStorage.isValid({
state: 'cached',
data: {} as any, // doesn't matter
createdAt: Date.now() - 2000, // 2 seconds in the past
Expand All @@ -24,8 +24,8 @@ describe('tests common storages', () => {
expect(isValid).toBe(false);
});

it('tests isCacheValid with overdue cached state', () => {
const isValid = isCacheValid({
it('tests isCacheValid with cached state', () => {
const isValid = AxiosStorage.isValid({
state: 'cached',
data: {} as any, // doesn't matter
createdAt: Date.now(),
Expand Down
6 changes: 3 additions & 3 deletions test/storage/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* @jest-environment jsdom
*/

import { LocalCacheStorage, SessionCacheStorage } from '../../src/storage/web';
import { BrowserAxiosStorage } from '../../src/storage/browser';
import { testStorage } from './storages';

describe('tests web storages', () => {
testStorage('local-storage', () => new LocalCacheStorage());
testStorage('session-storage', () => new SessionCacheStorage());
testStorage('local-storage', () => new BrowserAxiosStorage(localStorage));
testStorage('session-storage', () => new BrowserAxiosStorage(sessionStorage));
});

0 comments on commit b35ae3e

Please sign in to comment.