From de2b98251c1b09fefcb00721341b875075d798cd Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 8 Oct 2026 12:54:54 +0200 Subject: [PATCH] Switch the background effect with ObservableScope.reconcile - The pipeline's switches run through reconcile, which already runs them one at a time and skips those overtaken, in place of a queue of our own. - A failed switch is logged and the next still runs, now tested. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/state/BackgroundEffects.test.ts | 8 +++++ src/state/BackgroundEffects.ts | 56 ++++++++++++----------------- 2 files changed, 30 insertions(+), 34 deletions(-) diff --git a/src/state/BackgroundEffects.test.ts b/src/state/BackgroundEffects.test.ts index a57ad5161..a3c30ce64 100644 --- a/src/state/BackgroundEffects.test.ts +++ b/src/state/BackgroundEffects.test.ts @@ -113,6 +113,14 @@ describe("background effects", () => { ]); }); + it("keeps switching after a switch fails", async () => { + vi.mocked(fake.pipeline.switchTo).mockRejectedValueOnce(new Error("lost")); + build(); + await blur(true); + await blur(false); + expect(fake.switches).toEqual([{ mode: "disabled" }]); + }); + it("switches one at a time, skipping those overtaken", async () => { const finished = fake.holdNext(); build(); diff --git a/src/state/BackgroundEffects.ts b/src/state/BackgroundEffects.ts index 58bcf2b73..ba4ac114a 100644 --- a/src/state/BackgroundEffects.ts +++ b/src/state/BackgroundEffects.ts @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { combineLatest, distinctUntilChanged, filter, map, scan } from "rxjs"; +import { combineLatest, distinctUntilChanged, map, scan } from "rxjs"; import { type BackgroundProcessorWrapper, type SwitchBackgroundProcessorOptions, @@ -53,41 +53,29 @@ export function createBackgroundEffects( ), ); - const switchTo = oneSwitchAtATime(pipeline); - combineLatest([state$, blur$]) - .pipe( - filter(([{ processor }]) => processor !== undefined), - map(([, blur]): SwitchBackgroundProcessorOptions => - blur ? { mode: "background-blur", blurRadius } : { mode: "disabled" }, + const switchOptions$ = scope.behavior< + SwitchBackgroundProcessorOptions | undefined + >( + combineLatest([state$, blur$]).pipe( + map(([{ processor }, blur]) => + processor === undefined + ? undefined + : blur + ? { mode: "background-blur", blurRadius } + : { mode: "disabled" }, ), distinctUntilChanged(deepCompare), - scope.bind(), - ) - .subscribe((options) => { - switchTo(options).catch((e) => - logger.warn("Failed to switch background effect", e), - ); - }); + ), + ); + // In turn: a picture's switch ends once it loads, so an earlier one could land last. + scope.reconcile(switchOptions$, async (options) => { + if (options === undefined) return; + try { + await pipeline.switchTo(options); + } catch (e) { + logger.warn("Failed to switch background effect", e); + } + }); return { state$ }; } - -/** - * Switches the pipeline one choice at a time, skipping those overtaken while - * they waited. A switch to a picture ends only once it has loaded, so a - * slower, earlier choice would otherwise land after a later one. - */ -function oneSwitchAtATime( - pipeline: BackgroundProcessorWrapper, -): (options: SwitchBackgroundProcessorOptions) => Promise { - let latest: SwitchBackgroundProcessorOptions | undefined; - let queue = Promise.resolve(); - return async (options) => { - latest = options; - const turn = queue.then(async () => { - if (options === latest) await pipeline.switchTo(options); - }); - queue = turn.catch(() => {}); - return turn; - }; -}