A subscription's forwarding task reaps its own entry from ActiveSubscriptions
when its stream ends, but that does not cover a webview reload.
on_event.send() only errors when the underlying eval fails. In a default build
(tracing is not a default Tauri feature) eval_script resolves to the
send_user_message variant in tauri-runtime-wry, which returns Ok as soon as
the message is queued on the app-global proxy, and the handler no-ops for a
missing webview. So send() returns Ok for a reloaded webview and for a
destroyed one; the reap effectively fires only when the stream ends (broker torn
down) or on event-loop teardown.
Consequences for a window that reloads without calling unsubscribe():
- one live forwarding task leaks per reload, each still eval'ing into a dead JS
callback on every matching change
- those entries count against the 100-subscriptions-per-database cap, which is
shared across windows, so one window's churn can eventually block other
windows from subscribing
- change payloads of 8192 bytes or more are inserted into Tauri's
ChannelDataIpcQueue and removed only when JS fetches them
On the queue growth specifically — note the failure mode is a destroyed
webview, not a reloaded one. A reloaded page re-injects __TAURI_INTERNALS__,
runs the queued eval, and drains its entry, so reload leaks the tracking entry but
not the payload. A destroyed webview never drains, and the existing window-
Destroyed handler only reaps subscriptions for databases whose observer count
newly hit zero — so closing one window while another still observes that database
leaves its subscriptions live and its oversized payloads accumulating in an
app-managed map indefinitely. With captureValues defaulting to true, wide rows
cross that threshold easily. That case is unbounded memory growth, not just a
counter leak, and it is the more serious half of this issue.
Proposed fix
Hook PluginBuilder::on_page_load with PageLoadEvent::Started and sweep the
loading webview's prior subscriptions. This requires keying ActiveSubscriptions
by webview label — feasible, since observe()/unobserve() (and now
subscribe()) already establish the Webview<R> extractor pattern.
Rejected: sweeping a label's subscriptions on subscribe(). A window
legitimately holds several concurrent subscriptions (guest-js/index.ts:1262), so
that would break valid usage.
Note the existing window-Destroyed handler sweeps only databases whose
registration count newly hit zero, so it does not cover this either.
Split out of the code review on #55, where it was validated as real but out of scope for that PR. See the review threads there for the supporting analysis.
Issue filed by AI model Claude
A subscription's forwarding task reaps its own entry from
ActiveSubscriptionswhen its stream ends, but that does not cover a webview reload.
on_event.send()only errors when the underlyingevalfails. In a default build(
tracingis not a default Tauri feature)eval_scriptresolves to thesend_user_messagevariant intauri-runtime-wry, which returnsOkas soon asthe message is queued on the app-global proxy, and the handler no-ops for a
missing webview. So
send()returnsOkfor a reloaded webview and for adestroyed one; the reap effectively fires only when the stream ends (broker torn
down) or on event-loop teardown.
Consequences for a window that reloads without calling
unsubscribe():callback on every matching change
shared across windows, so one window's churn can eventually block other
windows from subscribing
ChannelDataIpcQueueand removed only when JS fetches themOn the queue growth specifically — note the failure mode is a destroyed
webview, not a reloaded one. A reloaded page re-injects
__TAURI_INTERNALS__,runs the queued eval, and drains its entry, so reload leaks the tracking entry but
not the payload. A destroyed webview never drains, and the existing window-
Destroyedhandler only reaps subscriptions for databases whose observer countnewly hit zero — so closing one window while another still observes that database
leaves its subscriptions live and its oversized payloads accumulating in an
app-managed map indefinitely. With
captureValuesdefaulting to true, wide rowscross that threshold easily. That case is unbounded memory growth, not just a
counter leak, and it is the more serious half of this issue.
Proposed fix
Hook
PluginBuilder::on_page_loadwithPageLoadEvent::Startedand sweep theloading webview's prior subscriptions. This requires keying
ActiveSubscriptionsby webview label — feasible, since
observe()/unobserve()(and nowsubscribe()) already establish theWebview<R>extractor pattern.Rejected: sweeping a label's subscriptions on
subscribe(). A windowlegitimately holds several concurrent subscriptions (
guest-js/index.ts:1262), sothat would break valid usage.
Note the existing window-
Destroyedhandler sweeps only databases whoseregistration count newly hit zero, so it does not cover this either.
Split out of the code review on #55, where it was validated as real but out of scope for that PR. See the review threads there for the supporting analysis.
Issue filed by AI model Claude