From 13696e394f47679d78692f64f54be802831f6636 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Tue, 14 Oct 2025 15:34:34 +0800 Subject: [PATCH] refactor: enhance error handling and state management in auth and image modules --- .../lib/auth/adapter/presenter/authBloc.ts | 27 +++++++----- .../auth/framework/api/authApiServiceImpl.ts | 8 +++- .../common/adapter/presenter/asyncState.ts | 41 ++++++++++++++++--- .../src/lib/common/framework/web/httpError.ts | 8 ++++ .../common/framework/web/httpStatusCode.ts | 4 ++ .../lib/image/adapter/presenter/imageBloc.ts | 16 +++++--- .../framework/api/imageApiServiceImpl.ts | 4 +- .../lib/post/adapter/presenter/postBloc.ts | 32 +++++++++------ .../post/adapter/presenter/postListBloc.ts | 25 +++++++---- .../post/framework/api/postApiServiceImpl.ts | 10 ++++- frontend/src/routes/dashboard/+layout.svelte | 14 ++----- 11 files changed, 135 insertions(+), 54 deletions(-) create mode 100644 frontend/src/lib/common/framework/web/httpError.ts create mode 100644 frontend/src/lib/common/framework/web/httpStatusCode.ts diff --git a/frontend/src/lib/auth/adapter/presenter/authBloc.ts b/frontend/src/lib/auth/adapter/presenter/authBloc.ts index 2a962a4..1d1c7d8 100644 --- a/frontend/src/lib/auth/adapter/presenter/authBloc.ts +++ b/frontend/src/lib/auth/adapter/presenter/authBloc.ts @@ -1,7 +1,12 @@ import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel'; import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel'; import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type AuthState = AsyncState; @@ -26,16 +31,18 @@ export class AuthBloc { } private async loadCurrentUser(): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + this.state.set(StateFactory.loading(get(this.state).data)); - const user = await this.getCurrentUserUseCase.execute(); - - const userViewModel = user ? UserViewModel.fromEntity(user) : null; - const authViewModel = AuthViewModel.fromEntity(userViewModel); - const result: AuthState = { - status: StatusType.Success, - data: authViewModel, - }; + let result: AuthState; + try { + const user = await this.getCurrentUserUseCase.execute(); + const userViewModel = user ? UserViewModel.fromEntity(user) : null; + const authViewModel = AuthViewModel.fromEntity(userViewModel); + result = StateFactory.success(authViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); + } this.state.set(result); return result; diff --git a/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts index f041c0e..31b72e0 100644 --- a/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts +++ b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts @@ -1,5 +1,7 @@ import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto'; +import { HttpError } from '$lib/common/framework/web/httpError'; +import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode'; import { Environment } from '$lib/environment'; export class AuthApiServiceImpl implements AuthApiService { @@ -10,10 +12,14 @@ export class AuthApiServiceImpl implements AuthApiService { const response = await this.fetchFn(url); - if (!response.ok) { + if (response.status === HttpStatusCode.UNAUTHORIZED) { return null; } + if (!response.ok) { + throw new HttpError(response.status, url); + } + const json = await response.json(); return UserResponseDto.fromJson(json); } diff --git a/frontend/src/lib/common/adapter/presenter/asyncState.ts b/frontend/src/lib/common/adapter/presenter/asyncState.ts index 19e10cf..bd90704 100644 --- a/frontend/src/lib/common/adapter/presenter/asyncState.ts +++ b/frontend/src/lib/common/adapter/presenter/asyncState.ts @@ -5,25 +5,56 @@ export enum StatusType { Error, } -export interface IdleState { +export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; + +interface IdleState { status: StatusType.Idle; data?: T; } -export interface LoadingState { +interface LoadingState { status: StatusType.Loading; data?: T; } -export interface SuccessState { +interface SuccessState { status: StatusType.Success; data: T; } -export interface ErrorState { +interface ErrorState { status: StatusType.Error; data?: T; error: Error; } -export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; +export abstract class StateFactory { + static idle(data?: T): AsyncState { + return { + status: StatusType.Idle, + data, + }; + } + + static loading(data?: T): AsyncState { + return { + status: StatusType.Loading, + data, + }; + } + + static success(data: T): AsyncState { + return { + status: StatusType.Success, + data, + }; + } + + static error(error: unknown, data?: T): AsyncState { + return { + status: StatusType.Error, + error: error instanceof Error ? error : new Error('Unknown error'), + data, + }; + } +} diff --git a/frontend/src/lib/common/framework/web/httpError.ts b/frontend/src/lib/common/framework/web/httpError.ts new file mode 100644 index 0000000..207d828 --- /dev/null +++ b/frontend/src/lib/common/framework/web/httpError.ts @@ -0,0 +1,8 @@ +export class HttpError extends Error { + constructor( + public readonly status: number, + url: URL + ) { + super(`HTTP ${status} at ${url.href}`); + } +} diff --git a/frontend/src/lib/common/framework/web/httpStatusCode.ts b/frontend/src/lib/common/framework/web/httpStatusCode.ts new file mode 100644 index 0000000..071763b --- /dev/null +++ b/frontend/src/lib/common/framework/web/httpStatusCode.ts @@ -0,0 +1,4 @@ +export enum HttpStatusCode { + UNAUTHORIZED = 401, + NOT_FOUND = 404, +} diff --git a/frontend/src/lib/image/adapter/presenter/imageBloc.ts b/frontend/src/lib/image/adapter/presenter/imageBloc.ts index 7fceeb1..ee44433 100644 --- a/frontend/src/lib/image/adapter/presenter/imageBloc.ts +++ b/frontend/src/lib/image/adapter/presenter/imageBloc.ts @@ -1,6 +1,11 @@ -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; import { ImageInfoViewModel } from '$lib/image/adapter/presenter/imageInfoViewModel'; import type { UploadImageUseCase } from '$lib/image/application/useCase/uploadImageUseCase'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type ImageInfoState = AsyncState; @@ -25,15 +30,16 @@ export class ImageBloc { } private async uploadImage(file: File): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + this.state.set(StateFactory.loading(get(this.state).data)); let result: ImageInfoState; try { const imageInfo = await this.uploadImageUseCase.execute(file); const imageInfoViewModel = ImageInfoViewModel.fromEntity(imageInfo); - result = { status: StatusType.Success, data: imageInfoViewModel }; - } catch (error) { - result = { status: StatusType.Error, error: error as Error }; + result = StateFactory.success(imageInfoViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); } return result; diff --git a/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts b/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts index 276aaf4..813fc0c 100644 --- a/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts +++ b/frontend/src/lib/image/framework/api/imageApiServiceImpl.ts @@ -1,3 +1,4 @@ +import { HttpError } from '$lib/common/framework/web/httpError'; import { Environment } from '$lib/environment'; import type { ImageApiService } from '$lib/image/adapter/gateway/imageApiService'; import { ImageInfoResponseDto } from '$lib/image/adapter/gateway/imageInfoResponseDto'; @@ -17,8 +18,9 @@ export class ImageApiServiceImpl implements ImageApiService { }); if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`); + throw new HttpError(response.status, url); } + const data = await response.json(); return ImageInfoResponseDto.fromJson(data); } diff --git a/frontend/src/lib/post/adapter/presenter/postBloc.ts b/frontend/src/lib/post/adapter/presenter/postBloc.ts index a0fb224..0b6190e 100644 --- a/frontend/src/lib/post/adapter/presenter/postBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postBloc.ts @@ -1,6 +1,11 @@ -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; import { PostViewModel } from '$lib/post/adapter/presenter/postViewModel'; import type { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type PostState = AsyncState; @@ -33,20 +38,23 @@ export class PostBloc { } private async loadPost(id: string): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + this.state.set(StateFactory.loading(get(this.state).data)); - const post = await this.getPostUseCase.execute(id); - if (!post) { - this.state.set({ status: StatusType.Error, error: new Error('Post not found') }); - return get(this.state); + let result: PostState; + try { + const post = await this.getPostUseCase.execute(id); + if (!post) { + result = StateFactory.error(new Error('Post not found')); + this.state.set(result); + return result; + } + const postViewModel = PostViewModel.fromEntity(post); + result = StateFactory.success(postViewModel); + } catch (e) { + result = StateFactory.error(e); + captureException(e); } - const postViewModel = PostViewModel.fromEntity(post); - const result: PostState = { - status: StatusType.Success, - data: postViewModel, - }; - this.state.set(result); return result; } diff --git a/frontend/src/lib/post/adapter/presenter/postListBloc.ts b/frontend/src/lib/post/adapter/presenter/postListBloc.ts index 829c38d..48fbcf1 100644 --- a/frontend/src/lib/post/adapter/presenter/postListBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postListBloc.ts @@ -1,6 +1,11 @@ -import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { + StateFactory, + StatusType, + type AsyncState, +} from '$lib/common/adapter/presenter/asyncState'; import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import type { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; +import { captureException } from '@sentry/sveltekit'; import { get, writable } from 'svelte/store'; export type PostListState = AsyncState; @@ -33,13 +38,17 @@ export class PostListBloc { } private async loadPosts(): Promise { - this.state.set({ status: StatusType.Loading, data: get(this.state).data }); - const posts = await this.getAllPostsUseCase.execute(); - const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post)); - const result: PostListState = { - status: StatusType.Success, - data: postViewModels, - }; + this.state.set(StateFactory.loading(get(this.state).data)); + + let result: PostListState; + try { + const posts = await this.getAllPostsUseCase.execute(); + const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post)); + result = StateFactory.success(postViewModels); + } catch (e) { + result = StateFactory.error(e); + captureException(e); + } this.state.set(result); return result; diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index d00a847..18d5485 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -1,3 +1,5 @@ +import { HttpError } from '$lib/common/framework/web/httpError'; +import { HttpStatusCode } from '$lib/common/framework/web/httpStatusCode'; import { Environment } from '$lib/environment'; import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto'; @@ -12,7 +14,7 @@ export class PostApiServiceImpl implements PostApiService { const response = await this.fetchFn(url); if (!response.ok) { - return []; + throw new HttpError(response.status, url); } const json = await response.json(); @@ -24,10 +26,14 @@ export class PostApiServiceImpl implements PostApiService { const response = await this.fetchFn(url); - if (!response.ok) { + if (response.status === HttpStatusCode.NOT_FOUND) { return null; } + if (!response.ok) { + throw new HttpError(response.status, url); + } + const json = await response.json(); return PostResponseDto.fromJson(json); } diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 0f6b7d7..73bedb6 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -20,15 +20,9 @@ const isLoading = $derived( authState.status === StatusType.Loading || authState.status === StatusType.Idle ); - const hasError = $derived.by(() => { - if (authState.status === StatusType.Error) { - return true; - } - if (authState.status === StatusType.Success && !authState.data.isAuthenticated) { - return true; - } - return false; - }); + const isAuthenticated = $derived( + authState.status === StatusType.Success && authState.data.isAuthenticated + ); const links: DashboardLink[] = [ { label: 'Post', href: '/dashboard/post' }, @@ -39,7 +33,7 @@ {#if isLoading}
-{:else if hasError} +{:else if !isAuthenticated} {:else}