Skip to content

Commit

Permalink
feat: generic Cache and Storage classes
Browse files Browse the repository at this point in the history
  • Loading branch information
calintamas committed Oct 31, 2021
1 parent b12f669 commit 59d7ed7
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 185 deletions.
10 changes: 3 additions & 7 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ module.exports = {
extends: ['backpacker-react-ts'],
rules: {
'import/no-extraneous-dependencies': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/lines-between-class-members': 'off',
'arrow-body-style': 'off',
'no-underscore-dangle': 'off',
},
}
'@typescript-eslint/explicit-module-boundary-types': 'off'
}
};
134 changes: 60 additions & 74 deletions src/Cache.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,89 @@
import Storage from './Storage'
import { CacheObj, CacheOptions } from './types'
import Utils from './utils'

export class Cache {
storage: Storage
ttl: CacheOptions['ttl']
replacementPolicy: CacheOptions['replacementPolicy']
prefix: CacheOptions['prefix']

constructor({ ttl, replacementPolicy, prefix }: CacheOptions) {
this.storage = new Storage()
this.ttl = ttl
this.replacementPolicy = replacementPolicy
this.prefix = prefix

if (prefix) {
Utils.setPrefix(prefix)
}
import { TTL_1H } from './constants';
import { Storage } from './Storage';
import { CacheObj, CacheOptions, ReplacementPolicy } from './types';
import { addPrefix } from './utils/string';

function isExpired(expiryDate: number | string | Date) {
const now = new Date();
const expiryDateAsDate = new Date(expiryDate);
return expiryDateAsDate < now;
}

export class Cache<DataType> {
storage: Storage<CacheObj<DataType>>;

name: string;

ttl: number;

replacementPolicy: ReplacementPolicy;

size?: number;

constructor(cacheName: string, cacheOptions?: CacheOptions) {
const {
ttl = TTL_1H,
replacementPolicy = 'LRU',
size // TODO add a default size
} = cacheOptions ?? {};

this.storage = new Storage();
this.name = cacheName;
this.ttl = ttl;
this.replacementPolicy = replacementPolicy;
this.size = size;
}

set(key: string, data: string, attempts?: number) {
const now = new Date()
let payload: CacheObj = {
set(key: string, data: DataType) {
const now = new Date();
const payload: CacheObj<DataType> = {
data,
expiry_date: now.setSeconds(now.getSeconds() + this.ttl), // save as Unix timestamp (miliseconds)
}
expiryDate: now.setSeconds(now.getSeconds() + this.ttl) // save as Unix timestamp (milliseconds)
};

// Cache data can be invalidated after a number of attempts,
// or it can live until expiry_date if the 'attempts' param is null
if (attempts) {
payload = {
...payload,
attempts,
}
}
// TODO implement replacement policies; take into account `size` and `replacementPolicy` type

return this.storage.store(Utils.addPrefix(key), payload)
return this.storage.store(addPrefix(key, this.name), payload);
}

async get(key: string) {
try {
const payload = await this.storage.get(Utils.addPrefix(key))

if (!payload) {
throw new Error('No cached data')
}

if (Utils.isExpired(payload.expiry_date)) {
this.clear(key)
throw new Error('Expired cached data')
const res = await this.storage.get(addPrefix(key, this.name));
if (!res) {
throw new Error('No cached data');
}

// If a max number of attempts has been set,
// check if it's been passed or not
if (payload.attempts != null) {
if (payload.attempts === 0) {
throw new Error('Invalidated cached data')
}

await this.update(key, {
...payload,
attempts: payload.attempts - 1,
})
if (isExpired(res.expiryDate)) {
this.clear(key);
throw new Error('Expired cached data');
}

return payload.data
return res.data;
} catch (err) {
return null
return null;
}
}

async getAll(preserveKeys?: boolean) {
const allKeys = await this.storage.getAllKeys()
const myKeys = Utils.onlyMyKeys(allKeys)

return this.storage.multiGet(myKeys, preserveKeys)
}

private update(key: string, data: string) {
return this.storage.update(Utils.addPrefix(key), data)
async getAll() {
const keys = await this.storage.getKeysWithPrefix(this.name);
return this.storage.multiGet(keys);
}

async clear(key?: string): Promise<boolean> {
async clear(key?: string) {
if (!key) {
const all = await this.getAll(true)
const keys = all.map((i) => i.__cachemere__key)

return this.storage.multiRemove(keys)
const keys = await this.storage.getKeysWithPrefix(this.name);
return this.storage.multiRemove(keys);
}

return this.storage.remove(Utils.addPrefix(key))
return this.storage.remove(addPrefix(key, this.name));
}

async clearExpired() {
const all = await this.getAll(true)
const all = await this.getAll();
const expiredKeys = all
.filter((i) => Utils.isExpired(i.expiry_date))
.map((i) => i.__cachemere__key)
.filter((item) => isExpired(item.expiryDate))
.map((item) => item.key);

return this.storage.multiRemove(expiredKeys)
return this.storage.multiRemove(expiredKeys);
}
}
87 changes: 40 additions & 47 deletions src/Storage.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,84 @@
import AsyncStorage, {
AsyncStorageStatic,
} from '@react-native-async-storage/async-storage'
AsyncStorageStatic
} from '@react-native-async-storage/async-storage';

import Utils from './utils'

class Storage {
StorageSystem: AsyncStorageStatic
export class Storage<DataType> {
StorageSystem: AsyncStorageStatic;

constructor() {
this.StorageSystem = AsyncStorage
this.StorageSystem = AsyncStorage;
}

async store(key: string, value: any): Promise<string | null> {
async store(key: string, value: DataType): Promise<boolean> {
try {
await this.StorageSystem.setItem(key, JSON.stringify(value))

return value
await this.StorageSystem.setItem(key, JSON.stringify(value));
return true;
} catch (err) {
return null
return false;
}
}

async get(key: string): Promise<any> {
async get(key: string): Promise<DataType | null> {
try {
const value = await this.StorageSystem.getItem(key)

if (value === null) {
return value
const data = await this.StorageSystem.getItem(key);
if (!data) {
throw new Error('No data');
}

return JSON.parse(value)
return JSON.parse(data);
} catch (err) {
return null
return null;
}
}

async remove(key: string): Promise<boolean> {
try {
await this.StorageSystem.removeItem(key)

return true
await this.StorageSystem.removeItem(key);
return true;
} catch (err) {
return false
return false;
}
}

async update(key: string, data: string): Promise<string | null> {
return this.store(key, data)
async update(key: string, data: DataType): Promise<boolean> {
return this.store(key, data);
}

// Gets all keys known to your app, for all callers, libraries.
async getAllKeys(): Promise<string[]> {
async getKeysWithPrefix(prefix: string): Promise<string[]> {
try {
const allKeys = await this.StorageSystem.getAllKeys()

return allKeys
const keys = await this.StorageSystem.getAllKeys();
const keysWithPrefix = keys.filter((item) => {
return item.match(new RegExp(`^${prefix}+`));
});
return keysWithPrefix;
} catch (err) {
return []
return [];
}
}

async multiGet(keys: string[] = [], preserveKeys = false) {
async multiGet(keys: string[]): Promise<(DataType & { key: string })[]> {
try {
const all: [string, string | null][] = await this.StorageSystem.multiGet(
keys
)
const all = await this.StorageSystem.multiGet(keys);
const definedValues = all.filter(([, value]) => value !== null);

return all.map((item) => {
return definedValues.map(([key, value]) => {
const data = JSON.parse(value as string) as DataType;
return {
...JSON.parse(String(item[1])),
__cachemere__key: preserveKeys ? item[0] : Utils.stripPrefix(item[0]),
}
})
...data,
key
};
});
} catch (err) {
return []
return [];
}
}

async multiRemove(keys: string[] = []): Promise<boolean> {
try {
await this.StorageSystem.multiRemove(keys)
await this.StorageSystem.multiRemove(keys);

return true
return true;
} catch (err) {
return false
return false;
}
}
}

export default Storage
11 changes: 6 additions & 5 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const TTL_12H = 43200
export const TTL_8H = 28800
export const TTL_6H = 21600
export const TTL_4H = 14400
export const TTL_1H = 3600
export const TTL_30M = 1800;
export const TTL_1H = TTL_30M * 2;
export const TTL_4H = TTL_1H * 4;
export const TTL_6H = TTL_1H * 6;
export const TTL_8H = TTL_1H * 8;
export const TTL_12H = TTL_1H * 12;
27 changes: 9 additions & 18 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
export enum ReplacementPolicies {
FIFO = 'FIFO',
LRU = 'LRU',
}

export type ReplacementPolicy = Readonly<{
type: ReplacementPolicies
limit: number
}>
export type ReplacementPolicy = 'LRU' | 'FIFO';

export type CacheOptions = {
ttl: number
replacementPolicy?: ReplacementPolicy
prefix?: string
}
ttl?: number;
size?: number;
replacementPolicy?: ReplacementPolicy;
};

export type CacheObj = Readonly<{
data: string
expiry_date: number
attempts?: number
}>
export type CacheObj<DataType> = Readonly<{
data: DataType;
expiryDate: number;
}>;
34 changes: 0 additions & 34 deletions src/utils.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/utils/string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function addPrefix(str: string, prefix: string) {
return `${prefix}${str}`;
}

0 comments on commit 59d7ed7

Please sign in to comment.