From 6e0223a9b10e37127627ae356e96225c2f1b77de Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 10 May 2026 15:39:00 -0400 Subject: [PATCH] refactor(web): runMutation helper for generic-toast admin handlers (#375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the dominant pattern (try { await fn } catch { pushToast(errMessage, 'error') }) for the 5 admin/+page.svelte handlers that surface errMessage directly: onApprove, onReject, onResolve, onDeleteFile, onDeleteLidarr. Tighter scope than originally planned — the 3 handlers in admin/users/+page.svelte (onToggleAutoApprove, onGenerateInvite, onRevokeInvite) use errCode + verb prefix ("Generate failed: ${code}") which is structurally different and would change UX wire output if forced through the helper. Skipping them keeps behavior identical. The helper SWALLOWS errors so callers no longer need try/finally for busy-state — `saving = true; await runMutation(...); saving = false;` is correct because runMutation can't throw. Co-Authored-By: Claude Opus 4.7 (1M context) --- web/src/lib/util/runMutation.ts | 28 ++++++++++++++++++++++++++ web/src/routes/admin/+page.svelte | 33 +++++++++++-------------------- 2 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 web/src/lib/util/runMutation.ts diff --git a/web/src/lib/util/runMutation.ts b/web/src/lib/util/runMutation.ts new file mode 100644 index 00000000..82afccac --- /dev/null +++ b/web/src/lib/util/runMutation.ts @@ -0,0 +1,28 @@ +// Wraps the dominant admin-handler mutation pattern: +// try { await fn(); pushToast(success?); } +// catch (e) { pushToast(errMessage(e), 'error'); } +// +// Errors are SWALLOWED — caller does not need a try/catch. Any +// `saving = true / saving = false` state flag can sit around the +// `await runMutation(...)` call without try/finally because the +// helper never rethrows. +// +// Use ONLY for handlers that surface errMessage(e) directly. +// Handlers that branch on specific error codes (last_admin, +// password_too_short) or build custom copy with errCode keep +// their inline catch — wrapping those would obscure intent. + +import { errMessage } from '$lib/api/errors'; +import { pushToast } from '$lib/stores/toast.svelte'; + +export async function runMutation( + fn: () => Promise, + opts: { successMessage?: string } = {} +): Promise { + try { + await fn(); + if (opts.successMessage) pushToast(opts.successMessage); + } catch (e) { + pushToast(errMessage(e), 'error'); + } +} diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 82328ab9..5f570a37 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -24,8 +24,9 @@ type ScanScheduleMode } from '$lib/api/admin'; import { qk } from '$lib/api/queries'; - import { errCode, errMessage } from '$lib/api/errors'; + import { errCode } from '$lib/api/errors'; import { pushToast } from '$lib/stores/toast.svelte'; + import { runMutation } from '$lib/util/runMutation'; import type { AdminQuarantineRow, LidarrRequest, @@ -119,20 +120,16 @@ } async function onApprove(r: LidarrRequest) { - try { + await runMutation(async () => { await approveRequest(r.id); await invalidateRequests(); - } catch (e) { - pushToast(errMessage(e), 'error'); - } + }); } async function onReject(r: LidarrRequest) { - try { + await runMutation(async () => { await rejectRequest(r.id); await invalidateRequests(); - } catch (e) { - pushToast(errMessage(e), 'error'); - } + }); } async function invalidateQuarantine() { @@ -143,12 +140,10 @@ } async function onResolve(row: AdminQuarantineRow) { - try { + await runMutation(async () => { await resolveQuarantine(row.track_id); await invalidateQuarantine(); - } catch (e) { - pushToast(errMessage(e), 'error'); - } + }); } async function onDeleteFile(row: AdminQuarantineRow) { const key = confirmKey(row.track_id, 'delete-file'); @@ -157,12 +152,10 @@ return; } clearConfirm(); - try { + await runMutation(async () => { await deleteQuarantineFile(row.track_id); await invalidateQuarantine(); - } catch (e) { - pushToast(errMessage(e), 'error'); - } + }); } async function onDeleteLidarr(row: AdminQuarantineRow) { const key = confirmKey(row.track_id, 'delete-lidarr'); @@ -171,12 +164,10 @@ return; } clearConfirm(); - try { + await runMutation(async () => { await deleteQuarantineViaLidarr(row.track_id); await invalidateQuarantine(); - } catch (e) { - pushToast(errMessage(e), 'error'); - } + }); } // ---- Library scan ----