From 8d7eabf1e31df9d1b9d9be1dffbcc071a8a9be50 Mon Sep 17 00:00:00 2001 From: Matchu Date: Thu, 10 Aug 2023 18:09:10 -0700 Subject: [PATCH] Add AppProvider to wardrobe-2020 Hey the app runs now! How exciting! It doesn't run *correctly* but it renders at all!! --- app/javascript/wardrobe-2020-page.js | 16 +- app/javascript/wardrobe-2020/AppProvider.js | 157 ++++++++++++ app/javascript/wardrobe-2020/ItemPage.js | 4 +- .../wardrobe-2020/WardrobePage/index.js | 62 +++-- .../WardrobePage/support/PosePickerSupport.js | 4 +- .../WardrobePage/useOutfitState.js | 9 +- .../WardrobePage/useSearchResults.js | 6 +- app/javascript/wardrobe-2020/apolloClient.js | 230 ++++++++++++++++++ .../components/SpeciesColorPicker.js | 19 +- .../components/getVisibleLayers.js | 4 - .../components/useOutfitAppearance.js | 6 +- app/javascript/wardrobe-2020/index.js | 3 +- package.json | 5 +- yarn.lock | 88 ++++++- 14 files changed, 549 insertions(+), 64 deletions(-) create mode 100644 app/javascript/wardrobe-2020/AppProvider.js create mode 100644 app/javascript/wardrobe-2020/apolloClient.js diff --git a/app/javascript/wardrobe-2020-page.js b/app/javascript/wardrobe-2020-page.js index e2688984..7f428d79 100644 --- a/app/javascript/wardrobe-2020-page.js +++ b/app/javascript/wardrobe-2020-page.js @@ -1,7 +1,19 @@ import React from "react"; import ReactDOM from "react-dom"; +import { loadErrorMessages, loadDevMessages } from "@apollo/client/dev"; -import { WardrobePage } from "./wardrobe-2020"; +import { AppProvider, WardrobePage } from "./wardrobe-2020"; + +// Use Apollo's error messages in development. +if (process.env["NODE_ENV"] === "development") { + loadErrorMessages(); + loadDevMessages(); +} const rootNode = document.querySelector("#wardrobe-2020-root"); -ReactDOM.render(, rootNode); +ReactDOM.render( + + + , + rootNode +); diff --git a/app/javascript/wardrobe-2020/AppProvider.js b/app/javascript/wardrobe-2020/AppProvider.js new file mode 100644 index 00000000..a4512c85 --- /dev/null +++ b/app/javascript/wardrobe-2020/AppProvider.js @@ -0,0 +1,157 @@ +import React from "react"; +import * as Sentry from "@sentry/react"; +import { Integrations } from "@sentry/tracing"; +import { Auth0Provider } from "@auth0/auth0-react"; +import { CSSReset, ChakraProvider, extendTheme } from "@chakra-ui/react"; +import { ApolloProvider } from "@apollo/client"; +import { useAuth0 } from "@auth0/auth0-react"; +import { mode } from "@chakra-ui/theme-tools"; +import { BrowserRouter } from "react-router-dom"; + +import buildApolloClient from "./apolloClient"; + +const theme = extendTheme({ + styles: { + global: (props) => ({ + html: { + // HACK: Chakra sets body as the relative position element, which is + // fine, except its `min-height: 100%` doesn't actually work + // unless paired with height on the root element too! + height: "100%", + }, + body: { + background: mode("gray.50", "gray.800")(props), + color: mode("green.800", "green.50")(props), + transition: "all 0.25s", + }, + }), + }, +}); + +export default function AppProvider({ children }) { + React.useEffect(() => setupLogging(), []); + + return ( + + + + + + {children} + + + + + ); +} + +function DTIApolloProvider({ children, additionalCacheState = {} }) { + const auth0 = useAuth0(); + const auth0Ref = React.useRef(auth0); + + React.useEffect(() => { + auth0Ref.current = auth0; + }, [auth0]); + + // Save the first `additionalCacheState` we get as our `initialCacheState`, + // which we'll use to initialize the client without having to wait a tick. + const [initialCacheState, unusedSetInitialCacheState] = + React.useState(additionalCacheState); + + const client = React.useMemo( + () => + buildApolloClient({ + getAuth0: () => auth0Ref.current, + initialCacheState, + }), + [initialCacheState] + ); + + // When we get a new `additionalCacheState` object, merge it into the cache: + // copy the previous cache state, merge the new cache state's entries in, + // and "restore" the new merged cache state. + // + // HACK: Using `useMemo` for this is a dastardly trick!! What we want is the + // semantics of `useEffect` kinda, but we need to ensure it happens + // *before* all the children below get rendered, so they don't fire off + // unnecessary network requests. Using `useMemo` but throwing away the + // result kinda does that. It's evil! It's nasty! It's... perfect? + // (This operation is safe to run multiple times too, in case memo + // re-runs it. It's just, y'know, a performance loss. Maybe it's + // actually kinda perfect lol) + // + // I feel like there's probably a better way to do this... like, I want + // the semantic of replacing this client with an updated client - but I + // don't want to actually replace the client, because that'll break + // other kinds of state, like requests loading in the shared layout. + // Idk! I'll see how it goes! + React.useMemo(() => { + const previousCacheState = client.cache.extract(); + const mergedCacheState = { ...previousCacheState }; + for (const key of Object.keys(additionalCacheState)) { + mergedCacheState[key] = { + ...mergedCacheState[key], + ...additionalCacheState[key], + }; + } + console.debug( + "Merging Apollo cache:", + additionalCacheState, + mergedCacheState + ); + client.cache.restore(mergedCacheState); + }, [client, additionalCacheState]); + + return {children}; +} + +function setupLogging() { + Sentry.init({ + dsn: "https://c55875c3b0904264a1a99e5b741a221e@o506079.ingest.sentry.io/5595379", + autoSessionTracking: true, + integrations: [ + new Integrations.BrowserTracing({ + beforeNavigate: (context) => ({ + ...context, + // Assume any path segment starting with a digit is an ID, and replace + // it with `:id`. This will help group related routes in Sentry stats. + // NOTE: I'm a bit uncertain about the timing on this for tracking + // client-side navs... but we now only track first-time + // pageloads, and it definitely works correctly for them! + name: window.location.pathname.replaceAll(/\/[0-9][^/]*/g, "/:id"), + }), + + // We have a _lot_ of location changes that don't actually signify useful + // navigations, like in the wardrobe page. It could be useful to trace + // them with better filtering someday, but frankly we don't use the perf + // features besides Web Vitals right now, and those only get tracked on + // first-time pageloads, anyway. So, don't track client-side navs! + startTransactionOnLocationChange: false, + }), + ], + denyUrls: [ + // Don't log errors that were probably triggered by extensions and not by + // our own app. (Apparently Sentry's setting to ignore browser extension + // errors doesn't do this anywhere near as consistently as I'd expect?) + // + // Adapted from https://gist.github.com/impressiver/5092952, as linked in + // https://docs.sentry.io/platforms/javascript/configuration/filtering/. + /^chrome-extension:\/\//, + /^moz-extension:\/\//, + ], + + // Since we're only tracking first-page loads and not navigations, 100% + // sampling isn't actually so much! Tune down if it becomes a problem, tho. + tracesSampleRate: 1.0, + }); +} diff --git a/app/javascript/wardrobe-2020/ItemPage.js b/app/javascript/wardrobe-2020/ItemPage.js index 0d655421..d53211e9 100644 --- a/app/javascript/wardrobe-2020/ItemPage.js +++ b/app/javascript/wardrobe-2020/ItemPage.js @@ -854,7 +854,7 @@ function ItemPageOutfitPreview({ itemId }) { name restrictedZones { id - label @client + label } compatibleBodiesAndTheirZones { body { @@ -867,7 +867,7 @@ function ItemPageOutfitPreview({ itemId }) { } zones { id - label @client + label } } canonicalAppearance( diff --git a/app/javascript/wardrobe-2020/WardrobePage/index.js b/app/javascript/wardrobe-2020/WardrobePage/index.js index 4ace189c..5c57ad45 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/index.js +++ b/app/javascript/wardrobe-2020/WardrobePage/index.js @@ -80,40 +80,34 @@ function WardrobePage() { // that need it, where it's more useful and more performant to access // via context. return ( - <> - - - - - - - } - itemsAndMaybeSearchPanel={ - - } - searchFooter={ - - } - /> - - + + + } + itemsAndMaybeSearchPanel={ + + } + searchFooter={ + + } + /> + ); } diff --git a/app/javascript/wardrobe-2020/WardrobePage/support/PosePickerSupport.js b/app/javascript/wardrobe-2020/WardrobePage/support/PosePickerSupport.js index 8302057e..4791e0b0 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/support/PosePickerSupport.js +++ b/app/javascript/wardrobe-2020/WardrobePage/support/PosePickerSupport.js @@ -46,7 +46,7 @@ function PosePickerSupport({ id zone { id - label @client + label } # For AppearanceLayerSupportModal @@ -59,7 +59,7 @@ function PosePickerSupport({ } restrictedZones { id - label @client + label } # For AppearanceLayerSupportModal to know the name diff --git a/app/javascript/wardrobe-2020/WardrobePage/useOutfitState.js b/app/javascript/wardrobe-2020/WardrobePage/useOutfitState.js index 5d26c4f9..a27b394b 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/useOutfitState.js +++ b/app/javascript/wardrobe-2020/WardrobePage/useOutfitState.js @@ -2,6 +2,7 @@ import React from "react"; import gql from "graphql-tag"; import produce, { enableMapSet } from "immer"; import { useQuery, useApolloClient } from "@apollo/client"; +import { useSearchParams } from "react-router-dom"; import { itemAppearanceFragment } from "../components/useOutfitAppearance"; @@ -156,13 +157,13 @@ function useOutfitState() { layers { zone { id - label @client + label } } restrictedZones { id - label @client - isCommonlyUsedByItems @client + label + isCommonlyUsedByItems } } } @@ -387,7 +388,7 @@ function useParseOutfitUrl() { // stable object! const memoizedOutfitState = React.useMemo( () => readOutfitStateFromSearchParams(searchParams), - [query] + [searchParams] ); return memoizedOutfitState; diff --git a/app/javascript/wardrobe-2020/WardrobePage/useSearchResults.js b/app/javascript/wardrobe-2020/WardrobePage/useSearchResults.js index 1dea94fc..a4e39b92 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/useSearchResults.js +++ b/app/javascript/wardrobe-2020/WardrobePage/useSearchResults.js @@ -87,13 +87,13 @@ export function useSearchResults( layers { zone { id - label @client + label } } restrictedZones { id - label @client - isCommonlyUsedByItems @client + label + isCommonlyUsedByItems } } } diff --git a/app/javascript/wardrobe-2020/apolloClient.js b/app/javascript/wardrobe-2020/apolloClient.js new file mode 100644 index 00000000..ca3a2f0c --- /dev/null +++ b/app/javascript/wardrobe-2020/apolloClient.js @@ -0,0 +1,230 @@ +import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; +import { setContext } from "@apollo/client/link/context"; +import { createPersistedQueryLink } from "apollo-link-persisted-queries"; + +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: { + 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: "https://impress-2020.openneo.net/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 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 }) => { + return new ApolloClient({ + link: buildLink(getAuth0), + cache: new InMemoryCache({ typePolicies }).restore(initialCacheState), + connectToDevTools: true, + }); +}; + +export default buildClient; diff --git a/app/javascript/wardrobe-2020/components/SpeciesColorPicker.js b/app/javascript/wardrobe-2020/components/SpeciesColorPicker.js index 0dd915f1..f6477dd9 100644 --- a/app/javascript/wardrobe-2020/components/SpeciesColorPicker.js +++ b/app/javascript/wardrobe-2020/components/SpeciesColorPicker.js @@ -31,7 +31,11 @@ function SpeciesColorPicker({ colorTestId = null, onChange, }) { - const { loading: loadingMeta, error: errorMeta, data: meta } = useQuery(gql` + const { + loading: loadingMeta, + error: errorMeta, + data: meta, + } = useQuery(gql` query SpeciesColorPicker { allSpecies { id @@ -340,11 +344,14 @@ let cachedResponseForAllValidPetPoses = null; * data from GraphQL serves on the first render, without a loading state. */ export function useAllValidPetPoses() { - const networkResponse = useFetch("/api/validPetPoses", { - responseType: "arrayBuffer", - // If we already have globally-cached valids, skip the request. - skip: cachedResponseForAllValidPetPoses != null, - }); + const networkResponse = useFetch( + "https://impress-2020.openneo.net/api/validPetPoses", + { + responseType: "arrayBuffer", + // If we already have globally-cached valids, skip the request. + skip: cachedResponseForAllValidPetPoses != null, + } + ); // Use the globally-cached response if we have one, or await the network // response if not. diff --git a/app/javascript/wardrobe-2020/components/getVisibleLayers.js b/app/javascript/wardrobe-2020/components/getVisibleLayers.js index 4a2aab99..13be0014 100644 --- a/app/javascript/wardrobe-2020/components/getVisibleLayers.js +++ b/app/javascript/wardrobe-2020/components/getVisibleLayers.js @@ -94,8 +94,6 @@ function getVisibleLayers(petAppearance, itemAppearances) { return visibleLayers; } -// TODO: The web client could save bandwidth by applying @client to the `depth` -// field, because it already has zone depths cached. export const itemAppearanceFragmentForGetVisibleLayers = gql` fragment ItemAppearanceForGetVisibleLayers on ItemAppearance { id @@ -113,8 +111,6 @@ export const itemAppearanceFragmentForGetVisibleLayers = gql` } `; -// TODO: The web client could save bandwidth by applying @client to the `depth` -// field, because it already has zone depths cached. export const petAppearanceFragmentForGetVisibleLayers = gql` fragment PetAppearanceForGetVisibleLayers on PetAppearance { id diff --git a/app/javascript/wardrobe-2020/components/useOutfitAppearance.js b/app/javascript/wardrobe-2020/components/useOutfitAppearance.js index 69394282..8d94da34 100644 --- a/app/javascript/wardrobe-2020/components/useOutfitAppearance.js +++ b/app/javascript/wardrobe-2020/components/useOutfitAppearance.js @@ -136,8 +136,8 @@ export const appearanceLayerFragment = gql` knownGlitches # For HTML5 & Known Glitches UI zone { id - depth @client - label @client + depth + label } } `; @@ -149,7 +149,7 @@ export const appearanceLayerFragmentForSupport = gql` swfUrl # HACK: This is for Support tools, but other views don't need it zone { id - label @client # HACK: This is for Support tools, but other views don't need it + label # HACK: This is for Support tools, but other views don't need it } } `; diff --git a/app/javascript/wardrobe-2020/index.js b/app/javascript/wardrobe-2020/index.js index c5122a19..b95e0699 100644 --- a/app/javascript/wardrobe-2020/index.js +++ b/app/javascript/wardrobe-2020/index.js @@ -1,3 +1,4 @@ +import AppProvider from "./AppProvider"; import WardrobePage from "./WardrobePage"; -export { WardrobePage }; +export { AppProvider, WardrobePage }; diff --git a/package.json b/package.json index ccd70847..73286ae2 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "@emotion/styled": "^11.0.0", "@loadable/component": "^5.12.0", "@sentry/react": "^5.30.0", + "@sentry/tracing": "^5.30.0", + "apollo-link-persisted-queries": "^0.2.2", "easeljs": "^1.0.2", "esbuild": "^0.19.0", "framer-motion": "^4.1.11", @@ -25,6 +27,7 @@ "tweenjs": "^1.0.2" }, "scripts": { - "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text" + "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text", + "build:production": "yarn build --minify" } } diff --git a/yarn.lock b/yarn.lock index 52c93b31..19ac328f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -985,6 +985,17 @@ hoist-non-react-statics "^3.3.2" tslib "^1.9.3" +"@sentry/tracing@^5.30.0": + version "5.30.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.30.0.tgz#501d21f00c3f3be7f7635d8710da70d9419d4e1f" + integrity sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw== + dependencies: + "@sentry/hub" "5.30.0" + "@sentry/minimal" "5.30.0" + "@sentry/types" "5.30.0" + "@sentry/utils" "5.30.0" + tslib "^1.9.3" + "@sentry/types@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.30.0.tgz#19709bbe12a1a0115bc790b8942917da5636f402" @@ -1046,6 +1057,13 @@ dependencies: tslib "^2.3.0" +"@wry/equality@^0.1.2": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790" + integrity sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA== + dependencies: + tslib "^1.9.3" + "@wry/equality@^0.5.6": version "0.5.6" resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.6.tgz#cd4a533c72c3752993ab8cbf682d3d20e3cb601e" @@ -1072,6 +1090,34 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +apollo-link-persisted-queries@^0.2.2: + version "0.2.5" + resolved "https://registry.yarnpkg.com/apollo-link-persisted-queries/-/apollo-link-persisted-queries-0.2.5.tgz#76deabf68dac218d83f2fa23eebc3b25772fd914" + integrity sha512-PYWsMFcRGT9NZ6e6EK5rlhNDtcK6FR76JDy1RIngEfR6RdM5a2Z0IhZdn9RTTNB3V/+s7iWviQmoCfQrTVXu0A== + dependencies: + apollo-link "^1.2.1" + hash.js "^1.1.7" + +apollo-link@^1.2.1: + version "1.2.14" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9" + integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg== + dependencies: + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.21" + +apollo-utilities@^1.3.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf" + integrity sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig== + dependencies: + "@wry/equality" "^0.1.2" + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.4.0" + tslib "^1.10.0" + aria-hidden@^1.1.1: version "1.2.3" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" @@ -1244,6 +1290,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + fast-text-encoding@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" @@ -1315,6 +1366,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + hey-listen@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" @@ -1340,6 +1399,11 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +inherits@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" @@ -1398,6 +1462,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" @@ -1689,7 +1758,14 @@ ts-invariant@^0.10.3: dependencies: tslib "^2.1.0" -tslib@^1.0.0, tslib@^1.9.3: +ts-invariant@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" + +tslib@^1.0.0, tslib@^1.10.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -1741,6 +1817,14 @@ yaml@^1.10.0: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +zen-observable-ts@^0.8.21: + version "0.8.21" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d" + integrity sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg== + dependencies: + tslib "^1.9.3" + zen-observable "^0.8.0" + zen-observable-ts@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" @@ -1748,7 +1832,7 @@ zen-observable-ts@^1.2.5: dependencies: zen-observable "0.8.15" -zen-observable@0.8.15: +zen-observable@0.8.15, zen-observable@^0.8.0: version "0.8.15" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==