From 36354994f702f22c569e52524ea4a3f523050c5a Mon Sep 17 00:00:00 2001 From: alvarius Date: Tue, 16 Apr 2024 15:01:07 +0100 Subject: [PATCH] feat(store-indexer): add cache headers (#2669) Co-authored-by: Kevin Ingersoll --- .changeset/fifty-dryers-enjoy.md | 5 ++++ .../store-indexer/src/postgres/apiRoutes.ts | 25 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .changeset/fifty-dryers-enjoy.md diff --git a/.changeset/fifty-dryers-enjoy.md b/.changeset/fifty-dryers-enjoy.md new file mode 100644 index 0000000000..66e9549920 --- /dev/null +++ b/.changeset/fifty-dryers-enjoy.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/store-indexer": patch +--- + +Added `Cache-Control` and `Content-Type` headers to the postgres indexer API. diff --git a/packages/store-indexer/src/postgres/apiRoutes.ts b/packages/store-indexer/src/postgres/apiRoutes.ts index 66f5d63397..01a38f3e0d 100644 --- a/packages/store-indexer/src/postgres/apiRoutes.ts +++ b/packages/store-indexer/src/postgres/apiRoutes.ts @@ -21,6 +21,7 @@ export function apiRoutes(database: Sql): Middleware { options = input.parse(typeof ctx.query.input === "string" ? JSON.parse(ctx.query.input) : {}); } catch (e) { ctx.status = 400; + ctx.set("Content-Type", "application/json"); ctx.body = JSON.stringify(e); debug(e); return; @@ -33,6 +34,11 @@ export function apiRoutes(database: Sql): Middleware { const logs = records.map(recordToLog); benchmark("map records to logs"); + // Ideally we would immediately return an error if the request is for a Store that the indexer + // is not configured to index. Since we don't have easy access to this information here, + // we return an error if there are no logs found for a given Store, since that would never + // be the case for a Store that is being indexed (since there would at least be records for the + // Tables table with tables created during Store initialization). if (records.length === 0) { ctx.status = 404; ctx.body = "no logs found"; @@ -45,10 +51,27 @@ export function apiRoutes(database: Sql): Middleware { } const blockNumber = records[0].chainBlockNumber; - ctx.body = JSON.stringify({ blockNumber, logs }); ctx.status = 200; + + // max age is set to several multiples of the uncached response time (currently ~10s, but using 60s for wiggle room) to ensure only ~one origin request at a time + // and stale-while-revalidate below means that the cache is refreshed under the hood while still responding fast (cached) + const maxAgeSeconds = 60 * 5; + // we set stale-while-revalidate to the time elapsed by the number of blocks we can fetch from the RPC in the same amount of time as an uncached response + // meaning it would take ~the same about of time to get an uncached response from the origin as it would to catch up from the currently cached response + // if an uncached response takes ~10 seconds, we have ~10s to catch up, so let's say we can do enough RPC calls to fetch 4000 blocks + // with a block per 2 seconds, that means we can serve a stale/cached response for 8000 seconds before we should require the response be returned by the origin + const staleWhileRevalidateSeconds = 4000 * 2; + + ctx.set( + "Cache-Control", + `public, max-age=${maxAgeSeconds}, stale-while-revalidate=${staleWhileRevalidateSeconds}`, + ); + + ctx.set("Content-Type", "application/json"); + ctx.body = JSON.stringify({ blockNumber, logs }); } catch (e) { ctx.status = 500; + ctx.set("Content-Type", "application/json"); ctx.body = JSON.stringify(e); error(e); }