From 7a61f25331269f8902dcd8dc19635c54301a8db0 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Thu, 5 Dec 2024 21:09:54 -0500 Subject: [PATCH] Auditing async usage in frontend code (#1402) I found a lot of places where asyncs weren't being properly wrapped or awaited --- emain/emain-window.ts | 4 +- emain/emain.ts | 2 +- emain/updater.ts | 6 +- frontend/app/modals/tos.tsx | 12 +- frontend/app/modals/userinputmodal.tsx | 44 ++++--- frontend/app/store/keymodel.ts | 3 +- frontend/app/store/wos.ts | 3 +- frontend/app/tab/tab.tsx | 44 ++++--- frontend/app/tab/tabbar.tsx | 17 ++- frontend/app/tab/workspaceswitcher.tsx | 20 ++-- .../app/view/preview/directorypreview.tsx | 60 +++++----- frontend/app/view/preview/preview.tsx | 113 +++++++++--------- frontend/app/view/term/termwrap.ts | 10 +- frontend/app/view/waveai/waveai.tsx | 16 +-- frontend/app/view/webview/webview.tsx | 12 +- frontend/layout/lib/TileLayout.tsx | 1 - frontend/layout/lib/layoutModel.ts | 4 +- frontend/layout/lib/layoutModelHooks.ts | 4 +- 18 files changed, 194 insertions(+), 181 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 67de6c38f..393599c9e 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -165,7 +165,7 @@ export class WaveBrowserWindow extends BaseWindow { } focusedWaveWindow = this; console.log("focus win", this.waveWindowId); - fireAndForget(async () => await ClientService.FocusWindow(this.waveWindowId)); + fireAndForget(() => ClientService.FocusWindow(this.waveWindowId)); setWasInFg(true); setWasActive(true); }); @@ -235,7 +235,7 @@ export class WaveBrowserWindow extends BaseWindow { } if (this.deleteAllowed) { console.log("win removing window from backend DB", this.waveWindowId); - fireAndForget(async () => await WindowService.CloseWindow(this.waveWindowId, true)); + fireAndForget(() => WindowService.CloseWindow(this.waveWindowId, true)); } for (const tabView of this.allTabViews.values()) { tabView?.destroy(); diff --git a/emain/emain.ts b/emain/emain.ts index 9fedeb32d..2d2c15047 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -368,7 +368,7 @@ electron.ipcMain.on("quicklook", (event, filePath: string) => { electron.ipcMain.on("open-native-path", (event, filePath: string) => { console.log("open-native-path", filePath); - fireAndForget(async () => + fireAndForget(() => electron.shell.openPath(filePath).then((excuse) => { if (excuse) console.error(`Failed to open ${filePath} in native application: ${excuse}`); }) diff --git a/emain/updater.ts b/emain/updater.ts index 240020b77..93ec747a9 100644 --- a/emain/updater.ts +++ b/emain/updater.ts @@ -96,7 +96,7 @@ export class Updater { body: "A new version of Wave Terminal is ready to install.", }); updateNotification.on("click", () => { - fireAndForget(() => this.promptToInstallUpdate()); + fireAndForget(this.promptToInstallUpdate.bind(this)); }); updateNotification.show(); }); @@ -188,7 +188,7 @@ export class Updater { if (allWindows.length > 0) { await dialog.showMessageBox(focusedWaveWindow ?? allWindows[0], dialogOpts).then(({ response }) => { if (response === 0) { - fireAndForget(async () => this.installUpdate()); + fireAndForget(this.installUpdate.bind(this)); } }); } @@ -210,7 +210,7 @@ export function getResolvedUpdateChannel(): string { return isDev() ? "dev" : (autoUpdater.channel ?? "latest"); } -ipcMain.on("install-app-update", () => fireAndForget(() => updater?.promptToInstallUpdate())); +ipcMain.on("install-app-update", () => fireAndForget(updater?.promptToInstallUpdate.bind(updater))); ipcMain.on("get-app-update-status", (event) => { event.returnValue = updater?.status; }); diff --git a/frontend/app/modals/tos.tsx b/frontend/app/modals/tos.tsx index f0519ebdb..02a281a48 100644 --- a/frontend/app/modals/tos.tsx +++ b/frontend/app/modals/tos.tsx @@ -12,6 +12,7 @@ import { FlexiModal } from "./modal"; import { QuickTips } from "@/app/element/quicktips"; import { atoms, getApi } from "@/app/store/global"; import { modalsModel } from "@/app/store/modalmodel"; +import { fireAndForget } from "@/util/util"; import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from "jotai"; import "./tos.scss"; @@ -20,25 +21,22 @@ const pageNumAtom: PrimitiveAtom = atom(1); const ModalPage1 = () => { const settings = useAtomValue(atoms.settingsAtom); const clientData = useAtomValue(atoms.client); - const [tosOpen, setTosOpen] = useAtom(modalsModel.tosOpen); const [telemetryEnabled, setTelemetryEnabled] = useState(!!settings["telemetry:enabled"]); const setPageNum = useSetAtom(pageNumAtom); const acceptTos = () => { if (!clientData.tosagreed) { - services.ClientService.AgreeTos(); + fireAndForget(services.ClientService.AgreeTos); } setPageNum(2); }; const setTelemetry = (value: boolean) => { - services.ClientService.TelemetryUpdate(value) - .then(() => { + fireAndForget(() => + services.ClientService.TelemetryUpdate(value).then(() => { setTelemetryEnabled(value); }) - .catch((error) => { - console.error("failed to set telemetry:", error); - }); + ); }; const label = telemetryEnabled ? "Telemetry Enabled" : "Telemetry Disabled"; diff --git a/frontend/app/modals/userinputmodal.tsx b/frontend/app/modals/userinputmodal.tsx index daeec9755..7a3a839f0 100644 --- a/frontend/app/modals/userinputmodal.tsx +++ b/frontend/app/modals/userinputmodal.tsx @@ -5,9 +5,9 @@ import { Modal } from "@/app/modals/modal"; import { Markdown } from "@/element/markdown"; import { modalsModel } from "@/store/modalmodel"; import * as keyutil from "@/util/keyutil"; -import { UserInputService } from "../store/services"; - +import { fireAndForget } from "@/util/util"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { UserInputService } from "../store/services"; import "./userinputmodal.scss"; const UserInputModal = (userInputRequest: UserInputRequest) => { @@ -16,33 +16,39 @@ const UserInputModal = (userInputRequest: UserInputRequest) => { const checkboxRef = useRef(); const handleSendErrResponse = useCallback(() => { - UserInputService.SendUserInputResponse({ - type: "userinputresp", - requestid: userInputRequest.requestid, - errormsg: "Canceled by the user", - }); + fireAndForget(() => + UserInputService.SendUserInputResponse({ + type: "userinputresp", + requestid: userInputRequest.requestid, + errormsg: "Canceled by the user", + }) + ); modalsModel.popModal(); }, [responseText, userInputRequest]); const handleSendText = useCallback(() => { - UserInputService.SendUserInputResponse({ - type: "userinputresp", - requestid: userInputRequest.requestid, - text: responseText, - checkboxstat: checkboxRef?.current?.checked ?? false, - }); + fireAndForget(() => + UserInputService.SendUserInputResponse({ + type: "userinputresp", + requestid: userInputRequest.requestid, + text: responseText, + checkboxstat: checkboxRef?.current?.checked ?? false, + }) + ); modalsModel.popModal(); }, [responseText, userInputRequest]); console.log("bar"); const handleSendConfirm = useCallback( (response: boolean) => { - UserInputService.SendUserInputResponse({ - type: "userinputresp", - requestid: userInputRequest.requestid, - confirm: response, - checkboxstat: checkboxRef?.current?.checked ?? false, - }); + fireAndForget(() => + UserInputService.SendUserInputResponse({ + type: "userinputresp", + requestid: userInputRequest.requestid, + confirm: response, + checkboxstat: checkboxRef?.current?.checked ?? false, + }) + ); modalsModel.popModal(); }, [userInputRequest] diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index f49a44979..82daad8b9 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -19,6 +19,7 @@ import { } from "@/layout/index"; import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks"; import * as keyutil from "@/util/keyutil"; +import { fireAndForget } from "@/util/util"; import * as jotai from "jotai"; const simpleControlShiftAtom = jotai.atom(false); @@ -83,7 +84,7 @@ function genericClose(tabId: string) { return; } const layoutModel = getLayoutModelForTab(tabAtom); - layoutModel.closeFocusedNode(); + fireAndForget(layoutModel.closeFocusedNode.bind(layoutModel)); } function switchBlockByBlockNum(index: number) { diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index dadb43d49..6d78529b6 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -6,6 +6,7 @@ import { waveEventSubscribe } from "@/app/store/wps"; import { getWebServerEndpoint } from "@/util/endpoints"; import { fetch } from "@/util/fetchutil"; +import { fireAndForget } from "@/util/util"; import { atom, Atom, Getter, PrimitiveAtom, Setter, useAtomValue } from "jotai"; import { useEffect } from "react"; import { globalStore } from "./jotaiStore"; @@ -301,7 +302,7 @@ function setObjectValue(value: T, setFn?: Setter, pushToServe } setFn(wov.dataAtom, { value: value, loading: false }); if (pushToServer) { - ObjectService.UpdateObject(value, false); + fireAndForget(() => ObjectService.UpdateObject(value, false)); } } diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 302d66154..76d892610 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -6,6 +6,7 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { Button } from "@/element/button"; import { ContextMenuModel } from "@/store/contextmenu"; +import { fireAndForget } from "@/util/util"; import { clsx } from "clsx"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { ObjectService } from "../store/services"; @@ -72,14 +73,21 @@ const Tab = memo( }; }, []); - const handleRenameTab = (event) => { + const selectEditableText = useCallback(() => { + if (editableRef.current) { + const range = document.createRange(); + const selection = window.getSelection(); + range.selectNodeContents(editableRef.current); + selection.removeAllRanges(); + selection.addRange(range); + } + }, []); + + const handleRenameTab: React.MouseEventHandler = (event) => { event?.stopPropagation(); setIsEditable(true); editableTimeoutRef.current = setTimeout(() => { - if (editableRef.current) { - editableRef.current.focus(); - document.execCommand("selectAll", false); - } + selectEditableText(); }, 0); }; @@ -88,20 +96,14 @@ const Tab = memo( newText = newText || originalName; editableRef.current.innerText = newText; setIsEditable(false); - ObjectService.UpdateTabName(id, newText); + fireAndForget(() => ObjectService.UpdateTabName(id, newText)); setTimeout(() => refocusNode(null), 10); }; - const handleKeyDown = (event) => { + const handleKeyDown: React.KeyboardEventHandler = (event) => { if ((event.metaKey || event.ctrlKey) && event.key === "a") { event.preventDefault(); - if (editableRef.current) { - const range = document.createRange(); - const selection = window.getSelection(); - range.selectNodeContents(editableRef.current); - selection.removeAllRanges(); - selection.addRange(range); - } + selectEditableText(); return; } // this counts glyphs, not characters @@ -150,7 +152,10 @@ const Tab = memo( let menu: ContextMenuItem[] = [ { label: isPinned ? "Unpin Tab" : "Pin Tab", click: () => onPinChange() }, { label: "Rename Tab", click: () => handleRenameTab(null) }, - { label: "Copy TabId", click: () => navigator.clipboard.writeText(id) }, + { + label: "Copy TabId", + click: () => fireAndForget(() => navigator.clipboard.writeText(id)), + }, { type: "separator" }, ]; const fullConfig = globalStore.get(atoms.fullConfigAtom); @@ -175,10 +180,11 @@ const Tab = memo( } submenu.push({ label: preset["display:name"] ?? presetName, - click: () => { - ObjectService.UpdateObjectMeta(oref, preset); - RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 }); - }, + click: () => + fireAndForget(async () => { + await ObjectService.UpdateObjectMeta(oref, preset); + await RpcApi.ActivityCommand(TabRpcClient, { settabtheme: 1 }); + }), }); } menu.push({ label: "Backgrounds", type: "submenu", submenu }, { type: "separator" }); diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index be3994bc6..817f48262 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -467,13 +467,12 @@ const TabBar = memo(({ workspace }: TabBarProps) => { // Reset dragging state setDraggingTab(null); // Update workspace tab ids - fireAndForget( - async () => - await WorkspaceService.UpdateTabIds( - workspace.oid, - tabIds.slice(pinnedTabCount), - tabIds.slice(0, pinnedTabCount) - ) + fireAndForget(() => + WorkspaceService.UpdateTabIds( + workspace.oid, + tabIds.slice(pinnedTabCount), + tabIds.slice(0, pinnedTabCount) + ) ); }), [] @@ -579,9 +578,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const handlePinChange = useCallback( (tabId: string, pinned: boolean) => { console.log("handlePinChange", tabId, pinned); - fireAndForget(async () => { - await WorkspaceService.ChangeTabPinning(workspace.oid, tabId, pinned); - }); + fireAndForget(() => WorkspaceService.ChangeTabPinning(workspace.oid, tabId, pinned)); }, [workspace] ); diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index 80944ef8e..fc33615e2 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -189,12 +189,10 @@ const WorkspaceSwitcher = () => { }, []); const onDeleteWorkspace = useCallback((workspaceId: string) => { - fireAndForget(async () => { - getApi().deleteWorkspace(workspaceId); - setTimeout(() => { - fireAndForget(updateWorkspaceList); - }, 10); - }); + getApi().deleteWorkspace(workspaceId); + setTimeout(() => { + fireAndForget(updateWorkspaceList); + }, 10); }, []); const isActiveWorkspaceSaved = !!(activeWorkspace.name && activeWorkspace.icon); @@ -267,12 +265,10 @@ const WorkspaceSwitcherItem = ({ const isCurrentWorkspace = activeWorkspace.oid === workspace.oid; const setWorkspace = useCallback((newWorkspace: Workspace) => { - fireAndForget(async () => { - if (newWorkspace.name != "") { - setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true); - } - setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace }); - }); + if (newWorkspace.name != "") { + setObjectValue({ ...newWorkspace, otype: "workspace" }, undefined, true); + } + setWorkspaceEntry({ ...workspaceEntry, workspace: newWorkspace }); }, []); const isActive = !!workspaceEntry.windowId; diff --git a/frontend/app/view/preview/directorypreview.tsx b/frontend/app/view/preview/directorypreview.tsx index 00ab63da5..c7c4bd6ba 100644 --- a/frontend/app/view/preview/directorypreview.tsx +++ b/frontend/app/view/preview/directorypreview.tsx @@ -543,26 +543,26 @@ function TableBody({ }, { label: "Copy File Name", - click: () => navigator.clipboard.writeText(fileName), + click: () => fireAndForget(() => navigator.clipboard.writeText(fileName)), }, { label: "Copy Full File Name", - click: () => navigator.clipboard.writeText(finfo.path), + click: () => fireAndForget(() => navigator.clipboard.writeText(finfo.path)), }, { label: "Copy File Name (Shell Quoted)", - click: () => navigator.clipboard.writeText(shellQuote([fileName])), + click: () => fireAndForget(() => navigator.clipboard.writeText(shellQuote([fileName]))), }, { label: "Copy Full File Name (Shell Quoted)", - click: () => navigator.clipboard.writeText(shellQuote([finfo.path])), + click: () => fireAndForget(() => navigator.clipboard.writeText(shellQuote([finfo.path]))), }, { type: "separator", }, { label: "Download File", - click: async () => { + click: () => { getApi().downloadFile(normPath); }, }, @@ -572,7 +572,7 @@ function TableBody({ // TODO: Only show this option for local files, resolve correct host path if connection is WSL { label: openNativeLabel, - click: async () => { + click: () => { getApi().openNativePath(normPath); }, }, @@ -581,30 +581,32 @@ function TableBody({ }, { label: "Open Preview in New Block", - click: async () => { - const blockDef: BlockDef = { - meta: { - view: "preview", - file: finfo.path, - }, - }; - await createBlock(blockDef); - }, + click: () => + fireAndForget(async () => { + const blockDef: BlockDef = { + meta: { + view: "preview", + file: finfo.path, + }, + }; + await createBlock(blockDef); + }), }, ]; if (finfo.mimetype == "directory") { menu.push({ label: "Open Terminal in New Block", - click: async () => { - const termBlockDef: BlockDef = { - meta: { - controller: "shell", - view: "term", - "cmd:cwd": finfo.path, - }, - }; - await createBlock(termBlockDef); - }, + click: () => + fireAndForget(async () => { + const termBlockDef: BlockDef = { + meta: { + controller: "shell", + view: "term", + "cmd:cwd": finfo.path, + }, + }; + await createBlock(termBlockDef); + }), }); } menu.push( @@ -613,9 +615,11 @@ function TableBody({ }, { label: "Delete", - click: async () => { - await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e)); - setRefreshVersion((current) => current + 1); + click: () => { + fireAndForget(async () => { + await FileService.DeleteFile(conn, finfo.path).catch((e) => console.log(e)); + setRefreshVersion((current) => current + 1); + }); }, } ); diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 77ebd5898..618ad100f 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -24,7 +24,7 @@ import * as WOS from "@/store/wos"; import { getWebServerEndpoint } from "@/util/endpoints"; import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil"; -import { base64ToString, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util"; +import { base64ToString, fireAndForget, isBlank, jotaiLoadableValue, makeConnRoute, stringToBase64 } from "@/util/util"; import { Monaco } from "@monaco-editor/react"; import clsx from "clsx"; import { Atom, atom, Getter, PrimitiveAtom, useAtomValue, useSetAtom, WritableAtom } from "jotai"; @@ -257,7 +257,7 @@ export class PreviewModel implements ViewModel { className: clsx( `${saveClassName} warning border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500` ), - onClick: this.handleFileSave.bind(this), + onClick: () => fireAndForget(this.handleFileSave.bind(this)), }); if (get(this.canPreview)) { viewTextChildren.push({ @@ -265,7 +265,7 @@ export class PreviewModel implements ViewModel { text: "Preview", className: "grey border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500", - onClick: () => this.setEditMode(false), + onClick: () => fireAndForget(() => this.setEditMode(false)), }); } } else if (get(this.canPreview)) { @@ -274,7 +274,7 @@ export class PreviewModel implements ViewModel { text: "Edit", className: "grey border-radius-4 vertical-padding-2 horizontal-padding-10 font-size-11 font-weight-500", - onClick: () => this.setEditMode(true), + onClick: () => fireAndForget(() => this.setEditMode(true)), }); } return [ @@ -497,7 +497,7 @@ export class PreviewModel implements ViewModel { return; } const blockOref = WOS.makeORef("block", this.blockId); - services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); // Clear the saved file buffers globalStore.set(this.fileContentSaved, null); @@ -538,7 +538,7 @@ export class PreviewModel implements ViewModel { } console.log(newFileInfo.path); this.updateOpenFileModalAndError(false); - this.goHistory(newFileInfo.path); + await this.goHistory(newFileInfo.path); refocusNode(this.blockId); } catch (e) { globalStore.set(this.openFileError, e.message); @@ -546,7 +546,7 @@ export class PreviewModel implements ViewModel { } } - goHistoryBack() { + async goHistoryBack() { const blockMeta = globalStore.get(this.blockAtom)?.meta; const curPath = globalStore.get(this.metaFilePath); const updateMeta = goHistoryBack("file", curPath, blockMeta, true); @@ -555,10 +555,10 @@ export class PreviewModel implements ViewModel { } updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); - services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } - goHistoryForward() { + async goHistoryForward() { const blockMeta = globalStore.get(this.blockAtom)?.meta; const curPath = globalStore.get(this.metaFilePath); const updateMeta = goHistoryForward("file", curPath, blockMeta); @@ -567,13 +567,13 @@ export class PreviewModel implements ViewModel { } updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); - services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } - setEditMode(edit: boolean) { + async setEditMode(edit: boolean) { const blockMeta = globalStore.get(this.blockAtom)?.meta; const blockOref = WOS.makeORef("block", this.blockId); - services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit }); + await services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit }); } async handleFileSave() { @@ -588,7 +588,7 @@ export class PreviewModel implements ViewModel { } const conn = globalStore.get(this.connection) ?? ""; try { - services.FileService.SaveFile(conn, filePath, stringToBase64(newFileContent)); + await services.FileService.SaveFile(conn, filePath, stringToBase64(newFileContent)); globalStore.set(this.fileContent, newFileContent); globalStore.set(this.newFileContent, null); console.log("saved file", filePath); @@ -630,42 +630,44 @@ export class PreviewModel implements ViewModel { getSettingsMenuItems(): ContextMenuItem[] { const menuItems: ContextMenuItem[] = []; - const blockData = globalStore.get(this.blockAtom); menuItems.push({ label: "Copy Full Path", - click: async () => { - const filePath = await globalStore.get(this.normFilePath); - if (filePath == null) { - return; - } - navigator.clipboard.writeText(filePath); - }, + click: () => + fireAndForget(async () => { + const filePath = await globalStore.get(this.normFilePath); + if (filePath == null) { + return; + } + await navigator.clipboard.writeText(filePath); + }), }); menuItems.push({ label: "Copy File Name", - click: async () => { - const fileInfo = await globalStore.get(this.statFile); - if (fileInfo == null || fileInfo.name == null) { - return; - } - navigator.clipboard.writeText(fileInfo.name); - }, + click: () => + fireAndForget(async () => { + const fileInfo = await globalStore.get(this.statFile); + if (fileInfo == null || fileInfo.name == null) { + return; + } + await navigator.clipboard.writeText(fileInfo.name); + }), }); const mimeType = jotaiLoadableValue(globalStore.get(this.fileMimeTypeLoadable), ""); if (mimeType == "directory") { menuItems.push({ label: "Open Terminal in New Block", - click: async () => { - const fileInfo = await globalStore.get(this.statFile); - const termBlockDef: BlockDef = { - meta: { - view: "term", - controller: "shell", - "cmd:cwd": fileInfo.dir, - }, - }; - await createBlock(termBlockDef); - }, + click: () => + fireAndForget(async () => { + const fileInfo = await globalStore.get(this.statFile); + const termBlockDef: BlockDef = { + meta: { + view: "term", + controller: "shell", + "cmd:cwd": fileInfo.dir, + }, + }; + await createBlock(termBlockDef); + }), }); } const loadableSV = globalStore.get(this.loadableSpecializedView); @@ -677,11 +679,11 @@ export class PreviewModel implements ViewModel { menuItems.push({ type: "separator" }); menuItems.push({ label: "Save File", - click: this.handleFileSave.bind(this), + click: () => fireAndForget(this.handleFileSave.bind(this)), }); menuItems.push({ label: "Revert File", - click: this.handleFileRevert.bind(this), + click: () => fireAndForget(this.handleFileRevert.bind(this)), }); } menuItems.push({ type: "separator" }); @@ -689,12 +691,13 @@ export class PreviewModel implements ViewModel { label: "Word Wrap", type: "checkbox", checked: wordWrap, - click: () => { - const blockOref = WOS.makeORef("block", this.blockId); - services.ObjectService.UpdateObjectMeta(blockOref, { - "editor:wordwrap": !wordWrap, - }); - }, + click: () => + fireAndForget(async () => { + const blockOref = WOS.makeORef("block", this.blockId); + await services.ObjectService.UpdateObjectMeta(blockOref, { + "editor:wordwrap": !wordWrap, + }); + }), }); } } @@ -716,16 +719,16 @@ export class PreviewModel implements ViewModel { keyDownHandler(e: WaveKeyboardEvent): boolean { if (checkKeyPressed(e, "Cmd:ArrowLeft")) { - this.goHistoryBack(); + fireAndForget(this.goHistoryBack.bind(this)); return true; } if (checkKeyPressed(e, "Cmd:ArrowRight")) { - this.goHistoryForward(); + fireAndForget(this.goHistoryForward.bind(this)); return true; } if (checkKeyPressed(e, "Cmd:ArrowUp")) { // handle up directory - this.goParentDirectory({}); + fireAndForget(() => this.goParentDirectory({})); return true; } const openModalOpen = globalStore.get(this.openFileModal); @@ -739,7 +742,7 @@ export class PreviewModel implements ViewModel { if (canPreview) { if (checkKeyPressed(e, "Cmd:e")) { const editMode = globalStore.get(this.editMode); - this.setEditMode(!editMode); + fireAndForget(() => this.setEditMode(!editMode)); return true; } } @@ -833,15 +836,15 @@ function CodeEditPreview({ model }: SpecializedViewProps) { function codeEditKeyDownHandler(e: WaveKeyboardEvent): boolean { if (checkKeyPressed(e, "Cmd:e")) { - model.setEditMode(false); + fireAndForget(() => model.setEditMode(false)); return true; } if (checkKeyPressed(e, "Cmd:s") || checkKeyPressed(e, "Ctrl:s")) { - model.handleFileSave(); + fireAndForget(model.handleFileSave.bind(model)); return true; } if (checkKeyPressed(e, "Cmd:r")) { - model.handleFileRevert(); + fireAndForget(model.handleFileRevert.bind(model)); return true; } return false; @@ -990,7 +993,7 @@ const OpenFileModal = memo( const handleCommandOperations = async () => { if (checkKeyPressed(waveEvent, "Enter")) { - model.handleOpenFile(filePath); + await model.handleOpenFile(filePath); return true; } return false; diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 270850c6c..c26ab2a32 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -119,7 +119,11 @@ export class TermWrap { data = data.substring(nextSlashIdx); } setTimeout(() => { - services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { "cmd:cwd": data }); + fireAndForget(() => + services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { + "cmd:cwd": data, + }) + ); }, 0); return true; }); @@ -284,7 +288,9 @@ export class TermWrap { const serializedOutput = this.serializeAddon.serialize(); const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols }; console.log("idle timeout term", this.dataBytesProcessed, serializedOutput.length, termSize); - services.BlockService.SaveTerminalState(this.blockId, serializedOutput, "full", this.ptyOffset, termSize); + fireAndForget(() => + services.BlockService.SaveTerminalState(this.blockId, serializedOutput, "full", this.ptyOffset, termSize) + ); this.dataBytesProcessed = 0; } diff --git a/frontend/app/view/waveai/waveai.tsx b/frontend/app/view/waveai/waveai.tsx index cae5b8856..f29e5fa04 100644 --- a/frontend/app/view/waveai/waveai.tsx +++ b/frontend/app/view/waveai/waveai.tsx @@ -221,12 +221,12 @@ export class WaveAiModel implements ViewModel { ({ label: preset[1]["display:name"], onClick: () => - fireAndForget(async () => { - await ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { + fireAndForget(() => + ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { ...preset[1], "ai:preset": preset[0], - }); - }), + }) + ), }) as MenuItem ); dropdownItems.push({ @@ -386,7 +386,7 @@ export class WaveAiModel implements ViewModel { this.setLocked(false); this.cancel = false; }; - handleAiStreamingResponse(); + fireAndForget(handleAiStreamingResponse); } useWaveAi() { @@ -404,14 +404,14 @@ export class WaveAiModel implements ViewModel { keyDownHandler(waveEvent: WaveKeyboardEvent): boolean { if (checkKeyPressed(waveEvent, "Cmd:l")) { - this.clearMessages(); + fireAndForget(this.clearMessages.bind(this)); return true; } return false; } } -function makeWaveAiViewModel(blockId): WaveAiModel { +function makeWaveAiViewModel(blockId: string): WaveAiModel { const waveAiModel = new WaveAiModel(blockId); return waveAiModel; } @@ -634,7 +634,7 @@ const WaveAi = ({ model }: { model: WaveAiModel; blockId: string }) => { // a weird workaround to initialize ansynchronously useEffect(() => { - model.populateMessages(); + fireAndForget(model.populateMessages.bind(model)); }, []); const handleTextAreaChange = (e: React.ChangeEvent) => { diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx index dee0eef88..bfc8e11f2 100644 --- a/frontend/app/view/webview/webview.tsx +++ b/frontend/app/view/webview/webview.tsx @@ -293,7 +293,7 @@ export class WebViewModel implements ViewModel { * @param url The URL that has been navigated to. */ handleNavigate(url: string) { - ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { url }); + fireAndForget(() => ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), { url })); globalStore.set(this.url, url); } @@ -432,22 +432,18 @@ export class WebViewModel implements ViewModel { return [ { label: "Set Block Homepage", - click: async () => { - await this.setHomepageUrl(this.getUrl(), "block"); - }, + click: () => fireAndForget(() => this.setHomepageUrl(this.getUrl(), "block")), }, { label: "Set Default Homepage", - click: async () => { - await this.setHomepageUrl(this.getUrl(), "global"); - }, + click: () => fireAndForget(() => this.setHomepageUrl(this.getUrl(), "global")), }, { type: "separator", }, { label: this.webviewRef.current?.isDevToolsOpened() ? "Close DevTools" : "Open DevTools", - click: async () => { + click: () => { if (this.webviewRef.current) { if (this.webviewRef.current.isDevToolsOpened()) { this.webviewRef.current.closeDevTools(); diff --git a/frontend/layout/lib/TileLayout.tsx b/frontend/layout/lib/TileLayout.tsx index d78aa2e2e..5b45a9e20 100644 --- a/frontend/layout/lib/TileLayout.tsx +++ b/frontend/layout/lib/TileLayout.tsx @@ -58,7 +58,6 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr const setActiveDrag = useSetAtom(layoutModel.activeDrag); const setReady = useSetAtom(layoutModel.ready); const isResizing = useAtomValue(layoutModel.isResizing); - const ephemeralNode = useAtomValue(layoutModel.ephemeralNode); const { activeDrag, dragClientOffset } = useDragLayer((monitor) => ({ activeDrag: monitor.isDragging(), diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 3d19d26ef..1a70fc22d 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { getSettingsKeyAtom } from "@/app/store/global"; -import { atomWithThrottle, boundNumber } from "@/util/util"; +import { atomWithThrottle, boundNumber, fireAndForget } from "@/util/util"; import { Atom, atom, Getter, PrimitiveAtom, Setter } from "jotai"; import { splitAtom } from "jotai/utils"; import { createRef, CSSProperties } from "react"; @@ -852,7 +852,7 @@ export class LayoutModel { animationTimeS: this.animationTimeS, ready: this.ready, disablePointerEvents: this.activeDrag, - onClose: async () => await this.closeNode(nodeid), + onClose: () => fireAndForget(() => this.closeNode(nodeid)), toggleMagnify: () => this.magnifyNodeToggle(nodeid), focusNode: () => this.focusNode(nodeid), dragHandleRef: createRef(), diff --git a/frontend/layout/lib/layoutModelHooks.ts b/frontend/layout/lib/layoutModelHooks.ts index 12b840c33..9d9a39394 100644 --- a/frontend/layout/lib/layoutModelHooks.ts +++ b/frontend/layout/lib/layoutModelHooks.ts @@ -24,7 +24,7 @@ export function getLayoutModelForTab(tabAtom: Atom): LayoutModel { } const layoutTreeStateAtom = withLayoutTreeStateAtomFromTab(tabAtom); const layoutModel = new LayoutModel(layoutTreeStateAtom, globalStore.get, globalStore.set); - globalStore.sub(layoutTreeStateAtom, () => fireAndForget(async () => layoutModel.onTreeStateAtomUpdated())); + globalStore.sub(layoutTreeStateAtom, () => fireAndForget(layoutModel.onTreeStateAtomUpdated.bind(layoutModel))); layoutModelMap.set(tabId, layoutModel); return layoutModel; } @@ -56,7 +56,7 @@ export function useTileLayout(tabAtom: Atom, tileContent: TileLayoutContent useOnResize(layoutModel?.displayContainerRef, layoutModel?.onContainerResize); // Once the TileLayout is mounted, re-run the state update to get all the nodes to flow in the layout. - useEffect(() => fireAndForget(async () => layoutModel.onTreeStateAtomUpdated(true)), []); + useEffect(() => fireAndForget(() => layoutModel.onTreeStateAtomUpdated(true)), []); useEffect(() => layoutModel.registerTileLayout(tileContent), [tileContent]); return layoutModel;