feat: caches GraphQL queries for Cloudflare workers (#6929)
* feat: add token and nft injection * feat: basic tests * fix: get jest configured properly * fix: change timeout * fix: uninstall port ready * fix: readd port ready * fix: local tests work * Update yarn.lock * add lint disable for setup files * fix: update dependencies * fix: basic test suite for nfts/tokens * feat: collection data * fix: make tests more comprehensive * fix: change matches to contains * fix: tests for twitter alt image tag * fix: image gen * fix: add patch-package * fix: update yarn install * feat: basic image gen for nfts and collections * fix: remove vibrant attempt * use watermark asset * dynamically grab color * modularize code and prototype for token preview * refactor code * finalize css * fix color grabber * update tests * fix up css * refactor code a bit more * remove console logs * tests * update tests * update images based on design feedback * network logos * update lint * slight refactoring * more refactoring * fix packages * Update yarn.lock * remove dynamically generated image stuff * cleanup return values * Create README.md * Revert "Create README.md" This reverts commit 7a91c98d384995fba914c9bf9a2fb3072793621f. * First round of feedback * comments * feat: cache * Update test.yml * Update test.yml * Update test.yml * feedback round 2 * final feedback * final final feedback * add coverage and other options * Update test.yml * start typecheck * update cache * update snapshots? * Update jest.config.json * Update jest.config.json * give timeout some buffer * update import * upgrade ts * fix typing for apollo deps * finalize typechecks * downgrade typescript to original version * add cache directory to jest * remove coverage * remove google analytics from tests * review changes * try cache setup * Update cache.test.ts * make cache helper function * cache test * remove unneeded test causing issues * feat: parallelize cache (#6930) * feat: parallelize cache? * remove graph query from concurrency await * most of feedback * move tests * update token tests * singleton cache * restructuring res and cache promise * abstract away repeated graph logic * final feedback * Update yarn.lock * final final feedback * final final final feedback! * final final final final feedback?
This commit is contained in:
parent
dbb2f7f6a2
commit
f845695f6e
@ -1,20 +1,15 @@
|
|||||||
/* eslint-disable import/no-unused-modules */
|
/* eslint-disable import/no-unused-modules */
|
||||||
import { MetaTagInjector } from '../../components/metaTagInjector'
|
|
||||||
import getAsset from '../../utils/getAsset'
|
import getAsset from '../../utils/getAsset'
|
||||||
|
import getRequest from '../../utils/getRequest'
|
||||||
|
|
||||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||||
const { index } = params
|
const res = next()
|
||||||
const collectionAddress = index[0]?.toString()
|
|
||||||
const tokenId = index[1]?.toString()
|
|
||||||
const assetPromise = getAsset(collectionAddress, tokenId, request.url)
|
|
||||||
const resPromise = next()
|
|
||||||
try {
|
try {
|
||||||
const [data, res] = await Promise.all([assetPromise, resPromise])
|
const { index } = params
|
||||||
if (!data) {
|
const collectionAddress = index[0]?.toString()
|
||||||
return resPromise
|
const tokenId = index[1]?.toString()
|
||||||
}
|
return getRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
|
||||||
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(res)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return resPromise
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
/* eslint-disable import/no-unused-modules */
|
/* eslint-disable import/no-unused-modules */
|
||||||
import { MetaTagInjector } from '../../components/metaTagInjector'
|
|
||||||
import getCollection from '../../utils/getCollection'
|
import getCollection from '../../utils/getCollection'
|
||||||
|
import getRequest from '../../utils/getRequest'
|
||||||
|
|
||||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||||
const { index } = params
|
const res = next()
|
||||||
const collectionAddress = index?.toString()
|
|
||||||
const collectionPromise = getCollection(collectionAddress, request.url)
|
|
||||||
const resPromise = next()
|
|
||||||
try {
|
try {
|
||||||
const [data, res] = await Promise.all([collectionPromise, resPromise])
|
const { index } = params
|
||||||
if (!data) {
|
const collectionAddress = index?.toString()
|
||||||
return resPromise
|
return getRequest(res, request.url, () => getCollection(collectionAddress, request.url))
|
||||||
}
|
|
||||||
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(res)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return resPromise
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable import/no-unused-modules */
|
/* eslint-disable import/no-unused-modules */
|
||||||
import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks'
|
import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||||
import { MetaTagInjector } from '../components/metaTagInjector'
|
import getRequest from '../utils/getRequest'
|
||||||
import getToken from '../utils/getToken'
|
import getToken from '../utils/getToken'
|
||||||
|
|
||||||
const convertTokenAddress = (tokenAddress: string, networkName: string) => {
|
const convertTokenAddress = (tokenAddress: string, networkName: string) => {
|
||||||
@ -18,22 +18,17 @@ const convertTokenAddress = (tokenAddress: string, networkName: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||||
const { index } = params
|
const res = next()
|
||||||
const networkName = index[0]?.toString().toUpperCase()
|
|
||||||
const tokenString = index[1]?.toString()
|
|
||||||
if (!tokenString) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
const tokenAddress = convertTokenAddress(tokenString, networkName)
|
|
||||||
const tokenPromise = getToken(networkName, tokenAddress, request.url)
|
|
||||||
const resPromise = next()
|
|
||||||
try {
|
try {
|
||||||
const [data, res] = await Promise.all([tokenPromise, resPromise])
|
const { index } = params
|
||||||
if (!data) {
|
const networkName = index[0]?.toString().toUpperCase()
|
||||||
return resPromise
|
const tokenString = index[1]?.toString()
|
||||||
|
if (!tokenString) {
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(res)
|
const tokenAddress = convertTokenAddress(tokenString, networkName)
|
||||||
|
return getRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return resPromise
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
43
functions/utils/cache.test.ts
Normal file
43
functions/utils/cache.test.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import CacheMock from 'browser-cache-mock'
|
||||||
|
|
||||||
|
import { mocked } from '../../src/test-utils/mocked'
|
||||||
|
import Cache from './cache'
|
||||||
|
|
||||||
|
const cacheMock = new CacheMock()
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
title: 'test',
|
||||||
|
image: 'testImage',
|
||||||
|
url: 'testUrl',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const globalAny: any = global
|
||||||
|
globalAny.caches = {
|
||||||
|
open: async () => cacheMock,
|
||||||
|
...cacheMock,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should put cache properly', async () => {
|
||||||
|
jest.spyOn(cacheMock, 'put')
|
||||||
|
await Cache.put(data, 'https://example.com')
|
||||||
|
expect(cacheMock.put).toHaveBeenCalledWith('https://example.com', expect.anything())
|
||||||
|
const call = mocked(cacheMock.put).mock.calls[0]
|
||||||
|
const response = JSON.parse(await (call[1] as Response).clone().text())
|
||||||
|
expect(response).toStrictEqual(data)
|
||||||
|
|
||||||
|
await expect(Cache.match('https://example.com')).resolves.toStrictEqual(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should match cache properly', async () => {
|
||||||
|
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify(data)))
|
||||||
|
const response = await Cache.match('https://example.com')
|
||||||
|
expect(response).toStrictEqual(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Should return undefined if not all data is present', async () => {
|
||||||
|
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify({ ...data, title: undefined })))
|
||||||
|
const response = await Cache.match('https://example.com')
|
||||||
|
expect(response).toBeUndefined()
|
||||||
|
})
|
28
functions/utils/cache.ts
Normal file
28
functions/utils/cache.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
interface Data {
|
||||||
|
title: string
|
||||||
|
image: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CACHE_NAME = 'functions-cache' as const
|
||||||
|
|
||||||
|
class Cache {
|
||||||
|
async match(request: string): Promise<Data | undefined> {
|
||||||
|
const cache = await caches.open(CACHE_NAME)
|
||||||
|
const response = await cache.match(request)
|
||||||
|
if (!response) return undefined
|
||||||
|
const data: Data = JSON.parse(await response.text())
|
||||||
|
if (!data.title || !data.image || !data.url) return undefined
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(data: Data, request: string) {
|
||||||
|
// Set max age to 1 week
|
||||||
|
const response = new Response(JSON.stringify(data))
|
||||||
|
response.headers.set('Cache-Control', 'max-age=604800')
|
||||||
|
const cache = await caches.open(CACHE_NAME)
|
||||||
|
await cache.put(request, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new Cache()
|
38
functions/utils/getRequest.test.ts
Normal file
38
functions/utils/getRequest.test.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as matchers from 'jest-extended'
|
||||||
|
expect.extend(matchers)
|
||||||
|
|
||||||
|
import { mocked } from '../../src/test-utils/mocked'
|
||||||
|
import Cache from './cache'
|
||||||
|
import getRequest from './getRequest'
|
||||||
|
|
||||||
|
jest.mock('./cache', () => ({
|
||||||
|
match: jest.fn(),
|
||||||
|
put: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
test('should call Cache.match before calling getData when request is not cached', async () => {
|
||||||
|
const url = 'https://example.com'
|
||||||
|
const getData = jest.fn().mockResolvedValueOnce({
|
||||||
|
title: 'test',
|
||||||
|
image: 'testImage',
|
||||||
|
url: 'testUrl',
|
||||||
|
})
|
||||||
|
await getRequest(Promise.resolve(new Response()), url, getData)
|
||||||
|
expect(Cache.match).toHaveBeenCalledWith(url)
|
||||||
|
expect(getData).toHaveBeenCalled()
|
||||||
|
expect(Cache.match).toHaveBeenCalledBefore(getData)
|
||||||
|
expect(Cache.put).toHaveBeenCalledAfter(getData)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getData should not be called when request is cached', async () => {
|
||||||
|
const url = 'https://example.com'
|
||||||
|
mocked(Cache.match).mockResolvedValueOnce({
|
||||||
|
title: 'test',
|
||||||
|
image: 'testImage',
|
||||||
|
url: 'testUrl',
|
||||||
|
})
|
||||||
|
const getData = jest.fn()
|
||||||
|
await getRequest(Promise.resolve(new Response()), url, getData)
|
||||||
|
expect(Cache.match).toHaveBeenCalledWith(url)
|
||||||
|
expect(getData).not.toHaveBeenCalled()
|
||||||
|
})
|
31
functions/utils/getRequest.ts
Normal file
31
functions/utils/getRequest.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { MetaTagInjector } from '../components/metaTagInjector'
|
||||||
|
import Cache from './cache'
|
||||||
|
|
||||||
|
export default async function getRequest(
|
||||||
|
res: Promise<Response>,
|
||||||
|
url: string,
|
||||||
|
getData: () => Promise<
|
||||||
|
| {
|
||||||
|
title: string
|
||||||
|
image: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const cachedData = await Cache.match(url)
|
||||||
|
if (cachedData) {
|
||||||
|
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
|
||||||
|
} else {
|
||||||
|
const data = await getData()
|
||||||
|
if (!data) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
await Cache.put(data, url)
|
||||||
|
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
@ -112,6 +112,7 @@
|
|||||||
"@walletconnect/types": "^2.8.6",
|
"@walletconnect/types": "^2.8.6",
|
||||||
"babel-jest": "^29.6.1",
|
"babel-jest": "^29.6.1",
|
||||||
"babel-plugin-istanbul": "^6.1.1",
|
"babel-plugin-istanbul": "^6.1.1",
|
||||||
|
"browser-cache-mock": "^0.1.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"concurrently": "^8.0.1",
|
"concurrently": "^8.0.1",
|
||||||
"cypress": "12.12.0",
|
"cypress": "12.12.0",
|
||||||
@ -123,6 +124,7 @@
|
|||||||
"hardhat": "^2.14.0",
|
"hardhat": "^2.14.0",
|
||||||
"jest": "^29.6.1",
|
"jest": "^29.6.1",
|
||||||
"jest-dev-server": "^9.0.0",
|
"jest-dev-server": "^9.0.0",
|
||||||
|
"jest-extended": "^4.0.1",
|
||||||
"jest-fail-on-console": "^3.1.1",
|
"jest-fail-on-console": "^3.1.1",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-styled-components": "^7.0.8",
|
"jest-styled-components": "^7.0.8",
|
||||||
|
33
yarn.lock
33
yarn.lock
@ -8446,6 +8446,11 @@ brorand@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||||
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||||
|
|
||||||
|
browser-cache-mock@^0.1.7:
|
||||||
|
version "0.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/browser-cache-mock/-/browser-cache-mock-0.1.7.tgz#4bfa4aa1022b1ce642e656e3cc8158deaaf30e12"
|
||||||
|
integrity sha512-+0bM5cjnj8Z8caAYBe1A9AA30juciI8fBHhw+aPBSSGL89UHPUcBA/E6CrQAecC635MEfx8qadTnaocEwE2q8Q==
|
||||||
|
|
||||||
browser-level@^1.0.1:
|
browser-level@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-1.0.1.tgz#36e8c3183d0fe1c405239792faaab5f315871011"
|
resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-1.0.1.tgz#36e8c3183d0fe1c405239792faaab5f315871011"
|
||||||
@ -13862,15 +13867,15 @@ jest-diff@^27.5.1:
|
|||||||
jest-get-type "^27.5.1"
|
jest-get-type "^27.5.1"
|
||||||
pretty-format "^27.5.1"
|
pretty-format "^27.5.1"
|
||||||
|
|
||||||
jest-diff@^29.6.1:
|
jest-diff@^29.0.0, jest-diff@^29.6.1:
|
||||||
version "29.6.1"
|
version "29.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.1.tgz#13df6db0a89ee6ad93c747c75c85c70ba941e545"
|
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46"
|
||||||
integrity sha512-FsNCvinvl8oVxpNLttNQX7FAq7vR+gMDGj90tiP7siWw1UdakWUGqrylpsYrpvj908IYckm5Y0Q7azNAozU1Kg==
|
integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
diff-sequences "^29.4.3"
|
diff-sequences "^29.4.3"
|
||||||
jest-get-type "^29.4.3"
|
jest-get-type "^29.4.3"
|
||||||
pretty-format "^29.6.1"
|
pretty-format "^29.6.2"
|
||||||
|
|
||||||
jest-docblock@^27.5.1:
|
jest-docblock@^27.5.1:
|
||||||
version "27.5.1"
|
version "27.5.1"
|
||||||
@ -13945,6 +13950,14 @@ jest-environment-node@^29.6.1:
|
|||||||
jest-mock "^29.6.1"
|
jest-mock "^29.6.1"
|
||||||
jest-util "^29.6.1"
|
jest-util "^29.6.1"
|
||||||
|
|
||||||
|
jest-extended@^4.0.1:
|
||||||
|
version "4.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-4.0.1.tgz#2315cb5914fc132e5acd07945bfaa01aac3947c2"
|
||||||
|
integrity sha512-KM6dwuBUAgy6QONuR19CGubZB9Hkjqvl/d5Yc/FXsdB8+gsGxB2VQ+NEdOrr95J4GMPeLnDoPOKyi6+mKCCnZQ==
|
||||||
|
dependencies:
|
||||||
|
jest-diff "^29.0.0"
|
||||||
|
jest-get-type "^29.0.0"
|
||||||
|
|
||||||
jest-fail-on-console@^3.1.1:
|
jest-fail-on-console@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/jest-fail-on-console/-/jest-fail-on-console-3.1.1.tgz#4ca0d0cc8f11675e8e9f52159a37a6602a7e6c09"
|
resolved "https://registry.yarnpkg.com/jest-fail-on-console/-/jest-fail-on-console-3.1.1.tgz#4ca0d0cc8f11675e8e9f52159a37a6602a7e6c09"
|
||||||
@ -13975,7 +13988,7 @@ jest-get-type@^27.5.1:
|
|||||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
|
||||||
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
|
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
|
||||||
|
|
||||||
jest-get-type@^29.4.3:
|
jest-get-type@^29.0.0, jest-get-type@^29.4.3:
|
||||||
version "29.4.3"
|
version "29.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
|
||||||
integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
|
integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
|
||||||
@ -17320,10 +17333,10 @@ pretty-format@^28.1.3:
|
|||||||
ansi-styles "^5.0.0"
|
ansi-styles "^5.0.0"
|
||||||
react-is "^18.0.0"
|
react-is "^18.0.0"
|
||||||
|
|
||||||
pretty-format@^29.6.1:
|
pretty-format@^29.6.1, pretty-format@^29.6.2:
|
||||||
version "29.6.1"
|
version "29.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.1.tgz#ec838c288850b7c4f9090b867c2d4f4edbfb0f3e"
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47"
|
||||||
integrity sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==
|
integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jest/schemas" "^29.6.0"
|
"@jest/schemas" "^29.6.0"
|
||||||
ansi-styles "^5.0.0"
|
ansi-styles "^5.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user