fix(visualizer): bars recover after far-forward seeks into undecoded audio

ensureDecodeAround let a running decode pass close ANY forward gap in
place — at 4x pacing, skipping 30 min ahead meant ~7.5 min for the
frontier to arrive: bars held their last frame indefinitely. Now a gap
beyond 15s restarts the pass at the seek target (network coverage there
in ~1.6s, verified); smaller gaps close in place (cheaper than a
reconnect + range request). Seek-key holds are debounced (400ms) so
rapid re-seeks don't reconnect-spam the stream's server, and pending
seek-decode cancels on pause/stop so no ffmpeg restarts while paused.

Also fixes a pre-existing config-write race that intermittently failed
visualizer-toggle.test.ts: updateConfig re-resolved the config path and
re-read the patch state when the deferred write-chain drained, so a
queued save could land in a directory XDG_CONFIG_HOME had since been
pointed at (or carry state mutated after queueing). Path and patch
snapshot are now captured eagerly at call time.
This commit is contained in:
2026-08-11 21:08:09 -04:00
parent 2cf9559b0b
commit af827a9a96
4 changed files with 139 additions and 29 deletions

View File

@@ -13,6 +13,7 @@
* always overwrite — no backup files are created.
*/
import { mkdir } from "fs/promises";
import { ensureConfigDir, getConfigDir, getConfigFilePath } from "./config-dir";
import type {
AppSettings,
@@ -58,15 +59,34 @@ let writeChain: Promise<void> = Promise.resolve();
/** Update sections of config.json (read-modify-write, serialized, overwrite). */
export function updateConfig(patch: Partial<PodTuiConfig>): void {
// Capture the target path AND the patch data eagerly, at call time:
// the write chain defers execution, and both the config dir (tests
// re-point XDG_CONFIG_HOME between ops) and the state object (stores
// mutate in place) move under a pending write. Without the capture, a
// queued save writes the LATEST state into whatever directory is
// current when the chain drains — a cross-directory misdelivery that
// was the source of a flaky "enabled:false survives reload" test.
const configPath = getConfigFilePath(CONFIG_FILE);
const configDir = getConfigDir();
const snapshot = JSON.parse(JSON.stringify(patch)) as Partial<PodTuiConfig>;
writeChain = writeChain.then(async () => {
try {
await ensureConfigDir();
const current = await loadConfig();
const next = { ...current, ...patch };
await Bun.write(
getConfigFilePath(CONFIG_FILE),
JSON.stringify(next, null, 2),
);
await migrateOnce();
await mkdir(configDir, { recursive: true });
let current: PodTuiConfig = {};
try {
const file = Bun.file(configPath);
if (await file.exists()) {
const raw = await file.json();
if (raw && typeof raw === "object") {
current = raw as PodTuiConfig;
}
}
} catch {
/* unreadable existing config — treat as empty */
}
const next = { ...current, ...snapshot };
await Bun.write(configPath, JSON.stringify(next, null, 2));
} catch {
// Fire-and-forget persistence — silently ignore write errors.
}