Skip to content

Instantly share code, notes, and snippets.

@peplocanto
Last active October 7, 2022 06:23
Show Gist options
  • Save peplocanto/b96b4cebee594b738da85c03702d903e to your computer and use it in GitHub Desktop.
Save peplocanto/b96b4cebee594b738da85c03702d903e to your computer and use it in GitHub Desktop.

Browser Cache Api

interface CacheConfig {
  name: string;
  validTime: number;
  endpoints: string[];
  options?: CacheOptions;
  clearOnHide?: boolean;
}

interface CacheOptions {
  ignoreSearch: boolean;
  ignoreMethod: boolean;
  ignoreVary: boolean;
}

const CACHE_CONFIG: CacheConfig[] = [];

const DEFAULT_CACHE_OPTIONS = {
  ignoreSearch: true,
  ignoreMethod: false,
  ignoreVary: true,
};

const DEFAULT_SEPARATOR = '@';

async function fetchData<T>(endpoint: string): Promise<T | void> {
  try {
    const response: Response = await handleEndpoint(endpoint);
    if (response.ok) {
      return await response.json();
    } else {
      return handleError(response.status);
    }
  } catch (error) {
    return handleError(error);
  }
}

function handleEndpoint(endpoint: string): Promise<Response> {
  const config = CACHE_CONFIG.find((config) =>
    config.endpoints.some((ep) => endpoint.includes(ep))
  );
  if (config) {
    return handleCachedEndpoint(endpoint, config);
  } else {
    return fetch(endpoint);
  }
}

async function handleCachedEndpoint(
  endpoint: string,
  config: CacheConfig
): Promise<Response> {
  if ('caches' in self) {
    const cacheName = await getCacheName(config);
    try {
      if (config.clearOnHide) {
        handleClearOnHide(cacheName);
      }
      const cache = await caches.open(cacheName);
      const options = config.options ?? DEFAULT_CACHE_OPTIONS;
      const response = await cache.match(endpoint, options);
      if (response) {
        return response;
      } else {
        await cache.add(endpoint);
        return (await cache.match(endpoint, options)) as Response;
      }
    } catch (error) {
      handleError(error);
      return fetch(endpoint);
    }
  } else {
    return fetch(endpoint);
  }
}

async function getCacheName(config: CacheConfig): Promise<string> {
  const keys = await caches.keys();
  const cacheName = keys.find((key) => key.includes(config.name));
  const now = getTimestamp();
  if (cacheName) {
    const timestamp =
      Number(cacheName.split(DEFAULT_SEPARATOR)[1]) || undefined;
    if (!timestamp || now - timestamp >= config.validTime) {
      await caches.delete(cacheName);
      return getNewCacheName(config.name, now);
    } else {
      return cacheName;
    }
  } else {
    return getNewCacheName(config.name, now);
  }
}

function getTimestamp(): number {
  const now = new Date().getTime();
  return Math.floor(now / 1000) * 1000;
}

function getNewCacheName(name: string, now: number) {
  return `${name}${DEFAULT_SEPARATOR}${now}`;
}

function handleClearOnHide(cacheName: string) {
  if (document) {
    document.onvisibilitychange = async () => {
      if (document.visibilityState === 'hidden') {
        await caches.delete(cacheName);
      }
    };
  }
}

function handleError(error: unknown): void {
  console.log(error);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment