优化事件处理
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { acpListAgents } from "@/lib/tauri"
|
import { acpListAgents } from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import type { AgentType, AcpAgentInfo } from "@/lib/types"
|
import type { AgentType, AcpAgentInfo } from "@/lib/types"
|
||||||
import { AGENT_LABELS } from "@/lib/types"
|
import { AGENT_LABELS } from "@/lib/types"
|
||||||
import { AgentIcon } from "@/components/agent-icon"
|
import { AgentIcon } from "@/components/agent-icon"
|
||||||
@@ -100,7 +101,7 @@ export function AgentSelector({
|
|||||||
)
|
)
|
||||||
.then((dispose) => {
|
.then((dispose) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
dispose()
|
disposeTauriListener(dispose, "AgentSelector.agentsUpdated")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
unlisten = dispose
|
unlisten = dispose
|
||||||
@@ -112,9 +113,7 @@ export function AgentSelector({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
window.removeEventListener("focus", onWindowFocus)
|
window.removeEventListener("focus", onWindowFocus)
|
||||||
if (unlisten) {
|
disposeTauriListener(unlisten, "AgentSelector.agentsUpdated")
|
||||||
unlisten()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [defaultAgentType])
|
}, [defaultAgentType])
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { TauriEvent } from "@tauri-apps/api/event"
|
||||||
import { getCurrentWebview } from "@tauri-apps/api/webview"
|
import { getCurrentWebview } from "@tauri-apps/api/webview"
|
||||||
import { open } from "@tauri-apps/plugin-dialog"
|
import { open } from "@tauri-apps/plugin-dialog"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
@@ -10,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea"
|
|||||||
import { FileSearch, Plus, Send, Square, X } from "lucide-react"
|
import { FileSearch, Plus, Send, Square, X } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { readFileBase64 } from "@/lib/tauri"
|
import { readFileBase64 } from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import type {
|
import type {
|
||||||
AvailableCommandInfo,
|
AvailableCommandInfo,
|
||||||
PromptCapabilitiesInfo,
|
PromptCapabilitiesInfo,
|
||||||
@@ -686,46 +688,109 @@ export function MessageInput({
|
|||||||
}, [appendResourceAttachments, attachmentTabId])
|
}, [appendResourceAttachments, attachmentTabId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlisten: (() => void) | null = null
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
const unlisteners: Array<() => void | Promise<void>> = []
|
||||||
|
|
||||||
getCurrentWebview()
|
const cleanupListeners = () => {
|
||||||
.onDragDropEvent((event) => {
|
for (const fn of unlisteners.splice(0)) {
|
||||||
const host = containerRef.current
|
disposeTauriListener(fn, "MessageInput.dragDrop")
|
||||||
if (!host) return
|
}
|
||||||
const payload = event.payload
|
}
|
||||||
if (payload.type === "leave") {
|
|
||||||
setIsDragActive(false)
|
type DragDropPayload =
|
||||||
return
|
| {
|
||||||
|
type: "enter" | "drop"
|
||||||
|
paths: string[]
|
||||||
|
position: { x: number; y: number }
|
||||||
}
|
}
|
||||||
const inside = pointWithinElement(payload.position, host)
|
| {
|
||||||
if (payload.type === "drop") {
|
type: "over"
|
||||||
setIsDragActive(false)
|
position: { x: number; y: number }
|
||||||
if (Date.now() - lastDomDropAtRef.current < 250) return
|
}
|
||||||
if (!inside || disabled || isPrompting) return
|
| { type: "leave" }
|
||||||
void appendPathsFromDrop(payload.paths).catch((error) => {
|
|
||||||
console.error("[MessageInput] drag drop paths failed:", error)
|
const handlePayload = (payload: DragDropPayload) => {
|
||||||
|
const host = containerRef.current
|
||||||
|
if (!host) return
|
||||||
|
if (payload.type === "leave") {
|
||||||
|
setIsDragActive(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const inside = pointWithinElement(payload.position, host)
|
||||||
|
if (payload.type === "drop") {
|
||||||
|
setIsDragActive(false)
|
||||||
|
if (Date.now() - lastDomDropAtRef.current < 250) return
|
||||||
|
if (!inside || disabled || isPrompting) return
|
||||||
|
void appendPathsFromDrop(payload.paths).catch((error) => {
|
||||||
|
console.error("[MessageInput] drag drop paths failed:", error)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsDragActive(inside && !disabled && !isPrompting)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
const webview = getCurrentWebview()
|
||||||
|
try {
|
||||||
|
const unlistenEnter = await webview.listen<{
|
||||||
|
paths: string[]
|
||||||
|
position: { x: number; y: number }
|
||||||
|
}>(TauriEvent.DRAG_ENTER, (event) => {
|
||||||
|
if (cancelled) return
|
||||||
|
handlePayload({
|
||||||
|
type: "enter",
|
||||||
|
paths: event.payload.paths,
|
||||||
|
position: event.payload.position,
|
||||||
})
|
})
|
||||||
return
|
})
|
||||||
}
|
unlisteners.push(unlistenEnter)
|
||||||
setIsDragActive(inside && !disabled && !isPrompting)
|
|
||||||
})
|
const unlistenOver = await webview.listen<{
|
||||||
.then((fn) => {
|
position: { x: number; y: number }
|
||||||
if (cancelled) {
|
}>(TauriEvent.DRAG_OVER, (event) => {
|
||||||
fn()
|
if (cancelled) return
|
||||||
} else {
|
handlePayload({
|
||||||
unlisten = fn
|
type: "over",
|
||||||
}
|
position: event.payload.position,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
})
|
||||||
|
unlisteners.push(unlistenOver)
|
||||||
|
|
||||||
|
const unlistenDrop = await webview.listen<{
|
||||||
|
paths: string[]
|
||||||
|
position: { x: number; y: number }
|
||||||
|
}>(TauriEvent.DRAG_DROP, (event) => {
|
||||||
|
if (cancelled) return
|
||||||
|
handlePayload({
|
||||||
|
type: "drop",
|
||||||
|
paths: event.payload.paths,
|
||||||
|
position: event.payload.position,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
unlisteners.push(unlistenDrop)
|
||||||
|
|
||||||
|
const unlistenLeave = await webview.listen(
|
||||||
|
TauriEvent.DRAG_LEAVE,
|
||||||
|
() => {
|
||||||
|
if (cancelled) return
|
||||||
|
handlePayload({ type: "leave" })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
unlisteners.push(unlistenLeave)
|
||||||
|
} catch {
|
||||||
// Ignore non-Tauri environments.
|
// Ignore non-Tauri environments.
|
||||||
})
|
} finally {
|
||||||
|
if (cancelled) {
|
||||||
|
cleanupListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
if (unlisten) {
|
cleanupListeners()
|
||||||
unlisten()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [appendPathsFromDrop, disabled, isPrompting])
|
}, [appendPathsFromDrop, disabled, isPrompting])
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
updateConversationStatus,
|
updateConversationStatus,
|
||||||
updateConversationExternalId,
|
updateConversationExternalId,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import { AgentSelector } from "@/components/chat/agent-selector"
|
import { AgentSelector } from "@/components/chat/agent-selector"
|
||||||
import { LiveMessageBlock } from "@/components/chat/live-message-block"
|
import { LiveMessageBlock } from "@/components/chat/live-message-block"
|
||||||
import { AgentPlanOverlay } from "@/components/chat/agent-plan-overlay"
|
import { AgentPlanOverlay } from "@/components/chat/agent-plan-overlay"
|
||||||
@@ -448,7 +449,7 @@ export function WelcomeInputPanel({
|
|||||||
)
|
)
|
||||||
.then((dispose) => {
|
.then((dispose) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
dispose()
|
disposeTauriListener(dispose, "WelcomeInputPanel.agentsUpdated")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
unlisten = dispose
|
unlisten = dispose
|
||||||
@@ -463,9 +464,7 @@ export function WelcomeInputPanel({
|
|||||||
clearTimeout(agentStatusRefreshTimerRef.current)
|
clearTimeout(agentStatusRefreshTimerRef.current)
|
||||||
agentStatusRefreshTimerRef.current = null
|
agentStatusRefreshTimerRef.current = null
|
||||||
}
|
}
|
||||||
if (unlisten) {
|
disposeTauriListener(unlisten, "WelcomeInputPanel.agentsUpdated")
|
||||||
unlisten()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type IntlLocale,
|
type IntlLocale,
|
||||||
} from "@/lib/i18n"
|
} from "@/lib/i18n"
|
||||||
import { getSystemLanguageSettings } from "@/lib/tauri"
|
import { getSystemLanguageSettings } from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import { AppBootLoading } from "@/components/layout/app-boot-loading"
|
import { AppBootLoading } from "@/components/layout/app-boot-loading"
|
||||||
import type { AppLocale, SystemLanguageSettings } from "@/lib/types"
|
import type { AppLocale, SystemLanguageSettings } from "@/lib/types"
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ export function AppI18nProvider({
|
|||||||
)
|
)
|
||||||
.then((dispose) => {
|
.then((dispose) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
dispose()
|
disposeTauriListener(dispose, "I18nProvider.languageSettings")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
unlisten = dispose
|
unlisten = dispose
|
||||||
@@ -174,9 +175,7 @@ export function AppI18nProvider({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
window.removeEventListener("storage", onStorage)
|
window.removeEventListener("storage", onStorage)
|
||||||
if (unlisten) {
|
disposeTauriListener(unlisten, "I18nProvider.languageSettings")
|
||||||
unlisten()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [setLanguageSettings])
|
}, [setLanguageSettings])
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
startFileTreeWatch,
|
startFileTreeWatch,
|
||||||
stopFileTreeWatch,
|
stopFileTreeWatch,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import { emitAttachFileToSession } from "@/lib/session-attachment-events"
|
import { emitAttachFileToSession } from "@/lib/session-attachment-events"
|
||||||
import type {
|
import type {
|
||||||
FileTreeChangedEvent,
|
FileTreeChangedEvent,
|
||||||
@@ -1961,9 +1962,7 @@ export function FileTreeTab() {
|
|||||||
pendingTreeRefreshRef.current = false
|
pendingTreeRefreshRef.current = false
|
||||||
pendingTreeRefreshNeedsStatusRef.current = false
|
pendingTreeRefreshNeedsStatusRef.current = false
|
||||||
pendingStatusRefreshRef.current = false
|
pendingStatusRefreshRef.current = false
|
||||||
if (unlisten) {
|
disposeTauriListener(unlisten, "AuxPanelFileTree.fileTreeChanged")
|
||||||
unlisten()
|
|
||||||
}
|
|
||||||
void stopFileTreeWatch(rootPath)
|
void stopFileTreeWatch(rootPath)
|
||||||
}
|
}
|
||||||
}, [fetchTree, folder?.path, openFilePreview, t])
|
}, [fetchTree, folder?.path, openFilePreview, t])
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
startFileTreeWatch,
|
startFileTreeWatch,
|
||||||
stopFileTreeWatch,
|
stopFileTreeWatch,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import type { FileTreeChangedEvent, GitStatusEntry } from "@/lib/types"
|
import type { FileTreeChangedEvent, GitStatusEntry } from "@/lib/types"
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -630,9 +631,7 @@ export function GitChangesTab() {
|
|||||||
clearTimeout(refreshTimerRef.current)
|
clearTimeout(refreshTimerRef.current)
|
||||||
refreshTimerRef.current = null
|
refreshTimerRef.current = null
|
||||||
}
|
}
|
||||||
if (unlisten) {
|
disposeTauriListener(unlisten, "AuxPanelGitChanges.fileTreeChanged")
|
||||||
unlisten()
|
|
||||||
}
|
|
||||||
void stopFileTreeWatch(rootPath)
|
void stopFileTreeWatch(rootPath)
|
||||||
}
|
}
|
||||||
}, [fetchChanges, folder?.path, isChangesTabActive])
|
}, [fetchChanges, folder?.path, isChangesTabActive])
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import {
|
|||||||
openCommitWindow,
|
openCommitWindow,
|
||||||
setFolderParentBranch,
|
setFolderParentBranch,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import type { GitBranchList } from "@/lib/types"
|
import type { GitBranchList } from "@/lib/types"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useFolderContext } from "@/contexts/folder-context"
|
import { useFolderContext } from "@/contexts/folder-context"
|
||||||
@@ -161,7 +162,7 @@ export function BranchDropdown({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (unlisten) unlisten()
|
disposeTauriListener(unlisten, "BranchDropdown.gitCommitSucceeded")
|
||||||
}
|
}
|
||||||
}, [folder, onBranchChange, t])
|
}, [folder, onBranchChange, t])
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
terminalKill,
|
terminalKill,
|
||||||
terminalList,
|
terminalList,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import type { FolderCommand, TerminalEvent } from "@/lib/types"
|
import type { FolderCommand, TerminalEvent } from "@/lib/types"
|
||||||
import { CommandManageDialog } from "./command-manage-dialog"
|
import { CommandManageDialog } from "./command-manage-dialog"
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export function CommandDropdown() {
|
|||||||
const clearRunningByTerminalId = useCallback((terminalId: string) => {
|
const clearRunningByTerminalId = useCallback((terminalId: string) => {
|
||||||
const unlisten = exitUnlistenersRef.current.get(terminalId)
|
const unlisten = exitUnlistenersRef.current.get(terminalId)
|
||||||
if (unlisten) {
|
if (unlisten) {
|
||||||
unlisten()
|
disposeTauriListener(unlisten, "CommandDropdown.terminalExit")
|
||||||
exitUnlistenersRef.current.delete(terminalId)
|
exitUnlistenersRef.current.delete(terminalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ export function CommandDropdown() {
|
|||||||
|
|
||||||
const clearAllRunningStates = useCallback(() => {
|
const clearAllRunningStates = useCallback(() => {
|
||||||
for (const unlisten of exitUnlistenersRef.current.values()) {
|
for (const unlisten of exitUnlistenersRef.current.values()) {
|
||||||
unlisten()
|
disposeTauriListener(unlisten, "CommandDropdown.terminalExit")
|
||||||
}
|
}
|
||||||
exitUnlistenersRef.current.clear()
|
exitUnlistenersRef.current.clear()
|
||||||
setRunningCommandTerminals({})
|
setRunningCommandTerminals({})
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react"
|
|||||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { usePlatform } from "@/hooks/use-platform"
|
import { usePlatform } from "@/hooks/use-platform"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export function WindowControls() {
|
export function WindowControls() {
|
||||||
@@ -59,7 +60,7 @@ export function WindowControls() {
|
|||||||
if (resizeFrame !== null) {
|
if (resizeFrame !== null) {
|
||||||
window.cancelAnimationFrame(resizeFrame)
|
window.cancelAnimationFrame(resizeFrame)
|
||||||
}
|
}
|
||||||
unlistenResize?.()
|
disposeTauriListener(unlistenResize, "WindowControls.resize")
|
||||||
}
|
}
|
||||||
}, [isWindows])
|
}, [isWindows])
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useRef } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
import { listen } from "@tauri-apps/api/event"
|
import { listen } from "@tauri-apps/api/event"
|
||||||
import { terminalWrite, terminalResize } from "@/lib/tauri"
|
import { terminalWrite, terminalResize } from "@/lib/tauri"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import type { TerminalEvent } from "@/lib/types"
|
import type { TerminalEvent } from "@/lib/types"
|
||||||
import type { ITheme } from "@xterm/xterm"
|
import type { ITheme } from "@xterm/xterm"
|
||||||
|
|
||||||
@@ -186,8 +187,8 @@ export function TerminalView({
|
|||||||
themeObserver.disconnect()
|
themeObserver.disconnect()
|
||||||
onDataDisposable.dispose()
|
onDataDisposable.dispose()
|
||||||
onResizeDisposable.dispose()
|
onResizeDisposable.dispose()
|
||||||
unlisten()
|
disposeTauriListener(unlisten, "TerminalView.output")
|
||||||
unlistenExit()
|
disposeTauriListener(unlistenExit, "TerminalView.exit")
|
||||||
term.dispose()
|
term.dispose()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -221,8 +222,8 @@ export function TerminalView({
|
|||||||
themeObserver.disconnect()
|
themeObserver.disconnect()
|
||||||
onDataDisposable.dispose()
|
onDataDisposable.dispose()
|
||||||
onResizeDisposable.dispose()
|
onResizeDisposable.dispose()
|
||||||
unlisten()
|
disposeTauriListener(unlisten, "TerminalView.output")
|
||||||
unlistenExit()
|
disposeTauriListener(unlistenExit, "TerminalView.exit")
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect()
|
||||||
term.dispose()
|
term.dispose()
|
||||||
fitAddonRef.current = null
|
fitAddonRef.current = null
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "react"
|
} from "react"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
|
||||||
|
import { disposeTauriListener } from "@/lib/tauri-listener"
|
||||||
import {
|
import {
|
||||||
acpConnect,
|
acpConnect,
|
||||||
acpListAgents,
|
acpListAgents,
|
||||||
@@ -1420,11 +1421,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
})
|
})
|
||||||
.then((fn) => {
|
.then((fn) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
try {
|
disposeTauriListener(fn, "AcpConnectionsProvider.globalEvent")
|
||||||
fn()
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
unlisten = fn
|
unlisten = fn
|
||||||
listenerReadyRef.current = true
|
listenerReadyRef.current = true
|
||||||
@@ -1444,7 +1441,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
clearTimeout(flushTimerRef.current)
|
clearTimeout(flushTimerRef.current)
|
||||||
flushTimerRef.current = null
|
flushTimerRef.current = null
|
||||||
}
|
}
|
||||||
unlisten?.()
|
disposeTauriListener(unlisten, "AcpConnectionsProvider.globalEvent")
|
||||||
}
|
}
|
||||||
}, [bufferUnmappedEvent, handleMappedEvent, resolveListenerReadyWaiters])
|
}, [bufferUnmappedEvent, handleMappedEvent, resolveListenerReadyWaiters])
|
||||||
|
|
||||||
|
|||||||
53
src/lib/tauri-listener.ts
Normal file
53
src/lib/tauri-listener.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
type TauriUnlisten = () => void | Promise<void>
|
||||||
|
|
||||||
|
const disposedListeners = new WeakSet<TauriUnlisten>()
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error) return error.message
|
||||||
|
return String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKnownDisposeRaceError(message: string): boolean {
|
||||||
|
const normalized = message.toLowerCase()
|
||||||
|
return (
|
||||||
|
normalized.includes("listeners[eventid].handlerid") ||
|
||||||
|
normalized.includes("cannot read properties of undefined") ||
|
||||||
|
normalized.includes("undefined is not an object")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisposeError(error: unknown, scope?: string): void {
|
||||||
|
const message = getErrorMessage(error)
|
||||||
|
if (isKnownDisposeRaceError(message)) return
|
||||||
|
if (scope) {
|
||||||
|
console.warn(`[${scope}] failed to dispose listener:`, error)
|
||||||
|
} else {
|
||||||
|
console.warn("[tauri-listener] failed to dispose listener:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose Tauri listener functions defensively.
|
||||||
|
*
|
||||||
|
* React StrictMode, rapid tab/window switches, and async listener setup can
|
||||||
|
* trigger duplicate dispose paths. Tauri can throw when the internal listener
|
||||||
|
* is already gone; this helper makes cleanup idempotent.
|
||||||
|
*/
|
||||||
|
export function disposeTauriListener(
|
||||||
|
unlisten: TauriUnlisten | null | undefined,
|
||||||
|
scope?: string
|
||||||
|
): void {
|
||||||
|
if (!unlisten) return
|
||||||
|
if (disposedListeners.has(unlisten)) return
|
||||||
|
|
||||||
|
disposedListeners.add(unlisten)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const disposeResult = unlisten()
|
||||||
|
void Promise.resolve(disposeResult).catch((error) => {
|
||||||
|
handleDisposeError(error, scope)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
handleDisposeError(error, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user