diff --git a/site/src/contexts/ProxyContext.test.tsx b/site/src/contexts/ProxyContext.test.tsx index 8e16e868627e3..03f2662037733 100644 --- a/site/src/contexts/ProxyContext.test.tsx +++ b/site/src/contexts/ProxyContext.test.tsx @@ -26,7 +26,11 @@ import type * as ProxyLatency from "./useProxyLatency"; // here and not inside a unit test. jest.mock("contexts/useProxyLatency", () => ({ useProxyLatency: () => { - return { proxyLatencies: hardCodedLatencies, refetch: jest.fn() }; + return { + proxyLatencies: hardCodedLatencies, + refetch: jest.fn(), + loaded: true, + }; }, })); @@ -115,7 +119,7 @@ describe("ProxyContextGetURLs", () => { preferredPathAppURL, preferredWildcardHostname, ) => { - const preferred = getPreferredProxy(regions, selected, latencies); + const preferred = getPreferredProxy(regions, selected, latencies, true); expect(preferred.preferredPathAppURL).toBe(preferredPathAppURL); expect(preferred.preferredWildcardHostname).toBe( preferredWildcardHostname, @@ -138,10 +142,22 @@ const TestingComponent = () => { // TestingScreen just mounts some components that we can check in the unit test. const TestingScreen = () => { - const { proxy, userProxy, isFetched, isLoading, clearProxy, setProxy } = - useProxy(); + const { + proxy, + userProxy, + isFetched, + isLoading, + latenciesLoaded, + clearProxy, + setProxy, + } = useProxy(); + return ( <> +
@@ -206,7 +222,6 @@ describe("ProxyContextSelection", () => { }; it.each([ - // Not latency behavior [ "empty", { @@ -220,6 +235,7 @@ describe("ProxyContextSelection", () => { "regions_no_selection", { expProxyID: MockPrimaryWorkspaceProxy.id, + expUserProxyID: MockPrimaryWorkspaceProxy.id, regions: MockWorkspaceProxies, storageProxy: undefined, }, @@ -261,11 +277,12 @@ describe("ProxyContextSelection", () => { expUserProxyID: MockHealthyWildWorkspaceProxy.id, }, ], - // Latency behavior is disabled, so the primary should be selected. + // First page load defers to the proxy by latency [ "regions_default_low_latency", { - expProxyID: MockPrimaryWorkspaceProxy.id, + expProxyID: MockHealthyWildWorkspaceProxy.id, + expUserProxyID: MockHealthyWildWorkspaceProxy.id, regions: MockWorkspaceProxies, storageProxy: undefined, latencies: { @@ -362,6 +379,10 @@ describe("ProxyContextSelection", () => { TestingComponent(); await waitForLoaderToBeRemoved(); + await screen.findByTestId("latenciesLoaded").then((x) => { + expect(x.title).toBe("true"); + }); + if (afterLoad) { await afterLoad(); } diff --git a/site/src/contexts/ProxyContext.tsx b/site/src/contexts/ProxyContext.tsx index 55637e32a3069..c162c2c4952ff 100644 --- a/site/src/contexts/ProxyContext.tsx +++ b/site/src/contexts/ProxyContext.tsx @@ -54,6 +54,9 @@ export interface ProxyContextValue { // then the latency has not been fetched yet. Calculations happen async for each proxy in the list. // Refer to the returned report for a given proxy for more information. proxyLatencies: ProxyLatencies; + // latenciesLoaded is true when the latencies have been initially loaded. + // Once set to true, it will not be set to false again. + latenciesLoaded: boolean; // refetchProxyLatencies will trigger refreshing of the proxy latencies. By default the latencies // are loaded once. refetchProxyLatencies: () => Date; @@ -122,8 +125,11 @@ export const ProxyProvider: FC = ({ children }) => { // Every time we get a new proxiesResponse, update the latency check // to each workspace proxy. - const { proxyLatencies, refetch: refetchProxyLatencies } = - useProxyLatency(proxiesResp); + const { + proxyLatencies, + refetch: refetchProxyLatencies, + loaded: latenciesLoaded, + } = useProxyLatency(proxiesResp); // updateProxy is a helper function that when called will // update the proxy being used. @@ -136,7 +142,8 @@ export const ProxyProvider: FC = ({ children }) => { loadUserSelectedProxy(), proxyLatencies, // Do not auto select based on latencies, as inconsistent latencies can cause this - // to behave poorly. + // to change on each call. updateProxy should be stable when selecting a proxy to + // prevent flickering. false, ), ); @@ -149,6 +156,34 @@ export const ProxyProvider: FC = ({ children }) => { updateProxy(); }, [proxiesResp, proxyLatencies]); + // This useEffect will auto select the best proxy if the user has not selected one. + // It must wait until all latencies are loaded to select based on latency. This does mean + // the first time a user loads the page, the proxy will "flicker" to the best proxy. + // + // Once the page is loaded, or the user selects a proxy, this will not run again. + // biome-ignore lint/correctness/useExhaustiveDependencies: Only update if the source data changes + useEffect(() => { + if (loadUserSelectedProxy() !== undefined) { + return; // User has selected a proxy, do not auto select. + } + if (!latenciesLoaded) { + // Wait until the latencies are loaded first. + return; + } + + const best = getPreferredProxy( + proxiesResp ?? [], + loadUserSelectedProxy(), + proxyLatencies, + true, + ); + + if (best?.proxy) { + saveUserSelectedProxy(best.proxy); + updateProxy(); + } + }, [latenciesLoaded, proxiesResp, proxyLatencies]); + return ( = ({ children }) => { userProxy: userSavedProxy, proxy: proxy, proxies: proxiesResp, + latenciesLoaded: latenciesLoaded, isLoading: proxiesLoading, isFetched: proxiesFetched, error: proxiesError, @@ -214,12 +250,12 @@ export const getPreferredProxy = ( // If no proxy is selected, or the selected proxy is unhealthy default to the primary proxy. if (!selectedProxy || !selectedProxy.healthy) { - // By default, use the primary proxy. + // Default to the primary proxy selectedProxy = proxies.find((proxy) => proxy.name === "primary"); // If we have latencies, then attempt to use the best proxy by latency instead. const best = selectByLatency(proxies, latencies); - if (autoSelectBasedOnLatency && best) { + if (autoSelectBasedOnLatency && best !== undefined) { selectedProxy = best; } } diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index ff8be8cd66135..f5f3d2acb415c 100644 --- a/site/src/contexts/useProxyLatency.ts +++ b/site/src/contexts/useProxyLatency.ts @@ -48,6 +48,11 @@ export const useProxyLatency = ( // Until the new values are loaded, the old values will still be used. refetch: () => Date; proxyLatencies: Record; + // loaded signals all latency requests have completed. Once set to true, this will not change. + // Latencies at this point should be loaded from local storage, and updated asynchronously as needed. + // If local storage has updated latencies, then this will be set to true with 0 actual network requests. + // The loaded latencies will all be from the cache. + loaded: boolean; } => { // maxStoredLatencies is the maximum number of latencies to store per proxy in local storage. let maxStoredLatencies = 1; @@ -73,6 +78,8 @@ export const useProxyLatency = ( new Date(new Date().getTime() - proxyIntervalSeconds * 1000).toISOString(), ); + const [loaded, setLoaded] = useState(false); + // Refetch will always set the latestFetchRequest to the current time, making all the cached latencies // stale and triggering a refetch of all proxies in the list. const refetch = () => { @@ -231,6 +238,7 @@ export const useProxyLatency = ( // Local storage cleanup garbageCollectStoredLatencies(proxies, maxStoredLatencies); + setLoaded(true); }); return () => { @@ -241,6 +249,7 @@ export const useProxyLatency = ( return { proxyLatencies, refetch, + loaded, }; }; diff --git a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx index 058c8799c95e0..cb186dcb973b0 100644 --- a/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx +++ b/site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx @@ -23,6 +23,7 @@ const meta: Meta = { component: MobileMenu, args: { proxyContextValue: { + latenciesLoaded: true, proxy: { preferredPathAppURL: "", preferredWildcardHostname: "", diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index 6739f666c2b17..358b717b492a4 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -6,6 +6,7 @@ import { renderWithAuth } from "testHelpers/renderHelpers"; import { NavbarView } from "./NavbarView"; const proxyContextValue: ProxyContextValue = { + latenciesLoaded: true, proxy: { preferredPathAppURL: "", preferredWildcardHostname: "", diff --git a/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx b/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx index 6df47684173fe..15dbb18471c3f 100644 --- a/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx +++ b/site/src/modules/dashboard/Navbar/ProxyMenu.stories.tsx @@ -15,6 +15,7 @@ import { withDesktopViewport } from "testHelpers/storybook"; import { ProxyMenu } from "./ProxyMenu"; const defaultProxyContextValue = { + latenciesLoaded: true, proxyLatencies: MockProxyLatencies, proxy: getPreferredProxy(MockWorkspaceProxies, undefined), proxies: MockWorkspaceProxies, diff --git a/site/src/testHelpers/storybook.tsx b/site/src/testHelpers/storybook.tsx index ed64c10958a0b..4b2ba94bd2577 100644 --- a/site/src/testHelpers/storybook.tsx +++ b/site/src/testHelpers/storybook.tsx @@ -167,6 +167,7 @@ export const withProxyProvider = return ( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy