-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: generic Cache and Storage classes
- Loading branch information
1 parent
b12f669
commit 59d7ed7
Showing
7 changed files
with
121 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}>; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function addPrefix(str: string, prefix: string) { | ||
return `${prefix}${str}`; | ||
} |