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) => {
|
.then((fn) => {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
try {
|
|
||||||
fn()
|
fn()
|
||||||
} catch {
|
|
||||||
// Tauri listener may not be fully registered yet
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
unlisten = fn
|
unlisten = fn
|
||||||
listenerReadyRef.current = true
|
listenerReadyRef.current = true
|
||||||
@@ -2066,11 +2062,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
clearTimeout(flushTimerRef.current)
|
clearTimeout(flushTimerRef.current)
|
||||||
flushTimerRef.current = null
|
flushTimerRef.current = null
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
unlisten?.()
|
unlisten?.()
|
||||||
} catch {
|
|
||||||
// Tauri listener may not be fully registered yet
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [bufferUnmappedEvent, handleMappedEvent, resolveListenerReadyWaiters])
|
}, [bufferUnmappedEvent, handleMappedEvent, resolveListenerReadyWaiters])
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,58 @@
|
|||||||
import type { Transport, UnsubscribeFn } from "./types"
|
import type { Transport, UnsubscribeFn } from "./types"
|
||||||
|
|
||||||
|
type TauriEventListenersWindow = {
|
||||||
|
__TAURI_EVENT_PLUGIN_INTERNALS__?: {
|
||||||
|
unregisterListener?: (event: string, eventId: number) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class TauriTransport implements Transport {
|
export class TauriTransport implements Transport {
|
||||||
async call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
async call<T>(command: string, args?: Record<string, unknown>): Promise<T> {
|
||||||
const { invoke } = await import("@tauri-apps/api/core")
|
const { invoke } = await import("@tauri-apps/api/core")
|
||||||
return invoke(command, args)
|
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>(
|
async subscribe<T>(
|
||||||
event: string,
|
event: string,
|
||||||
handler: (payload: T) => void
|
handler: (payload: T) => void
|
||||||
): Promise<UnsubscribeFn> {
|
): Promise<UnsubscribeFn> {
|
||||||
const { listen } = await import("@tauri-apps/api/event")
|
const { invoke, transformCallback } = await import("@tauri-apps/api/core")
|
||||||
return listen<T>(event, (e) => handler(e.payload))
|
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 {
|
isDesktop(): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user