From cdbbf4bda9e3b1bf4ff3d88237cd01b793ca52b8 Mon Sep 17 00:00:00 2001 From: Alexa Grey Date: Fri, 10 Oct 2025 09:53:58 -0700 Subject: [PATCH 1/2] [E] Add redis/valkey caching --- .dockerignore | 2 + Dockerfile | 1 + README.md | 36 ++++++++++- bin/build-local-image | 7 ++ bin/run-local-against-local-api | 13 ++++ bin/run-local-against-sandbox | 13 ++++ customized-cache-handler.cjs | 64 +++++++++++++++++++ docker-compose.yml | 21 ++++++ docker/local.env | 26 ++++++++ docker/sandbox.env | 26 ++++++++ docker/valkey/valkey.conf | 3 + next.config.js | 5 ++ package.json | 2 + yarn.lock | 110 ++++++++++++++++++++++++++++++-- 14 files changed, 320 insertions(+), 9 deletions(-) create mode 100755 bin/build-local-image create mode 100755 bin/run-local-against-local-api create mode 100755 bin/run-local-against-sandbox create mode 100644 customized-cache-handler.cjs create mode 100644 docker-compose.yml create mode 100644 docker/local.env create mode 100644 docker/sandbox.env create mode 100644 docker/valkey/valkey.conf diff --git a/.dockerignore b/.dockerignore index 7bfa853d9..1fca89f2d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,6 @@ .env.local .github .git +docker/local.env +docker/sandbox.env node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 889e260f4..5d5d8aefe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ ENV NEXT_PUBLIC_FE_URL="NP--FE--URL" ENV NEXT_PUBLIC_ADMIN_URL="NP--ADMIN--URL" ENV NEXT_PUBLIC_API_URL="NP--API--URL" ENV NEXT_PUBLIC_TUS_URL="NP--TUS--URL" +ENV REDIS_CACHE="enabled" COPY --from=deps /app/node_modules ./node_modules COPY . ./ diff --git a/README.md b/README.md index 020facebc..fa88cd37f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# NGLP Frontend +# Meru Frontend ## Development Environment ```sh yarn install -yarn run dev # to start dev client +yarn dev -p 3001 # to start dev client ``` That's it! Now go to http://localhost:3001/ @@ -40,6 +40,38 @@ Header.tsx // Defines the component styles.ts // Styled components ``` +### Cache Testing + +`meru-frontend` is designed to use Valkey for caching when running in production. To test this, you may also run it locally with the provided docker-compose.yml and some scripts in `bin`. + +First, ensure that Valkey is running locally on port 36379: + +```bash +docker compose up -d +``` + +Then, build a local version of the image. You will need to do this on any change to the application code. Since we need to test how it works in production, there's no live-reloading of changes. Once it has been built, you can run it against one of two versions of the API with some scripts. + +```bash +bin/build-local-image +``` + +To run against the local API, which supports testing revalidation: + +```bash +bin/run-local-against-local-api +``` + +This will be available on [localhost:14700](http://localhost:14700). + +To run against the Sandbox API, which _does not_ support testing revalidation: + +```bash +bin/run-local-against-sandbox +``` + +This will be available on [localhost:14800](http://localhost:14800). + ### Browser support - Edge (Chromium latest) diff --git a/bin/build-local-image b/bin/build-local-image new file mode 100755 index 000000000..ae3269cdb --- /dev/null +++ b/bin/build-local-image @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# This image will build a local version of the meru-frontend image +# that can be used for local development and testing. +# You can actually run it with the other scripts in this directory. + +docker buildx build --platform linux/amd64 -t meru/meru-frontend-local . \ No newline at end of file diff --git a/bin/run-local-against-local-api b/bin/run-local-against-local-api new file mode 100755 index 000000000..a93625c65 --- /dev/null +++ b/bin/run-local-against-local-api @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# The purpose of this script is to run a locally-built docker +# image of the meru-frontend against a locally-running meru API. +# Make sure that you have built the local version of the frontend +# with `bin/build-local-image`. + +# Ensure valkey is running +docker compose up -d + +docker run -it --rm -p 14700:3000 -w /app \ + --env-file docker/local.env \ + meru/meru-frontend-local \ No newline at end of file diff --git a/bin/run-local-against-sandbox b/bin/run-local-against-sandbox new file mode 100755 index 000000000..3366812ba --- /dev/null +++ b/bin/run-local-against-sandbox @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# The purpose of this script is to run a locally-built docker +# image of the meru-frontend against the sandbox version of the meru API. +# Make sure that you have built the local version of the frontend +# with `bin/build-local-image`. + +# Ensure valkey is running +docker compose up -d + +docker run -it --rm -p 14800:3000 -w /app \ + --env-file docker/sandbox.env \ + meru/meru-frontend-local \ No newline at end of file diff --git a/customized-cache-handler.cjs b/customized-cache-handler.cjs new file mode 100644 index 000000000..735e6ca08 --- /dev/null +++ b/customized-cache-handler.cjs @@ -0,0 +1,64 @@ +const { RedisStringsHandler } = require("@trieb.work/nextjs-turbo-redis-cache"); + +// Singleton pattern to ensure only one instance of RedisStringsHandler +let cachedHandler; + +function parseEnvInt(value, defaultValue) { + const parsed = parseInt(value, 10); + + return isNaN(parsed) ? defaultValue : parsed; +} + +function socketOptionsFor(redisURL) { + if (!redisURL || !redisURL.startsWith('rediss://')) { + return null; + } + + return { + tls: true, + rejectUnauthorized: false, + }; +} + +const REDIS_DB = parseEnvInt(process.env.REDIS_DB, 1); + +module.exports = class CustomizedCacheHandler { + constructor() { + // We need this class to be available during production / docker builds, + // but we only want to connect to Redis when actually running the server. + if (!cachedHandler && process.env.REDIS_URL) { + const socketOptions = socketOptionsFor(process.env.REDIS_URL); + + cachedHandler = new RedisStringsHandler({ + // https://github.com/trieb-work/nextjs-turbo-redis-cache?tab=readme-ov-file#available-options + database: REDIS_DB, + keyPrefix: 'meru-client:', + timeoutMs: 2_000, + revalidateTagQuerySize: 500, + sharedTagsKey: '__sharedTags__', + avgResyncIntervalMs: 10_000 * 60, + redisGetDeduplication: false, + inMemoryCachingTime: 3000, + defaultStaleAge: 1209600, + estimateExpireAge: (staleAge) => staleAge * 2, + socketOptions, + }); + } + } + + get(...args) { + return cachedHandler?.get(...args) ?? null; + } + + set(...args) { + return cachedHandler?.set(...args) ?? null; + } + + revalidateTag(...args) { + return cachedHandler?.revalidateTag(...args) ?? null; + } + + resetRequestCache(...args) { + return cachedHandler?.resetRequestCache(...args) ?? null; + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ee1bd65ab --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + redis: + image: valkey/valkey:8.0 + environment: + - "ALLOW_EMPTY_PASSWORD=yes" + logging: + driver: json-file + options: + max-size: "10m" + max-file: "10" + restart: unless-stopped + ports: + - "36379:6379" + volumes: + - ./docker/valkey:/usr/local/etc/valkey + command: ["valkey-server", "/usr/local/etc/valkey/valkey.conf"] + healthcheck: + test: ["CMD", "valkey-cli", "--raw", "incr", "ping"] + interval: 30s + timeout: 5s + retries: 5 diff --git a/docker/local.env b/docker/local.env new file mode 100644 index 000000000..fa4077a3d --- /dev/null +++ b/docker/local.env @@ -0,0 +1,26 @@ +AUTH_SECRET=12345secret +NODE_ENV=production + +NEXT_PUBLIC_API_URL=http://host.docker.internal:6222/graphql +NEXT_PUBLIC_TUS_URL=http://host.docker.internal:6222/files + +# Keycloak +NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=meru-public +NEXT_KEYCLOAK_CLIENT_SECRET=12345secret +NEXT_PUBLIC_KEYCLOAK_REALM=meru +NEXT_PUBLIC_KEYCLOAK_URL=https://id.sandbox.meru.host + +# Secret token for revalidation requests +REVALIDATE_SECRET=revalidatenow + +REDIS_URL=redis://host.docker.internal:36379 +REDIS_DB=2 + +# Other Next.js vars +NEXT_PUBLIC_FE_URL=http://localhost:14700 +NEXT_PUBLIC_VERSION=1.0.0 +NEXT_PUBLIC_ORDER_PATH_OPTIONS="props.volume.sortable_number,props.sortable_number,props.id" + +# Optional +NEXT_PUBLIC_SITEMAP_CACHE_MAXAGE=86400 +NEXT_PUBLIC_SITEMAP_CACHE_REVALIDATE=59 \ No newline at end of file diff --git a/docker/sandbox.env b/docker/sandbox.env new file mode 100644 index 000000000..4f6fa792b --- /dev/null +++ b/docker/sandbox.env @@ -0,0 +1,26 @@ +AUTH_SECRET=12345secret +NODE_ENV=production + +NEXT_PUBLIC_API_URL=https://api.sandbox.meru.host/graphql +NEXT_PUBLIC_TUS_URL=https://api.sandbox.meru.host/files + +# Keycloak +NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=meru-public +NEXT_KEYCLOAK_CLIENT_SECRET=12345secret +NEXT_PUBLIC_KEYCLOAK_REALM=meru +NEXT_PUBLIC_KEYCLOAK_URL=https://id.sandbox.meru.host + +# Secret token for revalidation requests +REVALIDATE_SECRET=revalidatenow + +REDIS_URL=redis://host.docker.internal:36379 +REDIS_DB=3 + +# Other Next.js vars +NEXT_PUBLIC_FE_URL=http://localhost:14800 +NEXT_PUBLIC_VERSION=1.0.0 +NEXT_PUBLIC_ORDER_PATH_OPTIONS="props.volume.sortable_number,props.sortable_number,props.id" + +# Optional +NEXT_PUBLIC_SITEMAP_CACHE_MAXAGE=86400 +NEXT_PUBLIC_SITEMAP_CACHE_REVALIDATE=59 \ No newline at end of file diff --git a/docker/valkey/valkey.conf b/docker/valkey/valkey.conf new file mode 100644 index 000000000..a93fab369 --- /dev/null +++ b/docker/valkey/valkey.conf @@ -0,0 +1,3 @@ +maxmemory-policy allkeys-lru + +notify-keyspace-events Exe \ No newline at end of file diff --git a/next.config.js b/next.config.js index 74175c006..7596d1e9d 100644 --- a/next.config.js +++ b/next.config.js @@ -105,6 +105,11 @@ const nextConfig = { }, }; +if (process.env.NODE_ENV === "production" || process.env.REDIS_CACHE === "enabled") { + nextConfig.cacheHandler = require.resolve("./customized-cache-handler.cjs"); + nextConfig.cacheMaxMemorySize = 0; +} + const withMDX = createMDX(); module.exports = withMDX(nextConfig); diff --git a/package.json b/package.json index 6bb0feab4..16126b5e3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@mdx-js/mdx": "^3.1.0", "@next/mdx": "^15.0.0", "@spissvinkel/alea": "^1.2.1", + "@trieb.work/nextjs-turbo-redis-cache": "^1.8.1", "@types/mdx": "^2.0.13", "chunk-text": "^2.0.1", "classnames": "^2.5.1", @@ -44,6 +45,7 @@ "react-pdf": "^10.1.0", "react-relay": "^16.2.0", "reakit": "^1.3.11", + "redis": "4.7.0", "regenerator-runtime": "^0.14.1", "rehype-raw": "^7.0.0", "relay-compiler": "^16.2.0", diff --git a/yarn.lock b/yarn.lock index 9c2149590..8988f4870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2215,6 +2215,62 @@ __metadata: languageName: node linkType: hard +"@redis/bloom@npm:1.2.0": + version: 1.2.0 + resolution: "@redis/bloom@npm:1.2.0" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 10c0/7dde8e67188164e96226c8a5c78ebd2801f1662947371e78fb95fb180c1e9ddff8d237012eb5e9182775be61cb546f67f759927cdaee0d178d863ee290e1fb27 + languageName: node + linkType: hard + +"@redis/client@npm:1.6.0": + version: 1.6.0 + resolution: "@redis/client@npm:1.6.0" + dependencies: + cluster-key-slot: "npm:1.1.2" + generic-pool: "npm:3.9.0" + yallist: "npm:4.0.0" + checksum: 10c0/c80a01b4f72d32284515dac6d1aefe0e9c881d08b8db33281f87b51650c1c116b18074a29ca81599d15dccb37b29eef9b26a75a5755150ae27d163e680c34bf6 + languageName: node + linkType: hard + +"@redis/graph@npm:1.1.1": + version: 1.1.1 + resolution: "@redis/graph@npm:1.1.1" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 10c0/64199db2cb3669c4911af8aba3b7116c4c2c1df37ca74b2a65555e62c863935a0cea74bc41bd92acf2e551074eb2a30c75f54a9f439b40e0f9bb67fc5fb66614 + languageName: node + linkType: hard + +"@redis/json@npm:1.0.7": + version: 1.0.7 + resolution: "@redis/json@npm:1.0.7" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 10c0/cef473711d66f7568a16edbd728acca7d237cfeaa15e0326b5b628dfab4afc0c76c7354e7f8efad6ecc64a1cb774e4aa060ee46497b633e18ba0a2f0aace1cc4 + languageName: node + linkType: hard + +"@redis/search@npm:1.2.0": + version: 1.2.0 + resolution: "@redis/search@npm:1.2.0" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 10c0/01d57ac10d2c5698e04e4a2f945440db3087e8834643ca950c099879dbcd77526604ca6f5c2ee883dfd4b337b0a24cb7d81ac56845aa83f89a4f161362a08dc6 + languageName: node + linkType: hard + +"@redis/time-series@npm:1.1.0": + version: 1.1.0 + resolution: "@redis/time-series@npm:1.1.0" + peerDependencies: + "@redis/client": ^1.0.0 + checksum: 10c0/503d0d5cbc9113d26666bb7b4dea57619badbcdfeee0369abf647250f26c5482ed5827c83f88f9f0cf22e021e3e7cb562459669d733fac05652972e208d6ba0f + languageName: node + linkType: hard + "@repeaterjs/repeater@npm:^3.0.4, @repeaterjs/repeater@npm:^3.0.6": version: 3.0.6 resolution: "@repeaterjs/repeater@npm:3.0.6" @@ -2269,6 +2325,16 @@ __metadata: languageName: node linkType: hard +"@trieb.work/nextjs-turbo-redis-cache@npm:^1.8.1": + version: 1.8.1 + resolution: "@trieb.work/nextjs-turbo-redis-cache@npm:1.8.1" + peerDependencies: + next: ">=15.0.3 <= 15.3.2" + redis: 4.7.0 + checksum: 10c0/cfa1700768eb4ae3da7e17ce05352261669fd72974645817b059a7c40da0a1342aaf3820bff31c86f4f913db7e76a57ef30ec2024e1e13f38eb4a21f57445ab9 + languageName: node + linkType: hard + "@types/chunk-text@npm:^1.0.2": version: 1.0.2 resolution: "@types/chunk-text@npm:1.0.2" @@ -3430,6 +3496,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:1.1.2": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 + languageName: node + linkType: hard + "collapse-white-space@npm:^2.0.0": version: 2.1.0 resolution: "collapse-white-space@npm:2.1.0" @@ -4836,6 +4909,13 @@ __metadata: languageName: node linkType: hard +"generic-pool@npm:3.9.0": + version: 3.9.0 + resolution: "generic-pool@npm:3.9.0" + checksum: 10c0/6b314d0d71170d5cbaf7162c423f53f8d6556b2135626a65bcdc03c089840b0a2f59eeb2d907939b8200e945eaf71ceb6630426f22d2128a1d242aec4b232aa7 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -6592,6 +6672,7 @@ __metadata: "@next/eslint-plugin-next": "npm:^15.5.3" "@next/mdx": "npm:^15.0.0" "@spissvinkel/alea": "npm:^1.2.1" + "@trieb.work/nextjs-turbo-redis-cache": "npm:^1.8.1" "@types/chunk-text": "npm:^1.0.2" "@types/lodash": "npm:^4.14.202" "@types/mdx": "npm:^2.0.13" @@ -6643,6 +6724,7 @@ __metadata: react-pdf: "npm:^10.1.0" react-relay: "npm:^16.2.0" reakit: "npm:^1.3.11" + redis: "npm:4.7.0" regenerator-runtime: "npm:^0.14.1" rehype-raw: "npm:^7.0.0" relay-compiler: "npm:^16.2.0" @@ -8405,6 +8487,20 @@ __metadata: languageName: node linkType: hard +"redis@npm:4.7.0": + version: 4.7.0 + resolution: "redis@npm:4.7.0" + dependencies: + "@redis/bloom": "npm:1.2.0" + "@redis/client": "npm:1.6.0" + "@redis/graph": "npm:1.1.1" + "@redis/json": "npm:1.0.7" + "@redis/search": "npm:1.2.0" + "@redis/time-series": "npm:1.1.0" + checksum: 10c0/a05632a58adbcaa4566238073cd6d00ed008522d2ef015a31aaef200c184a4eff4fa007c514eda91dda1e1205350b5901d0c7b58824dbfa593feb81a0087bf4d + languageName: node + linkType: hard + "reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9": version: 1.0.10 resolution: "reflect.getprototypeof@npm:1.0.10" @@ -10239,6 +10335,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:4.0.0, yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + languageName: node + linkType: hard + "yallist@npm:^3.0.2": version: 3.1.1 resolution: "yallist@npm:3.1.1" @@ -10246,13 +10349,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a - languageName: node - linkType: hard - "yallist@npm:^5.0.0": version: 5.0.0 resolution: "yallist@npm:5.0.0" From 6535e90ae10ecb6c282c7f352cf41106646bfba6 Mon Sep 17 00:00:00 2001 From: Alexa Grey Date: Fri, 10 Oct 2025 09:55:23 -0700 Subject: [PATCH 2/2] [C] Re-enable caching --- app/[frontend]/(pages)/layout.tsx | 2 +- app/[frontend]/layout.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/[frontend]/(pages)/layout.tsx b/app/[frontend]/(pages)/layout.tsx index a5cb159d9..38ae38437 100644 --- a/app/[frontend]/(pages)/layout.tsx +++ b/app/[frontend]/(pages)/layout.tsx @@ -15,7 +15,7 @@ import { BasePageParams } from "@/types/page"; import ProgressBar from "@/components/atomic/loading/ProgressBar"; import generateSiteMetadata from "./_metadata/site"; -export const revalidate = 0; +export const revalidate = 43200; export async function generateMetadata( props: BasePageParams, diff --git a/app/[frontend]/layout.tsx b/app/[frontend]/layout.tsx index 370ec7456..a13ed0cfd 100644 --- a/app/[frontend]/layout.tsx +++ b/app/[frontend]/layout.tsx @@ -11,7 +11,7 @@ import { layoutThemeQuery as Query } from "@/relay/layoutThemeQuery.graphql"; import { getThemeClasses } from "@/styles/theme-helpers"; import type { PropsWithChildren } from "react"; -export const revalidate = 0; +export const revalidate = 43200; export default async function RootLayout({ children }: PropsWithChildren) { const { data } = await fetchQuery(query, {});