diff --git a/package.json b/package.json index ae81ea5..5cc2c7b 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 0000000..e5cab08 --- /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 7085324..d702f75 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 e9ff0a7..8e3b5e7 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 2e2b065..5c102d2 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 0000000..94a2dd1 --- /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 0000000..df067fc --- /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 46d3674..ecae211 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 e11a15c..48db041 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);