diff --git a/src/api/routes/inscriptions.ts b/src/api/routes/inscriptions.ts index 0be0e200..23c2017b 100644 --- a/src/api/routes/inscriptions.ts +++ b/src/api/routes/inscriptions.ts @@ -22,7 +22,7 @@ import { Order, InscriptionIdParam, } from '../types'; -import { handleInscriptionCache } from '../util/cache'; +import { handleChainTipCache, handleInscriptionCache } from '../util/cache'; import { DEFAULT_API_LIMIT, hexToBuffer, @@ -38,6 +38,7 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy options, done ) => { + fastify.addHook('preHandler', handleChainTipCache); fastify.get( '/inscriptions', { diff --git a/src/api/util/cache.ts b/src/api/util/cache.ts index 86e86ea2..aaaee1e9 100644 --- a/src/api/util/cache.ts +++ b/src/api/util/cache.ts @@ -19,6 +19,10 @@ export async function handleInscriptionCache(request: FastifyRequest, reply: Fas return handleCache(ETagType.inscription, request, reply); } +export async function handleChainTipCache(request: FastifyRequest, reply: FastifyReply) { + return handleCache(ETagType.chainTip, request, reply); +} + async function handleCache(type: ETagType, request: FastifyRequest, reply: FastifyReply) { const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']); let etag: string | undefined; @@ -26,6 +30,9 @@ async function handleCache(type: ETagType, request: FastifyRequest, reply: Fasti case ETagType.inscription: etag = await getInscriptionLocationEtag(request); break; + case ETagType.chainTip: + etag = await getChainTipEtag(request); + break; } if (etag) { if (ifNoneMatch && ifNoneMatch.includes(etag)) { @@ -44,6 +51,7 @@ export function setReplyNonCacheable(reply: FastifyReply) { /** * Retrieve the inscriptions's location timestamp as a UNIX epoch so we can use it as the response * ETag. + * @param request - Fastify request * @returns Etag string */ async function getInscriptionLocationEtag(request: FastifyRequest): Promise { @@ -62,7 +70,23 @@ async function getInscriptionLocationEtag(request: FastifyRequest): Promise { + try { + const blockHeight = await request.server.db.getChainTipBlockHeight(); + if (blockHeight) { + return blockHeight.toString(); + } + } catch (error) { + return; } } diff --git a/tests/cache.test.ts b/tests/cache.test.ts index cb3fe606..9ecdcd3d 100644 --- a/tests/cache.test.ts +++ b/tests/cache.test.ts @@ -71,4 +71,83 @@ describe('ETag cache', () => { }); expect(cached2.statusCode).toBe(200); }); + + test('inscriptions index cache control', async () => { + await db.insertInscriptionGenesis({ + inscription: { + genesis_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', + mime_type: 'text/plain', + content_type: 'text/plain;charset=utf-8', + content_length: 5, + content: '0x48656C6C6F', + fee: 705n, + }, + location: { + inscription_id: 0, + block_height: 778575, + block_hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + tx_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', + address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', + output: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0', + offset: 0n, + value: 10000n, + timestamp: 1676913207, + sat_ordinal: 257418248345364n, + sat_rarity: 'common', + genesis: true, + current: true, + }, + }); + await db.insertInscriptionGenesis({ + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + mime_type: 'image/png', + content_type: 'image/png', + content_length: 5, + content: '0x48656C6C6F', + fee: 2805n, + }, + location: { + inscription_id: 0, + block_height: 775617, + block_hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: 0n, + value: 10000n, + timestamp: 1676913207, + sat_ordinal: 257418248345364n, + sat_rarity: 'common', + genesis: true, + current: true, + }, + }); + + // ETag response + const response = await fastify.inject({ + method: 'GET', + url: '/inscriptions', + }); + expect(response.statusCode).toBe(200); + expect(response.headers.etag).not.toBeUndefined(); + const etag = response.headers.etag; + + // Cached + const cached = await fastify.inject({ + method: 'GET', + url: '/inscriptions', + headers: { 'if-none-match': etag }, + }); + expect(cached.statusCode).toBe(304); + + // Simulate new chain tip + await db.updateChainTipBlockHeight({ blockHeight: 5 }); + const cached2 = await fastify.inject({ + method: 'GET', + url: '/inscriptions', + headers: { 'if-none-match': etag }, + }); + expect(cached2.statusCode).toBe(200); + }); });