Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic client side envs 2 #80

Merged
merged 15 commits into from
Oct 8, 2024
Merged

Dynamic client side envs 2 #80

merged 15 commits into from
Oct 8, 2024

Conversation

JasonMHasperhoven
Copy link
Contributor

@JasonMHasperhoven JasonMHasperhoven commented Oct 2, 2024

As per @conorsch 's request, this adds support to set envs dynamically post-build.

  • Routes need to pass envs from getServerSideProps as props
  • _app will take this and provide it as a context (in v2 we could use mobx or whatever state manager we decide on)
  • Components can consume useEnvContext to access the envs

  • Client now makes a request to /api/env to get the clientSideEnv
  • Refactored app to use fetchers (similar to minifront's design) -- which consume useRegistry & useEnv

<Analytics />
<SpeedInsights />
<Component {...pageProps} />
<EnvContext.Provider value={pageProps.envs}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meant as a placeholder until we have finalized a state manager.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We we anticipate this env context vars to be critical to a number of things in client side code, we should use react-query to fetch and sync its state with a mobx EnvContextStore.

@JasonMHasperhoven JasonMHasperhoven changed the title Implement dynamic client side envs Dynamic client side envs 2 Oct 2, 2024
@JasonMHasperhoven JasonMHasperhoven requested review from conorsch and a team October 2, 2024 14:07
@JasonMHasperhoven JasonMHasperhoven marked this pull request as ready for review October 2, 2024 15:01
@conorsch
Copy link
Contributor

conorsch commented Oct 2, 2024

Deploying from this tag, without updating the env vars, I see:

⨯ Error: Registry not found for 
     at Ct.get (/app/node_modules/.pnpm/@[email protected]/node_modules/@penumbra-labs/registry/dist/index.js:2:113241) 
     at h (/app/.next/server/chunks/215.js:8:6274) 
     at d (/app/.next/server/chunks/215.js:8:6293) 
     at c (/app/.next/server/chunks/215.js:8:6607)
     at b (/app/.next/server/pages/trade/[[...params]].js:1:2857)

Which indicates that we no longer have sane default values. Can we add a default value for PENUMBRA_CHAIN_ID="penumbra-1"? Or at least a more informative error message that indicates a required environment variable should have been set and wasn't. Remember that RC is running a deployment, as well, and changes like the lack of defaults will cause that instance to fail when it's upgraded, unless we preserve defaults.

After setting the env vars properly, according to the new env vars, I confirmed that the assets were loading correctly for the testnet deployment, which is magnificent.

Copy link
Contributor

@conorsch conorsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works with the new env vars set! Asking for a bit more error-handling around the new vars: let's provide defaults, or at least log which required env vars weren't set if they're missing. See example error message in comments above.

Copy link
Contributor

@grod220 grod220 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot in this pull request that opens a number of questions on how we want to handle data fetching within this app:

  • client vs server requests
  • async vs sync requests
  • global caching & performance
  • react-query syncing w/ state manager
  • using a state manager versus react-context

Think it is worthwhile for others on the web team to take a look and we discuss in today's meeting

Comment on lines 18 to 24
const tokenAssetsList = useTokenAssets();
const [tokenAssets, setTokenAssets] = useState<Record<string, Token>>({});
const [selectedAssets, setSelectedAssets] = useState<Token[]>([]);

useEffect(() => {
const tokenAssets = fetchAllTokenAssets();
setTokenAssets(Object.fromEntries(tokenAssets.map(asset => [asset.symbol, asset])));
}, []);
setTokenAssets(Object.fromEntries(tokenAssetsList.map(asset => [asset.symbol, asset])));
}, [tokenAssetsList]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in react-query

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, but let's keep that to another PR (also this is v1 code).

Comment on lines 710 to 718
export async function getServerSideProps() {
const envs = getClientSideEnvs();

return {
props: {
envs,
},
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two thoughts:

  • Can we put this at a kind of parent layout component so we don't have to do this for every page we create (and need to remember or else risk errors)?
  • What if we created an backend API route for returning this and we put it in react-query (client side) instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Unfortunately with this implementation it seems to be not possible
  • Perhaps that's a better approach

Comment on lines 37 to 40
export function useTokenAssets() {
const envs = useEnvContext();
return fetchAllTokenAssets(envs['PENUMBRA_CHAIN_ID'] ?? '');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is, I'm pretty sure that fetchAllTokenAssets() does not cache. If env context was in react-query, we could compose within react-query to handle caching globally for this work. Otherwise, every component's use of useTokenAssets() will be a new instance and re-running on every render.

<Analytics />
<SpeedInsights />
<Component {...pageProps} />
<EnvContext.Provider value={pageProps.envs}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We we anticipate this env context vars to be critical to a number of things in client side code, we should use react-query to fetch and sync its state with a mobx EnvContextStore.

@JasonMHasperhoven JasonMHasperhoven marked this pull request as draft October 4, 2024 17:43
@JasonMHasperhoven
Copy link
Contributor Author

JasonMHasperhoven commented Oct 4, 2024

Changing the implementation to use a request to fetch the client-side env. WIP.

@JasonMHasperhoven JasonMHasperhoven changed the title Dynamic client side envs 2 [WIP] Dynamic client side envs 2 Oct 4, 2024
@JasonMHasperhoven JasonMHasperhoven changed the title [WIP] Dynamic client side envs 2 Dynamic client side envs 2 Oct 7, 2024
@JasonMHasperhoven JasonMHasperhoven requested a review from a team October 7, 2024 11:17
@JasonMHasperhoven JasonMHasperhoven marked this pull request as ready for review October 7, 2024 11:30
@JasonMHasperhoven
Copy link
Contributor Author

It's ready for review again. I indeed like this implementation better @grod220 & added defaults for the client side @conorsch.

return useQuery({
queryKey: ['clientEnv'],
queryFn: async (): Promise<ClientEnv> => {
return fetch('/api/env').then(resp => resp.json() as unknown as ClientEnv);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: In async functions, I find sticking to async await to be more readable than promise chaining:

    queryFn: async (): Promise<ClientEnv> => {
      const res = await fetch('/api/env');
      return (await res.json()) as ClientEnv;
    },

queryKey: ['penumbraRegistry', chainId],
queryFn: async (): Promise<Registry | null> => {
if (!chainId) {
return null;
Copy link
Contributor

@grod220 grod220 Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: in general, let's try to avoid null when we can. This can go into bike shedding territory, but I'm in the camp that it is a mistake in the language.

There is another issue though, the returned loading and error states from useRegistry are not quite true. We should combine them together:

export const useRegistry = () => {
  const { data: env, isLoading: isEnvLoading, error: envError } = useEnv();

  const {
    data: registry,
    isLoading: isRegistryLoading,
    error: registryError,
  } = useQuery({
    queryKey: ['penumbraRegistry', env],
    queryFn: async () => {
      const chainId = env?.PENUMBRA_CHAIN_ID;
      if (!chainId) {
        throw new Error('chain id not available to query registry');
      }
      return chainRegistryClient.remote.get(chainId);
    },
    staleTime: Infinity,
    enabled: Boolean(env),
  });

  return {
    registry,
    isLoading: isEnvLoading || isRegistryLoading,
    error: envError || registryError,
  };
};

import { uint8ArrayToBase64, base64ToUint8Array } from '@/old/utils/math/base64';
import { Token } from '@/old/utils/types/token';

export const useTokenAssets = (): Token[] => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: with useRegistry() returning data/loading/error, this hook should also return the same so that can properly be handled by consumers if desired.

}) as Token[];
};

export const useTokenAsset = (tokenId: Uint8Array | string): null | Token => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: we should return some data field + loading + error states

inner: asset.penumbraAssetId?.inner && uint8ArrayToBase64(asset.penumbraAssetId.inner),
imagePath: imagePathFromAssetImages(asset.images),
};
}) as Token[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: it is tempting to use our own types, but we should do our best to use types from BufBuild that are more universally relied upon in the penumbra ecosystem. For instance, we shouldn't have components that accept a Token, but probably ones that accept a ValueView type

Comment on lines 8 to 20
export function getClientSideEnv() {
const whitelist: string[] = ['PENUMBRA_CHAIN_ID', 'PENUMBRA_CUILOA_URL'];

const env = whitelist.reduce(
(env, key) => ({
...env,
[key]: process.env[key] ?? '',
}),
defaults,
) as ClientEnv;

return env;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Think this would work the same

export function getClientSideEnv(): ClientEnv {
  const whitelist: string[] = ['PENUMBRA_CHAIN_ID', 'PENUMBRA_CUILOA_URL'];

  return whitelist.reduce(
    (env, key) => ({
      ...env,
      [key]: process.env[key] ?? '',
    }),
    defaults,
  );
}

Copy link
Contributor

@grod220 grod220 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline:

  • For hooks consumed by deprecated code, label as deprecated. Given they don't consume loading/error states, can keep simple as needed.
  • For hooks consumed by new code, should follow data/loading/error propagation pattern
  • No nulls (if possible)

@conorsch conorsch self-requested a review October 8, 2024 15:58
Copy link
Contributor

@conorsch conorsch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful! Thanks for your hard work on this, it's markedly more straightforward to deploy and configure now.

It's a nice touch to redirect the trading view to penumbra:gm on the testnet; however, that route should probably be configurable, too, maybe with the previous setting of penumbra:usdc retained as default behavior. Otherwise, deployments against the penumbra-1 chain will show nonsense data, because there's no gm asset. But we can circle back to that change later, I know there' a lot of work in-flight right now!

@conorsch conorsch merged commit df94a11 into main Oct 8, 2024
3 checks passed
@conorsch conorsch deleted the dynamic-client-side-envs2 branch October 8, 2024 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants