1
0
Fork 0
forked from OpenNeo/impress
impress/src/app/apolloClient.js
Matchu 2887d952de Fix /outfits/new init + add more SSR
Whew, setting up a cute GraphQL SSR system! I feel like it strikes a good balance of not having actually too many moving parts, though it's still a bit extensive for the problem we're solving 😅

Anyway, by doing SSR at _all_, we solve the problem where Next's "Automatic Static Optimization" was causing problems by setting the outfit state to the default at the start of the page load.

So I figured, why not try to SSR things _good_?

Now, when you navigate to the /outfits/new page, Next.js will go get the necessary GraphQL data to show the image before even putting the page into view. This makes the image show up all snappy-like! (when images.neopets.com is behaving :p)

We could do this with the stuff in the items panel too, but it's a tiny bit more annoying in the code right now, so I'm just gonna not worry about it and see how this performs in practice!

This change _doesn't_ include making the images actually show up before JS loads in, I assume because our JS code tries to validate that the images have loaded before fading them in on the page. Idk if we want to do something smarter there for the SSR case, to try to get them loading in faster!
2022-09-15 02:46:14 -07:00

257 lines
8.1 KiB
JavaScript

import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
import cachedZones from "./cached-data/zones.json";
import { getAuthModeFeatureFlag } from "./components/useCurrentUser";
// Teach Apollo to load certain fields from the cache, to avoid extra network
// requests. This happens a lot - e.g. reusing data from item search on the
// outfit immediately!
const typePolicies = {
Query: {
fields: {
allZones: (_, { toReference }) => {
return cachedZones.map((z) =>
toReference({ __typename: "Zone", id: z.id }, true)
);
},
closetList: (_, { args, toReference }) => {
return toReference({ __typename: "ClosetList", id: args.id }, true);
},
items: (_, { args, toReference }) => {
return args.ids.map((id) =>
toReference({ __typename: "Item", id }, true)
);
},
item: (_, { args, toReference }) => {
return toReference({ __typename: "Item", id: args.id }, true);
},
petAppearanceById: (_, { args, toReference }) => {
return toReference({ __typename: "PetAppearance", id: args.id }, true);
},
species: (_, { args, toReference }) => {
return toReference({ __typename: "Species", id: args.id }, true);
},
color: (_, { args, toReference }) => {
return toReference({ __typename: "Color", id: args.id }, true);
},
outfit: (_, { args, toReference }) => {
return toReference({ __typename: "Outfit", id: args.id }, true);
},
user: (_, { args, toReference }) => {
return toReference({ __typename: "User", id: args.id }, true);
},
},
},
Item: {
fields: {
appearanceOn: (appearance, { args, readField, toReference }) => {
// If we already have this exact appearance in the cache, serve it!
if (appearance) {
return appearance;
}
// Otherwise, we're going to see if this is a standard color, in which
// case we can reuse the standard color appearance if we already have
// it! This helps for fast loading when switching between standard
// colors.
const { speciesId, colorId } = args;
console.debug(
"[appearanceOn] seeking cached appearance",
speciesId,
colorId,
readField("id")
);
const speciesStandardBodyId = readField(
"standardBodyId",
toReference({ __typename: "Species", id: speciesId })
);
const colorIsStandard = readField(
"isStandard",
toReference({ __typename: "Color", id: colorId })
);
if (speciesStandardBodyId == null || colorIsStandard == null) {
// We haven't loaded all the species/colors into cache yet. We might
// be loading them, depending on the page? Either way, return
// `undefined`, meaning we don't know how to serve this from cache.
// This will cause us to start loading it from the server.
console.debug("[appearanceOn] species/colors not loaded yet");
return undefined;
}
if (colorIsStandard) {
const itemId = readField("id");
console.debug(
"[appearanceOn] standard color, will read:",
`item-${itemId}-body-${speciesStandardBodyId}`
);
return toReference({
__typename: "ItemAppearance",
id: `item-${itemId}-body-${speciesStandardBodyId}`,
});
} else {
console.debug("[appearanceOn] non-standard color, failure");
// This isn't a standard color, so we don't support special
// cross-color caching for it. Return `undefined`, meaning we don't
// know how to serve this from cache. This will cause us to start
// loading it from the server.
return undefined;
}
},
currentUserOwnsThis: (cachedValue, { readField }) => {
if (cachedValue != null) {
return cachedValue;
}
// Do we know what items this user owns? If so, scan for this item.
const currentUserRef = readField("currentUser", {
__ref: "ROOT_QUERY",
});
if (!currentUserRef) {
return undefined;
}
const thisItemId = readField("id");
const itemsTheyOwn = readField("itemsTheyOwn", currentUserRef);
if (!itemsTheyOwn) {
return undefined;
}
const theyOwnThisItem = itemsTheyOwn.some(
(itemRef) => readField("id", itemRef) === thisItemId
);
return theyOwnThisItem;
},
currentUserWantsThis: (cachedValue, { readField }) => {
if (cachedValue != null) {
return cachedValue;
}
// Do we know what items this user owns? If so, scan for this item.
const currentUserRef = readField("currentUser", {
__ref: "ROOT_QUERY",
});
if (!currentUserRef) {
return undefined;
}
const thisItemId = readField("id");
const itemsTheyWant = readField("itemsTheyWant", currentUserRef);
if (!itemsTheyWant) {
return undefined;
}
const theyWantThisItem = itemsTheyWant.some(
(itemRef) => readField("id", itemRef) === thisItemId
);
return theyWantThisItem;
},
},
},
ClosetList: {
fields: {
// When loading the updated contents of a list, replace it entirely.
items: { merge: false },
},
},
};
const httpLink = createHttpLink({ uri: "/api/graphql" });
const buildAuthLink = (getAuth0) =>
setContext(async (_, { headers = {}, sendAuth = false }) => {
if (!sendAuth) {
return;
}
const token = await getAccessToken(getAuth0);
if (token) {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
}
});
// This is a temporary way to pass the DTIAuthMode feature flag back to the
// server!
const authModeLink = setContext((_, { headers = {} }) => {
const authMode = getAuthModeFeatureFlag();
return {
headers: {
...headers,
"DTI-Auth-Mode": authMode,
},
};
});
async function getAccessToken(getAuth0) {
// Wait for auth0 to stop loading, so we can maybe get a token!
// We'll do this hackily by checking every 100ms until it's true.
await new Promise((resolve) => {
function check() {
if (getAuth0().isLoading) {
setTimeout(check, 100);
} else {
resolve();
}
}
check();
});
const { isAuthenticated, getAccessTokenSilently } = getAuth0();
if (isAuthenticated) {
const token = await getAccessTokenSilently();
return token;
}
}
const prebuiltCacheState = {};
for (const zone of cachedZones) {
prebuiltCacheState[`Zone:${zone.id}`] = { __typename: "Zone", ...zone };
}
const buildLink = (getAuth0) =>
buildAuthLink(getAuth0)
.concat(authModeLink)
.concat(
createPersistedQueryLink({
useGETForHashedQueries: true,
})
)
.concat(httpLink);
/**
* apolloClient is the global Apollo Client instance we use for GraphQL
* queries. This is how we communicate with the server!
*/
const buildClient = ({ getAuth0, initialCacheState }) => {
// We have both a pre-built cache of data that we just hardcode at build
// time, and the `initialCacheState` parameter, which some SSR'd pages give
// to us as Next.js props, so that we don't have to request the data again.
const mergedCacheState = mergeCacheStates([
prebuiltCacheState,
initialCacheState,
]);
return new ApolloClient({
link: buildLink(getAuth0),
cache: new InMemoryCache({ typePolicies }).restore(mergedCacheState),
connectToDevTools: true,
});
};
function mergeCacheStates(cacheStates) {
const mergedCacheState = {};
for (const cacheState of cacheStates) {
for (const key of Object.keys(cacheState)) {
mergedCacheState[key] = { ...mergedCacheState[key], ...cacheState[key] };
}
}
return mergedCacheState;
}
export default buildClient;