Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: storage abstractions #52

Merged
merged 4 commits into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
});