fix(transport): replace Tauri listen() to avoid unlisten race crashing on WKWebView
Call plugin:event|listen/unlisten directly and guard the client-side listener registry cleanup, so unsubscribe always reaches the backend even when the registration eval has not yet populated window.__TAURI_EVENT_LISTENERS__. Prevents the intermittent `listeners[eventId].handlerId` TypeError and the resulting leaked listener.
This commit is contained in:
@@ -2042,11 +2042,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
})
|
||||
.then((fn) => {
|
||||
if (cancelled) {
|
||||
try {
|
||||
fn()
|
||||
} catch {
|
||||
// Tauri listener may not be fully registered yet
|
||||
}
|
||||
} else {
|
||||
unlisten = fn
|
||||
listenerReadyRef.current = true
|
||||
@@ -2066,11 +2062,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
clearTimeout(flushTimerRef.current)
|
||||
flushTimerRef.current = null
|
||||
}
|
||||
try {
|
||||
unlisten?.()
|
||||
} catch {
|
||||
// Tauri listener may not be fully registered yet
|
||||
}
|
||||
}
|
||||
}, [bufferUnmappedEvent, handleMappedEvent, resolveListenerReadyWaiters])
|
||||
|
||||
|
||||
@@ -1,17 +1,58 @@
|
||||
import type { Transport, UnsubscribeFn } from "./types"
|
||||
|
||||
type TauriEventListenersWindow = {
|
||||
__TAURI_EVENT_PLUGIN_INTERNALS__?: {
|
||||
unregisterListener?: (event: string, eventId: number) => void
|
||||
}
|
||||
}
|
||||
|
||||
export class TauriTransport implements Transport {
|
||||
async call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||
const { invoke } = await import("@tauri-apps/api/core")
|
||||
return invoke(command, args)
|
||||
}
|
||||
|
||||
// Bypasses `@tauri-apps/api/event#listen` to sidestep an intermittent race:
|
||||
// Tauri's Rust `listen_js` schedules a fire-and-forget eval that populates
|
||||
// `window.__TAURI_EVENT_LISTENERS__[event][eventId]` and, separately, returns
|
||||
// `eventId` via the invoke response. On WKWebView the two can arrive out of
|
||||
// order. When cleanup fires before the eval lands, the built-in `_unlisten`
|
||||
// throws synchronously on `listeners[eventId].handlerId` — and because that
|
||||
// throw happens BEFORE `await invoke('plugin:event|unlisten')`, the backend
|
||||
// listener is never removed (handler + payload buffering leak).
|
||||
//
|
||||
// We own the eventId here so we can always issue the backend unlisten, even
|
||||
// when the client-side registry entry hasn't appeared yet.
|
||||
async subscribe<T>(
|
||||
event: string,
|
||||
handler: (payload: T) => void
|
||||
): Promise<UnsubscribeFn> {
|
||||
const { listen } = await import("@tauri-apps/api/event")
|
||||
return listen<T>(event, (e) => handler(e.payload))
|
||||
const { invoke, transformCallback } = await import("@tauri-apps/api/core")
|
||||
const handlerId = transformCallback((e: { payload: T }) => {
|
||||
handler(e.payload)
|
||||
})
|
||||
const eventId = await invoke<number>("plugin:event|listen", {
|
||||
event,
|
||||
target: { kind: "Any" },
|
||||
handler: handlerId,
|
||||
})
|
||||
|
||||
let unlistened = false
|
||||
return () => {
|
||||
if (unlistened) return
|
||||
unlistened = true
|
||||
try {
|
||||
const internals = (window as unknown as TauriEventListenersWindow)
|
||||
.__TAURI_EVENT_PLUGIN_INTERNALS__
|
||||
internals?.unregisterListener?.(event, eventId)
|
||||
} catch {
|
||||
// Registration eval has not landed yet; server-side unlisten below
|
||||
// still clears the listener so events stop flowing.
|
||||
}
|
||||
// Fire-and-forget: callers expect a sync unsubscribe, and a failure here
|
||||
// only means the backend already forgot this listener.
|
||||
invoke("plugin:event|unlisten", { event, eventId }).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
isDesktop(): boolean {
|
||||
|
||||
Reference in New Issue
Block a user