Keep background apps alive, and notify from their unread count
The report was that a backgrounded site dies slowly and stops fetching. It did, for three separate reasons, and the first two fixes each traded one failure for another: Hiding inactive views lets WebKit suspend them, so nothing is hidden any more - every view keeps its size and stays in the window, and the active one is ordered on top. Telling a background page it was hidden then made Gmail throttle its own syncing; its unread count sat unchanged for two and a half minutes. That spoof is gone. Neither was enough on its own: a background page keeps its timers but loses the connection its updates arrive on, and WKWebView has no equivalent of Electron's backgroundThrottling. Background apps are now poked every 45s with the events a page uses to catch up after you return to a tab - it fetches without losing a half-written reply. Notifications now come from the unread count in the title rather than the site's notification code, which Gmail will not run while it believes you are looking at it. Measured: Gmail behind Odoo went 155 -> 157 untouched and raised "Gmail / 1 new". Also adds a background-app probe to Settings, which is what turned this from guesswork into measurement, and drops the nav's blocked-element count and the hide-element button.
This commit is contained in:
+55
-20
@@ -20,8 +20,8 @@ pub struct AppState {
|
||||
pub stage: Mutex<Stage>,
|
||||
/// Webviews exist. Guards against bootstrapping twice on a hot reload.
|
||||
pub booted: Mutex<bool>,
|
||||
/// What the last page probe reported, for the notification diagnostic.
|
||||
pub diag: Mutex<String>,
|
||||
/// What each app's page last reported about itself, keyed by app id.
|
||||
pub diag: Mutex<std::collections::HashMap<String, String>>,
|
||||
/// The last notification a page raised, and what macOS did with it.
|
||||
pub last_notification: Mutex<String>,
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub fn build_state(handle: &AppHandle) -> Result<AppState, String> {
|
||||
active: Mutex::new(None),
|
||||
stage: Mutex::new((240.0, 38.0, 800.0, 600.0)),
|
||||
booted: Mutex::new(false),
|
||||
diag: Mutex::new(String::new()),
|
||||
diag: Mutex::new(std::collections::HashMap::new()),
|
||||
last_notification: Mutex::new(String::new()),
|
||||
})
|
||||
}
|
||||
@@ -139,6 +139,10 @@ pub fn bootstrap(app: AppHandle, state: State<'_, AppState>) -> Result<(), Strin
|
||||
if let Err(e) = webviews::create(&app, &a, &cfg, stage) {
|
||||
eprintln!("could not create webview for {}: {e}", a.name);
|
||||
}
|
||||
// Each new view is added on top of its siblings, so the app being
|
||||
// looked at has to be put back in front after every one.
|
||||
let active = state.active.lock().unwrap().clone();
|
||||
webviews::show_only(&app, active.as_deref(), &cfg, stage);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -156,7 +160,8 @@ pub fn set_active(app_id: String, app: AppHandle, state: State<'_, AppState>) {
|
||||
/// Hides every app, so a dialog is not painted over by a native view.
|
||||
#[tauri::command]
|
||||
pub fn hide_stage(app: AppHandle, state: State<'_, AppState>) {
|
||||
webviews::hide_all(&app, &state.cfg());
|
||||
let stage = *state.stage.lock().unwrap();
|
||||
webviews::hide_all(&app, &state.cfg(), stage);
|
||||
}
|
||||
|
||||
/// Puts the active app back after a dialog closes.
|
||||
@@ -246,6 +251,8 @@ pub fn add_app(
|
||||
let stage = *state.stage.lock().unwrap();
|
||||
if *state.booted.lock().unwrap() {
|
||||
webviews::create(&app, &new, &cfg, stage)?;
|
||||
let active = state.active.lock().unwrap().clone();
|
||||
webviews::show_only(&app, active.as_deref(), &cfg, stage);
|
||||
}
|
||||
Ok(cfg)
|
||||
}
|
||||
@@ -411,17 +418,6 @@ pub fn set_hidden(
|
||||
Ok(state.cfg())
|
||||
}
|
||||
|
||||
/// Starts the in-page element picker, for reaching something a right-click
|
||||
/// cannot land on cleanly.
|
||||
#[tauri::command]
|
||||
pub fn pick_hidden(app_id: String, app: AppHandle) -> Result<(), String> {
|
||||
let wv = app
|
||||
.get_webview(&webviews::label_for(&app_id))
|
||||
.ok_or_else(|| format!("{app_id} has no webview"))?;
|
||||
wv.eval("window.__workAppPick && window.__workAppPick()")
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Permission state, and what a notification raised straight from Rust does.
|
||||
///
|
||||
/// Splits the chain in two: if this succeeds and the page test does not, the
|
||||
@@ -446,12 +442,8 @@ pub fn notification_status(app: AppHandle) -> String {
|
||||
};
|
||||
let app_state = app.state::<AppState>();
|
||||
let last = app_state.last_notification.lock().unwrap().clone();
|
||||
let diag = app_state.diag.lock().unwrap().clone();
|
||||
let from_page = if last.is_empty() { "none yet".into() } else { last };
|
||||
// The probe's own report, which distinguishes "the page never ran" from
|
||||
// "the page ran and the message did not arrive".
|
||||
let probe = if diag.is_empty() { "no reply".into() } else { diag };
|
||||
format!("permission: {state} · direct: {raised} · from page: {from_page} · probe: {probe}")
|
||||
format!("permission: {state} · direct: {raised} · from page: {from_page}")
|
||||
}
|
||||
|
||||
/// Asks macOS for notification permission, and claims this app's identity.
|
||||
@@ -632,6 +624,49 @@ pub fn reload_active(app: &AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks every app's page what it currently believes about itself.
|
||||
///
|
||||
/// Built because guessing was not working: it separates "the page is asleep"
|
||||
/// from "the page is awake and thinks it is on screen" from "the page is awake,
|
||||
/// knows it is hidden, and simply has nothing to say".
|
||||
#[tauri::command]
|
||||
pub fn probe_apps(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.diag.lock().unwrap().clear();
|
||||
for a in state.cfg().apps {
|
||||
let Some(wv) = app.get_webview(&webviews::label_for(&a.id)) else { continue };
|
||||
let _ = wv.eval(
|
||||
r#"(function () {
|
||||
if (!window.__workAppSend) return;
|
||||
var st = window.__workAppState ? window.__workAppState() : {};
|
||||
window.__workAppSend('diag', {
|
||||
title: String(document.title || '').slice(0, 40),
|
||||
active: String(st.active),
|
||||
seen: String(st.seen),
|
||||
queued: String(st.queued) + '/' + String(st.sending)
|
||||
});
|
||||
})();"#,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What every app reported, from the newest run only.
|
||||
#[tauri::command]
|
||||
pub fn app_reports(state: State<'_, AppState>) -> Vec<(String, String)> {
|
||||
let cfg = state.cfg();
|
||||
let diag = state.diag.lock().unwrap();
|
||||
cfg.apps
|
||||
.iter()
|
||||
.map(|a| {
|
||||
let report = diag
|
||||
.get(&a.id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "no reply — page is asleep or gone".into());
|
||||
(a.name.clone(), report)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// -------------------------------------------------- notification clicks
|
||||
|
||||
/// Runs the page's own click handler for a notification it raised.
|
||||
|
||||
Reference in New Issue
Block a user