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) <noreply@anthropic.com>
This commit is contained in:
fkwp
2026-10-08 12:54:54 +02:00
co-authored by Claude Opus 5.5
parent 32e7c407c3
commit de2b98251c
2 changed files with 30 additions and 34 deletions
+8
View File
@@ -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();
+22 -34
View File
@@ -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<void> {
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;
};
}