From 34a2ae9b02110ef371c826ca8f656610cfacbfa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Fri, 11 Oct 2024 10:45:50 +0200 Subject: [PATCH] Implement stale-while-revalidation cache for fetch --- src/ens.mjs | 61 ++++------------------------------------ src/http.mjs | 8 ------ src/utils.mjs | 35 ++++++++++++++++++++++- src/views/moderation.mjs | 19 ++++++++----- 4 files changed, 51 insertions(+), 72 deletions(-) diff --git a/src/ens.mjs b/src/ens.mjs index d77d861..76a17d3 100644 --- a/src/ens.mjs +++ b/src/ens.mjs @@ -1,11 +1,12 @@ import { env } from "process"; import path from "path"; + import DOMPurify from "isomorphic-dompurify"; +import { fetchBuilder, FileSystemCache } from "node-fetch-cache"; +import { providers, utils } from "ethers"; -import { Response } from "node-fetch"; -import { fetchBuilder, FileSystemCache, getCacheKey } from "node-fetch-cache"; import { allowlist } from "./chainstate/registry.mjs"; -import { providers, utils } from "ethers"; +import { fetchCache } from "./utils.mjs"; const provider = new providers.JsonRpcProvider(env.RPC_HTTP_HOST); @@ -14,27 +15,7 @@ const cache = new FileSystemCache({ ttl: 86400000 * 5, // 72 hours }); const fetch = fetchBuilder.withCache(cache); - -const fetchStaleWhileRevalidate = async (url, options = {}) => { - const cacheKey = getCacheKey(url, options); - const cachedValue = await cache.get(cacheKey); - - (async () => { - try { - await fetch(url, options); - } catch (error) { - console.error(`Error fetching and caching data for ${url}:`, error); - } - })(); - - if (cachedValue) { - // NOTE: node-fetch-cache doesn't return a node-fetch Response, hence we're - // casting it to one before handing it back to the business logic. - return new Response(cachedValue.bodyStream, cachedValue.metaData); - } - - throw new Error(`No cached data momentarily available for ${url}`); -}; +const fetchStaleWhileRevalidate = fetchCache(fetch, cache); export async function toAddress(name) { const address = await provider.resolveName(name); @@ -210,35 +191,3 @@ export async function resolve(address) { }; return profile; } - -async function initializeCache() { - let addresses = Array.from(await allowlist()); - while (addresses.length === 0) { - await new Promise((r) => setTimeout(r, 5000)); // wait for 5 seconds - addresses = await allowlist(); - } - - if (addresses && Array.isArray(addresses)) { - const promises = addresses.map(async (address) => { - const profile = await resolve(address); - if (profile && profile.ens) { - try { - await toAddress(profile.ens); - } catch (err) { - // ignore error - } - } - }); - - await Promise.allSettled(promises); - } -} - -// NOTE: For nodes that have never downloaded and committed all addresses into -// LMDB during their first crawl, it may be that this function is launched and -// then initializeCache's `let addresses = Array.from(await allowlist());` will -// unexpectedly throw because it should wait for allowlist to be crawled at -// least onceit should wait for allowlist to be crawled at least once -if (env.NODE_ENV === "production") { - setTimeout(initializeCache, 30000); -} diff --git a/src/http.mjs b/src/http.mjs index 4a459fc..371934a 100644 --- a/src/http.mjs +++ b/src/http.mjs @@ -11,7 +11,6 @@ import htm from "htm"; import "express-async-errors"; import { sub } from "date-fns"; import DOMPurify from "isomorphic-dompurify"; -import { fetchBuilder, FileSystemCache } from "node-fetch-cache"; import ws from "ws"; import { createServer } from "http"; @@ -77,13 +76,6 @@ import { getLeaders, } from "./cache.mjs"; -const fetch = fetchBuilder.withCache( - new FileSystemCache({ - cacheDirectory: path.resolve(env.CACHE_DIR), - ttl: 86400000, // 24 hours - }), -); - const app = express(); const server = createServer(app); diff --git a/src/utils.mjs b/src/utils.mjs index 3084e77..5953dca 100644 --- a/src/utils.mjs +++ b/src/utils.mjs @@ -1,8 +1,41 @@ import path from "path"; import { fileURLToPath } from "url"; -let lastCall; +import { Response } from "node-fetch"; +import { getCacheKey } from "node-fetch-cache"; + +// NOTE: This is an extension of node-fetch-cache where we're loading the +// to-be-cached data in the background while returning an error to the caller +// in the meantime. What this does is that it stops blocking requests from +// being resolved, for example, in the ens module. +export function fetchCache(fetch, cache) { + if (!fetch || !cache) { + throw new Error("fetch and cache must be passed to fetchCache"); + } + + return async (url, options = {}) => { + const cacheKey = getCacheKey(url, options); + const cachedValue = await cache.get(cacheKey); + + (async () => { + try { + await fetch(url, options); + } catch (error) { + console.error(`Error fetching and caching data for ${url}:`, error); + } + })(); + if (cachedValue) { + // NOTE: node-fetch-cache doesn't return a node-fetch Response, hence we're + // casting it to one before handing it back to the business logic. + return new Response(cachedValue.bodyStream, cachedValue.metaData); + } + + throw new Error(`No cached data momentarily available for ${url}`); + }; +} + +let lastCall; export function logd(label = "") { const now = Date.now(); if (lastCall === undefined) { diff --git a/src/views/moderation.mjs b/src/views/moderation.mjs index edc5e12..28f1e79 100644 --- a/src/views/moderation.mjs +++ b/src/views/moderation.mjs @@ -1,21 +1,26 @@ // @format -import { fetchBuilder, MemoryCache } from "node-fetch-cache"; +import path from "path"; +import { env } from "process"; + +import { fetchBuilder, FileSystemCache } from "node-fetch-cache"; import normalizeUrl from "normalize-url"; import * as id from "../id.mjs"; import log from "../logger.mjs"; import { EIP712_MESSAGE } from "../constants.mjs"; +import { fetchCache } from "../utils.mjs"; -const fetch = fetchBuilder.withCache( - new MemoryCache({ - ttl: 60000, // 1min - }), -); +const cache = new FileSystemCache({ + cacheDirectory: path.resolve(env.CACHE_DIR), + ttl: 60000 * 5, // 5min +}); +const fetch = fetchBuilder.withCache(cache); +const fetchStaleWhileRevalidate = fetchCache(fetch, cache); const url = "https://opensheet.elk.sh/1kh9zHwzekLb7toabpdSfd87pINBpyVU6Q8jLliBXtEc/"; export async function getConfig(sheet) { - const response = await fetch(url + sheet); + const response = await fetchStaleWhileRevalidate(url + sheet); if (response.ok) { return await response.json(); } else {