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!)
This commit is contained in:
Emi Matchu 2020-08-19 17:50:05 -07:00
parent 20523a9562
commit 47d22ad25c
9 changed files with 84 additions and 9 deletions

View file

@ -32,13 +32,14 @@
"react-transition-group": "^4.3.0" "react-transition-group": "^4.3.0"
}, },
"scripts": { "scripts": {
"start": "react-app-rewired start", "start": "yarn build-cached-data && react-app-rewired start",
"build": "react-app-rewired build", "build": "yarn build-cached-data && react-app-rewired build",
"test": "react-app-rewired test --env=jsdom", "test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"setup-mysql-user": "mysql -h impress.openneo.net -u matchu -p < setup-mysql-user.sql", "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": "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", "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" "cache-asset-manifests": "node -r dotenv/config scripts/cache-asset-manifests.js"
}, },
"eslintConfig": { "eslintConfig": {

View file

@ -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;
});

View file

@ -239,7 +239,7 @@ function useSearchResults(query, outfitState) {
layers { layers {
zone { zone {
id id
label label @client
} }
} }
} }

View file

@ -47,7 +47,7 @@ function useOutfitState() {
layers { layers {
zone { zone {
id id
label label @client
} }
} }
} }

View file

@ -1,6 +1,8 @@
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { createPersistedQueryLink } from "apollo-link-persisted-queries"; import { createPersistedQueryLink } from "apollo-link-persisted-queries";
const cachedZones = require("./cached-data/zones.json");
const typePolicies = { const typePolicies = {
Query: { Query: {
fields: { 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 // The PersistedQueryLink in front of the HttpLink helps us send cacheable GET

1
src/app/cached-data/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.json

View file

@ -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.

View file

@ -125,8 +125,8 @@ export const itemAppearanceFragment = gql`
bodyId bodyId
zone { zone {
id id
depth depth @client
label # HACK: This is for Support tools, but other views don't need it 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) imageUrl(size: SIZE_600)
zone { zone {
id id
depth depth @client
} }
} }
} }

View file

@ -346,8 +346,7 @@ const resolvers = {
}, },
zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => { zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => {
const layer = await swfAssetLoader.load(id); const layer = await swfAssetLoader.load(id);
const zone = await zoneLoader.load(layer.zoneId); return { id: layer.zoneId };
return zone;
}, },
swfUrl: async ({ id }, _, { swfAssetLoader }) => { swfUrl: async ({ id }, _, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id); const layer = await swfAssetLoader.load(id);