From e02fb4fa94991308962c6035e5a588602957702b Mon Sep 17 00:00:00 2001 From: forehalo Date: Thu, 6 Mar 2025 13:10:18 +0000 Subject: [PATCH] refactor(core): standardize frontend error handling (#10667) --- packages/backend/server/src/base/error/def.ts | 6 + .../server/src/base/error/errors.gen.ts | 7 ++ packages/common/error/README.md | 3 + packages/common/error/package.json | 15 +++ .../common/error/src/__tests__/index.spec.ts | 102 +++++++++++++++++ packages/common/error/src/index.ts | 105 ++++++++++++++++++ packages/common/error/tsconfig.json | 10 ++ packages/common/infra/package.json | 1 + packages/common/infra/src/livedata/index.ts | 1 + packages/common/infra/src/livedata/ops.ts | 27 +++++ packages/common/infra/tsconfig.json | 6 +- packages/frontend/component/package.json | 1 + .../member-components/join-failed-page.tsx | 9 +- packages/frontend/component/tsconfig.json | 1 + packages/frontend/core/package.json | 1 + .../blocksuite/ai/provider/copilot-client.ts | 12 +- ...se-register-blocksuite-editor-commands.tsx | 4 +- .../providers/swr-config-provider.tsx | 24 ++-- .../root-app-sidebar/trash-button.tsx | 5 +- .../components/sign-in/sign-in-with-email.tsx | 17 +-- .../setting/general-setting/billing/index.tsx | 6 +- .../workspace-setting/billing/index.tsx | 6 +- .../license/self-host-team-card.tsx | 6 +- .../members/cloud-members-panel.tsx | 11 +- .../workspace-setting/members/member-list.tsx | 9 +- .../pages/auth/confirm-change-email.tsx | 12 +- .../pages/auth/email-verified-email.tsx | 11 +- .../core/src/desktop/pages/invite/index.tsx | 6 +- .../src/desktop/pages/subscribe/index.tsx | 15 +-- .../modules/cloud/entities/cloud-doc-meta.ts | 11 +- .../src/modules/cloud/entities/invoices.ts | 11 +- .../src/modules/cloud/entities/session.ts | 7 +- .../cloud/entities/subscription-prices.ts | 11 +- .../modules/cloud/entities/subscription.ts | 11 +- .../cloud/entities/user-copilot-quota.ts | 11 +- .../modules/cloud/entities/user-feature.ts | 11 +- .../src/modules/cloud/entities/user-quota.ts | 11 +- .../cloud/entities/workspace-invoices.ts | 11 +- .../cloud/entities/workspace-subscription.ts | 11 +- .../frontend/core/src/modules/cloud/error.ts | 30 ----- .../frontend/core/src/modules/cloud/index.ts | 6 - .../modules/cloud/services/accept-invite.ts | 12 +- .../core/src/modules/cloud/services/auth.ts | 12 +- .../core/src/modules/cloud/services/fetch.ts | 77 +++++++------ .../src/modules/cloud/services/graphql.ts | 15 +-- .../services/selfhost-generate-license.ts | 22 +--- .../cloud/services/selfhost-license.ts | 11 +- .../import-template/entities/downloader.ts | 11 +- .../modules/permissions/entities/members.ts | 11 +- .../permissions/services/doc-granted-users.ts | 11 +- .../permissions/services/member-search.ts | 11 +- .../core/src/modules/quota/entities/quota.ts | 12 +- .../share-doc/entities/share-docs-list.ts | 11 +- .../modules/share-doc/entities/share-info.ts | 11 +- .../general-access/members-permission.tsx | 5 +- .../general-access/public-page-button.tsx | 5 +- .../invite-member-editor.tsx | 9 +- .../member-management/member-item.tsx | 7 +- .../share-setting/entities/share-setting.ts | 12 +- packages/frontend/core/tsconfig.json | 1 + packages/frontend/graphql/package.json | 1 + packages/frontend/graphql/src/error.ts | 83 -------------- packages/frontend/graphql/src/fetcher.ts | 2 +- packages/frontend/graphql/src/index.ts | 1 - packages/frontend/graphql/tsconfig.json | 3 +- packages/frontend/i18n/src/i18n.gen.ts | 4 + packages/frontend/i18n/src/resources/en.json | 1 + tools/utils/src/workspace.gen.ts | 15 ++- tsconfig.json | 1 + yarn.lock | 17 ++- 70 files changed, 495 insertions(+), 480 deletions(-) create mode 100644 packages/common/error/README.md create mode 100644 packages/common/error/package.json create mode 100644 packages/common/error/src/__tests__/index.spec.ts create mode 100644 packages/common/error/src/index.ts create mode 100644 packages/common/error/tsconfig.json delete mode 100644 packages/frontend/core/src/modules/cloud/error.ts delete mode 100644 packages/frontend/graphql/src/error.ts diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 75fd8968c1..5a649f9e3e 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -5,6 +5,7 @@ import { HttpStatus, Logger } from '@nestjs/common'; import { ClsServiceManager } from 'nestjs-cls'; export type UserFriendlyErrorBaseType = + | 'network_error' | 'bad_request' | 'too_many_requests' | 'resource_not_found' @@ -26,6 +27,7 @@ export type UserFriendlyErrorOptions = { }; const BaseTypeToHttpStatusMap: Record = { + network_error: HttpStatus.GATEWAY_TIMEOUT, too_many_requests: HttpStatus.TOO_MANY_REQUESTS, bad_request: HttpStatus.BAD_REQUEST, resource_not_found: HttpStatus.NOT_FOUND, @@ -239,6 +241,10 @@ export const USER_FRIENDLY_ERRORS = { type: 'internal_server_error', message: 'An internal error occurred.', }, + network_error: { + type: 'network_error', + message: 'Network error.', + }, too_many_request: { type: 'too_many_requests', message: 'Too many requests.', diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index bd26f0903d..005903362b 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -10,6 +10,12 @@ export class InternalServerError extends UserFriendlyError { } } +export class NetworkError extends UserFriendlyError { + constructor(message?: string) { + super('network_error', 'network_error', message); + } +} + export class TooManyRequest extends UserFriendlyError { constructor(message?: string) { super('too_many_requests', 'too_many_request', message); @@ -851,6 +857,7 @@ export class UnsupportedClientVersion extends UserFriendlyError { } export enum ErrorNames { INTERNAL_SERVER_ERROR, + NETWORK_ERROR, TOO_MANY_REQUEST, NOT_FOUND, BAD_REQUEST, diff --git a/packages/common/error/README.md b/packages/common/error/README.md new file mode 100644 index 0000000000..652734ccb4 --- /dev/null +++ b/packages/common/error/README.md @@ -0,0 +1,3 @@ +# @affine/error + +AFFiNE error handler utilities diff --git a/packages/common/error/package.json b/packages/common/error/package.json new file mode 100644 index 0000000000..9661c4f1fa --- /dev/null +++ b/packages/common/error/package.json @@ -0,0 +1,15 @@ +{ + "name": "@affine/error", + "version": "0.20.0", + "private": true, + "exports": { + ".": "./src/index.ts" + }, + "peerDependencies": { + "@affine/graphql": "workspace:*", + "graphql": "^16.9.0" + }, + "devDependencies": { + "vitest": "^3.0.7" + } +} diff --git a/packages/common/error/src/__tests__/index.spec.ts b/packages/common/error/src/__tests__/index.spec.ts new file mode 100644 index 0000000000..3048f9c4a0 --- /dev/null +++ b/packages/common/error/src/__tests__/index.spec.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; + +import { GraphQLError, UserFriendlyError } from '..'; + +describe('user friendly error', () => { + it('should create from graphql error', () => { + const gqlError = new GraphQLError('test', { + extensions: { + status: 400, + code: 'BAD_REQUEST', + type: 'BAD_REQUEST', + name: 'SOME_ERROR_NAME', + message: 'test', + }, + }); + + const error = UserFriendlyError.fromAny(gqlError); + + expect(error.name).toBe('SOME_ERROR_NAME'); + expect(error.status).toBe(400); + expect(error.code).toBe('BAD_REQUEST'); + expect(error.type).toBe('BAD_REQUEST'); + expect(error.message).toBe('test'); + }); + + it('should create from any error', () => { + const error = UserFriendlyError.fromAny(new Error('test')); + expect(error.message).toBe('test'); + }); + + it('should create from object', () => { + const error = UserFriendlyError.fromAny({ + name: 'SOME_ERROR_NAME', + status: 400, + code: 'BAD_REQUEST', + type: 'BAD_REQUEST', + message: 'test', + }); + + expect(error.message).toBe('test'); + }); + + it('should create from string', () => { + const error = UserFriendlyError.fromAny('test error'); + expect(error.message).toBe('test error'); + }); + + it('should create fallback error', () => { + const error = UserFriendlyError.fromAny(null); + + expect(error.message).toBe( + 'Unhandled error raised. Please contact us for help.' + ); + }); + + it('should test network error', () => { + const error = UserFriendlyError.fromAny({ + name: 'NETWORK_ERROR', + status: 500, + code: 'INTERNAL_SERVER_ERROR', + type: 'INTERNAL_SERVER_ERROR', + message: 'test', + }); + + expect(error.isNetworkError()).toBe(true); + + const error2 = UserFriendlyError.fromAny({ + name: 'SOME_ERROR_NAME', + status: 400, + code: 'BAD_REQUEST', + type: 'BAD_REQUEST', + message: 'test', + }); + + expect(error2.isNetworkError()).toBe(false); + }); + + it('should test name', () => { + const error = UserFriendlyError.fromAny({ + name: 'SOME_ERROR_NAME', + status: 400, + code: 'BAD_REQUEST', + type: 'BAD_REQUEST', + message: 'test', + }); + + // @ts-expect-error test name + expect(error.is('SOME_ERROR_NAME')).toBe(true); + }); + + it('should test status', () => { + const error = UserFriendlyError.fromAny({ + name: 'SOME_ERROR_NAME', + status: 400, + code: 'BAD_REQUEST', + type: 'BAD_REQUEST', + message: 'test', + }); + + expect(error.isStatus(400)).toBe(true); + }); +}); diff --git a/packages/common/error/src/index.ts b/packages/common/error/src/index.ts new file mode 100644 index 0000000000..70a22e32af --- /dev/null +++ b/packages/common/error/src/index.ts @@ -0,0 +1,105 @@ +import type { ErrorDataUnion, ErrorNames } from '@affine/graphql'; +import { GraphQLError as BaseGraphQLError } from 'graphql'; + +export type ErrorName = keyof typeof ErrorNames | 'NETWORK_ERROR'; + +export interface UserFriendlyErrorResponse { + status: number; + code: string; + type: string; + name: ErrorName; + message: string; + data?: any; + stacktrace?: string; +} + +function UnknownError(message: string) { + return new UserFriendlyError({ + status: 500, + code: 'INTERNAL_SERVER_ERROR', + type: 'INTERNAL_SERVER_ERROR', + name: 'INTERNAL_SERVER_ERROR', + message, + }); +} + +type ToPascalCase = S extends `${infer A}_${infer B}` + ? `${Capitalize>}${ToPascalCase}` + : Capitalize>; + +export type ErrorData = { + [K in ErrorNames]: Extract< + ErrorDataUnion, + { __typename?: `${ToPascalCase}DataType` } + >; +}; + +export class GraphQLError extends BaseGraphQLError { + // @ts-expect-error better to be a known type without any type casting + override extensions!: UserFriendlyErrorResponse; +} + +export class UserFriendlyError + extends Error + implements UserFriendlyErrorResponse +{ + readonly status = this.response.status; + readonly code = this.response.code; + readonly type = this.response.type; + override readonly name = this.response.name; + override readonly message = this.response.message; + readonly data = this.response.data; + readonly stacktrace = this.response.stacktrace; + + static fromAny(anything: any) { + if (anything instanceof UserFriendlyError) { + return anything; + } + + switch (typeof anything) { + case 'string': + return UnknownError(anything); + case 'object': { + if (anything) { + if (anything instanceof GraphQLError) { + return new UserFriendlyError(anything.extensions); + } else if (anything.type && anything.name && anything.message) { + return new UserFriendlyError(anything); + } else if (anything.message) { + return UnknownError(anything.message); + } + } + } + } + + return UnknownError('Unhandled error raised. Please contact us for help.'); + } + + constructor(private readonly response: UserFriendlyErrorResponse) { + super(response.message); + } + + is(name: ErrorName) { + return this.name === name; + } + + isStatus(status: number) { + return this.status === status; + } + + static isNetworkError(error: UserFriendlyError) { + return error.name === 'NETWORK_ERROR'; + } + + static notNetworkError(error: UserFriendlyError) { + return !UserFriendlyError.isNetworkError(error); + } + + isNetworkError() { + return UserFriendlyError.isNetworkError(this); + } + + notNetworkError() { + return UserFriendlyError.notNetworkError(this); + } +} diff --git a/packages/common/error/tsconfig.json b/packages/common/error/tsconfig.json new file mode 100644 index 0000000000..76d5d68d61 --- /dev/null +++ b/packages/common/error/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.web.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" + }, + "include": ["./src"], + "references": [] +} diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index a6b41726f9..a4ac5a6d97 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -13,6 +13,7 @@ "dependencies": { "@affine/debug": "workspace:*", "@affine/env": "workspace:*", + "@affine/error": "workspace:*", "@affine/templates": "workspace:*", "@datastructures-js/binary-search-tree": "^5.3.2", "@preact/signals-core": "^1.8.0", diff --git a/packages/common/infra/src/livedata/index.ts b/packages/common/infra/src/livedata/index.ts index 0328d8443a..9e3690af8f 100644 --- a/packages/common/infra/src/livedata/index.ts +++ b/packages/common/infra/src/livedata/index.ts @@ -8,5 +8,6 @@ export { mapInto, onComplete, onStart, + smartRetry, } from './ops'; export { useEnsureLiveData, useLiveData } from './react'; diff --git a/packages/common/infra/src/livedata/ops.ts b/packages/common/infra/src/livedata/ops.ts index 711d5b2526..8aa584054b 100644 --- a/packages/common/infra/src/livedata/ops.ts +++ b/packages/common/infra/src/livedata/ops.ts @@ -1,3 +1,4 @@ +import { UserFriendlyError } from '@affine/error'; import { catchError, connect, @@ -144,6 +145,32 @@ export function backoffRetry({ ); } +export function smartRetry({ + count = 3, + delay = 200, + maxDelay = 15000, +}: { + count?: number; + delay?: number; + maxDelay?: number; +} = {}) { + return (obs$: Observable) => + obs$.pipe( + backoffRetry({ + when: UserFriendlyError.isNetworkError, + count: Infinity, + delay, + maxDelay, + }), + backoffRetry({ + when: UserFriendlyError.notNetworkError, + count, + delay, + maxDelay, + }) + ); +} + /** * An operator that combines `exhaustMap` and `switchMap`. * diff --git a/packages/common/infra/tsconfig.json b/packages/common/infra/tsconfig.json index 0fbc4070c2..605949f390 100644 --- a/packages/common/infra/tsconfig.json +++ b/packages/common/infra/tsconfig.json @@ -6,5 +6,9 @@ "outDir": "./dist", "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" }, - "references": [{ "path": "../debug" }, { "path": "../env" }] + "references": [ + { "path": "../debug" }, + { "path": "../env" }, + { "path": "../error" } + ] } diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index 98527882a2..019c61847b 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -22,6 +22,7 @@ "dependencies": { "@affine/debug": "workspace:*", "@affine/electron-api": "workspace:*", + "@affine/error": "workspace:*", "@affine/graphql": "workspace:*", "@affine/i18n": "workspace:*", "@atlaskit/pragmatic-drag-and-drop": "^1.4.0", diff --git a/packages/frontend/component/src/components/member-components/join-failed-page.tsx b/packages/frontend/component/src/components/member-components/join-failed-page.tsx index d4eb7adedf..3f44eb4937 100644 --- a/packages/frontend/component/src/components/member-components/join-failed-page.tsx +++ b/packages/frontend/component/src/components/member-components/join-failed-page.tsx @@ -1,9 +1,6 @@ import { AuthPageContainer } from '@affine/component/auth-components'; -import { - ErrorNames, - type GetInviteInfoQuery, - UserFriendlyError, -} from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { ErrorNames, type GetInviteInfoQuery } from '@affine/graphql'; import { Trans, useI18n } from '@affine/i18n'; import { Avatar } from '../../ui/avatar'; @@ -16,7 +13,7 @@ export const JoinFailedPage = ({ inviteInfo: GetInviteInfoQuery['getInviteInfo']; error?: any; }) => { - const userFriendlyError = UserFriendlyError.fromAnyError(error); + const userFriendlyError = UserFriendlyError.fromAny(error); const t = useI18n(); return ( { - if ( - e instanceof GraphQLError || - (Array.isArray(e) && e[0] instanceof GraphQLError) - ) { - const graphQLError = e instanceof GraphQLError ? e : e[0]; - notify.error({ - title: 'GraphQL Error', - message: graphQLError.toString(), - }); - } else { - notify.error({ - title: 'Error', - message: e.toString(), - }); - } + const error = UserFriendlyError.fromAny(e); + + notify.error({ + title: error.name, + message: error.message, + }); + throw e; }); } diff --git a/packages/frontend/core/src/components/root-app-sidebar/trash-button.tsx b/packages/frontend/core/src/components/root-app-sidebar/trash-button.tsx index beb283659c..37b2f73436 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/trash-button.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/trash-button.tsx @@ -9,7 +9,7 @@ import { DocsService } from '@affine/core/modules/doc'; import { GlobalContextService } from '@affine/core/modules/global-context'; import { GuardService } from '@affine/core/modules/permissions'; import type { AffineDNDData } from '@affine/core/types/dnd'; -import { UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; @@ -58,8 +58,7 @@ export const TrashButton = () => { docRecord.moveToTrash(); } catch (error) { console.error(error); - const userFriendlyError = - UserFriendlyError.fromAnyError(error); + const userFriendlyError = UserFriendlyError.fromAny(error); toast( t[`error.${userFriendlyError.name}`](userFriendlyError.data) ); diff --git a/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx b/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx index 83550acfb8..e1c0152e67 100644 --- a/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx +++ b/packages/frontend/core/src/components/sign-in/sign-in-with-email.tsx @@ -8,13 +8,10 @@ import { } from '@affine/component/auth-components'; import { Button } from '@affine/component/ui/button'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; -import { - AuthService, - BackendError, - CaptchaService, -} from '@affine/core/modules/cloud'; +import { AuthService, CaptchaService } from '@affine/core/modules/cloud'; import type { AuthSessionStatus } from '@affine/core/modules/cloud/entities/session'; import { Unreachable } from '@affine/env/constant'; +import type { UserFriendlyError } from '@affine/error'; import { Trans, useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import { @@ -143,12 +140,10 @@ export const SignInWithEmailStep = ({ try { await authService.signInMagicLink(email, otp, false); } catch (e) { - if (e instanceof BackendError) { - notify.error({ - title: e.originError.message, - }); - setOtpError(t['com.affine.auth.sign.auth.code.invalid']()); - } + notify.error({ + title: (e as UserFriendlyError).message, + }); + setOtpError(t['com.affine.auth.sign.auth.code.invalid']()); } finally { setIsVerifying(false); } diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx index ca759ebf29..97d9c3f4ff 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/billing/index.tsx @@ -15,6 +15,7 @@ import { SubscriptionService, } from '@affine/core/modules/cloud'; import { UrlService } from '@affine/core/modules/url'; +import { UserFriendlyError } from '@affine/error'; import type { InvoicesQuery } from '@affine/graphql'; import { createCustomerPortalMutation, @@ -22,7 +23,6 @@ import { SubscriptionPlan, SubscriptionRecurring, SubscriptionStatus, - UserFriendlyError, } from '@affine/graphql'; import { type I18nString, i18nTime, Trans, useI18n } from '@affine/i18n'; import { track } from '@affine/track'; @@ -539,8 +539,8 @@ const BillingHistory = () => { return ( {error - ? UserFriendlyError.fromAnyError(error).message - : 'Failed to load members'} + ? UserFriendlyError.fromAny(error).message + : 'Failed to load invoices'} ); } diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx index c1fdf3e868..43d6ca98c8 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/billing/index.tsx @@ -17,13 +17,13 @@ import { import { WorkspaceQuotaService } from '@affine/core/modules/quota'; import { UrlService } from '@affine/core/modules/url'; import { WorkspaceService } from '@affine/core/modules/workspace'; +import { UserFriendlyError } from '@affine/error'; import { createCustomerPortalMutation, type InvoicesQuery, InvoiceStatus, SubscriptionPlan, SubscriptionRecurring, - UserFriendlyError, } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; @@ -321,8 +321,8 @@ const BillingHistory = () => { return ( {error - ? UserFriendlyError.fromAnyError(error).message - : 'Failed to load members'} + ? UserFriendlyError.fromAny(error).message + : 'Failed to load invoices'} ); } diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx index 4fef244e24..d678c66763 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx @@ -8,7 +8,7 @@ import { import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; import { WorkspaceService } from '@affine/core/modules/workspace'; -import { UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; import { Trans, useI18n } from '@affine/i18n'; import { useLiveData, useService } from '@toeverything/infra'; import clsx from 'clsx'; @@ -91,7 +91,7 @@ export const SelfHostTeamCard = () => { setLoading(false); console.error(e); - const error = UserFriendlyError.fromAnyError(e); + const error = UserFriendlyError.fromAny(e); notify.error({ title: error.name, @@ -119,7 +119,7 @@ export const SelfHostTeamCard = () => { setLoading(false); console.error(e); - const error = UserFriendlyError.fromAnyError(e); + const error = UserFriendlyError.fromAny(e); notify.error({ title: error.name, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/cloud-members-panel.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/cloud-members-panel.tsx index eb658ead71..d90437d99f 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/cloud-members-panel.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/cloud-members-panel.tsx @@ -20,12 +20,9 @@ import { WorkspaceQuotaService } from '@affine/core/modules/quota'; import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting'; import { copyTextToClipboard } from '@affine/core/utils/clipboard'; import { emailRegex } from '@affine/core/utils/email-regex'; +import { UserFriendlyError } from '@affine/error'; import type { WorkspaceInviteLinkExpireTime } from '@affine/graphql'; -import { - ServerDeploymentType, - SubscriptionPlan, - UserFriendlyError, -} from '@affine/graphql'; +import { ServerDeploymentType, SubscriptionPlan } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { ExportIcon } from '@blocksuite/icons/rc'; @@ -129,7 +126,7 @@ export const CloudWorkspaceMembersPanel = ({ message: t['com.affine.payment.resume.success.team.message'](), }); } catch (err) { - const error = UserFriendlyError.fromAnyError(err); + const error = UserFriendlyError.fromAny(err); notify.error({ title: error.name, message: error.message, @@ -299,7 +296,7 @@ export const CloudWorkspaceMembersPanel = ({ return ( {error - ? UserFriendlyError.fromAnyError(error).message + ? UserFriendlyError.fromAny(error).message : 'Failed to load members'} ); diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx index 1385896746..77c42a183a 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/members/member-list.tsx @@ -6,11 +6,8 @@ import { WorkspaceMembersService, } from '@affine/core/modules/permissions'; import { WorkspaceService } from '@affine/core/modules/workspace'; -import { - Permission, - UserFriendlyError, - WorkspaceMemberStatus, -} from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { Permission, WorkspaceMemberStatus } from '@affine/graphql'; import { type I18nString, useI18n } from '@affine/i18n'; import { MoreVerticalIcon } from '@blocksuite/icons/rc'; import { @@ -75,7 +72,7 @@ export const MemberList = ({ ) : ( {error - ? UserFriendlyError.fromAnyError(error).message + ? UserFriendlyError.fromAny(error).message : 'Failed to load members'} ) diff --git a/packages/frontend/core/src/desktop/pages/auth/confirm-change-email.tsx b/packages/frontend/core/src/desktop/pages/auth/confirm-change-email.tsx index 2f3d26b611..25da4c8e40 100644 --- a/packages/frontend/core/src/desktop/pages/auth/confirm-change-email.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/confirm-change-email.tsx @@ -1,8 +1,9 @@ import { Button } from '@affine/component'; import { AuthPageContainer } from '@affine/component/auth-components'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; -import { BackendError, GraphQLService } from '@affine/core/modules/cloud'; -import { changeEmailMutation, ErrorNames } from '@affine/graphql'; +import { GraphQLService } from '@affine/core/modules/cloud'; +import { UserFriendlyError } from '@affine/error'; +import { changeEmailMutation } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { useService } from '@toeverything/infra'; import { type FC, useEffect, useState } from 'react'; @@ -33,11 +34,8 @@ export const ConfirmChangeEmail: FC<{ }, }) .catch(err => { - if (err instanceof BackendError) { - const userFriendlyError = err.originError; - if (userFriendlyError.name === ErrorNames.INVALID_EMAIL_TOKEN) { - return navigateHelper.jumpToExpired(); - } + if (UserFriendlyError.fromAny(err).is('INVALID_EMAIL_TOKEN')) { + return navigateHelper.jumpToExpired(); } throw err; }) diff --git a/packages/frontend/core/src/desktop/pages/auth/email-verified-email.tsx b/packages/frontend/core/src/desktop/pages/auth/email-verified-email.tsx index 4d7a53e8b6..cebc9514b6 100644 --- a/packages/frontend/core/src/desktop/pages/auth/email-verified-email.tsx +++ b/packages/frontend/core/src/desktop/pages/auth/email-verified-email.tsx @@ -2,11 +2,8 @@ import { Button } from '@affine/component'; import { AuthPageContainer } from '@affine/component/auth-components'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; import { GraphQLService } from '@affine/core/modules/cloud'; -import { - ErrorNames, - UserFriendlyError, - verifyEmailMutation, -} from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { verifyEmailMutation } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { useService } from '@toeverything/infra'; import { type FC, useEffect, useState } from 'react'; @@ -35,8 +32,8 @@ export const ConfirmVerifiedEmail: FC<{ }, }) .catch(error => { - const userFriendlyError = UserFriendlyError.fromAnyError(error); - if (userFriendlyError.name === ErrorNames.INVALID_EMAIL_TOKEN) { + const userFriendlyError = UserFriendlyError.fromAny(error); + if (userFriendlyError.is('INVALID_EMAIL_TOKEN')) { return navigateHelper.jumpToExpired(); } throw error; diff --git a/packages/frontend/core/src/desktop/pages/invite/index.tsx b/packages/frontend/core/src/desktop/pages/invite/index.tsx index 7064a2775b..2204f4acfb 100644 --- a/packages/frontend/core/src/desktop/pages/invite/index.tsx +++ b/packages/frontend/core/src/desktop/pages/invite/index.tsx @@ -3,7 +3,7 @@ import { ExpiredPage, JoinFailedPage, } from '@affine/component/member-components'; -import { ErrorNames, UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; import { useLiveData, useService } from '@toeverything/infra'; import { useCallback, useEffect } from 'react'; import { Navigate, useParams } from 'react-router-dom'; @@ -43,8 +43,8 @@ const AcceptInvite = ({ inviteId: targetInviteId }: { inviteId: string }) => { useEffect(() => { if (error && inviteId === targetInviteId) { - const err = UserFriendlyError.fromAnyError(error); - if (err.name === ErrorNames.ALREADY_IN_SPACE) { + const err = UserFriendlyError.fromAny(error); + if (err.is('ALREADY_IN_SPACE')) { return navigateHelper.jumpToIndex(); } } diff --git a/packages/frontend/core/src/desktop/pages/subscribe/index.tsx b/packages/frontend/core/src/desktop/pages/subscribe/index.tsx index 8a0008541e..9e2a621dca 100644 --- a/packages/frontend/core/src/desktop/pages/subscribe/index.tsx +++ b/packages/frontend/core/src/desktop/pages/subscribe/index.tsx @@ -1,4 +1,5 @@ import { Button, Loading } from '@affine/component'; +import { UserFriendlyError } from '@affine/error'; import { SubscriptionPlan, SubscriptionRecurring, @@ -16,11 +17,7 @@ import { RouteLogic, useNavigateHelper, } from '../../../components/hooks/use-navigate-helper'; -import { - AuthService, - BackendError, - SubscriptionService, -} from '../../../modules/cloud'; +import { AuthService, SubscriptionService } from '../../../modules/cloud'; import { container } from './subscribe.css'; interface ProductTriple { @@ -160,12 +157,8 @@ export const Component = () => { setMessage('Redirecting...'); location.href = checkout; } catch (err) { - if (err instanceof BackendError) { - setMessage(err.originError.message); - } else { - console.log(err); - setError('Something went wrong, please contact support.'); - } + const e = UserFriendlyError.fromAny(err); + setMessage(e.message); } }).pipe(mergeMap(() => EMPTY)); }) diff --git a/packages/frontend/core/src/modules/cloud/entities/cloud-doc-meta.ts b/packages/frontend/core/src/modules/cloud/entities/cloud-doc-meta.ts index 3be12ff3ec..7ebbb714cc 100644 --- a/packages/frontend/core/src/modules/cloud/entities/cloud-doc-meta.ts +++ b/packages/frontend/core/src/modules/cloud/entities/cloud-doc-meta.ts @@ -1,6 +1,5 @@ import type { GetWorkspacePageMetaByIdQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,12 +8,12 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, mergeMap } from 'rxjs'; import type { DocService } from '../../doc'; import type { GlobalCache } from '../../storage'; -import { isBackendError, isNetworkError } from '../error'; import type { CloudDocMetaStore } from '../stores/cloud-doc-meta'; export type CloudDocMetaType = @@ -47,13 +46,7 @@ export class CloudDocMeta extends Entity { return fromPromise( this.store.fetchCloudDocMeta(this.workspaceId, this.docId) ).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(meta => { this.cache.set(this.cacheKey, meta); return EMPTY; diff --git a/packages/frontend/core/src/modules/cloud/entities/invoices.ts b/packages/frontend/core/src/modules/cloud/entities/invoices.ts index 04d6b26c9b..5e462ea7f8 100644 --- a/packages/frontend/core/src/modules/cloud/entities/invoices.ts +++ b/packages/frontend/core/src/modules/cloud/entities/invoices.ts @@ -1,6 +1,5 @@ import type { InvoicesQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,10 +8,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, map, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { InvoicesStore } from '../stores/invoices'; export type Invoice = NonNullable< @@ -50,13 +49,7 @@ export class Invoices extends Entity { this.pageInvoices$.setValue(data.invoices); return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.pageInvoices$.setValue(undefined); diff --git a/packages/frontend/core/src/modules/cloud/entities/session.ts b/packages/frontend/core/src/modules/cloud/entities/session.ts index 701e67b6fd..9b3cad1ea5 100644 --- a/packages/frontend/core/src/modules/cloud/entities/session.ts +++ b/packages/frontend/core/src/modules/cloud/entities/session.ts @@ -1,3 +1,4 @@ +import { UserFriendlyError } from '@affine/error'; import { backoffRetry, effect, @@ -12,7 +13,6 @@ import { isEqual } from 'lodash-es'; import { EMPTY, mergeMap } from 'rxjs'; import { validateAndReduceImage } from '../../../utils/reduce-image'; -import { BackendError } from '../error'; import type { AccountProfile, AuthStore } from '../stores/auth'; export interface AuthSessionInfo { @@ -114,10 +114,7 @@ export class AuthSession extends Entity { return null; } } catch (e) { - if ( - e instanceof BackendError && - e.originError.name === 'UNSUPPORTED_CLIENT_VERSION' - ) { + if (UserFriendlyError.fromAny(e).is('UNSUPPORTED_CLIENT_VERSION')) { return null; } throw e; diff --git a/packages/frontend/core/src/modules/cloud/entities/subscription-prices.ts b/packages/frontend/core/src/modules/cloud/entities/subscription-prices.ts index ee2783a7fc..917d589fa4 100644 --- a/packages/frontend/core/src/modules/cloud/entities/subscription-prices.ts +++ b/packages/frontend/core/src/modules/cloud/entities/subscription-prices.ts @@ -1,6 +1,5 @@ import type { PricesQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,10 +8,10 @@ import { mapInto, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { exhaustMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { ServerService } from '../services/server'; import type { SubscriptionStore } from '../stores/subscription'; @@ -55,13 +54,7 @@ export class SubscriptionPrices extends Entity { } return this.store.fetchSubscriptionPrices(signal); }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mapInto(this.prices$), catchErrorInto(this.error$), onStart(() => this.isRevalidating$.next(true)), diff --git a/packages/frontend/core/src/modules/cloud/entities/subscription.ts b/packages/frontend/core/src/modules/cloud/entities/subscription.ts index f2cbf19a19..a62bda9c1d 100644 --- a/packages/frontend/core/src/modules/cloud/entities/subscription.ts +++ b/packages/frontend/core/src/modules/cloud/entities/subscription.ts @@ -5,7 +5,6 @@ import { SubscriptionVariant, } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -14,10 +13,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, map, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { AuthService } from '../services/auth'; import type { ServerService } from '../services/server'; import type { SubscriptionStore } from '../stores/subscription'; @@ -122,13 +121,7 @@ export class Subscription extends Entity { subscriptions: subscriptions, }; }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(data => { if (data) { this.store.setCachedSubscriptions( diff --git a/packages/frontend/core/src/modules/cloud/entities/user-copilot-quota.ts b/packages/frontend/core/src/modules/cloud/entities/user-copilot-quota.ts index 6f3509dca2..bf37b298aa 100644 --- a/packages/frontend/core/src/modules/cloud/entities/user-copilot-quota.ts +++ b/packages/frontend/core/src/modules/cloud/entities/user-copilot-quota.ts @@ -1,5 +1,4 @@ import { - backoffRetry, catchErrorInto, effect, Entity, @@ -8,10 +7,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, map, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { AuthService } from '../services/auth'; import type { ServerService } from '../services/server'; import type { UserCopilotQuotaStore } from '../stores/user-copilot-quota'; @@ -54,13 +53,7 @@ export class UserCopilotQuota extends Entity { return aiQuota; }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(data => { if (data) { const { limit, used } = data; diff --git a/packages/frontend/core/src/modules/cloud/entities/user-feature.ts b/packages/frontend/core/src/modules/cloud/entities/user-feature.ts index e3fdf30f8a..967ab4449a 100644 --- a/packages/frontend/core/src/modules/cloud/entities/user-feature.ts +++ b/packages/frontend/core/src/modules/cloud/entities/user-feature.ts @@ -1,6 +1,5 @@ import { FeatureType } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,10 +8,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, map, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { AuthService } from '../services/auth'; import type { UserFeatureStore } from '../stores/user-feature'; @@ -59,13 +58,7 @@ export class UserFeature extends Entity { features: features, }; }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(data => { if (data) { this.features$.next(data.features); diff --git a/packages/frontend/core/src/modules/cloud/entities/user-quota.ts b/packages/frontend/core/src/modules/cloud/entities/user-quota.ts index 265508d45d..b2031b7b50 100644 --- a/packages/frontend/core/src/modules/cloud/entities/user-quota.ts +++ b/packages/frontend/core/src/modules/cloud/entities/user-quota.ts @@ -1,6 +1,5 @@ import type { QuotaQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,12 +8,12 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { cssVar } from '@toeverything/theme'; import bytes from 'bytes'; import { EMPTY, map, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { AuthService } from '../services/auth'; import type { UserQuotaStore } from '../stores/user-quota'; @@ -79,13 +78,7 @@ export class UserQuota extends Entity { return { quota, used }; }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(data => { if (data) { const { quota, used } = data; diff --git a/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts b/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts index ef2b1eaac8..89a5d6d54e 100644 --- a/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts +++ b/packages/frontend/core/src/modules/cloud/entities/workspace-invoices.ts @@ -1,6 +1,5 @@ import type { InvoicesQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,11 +8,11 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, map, mergeMap } from 'rxjs'; import type { WorkspaceService } from '../../workspace'; -import { isBackendError, isNetworkError } from '../error'; import type { WorkspaceServerService } from '../services/workspace-server'; import { InvoicesStore } from '../stores/invoices'; @@ -61,13 +60,7 @@ export class WorkspaceInvoices extends Entity { this.pageInvoices$.setValue(data.invoices); return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.pageInvoices$.setValue(undefined); diff --git a/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts b/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts index a97640b98b..d946d01e54 100644 --- a/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts +++ b/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts @@ -1,7 +1,6 @@ import type { SubscriptionQuery, SubscriptionRecurring } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -10,11 +9,11 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, mergeMap } from 'rxjs'; import type { WorkspaceService } from '../../workspace'; -import { isBackendError, isNetworkError } from '../error'; import type { WorkspaceServerService } from '../services/workspace-server'; import { SubscriptionStore } from '../stores/subscription'; export type SubscriptionType = NonNullable< @@ -123,13 +122,7 @@ export class WorkspaceSubscription extends Entity { subscription: subscription, }; }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(data => { if (data && data.subscription && data.workspaceId && this.store) { this.store.setCachedWorkspaceSubscription( diff --git a/packages/frontend/core/src/modules/cloud/error.ts b/packages/frontend/core/src/modules/cloud/error.ts deleted file mode 100644 index cd15096215..0000000000 --- a/packages/frontend/core/src/modules/cloud/error.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { UserFriendlyError } from '@affine/graphql'; - -export class NetworkError extends Error { - constructor( - public readonly originError: Error, - public readonly status?: number - ) { - super(`Network error: ${originError.message}`); - this.stack = originError.stack; - } -} - -export function isNetworkError(error: Error): error is NetworkError { - return error instanceof NetworkError; -} - -export class BackendError extends Error { - get status() { - return this.originError.status; - } - - constructor(public readonly originError: UserFriendlyError) { - super(`Server error: ${originError.message}`); - this.stack = originError.stack; - } -} - -export function isBackendError(error: Error): error is BackendError { - return error instanceof BackendError; -} diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index 3c06452a6d..a2a11f5a6c 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -1,12 +1,6 @@ export type { Invoice } from './entities/invoices'; export { Server } from './entities/server'; export type { AuthAccountInfo } from './entities/session'; -export { - BackendError, - isBackendError, - isNetworkError, - NetworkError, -} from './error'; export { AccountChanged } from './events/account-changed'; export { AccountLoggedIn } from './events/account-logged-in'; export { AccountLoggedOut } from './events/account-logged-out'; diff --git a/packages/frontend/core/src/modules/cloud/services/accept-invite.ts b/packages/frontend/core/src/modules/cloud/services/accept-invite.ts index 66013bc1ff..07b3ea9601 100644 --- a/packages/frontend/core/src/modules/cloud/services/accept-invite.ts +++ b/packages/frontend/core/src/modules/cloud/services/accept-invite.ts @@ -1,6 +1,5 @@ import type { GetInviteInfoQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, fromPromise, @@ -8,10 +7,10 @@ import { onComplete, onStart, Service, + smartRetry, } from '@toeverything/infra'; import { EMPTY, exhaustMap, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { AcceptInviteStore } from '../stores/accept-invite'; import type { InviteInfoStore } from '../stores/invite-info'; @@ -52,14 +51,7 @@ export class AcceptInviteService extends Service { this.accepted$.next(res); return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - count: 3, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.inviteId$.setValue(inviteId); diff --git a/packages/frontend/core/src/modules/cloud/services/auth.ts b/packages/frontend/core/src/modules/cloud/services/auth.ts index b59ba2c580..c752627c6f 100644 --- a/packages/frontend/core/src/modules/cloud/services/auth.ts +++ b/packages/frontend/core/src/modules/cloud/services/auth.ts @@ -1,4 +1,5 @@ import { AIProvider } from '@affine/core/blocksuite/ai'; +import { UserFriendlyError } from '@affine/error'; import type { OAuthProviderType } from '@affine/graphql'; import { track } from '@affine/track'; import { OnEvent, Service } from '@toeverything/infra'; @@ -7,7 +8,6 @@ import { distinctUntilChanged, map, skip } from 'rxjs'; import { ApplicationFocused } from '../../lifecycle'; import type { UrlService } from '../../url'; import { type AuthAccountInfo, AuthSession } from '../entities/session'; -import { BackendError } from '../error'; import { AccountChanged } from '../events/account-changed'; import { AccountLoggedIn } from '../events/account-logged-in'; import { AccountLoggedOut } from '../events/account-logged-out'; @@ -103,7 +103,7 @@ export class AuthService extends Service { } catch (e) { track.$.$.auth.signInFail({ method: 'magic-link', - reason: e instanceof BackendError ? e.originError.name : 'unknown', + reason: UserFriendlyError.fromAny(e).name, }); throw e; } @@ -119,7 +119,7 @@ export class AuthService extends Service { } catch (e) { track.$.$.auth.signInFail({ method, - reason: e instanceof BackendError ? e.originError.name : 'unknown', + reason: UserFriendlyError.fromAny(e).name, }); throw e; } @@ -159,7 +159,7 @@ export class AuthService extends Service { track.$.$.auth.signInFail({ method: 'oauth', provider, - reason: e instanceof BackendError ? e.originError.name : 'unknown', + reason: UserFriendlyError.fromAny(e).name, }); throw e; } @@ -181,7 +181,7 @@ export class AuthService extends Service { track.$.$.auth.signInFail({ method: 'oauth', provider, - reason: e instanceof BackendError ? e.originError.name : 'unknown', + reason: UserFriendlyError.fromAny(e).name, }); throw e; } @@ -201,7 +201,7 @@ export class AuthService extends Service { } catch (e) { track.$.$.auth.signInFail({ method: 'password', - reason: e instanceof BackendError ? e.originError.name : 'unknown', + reason: UserFriendlyError.fromAny(e).name, }); throw e; } diff --git a/packages/frontend/core/src/modules/cloud/services/fetch.ts b/packages/frontend/core/src/modules/cloud/services/fetch.ts index 1b4dea04a9..7310919f18 100644 --- a/packages/frontend/core/src/modules/cloud/services/fetch.ts +++ b/packages/frontend/core/src/modules/cloud/services/fetch.ts @@ -1,8 +1,7 @@ import { DebugLogger } from '@affine/debug'; -import { UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; import { fromPromise, Service } from '@toeverything/infra'; -import { BackendError, NetworkError } from '../error'; import type { ServerService } from './server'; const logger = new DebugLogger('affine:fetch'); @@ -46,40 +45,54 @@ export class FetchService extends Service { abortController.abort('timeout'); }, timeout); - const res = await globalThis - .fetch(new URL(input, this.serverService.server.serverMetadata.baseUrl), { - ...init, - signal: abortController.signal, - headers: { - ...init?.headers, - 'x-affine-version': BUILD_CONFIG.appVersion, - }, - }) - .catch(err => { - logger.debug('network error', err); - throw new NetworkError(err); - }); - clearTimeout(timeoutId); - if (res.status === 504) { - const error = new Error('Gateway Timeout'); - logger.debug('network error', error); - throw new NetworkError(error, res.status); - } - if (!res.ok) { - logger.warn( - 'backend error', - new Error(`${res.status} ${res.statusText}`) + let res: Response; + + try { + res = await globalThis.fetch( + new URL(input, this.serverService.server.serverMetadata.baseUrl), + { + ...init, + signal: abortController.signal, + headers: { + ...init?.headers, + 'x-affine-version': BUILD_CONFIG.appVersion, + }, + } ); - let reason: string | any = ''; - if (res.headers.get('Content-Type')?.includes('application/json')) { - try { - reason = await res.json(); - } catch { - // ignore + } catch (err: any) { + throw new UserFriendlyError({ + status: 504, + code: 'NETWORK_ERROR', + type: 'NETWORK_ERROR', + name: 'NETWORK_ERROR', + message: `Network error: ${err.message}`, + stacktrace: err.stack, + }); + } finally { + clearTimeout(timeoutId); + } + + if (!res.ok) { + if (res.status === 504) { + const error = new Error('Gateway Timeout'); + logger.debug('network error', error); + throw new UserFriendlyError({ + status: 504, + code: 'NETWORK_ERROR', + type: 'NETWORK_ERROR', + name: 'NETWORK_ERROR', + message: 'Gateway Timeout', + stacktrace: error.stack, + }); + } else { + if (res.headers.get('Content-Type') === 'application/json') { + throw UserFriendlyError.fromAny(await res.json()); + } else { + throw UserFriendlyError.fromAny(await res.text()); } } - throw new BackendError(UserFriendlyError.fromAnyError(reason)); } + return res; }; } diff --git a/packages/frontend/core/src/modules/cloud/services/graphql.ts b/packages/frontend/core/src/modules/cloud/services/graphql.ts index 9ffcea9c80..c6ecbc5fab 100644 --- a/packages/frontend/core/src/modules/cloud/services/graphql.ts +++ b/packages/frontend/core/src/modules/cloud/services/graphql.ts @@ -1,15 +1,13 @@ +import { UserFriendlyError } from '@affine/error'; import { gqlFetcherFactory, - GraphQLError, type GraphQLQuery, type QueryOptions, type QueryResponse, - UserFriendlyError, } from '@affine/graphql'; import { fromPromise, Service } from '@toeverything/infra'; import type { Observable } from 'rxjs'; -import { BackendError } from '../error'; import { AuthService } from './auth'; import type { FetchService } from './fetch'; @@ -40,16 +38,9 @@ export class GraphQLService extends Service { try { return await this.rawGql(options); } catch (anyError) { - let error = anyError; + const error = UserFriendlyError.fromAny(anyError); - // NOTE(@forehalo): - // GraphQL error is not present by non-200 status code, but by responding `errors` fields in the body - // So it will never be `BackendError` originally. - if (anyError instanceof GraphQLError) { - error = new BackendError(UserFriendlyError.fromAnyError(anyError)); - } - - if (error instanceof BackendError && error.status === 403) { + if (error.isStatus(401)) { this.framework.get(AuthService).session.revalidate(); } diff --git a/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts b/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts index c2e8644ad9..2dca1567ed 100644 --- a/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts +++ b/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts @@ -1,16 +1,16 @@ -import { UserFriendlyError } from '@affine/graphql'; +import { type UserFriendlyError } from '@affine/error'; import { - backoffRetry, + catchErrorInto, effect, fromPromise, LiveData, onComplete, onStart, Service, + smartRetry, } from '@toeverything/infra'; -import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs'; +import { EMPTY, exhaustMap, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../error'; import type { SelfhostGenerateLicenseStore } from '../stores/selfhost-generate-license'; export class SelfhostGenerateLicenseService extends Service { @@ -26,22 +26,12 @@ export class SelfhostGenerateLicenseService extends Service { return fromPromise(async () => { return await this.store.generateKey(sessionId); }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(key => { this.licenseKey$.next(key); return EMPTY; }), - catchError(err => { - this.error$.next(UserFriendlyError.fromAnyError(err)); - console.error(err); - return EMPTY; - }), + catchErrorInto(this.error$), onStart(() => { this.isLoading$.next(true); }), diff --git a/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts b/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts index afc9d22eff..23108722b2 100644 --- a/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts +++ b/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts @@ -1,6 +1,5 @@ import type { License } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, exhaustMapWithTrailing, @@ -9,11 +8,11 @@ import { onComplete, onStart, Service, + smartRetry, } from '@toeverything/infra'; import { EMPTY, mergeMap } from 'rxjs'; import type { WorkspaceService } from '../../workspace'; -import { isBackendError, isNetworkError } from '../error'; import type { SelfhostLicenseStore } from '../stores/selfhost-license'; export class SelfhostLicenseService extends Service { @@ -36,13 +35,7 @@ export class SelfhostLicenseService extends Service { } return await this.store.getLicense(currentWorkspaceId, signal); }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(data => { if (data) { this.license$.next(data); diff --git a/packages/frontend/core/src/modules/import-template/entities/downloader.ts b/packages/frontend/core/src/modules/import-template/entities/downloader.ts index ef3250beee..63a10bac35 100644 --- a/packages/frontend/core/src/modules/import-template/entities/downloader.ts +++ b/packages/frontend/core/src/modules/import-template/entities/downloader.ts @@ -1,5 +1,4 @@ import { - backoffRetry, catchErrorInto, effect, Entity, @@ -7,10 +6,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, mergeMap, switchMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { TemplateDownloaderStore } from '../store/downloader'; export class TemplateDownloader extends Entity { @@ -29,13 +28,7 @@ export class TemplateDownloader extends Entity { this.data$.next(data); return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.isDownloading$.next(true); diff --git a/packages/frontend/core/src/modules/permissions/entities/members.ts b/packages/frontend/core/src/modules/permissions/entities/members.ts index 4b810a74e9..3287c67633 100644 --- a/packages/frontend/core/src/modules/permissions/entities/members.ts +++ b/packages/frontend/core/src/modules/permissions/entities/members.ts @@ -1,6 +1,5 @@ import type { GetMembersByWorkspaceIdQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -8,10 +7,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, map, mergeMap, switchMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { WorkspaceService } from '../../workspace'; import type { WorkspaceMembersStore } from '../stores/members'; @@ -51,13 +50,7 @@ export class WorkspaceMembers extends Entity { this.pageMembers$.setValue(data.members); return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.pageMembers$.setValue(undefined); diff --git a/packages/frontend/core/src/modules/permissions/services/doc-granted-users.ts b/packages/frontend/core/src/modules/permissions/services/doc-granted-users.ts index 5de0b766de..afef6bc86c 100644 --- a/packages/frontend/core/src/modules/permissions/services/doc-granted-users.ts +++ b/packages/frontend/core/src/modules/permissions/services/doc-granted-users.ts @@ -1,6 +1,5 @@ import { DocRole, type GetPageGrantedUsersListQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, fromPromise, @@ -8,10 +7,10 @@ import { onComplete, onStart, Service, + smartRetry, } from '@toeverything/infra'; import { EMPTY, exhaustMap, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { DocService } from '../../doc'; import type { WorkspaceService } from '../../workspace'; import type { DocGrantedUsersStore } from '../stores/doc-granted-users'; @@ -65,13 +64,7 @@ export class DocGrantedUsersService extends Service { return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.isLoading$.setValue(true); diff --git a/packages/frontend/core/src/modules/permissions/services/member-search.ts b/packages/frontend/core/src/modules/permissions/services/member-search.ts index 0ea530b23f..f743667d84 100644 --- a/packages/frontend/core/src/modules/permissions/services/member-search.ts +++ b/packages/frontend/core/src/modules/permissions/services/member-search.ts @@ -1,5 +1,4 @@ import { - backoffRetry, catchErrorInto, effect, fromPromise, @@ -7,10 +6,10 @@ import { onComplete, onStart, Service, + smartRetry, } from '@toeverything/infra'; import { EMPTY, exhaustMap, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { WorkspaceService } from '../../workspace'; import type { Member } from '../entities/members'; import type { MemberSearchStore } from '../stores/member-search'; @@ -50,13 +49,7 @@ export class MemberSearchService extends Service { return EMPTY; }), - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), catchErrorInto(this.error$), onStart(() => { this.isLoading$.setValue(true); diff --git a/packages/frontend/core/src/modules/quota/entities/quota.ts b/packages/frontend/core/src/modules/quota/entities/quota.ts index e220a5c372..8009fd942f 100644 --- a/packages/frontend/core/src/modules/quota/entities/quota.ts +++ b/packages/frontend/core/src/modules/quota/entities/quota.ts @@ -1,7 +1,6 @@ import { DebugLogger } from '@affine/debug'; import type { WorkspaceQuotaQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -10,12 +9,12 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { cssVarV2 } from '@toeverything/theme/v2'; import bytes from 'bytes'; import { EMPTY, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { WorkspaceService } from '../../workspace'; import type { WorkspaceQuotaStore } from '../stores/quota'; @@ -76,14 +75,7 @@ export class WorkspaceQuota extends Entity { ); return { quota: data, used: data.usedStorageQuota }; }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - count: 3, - }), + smartRetry(), mergeMap(data => { if (data) { const { quota, used } = data; diff --git a/packages/frontend/core/src/modules/share-doc/entities/share-docs-list.ts b/packages/frontend/core/src/modules/share-doc/entities/share-docs-list.ts index e7e281a7dd..8af65ac946 100644 --- a/packages/frontend/core/src/modules/share-doc/entities/share-docs-list.ts +++ b/packages/frontend/core/src/modules/share-doc/entities/share-docs-list.ts @@ -1,7 +1,6 @@ import { DebugLogger } from '@affine/debug'; import type { GetWorkspacePublicPagesQuery } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -10,10 +9,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { GlobalCache } from '../../storage'; import type { WorkspaceService } from '../../workspace'; import type { ShareDocsStore } from '../stores/share-docs'; @@ -43,13 +42,7 @@ export class ShareDocsList extends Entity { signal ); }).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mergeMap(list => { this.cache.set('share-docs', list); return EMPTY; diff --git a/packages/frontend/core/src/modules/share-doc/entities/share-info.ts b/packages/frontend/core/src/modules/share-doc/entities/share-info.ts index b86237f704..1fbbd8ccda 100644 --- a/packages/frontend/core/src/modules/share-doc/entities/share-info.ts +++ b/packages/frontend/core/src/modules/share-doc/entities/share-info.ts @@ -1,6 +1,5 @@ import type { GetWorkspacePageByIdQuery, PublicDocMode } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,10 +8,10 @@ import { mapInto, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { switchMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { DocService } from '../../doc'; import type { WorkspaceService } from '../../workspace'; import type { ShareStore } from '../stores/share'; @@ -44,13 +43,7 @@ export class ShareInfo extends Entity { signal ) ).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - }), + smartRetry(), mapInto(this.info$), catchErrorInto(this.error$), onStart(() => this.isRevalidating$.next(true)), diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx index ef6e0989d6..b313e8a7d9 100644 --- a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx +++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx @@ -8,7 +8,8 @@ import { import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { DocGrantedUsersService } from '@affine/core/modules/permissions'; import { ShareInfoService } from '@affine/core/modules/share-doc'; -import { DocRole, UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { DocRole } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { InformationIcon } from '@blocksuite/icons/rc'; @@ -62,7 +63,7 @@ export const MembersPermission = ({ await docGrantedUsersService.updateDocDefaultRole(docRole); shareInfoService.shareInfo.revalidate(); } catch (error) { - const err = UserFriendlyError.fromAnyError(error); + const err = UserFriendlyError.fromAny(error); notify.error({ title: err.name, message: err.message, diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx index a6260e8c40..ced8ca43e3 100644 --- a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx +++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/public-page-button.tsx @@ -1,7 +1,8 @@ import { Menu, MenuItem, MenuTrigger, notify } from '@affine/component'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { ShareInfoService } from '@affine/core/modules/share-doc'; -import { PublicDocMode, UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { PublicDocMode } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import track from '@affine/track'; import { @@ -74,7 +75,7 @@ export const PublicDoc = ({ disabled }: { disabled?: boolean }) => { icon: , }); } catch (error) { - const err = UserFriendlyError.fromAnyError(error); + const err = UserFriendlyError.fromAny(error); notify.error({ title: err.name, message: err.message, diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx index 08c69c75fb..603dabf20b 100644 --- a/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx +++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/invite-member-editor/invite-member-editor.tsx @@ -14,11 +14,8 @@ import { type Member, MemberSearchService, } from '@affine/core/modules/permissions'; -import { - DocRole, - UserFriendlyError, - WorkspaceMemberStatus, -} from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { DocRole, WorkspaceMemberStatus } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { ArrowLeftBigIcon } from '@blocksuite/icons/rc'; @@ -113,7 +110,7 @@ export const InviteMemberEditor = ({ title: t['Invitation sent'](), }); } catch (error) { - const err = UserFriendlyError.fromAnyError(error); + const err = UserFriendlyError.fromAny(error); notify.error({ title: t[`error.${err.name}`](err.data), }); diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx index 807f6ecb89..273a34840f 100644 --- a/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx +++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/member-management/member-item.tsx @@ -16,7 +16,8 @@ import { GuardService, WorkspacePermissionService, } from '@affine/core/modules/permissions'; -import { DocRole, UserFriendlyError } from '@affine/graphql'; +import { UserFriendlyError } from '@affine/error'; +import { DocRole } from '@affine/graphql'; import { useI18n } from '@affine/i18n'; import track from '@affine/track'; import { useLiveData, useService } from '@toeverything/infra'; @@ -161,7 +162,7 @@ const Options = ({ }); } } catch (error) { - const err = UserFriendlyError.fromAnyError(error); + const err = UserFriendlyError.fromAny(error); notify.error({ title: t[`error.${err.name}`](err.data), }); @@ -219,7 +220,7 @@ const Options = ({ await docGrantedUsersService.revokeUsersRole(userId); docGrantedUsersService.loadMore(); } catch (error) { - const err = UserFriendlyError.fromAnyError(error); + const err = UserFriendlyError.fromAny(error); notify.error({ title: t[`error.${err.name}`](err.data), }); diff --git a/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts b/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts index e837f676b7..c04a560f83 100644 --- a/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts +++ b/packages/frontend/core/src/modules/share-setting/entities/share-setting.ts @@ -1,7 +1,6 @@ import { DebugLogger } from '@affine/debug'; import type { GetWorkspaceConfigQuery, InviteLink } from '@affine/graphql'; import { - backoffRetry, catchErrorInto, effect, Entity, @@ -9,10 +8,10 @@ import { LiveData, onComplete, onStart, + smartRetry, } from '@toeverything/infra'; import { EMPTY, exhaustMap, mergeMap } from 'rxjs'; -import { isBackendError, isNetworkError } from '../../cloud'; import type { WorkspaceService } from '../../workspace'; import type { WorkspaceShareSettingStore } from '../stores/share-setting'; @@ -45,14 +44,7 @@ export class WorkspaceShareSetting extends Entity { signal ) ).pipe( - backoffRetry({ - when: isNetworkError, - count: Infinity, - }), - backoffRetry({ - when: isBackendError, - count: 3, - }), + smartRetry(), mergeMap(value => { if (value) { this.enableAi$.next(value.enableAi); diff --git a/packages/frontend/core/tsconfig.json b/packages/frontend/core/tsconfig.json index d3e9b2d732..d8e547789c 100644 --- a/packages/frontend/core/tsconfig.json +++ b/packages/frontend/core/tsconfig.json @@ -11,6 +11,7 @@ { "path": "../../common/debug" }, { "path": "../electron-api" }, { "path": "../../common/env" }, + { "path": "../../common/error" }, { "path": "../graphql" }, { "path": "../i18n" }, { "path": "../../common/nbstore" }, diff --git a/packages/frontend/graphql/package.json b/packages/frontend/graphql/package.json index 3a8b9f268c..22e3e846d4 100644 --- a/packages/frontend/graphql/package.json +++ b/packages/frontend/graphql/package.json @@ -26,6 +26,7 @@ "dependencies": { "@affine/debug": "workspace:*", "@affine/env": "workspace:*", + "@affine/error": "workspace:*", "graphql": "^16.9.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21" diff --git a/packages/frontend/graphql/src/error.ts b/packages/frontend/graphql/src/error.ts deleted file mode 100644 index 35adc9dc46..0000000000 --- a/packages/frontend/graphql/src/error.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { GraphQLError as BaseGraphQLError } from 'graphql'; - -import { type ErrorDataUnion, ErrorNames } from './schema'; - -export interface UserFriendlyErrorResponse { - status: number; - code: string; - type: string; - name: ErrorNames; - message: string; - data?: any; - stacktrace?: string; -} - -export class UserFriendlyError - extends Error - implements UserFriendlyErrorResponse -{ - readonly status = this.response.status; - readonly code = this.response.code; - readonly type = this.response.type; - readonly rawName = this.response.name; - override readonly message = this.response.message; - readonly data = this.response.data; - readonly stacktrace = this.response.stacktrace; - - override get name() { - if (this.rawName in ErrorNames) { - return this.rawName; - } - return ErrorNames.INTERNAL_SERVER_ERROR; - } - - static fromAnyError(response: any) { - if (response instanceof GraphQLError) { - return new UserFriendlyError(response.extensions); - } - - if ( - 'originError' in response && - response.originError instanceof UserFriendlyError - ) { - return response.originError as UserFriendlyError; - } - - if ( - response && - typeof response === 'object' && - response.type && - response.name - ) { - return new UserFriendlyError(response); - } - - return new UserFriendlyError({ - status: 500, - code: 'INTERNAL_SERVER_ERROR', - type: 'INTERNAL_SERVER_ERROR', - name: ErrorNames.INTERNAL_SERVER_ERROR, - message: 'Internal server error', - }); - } - - constructor(private readonly response: UserFriendlyErrorResponse) { - super(response.message); - } -} - -export class GraphQLError extends BaseGraphQLError { - // @ts-expect-error better to be a known type without any type casting - override extensions!: UserFriendlyErrorResponse; -} - -type ToPascalCase = S extends `${infer A}_${infer B}` - ? `${Capitalize>}${ToPascalCase}` - : Capitalize>; - -export type ErrorData = { - [K in ErrorNames]: Extract< - ErrorDataUnion, - { __typename?: `${ToPascalCase}DataType` } - >; -}; diff --git a/packages/frontend/graphql/src/fetcher.ts b/packages/frontend/graphql/src/fetcher.ts index 1152fa6b44..d90cfd01d7 100644 --- a/packages/frontend/graphql/src/fetcher.ts +++ b/packages/frontend/graphql/src/fetcher.ts @@ -1,8 +1,8 @@ import { DebugLogger } from '@affine/debug'; +import { GraphQLError } from '@affine/error'; import type { ExecutionResult } from 'graphql'; import { isNil, isObject, merge } from 'lodash-es'; -import { GraphQLError } from './error'; import type { GraphQLQuery } from './graphql'; import type { Mutations, Queries } from './schema'; diff --git a/packages/frontend/graphql/src/index.ts b/packages/frontend/graphql/src/index.ts index 95546b71bc..2231ca4669 100644 --- a/packages/frontend/graphql/src/index.ts +++ b/packages/frontend/graphql/src/index.ts @@ -1,4 +1,3 @@ -export * from './error'; export * from './fetcher'; export * from './graphql'; export * from './schema'; diff --git a/packages/frontend/graphql/tsconfig.json b/packages/frontend/graphql/tsconfig.json index 3719aa4273..98fa9f08b9 100644 --- a/packages/frontend/graphql/tsconfig.json +++ b/packages/frontend/graphql/tsconfig.json @@ -8,6 +8,7 @@ }, "references": [ { "path": "../../common/debug" }, - { "path": "../../common/env" } + { "path": "../../common/env" }, + { "path": "../../common/error" } ] } diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 0f0b390577..6895d60983 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7067,6 +7067,10 @@ export function useAFFiNEI18N(): { * `An internal error occurred.` */ ["error.INTERNAL_SERVER_ERROR"](): string; + /** + * `Network error.` + */ + ["error.NETWORK_ERROR"](): string; /** * `Too many requests.` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 19d5f3569b..813bcfdc0c 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1758,6 +1758,7 @@ "com.affine.fail-to-join-workspace.description-1": "Unable to join <1/> <2>{{workspaceName}} due to insufficient seats available.", "com.affine.fail-to-join-workspace.description-2": "Please contact your workspace owner to add more seats.", "error.INTERNAL_SERVER_ERROR": "An internal error occurred.", + "error.NETWORK_ERROR": "Network error.", "error.TOO_MANY_REQUEST": "Too many requests.", "error.NOT_FOUND": "Resource not found.", "error.BAD_REQUEST": "Bad request.", diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 8d95030f28..e16cd6e209 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -599,12 +599,18 @@ export const PackageList = [ name: '@affine/env', workspaceDependencies: [], }, + { + location: 'packages/common/error', + name: '@affine/error', + workspaceDependencies: [], + }, { location: 'packages/common/infra', name: '@toeverything/infra', workspaceDependencies: [ 'packages/common/debug', 'packages/common/env', + 'packages/common/error', 'packages/frontend/templates', ], }, @@ -704,6 +710,7 @@ export const PackageList = [ workspaceDependencies: [ 'packages/common/debug', 'packages/frontend/electron-api', + 'packages/common/error', 'packages/frontend/graphql', 'packages/frontend/i18n', 'tools/utils', @@ -718,6 +725,7 @@ export const PackageList = [ 'packages/common/debug', 'packages/frontend/electron-api', 'packages/common/env', + 'packages/common/error', 'packages/frontend/graphql', 'packages/frontend/i18n', 'packages/common/nbstore', @@ -735,7 +743,11 @@ export const PackageList = [ { location: 'packages/frontend/graphql', name: '@affine/graphql', - workspaceDependencies: ['packages/common/debug', 'packages/common/env'], + workspaceDependencies: [ + 'packages/common/debug', + 'packages/common/env', + 'packages/common/error', + ], }, { location: 'packages/frontend/i18n', @@ -890,6 +902,7 @@ export type PackageName = | '@affine/server' | '@affine/debug' | '@affine/env' + | '@affine/error' | '@toeverything/infra' | '@affine/nbstore' | '@affine/admin' diff --git a/tsconfig.json b/tsconfig.json index b41a8eeca7..338813c479 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -95,6 +95,7 @@ { "path": "./packages/backend/server" }, { "path": "./packages/common/debug" }, { "path": "./packages/common/env" }, + { "path": "./packages/common/error" }, { "path": "./packages/common/infra" }, { "path": "./packages/common/nbstore" }, { "path": "./packages/frontend/admin" }, diff --git a/yarn.lock b/yarn.lock index 106ccfe017..9268e0edac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -277,6 +277,7 @@ __metadata: "@affine-tools/utils": "workspace:*" "@affine/debug": "workspace:*" "@affine/electron-api": "workspace:*" + "@affine/error": "workspace:*" "@affine/graphql": "workspace:*" "@affine/i18n": "workspace:*" "@atlaskit/pragmatic-drag-and-drop": "npm:^1.4.0" @@ -363,6 +364,7 @@ __metadata: "@affine/debug": "workspace:*" "@affine/electron-api": "workspace:*" "@affine/env": "workspace:*" + "@affine/error": "workspace:*" "@affine/graphql": "workspace:*" "@affine/i18n": "workspace:*" "@affine/nbstore": "workspace:*" @@ -567,12 +569,24 @@ __metadata: languageName: unknown linkType: soft +"@affine/error@workspace:*, @affine/error@workspace:packages/common/error": + version: 0.0.0-use.local + resolution: "@affine/error@workspace:packages/common/error" + dependencies: + vitest: "npm:^3.0.7" + peerDependencies: + "@affine/graphql": "workspace:*" + graphql: ^16.9.0 + languageName: unknown + linkType: soft + "@affine/graphql@workspace:*, @affine/graphql@workspace:packages/frontend/graphql": version: 0.0.0-use.local resolution: "@affine/graphql@workspace:packages/frontend/graphql" dependencies: "@affine/debug": "workspace:*" "@affine/env": "workspace:*" + "@affine/error": "workspace:*" "@graphql-codegen/add": "npm:^5.0.3" "@graphql-codegen/cli": "npm:5.0.5" "@graphql-codegen/typescript": "npm:^4.1.2" @@ -13479,6 +13493,7 @@ __metadata: dependencies: "@affine/debug": "workspace:*" "@affine/env": "workspace:*" + "@affine/error": "workspace:*" "@affine/templates": "workspace:*" "@datastructures-js/binary-search-tree": "npm:^5.3.2" "@emotion/react": "npm:^11.14.0" @@ -32883,7 +32898,7 @@ __metadata: languageName: node linkType: hard -"vitest@npm:3.0.7, vitest@npm:^3.0.0": +"vitest@npm:3.0.7, vitest@npm:^3.0.0, vitest@npm:^3.0.7": version: 3.0.7 resolution: "vitest@npm:3.0.7" dependencies: