Browse Source

fix: make tunnels work in multi-instance scenarios (#6178)

* fix: make tunnels work in multi-instance scenarios

* factor tunnels out

* use tunnel-rat fork until upsteam updated
David Luzar 2 years ago
parent
commit
7562d9b533

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     ]
   },
   "dependencies": {
+    "@dwelle/tunnel-rat": "0.1.1",
     "@sentry/browser": "6.2.5",
     "@sentry/integrations": "6.2.5",
     "@testing-library/jest-dom": "5.16.2",

+ 17 - 12
src/components/LayerUI.tsx

@@ -40,17 +40,12 @@ import { actionToggleStats } from "../actions/actionToggleStats";
 import Footer from "./footer/Footer";
 import { hostSidebarCountersAtom } from "./Sidebar/Sidebar";
 import { jotaiScope } from "../jotai";
-import { useAtom } from "jotai";
+import { Provider, useAtom } from "jotai";
 import MainMenu from "./main-menu/MainMenu";
 import { ActiveConfirmDialog } from "./ActiveConfirmDialog";
 import { HandButton } from "./HandButton";
 import { isHandToolActive } from "../appState";
-import {
-  mainMenuTunnel,
-  welcomeScreenMenuHintTunnel,
-  welcomeScreenToolbarHintTunnel,
-  welcomeScreenCenterTunnel,
-} from "./tunnels";
+import { TunnelsContext, useInitializeTunnels } from "./context/tunnels";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -130,6 +125,8 @@ const LayerUI = ({
 }: LayerUIProps) => {
   const device = useDevice();
 
+  const tunnels = useInitializeTunnels();
+
   const renderJSONExportDialog = () => {
     if (!UIOptions.canvasActions.export) {
       return null;
@@ -201,8 +198,8 @@ const LayerUI = ({
     <div style={{ position: "relative" }}>
       {/* wrapping to Fragment stops React from occasionally complaining
                 about identical Keys */}
-      <mainMenuTunnel.Out />
-      {renderWelcomeScreen && <welcomeScreenMenuHintTunnel.Out />}
+      <tunnels.mainMenuTunnel.Out />
+      {renderWelcomeScreen && <tunnels.welcomeScreenMenuHintTunnel.Out />}
     </div>
   );
 
@@ -254,7 +251,7 @@ const LayerUI = ({
               {(heading: React.ReactNode) => (
                 <div style={{ position: "relative" }}>
                   {renderWelcomeScreen && (
-                    <welcomeScreenToolbarHintTunnel.Out />
+                    <tunnels.welcomeScreenToolbarHintTunnel.Out />
                   )}
                   <Stack.Col gap={4} align="start">
                     <Stack.Row
@@ -354,7 +351,7 @@ const LayerUI = ({
 
   const [hostSidebarCounters] = useAtom(hostSidebarCountersAtom, jotaiScope);
 
-  return (
+  const layerUIJSX = (
     <>
       {/* ------------------------- tunneled UI ---------------------------- */}
       {/* make sure we render host app components first so that we can detect
@@ -434,7 +431,7 @@ const LayerUI = ({
                 : {}
             }
           >
-            {renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
+            {renderWelcomeScreen && <tunnels.welcomeScreenCenterTunnel.Out />}
             {renderFixedSideContainer()}
             <Footer
               appState={appState}
@@ -471,6 +468,14 @@ const LayerUI = ({
       )}
     </>
   );
+
+  return (
+    <Provider scope={tunnels.jotaiScope}>
+      <TunnelsContext.Provider value={tunnels}>
+        {layerUIJSX}
+      </TunnelsContext.Provider>
+    </Provider>
+  );
 };
 
 const stripIrrelevantAppStateProps = (

+ 2 - 1
src/components/MobileMenu.tsx

@@ -19,7 +19,7 @@ import { Stats } from "./Stats";
 import { actionToggleStats } from "../actions";
 import { HandButton } from "./HandButton";
 import { isHandToolActive } from "../appState";
-import { mainMenuTunnel, welcomeScreenCenterTunnel } from "./tunnels";
+import { useTunnels } from "./context/tunnels";
 
 type MobileMenuProps = {
   appState: AppState;
@@ -58,6 +58,7 @@ export const MobileMenu = ({
   renderSidebars,
   device,
 }: MobileMenuProps) => {
+  const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
   const renderToolbar = () => {
     return (
       <FixedSideContainer side="top" className="App-top-bar">

+ 32 - 0
src/components/context/tunnels.ts

@@ -0,0 +1,32 @@
+import React from "react";
+import tunnel from "@dwelle/tunnel-rat";
+
+type Tunnel = ReturnType<typeof tunnel>;
+
+type TunnelsContextValue = {
+  mainMenuTunnel: Tunnel;
+  welcomeScreenMenuHintTunnel: Tunnel;
+  welcomeScreenToolbarHintTunnel: Tunnel;
+  welcomeScreenHelpHintTunnel: Tunnel;
+  welcomeScreenCenterTunnel: Tunnel;
+  footerCenterTunnel: Tunnel;
+  jotaiScope: symbol;
+};
+
+export const TunnelsContext = React.createContext<TunnelsContextValue>(null!);
+
+export const useTunnels = () => React.useContext(TunnelsContext);
+
+export const useInitializeTunnels = () => {
+  return React.useMemo((): TunnelsContextValue => {
+    return {
+      mainMenuTunnel: tunnel(),
+      welcomeScreenMenuHintTunnel: tunnel(),
+      welcomeScreenToolbarHintTunnel: tunnel(),
+      welcomeScreenHelpHintTunnel: tunnel(),
+      welcomeScreenCenterTunnel: tunnel(),
+      footerCenterTunnel: tunnel(),
+      jotaiScope: Symbol(),
+    };
+  }, []);
+};

+ 3 - 1
src/components/footer/Footer.tsx

@@ -9,10 +9,10 @@ import {
   ZoomActions,
 } from "../Actions";
 import { useDevice } from "../App";
+import { useTunnels } from "../context/tunnels";
 import { HelpButton } from "../HelpButton";
 import { Section } from "../Section";
 import Stack from "../Stack";
-import { footerCenterTunnel, welcomeScreenHelpHintTunnel } from "../tunnels";
 
 const Footer = ({
   appState,
@@ -25,6 +25,8 @@ const Footer = ({
   showExitZenModeBtn: boolean;
   renderWelcomeScreen: boolean;
 }) => {
+  const { footerCenterTunnel, welcomeScreenHelpHintTunnel } = useTunnels();
+
   const device = useDevice();
   const showFinalize =
     !appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;

+ 2 - 1
src/components/footer/FooterCenter.tsx

@@ -1,9 +1,10 @@
 import clsx from "clsx";
 import { useExcalidrawAppState } from "../App";
-import { footerCenterTunnel } from "../tunnels";
+import { useTunnels } from "../context/tunnels";
 import "./FooterCenter.scss";
 
 const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
+  const { footerCenterTunnel } = useTunnels();
   const appState = useExcalidrawAppState();
   return (
     <footerCenterTunnel.In>

+ 3 - 1
src/components/hoc/withInternalFallback.tsx

@@ -1,5 +1,6 @@
 import { atom, useAtom } from "jotai";
 import React, { useLayoutEffect } from "react";
+import { useTunnels } from "../context/tunnels";
 
 export const withInternalFallback = <P,>(
   componentName: string,
@@ -17,7 +18,8 @@ export const withInternalFallback = <P,>(
       __fallback?: boolean;
     }
   > = (props) => {
-    const [counter, setCounter] = useAtom(counterAtom);
+    const { jotaiScope } = useTunnels();
+    const [counter, setCounter] = useAtom(counterAtom, jotaiScope);
 
     useLayoutEffect(() => {
       setCounter((counter) => counter + 1);

+ 2 - 1
src/components/main-menu/MainMenu.tsx

@@ -13,7 +13,7 @@ import { t } from "../../i18n";
 import { HamburgerMenuIcon } from "../icons";
 import { withInternalFallback } from "../hoc/withInternalFallback";
 import { composeEventHandlers } from "../../utils";
-import { mainMenuTunnel } from "../tunnels";
+import { useTunnels } from "../context/tunnels";
 
 const MainMenu = Object.assign(
   withInternalFallback(
@@ -28,6 +28,7 @@ const MainMenu = Object.assign(
        */
       onSelect?: (event: Event) => void;
     }) => {
+      const { mainMenuTunnel } = useTunnels();
       const device = useDevice();
       const appState = useExcalidrawAppState();
       const setAppState = useExcalidrawSetAppState();

+ 0 - 8
src/components/tunnels.ts

@@ -1,8 +0,0 @@
-import tunnel from "tunnel-rat";
-
-export const mainMenuTunnel = tunnel();
-export const welcomeScreenMenuHintTunnel = tunnel();
-export const welcomeScreenToolbarHintTunnel = tunnel();
-export const welcomeScreenHelpHintTunnel = tunnel();
-export const welcomeScreenCenterTunnel = tunnel();
-export const footerCenterTunnel = tunnel();

+ 2 - 1
src/components/welcome-screen/WelcomeScreen.Center.tsx

@@ -6,8 +6,8 @@ import {
   useExcalidrawActionManager,
   useExcalidrawAppState,
 } from "../App";
+import { useTunnels } from "../context/tunnels";
 import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
-import { welcomeScreenCenterTunnel } from "../tunnels";
 
 const WelcomeScreenMenuItemContent = ({
   icon,
@@ -89,6 +89,7 @@ const WelcomeScreenMenuItemLink = ({
 WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
 
 const Center = ({ children }: { children?: React.ReactNode }) => {
+  const { welcomeScreenCenterTunnel } = useTunnels();
   return (
     <welcomeScreenCenterTunnel.In>
       <div className="welcome-screen-center">

+ 4 - 5
src/components/welcome-screen/WelcomeScreen.Hints.tsx

@@ -1,16 +1,13 @@
 import { t } from "../../i18n";
+import { useTunnels } from "../context/tunnels";
 import {
   WelcomeScreenHelpArrow,
   WelcomeScreenMenuArrow,
   WelcomeScreenTopToolbarArrow,
 } from "../icons";
-import {
-  welcomeScreenMenuHintTunnel,
-  welcomeScreenToolbarHintTunnel,
-  welcomeScreenHelpHintTunnel,
-} from "../tunnels";
 
 const MenuHint = ({ children }: { children?: React.ReactNode }) => {
+  const { welcomeScreenMenuHintTunnel } = useTunnels();
   return (
     <welcomeScreenMenuHintTunnel.In>
       <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu">
@@ -25,6 +22,7 @@ const MenuHint = ({ children }: { children?: React.ReactNode }) => {
 MenuHint.displayName = "MenuHint";
 
 const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
+  const { welcomeScreenToolbarHintTunnel } = useTunnels();
   return (
     <welcomeScreenToolbarHintTunnel.In>
       <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar">
@@ -39,6 +37,7 @@ const ToolbarHint = ({ children }: { children?: React.ReactNode }) => {
 ToolbarHint.displayName = "ToolbarHint";
 
 const HelpHint = ({ children }: { children?: React.ReactNode }) => {
+  const { welcomeScreenHelpHintTunnel } = useTunnels();
   return (
     <welcomeScreenHelpHintTunnel.In>
       <div className="virgil welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help">

+ 8 - 1
yarn.lock

@@ -1437,6 +1437,13 @@
   resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
   integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
 
+"@dwelle/tunnel-rat@0.1.1":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@dwelle/tunnel-rat/-/tunnel-rat-0.1.1.tgz#0a0b235f8fc22ff1cf47ed102f4cc612eb51bc71"
+  integrity sha512-jb5/ZsT/af1J7tnbBXp7KO1xEyw61lWSDqJ+Bqdc6JlL3vbAvsifNhe+/mRFs6aSBCRaDqp5f2pJDHtA3MUZLw==
+  dependencies:
+    zustand "^4.3.2"
+
 "@eslint/eslintrc@^0.4.3":
   version "0.4.3"
   resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@@ -11028,7 +11035,7 @@ yocto-queue@^0.1.0:
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
 
-zustand@^4.1.0:
+zustand@^4.1.0, zustand@^4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.3.2.tgz#bb121fcad84c5a569e94bd1a2695e1a93ba85d39"
   integrity sha512-rd4haDmlwMTVWVqwvgy00ny8rtti/klRoZjFbL/MAcDnmD5qSw/RZc+Vddstdv90M5Lv6RPgWvm1Hivyn0QgJw==