All files / src/services cacheService.ts

100% Statements 49/49
100% Branches 24/24
100% Functions 11/11
100% Lines 46/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122                15x 15x         15x 15x       91x       14x       39x 39x 39x   10x 1x 1x     9x       38x 38x 38x       48x 48x 35x 33x           6x 6x 6x   4x 4x 2x 2x     2x 2x                 4x 4x         4x 4x       3x 3x 3x   2x 2x       2x             2x 2x               1x 1x 1x 1x       15x      
import AsyncStorage from "@react-native-async-storage/async-storage";
 
interface CacheEnvelope<T> {
  value: T;
  expiresAt: number | null;
}
 
class CacheService {
  private static instance: CacheService | null = null;
  private readonly memory = new Map<string, CacheEnvelope<unknown>>();
 
  private constructor() {}
 
  static getInstance(): CacheService {
    CacheService.instance ??= new CacheService();
    return CacheService.instance;
  }
 
  private makeKey(namespace: string, key: string): string {
    return `${namespace}:${key}`;
  }
 
  private isExpired(expiresAt: number | null): boolean {
    return expiresAt !== null && Date.now() > expiresAt;
  }
 
  getMemory<T>(namespace: string, key: string): T | null {
    const cacheKey = this.makeKey(namespace, key);
    const cached = this.memory.get(cacheKey);
    if (!cached) return null;
 
    if (this.isExpired(cached.expiresAt)) {
      this.memory.delete(cacheKey);
      return null;
    }
 
    return cached.value as T;
  }
 
  setMemory<T>(namespace: string, key: string, value: T, ttlMs?: number): void {
    const cacheKey = this.makeKey(namespace, key);
    const expiresAt = ttlMs ? Date.now() + ttlMs : null;
    this.memory.set(cacheKey, { value, expiresAt });
  }
 
  clearMemoryNamespace(namespace: string): void {
    const prefix = `${namespace}:`;
    for (const key of this.memory.keys()) {
      if (key.startsWith(prefix)) {
        this.memory.delete(key);
      }
    }
  }
 
  async getPersistent<T>(namespace: string, key: string): Promise<T | null> {
    const cacheKey = this.makeKey(namespace, key);
    const raw = await AsyncStorage.getItem(cacheKey);
    if (!raw) return null;
 
    const envelope = JSON.parse(raw) as CacheEnvelope<T>;
    if (this.isExpired(envelope.expiresAt)) {
      await AsyncStorage.removeItem(cacheKey);
      return null;
    }
 
    this.memory.set(cacheKey, envelope as CacheEnvelope<unknown>);
    return envelope.value;
  }
 
  async setPersistent<T>(
    namespace: string,
    key: string,
    value: T,
    ttlMs?: number,
  ): Promise<void> {
    const cacheKey = this.makeKey(namespace, key);
    const envelope: CacheEnvelope<T> = {
      value,
      expiresAt: ttlMs ? Date.now() + ttlMs : null,
    };
 
    this.memory.set(cacheKey, envelope as CacheEnvelope<unknown>);
    await AsyncStorage.setItem(cacheKey, JSON.stringify(envelope));
  }
 
  async getPersistentRaw<T>(namespace: string, key: string): Promise<T | null> {
    const cacheKey = this.makeKey(namespace, key);
    const raw = await AsyncStorage.getItem(cacheKey);
    if (!raw) return null;
 
    const parsed = JSON.parse(raw) as unknown;
    const maybeEnvelope = parsed as Partial<CacheEnvelope<T>>;
 
    // Backward compatible with legacy raw values that were stored without envelope.
    const envelope: CacheEnvelope<T> =
      maybeEnvelope &&
      typeof maybeEnvelope === "object" &&
      "value" in maybeEnvelope &&
      "expiresAt" in maybeEnvelope
        ? (maybeEnvelope as CacheEnvelope<T>)
        : { value: parsed as T, expiresAt: null };
 
    this.memory.set(cacheKey, envelope as CacheEnvelope<unknown>);
    return envelope.value;
  }
 
  async setPersistentRaw<T>(
    namespace: string,
    key: string,
    value: T,
  ): Promise<void> {
    const cacheKey = this.makeKey(namespace, key);
    const envelope: CacheEnvelope<T> = { value, expiresAt: null };
    this.memory.set(cacheKey, envelope as CacheEnvelope<unknown>);
    await AsyncStorage.setItem(cacheKey, JSON.stringify(envelope));
  }
}
 
const cacheService = CacheService.getInstance();
 
export default cacheService;