From 8676fa9e6ac30d398ca50e87747390103ad6dbac Mon Sep 17 00:00:00 2001 From: Henrik Breines <46938312+henb13@users.noreply.github.com> Date: Tue, 31 Oct 2023 00:12:22 +0100 Subject: [PATCH] feat: start api refactor --- client/package.json | 2 +- server/.env.example | 3 ++ server/api/index.js | 88 +++++++++++++++++++------------- server/lib/getSpotifyEpisodes.js | 25 ++++----- server/lib/spotify-client.js | 33 ++++++++++++ server/package-lock.json | 20 ++++++++ server/package.json | 1 + 7 files changed, 120 insertions(+), 52 deletions(-) create mode 100644 server/lib/spotify-client.js diff --git a/client/package.json b/client/package.json index 40b6850..6ff23fe 100644 --- a/client/package.json +++ b/client/package.json @@ -21,7 +21,7 @@ "web-vitals": "^3.5.0" }, "scripts": { - "start": "vite", + "start": "vite --open", "build": "tsc && vite build", "serve": "vite preview", "test": "vitest", diff --git a/server/.env.example b/server/.env.example index 48d848d..2108d06 100644 --- a/server/.env.example +++ b/server/.env.example @@ -4,6 +4,9 @@ SPOTIFY_CLIENT_SECRET=YOUR_SECRET SUPABASE_PROJECT_URL=YOUR_SUPABASE_PROJECT_URL SUPABASE_API_KEY=YOUR_SUPABASE_API_KEY +SUPABASE_PROJECT_URL_DEV=YOUR_SUPABASE_DEV_PROJECT_URL +SUPABASE_API_KEY_DEV=YOUR_SUPABASE_DEV_API_KEY + PORT=3000 # How often to compare with Spotify API in minutes diff --git a/server/api/index.js b/server/api/index.js index 23995d2..5f19c17 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -2,61 +2,77 @@ const express = require("express"); const router = express.Router(); const DB = require("../db/db"); const pool = require("../db/connect"); -const { mockResponse } = require("./__mocks__/mockResponse"); +const NodeCache = require("node-cache"); router.use(express.json()); require("dotenv").config(); -let missingEpisodesCache; -let shortenedEpisodesCache; -let lastCheckedCache; +const CRON_INTERVAL = parseInt(process.env.CRON_INTERVAL, 10) ?? 30; -const { CRON_INTERVAL, USE_MOCK_DATA, NODE_ENV } = process.env; -const isDev = NODE_ENV === "development"; +const cache = new NodeCache({ stdTTL: CRON_INTERVAL * 60 }); + +const KEYS = { + missingEpisodes: "missing-episodes", + shortenedEpisodes: "shortened-episodes", + lastChecked: "last-checked", +}; //TODO: Change when client in prod -const allowOrigin = isDev ? "http://localhost:3000" : "https://not-yet-prod-domain.com"; +const allowOrigin = + process.env.NODE_ENV === "development" + ? "http://localhost:3000" + : "https://not-yet-prod-domain.com"; router.get("/api/episodes", async (_, res) => { - if (isDev && USE_MOCK_DATA === "true") { - return res.json(mockResponse); - } - - const timeSinceLastCheckedDbInMins = - lastCheckedCache && (Date.now() - lastCheckedCache) / 1000 / 60; + console.info("request fired"); - const cacheTimeLeftSecs = Math.floor((CRON_INTERVAL - timeSinceLastCheckedDbInMins) * 60); - const buffer = 120; + const maxAgeMs = Math.min( + cache.getTtl(KEYS.missingEpisodes), + cache.getTtl(KEYS.shortenedEpisodes) + ); + const maxAge = maxAgeMs ? Math.round((new Date(maxAgeMs).getTime() - Date.now()) / 1000) : 0; res.header({ - "cache-control": `no-transform, max-age=${cacheTimeLeftSecs + buffer || 1}`, + "cache-control": `no-transform, max-age=${maxAge}`, "Access-Control-Allow-Origin": allowOrigin, }); - if ( - !missingEpisodesCache || - !shortenedEpisodesCache || - timeSinceLastCheckedDbInMins > CRON_INTERVAL - ) { - await (async () => { - const client = await pool.connect(); - const db = DB(client); - try { - missingEpisodesCache = await db.getMissingEpisodes(); - shortenedEpisodesCache = await db.getShortenedEpisodes(); - lastCheckedCache = await db.getLastChecked(); - console.info("db queried and cache updated"); - } finally { - client.release(); + const missingCacheExists = cache.has(KEYS.missingEpisodes); + const shortenedCacheExists = cache.has(KEYS.shortenedEpisodes); + const lastCheckedExists = cache.has(KEYS.lastChecked); + + if (!missingCacheExists || !shortenedCacheExists || !lastCheckedExists) { + const client = await pool.connect(); + const db = DB(client); + + try { + if (!missingCacheExists) { + const missingEpisodes = await db.getMissingEpisodes(); + cache.set(KEYS.missingEpisodes, missingEpisodes); } - })().catch((err) => console.error(err.message)); + + if (!shortenedCacheExists) { + const shortenedEpisodes = await db.getshortenedEpisodes(); + cache.set(KEYS.shortenedEpisodes, shortenedEpisodes); + } + + if (!lastCheckedExists) { + const lastChecked = await db.getlastChecked(); + cache.set(KEYS.lastChecked, lastChecked); + } + + console.info("db queried and cache updated"); + } catch (error) { + console.error(error.message); + } finally { + client.release(); + } } - console.info("request fired"); res.json({ - missingEpisodes: missingEpisodesCache, - shortenedEpisodes: shortenedEpisodesCache, - lastCheckedInMs: lastCheckedCache, + missingEpisodes: cache.get(KEYS.missingEpisodes), + shortenedEpisodes: cache.get(KEYS.shortenedEpisodes), + lastCheckedInMs: cache.get(KEYS.lastChecked), }); }); diff --git a/server/lib/getSpotifyEpisodes.js b/server/lib/getSpotifyEpisodes.js index e6ff6bf..089a877 100644 --- a/server/lib/getSpotifyEpisodes.js +++ b/server/lib/getSpotifyEpisodes.js @@ -1,22 +1,17 @@ -/* eslint-disable no-undef */ -const SpotifyWebApi = require("spotify-web-api-node"); -require("dotenv").config(); +const initializeSpotifyClient = require("./spotify-client"); const JRE_SHOW_ID = "4rOoJ6Egrf8K2IrywzwOMk"; -async function getSpotifyEpisodes() { - try { - const spotifyApi = new SpotifyWebApi({ - clientId: process.env.SPOTIFY_CLIENT_ID, - clientSecret: process.env.SPOTIFY_CLIENT_SECRET, - }); +let spotifyClient; - const tokenData = await spotifyApi.clientCredentialsGrant(); - console.info(`The access token expires in ${tokenData.body["expires_in"]}`); - spotifyApi.setAccessToken(tokenData.body["access_token"]); +async function getSpotifyEpisodes() { + if (!spotifyClient) { + spotifyClient = await initializeSpotifyClient(); + } + try { const spotifyEpisodes = []; - const episodes = await spotifyApi.getShowEpisodes(JRE_SHOW_ID, { + const episodes = await spotifyClient.getShowEpisodes(JRE_SHOW_ID, { market: "US", limit: 50, offset: spotifyEpisodes.length, @@ -33,7 +28,7 @@ async function getSpotifyEpisodes() { const totalEpisodes = episodes.body.total; while (spotifyEpisodes.length < totalEpisodes) { - const episodes = await spotifyApi.getShowEpisodes(JRE_SHOW_ID, { + const episodes = await spotifyClient.getShowEpisodes(JRE_SHOW_ID, { market: "US", limit: 50, offset: spotifyEpisodes.length, @@ -50,7 +45,7 @@ async function getSpotifyEpisodes() { return spotifyEpisodes; } catch (err) { - console.error(err.message); + console.error("something went wrong fetching from Spotify: ", err.message); } } diff --git a/server/lib/spotify-client.js b/server/lib/spotify-client.js new file mode 100644 index 0000000..9c3e9a0 --- /dev/null +++ b/server/lib/spotify-client.js @@ -0,0 +1,33 @@ +const SpotifyWebApi = require("spotify-web-api-node"); +require("dotenv").config(); + +async function initializeSpotifyClient() { + try { + const spotifyClient = new SpotifyWebApi({ + clientId: process.env.SPOTIFY_CLIENT_ID, + clientSecret: process.env.SPOTIFY_CLIENT_SECRET, + }); + + const data = await spotifyClient.clientCredentialsGrant(); + const accessToken = data.body["access_token"]; + const refreshToken = data.body["refresh_token"]; + const expiresIn = data.body["expires_in"]; + + spotifyClient.setAccessToken(accessToken); + spotifyClient.setRefreshToken(refreshToken); + + setInterval(async () => { + const data = await spotifyClient.refreshAccessToken(); + const accessToken = data.body["access_token"]; + spotifyClient.setAccessToken(accessToken); + + console.log("The access token has been refreshed!"); + }, expiresIn - 60 * 1000); + + spotifyClient.setAccessToken(data.body["access_token"]); + } catch (error) { + return null; + } +} + +module.exports = { initializeSpotifyClient }; diff --git a/server/package-lock.json b/server/package-lock.json index 163fcfe..2a65c4f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,6 +15,7 @@ "express-rate-limit": "^7.1.3", "express-slow-down": "^2.0.0", "helmet": "^7.0.0", + "node-cache": "^5.1.2", "node-schedule": "^2.1.1", "npm-run-all": "^4.1.5", "pg": "^8.11.3", @@ -1954,6 +1955,14 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4686,6 +4695,17 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/server/package.json b/server/package.json index de08946..b54764b 100644 --- a/server/package.json +++ b/server/package.json @@ -23,6 +23,7 @@ "express-rate-limit": "^7.1.3", "express-slow-down": "^2.0.0", "helmet": "^7.0.0", + "node-cache": "^5.1.2", "node-schedule": "^2.1.1", "npm-run-all": "^4.1.5", "pg": "^8.11.3",