Skip to content

Observation registration requires a webview label, so destroying a window silently ends a Rust caller's subscription #65

Description

@yokuze

ObserverRegistrations stores observer identity as a webview label
(src/subscriptions.rs:290), so the only way to join the count is to call the observe
command from a webview. A Rust caller that enables observation on a DatabaseWrapper
directly registers nothing, and since #53 made observation a property of the database
rather than of a handle, it shares the broker the count governs. It is invisible to the
count while being fully subject to its teardown.

Destroying a window needs no cooperation from anyone. When the one window that had called
observe() closes, the Destroyed handler releases its registrations, finds the database
at zero observers, and calls disable_observation() — ending a Rust subscription that
window never created and never asked to end. The Rust task's while let Some(event) = stream.next().await loop falls through and the task exits. Nothing logs it and nothing
re-enables observation, so every later write takes the unobserved path for the rest of the
process's life. This does not need the last window, which would end the process anyway: it
needs any window to close while another stays open, with the closing one being the only
webview that had registered that database.

Repro

Minimal, runnable repro (cargo run, prints an explicit verdict). It is a real Tauri app
because tauri::test::MockRuntime never emits RuntimeWindowEvent::Destroyed, so the
handler is unreachable from a mock app — worth knowing for a regression test. Two controls
are built in: the subscriber receives a committed change moments before the close, and
destroying a different window that never called observe() leaves it working.
https://github.com/yokuze/mcve/tree/9e77d09620300bad7dd0fc545beb3f3ba748dc4b/cases/005-tauri-plugin-sqlite-window-close-kills-rust-observation

Root cause (rev b31142e)

The behavior is documented — disable_observation's doc names the window case
(wrapper.rs#L706-L718),
as do unobserve's doc and the README caveats — but the documented workarounds do not
cover this trigger. "Coordinate above this crate" assumes someone performs an action; a
window closing is not one, and the release runs inside the plugin's own event handler.
"Use your own database file" gives a Rust consumer a file the webviews do not write to,
which is the reason it was observing. "Re-enable after a teardown" needs a teardown signal,
and there is none.

Expected

Widen who is allowed to hold a registration. ObserverRegistrations already keeps a set
per database and already tears down when that set empties; the only thing that has to
change is the identity stored in it — a webview label, or an opaque token handed to a
direct Rust caller. Hand that token back as a guard released by Drop, so a registration
cannot leak the way the phantom registrations that motivated the Destroyed handler did.
This is the same shape as the per-caller registration token proposed in #57, which reports
the other failure of label-keyed identity: there, two callers share one label; here, one
caller has no label at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions