Skip to content

Dead subscriptions accumulate across webview reloads and destroyed webviews #58

Description

@jjhafer

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions