Skip to content

Commit

Permalink
Implement stale-while-revalidation cache for fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
TimDaub committed Oct 11, 2024
1 parent f68f996 commit 34a2ae9
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 72 deletions.
61 changes: 5 additions & 56 deletions src/ens.mjs
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
8 changes: 0 additions & 8 deletions src/http.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);

Expand Down
35 changes: 34 additions & 1 deletion src/utils.mjs
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
19 changes: 12 additions & 7 deletions src/views/moderation.mjs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 34a2ae9

Please sign in to comment.