From 6fe776b3b4034a66c8b7b4dd96ed8c919a809c6f Mon Sep 17 00:00:00 2001 From: clockblocker Date: Fri, 20 Feb 2026 21:07:34 +0100 Subject: [PATCH] Extract duplicated error-handling into shared helpers Add extractErrorReason() and toApiCommandError() to errors.ts, and notifyAndLogError() to orchestration/shared/notify-error.ts. Replace 4 inline "reason" in error ternaries and 3 identical .mapErr() blocks across textfresser.ts, execute-lemma-flow.ts, background-generate-coordinator.ts, and generate-sections.ts. Nightshift-Task: auto-dry Nightshift-Ref: https://github.com/marcus/nightshift Co-Authored-By: Claude Opus 4.6 --- .../generate/steps/generate-sections.ts | 13 +++-------- src/commanders/textfresser/errors.ts | 16 ++++++++++++++ .../background-generate-coordinator.ts | 6 ++--- .../orchestration/lemma/execute-lemma-flow.ts | 22 +++---------------- .../orchestration/shared/notify-error.ts | 16 ++++++++++++++ src/commanders/textfresser/textfresser.ts | 12 ++-------- 6 files changed, 42 insertions(+), 43 deletions(-) create mode 100644 src/commanders/textfresser/orchestration/shared/notify-error.ts diff --git a/src/commanders/textfresser/commands/generate/steps/generate-sections.ts b/src/commanders/textfresser/commands/generate/steps/generate-sections.ts index 6d5e25be1..17bdd64ad 100644 --- a/src/commanders/textfresser/commands/generate/steps/generate-sections.ts +++ b/src/commanders/textfresser/commands/generate/steps/generate-sections.ts @@ -1,13 +1,12 @@ import { ResultAsync } from "neverthrow"; -import { getErrorMessage } from "../../../../../utils/get-error-message"; import type { DictEntry } from "../../../domain/dict-note/types"; +import { toApiCommandError } from "../../../errors"; import { cssSuffixFor } from "../../../targets/de/sections/section-css-kind"; import { DictSectionKind, TitleReprFor, } from "../../../targets/de/sections/section-kind"; import type { CommandError } from "../../types"; -import { CommandErrorKind } from "../../types"; import { buildEntityMeta, buildLinguisticUnitMeta, @@ -164,10 +163,7 @@ export function generateSections( if (ctx.matchedEntry) { return ResultAsync.fromPromise( buildReEncounterResult(ctx), - (error): CommandError => ({ - kind: CommandErrorKind.ApiError, - reason: getErrorMessage(error), - }), + toApiCommandError, ); } @@ -263,9 +259,6 @@ export function generateSections( targetBlockId: generated.entryId, }; })(), - (error): CommandError => ({ - kind: CommandErrorKind.ApiError, - reason: getErrorMessage(error), - }), + toApiCommandError, ); } diff --git a/src/commanders/textfresser/errors.ts b/src/commanders/textfresser/errors.ts index cc1fc88d3..c98f85c70 100644 --- a/src/commanders/textfresser/errors.ts +++ b/src/commanders/textfresser/errors.ts @@ -1,4 +1,5 @@ import z from "zod"; +import { getErrorMessage } from "../../utils/get-error-message"; import { BASE_COMMAND_ERROR_KIND_STR, type BaseCommandError, @@ -44,3 +45,18 @@ export const AttestationParsingErrorKind = export type AttestationParsingError = | { kind: typeof AttestationParsingErrorKind.WikilinkNotFound } | { kind: typeof AttestationParsingErrorKind.BlockIdNotFound }; + +// ─── Error Helpers ─── + +/** Extract a human-readable reason from a CommandError. */ +export function extractErrorReason(error: CommandError): string { + return "reason" in error ? error.reason : `Command failed: ${error.kind}`; +} + +/** Convert an unknown thrown value into an ApiError CommandError. */ +export function toApiCommandError(error: unknown): CommandError { + return { + kind: CommandErrorKind.ApiError, + reason: getErrorMessage(error), + }; +} diff --git a/src/commanders/textfresser/orchestration/background/background-generate-coordinator.ts b/src/commanders/textfresser/orchestration/background/background-generate-coordinator.ts index 293226ad7..e881ebf97 100644 --- a/src/commanders/textfresser/orchestration/background/background-generate-coordinator.ts +++ b/src/commanders/textfresser/orchestration/background/background-generate-coordinator.ts @@ -19,6 +19,7 @@ import { sleep } from "../../../../utils/sleep"; import type { LemmaResult } from "../../commands/lemma/types"; import type { CommandError, CommandInput } from "../../commands/types"; import { buildPolicyDestinationPath } from "../../common/lemma-link-routing"; +import { extractErrorReason } from "../../errors"; import type { InFlightGenerate, PendingGenerate, @@ -169,10 +170,7 @@ export function createBackgroundGenerateCoordinator(params: { if (generateResult.isErr()) { const cleanupSummary = await cleanupIfEmpty(); const error = generateResult.error; - const reason = - "reason" in error - ? error.reason - : `Command failed: ${error.kind}`; + const reason = extractErrorReason(error); throw new Error( `${reason} (cleanup=${cleanupSummary}, owned=${targetOwnedByInvocation}, existedBefore=${targetExistedBefore})`, ); diff --git a/src/commanders/textfresser/orchestration/lemma/execute-lemma-flow.ts b/src/commanders/textfresser/orchestration/lemma/execute-lemma-flow.ts index f545636f0..f7adfe5ba 100644 --- a/src/commanders/textfresser/orchestration/lemma/execute-lemma-flow.ts +++ b/src/commanders/textfresser/orchestration/lemma/execute-lemma-flow.ts @@ -1,12 +1,12 @@ import { errAsync, ResultAsync } from "neverthrow"; import type { CommandContext } from "../../../../managers/obsidian/command-executor"; import type { VaultActionManager } from "../../../../managers/obsidian/vault-action-manager"; -import { logger } from "../../../../utils/logger"; import { resolveAttestation } from "../../commands/lemma/lemma-command"; import type { CommandError, CommandInput } from "../../commands/types"; import { buildPolicyDestinationPath } from "../../common/lemma-link-routing"; import { CommandErrorKind } from "../../errors"; import type { TextfresserState } from "../../state/textfresser-state"; +import { notifyAndLogError } from "../shared/notify-error"; import { buildLemmaInvocationKey, getValidLemmaInvocationCache, @@ -49,15 +49,7 @@ export function executeLemmaFlow(params: { readContent: (splitPath) => vam.readContent(splitPath), state, }), - ).mapErr((error) => { - const reason = - "reason" in error - ? error.reason - : `Command failed: ${error.kind}`; - notify(`⚠ ${reason}`); - logger.warn("[Textfresser.Lemma] Failed:", error); - return error; - }); + ).mapErr(notifyAndLogError(notify, "Textfresser.Lemma")); } return new ResultAsync( @@ -99,13 +91,5 @@ export function executeLemmaFlow(params: { notify(`✓ ${lemma.lemma}${pos}`); requestBackgroundGenerate(notify); }) - .mapErr((error) => { - const reason = - "reason" in error - ? error.reason - : `Command failed: ${error.kind}`; - notify(`⚠ ${reason}`); - logger.warn("[Textfresser.Lemma] Failed:", error); - return error; - }); + .mapErr(notifyAndLogError(notify, "Textfresser.Lemma")); } diff --git a/src/commanders/textfresser/orchestration/shared/notify-error.ts b/src/commanders/textfresser/orchestration/shared/notify-error.ts new file mode 100644 index 000000000..816fbd55f --- /dev/null +++ b/src/commanders/textfresser/orchestration/shared/notify-error.ts @@ -0,0 +1,16 @@ +import { logger } from "../../../../utils/logger"; +import type { CommandError } from "../../commands/types"; +import { extractErrorReason } from "../../errors"; + +/** Returns a .mapErr() callback that notifies the user and logs the error. */ +export function notifyAndLogError( + notify: (message: string) => void, + logContext: string, +): (error: CommandError) => CommandError { + return (error) => { + const reason = extractErrorReason(error); + notify(`⚠ ${reason}`); + logger.warn(`[${logContext}] Failed:`, error); + return error; + }; +} diff --git a/src/commanders/textfresser/textfresser.ts b/src/commanders/textfresser/textfresser.ts index 3e1514961..187eba8ff 100644 --- a/src/commanders/textfresser/textfresser.ts +++ b/src/commanders/textfresser/textfresser.ts @@ -9,7 +9,6 @@ import type { EventHandler } from "../../managers/obsidian/user-event-intercepto import type { VaultActionManager } from "../../managers/obsidian/vault-action-manager"; import type { ApiService } from "../../stateless-helpers/api-service"; import type { LanguagesConfig } from "../../types"; -import { logger } from "../../utils/logger"; import { actionCommandFnForCommandKind } from "./commands"; import type { CommandInput, TextfresserCommandKind } from "./commands/types"; import type { PathLookupFn } from "./common/target-path-resolver"; @@ -21,6 +20,7 @@ import { import { createWikilinkClickHandler } from "./orchestration/handlers/wikilink-click-handler"; import { executeLemmaFlow } from "./orchestration/lemma/execute-lemma-flow"; import { dispatchActions } from "./orchestration/shared/dispatch-actions"; +import { notifyAndLogError } from "./orchestration/shared/notify-error"; import { createInitialTextfresserState, type TextfresserState, @@ -104,15 +104,7 @@ export class Textfresser { this.scrollToTargetBlock(); } }) - .mapErr((error) => { - const reason = - "reason" in error - ? error.reason - : `Command failed: ${error.kind}`; - notify(`⚠ ${reason}`); - logger.warn(`[Textfresser.${commandName}] Failed:`, error); - return error; - }); + .mapErr(notifyAndLogError(notify, `Textfresser.${commandName}`)); } createHandler(): EventHandler {