Merge pull request #17 from ttusk/fix/error-handling

fix(errors): ValidationError for domain failures, runGuarded boundary
This commit is contained in:
Luiz Gustavo 2026-07-03 00:40:45 -03:00 committed by GitHub
commit 08138c1d2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 710 additions and 700 deletions

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ import { ActiveCycleSnapshot } from "@/application/use-cases/GetActiveCycleSnaps
import { CycleService } from "@/domain/services/CycleService";
import { ItemProgressService } from "@/domain/services/ItemProgressService";
import { ActiveContestGuard } from "@/application/guards/ActiveContestGuard";
import { NotFoundError } from "@/domain/errors/DomainErrors";
import { NotFoundError, ValidationError } from "@/domain/errors/DomainErrors";
/**
* Use case for advancing the study cycle to the next subject.
@ -38,7 +38,7 @@ export class AdvanceCycleUseCase {
);
if (!nextSubject) {
throw new Error(`Contest "${activeContestId}" has no active subjects.`);
throw new ValidationError(`Contest "${activeContestId}" has no active subjects.`);
}
const subjectItems = data.studyItems.filter(

View file

@ -37,14 +37,14 @@ export class ReorderSubjectsUseCase {
.filter((subject) => subject.contestId === input.contestId);
if (contestSubjects.length !== input.subjectIdsInOrder.length) {
throw new Error("The provided subject order does not match the contest subject list.");
throw new ValidationError("The provided subject order does not match the contest subject list.");
}
const subjectIdSet = new Set(contestSubjects.map((subject) => subject.id));
for (const subjectId of input.subjectIdsInOrder) {
if (!subjectIdSet.has(subjectId)) {
throw new Error(`Subject "${subjectId}" does not belong to contest "${input.contestId}".`);
throw new ValidationError(`Subject "${subjectId}" does not belong to contest "${input.contestId}".`);
}
}

View file

@ -2,6 +2,7 @@ import type { Plugin } from "obsidian";
import { Notice } from "obsidian";
import type { PluginDataStore } from "@/application/ports/PluginDataStore";
import { DomHelpers } from "@/ui/view/shared/DomHelpers";
import { AdvanceCycleUseCase } from "@/application/use-cases/AdvanceCycleUseCase";
import { CreateContestUseCase } from "@/application/use-cases/CreateContestUseCase";
import { CreateStudyItemUseCase } from "@/application/use-cases/CreateStudyItemUseCase";
@ -184,21 +185,18 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
plugin.addCommand({
id: "leif-advance-cycle",
name: "Advance cycle",
callback: async () => {
try {
callback: () =>
DomHelpers.runGuarded(async () => {
const state = await advanceCycle.execute();
new Notice(`Current subject: ${state.currentSubjectId ?? "none"}`);
} catch (error) {
new Notice(error instanceof Error ? error.message : "Could not advance cycle.");
}
}
}, "Could not advance cycle.")
});
plugin.addCommand({
id: "leif-show-cycle-snapshot",
name: "Show cycle snapshot",
callback: async () => {
try {
callback: () =>
DomHelpers.runGuarded(async () => {
const snapshot = await getActiveCycleSnapshot.execute();
const data = await dataStore.load();
const itemMap = new Map(data.studyItems.map((item) => [item.id, item.title]));
@ -210,10 +208,7 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
new Notice(
`Current: ${currentLabel} | Next: ${nextLabel} | Current item: ${currentItemLabel} | Next item: ${nextItemLabel}`
);
} catch (error) {
new Notice(error instanceof Error ? error.message : "Could not read cycle snapshot.");
}
}
}, "Could not read cycle snapshot.")
});
plugin.addCommand({
@ -237,8 +232,8 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
plugin.addCommand({
id: "leif-show-summary",
name: "Show active contest summary",
callback: async () => {
try {
callback: () =>
DomHelpers.runGuarded(async () => {
const summary = await getActiveContestSummary.execute();
if (summary.subjectSummaries.length === 0) {
@ -258,10 +253,7 @@ export function registerCommands(plugin: Plugin, dataStore: PluginDataStore): vo
.join(" | ");
new Notice(message);
} catch (error) {
new Notice(error instanceof Error ? error.message : "Could not load active contest summary.");
}
}
}, "Could not load active contest summary.")
});
plugin.addCommand({

View file

@ -493,6 +493,23 @@ export class DomHelpers {
new Notice(error instanceof Error ? error.message : fallbackMessage);
}
/**
* Error-boundary helper: runs an async action and surfaces any failure
* through notifyError so callers don't repeat try/catch + Notice boilerplate.
*/
static runGuarded(
action: () => void | Promise<void>,
fallbackMessage: string
): Promise<void> {
return (async () => {
try {
await action();
} catch (error) {
this.notifyError(error, fallbackMessage);
}
})();
}
/**
* Creates a modal overlay with a centered card.
* Implements role=dialog, aria-modal, a focus trap and Escape-to-close.