fix: keep hidden windows active during idle

This commit is contained in:
David Perov 2026-10-02 10:57:43 +03:00
parent 048f9d2096
commit 03fb898932
6 changed files with 135 additions and 0 deletions

View file

@ -37,6 +37,7 @@ fn create_settings_window(
.title("Talkis")
.visible(visible)
.focused(visible)
.background_throttling(tauri::utils::config::BackgroundThrottlingPolicy::Disabled)
.inner_size(920.0, 680.0)
.min_inner_size(820.0, 560.0)
.center();

View file

@ -15,6 +15,7 @@
{
"label": "widget",
"visible": false,
"backgroundThrottling": "disabled",
"title": "Talkis",
"url": "index.html?window=widget",
"width": 129,

View file

@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import { holdBackgroundActivity } from "./backgroundActivity";
describe("background activity lifetime", () => {
test("keeps a granted shared lock pending until the window unmounts", async () => {
let settled = false;
let signal: AbortSignal | undefined;
let request: Promise<void> = Promise.resolve();
const release = holdBackgroundActivity("widget", {
request: (name, options, callback): Promise<void> => {
expect(name).toBe("talkis:widget:background");
expect(options.mode).toBe("shared");
signal = options.signal;
request = callback({ name, mode: "shared" }).then(() => {
settled = true;
});
return request;
},
});
await Promise.resolve();
expect(settled).toBe(false);
expect(signal?.aborted).toBe(false);
release();
release();
await request;
expect(settled).toBe(true);
expect(signal?.aborted).toBe(true);
});
test("releases a late grant when cleanup precedes acquisition", async () => {
let grant: (() => Promise<void>) | undefined;
let signal: AbortSignal | undefined;
const release = holdBackgroundActivity("settings", {
request: (name, options, callback): Promise<void> => {
signal = options.signal;
grant = (): Promise<void> => callback({ name, mode: "shared" });
return Promise.resolve();
},
});
release();
expect(signal?.aborted).toBe(true);
expect(grant).toBeDefined();
await grant?.();
});
test("does not block window startup if the browser rejects the request", async () => {
const release = holdBackgroundActivity("widget", {
request: (): Promise<void> => Promise.reject(new Error("locks disabled")),
});
await Promise.resolve();
expect(release).toBeFunction();
release();
});
test("supports browsers without the Locks API", () => {
const release = holdBackgroundActivity("settings");
expect(release).toBeFunction();
release();
});
});

View file

@ -0,0 +1,58 @@
import { logError, logInfo } from "./logger";
interface BackgroundLockManager {
request(
name: string,
options: LockOptions,
callback: (lock: Lock | null) => Promise<void>,
): Promise<void>;
}
/** Keep event-driven background windows eligible to receive hotkeys and IPC. */
export function holdBackgroundActivity(
windowLabel: "widget" | "settings",
locks: BackgroundLockManager | undefined =
typeof navigator === "undefined" ? undefined : navigator.locks,
): () => void {
if (!locks) {
return (): void => {};
}
const controller = new AbortController();
let released = false;
let finish: () => void = (): void => {};
const lifetime = new Promise<void>((resolve) => {
finish = resolve;
});
try {
void locks.request(
`talkis:${windowLabel}:background`,
{ mode: "shared", signal: controller.signal },
async (): Promise<void> => {
if (released) return;
void logInfo("BACKGROUND", `Activity lock acquired: ${windowLabel}`);
await lifetime;
},
).catch((error: unknown) => {
if (!released) {
void logError(
"BACKGROUND",
`Activity lock unavailable (${windowLabel}): ${String(error)}`,
);
}
});
} catch (error) {
void logError(
"BACKGROUND",
`Failed to request activity lock (${windowLabel}): ${String(error)}`,
);
}
return (): void => {
released = true;
finish();
controller.abort();
};
}

View file

@ -45,6 +45,7 @@ import {
} from "../../lib/store";
import { checkAllPermissions } from "../../lib/permissions";
import { logError, logInfo } from "../../lib/logger";
import { holdBackgroundActivity } from "../../lib/backgroundActivity";
import { UserPanel } from "../../components/UserPanel";
import { watchThemePreference } from "../../lib/theme";
import { syncWindowsTitlebarTheme } from "../../lib/windowsTitlebar";
@ -381,6 +382,8 @@ function getCurrentDomSelectionText(): string {
}
export function SettingsApp(): ReactElement {
useEffect(() => holdBackgroundActivity("settings"), []);
const { t } = useI18n();
const initialQuery = new URLSearchParams(window.location.search);
const checkUpdateAtStartup = initialQuery.get("checkUpdate") === "1";

View file

@ -54,6 +54,7 @@ import {
transcribeFilePathOnly,
} from "../../lib/fileTranscription";
import { logError, logInfo } from "../../lib/logger";
import { holdBackgroundActivity } from "../../lib/backgroundActivity";
import { tn, useI18n } from "../../lib/i18n";
import {
requestSystemAudioPermission,
@ -339,6 +340,8 @@ function isCallCapturePermissionError(error: unknown): boolean {
}
export function Widget() {
useEffect(() => holdBackgroundActivity("widget"), []);
const { t } = useI18n();
const widgetWindow = getCurrentWindow();
const dragStartRef = useRef<{ x: number; y: number } | null>(null);