diff --git a/.env.example b/.env.example index fff2497..4249a7e 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,8 @@ APP_SECRET= SPACE_ID= COOKIE_PASSWORD= COOKIE_NAME= -NEXT_PUBLIC_SITE_URL= \ No newline at end of file +SITE_URL= +REDIS_HOST= +REDIS_PASSWORD= +REDIS_PORT= +NEXT_PUBLIC_SITE_URL= diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index 0d147f9..eaa4cf0 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -7,7 +7,7 @@ name: Deploy Next.js site to Pages on: # Runs on pushes targeting the default branch push: - branches: + branches: - github-pages # Allows you to run this workflow manually from the Actions tab @@ -26,6 +26,20 @@ concurrency: cancel-in-progress: false jobs: + services: + # Label used to access the service container + redis: + # Docker Hub image + image: redis + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps port 6379 on service container to the host + - 6379:6379 # Build job build: runs-on: ubuntu-latest @@ -74,13 +88,16 @@ jobs: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- - name: Install dependencies run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} - - name: 'Create env file' + - name: "Create env file" run: | touch .env echo APP_ID=${{ secrets.APP_ID }} >> .env echo APP_SECRET=${{ secrets.APP_SECRET }} >> .env echo SPACE_ID=${{ secrets.SPACE_ID }} >> .env echo NEXT_PUBLIC_SITE_URL=${{ vars.SITE_URL }} >> .env + echo REDIS_HOST=redis >> .env + echo REDIS_PORT=6379 >> .env + echo REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} >> .env - name: Build with Next.js run: ${{ steps.detect-package-manager.outputs.runner }} next build - name: Upload artifact @@ -98,4 +115,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 14b9091..01cb6fc 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,18 @@ Run the following command to copy the default example and replace with your own cp .env.example .env ``` +### Redis + +We use Redis optimized for fast reads locally. We do not need to use Redis in the production environment because we are using static exports. + +First, install Redis locally by running brew install redis. +Start the Redis service by running redis-server. You will see the Redis port number at this point. +Replace , , and in the .env file with your local Redis host address, port number, and password, respectively. + +- : If running on the local machine, you can use 127.0.0.1 or localhost. +- : The default Redis port number is 6379. If you have not changed Redis's default configuration, you can use 6379. +- : If your Redis server has password protection enabled, use the set password. If no password is set, you can leave it blank. + ### Run the development server ```bash diff --git a/configuration.ts b/configuration.ts new file mode 100644 index 0000000..abe3bbb --- /dev/null +++ b/configuration.ts @@ -0,0 +1,7 @@ +export default { + redis: { + host: process.env.REDIS_HOST || "", + password: process.env.REDIS_PASSWORD || "", + port: process.env.REDIS_PORT || "", + }, +}; diff --git a/lib/api.ts b/lib/api.ts index 01a1061..e1f0af7 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,10 +1,22 @@ import { getTenantAccessToken } from "@/services/get-tenant-access-token"; import { backOff } from "exponential-backoff"; - +import { createRedisInstance } from "./redis"; +// get redis instance +const redis = createRedisInstance(); export const fetcher = async ( url: string, next?: NextFetchRequestConfig ): Promise => { + const startTime = new Date(); + let elapsedTime; + // try fetch cached data + const cached = await redis.get(url); + if (cached) { + elapsedTime = new Date().getTime() - startTime.getTime(); + console.log(url, "=======yyyyyyyyyy=====", elapsedTime); + return JSON.parse(cached) as any; + } + // fetch fresh data const tenantAccessToken = await getTenantAccessToken(); const res = await backOff(() => fetch(url, { @@ -15,6 +27,15 @@ export const fetcher = async ( next: next || { revalidate: 6000 }, }) ); - - return await res.json(); + elapsedTime = new Date().getTime() - startTime.getTime(); + console.log(url, "=======xxxxxxxxxx=====", elapsedTime); + const result = await res.json(); + // cache data setting an expiry of 1 hour + // this means that the cached data will remain alive for 60 minutes + // after that, we'll get fresh data from the DB + const MAX_AGE = 60_000 * 60; // 1 hour + const EXPIRY_MS = `PX`; // milliseconds + // cache data + await redis.set(url, JSON.stringify(result), EXPIRY_MS, MAX_AGE); + return result; }; diff --git a/lib/redis.ts b/lib/redis.ts new file mode 100644 index 0000000..9727936 --- /dev/null +++ b/lib/redis.ts @@ -0,0 +1,47 @@ +import Redis, { RedisOptions } from "ioredis"; +import configuration from "../configuration"; + +function getRedisConfiguration(): { + port: string; + host: string; + password: string; +} { + return configuration.redis; +} + +export function createRedisInstance(config = getRedisConfiguration()) { + try { + const options: RedisOptions = { + host: config.host, + lazyConnect: true, + showFriendlyErrorStack: true, + enableAutoPipelining: true, + maxRetriesPerRequest: 0, + retryStrategy: (times: number) => { + if (times > 3) { + throw new Error(`[Redis] Could not connect after ${times} attempts`); + } + + return Math.min(times * 200, 1000); + }, + }; + + if (config.port) { + options.port = +config.port; + } + + if (config.password) { + options.password = config.password; + } + + const redis = new Redis(options); + + redis.on("error", (error: unknown) => { + console.warn("[Redis] Error connecting", error); + }); + + return redis; + } catch (e) { + throw new Error(`[Redis] Could not create a Redis instance`); + } +} diff --git a/lib/utils.ts b/lib/utils.ts index 37047e5..f2fb3ae 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -100,7 +100,10 @@ export async function getFileByFolderToken(folderNodes?: NodesItem[]) { await getFileByFolderToken(child.items); } } - data.items = items; + if (data) { + data.items = items; + } + return data as NodesData; } diff --git a/package-lock.json b/package-lock.json index 7974d8b..0395c0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "date-fns": "^3.6.0", "exponential-backoff": "^3.1.1", "github-slugger": "^2.0.0", + "ioredis": "^5.4.1", "iron-session": "^8.0.2", "lucide-react": "^0.408.0", "nanoid": "^5.0.7", @@ -482,6 +483,11 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2287,6 +2293,14 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", @@ -2833,7 +2847,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2927,6 +2940,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4166,6 +4187,29 @@ "loose-envify": "^1.0.0" } }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/iron-session": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.2.tgz", @@ -4755,6 +4799,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4875,8 +4929,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -6270,6 +6323,25 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -6581,6 +6653,11 @@ "node": ">=0.10.0" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", diff --git a/package.json b/package.json index a4482d9..b5edc5b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "next dev", - "build": "rm -rf .next && next build", + "build": "next build", "start": "next start", "lint": "next lint" }, @@ -25,6 +25,7 @@ "date-fns": "^3.6.0", "exponential-backoff": "^3.1.1", "github-slugger": "^2.0.0", + "ioredis": "^5.4.1", "iron-session": "^8.0.2", "lucide-react": "^0.408.0", "nanoid": "^5.0.7", diff --git a/services/get-child-nodes.ts b/services/get-child-nodes.ts index 804a53d..02a1fe6 100644 --- a/services/get-child-nodes.ts +++ b/services/get-child-nodes.ts @@ -17,6 +17,5 @@ export async function getChildNodes(parentId: string) { `https://open.larksuite.com/open-apis/wiki/v2/spaces/${process.env.SPACE_ID}/nodes?parent_node_token=${parentId}`, { tags: [parentId] } ); - return schema.parse(res); }