refactor: enhance error handling and state management in auth and image modules

This commit is contained in:
SquidSpirit 2025-10-14 15:34:34 +08:00
parent c5fab7d61a
commit 13696e394f
No known key found for this signature in database
GPG Key ID: 9442A714C9F83C51
11 changed files with 135 additions and 54 deletions

View File

@ -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<AuthViewModel>;
@ -26,16 +31,18 @@ export class AuthBloc {
}
private async loadCurrentUser(): Promise<AuthState> {
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;

View File

@ -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);
}

View File

@ -5,25 +5,56 @@ export enum StatusType {
Error,
}
export interface IdleState<T> {
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
interface IdleState<T> {
status: StatusType.Idle;
data?: T;
}
export interface LoadingState<T> {
interface LoadingState<T> {
status: StatusType.Loading;
data?: T;
}
export interface SuccessState<T> {
interface SuccessState<T> {
status: StatusType.Success;
data: T;
}
export interface ErrorState<T> {
interface ErrorState<T> {
status: StatusType.Error;
data?: T;
error: Error;
}
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
export abstract class StateFactory {
static idle<T>(data?: T): AsyncState<T> {
return {
status: StatusType.Idle,
data,
};
}
static loading<T>(data?: T): AsyncState<T> {
return {
status: StatusType.Loading,
data,
};
}
static success<T>(data: T): AsyncState<T> {
return {
status: StatusType.Success,
data,
};
}
static error<T>(error: unknown, data?: T): AsyncState<T> {
return {
status: StatusType.Error,
error: error instanceof Error ? error : new Error('Unknown error'),
data,
};
}
}

View File

@ -0,0 +1,8 @@
export class HttpError extends Error {
constructor(
public readonly status: number,
url: URL
) {
super(`HTTP ${status} at ${url.href}`);
}
}

View File

@ -0,0 +1,4 @@
export enum HttpStatusCode {
UNAUTHORIZED = 401,
NOT_FOUND = 404,
}

View File

@ -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<ImageInfoViewModel>;
@ -25,15 +30,16 @@ export class ImageBloc {
}
private async uploadImage(file: File): Promise<ImageInfoState> {
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;

View File

@ -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);
}

View File

@ -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<PostViewModel>;
@ -33,20 +38,23 @@ export class PostBloc {
}
private async loadPost(id: string): Promise<PostState> {
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;
}

View File

@ -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<readonly PostInfoViewModel[]>;
@ -33,13 +38,17 @@ export class PostListBloc {
}
private async loadPosts(): Promise<PostListState> {
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;

View File

@ -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);
}

View File

@ -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}
<div></div>
{:else if hasError}
{:else if !isAuthenticated}
<ErrorPage />
{:else}
<div class="min-h-content-height grid grid-cols-[auto_1fr]">