From 47d22ad25c1e0de16db8d5f3ecadbf19ca66f4df Mon Sep 17 00:00:00 2001 From: Matchu Date: Wed, 19 Aug 2020 17:50:05 -0700 Subject: [PATCH] Build cached zones, stop querying on server In this change, we cache the zones table as part of the JS build process. This keeps the database as our source of truth, while aggressively caching the data at deploy time. See the new README for some rationale! I tested this by pulling up dev Honeycomb, and observing that we no longer run db queries to `zones` in the new traces for the wardrobe page. (It's a good thing we did it this way, because I noticed some code in the server that was still loading the zone anyway, and fixed it here!) --- package.json | 5 +-- scripts/build-cached-data.js | 41 +++++++++++++++++++++++ src/app/WardrobePage/SearchPanel.js | 2 +- src/app/WardrobePage/useOutfitState.js | 2 +- src/app/apolloClient.js | 14 ++++++++ src/app/cached-data/.gitignore | 1 + src/app/cached-data/README.md | 19 +++++++++++ src/app/components/useOutfitAppearance.js | 6 ++-- src/server/index.js | 3 +- 9 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 scripts/build-cached-data.js create mode 100644 src/app/cached-data/.gitignore create mode 100644 src/app/cached-data/README.md diff --git a/package.json b/package.json index ae81ea58..5cc2c7b3 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,14 @@ "react-transition-group": "^4.3.0" }, "scripts": { - "start": "react-app-rewired start", - "build": "react-app-rewired build", + "start": "yarn build-cached-data && react-app-rewired start", + "build": "yarn build-cached-data && react-app-rewired build", "test": "react-app-rewired test --env=jsdom", "eject": "react-scripts eject", "setup-mysql-user": "mysql -h impress.openneo.net -u matchu -p < setup-mysql-user.sql", "mysql": "mysql --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --database=openneo_impress", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", + "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", "cache-asset-manifests": "node -r dotenv/config scripts/cache-asset-manifests.js" }, "eslintConfig": { diff --git a/scripts/build-cached-data.js b/scripts/build-cached-data.js new file mode 100644 index 00000000..e5cab080 --- /dev/null +++ b/scripts/build-cached-data.js @@ -0,0 +1,41 @@ +// We run this on build to cache some stable database tables into the JS +// bundle! +const fs = require("fs").promises; +const path = require("path"); + +const connectToDb = require("../src/server/db"); +const { normalizeRow } = require("../src/server/util"); + +const cachedDataPath = path.join(__dirname, "..", "src", "app", "cached-data"); + +async function buildZonesCache(db) { + const [rows] = await db.query( + `SELECT z.id, z.depth, zt.label FROM zones z ` + + `INNER JOIN zone_translations zt ON z.id = zt.zone_id ` + + `WHERE locale = "en" ORDER BY z.id;` + ); + const entities = rows.map(normalizeRow); + + const filePath = path.join(cachedDataPath, "zones.json"); + fs.writeFile(filePath, JSON.stringify(entities, null, 4), "utf8"); + + console.log(`📚 Wrote zones to ${path.relative(process.cwd(), filePath)}`); +} + +async function main() { + const db = await connectToDb(); + await fs.mkdir(cachedDataPath, { recursive: true }); + + try { + await buildZonesCache(db); + } catch (e) { + db.close(); + throw e; + } + db.close(); +} + +main().catch((e) => { + console.error(e); + process.exitCode = 1; +}); diff --git a/src/app/WardrobePage/SearchPanel.js b/src/app/WardrobePage/SearchPanel.js index 70853243..d702f75d 100644 --- a/src/app/WardrobePage/SearchPanel.js +++ b/src/app/WardrobePage/SearchPanel.js @@ -239,7 +239,7 @@ function useSearchResults(query, outfitState) { layers { zone { id - label + label @client } } } diff --git a/src/app/WardrobePage/useOutfitState.js b/src/app/WardrobePage/useOutfitState.js index e9ff0a77..8e3b5e72 100644 --- a/src/app/WardrobePage/useOutfitState.js +++ b/src/app/WardrobePage/useOutfitState.js @@ -47,7 +47,7 @@ function useOutfitState() { layers { zone { id - label + label @client } } } diff --git a/src/app/apolloClient.js b/src/app/apolloClient.js index 2e2b0651..5c102d21 100644 --- a/src/app/apolloClient.js +++ b/src/app/apolloClient.js @@ -1,6 +1,8 @@ import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; import { createPersistedQueryLink } from "apollo-link-persisted-queries"; +const cachedZones = require("./cached-data/zones.json"); + const typePolicies = { Query: { fields: { @@ -24,6 +26,18 @@ const typePolicies = { }, }, }, + + Zone: { + fields: { + depth: (depth, { readField }) => { + return depth || cachedZones[readField("id")].depth; + }, + + label: (label, { readField }) => { + return label || cachedZones[readField("id")].label; + }, + }, + }, }; // The PersistedQueryLink in front of the HttpLink helps us send cacheable GET diff --git a/src/app/cached-data/.gitignore b/src/app/cached-data/.gitignore new file mode 100644 index 00000000..94a2dd14 --- /dev/null +++ b/src/app/cached-data/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/src/app/cached-data/README.md b/src/app/cached-data/README.md new file mode 100644 index 00000000..df067fc2 --- /dev/null +++ b/src/app/cached-data/README.md @@ -0,0 +1,19 @@ +The data in this folder is read from the database on build, and included in the +JS bundle we ship to the client. + +We do this for small stable database tables, where the round-trip between the +cloud server and the database creates noticeable latency. + +The build itself happens in `scripts/build-cached-data.js`, as part of both the +`yarn build` production build process, and the `yarn start` dev server process. + +The require happens in `src/app/apolloClient.js`, when we build our local +field resolvers. That way, most of the app is unaware of the distinction +between server data and client-cached data. But you _will_ see GQL queries +decorate the relevant fields with `@client`, to make clear that we don't want +to load from the server! + +NOTE: We could consider pulling this data out of the database altogether, and +just commit it to the codebase? But, because we're still fragmented across two +apps, I'd rather maintain one source of truth, and use this simple code to be +confident that everything's always in sync. diff --git a/src/app/components/useOutfitAppearance.js b/src/app/components/useOutfitAppearance.js index 46d36748..ecae211a 100644 --- a/src/app/components/useOutfitAppearance.js +++ b/src/app/components/useOutfitAppearance.js @@ -125,8 +125,8 @@ export const itemAppearanceFragment = gql` bodyId zone { id - depth - label # HACK: This is for Support tools, but other views don't need it + depth @client + label @client # HACK: This is for Support tools, but other views don't need it } } @@ -146,7 +146,7 @@ export const petAppearanceFragment = gql` imageUrl(size: SIZE_600) zone { id - depth + depth @client } } } diff --git a/src/server/index.js b/src/server/index.js index e11a15ca..48db0415 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -346,8 +346,7 @@ const resolvers = { }, zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => { const layer = await swfAssetLoader.load(id); - const zone = await zoneLoader.load(layer.zoneId); - return zone; + return { id: layer.zoneId }; }, swfUrl: async ({ id }, _, { swfAssetLoader }) => { const layer = await swfAssetLoader.load(id);