diff --git a/src/app/components/useOutfitAppearance.js b/src/app/components/useOutfitAppearance.js index 9771a37..750769d 100644 --- a/src/app/components/useOutfitAppearance.js +++ b/src/app/components/useOutfitAppearance.js @@ -9,31 +9,31 @@ import { useQuery } from "@apollo/react-hooks"; export default function useOutfitAppearance(outfitState) { const { wornItemIds, speciesId, colorId, pose } = outfitState; - const { loading, error, data } = useQuery( + // We split this query out from the other one, so that we can HTTP cache it. + // + // While Apollo gives us fine-grained caching during the page session, we can + // only HTTP a full query at a time. + // + // This is a minor optimization with respect to keeping the user's cache + // populated with their favorite species/color combinations. Once we start + // caching the items by body instead of species/color, this could make color + // changes really snappy! + // + // The larger optimization is that this enables the CDN to edge-cache the + // most popular species/color combinations, for very fast previews on the + // HomePage. At time of writing, Vercel isn't actually edge-caching these, I + // assume because our traffic isn't enough - so let's keep an eye on this! + const { loading: loading1, error: error1, data: data1 } = useQuery( gql` - query OutfitAppearance( - $wornItemIds: [ID!]! - $speciesId: ID! - $colorId: ID! - $pose: Pose! - ) { + query OutfitPetAppearance($speciesId: ID!, $colorId: ID!, $pose: Pose!) { petAppearance(speciesId: $speciesId, colorId: $colorId, pose: $pose) { ...PetAppearanceForOutfitPreview } - - items(ids: $wornItemIds) { - id - appearanceOn(speciesId: $speciesId, colorId: $colorId) { - ...ItemAppearanceForOutfitPreview - } - } } - ${itemAppearanceFragment} ${petAppearanceFragment} `, { variables: { - wornItemIds, speciesId, colorId, pose, @@ -42,16 +42,46 @@ export default function useOutfitAppearance(outfitState) { } ); - const itemAppearances = React.useMemo( - () => (data?.items || []).map((i) => i.appearanceOn), - [data] - ); - const visibleLayers = React.useMemo( - () => getVisibleLayers(data?.petAppearance, itemAppearances), - [data, itemAppearances] + const { loading: loading2, error: error2, data: data2 } = useQuery( + gql` + query OutfitItemsAppearance( + $speciesId: ID! + $colorId: ID! + $wornItemIds: [ID!]! + ) { + items(ids: $wornItemIds) { + id + appearanceOn(speciesId: $speciesId, colorId: $colorId) { + ...ItemAppearanceForOutfitPreview + } + } + } + ${itemAppearanceFragment} + `, + { + variables: { + speciesId, + colorId, + wornItemIds, + }, + skip: speciesId == null || colorId == null || wornItemIds.length === 0, + } ); - return { loading, error, visibleLayers }; + const itemAppearances = React.useMemo( + () => (data2?.items || []).map((i) => i.appearanceOn), + [data2] + ); + const visibleLayers = React.useMemo( + () => getVisibleLayers(data1?.petAppearance, itemAppearances), + [data1, itemAppearances] + ); + + return { + loading: loading1 || loading2, + error: error1 || error2, + visibleLayers, + }; } export function getVisibleLayers(petAppearance, itemAppearances) { diff --git a/src/server/index.js b/src/server/index.js index 616ad48..8b7f482 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -68,7 +68,8 @@ const typeDefs = gql` appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance } - type PetAppearance { + # Cache for 1 week (unlikely to change) + type PetAppearance @cacheControl(maxAge: 604800) { id: ID! species: Species! color: Color! @@ -84,7 +85,8 @@ const typeDefs = gql` restrictedZones: [Zone!]! } - type AppearanceLayer { + # Cache for 1 week (unlikely to change) + type AppearanceLayer @cacheControl(maxAge: 604800) { id: ID! zone: Zone! imageUrl(size: LayerImageSize): String @@ -98,7 +100,8 @@ const typeDefs = gql` svgUrl: String } - type Zone { + # Cache for 1 week (unlikely to change) + type Zone @cacheControl(maxAge: 604800) { id: ID! depth: Int! label: String! @@ -109,12 +112,14 @@ const typeDefs = gql` items: [Item!]! } - type Color { + # Cache for 1 week (unlikely to change) + type Color @cacheControl(maxAge: 604800) { id: ID! name: String! } - type Species { + # Cache for 1 week (unlikely to change) + type Species @cacheControl(maxAge: 604800) { id: ID! name: String! } @@ -138,8 +143,8 @@ const typeDefs = gql` } type Query { - allColors: [Color!]! @cacheControl(maxAge: 10800) # Cache for 3 hours - allSpecies: [Species!]! @cacheControl(maxAge: 10800) # Cache for 3 hours + allColors: [Color!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!) + allSpecies: [Species!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!) allValidSpeciesColorPairs: [SpeciesColorPair!]! # deprecated items(ids: [ID!]!): [Item!]! itemSearch(query: String!): ItemSearchResult! @@ -151,8 +156,9 @@ const typeDefs = gql` limit: Int ): ItemSearchResult! petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance + @cacheControl(maxAge: 604800) # Cache for 1 week (unlikely to change) petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]! - + @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!) outfit(id: ID!): Outfit petOnNeopetsDotCom(petName: String!): Outfit @@ -371,7 +377,7 @@ const resolvers = { return allPairs; }, items: (_, { ids }) => { - return ids.map(id => ({ id })); + return ids.map((id) => ({ id })); }, itemSearch: async (_, { query }, { itemSearchLoader }) => { const items = await itemSearchLoader.load(query.trim());