refactor(core): initial multiple servers infra (#8745)

This is the initial refactoring of affine to support multiple servers, but many more changes are needed to make multi-server actually work.
This commit is contained in:
EYHN
2024-11-27 06:44:46 +00:00
parent 3f4cb5be40
commit 6b4a1aa917
80 changed files with 1141 additions and 519 deletions

View File

@@ -10,6 +10,7 @@ import type {
DocStorage, DocStorage,
} from '../../../sync'; } from '../../../sync';
import type { WorkspaceProfileInfo } from '../entities/profile'; import type { WorkspaceProfileInfo } from '../entities/profile';
import type { Workspace } from '../entities/workspace';
import type { WorkspaceMetadata } from '../metadata'; import type { WorkspaceMetadata } from '../metadata';
export interface WorkspaceEngineProvider { export interface WorkspaceEngineProvider {
@@ -55,6 +56,8 @@ export interface WorkspaceFlavourProvider {
getWorkspaceBlob(id: string, blob: string): Promise<Blob | null>; getWorkspaceBlob(id: string, blob: string): Promise<Blob | null>;
getEngineProvider(workspaceId: string): WorkspaceEngineProvider; getEngineProvider(workspaceId: string): WorkspaceEngineProvider;
onWorkspaceInitialized?(workspace: Workspace): void;
} }
export const WorkspaceFlavourProvider = export const WorkspaceFlavourProvider =

View File

@@ -83,11 +83,12 @@ export class WorkspaceRepositoryService extends Service {
logger.info( logger.info(
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} ` `open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
); );
const flavourProvider = this.providers.find(
p => p.flavour === openOptions.metadata.flavour
);
const provider = const provider =
customProvider ?? customProvider ??
this.providers flavourProvider?.getEngineProvider(openOptions.metadata.id);
.find(p => p.flavour === openOptions.metadata.flavour)
?.getEngineProvider(openOptions.metadata.id);
if (!provider) { if (!provider) {
throw new Error( throw new Error(
`Unknown workspace flavour: ${openOptions.metadata.flavour}` `Unknown workspace flavour: ${openOptions.metadata.flavour}`
@@ -106,6 +107,8 @@ export class WorkspaceRepositoryService extends Service {
this.framework.emitEvent(WorkspaceInitialized, workspace); this.framework.emitEvent(WorkspaceInitialized, workspace);
flavourProvider?.onWorkspaceInitialized?.(workspace);
this.profileRepo this.profileRepo
.getProfile(openOptions.metadata) .getProfile(openOptions.metadata)
.syncWithWorkspace(workspace); .syncWithWorkspace(workspace);

View File

@@ -3,7 +3,7 @@
* *
* for support arraybuffer response type * for support arraybuffer response type
*/ */
import { FetchProvider } from '@affine/core/modules/cloud/provider/fetch'; import { RawFetchProvider } from '@affine/core/modules/cloud/provider/fetch';
import { CapacitorHttp } from '@capacitor/core'; import { CapacitorHttp } from '@capacitor/core';
import type { Framework } from '@toeverything/infra'; import type { Framework } from '@toeverything/infra';
@@ -121,7 +121,7 @@ function base64ToUint8Array(base64: string) {
return new Uint8Array(binaryArray); return new Uint8Array(binaryArray);
} }
export function configureFetchProvider(framework: Framework) { export function configureFetchProvider(framework: Framework) {
framework.override(FetchProvider, { framework.override(RawFetchProvider, {
fetch: async (input, init) => { fetch: async (input, init) => {
const request = new Request(input, init); const request = new Request(input, init);
const { method } = request; const { method } = request;

View File

@@ -1,22 +0,0 @@
import { useI18n } from '@affine/i18n';
import type { FC } from 'react';
import { Button } from '../../ui/button';
import { AuthPageContainer } from './auth-page-container';
export const ConfirmChangeEmail: FC<{
onOpenAffine: () => void;
}> = ({ onOpenAffine }) => {
const t = useI18n();
return (
<AuthPageContainer
title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
);
};

View File

@@ -1,22 +0,0 @@
import { useI18n } from '@affine/i18n';
import type { FC } from 'react';
import { Button } from '../../ui/button';
import { AuthPageContainer } from './auth-page-container';
export const ConfirmVerifiedEmail: FC<{
onOpenAffine: () => void;
}> = ({ onOpenAffine }) => {
const t = useI18n();
return (
<AuthPageContainer
title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
);
};

View File

@@ -4,9 +4,7 @@ export * from './auth-page-container';
export * from './back-button'; export * from './back-button';
export * from './change-email-page'; export * from './change-email-page';
export * from './change-password-page'; export * from './change-password-page';
export * from './confirm-change-email';
export * from './count-down-render'; export * from './count-down-render';
export * from './email-verified-email';
export * from './modal'; export * from './modal';
export * from './modal-header'; export * from './modal-header';
export * from './onboarding-page'; export * from './onboarding-page';

View File

@@ -1,5 +1,6 @@
import { Skeleton } from '@affine/component'; import { Skeleton } from '@affine/component';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { ServerService } from '@affine/core/modules/cloud';
import { UrlService } from '@affine/core/modules/url'; import { UrlService } from '@affine/core/modules/url';
import { OAuthProviderType } from '@affine/graphql'; import { OAuthProviderType } from '@affine/graphql';
import track from '@affine/track'; import track from '@affine/track';
@@ -7,8 +8,6 @@ import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { type ReactElement, useCallback } from 'react'; import { type ReactElement, useCallback } from 'react';
import { ServerConfigService } from '../../../modules/cloud';
const OAuthProviderMap: Record< const OAuthProviderMap: Record<
OAuthProviderType, OAuthProviderType,
{ {
@@ -30,11 +29,11 @@ const OAuthProviderMap: Record<
}; };
export function OAuth({ redirectUrl }: { redirectUrl?: string }) { export function OAuth({ redirectUrl }: { redirectUrl?: string }) {
const serverConfig = useService(ServerConfigService).serverConfig; const serverService = useService(ServerService);
const urlService = useService(UrlService); const urlService = useService(UrlService);
const oauth = useLiveData(serverConfig.features$.map(r => r?.oauth)); const oauth = useLiveData(serverService.server.features$.map(r => r?.oauth));
const oauthProviders = useLiveData( const oauthProviders = useLiveData(
serverConfig.config$.map(r => r?.oauthProviders) serverService.server.config$.map(r => r?.oauthProviders)
); );
const scheme = urlService.getClientScheme(); const scheme = urlService.getClientScheme();
@@ -66,6 +65,7 @@ function OAuthProvider({
scheme?: string; scheme?: string;
popupWindow: (url: string) => void; popupWindow: (url: string) => void;
}) { }) {
const serverService = useService(ServerService);
const { icon } = OAuthProviderMap[provider]; const { icon } = OAuthProviderMap[provider];
const onClick = useCallback(() => { const onClick = useCallback(() => {
@@ -85,12 +85,12 @@ function OAuthProvider({
// if (BUILD_CONFIG.isAndroid) {} // if (BUILD_CONFIG.isAndroid) {}
const oauthUrl = const oauthUrl =
BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`; serverService.server.baseUrl + `/oauth/login?${params.toString()}`;
track.$.$.auth.signIn({ method: 'oauth', provider }); track.$.$.auth.signIn({ method: 'oauth', provider });
popupWindow(oauthUrl); popupWindow(oauthUrl);
}, [popupWindow, provider, redirectUrl, scheme]); }, [popupWindow, provider, redirectUrl, scheme, serverService]);
return ( return (
<Button <Button

View File

@@ -18,7 +18,7 @@ import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useMutation } from '../../../components/hooks/use-mutation'; import { useMutation } from '../../../components/hooks/use-mutation';
import { ServerConfigService } from '../../../modules/cloud'; import { ServerService } from '../../../modules/cloud';
import type { AuthPanelProps } from './index'; import type { AuthPanelProps } from './index';
const useEmailTitle = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => { const useEmailTitle = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
@@ -142,10 +142,10 @@ export const SendEmail = ({
// todo(@pengx17): impl redirectUrl for sendEmail? // todo(@pengx17): impl redirectUrl for sendEmail?
}: AuthPanelProps<'sendEmail'>) => { }: AuthPanelProps<'sendEmail'>) => {
const t = useI18n(); const t = useI18n();
const serverConfig = useService(ServerConfigService).serverConfig; const serverService = useService(ServerService);
const passwordLimits = useLiveData( const passwordLimits = useLiveData(
serverConfig.credentialsRequirement$.map(r => r?.password) serverService.server.credentialsRequirement$.map(r => r?.password)
); );
const [hasSentEmail, setHasSentEmail] = useState(false); const [hasSentEmail, setHasSentEmail] = useState(false);

View File

@@ -1,13 +1,10 @@
import { Tooltip } from '@affine/component/ui/tooltip'; import { Tooltip } from '@affine/component/ui/tooltip';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useServices } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { type SyntheticEvent, useEffect } from 'react'; import { type SyntheticEvent, useEffect } from 'react';
import { import { ServerService, SubscriptionService } from '../../../modules/cloud';
ServerConfigService,
SubscriptionService,
} from '../../../modules/cloud';
import * as styles from './style.css'; import * as styles from './style.css';
export const UserPlanButton = ({ export const UserPlanButton = ({
@@ -15,13 +12,11 @@ export const UserPlanButton = ({
}: { }: {
onClick: (e: SyntheticEvent<Element, Event>) => void; onClick: (e: SyntheticEvent<Element, Event>) => void;
}) => { }) => {
const { serverConfigService, subscriptionService } = useServices({ const serverService = useService(ServerService);
ServerConfigService, const subscriptionService = useService(SubscriptionService);
SubscriptionService,
});
const hasPayment = useLiveData( const hasPayment = useLiveData(
serverConfigService.serverConfig.features$.map(r => r?.payment) serverService.server.features$.map(r => r?.payment)
); );
const plan = useLiveData( const plan = useLiveData(
subscriptionService.subscription.pro$.map(subscription => subscriptionService.subscription.pro$.map(subscription =>

View File

@@ -6,7 +6,7 @@ import {
useSharingUrl, useSharingUrl,
} from '@affine/core/components/hooks/affine/use-share-url'; } from '@affine/core/components/hooks/affine/use-share-url';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { ServerConfigService } from '@affine/core/modules/cloud'; import { ServerService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs'; import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspacePermissionService } from '@affine/core/modules/permissions';
@@ -70,13 +70,13 @@ export const AFFiNESharePage = (props: ShareMenuProps) => {
const currentMode = useLiveData(editor.mode$); const currentMode = useLiveData(editor.mode$);
const editorContainer = useLiveData(editor.editorContainer$); const editorContainer = useLiveData(editor.editorContainer$);
const shareInfoService = useService(ShareInfoService); const shareInfoService = useService(ShareInfoService);
const serverConfig = useService(ServerConfigService).serverConfig; const serverService = useService(ServerService);
useEffect(() => { useEffect(() => {
shareInfoService.shareInfo.revalidate(); shareInfoService.shareInfo.revalidate();
}, [shareInfoService]); }, [shareInfoService]);
const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$); const isSharedPage = useLiveData(shareInfoService.shareInfo.isShared$);
const sharedMode = useLiveData(shareInfoService.shareInfo.sharedMode$); const sharedMode = useLiveData(shareInfoService.shareInfo.sharedMode$);
const baseUrl = useLiveData(serverConfig.config$.map(c => c?.baseUrl)); const baseUrl = serverService.server.baseUrl;
const isLoading = const isLoading =
isSharedPage === null || sharedMode === null || baseUrl === null; isSharedPage === null || sharedMode === null || baseUrl === null;

View File

@@ -3,12 +3,11 @@ import {
cleanupCopilotSessionMutation, cleanupCopilotSessionMutation,
createCopilotMessageMutation, createCopilotMessageMutation,
createCopilotSessionMutation, createCopilotSessionMutation,
fetcher as defaultFetcher,
forkCopilotSessionMutation, forkCopilotSessionMutation,
getBaseUrl,
getCopilotHistoriesQuery, getCopilotHistoriesQuery,
getCopilotHistoryIdsQuery, getCopilotHistoryIdsQuery,
getCopilotSessionsQuery, getCopilotSessionsQuery,
gqlFetcherFactory,
GraphQLError, GraphQLError,
type GraphQLQuery, type GraphQLQuery,
type QueryOptions, type QueryOptions,
@@ -22,6 +21,26 @@ import {
} from '@blocksuite/affine/blocks'; } from '@blocksuite/affine/blocks';
import { getCurrentStore } from '@toeverything/infra'; import { getCurrentStore } from '@toeverything/infra';
/**
* @deprecated will be removed soon
*/
export function getBaseUrl(): string {
if (BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid) {
return BUILD_CONFIG.serverUrlPrefix;
}
if (typeof window === 'undefined') {
// is nodejs
return '';
}
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
}
/**
* @deprecated will be removed soon
*/
const defaultFetcher = gqlFetcherFactory(getBaseUrl() + '/graphql');
type OptionsField<T extends GraphQLQuery> = type OptionsField<T extends GraphQLQuery> =
RequestOptions<T>['variables'] extends { options: infer U } ? U : never; RequestOptions<T>['variables'] extends { options: infer U } ? U : never;

View File

@@ -2,7 +2,6 @@ import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis'; import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
import { authAtom } from '@affine/core/components/atoms'; import { authAtom } from '@affine/core/components/atoms';
import { import {
getBaseUrl,
type getCopilotHistoriesQuery, type getCopilotHistoriesQuery,
type RequestOptions, type RequestOptions,
} from '@affine/graphql'; } from '@affine/graphql';
@@ -11,6 +10,7 @@ import { assertExists } from '@blocksuite/affine/global/utils';
import { getCurrentStore } from '@toeverything/infra'; import { getCurrentStore } from '@toeverything/infra';
import { z } from 'zod'; import { z } from 'zod';
import { getBaseUrl } from './copilot-client';
import type { PromptKey } from './prompt'; import type { PromptKey } from './prompt';
import { import {
cleanupSessions, cleanupSessions,

View File

@@ -3,7 +3,7 @@ import {
generateUrl, generateUrl,
type UseSharingUrl, type UseSharingUrl,
} from '@affine/core/components/hooks/affine/use-share-url'; } from '@affine/core/components/hooks/affine/use-share-url';
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { ServerService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard'; import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard';
import { I18n } from '@affine/i18n'; import { I18n } from '@affine/i18n';
@@ -41,9 +41,7 @@ function createCopyLinkToBlockMenuItem(
return mode === 'edgeless'; return mode === 'edgeless';
}, },
select: () => { select: () => {
const baseUrl = getAffineCloudBaseUrl(); const serverService = framework.get(ServerService);
if (!baseUrl) return;
const pageId = model.doc.id; const pageId = model.doc.id;
const { editor } = framework.get(EditorService); const { editor } = framework.get(EditorService);
const mode = editor.mode$.value; const mode = editor.mode$.value;
@@ -58,7 +56,10 @@ function createCopyLinkToBlockMenuItem(
blockIds: [model.id], blockIds: [model.id],
}; };
const str = generateUrl(options); const str = generateUrl({
...options,
baseUrl: serverService.server.baseUrl,
});
if (!str) return; if (!str) return;
const type = model.flavour; const type = model.flavour;

View File

@@ -3,7 +3,7 @@ import {
generateUrl, generateUrl,
type UseSharingUrl, type UseSharingUrl,
} from '@affine/core/components/hooks/affine/use-share-url'; } from '@affine/core/components/hooks/affine/use-share-url';
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { WorkspaceServerService } from '@affine/core/modules/cloud';
import { EditorService } from '@affine/core/modules/editor'; import { EditorService } from '@affine/core/modules/editor';
import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard'; import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard';
import { I18n } from '@affine/i18n'; import { I18n } from '@affine/i18n';
@@ -79,11 +79,7 @@ function createCopyLinkToBlockMenuItem(
return { return {
...item, ...item,
action: async (ctx: MenuContext) => { action: async (ctx: MenuContext) => {
const baseUrl = getAffineCloudBaseUrl(); const workspaceServerService = framework.get(WorkspaceServerService);
if (!baseUrl) {
ctx.close();
return;
}
const { editor } = framework.get(EditorService); const { editor } = framework.get(EditorService);
const mode = editor.mode$.value; const mode = editor.mode$.value;
@@ -109,7 +105,10 @@ function createCopyLinkToBlockMenuItem(
} }
} }
const str = generateUrl(options); const str = generateUrl({
...options,
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
});
if (!str) { if (!str) {
ctx.close(); ctx.close();
return; return;

View File

@@ -1,5 +1,5 @@
import { notify } from '@affine/component'; import { notify } from '@affine/component';
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { ServerService } from '@affine/core/modules/cloud';
import { toURLSearchParams } from '@affine/core/modules/navigation'; import { toURLSearchParams } from '@affine/core/modules/navigation';
import { copyTextToClipboard } from '@affine/core/utils/clipboard'; import { copyTextToClipboard } from '@affine/core/utils/clipboard';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
@@ -7,6 +7,7 @@ import { track } from '@affine/track';
import { type EditorHost } from '@blocksuite/affine/block-std'; import { type EditorHost } from '@blocksuite/affine/block-std';
import { GfxBlockElementModel } from '@blocksuite/affine/block-std/gfx'; import { GfxBlockElementModel } from '@blocksuite/affine/block-std/gfx';
import type { DocMode, EdgelessRootService } from '@blocksuite/affine/blocks'; import type { DocMode, EdgelessRootService } from '@blocksuite/affine/blocks';
import { useService } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
export type UseSharingUrl = { export type UseSharingUrl = {
@@ -24,17 +25,14 @@ export type UseSharingUrl = {
* https://app.affine.pro/workspace/workspaceId/docId?mode=DocMode&elementIds=seletedElementIds&blockIds=selectedBlockIds * https://app.affine.pro/workspace/workspaceId/docId?mode=DocMode&elementIds=seletedElementIds&blockIds=selectedBlockIds
*/ */
export const generateUrl = ({ export const generateUrl = ({
baseUrl,
workspaceId, workspaceId,
pageId, pageId,
blockIds, blockIds,
elementIds, elementIds,
shareMode: mode, shareMode: mode,
xywh, // not needed currently xywh, // not needed currently
}: UseSharingUrl) => { }: UseSharingUrl & { baseUrl: string }) => {
// Base URL construction
const baseUrl = getAffineCloudBaseUrl();
if (!baseUrl) return null;
try { try {
const url = new URL(`/workspace/${workspaceId}/${pageId}`, baseUrl); const url = new URL(`/workspace/${workspaceId}/${pageId}`, baseUrl);
const search = toURLSearchParams({ mode, blockIds, elementIds, xywh }); const search = toURLSearchParams({ mode, blockIds, elementIds, xywh });
@@ -130,10 +128,12 @@ export const getSelectedNodes = (
export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => { export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
const t = useI18n(); const t = useI18n();
const serverService = useService(ServerService);
const onClickCopyLink = useCallback( const onClickCopyLink = useCallback(
(shareMode?: DocMode, blockIds?: string[], elementIds?: string[]) => { (shareMode?: DocMode, blockIds?: string[], elementIds?: string[]) => {
const sharingUrl = generateUrl({ const sharingUrl = generateUrl({
baseUrl: serverService.server.baseUrl,
workspaceId, workspaceId,
pageId, pageId,
blockIds, blockIds,
@@ -160,7 +160,7 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
notify.error({ title: 'Network not available' }); notify.error({ title: 'Network not available' });
} }
}, },
[pageId, t, workspaceId] [pageId, serverService, t, workspaceId]
); );
return { return {

View File

@@ -1,3 +1,4 @@
import { GraphQLService } from '@affine/core/modules/cloud';
import type { import type {
GraphQLQuery, GraphQLQuery,
MutationOptions, MutationOptions,
@@ -5,7 +6,7 @@ import type {
QueryVariables, QueryVariables,
RecursiveMaybeFields, RecursiveMaybeFields,
} from '@affine/graphql'; } from '@affine/graphql';
import { fetcher } from '@affine/graphql'; import { useService } from '@toeverything/infra';
import type { GraphQLError } from 'graphql'; import type { GraphQLError } from 'graphql';
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { Key } from 'swr'; import type { Key } from 'swr';
@@ -51,10 +52,15 @@ export function useMutation(
options: Omit<MutationOptions<GraphQLQuery>, 'variables'>, options: Omit<MutationOptions<GraphQLQuery>, 'variables'>,
config?: any config?: any
) { ) {
const graphqlService = useService(GraphQLService);
return useSWRMutation( return useSWRMutation(
() => ['cloud', options.mutation.id], () => ['cloud', options.mutation.id],
(_: unknown[], { arg }: { arg: any }) => (_: unknown[], { arg }: { arg: any }) =>
fetcher({ ...options, query: options.mutation, variables: arg }), graphqlService.gql({
...options,
query: options.mutation,
variables: arg,
}),
config config
); );
} }

View File

@@ -1,9 +1,10 @@
import { GraphQLService } from '@affine/core/modules/cloud';
import type { import type {
GraphQLQuery, GraphQLQuery,
QueryOptions, QueryOptions,
QueryResponse, QueryResponse,
} from '@affine/graphql'; } from '@affine/graphql';
import { fetcher } from '@affine/graphql'; import { useService } from '@toeverything/infra';
import type { GraphQLError } from 'graphql'; import type { GraphQLError } from 'graphql';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import type { SWRConfiguration, SWRResponse } from 'swr'; import type { SWRConfiguration, SWRResponse } from 'swr';
@@ -32,7 +33,11 @@ import useSWRInfinite from 'swr/infinite';
type useQueryFn = <Query extends GraphQLQuery>( type useQueryFn = <Query extends GraphQLQuery>(
options?: QueryOptions<Query>, options?: QueryOptions<Query>,
config?: Omit< config?: Omit<
SWRConfiguration<QueryResponse<Query>, GraphQLError, typeof fetcher<Query>>, SWRConfiguration<
QueryResponse<Query>,
GraphQLError,
(options: QueryOptions<Query>) => Promise<QueryResponse<Query>>
>,
'fetcher' 'fetcher'
> >
) => SWRResponse< ) => SWRResponse<
@@ -53,11 +58,12 @@ const createUseQuery =
}), }),
[config] [config]
); );
const graphqlService = useService(GraphQLService);
const useSWRFn = immutable ? useSWRImutable : useSWR; const useSWRFn = immutable ? useSWRImutable : useSWR;
return useSWRFn( return useSWRFn(
options ? () => ['cloud', options.query.id, options.variables] : null, options ? () => ['cloud', options.query.id, options.variables] : null,
options ? () => fetcher(options) : null, options ? () => graphqlService.gql(options) : null,
configWithSuspense configWithSuspense
); );
}; };
@@ -76,7 +82,7 @@ export function useQueryInfinite<Query extends GraphQLQuery>(
SWRConfiguration< SWRConfiguration<
QueryResponse<Query>, QueryResponse<Query>,
GraphQLError | GraphQLError[], GraphQLError | GraphQLError[],
typeof fetcher<Query> (options: QueryOptions<Query>) => Promise<QueryResponse<Query>>
>, >,
'fetcher' 'fetcher'
> >
@@ -88,6 +94,7 @@ export function useQueryInfinite<Query extends GraphQLQuery>(
}), }),
[config] [config]
); );
const graphqlService = useService(GraphQLService);
const { data, setSize, size, error } = useSWRInfinite< const { data, setSize, size, error } = useSWRInfinite<
QueryResponse<Query>, QueryResponse<Query>,
@@ -100,7 +107,7 @@ export function useQueryInfinite<Query extends GraphQLQuery>(
], ],
async ([_, __, variables]) => { async ([_, __, variables]) => {
const params = { ...options, variables } as QueryOptions<Query>; const params = { ...options, variables } as QueryOptions<Query>;
return fetcher(params); return graphqlService.gql(params);
}, },
configWithSuspense configWithSuspense
); );

View File

@@ -23,7 +23,7 @@ import { useCallback, useEffect } from 'react';
import { import {
type AuthAccountInfo, type AuthAccountInfo,
AuthService, AuthService,
ServerConfigService, ServerService,
SubscriptionService, SubscriptionService,
UserCopilotQuotaService, UserCopilotQuotaService,
UserQuotaService, UserQuotaService,
@@ -276,10 +276,8 @@ const AIUsage = () => {
}; };
const OperationMenu = () => { const OperationMenu = () => {
const serverConfigService = useService(ServerConfigService); const serverService = useService(ServerService);
const serverFeatures = useLiveData( const serverFeatures = useLiveData(serverService.server.features$);
serverConfigService.serverConfig.features$
);
return ( return (
<> <>

View File

@@ -1,7 +1,7 @@
import { Button, ErrorMessage, Skeleton } from '@affine/component'; import { Button, ErrorMessage, Skeleton } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import { import {
ServerConfigService, ServerService,
SubscriptionService, SubscriptionService,
UserCopilotQuotaService, UserCopilotQuotaService,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
@@ -22,9 +22,9 @@ export const AIUsagePanel = ({
onChangeSettingState?: (settingState: SettingState) => void; onChangeSettingState?: (settingState: SettingState) => void;
}) => { }) => {
const t = useI18n(); const t = useI18n();
const serverConfigService = useService(ServerConfigService); const serverService = useService(ServerService);
const hasPaymentFeature = useLiveData( const hasPaymentFeature = useLiveData(
serverConfigService.serverConfig.features$.map(f => f?.payment) serverService.server.features$.map(f => f?.payment)
); );
const subscriptionService = useService(SubscriptionService); const subscriptionService = useService(SubscriptionService);
const aiSubscription = useLiveData(subscriptionService.subscription.ai$); const aiSubscription = useLiveData(subscriptionService.subscription.ai$);

View File

@@ -23,7 +23,7 @@ import {
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { AuthService, ServerConfigService } from '../../../../modules/cloud'; import { AuthService, ServerService } from '../../../../modules/cloud';
import type { SettingState } from '../types'; import type { SettingState } from '../types';
import { AIUsagePanel } from './ai-usage-panel'; import { AIUsagePanel } from './ai-usage-panel';
import { StorageProgress } from './storage-progress'; import { StorageProgress } from './storage-progress';
@@ -178,13 +178,11 @@ export const AccountSetting = ({
}: { }: {
onChangeSettingState?: (settingState: SettingState) => void; onChangeSettingState?: (settingState: SettingState) => void;
}) => { }) => {
const { authService, serverConfigService } = useServices({ const { authService, serverService } = useServices({
AuthService, AuthService,
ServerConfigService, ServerService,
}); });
const serverFeatures = useLiveData( const serverFeatures = useLiveData(serverService.server.features$);
serverConfigService.serverConfig.features$
);
const t = useI18n(); const t = useI18n();
const session = authService.session; const session = authService.session;
useEffect(() => { useEffect(() => {

View File

@@ -5,7 +5,7 @@ import { cssVar } from '@toeverything/theme';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { import {
ServerConfigService, ServerService,
SubscriptionService, SubscriptionService,
UserQuotaService, UserQuotaService,
} from '../../../../modules/cloud'; } from '../../../../modules/cloud';
@@ -34,9 +34,9 @@ export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => {
const maxFormatted = useLiveData(quota.maxFormatted$); const maxFormatted = useLiveData(quota.maxFormatted$);
const percent = useLiveData(quota.percent$); const percent = useLiveData(quota.percent$);
const serverConfigService = useService(ServerConfigService); const serverService = useService(ServerService);
const hasPaymentFeature = useLiveData( const hasPaymentFeature = useLiveData(
serverConfigService.serverConfig.features$.map(f => f?.payment) serverService.server.features$.map(f => f?.payment)
); );
const subscription = useService(SubscriptionService).subscription; const subscription = useService(SubscriptionService).subscription;
useEffect(() => { useEffect(() => {

View File

@@ -16,7 +16,7 @@ import {
SettingWrapper, SettingWrapper,
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { ServerConfigService } from '@affine/core/modules/cloud'; import { ServerService } from '@affine/core/modules/cloud';
import { DesktopApiService } from '@affine/core/modules/desktop-api'; import { DesktopApiService } from '@affine/core/modules/desktop-api';
import { import {
EditorSettingService, EditorSettingService,
@@ -358,13 +358,11 @@ const NewDocDefaultModeSettings = () => {
const AISettings = () => { const AISettings = () => {
const t = useI18n(); const t = useI18n();
const { openConfirmModal } = useConfirmModal(); const { openConfirmModal } = useConfirmModal();
const { featureFlagService, serverConfigService } = useServices({ const { featureFlagService, serverService } = useServices({
FeatureFlagService, FeatureFlagService,
ServerConfigService, ServerService,
}); });
const serverFeatures = useLiveData( const serverFeatures = useLiveData(serverService.server.features$);
serverConfigService.serverConfig.features$
);
const enableAI = useLiveData(featureFlagService.flags.enable_ai.$); const enableAI = useLiveData(featureFlagService.flags.enable_ai.$);
const onAIChange = useCallback( const onAIChange = useCallback(

View File

@@ -16,7 +16,7 @@ import {
import type { ReactElement, SVGProps } from 'react'; import type { ReactElement, SVGProps } from 'react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AuthService, ServerConfigService } from '../../../../modules/cloud'; import { AuthService, ServerService } from '../../../../modules/cloud';
import type { SettingState } from '../types'; import type { SettingState } from '../types';
import { AboutAffine } from './about'; import { AboutAffine } from './about';
import { AppearanceSettings } from './appearance'; import { AppearanceSettings } from './appearance';
@@ -38,20 +38,16 @@ export type GeneralSettingList = GeneralSettingListItem[];
export const useGeneralSettingList = (): GeneralSettingList => { export const useGeneralSettingList = (): GeneralSettingList => {
const t = useI18n(); const t = useI18n();
const { const { authService, serverService, userFeatureService, featureFlagService } =
authService, useServices({
serverConfigService, AuthService,
userFeatureService, ServerService,
featureFlagService, UserFeatureService,
} = useServices({ FeatureFlagService,
AuthService, });
ServerConfigService,
UserFeatureService,
FeatureFlagService,
});
const status = useLiveData(authService.session.status$); const status = useLiveData(authService.session.status$);
const hasPaymentFeature = useLiveData( const hasPaymentFeature = useLiveData(
serverConfigService.serverConfig.features$.map(f => f?.payment) serverService.server.features$.map(f => f?.payment)
); );
const enableEditorSettings = useLiveData( const enableEditorSettings = useLiveData(
featureFlagService.flags.enable_editor_settings.$ featureFlagService.flags.enable_editor_settings.$

View File

@@ -40,7 +40,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
type AuthAccountInfo, type AuthAccountInfo,
AuthService, AuthService,
ServerConfigService, ServerService,
SubscriptionService, SubscriptionService,
} from '../../../../../modules/cloud'; } from '../../../../../modules/cloud';
import type { SettingState } from '../../types'; import type { SettingState } from '../../types';
@@ -65,9 +65,9 @@ export const CloudWorkspaceMembersPanel = ({
}: { }: {
onChangeSettingState: (settingState: SettingState) => void; onChangeSettingState: (settingState: SettingState) => void;
}) => { }) => {
const serverConfig = useService(ServerConfigService).serverConfig; const serverService = useService(ServerService);
const hasPaymentFeature = useLiveData( const hasPaymentFeature = useLiveData(
serverConfig.features$.map(f => f?.payment) serverService.server.features$.map(f => f?.payment)
); );
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;

View File

@@ -2,19 +2,14 @@ import { notify } from '@affine/component';
import { import {
ChangeEmailPage, ChangeEmailPage,
ChangePasswordPage, ChangePasswordPage,
ConfirmChangeEmail,
ConfirmVerifiedEmail,
OnboardingPage, OnboardingPage,
SetPasswordPage, SetPasswordPage,
SignInSuccessPage, SignInSuccessPage,
SignUpPage, SignUpPage,
} from '@affine/component/auth-components'; } from '@affine/component/auth-components';
import { import {
changeEmailMutation,
changePasswordMutation, changePasswordMutation,
fetcher,
sendVerifyChangeEmailMutation, sendVerifyChangeEmailMutation,
verifyEmailMutation,
} from '@affine/graphql'; } from '@affine/graphql';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
@@ -28,7 +23,10 @@ import {
RouteLogic, RouteLogic,
useNavigateHelper, useNavigateHelper,
} from '../../../components/hooks/use-navigate-helper'; } from '../../../components/hooks/use-navigate-helper';
import { AuthService, ServerConfigService } from '../../../modules/cloud'; import { AuthService, ServerService } from '../../../modules/cloud';
import { AppContainer } from '../../components/app-container';
import { ConfirmChangeEmail } from './confirm-change-email';
import { ConfirmVerifiedEmail } from './email-verified-email';
const authTypeSchema = z.enum([ const authTypeSchema = z.enum([
'onboarding', 'onboarding',
@@ -46,9 +44,9 @@ export const Component = () => {
const authService = useService(AuthService); const authService = useService(AuthService);
const account = useLiveData(authService.session.account$); const account = useLiveData(authService.session.account$);
const t = useI18n(); const t = useI18n();
const serverConfig = useService(ServerConfigService).serverConfig; const serverService = useService(ServerService);
const passwordLimits = useLiveData( const passwordLimits = useLiveData(
serverConfig.credentialsRequirement$.map(r => r?.password) serverService.server.credentialsRequirement$.map(r => r?.password)
); );
const { authType } = useParams(); const { authType } = useParams();
@@ -103,8 +101,7 @@ export const Component = () => {
}, [jumpToIndex]); }, [jumpToIndex]);
if (!passwordLimits) { if (!passwordLimits) {
// TODO(@eyhn): loading UI return <AppContainer fallback />;
return null;
} }
switch (authType) { switch (authType) {
@@ -171,36 +168,5 @@ export const loader: LoaderFunction = async args => {
return redirect('/404'); return redirect('/404');
} }
if (args.params.authType === 'confirm-change-email') {
const url = new URL(args.request.url);
const searchParams = url.searchParams;
const token = searchParams.get('token') ?? '';
const email = decodeURIComponent(searchParams.get('email') ?? '');
const res = await fetcher({
query: changeEmailMutation,
variables: {
token: token,
email: email,
},
}).catch(console.error);
// TODO(@eyhn): Add error handling
if (!res?.changeEmail) {
return redirect('/expired');
}
} else if (args.params.authType === 'verify-email') {
const url = new URL(args.request.url);
const searchParams = url.searchParams;
const token = searchParams.get('token') ?? '';
const res = await fetcher({
query: verifyEmailMutation,
variables: {
token: token,
},
}).catch(console.error);
if (!res?.verifyEmail) {
return redirect('/expired');
}
}
return null; return null;
}; };

View File

@@ -0,0 +1,64 @@
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 { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra';
import { type FC, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { AppContainer } from '../../components/app-container';
export const ConfirmChangeEmail: FC<{
onOpenAffine: () => void;
}> = ({ onOpenAffine }) => {
const t = useI18n();
const [searchParams] = useSearchParams();
const navigateHelper = useNavigateHelper();
const graphqlService = useService(GraphQLService);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
(async () => {
const token = searchParams.get('token') ?? '';
const email = decodeURIComponent(searchParams.get('email') ?? '');
setIsLoading(true);
await graphqlService
.gql({
query: changeEmailMutation,
variables: {
token: token,
email: email,
},
})
.catch(err => {
if (err instanceof BackendError) {
const userFriendlyError = err.originError;
if (userFriendlyError.name === ErrorNames.INVALID_EMAIL_TOKEN) {
return navigateHelper.jumpToExpired();
}
}
throw err;
});
})().catch(err => {
// TODO(@eyhn): Add error handling
console.error(err);
});
}, [graphqlService, navigateHelper, searchParams]);
if (isLoading) {
return <AppContainer fallback />;
}
return (
<AuthPageContainer
title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,64 @@
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 { useI18n } from '@affine/i18n';
import { useService } from '@toeverything/infra';
import { type FC, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { AppContainer } from '../../components/app-container';
export const ConfirmVerifiedEmail: FC<{
onOpenAffine: () => void;
}> = ({ onOpenAffine }) => {
const t = useI18n();
const graphqlService = useService(GraphQLService);
const [isLoading, setIsLoading] = useState(false);
const [searchParams] = useSearchParams();
const navigateHelper = useNavigateHelper();
useEffect(() => {
(async () => {
const token = searchParams.get('token') ?? '';
setIsLoading(true);
await graphqlService
.gql({
query: verifyEmailMutation,
variables: {
token: token,
},
})
.catch(error => {
const userFriendlyError = UserFriendlyError.fromAnyError(error);
if (userFriendlyError.name === ErrorNames.INVALID_EMAIL_TOKEN) {
return navigateHelper.jumpToExpired();
}
throw error;
});
})().catch(err => {
// TODO(@eyhn): Add error handling
console.error(err);
});
}, [graphqlService, navigateHelper, searchParams]);
if (isLoading) {
return <AppContainer fallback />;
}
return (
<AuthPageContainer
title={t['com.affine.auth.change.email.page.success.title']()}
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
>
<Button variant="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
);
};

View File

@@ -2,56 +2,29 @@ import { AcceptInvitePage } from '@affine/component/member-components';
import type { GetInviteInfoQuery } from '@affine/graphql'; import type { GetInviteInfoQuery } from '@affine/graphql';
import { import {
acceptInviteByInviteIdMutation, acceptInviteByInviteIdMutation,
fetcher,
getInviteInfoQuery, getInviteInfoQuery,
} from '@affine/graphql'; } from '@affine/graphql';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { LoaderFunction } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { redirect, useLoaderData } from 'react-router-dom';
import { import {
RouteLogic, RouteLogic,
useNavigateHelper, useNavigateHelper,
} from '../../../components/hooks/use-navigate-helper'; } from '../../../components/hooks/use-navigate-helper';
import { AuthService } from '../../../modules/cloud'; import { AuthService, GraphQLService } from '../../../modules/cloud';
import { AppContainer } from '../../components/app-container';
/** /**
* /invite/:inviteId page * /invite/:inviteId page
* *
* only for web * only for web
*/ */
export const loader: LoaderFunction = async args => { const AcceptInvite = ({
const inviteId = args.params.inviteId || ''; inviteInfo,
const res = await fetcher({ }: {
query: getInviteInfoQuery, inviteInfo: GetInviteInfoQuery['getInviteInfo'];
variables: { }) => {
inviteId,
},
}).catch(console.error);
// If the inviteId is invalid, redirect to 404 page
if (!res || !res?.getInviteInfo) {
return redirect('/404');
}
// No mater sign in or not, we need to accept the invite
await fetcher({
query: acceptInviteByInviteIdMutation,
variables: {
workspaceId: res.getInviteInfo.workspace.id,
inviteId,
sendAcceptMail: true,
},
}).catch(console.error);
return {
inviteId,
inviteInfo: res.getInviteInfo,
};
};
export const Component = () => {
const authService = useService(AuthService); const authService = useService(AuthService);
const isRevalidating = useLiveData(authService.session.isRevalidating$); const isRevalidating = useLiveData(authService.session.isRevalidating$);
const loginStatus = useLiveData(authService.session.status$); const loginStatus = useLiveData(authService.session.status$);
@@ -63,11 +36,6 @@ export const Component = () => {
const { jumpToSignIn } = useNavigateHelper(); const { jumpToSignIn } = useNavigateHelper();
const { jumpToPage } = useNavigateHelper(); const { jumpToPage } = useNavigateHelper();
const { inviteInfo } = useLoaderData() as {
inviteId: string;
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
};
const openWorkspace = useCallback(() => { const openWorkspace = useCallback(() => {
jumpToPage(inviteInfo.workspace.id, 'all', RouteLogic.REPLACE); jumpToPage(inviteInfo.workspace.id, 'all', RouteLogic.REPLACE);
}, [inviteInfo.workspace.id, jumpToPage]); }, [inviteInfo.workspace.id, jumpToPage]);
@@ -99,3 +67,57 @@ export const Component = () => {
return null; return null;
}; };
export const Component = () => {
const graphqlService = useService(GraphQLService);
const params = useParams<{ inviteId: string }>();
const navigateHelper = useNavigateHelper();
const [data, setData] = useState<{
inviteId: string;
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
} | null>(null);
useEffect(() => {
(async () => {
setData(null);
const inviteId = params.inviteId || '';
const res = await graphqlService.gql({
query: getInviteInfoQuery,
variables: {
inviteId,
},
});
// If the inviteId is invalid, redirect to 404 page
if (!res || !res?.getInviteInfo) {
return navigateHelper.jumpTo404();
}
// No mater sign in or not, we need to accept the invite
await graphqlService.gql({
query: acceptInviteByInviteIdMutation,
variables: {
workspaceId: res.getInviteInfo.workspace.id,
inviteId,
sendAcceptMail: true,
},
});
setData({
inviteId,
inviteInfo: res.getInviteInfo,
});
return;
})().catch(err => {
// TODO: handle error
console.error(err);
});
}, [graphqlService, navigateHelper, params.inviteId]);
if (!data) {
return <AppContainer fallback />;
}
return <AcceptInvite inviteInfo={data.inviteInfo} />;
};

View File

@@ -1,14 +1,13 @@
import { GraphQLService } from '@affine/core/modules/cloud';
import { OpenInAppPage } from '@affine/core/modules/open-in-app/views/open-in-app-page'; import { OpenInAppPage } from '@affine/core/modules/open-in-app/views/open-in-app-page';
import { appSchemes, channelToScheme } from '@affine/core/utils/channel'; import { appSchemes, channelToScheme } from '@affine/core/utils/channel';
import type { GetCurrentUserQuery } from '@affine/graphql'; import type { GetCurrentUserQuery } from '@affine/graphql';
import { fetcher, getCurrentUserQuery } from '@affine/graphql'; import { getCurrentUserQuery } from '@affine/graphql';
import type { LoaderFunction } from 'react-router-dom'; import { useService } from '@toeverything/infra';
import { useLoaderData, useSearchParams } from 'react-router-dom'; import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
interface LoaderData { import { AppContainer } from '../../components/app-container';
action: 'url' | 'signin-redirect';
currentUser?: GetCurrentUserQuery['currentUser'];
}
const OpenUrl = () => { const OpenUrl = () => {
const [params] = useSearchParams(); const [params] = useSearchParams();
@@ -33,8 +32,11 @@ const OpenUrl = () => {
* @deprecated * @deprecated
*/ */
const OpenOAuthJwt = () => { const OpenOAuthJwt = () => {
const { currentUser } = useLoaderData() as LoaderData; const [currentUser, setCurrentUser] = useState<
GetCurrentUserQuery['currentUser'] | null
>(null);
const [params] = useSearchParams(); const [params] = useSearchParams();
const graphqlService = useService(GraphQLService);
const maybeScheme = appSchemes.safeParse(params.get('scheme')); const maybeScheme = appSchemes.safeParse(params.get('scheme'));
const scheme = maybeScheme.success const scheme = maybeScheme.success
@@ -42,8 +44,19 @@ const OpenOAuthJwt = () => {
: channelToScheme[BUILD_CONFIG.appBuildType]; : channelToScheme[BUILD_CONFIG.appBuildType];
const next = params.get('next'); const next = params.get('next');
useEffect(() => {
graphqlService
.gql({
query: getCurrentUserQuery,
})
.then(res => {
setCurrentUser(res?.currentUser || null);
})
.catch(console.error);
}, [graphqlService]);
if (!currentUser || !currentUser?.token?.sessionToken) { if (!currentUser || !currentUser?.token?.sessionToken) {
return null; return <AppContainer fallback />;
} }
const urlToOpen = `${scheme}://signin-redirect?token=${ const urlToOpen = `${scheme}://signin-redirect?token=${
@@ -54,7 +67,8 @@ const OpenOAuthJwt = () => {
}; };
export const Component = () => { export const Component = () => {
const { action } = useLoaderData() as LoaderData; const params = useParams<{ action: string }>();
const action = params.action || '';
if (action === 'url') { if (action === 'url') {
return <OpenUrl />; return <OpenUrl />;
@@ -63,22 +77,3 @@ export const Component = () => {
} }
return null; return null;
}; };
export const loader: LoaderFunction = async args => {
const action = args.params.action || '';
if (action === 'signin-redirect') {
const res = await fetcher({
query: getCurrentUserQuery,
}).catch(console.error);
return {
action,
currentUser: res?.currentUser || null,
};
} else {
return {
action,
};
}
};

View File

@@ -1,4 +1,7 @@
import { NotificationCenter } from '@affine/component'; import { NotificationCenter } from '@affine/component';
import { DefaultServerService } from '@affine/core/modules/cloud';
import { FrameworkScope, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { GlobalDialogs } from '../../dialogs'; import { GlobalDialogs } from '../../dialogs';
@@ -6,13 +9,34 @@ import { CustomThemeModifier } from './custom-theme';
import { FindInPageModal } from './find-in-page/find-in-page-modal'; import { FindInPageModal } from './find-in-page/find-in-page-modal';
export const RootWrapper = () => { export const RootWrapper = () => {
const defaultServerService = useService(DefaultServerService);
const [isServerReady, setIsServerReady] = useState(false);
useEffect(() => {
if (isServerReady) {
return;
}
const abortController = new AbortController();
defaultServerService.server
.waitForConfigRevalidation(abortController.signal)
.then(() => {
setIsServerReady(true);
})
.catch(error => {
console.error(error);
});
return () => {
abortController.abort();
};
}, [defaultServerService, isServerReady]);
return ( return (
<> <FrameworkScope scope={defaultServerService.server.scope}>
<GlobalDialogs /> <GlobalDialogs />
<NotificationCenter /> <NotificationCenter />
<Outlet /> <Outlet />
<CustomThemeModifier /> <CustomThemeModifier />
{BUILD_CONFIG.isElectron && <FindInPageModal />} {BUILD_CONFIG.isElectron && <FindInPageModal />}
</> </FrameworkScope>
); );
}; };

View File

@@ -1,7 +1,4 @@
import { import { ServerService, SubscriptionService } from '@affine/core/modules/cloud';
ServerConfigService,
SubscriptionService,
} from '@affine/core/modules/cloud';
import { SubscriptionPlan } from '@affine/graphql'; import { SubscriptionPlan } from '@affine/graphql';
import { useLiveData, useServices } from '@toeverything/infra'; import { useLiveData, useServices } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -13,12 +10,12 @@ export const UserPlanTag = forwardRef<
HTMLDivElement, HTMLDivElement,
HTMLProps<HTMLDivElement> HTMLProps<HTMLDivElement>
>(function UserPlanTag({ className, ...attrs }, ref) { >(function UserPlanTag({ className, ...attrs }, ref) {
const { serverConfigService, subscriptionService } = useServices({ const { serverService, subscriptionService } = useServices({
ServerConfigService, ServerService,
SubscriptionService, SubscriptionService,
}); });
const hasPayment = useLiveData( const hasPayment = useLiveData(
serverConfigService.serverConfig.features$.map(r => r?.payment) serverService.server.features$.map(r => r?.payment)
); );
const plan = useLiveData( const plan = useLiveData(
subscriptionService.subscription.pro$.map(subscription => subscriptionService.subscription.pro$.map(subscription =>

View File

@@ -1,7 +1,7 @@
import { Skeleton } from '@affine/component'; import { Skeleton } from '@affine/component';
import { import {
AuthService, AuthService,
ServerConfigService, ServerService,
UserCopilotQuotaService, UserCopilotQuotaService,
UserQuotaService, UserQuotaService,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
@@ -71,10 +71,8 @@ const Loading = () => {
}; };
const UsagePanel = () => { const UsagePanel = () => {
const serverConfigService = useService(ServerConfigService); const serverService = useService(ServerService);
const serverFeatures = useLiveData( const serverFeatures = useLiveData(serverService.server.features$);
serverConfigService.serverConfig.features$
);
return ( return (
<SettingGroup title="Storage" contentStyle={{ padding: '10px 16px' }}> <SettingGroup title="Storage" contentStyle={{ padding: '10px 16px' }}>

View File

@@ -0,0 +1,40 @@
import { NotificationCenter } from '@affine/component';
import { DefaultServerService } from '@affine/core/modules/cloud';
import { FrameworkScope, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import { GlobalDialogs } from '../../dialogs';
import { MobileSignInModal } from '../../views/sign-in/modal';
export const RootWrapper = () => {
const defaultServerService = useService(DefaultServerService);
const [isServerReady, setIsServerReady] = useState(false);
useEffect(() => {
if (isServerReady) {
return;
}
const abortController = new AbortController();
defaultServerService.server
.waitForConfigRevalidation(abortController.signal)
.then(() => {
setIsServerReady(true);
})
.catch(error => {
console.error(error);
});
return () => {
abortController.abort();
};
}, [defaultServerService, isServerReady]);
return (
<FrameworkScope scope={defaultServerService.server.scope}>
<GlobalDialogs />
<NotificationCenter />
<MobileSignInModal />
<Outlet />
</FrameworkScope>
);
};

View File

@@ -1,18 +1,15 @@
import { NotificationCenter } from '@affine/component';
import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper'; import { NavigateContext } from '@affine/core/components/hooks/use-navigate-helper';
import { wrapCreateBrowserRouter } from '@sentry/react'; import { wrapCreateBrowserRouter } from '@sentry/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { RouteObject } from 'react-router-dom'; import type { RouteObject } from 'react-router-dom';
import { import {
createBrowserRouter as reactRouterCreateBrowserRouter, createBrowserRouter as reactRouterCreateBrowserRouter,
Outlet,
redirect, redirect,
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
useNavigate, useNavigate,
} from 'react-router-dom'; } from 'react-router-dom';
import { GlobalDialogs } from './dialogs'; import { RootWrapper } from './pages/root';
import { MobileSignInModal } from './views/sign-in/modal';
function RootRouter() { function RootRouter() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -25,10 +22,7 @@ function RootRouter() {
return ( return (
ready && ( ready && (
<NavigateContext.Provider value={navigate}> <NavigateContext.Provider value={navigate}>
<GlobalDialogs /> <RootWrapper />
<NotificationCenter />
<MobileSignInModal />
<Outlet />
</NavigateContext.Provider> </NavigateContext.Provider>
) )
); );

View File

@@ -0,0 +1,152 @@
import {
OAuthProviderType,
ServerDeploymentType,
ServerFeature,
} from '@affine/graphql';
import type { ServerConfig, ServerMetadata } from './types';
export const BUILD_IN_SERVERS: (ServerMetadata & { config: ServerConfig })[] =
environment.isSelfHosted
? [
{
id: 'affine-cloud',
baseUrl: location.origin,
// selfhosted baseUrl is `location.origin`
// this is ok for web app, but not for desktop app
// since we never build desktop app in selfhosted mode, so it's fine
config: {
serverName: 'Affine Selfhost',
features: [],
oauthProviders: [],
type: ServerDeploymentType.Selfhosted,
credentialsRequirement: {
password: {
minLength: 8,
maxLength: 32,
},
},
},
},
]
: BUILD_CONFIG.debug
? [
{
id: 'affine-cloud',
baseUrl: 'http://localhost:8080',
config: {
serverName: 'Affine Cloud',
features: [
ServerFeature.Captcha,
ServerFeature.Copilot,
ServerFeature.OAuth,
ServerFeature.Payment,
],
oauthProviders: [OAuthProviderType.Google],
type: ServerDeploymentType.Affine,
credentialsRequirement: {
password: {
minLength: 8,
maxLength: 32,
},
},
},
},
]
: BUILD_CONFIG.appBuildType === 'stable'
? [
{
id: 'affine-cloud',
baseUrl: 'https://app.affine.pro',
config: {
serverName: 'Affine Cloud',
features: [
ServerFeature.Captcha,
ServerFeature.Copilot,
ServerFeature.OAuth,
ServerFeature.Payment,
],
oauthProviders: [OAuthProviderType.Google],
type: ServerDeploymentType.Affine,
credentialsRequirement: {
password: {
minLength: 8,
maxLength: 32,
},
},
},
},
]
: BUILD_CONFIG.appBuildType === 'beta'
? [
{
id: 'affine-cloud',
baseUrl: 'https://insider.affine.pro',
config: {
serverName: 'Affine Cloud',
features: [
ServerFeature.Captcha,
ServerFeature.Copilot,
ServerFeature.OAuth,
ServerFeature.Payment,
],
oauthProviders: [OAuthProviderType.Google],
type: ServerDeploymentType.Affine,
credentialsRequirement: {
password: {
minLength: 8,
maxLength: 32,
},
},
},
},
]
: BUILD_CONFIG.appBuildType === 'internal'
? [
{
id: 'affine-cloud',
baseUrl: 'https://insider.affine.pro',
config: {
serverName: 'Affine Cloud',
features: [
ServerFeature.Captcha,
ServerFeature.Copilot,
ServerFeature.OAuth,
ServerFeature.Payment,
],
oauthProviders: [OAuthProviderType.Google],
type: ServerDeploymentType.Affine,
credentialsRequirement: {
password: {
minLength: 8,
maxLength: 32,
},
},
},
},
]
: BUILD_CONFIG.appBuildType === 'canary'
? [
{
id: 'affine-cloud',
baseUrl: 'https://affine.fail',
config: {
serverName: 'Affine Cloud',
features: [
ServerFeature.Captcha,
ServerFeature.Copilot,
ServerFeature.OAuth,
ServerFeature.Payment,
],
oauthProviders: [OAuthProviderType.Google],
type: ServerDeploymentType.Affine,
credentialsRequirement: {
password: {
minLength: 8,
maxLength: 32,
},
},
},
},
]
: [];

View File

@@ -1,70 +0,0 @@
import type {
OauthProvidersQuery,
ServerConfigQuery,
ServerFeature,
} from '@affine/graphql';
import {
backoffRetry,
effect,
Entity,
fromPromise,
LiveData,
} from '@toeverything/infra';
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
import type { ServerConfigStore } from '../stores/server-config';
type LowercaseServerFeature = Lowercase<ServerFeature>;
type ServerFeatureRecord = {
[key in LowercaseServerFeature]: boolean;
};
export type ServerConfigType = ServerConfigQuery['serverConfig'] &
OauthProvidersQuery['serverConfig'];
export class ServerConfig extends Entity {
readonly config$ = new LiveData<ServerConfigType | null>(null);
readonly features$ = this.config$.map(config => {
return config
? Array.from(new Set(config.features)).reduce((acc, cur) => {
acc[cur.toLowerCase() as LowercaseServerFeature] = true;
return acc;
}, {} as ServerFeatureRecord)
: null;
});
readonly credentialsRequirement$ = this.config$.map(config => {
return config ? config.credentialsRequirement : null;
});
constructor(private readonly store: ServerConfigStore) {
super();
}
revalidate = effect(
exhaustMap(() => {
return fromPromise<ServerConfigType>(signal =>
this.store.fetchServerConfig(signal)
).pipe(
backoffRetry({
count: Infinity,
}),
mergeMap(config => {
this.config$.next(config);
return EMPTY;
})
);
})
);
revalidateIfNeeded = () => {
if (!this.config$.value) {
this.revalidate();
}
};
override dispose(): void {
this.revalidate.unsubscribe();
}
}

View File

@@ -0,0 +1,110 @@
import type { ServerFeature } from '@affine/graphql';
import {
backoffRetry,
effect,
Entity,
fromPromise,
LiveData,
onComplete,
onStart,
} from '@toeverything/infra';
import { EMPTY, exhaustMap, map, mergeMap } from 'rxjs';
import { ServerScope } from '../scopes/server';
import { FetchService } from '../services/fetch';
import { GraphQLService } from '../services/graphql';
import { ServerConfigStore } from '../stores/server-config';
import type { ServerListStore } from '../stores/server-list';
import type { ServerConfig, ServerMetadata } from '../types';
type LowercaseServerFeature = Lowercase<ServerFeature>;
type ServerFeatureRecord = {
[key in LowercaseServerFeature]: boolean;
};
export class Server extends Entity<{
serverMetadata: ServerMetadata;
}> {
readonly id = this.props.serverMetadata.id;
readonly baseUrl = this.props.serverMetadata.baseUrl;
readonly scope = this.framework.createScope(ServerScope, {
server: this as Server,
});
readonly serverConfigStore = this.scope.framework.get(ServerConfigStore);
readonly fetch = this.scope.framework.get(FetchService).fetch;
readonly gql = this.scope.framework.get(GraphQLService).gql;
readonly serverMetadata = this.props.serverMetadata;
constructor(private readonly serverListStore: ServerListStore) {
super();
}
readonly config$ = LiveData.from<ServerConfig>(
this.serverListStore.watchServerConfig(this.serverMetadata.id).pipe(
map(config => {
if (!config) {
throw new Error('Failed to load server config');
}
return config;
})
),
null as any
);
readonly isConfigRevalidating$ = new LiveData(false);
readonly features$ = this.config$.map(config => {
return Array.from(new Set(config.features)).reduce((acc, cur) => {
acc[cur.toLowerCase() as LowercaseServerFeature] = true;
return acc;
}, {} as ServerFeatureRecord);
});
readonly credentialsRequirement$ = this.config$.map(config => {
return config ? config.credentialsRequirement : null;
});
readonly revalidateConfig = effect(
exhaustMap(() => {
return fromPromise(signal =>
this.serverConfigStore.fetchServerConfig(signal)
).pipe(
backoffRetry({
count: Infinity,
}),
mergeMap(config => {
this.serverListStore.updateServerConfig(this.serverMetadata.id, {
credentialsRequirement: config.credentialsRequirement,
features: config.features,
oauthProviders: config.oauthProviders,
serverName: config.name,
type: config.type,
version: config.version,
initialized: config.initialized,
});
return EMPTY;
}),
onStart(() => {
this.isConfigRevalidating$.next(true);
}),
onComplete(() => {
this.isConfigRevalidating$.next(false);
})
);
})
);
async waitForConfigRevalidation(signal?: AbortSignal) {
this.revalidateConfig();
await this.isConfigRevalidating$.waitFor(
isRevalidating => !isRevalidating,
signal
);
}
override dispose(): void {
this.scope.dispose();
this.revalidateConfig.unsubscribe();
}
}

View File

@@ -13,7 +13,7 @@ import {
import { exhaustMap } from 'rxjs'; import { exhaustMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error'; import { isBackendError, isNetworkError } from '../error';
import type { ServerConfigService } from '../services/server-config'; import type { ServerService } from '../services/server';
import type { SubscriptionStore } from '../stores/subscription'; import type { SubscriptionStore } from '../stores/subscription';
export class SubscriptionPrices extends Entity { export class SubscriptionPrices extends Entity {
@@ -35,7 +35,7 @@ export class SubscriptionPrices extends Entity {
); );
constructor( constructor(
private readonly serverConfigService: ServerConfigService, private readonly serverService: ServerService,
private readonly store: SubscriptionStore private readonly store: SubscriptionStore
) { ) {
super(); super();
@@ -44,13 +44,7 @@ export class SubscriptionPrices extends Entity {
revalidate = effect( revalidate = effect(
exhaustMap(() => { exhaustMap(() => {
return fromPromise(async signal => { return fromPromise(async signal => {
// ensure server config is loaded const serverConfig = this.serverService.server.features$.value;
this.serverConfigService.serverConfig.revalidateIfNeeded();
const serverConfig =
await this.serverConfigService.serverConfig.features$.waitForNonNull(
signal
);
if (!serverConfig.payment) { if (!serverConfig.payment) {
// No payment feature, no subscription // No payment feature, no subscription

View File

@@ -19,7 +19,7 @@ import { EMPTY, map, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error'; import { isBackendError, isNetworkError } from '../error';
import type { AuthService } from '../services/auth'; import type { AuthService } from '../services/auth';
import type { ServerConfigService } from '../services/server-config'; import type { ServerService } from '../services/server';
import type { SubscriptionStore } from '../stores/subscription'; import type { SubscriptionStore } from '../stores/subscription';
export type SubscriptionType = NonNullable< export type SubscriptionType = NonNullable<
@@ -54,7 +54,7 @@ export class Subscription extends Entity {
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly serverConfigService: ServerConfigService, private readonly serverService: ServerService,
private readonly store: SubscriptionStore private readonly store: SubscriptionStore
) { ) {
super(); super();
@@ -100,9 +100,7 @@ export class Subscription extends Entity {
} }
const serverConfig = const serverConfig =
await this.serverConfigService.serverConfig.features$.waitForNonNull( await this.serverService.server.features$.waitForNonNull(signal);
signal
);
if (!serverConfig.payment) { if (!serverConfig.payment) {
// No payment feature, no subscription // No payment feature, no subscription

View File

@@ -13,7 +13,7 @@ import { EMPTY, map, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error'; import { isBackendError, isNetworkError } from '../error';
import type { AuthService } from '../services/auth'; import type { AuthService } from '../services/auth';
import type { ServerConfigService } from '../services/server-config'; import type { ServerService } from '../services/server';
import type { UserCopilotQuotaStore } from '../stores/user-copilot-quota'; import type { UserCopilotQuotaStore } from '../stores/user-copilot-quota';
export class UserCopilotQuota extends Entity { export class UserCopilotQuota extends Entity {
@@ -26,7 +26,7 @@ export class UserCopilotQuota extends Entity {
constructor( constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly store: UserCopilotQuotaStore, private readonly store: UserCopilotQuotaStore,
private readonly serverConfigService: ServerConfigService private readonly serverService: ServerService
) { ) {
super(); super();
} }
@@ -44,9 +44,7 @@ export class UserCopilotQuota extends Entity {
} }
const serverConfig = const serverConfig =
await this.serverConfigService.serverConfig.features$.waitForNonNull( await this.serverService.server.features$.waitForNonNull(signal);
signal
);
let aiQuota = null; let aiQuota = null;

View File

@@ -1,4 +1,5 @@
export type { Invoice } from './entities/invoices'; export type { Invoice } from './entities/invoices';
export { Server } from './entities/server';
export type { AuthAccountInfo } from './entities/session'; export type { AuthAccountInfo } from './entities/session';
export { export {
BackendError, BackendError,
@@ -6,19 +7,23 @@ export {
isNetworkError, isNetworkError,
NetworkError, NetworkError,
} from './error'; } from './error';
export { RawFetchProvider } from './provider/fetch';
export { ValidatorProvider } from './provider/validator'; export { ValidatorProvider } from './provider/validator';
export { WebSocketAuthProvider } from './provider/websocket-auth'; export { WebSocketAuthProvider } from './provider/websocket-auth';
export { AccountChanged, AuthService } from './services/auth'; export { AccountChanged, AuthService } from './services/auth';
export { CaptchaService } from './services/captcha'; export { CaptchaService } from './services/captcha';
export { DefaultServerService } from './services/default-server';
export { FetchService } from './services/fetch'; export { FetchService } from './services/fetch';
export { GraphQLService } from './services/graphql'; export { GraphQLService } from './services/graphql';
export { InvoicesService } from './services/invoices'; export { InvoicesService } from './services/invoices';
export { ServerConfigService } from './services/server-config'; export { ServerService } from './services/server';
export { ServersService } from './services/servers';
export { SubscriptionService } from './services/subscription'; export { SubscriptionService } from './services/subscription';
export { UserCopilotQuotaService } from './services/user-copilot-quota'; export { UserCopilotQuotaService } from './services/user-copilot-quota';
export { UserFeatureService } from './services/user-feature'; export { UserFeatureService } from './services/user-feature';
export { UserQuotaService } from './services/user-quota'; export { UserQuotaService } from './services/user-quota';
export { WebSocketService } from './services/websocket'; export { WebSocketService } from './services/websocket';
export { WorkspaceServerService } from './services/workspace-server';
import { import {
DocScope, DocScope,
@@ -26,38 +31,44 @@ import {
type Framework, type Framework,
GlobalCache, GlobalCache,
GlobalState, GlobalState,
GlobalStateService,
WorkspaceScope, WorkspaceScope,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { UrlService } from '../url'; import { UrlService } from '../url';
import { CloudDocMeta } from './entities/cloud-doc-meta'; import { CloudDocMeta } from './entities/cloud-doc-meta';
import { Invoices } from './entities/invoices'; import { Invoices } from './entities/invoices';
import { ServerConfig } from './entities/server-config'; import { Server } from './entities/server';
import { AuthSession } from './entities/session'; import { AuthSession } from './entities/session';
import { Subscription } from './entities/subscription'; import { Subscription } from './entities/subscription';
import { SubscriptionPrices } from './entities/subscription-prices'; import { SubscriptionPrices } from './entities/subscription-prices';
import { UserCopilotQuota } from './entities/user-copilot-quota'; import { UserCopilotQuota } from './entities/user-copilot-quota';
import { UserFeature } from './entities/user-feature'; import { UserFeature } from './entities/user-feature';
import { UserQuota } from './entities/user-quota'; import { UserQuota } from './entities/user-quota';
import { DefaultFetchProvider, FetchProvider } from './provider/fetch'; import { DefaultRawFetchProvider, RawFetchProvider } from './provider/fetch';
import { ValidatorProvider } from './provider/validator'; import { ValidatorProvider } from './provider/validator';
import { WebSocketAuthProvider } from './provider/websocket-auth'; import { WebSocketAuthProvider } from './provider/websocket-auth';
import { ServerScope } from './scopes/server';
import { AuthService } from './services/auth'; import { AuthService } from './services/auth';
import { CaptchaService } from './services/captcha'; import { CaptchaService } from './services/captcha';
import { CloudDocMetaService } from './services/cloud-doc-meta'; import { CloudDocMetaService } from './services/cloud-doc-meta';
import { DefaultServerService } from './services/default-server';
import { FetchService } from './services/fetch'; import { FetchService } from './services/fetch';
import { GraphQLService } from './services/graphql'; import { GraphQLService } from './services/graphql';
import { InvoicesService } from './services/invoices'; import { InvoicesService } from './services/invoices';
import { ServerConfigService } from './services/server-config'; import { ServerService } from './services/server';
import { ServersService } from './services/servers';
import { SubscriptionService } from './services/subscription'; import { SubscriptionService } from './services/subscription';
import { UserCopilotQuotaService } from './services/user-copilot-quota'; import { UserCopilotQuotaService } from './services/user-copilot-quota';
import { UserFeatureService } from './services/user-feature'; import { UserFeatureService } from './services/user-feature';
import { UserQuotaService } from './services/user-quota'; import { UserQuotaService } from './services/user-quota';
import { WebSocketService } from './services/websocket'; import { WebSocketService } from './services/websocket';
import { WorkspaceServerService } from './services/workspace-server';
import { AuthStore } from './stores/auth'; import { AuthStore } from './stores/auth';
import { CloudDocMetaStore } from './stores/cloud-doc-meta'; import { CloudDocMetaStore } from './stores/cloud-doc-meta';
import { InvoicesStore } from './stores/invoices'; import { InvoicesStore } from './stores/invoices';
import { ServerConfigStore } from './stores/server-config'; import { ServerConfigStore } from './stores/server-config';
import { ServerListStore } from './stores/server-list';
import { SubscriptionStore } from './stores/subscription'; import { SubscriptionStore } from './stores/subscription';
import { UserCopilotQuotaStore } from './stores/user-copilot-quota'; import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
import { UserFeatureStore } from './stores/user-feature'; import { UserFeatureStore } from './stores/user-feature';
@@ -65,23 +76,28 @@ import { UserQuotaStore } from './stores/user-quota';
export function configureCloudModule(framework: Framework) { export function configureCloudModule(framework: Framework) {
framework framework
.service(FetchService, [FetchProvider]) .impl(RawFetchProvider, DefaultRawFetchProvider)
.impl(FetchProvider, DefaultFetchProvider) .service(ServersService, [ServerListStore])
.service(DefaultServerService, [ServersService])
.store(ServerListStore, [GlobalStateService])
.entity(Server, [ServerListStore])
.scope(ServerScope)
.service(ServerService, [ServerScope])
.service(FetchService, [RawFetchProvider, ServerService])
.service(GraphQLService, [FetchService]) .service(GraphQLService, [FetchService])
.service( .service(
WebSocketService, WebSocketService,
f => f =>
new WebSocketService( new WebSocketService(
f.get(ServerService),
f.get(AuthService), f.get(AuthService),
f.getOptional(WebSocketAuthProvider) f.getOptional(WebSocketAuthProvider)
) )
) )
.service(ServerConfigService)
.entity(ServerConfig, [ServerConfigStore])
.store(ServerConfigStore, [GraphQLService]) .store(ServerConfigStore, [GraphQLService])
.service(CaptchaService, f => { .service(CaptchaService, f => {
return new CaptchaService( return new CaptchaService(
f.get(ServerConfigService), f.get(ServerService),
f.get(FetchService), f.get(FetchService),
f.getOptional(ValidatorProvider) f.getOptional(ValidatorProvider)
); );
@@ -90,9 +106,14 @@ export function configureCloudModule(framework: Framework) {
.store(AuthStore, [FetchService, GraphQLService, GlobalState]) .store(AuthStore, [FetchService, GraphQLService, GlobalState])
.entity(AuthSession, [AuthStore]) .entity(AuthSession, [AuthStore])
.service(SubscriptionService, [SubscriptionStore]) .service(SubscriptionService, [SubscriptionStore])
.store(SubscriptionStore, [GraphQLService, GlobalCache, UrlService]) .store(SubscriptionStore, [
.entity(Subscription, [AuthService, ServerConfigService, SubscriptionStore]) GraphQLService,
.entity(SubscriptionPrices, [ServerConfigService, SubscriptionStore]) GlobalCache,
UrlService,
ServerService,
])
.entity(Subscription, [AuthService, ServerService, SubscriptionStore])
.entity(SubscriptionPrices, [ServerService, SubscriptionStore])
.service(UserQuotaService) .service(UserQuotaService)
.store(UserQuotaStore, [GraphQLService]) .store(UserQuotaStore, [GraphQLService])
.entity(UserQuota, [AuthService, UserQuotaStore]) .entity(UserQuota, [AuthService, UserQuotaStore])
@@ -101,7 +122,7 @@ export function configureCloudModule(framework: Framework) {
.entity(UserCopilotQuota, [ .entity(UserCopilotQuota, [
AuthService, AuthService,
UserCopilotQuotaStore, UserCopilotQuotaStore,
ServerConfigService, ServerService,
]) ])
.service(UserFeatureService) .service(UserFeatureService)
.entity(UserFeature, [AuthService, UserFeatureStore]) .entity(UserFeature, [AuthService, UserFeatureStore])
@@ -114,4 +135,6 @@ export function configureCloudModule(framework: Framework) {
.service(CloudDocMetaService) .service(CloudDocMetaService)
.entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache]) .entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache])
.store(CloudDocMetaStore, [GraphQLService]); .store(CloudDocMetaStore, [GraphQLService]);
framework.scope(WorkspaceScope).service(WorkspaceServerService);
} }

View File

@@ -2,15 +2,16 @@ import { createIdentifier } from '@toeverything/infra';
import type { FetchInit } from '../services/fetch'; import type { FetchInit } from '../services/fetch';
export interface FetchProvider { export interface RawFetchProvider {
/** /**
* standard fetch, in ios&android, we can use native fetch to implement this * standard fetch, in ios&android, we can use native fetch to implement this
*/ */
fetch: (input: string | URL, init?: FetchInit) => Promise<Response>; fetch: (input: string | URL, init?: FetchInit) => Promise<Response>;
} }
export const FetchProvider = createIdentifier<FetchProvider>('FetchProvider'); export const RawFetchProvider =
createIdentifier<RawFetchProvider>('FetchProvider');
export const DefaultFetchProvider = { export const DefaultRawFetchProvider = {
fetch: globalThis.fetch.bind(globalThis), fetch: globalThis.fetch.bind(globalThis),
}; };

View File

@@ -0,0 +1,7 @@
import { Scope } from '@toeverything/infra';
import type { Server } from '../entities/server';
export class ServerScope extends Scope<{ server: Server }> {
readonly server = this.props.server;
}

View File

@@ -11,10 +11,10 @@ import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
import type { ValidatorProvider } from '../provider/validator'; import type { ValidatorProvider } from '../provider/validator';
import type { FetchService } from './fetch'; import type { FetchService } from './fetch';
import type { ServerConfigService } from './server-config'; import type { ServerService } from './server';
export class CaptchaService extends Service { export class CaptchaService extends Service {
needCaptcha$ = this.serverConfigService.serverConfig.features$.map( needCaptcha$ = this.serverService.server.features$.map(
r => r?.captcha || false r => r?.captcha || false
); );
challenge$ = new LiveData<string | undefined>(undefined); challenge$ = new LiveData<string | undefined>(undefined);
@@ -23,7 +23,7 @@ export class CaptchaService extends Service {
error$ = new LiveData<any | undefined>(undefined); error$ = new LiveData<any | undefined>(undefined);
constructor( constructor(
private readonly serverConfigService: ServerConfigService, private readonly serverService: ServerService,
private readonly fetchService: FetchService, private readonly fetchService: FetchService,
public readonly validatorProvider?: ValidatorProvider public readonly validatorProvider?: ValidatorProvider
) { ) {

View File

@@ -0,0 +1,26 @@
import { ServerDeploymentType } from '@affine/graphql';
import { Service } from '@toeverything/infra';
import type { Server } from '../entities/server';
import type { ServersService } from './servers';
export class DefaultServerService extends Service {
readonly server: Server;
constructor(private readonly serversService: ServersService) {
super();
// global server is always affine-cloud
const server = this.serversService.server$('affine-cloud').value;
if (!server) {
throw new Error('No server found');
}
this.server = server;
}
async waitForSelfhostedServerConfig() {
if (this.server.config$.value.type === ServerDeploymentType.Selfhosted) {
await this.server.waitForConfigRevalidation();
}
}
}

View File

@@ -3,22 +3,18 @@ import { UserFriendlyError } from '@affine/graphql';
import { fromPromise, Service } from '@toeverything/infra'; import { fromPromise, Service } from '@toeverything/infra';
import { BackendError, NetworkError } from '../error'; import { BackendError, NetworkError } from '../error';
import type { FetchProvider } from '../provider/fetch'; import type { RawFetchProvider } from '../provider/fetch';
import type { ServerService } from './server';
export function getAffineCloudBaseUrl(): string {
if (BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid) {
return BUILD_CONFIG.serverUrlPrefix;
}
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
}
const logger = new DebugLogger('affine:fetch'); const logger = new DebugLogger('affine:fetch');
export type FetchInit = RequestInit & { timeout?: number }; export type FetchInit = RequestInit & { timeout?: number };
export class FetchService extends Service { export class FetchService extends Service {
constructor(private readonly fetchProvider: FetchProvider) { constructor(
private readonly fetchProvider: RawFetchProvider,
private readonly serverService: ServerService
) {
super(); super();
} }
rxFetch = ( rxFetch = (
@@ -55,7 +51,7 @@ export class FetchService extends Service {
}, timeout); }, timeout);
const res = await this.fetchProvider const res = await this.fetchProvider
.fetch(new URL(input, getAffineCloudBaseUrl()), { .fetch(new URL(input, this.serverService.server.serverMetadata.baseUrl), {
...init, ...init,
signal: abortController.signal, signal: abortController.signal,
}) })

View File

@@ -1,12 +0,0 @@
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
import { ServerConfig } from '../entities/server-config';
@OnEvent(ApplicationStarted, e => e.onApplicationStart)
export class ServerConfigService extends Service {
serverConfig = this.framework.createEntity(ServerConfig);
private onApplicationStart() {
this.serverConfig.revalidate();
}
}

View File

@@ -0,0 +1,10 @@
import { Service } from '@toeverything/infra';
import type { ServerScope } from '../scopes/server';
export class ServerService extends Service {
readonly server = this.serverScope.server;
constructor(private readonly serverScope: ServerScope) {
super();
}
}

View File

@@ -0,0 +1,55 @@
import { LiveData, ObjectPool, Service } from '@toeverything/infra';
import { finalize, of, switchMap } from 'rxjs';
import { Server } from '../entities/server';
import type { ServerListStore } from '../stores/server-list';
import type { ServerConfig, ServerMetadata } from '../types';
export class ServersService extends Service {
constructor(private readonly serverListStore: ServerListStore) {
super();
}
servers$ = LiveData.from<Server[]>(
this.serverListStore.watchServerList().pipe(
switchMap(metadatas => {
const refs = metadatas.map(metadata => {
const exists = this.serverPool.get(metadata.id);
if (exists) {
return exists;
}
const server = this.framework.createEntity(Server, {
serverMetadata: metadata,
});
const ref = this.serverPool.put(metadata.id, server);
return ref;
});
return of(refs.map(ref => ref.obj)).pipe(
finalize(() => {
refs.forEach(ref => {
ref.release();
});
})
);
})
),
[] as any
);
server$(id: string) {
return this.servers$.map(servers =>
servers.find(server => server.id === id)
);
}
private readonly serverPool = new ObjectPool<string, Server>({
onDelete(obj) {
obj.dispose();
},
});
addServer(metadata: ServerMetadata, config: ServerConfig) {
this.serverListStore.addServer(metadata, config);
}
}

View File

@@ -2,14 +2,14 @@ import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
import { Manager } from 'socket.io-client'; import { Manager } from 'socket.io-client';
import type { WebSocketAuthProvider } from '../provider/websocket-auth'; import type { WebSocketAuthProvider } from '../provider/websocket-auth';
import { getAffineCloudBaseUrl } from '../services/fetch';
import type { AuthService } from './auth'; import type { AuthService } from './auth';
import { AccountChanged } from './auth'; import { AccountChanged } from './auth';
import type { ServerService } from './server';
@OnEvent(AccountChanged, e => e.update) @OnEvent(AccountChanged, e => e.update)
@OnEvent(ApplicationStarted, e => e.update) @OnEvent(ApplicationStarted, e => e.update)
export class WebSocketService extends Service { export class WebSocketService extends Service {
ioManager: Manager = new Manager(`${getAffineCloudBaseUrl()}/`, { ioManager: Manager = new Manager(`${this.serverService.server.baseUrl}/`, {
autoConnect: false, autoConnect: false,
transports: ['websocket'], transports: ['websocket'],
secure: location.protocol === 'https:', secure: location.protocol === 'https:',
@@ -18,7 +18,7 @@ export class WebSocketService extends Service {
auth: this.webSocketAuthProvider auth: this.webSocketAuthProvider
? cb => { ? cb => {
this.webSocketAuthProvider this.webSocketAuthProvider
?.getAuthToken(`${getAffineCloudBaseUrl()}/`) ?.getAuthToken(`${this.serverService.server.baseUrl}/`)
.then(v => { .then(v => {
cb(v ?? {}); cb(v ?? {});
}) })
@@ -31,6 +31,7 @@ export class WebSocketService extends Service {
refCount = 0; refCount = 0;
constructor( constructor(
private readonly serverService: ServerService,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly webSocketAuthProvider?: WebSocketAuthProvider private readonly webSocketAuthProvider?: WebSocketAuthProvider
) { ) {

View File

@@ -0,0 +1,11 @@
import { Service } from '@toeverything/infra';
import type { Server } from '../entities/server';
export class WorkspaceServerService extends Service {
server: Server | null = null;
bindServer(server: Server) {
this.server = server;
}
}

View File

@@ -1,13 +1,17 @@
import { import {
type OauthProvidersQuery,
oauthProvidersQuery, oauthProvidersQuery,
type ServerConfigQuery,
serverConfigQuery, serverConfigQuery,
ServerFeature, ServerFeature,
} from '@affine/graphql'; } from '@affine/graphql';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
import type { ServerConfigType } from '../entities/server-config';
import type { GraphQLService } from '../services/graphql'; import type { GraphQLService } from '../services/graphql';
export type ServerConfigType = ServerConfigQuery['serverConfig'] &
OauthProvidersQuery['serverConfig'];
export class ServerConfigStore extends Store { export class ServerConfigStore extends Store {
constructor(private readonly gqlService: GraphQLService) { constructor(private readonly gqlService: GraphQLService) {
super(); super();

View File

@@ -0,0 +1,85 @@
import type { GlobalStateService } from '@toeverything/infra';
import { Store } from '@toeverything/infra';
import { map } from 'rxjs';
import { BUILD_IN_SERVERS } from '../constant';
import type { ServerConfig, ServerMetadata } from '../types';
export class ServerListStore extends Store {
constructor(private readonly globalStateService: GlobalStateService) {
super();
}
watchServerList() {
return this.globalStateService.globalState
.watch<ServerMetadata[]>('serverList')
.pipe(
map(servers => {
const serverList = [...BUILD_IN_SERVERS, ...(servers ?? [])];
return serverList;
})
);
}
getServerList() {
return [
...BUILD_IN_SERVERS,
...(this.globalStateService.globalState.get<ServerMetadata[]>(
'serverList'
) ?? []),
];
}
addServer(server: ServerMetadata, serverConfig: ServerConfig) {
this.updateServerConfig(server.id, serverConfig);
const oldServers =
this.globalStateService.globalState.get<ServerMetadata[]>('serverList') ??
[];
this.globalStateService.globalState.set<ServerMetadata[]>('serverList', [
...oldServers,
server,
]);
}
removeServer(serverId: string) {
const oldServers =
this.globalStateService.globalState.get<ServerMetadata[]>('serverList') ??
[];
this.globalStateService.globalState.set<ServerMetadata[]>(
'serverList',
oldServers.filter(server => server.id !== serverId)
);
}
watchServerConfig(serverId: string) {
return this.globalStateService.globalState
.watch<ServerConfig>(`serverConfig:${serverId}`)
.pipe(
map(config => {
if (!config) {
return BUILD_IN_SERVERS.find(server => server.id === serverId)
?.config;
} else {
return config;
}
})
);
}
getServerConfig(serverId: string) {
return (
this.globalStateService.globalState.get<ServerConfig>(
`serverConfig:${serverId}`
) ?? BUILD_IN_SERVERS.find(server => server.id === serverId)?.config
);
}
updateServerConfig(serverId: string, config: ServerConfig) {
this.globalStateService.globalState.set<ServerConfig>(
`serverConfig:${serverId}`,
config
);
}
}

View File

@@ -16,18 +16,19 @@ import { Store } from '@toeverything/infra';
import type { UrlService } from '../../url'; import type { UrlService } from '../../url';
import type { SubscriptionType } from '../entities/subscription'; import type { SubscriptionType } from '../entities/subscription';
import { getAffineCloudBaseUrl } from '../services/fetch';
import type { GraphQLService } from '../services/graphql'; import type { GraphQLService } from '../services/graphql';
import type { ServerService } from '../services/server';
const SUBSCRIPTION_CACHE_KEY = 'subscription:'; const SUBSCRIPTION_CACHE_KEY = 'subscription:';
const getDefaultSubscriptionSuccessCallbackLink = ( const getDefaultSubscriptionSuccessCallbackLink = (
baseUrl: string,
plan: SubscriptionPlan | null, plan: SubscriptionPlan | null,
scheme?: string scheme?: string
) => { ) => {
const path = const path =
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success'; plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
const urlString = getAffineCloudBaseUrl() + path; const urlString = baseUrl + path;
const url = new URL(urlString); const url = new URL(urlString);
if (scheme) { if (scheme) {
url.searchParams.set('scheme', scheme); url.searchParams.set('scheme', scheme);
@@ -39,7 +40,8 @@ export class SubscriptionStore extends Store {
constructor( constructor(
private readonly gqlService: GraphQLService, private readonly gqlService: GraphQLService,
private readonly globalCache: GlobalCache, private readonly globalCache: GlobalCache,
private readonly urlService: UrlService private readonly urlService: UrlService,
private readonly serverService: ServerService
) { ) {
super(); super();
} }
@@ -132,6 +134,7 @@ export class SubscriptionStore extends Store {
successCallbackLink: successCallbackLink:
input.successCallbackLink || input.successCallbackLink ||
getDefaultSubscriptionSuccessCallbackLink( getDefaultSubscriptionSuccessCallbackLink(
this.serverService.server.baseUrl,
input.plan, input.plan,
this.urlService.getClientScheme() this.urlService.getClientScheme()
), ),

View File

@@ -0,0 +1,22 @@
import type {
CredentialsRequirementType,
OAuthProviderType,
ServerDeploymentType,
ServerFeature,
} from '@affine/graphql';
export interface ServerMetadata {
id: string;
baseUrl: string;
}
export interface ServerConfig {
serverName: string;
features: ServerFeature[];
oauthProviders: OAuthProviderType[];
type: ServerDeploymentType;
initialized?: boolean;
version?: string;
credentialsRequirement: CredentialsRequirementType;
}

View File

@@ -15,7 +15,7 @@ import {
useNavigationType, useNavigationType,
} from 'react-router-dom'; } from 'react-router-dom';
import { AuthService } from '../../cloud'; import { AuthService, ServersService } from '../../cloud';
import type { DesktopApi } from '../entities/electron-api'; import type { DesktopApi } from '../entities/electron-api';
@OnEvent(ApplicationStarted, e => e.setupStartListener) @OnEvent(ApplicationStarted, e => e.setupStartListener)
@@ -140,10 +140,19 @@ export class DesktopApiService extends Service {
private setupAuthRequestEvent() { private setupAuthRequestEvent() {
this.events.ui.onAuthenticationRequest(({ method, payload }) => { this.events.ui.onAuthenticationRequest(({ method, payload }) => {
(async () => { (async () => {
const authService = this.framework.get(AuthService);
if (!(await this.api.handler.ui.isActiveTab())) { if (!(await this.api.handler.ui.isActiveTab())) {
return; return;
} }
// TODO: support multiple servers
const affineCloudServer = this.framework
.get(ServersService)
.server$('affine-cloud').value;
if (!affineCloudServer) {
throw new Error('Affine Cloud server not found');
}
const authService = affineCloudServer.scope.get(AuthService);
switch (method) { switch (method) {
case 'magic-link': { case 'magic-link': {
const { email, token } = payload; const { email, token } = payload;

View File

@@ -2,21 +2,29 @@ import type { GlobalState } from '@toeverything/infra';
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
import { map, type Observable, switchMap } from 'rxjs'; import { map, type Observable, switchMap } from 'rxjs';
import type { UserDBService } from '../../userspace'; import type { ServersService } from '../../cloud';
import { UserDBService } from '../../userspace';
import type { EditorSettingProvider } from '../provider/editor-setting-provider'; import type { EditorSettingProvider } from '../provider/editor-setting-provider';
export class CurrentUserDBEditorSettingProvider export class CurrentUserDBEditorSettingProvider
extends Service extends Service
implements EditorSettingProvider implements EditorSettingProvider
{ {
currentUserDB$ = this.userDBService.currentUserDB.db$; private readonly currentUserDB$;
fallback = new GlobalStateEditorSettingProvider(this.globalState); fallback = new GlobalStateEditorSettingProvider(this.globalState);
constructor( constructor(
public readonly userDBService: UserDBService, public readonly serversService: ServersService,
public readonly globalState: GlobalState public readonly globalState: GlobalState
) { ) {
super(); super();
const affineCloudServer = this.serversService.server$('affine-cloud').value; // TODO: support multiple servers
if (!affineCloudServer) {
throw new Error('affine-cloud server not found');
}
const userDBService = affineCloudServer.scope.get(UserDBService);
this.currentUserDB$ = userDBService.currentUserDB.db$;
} }
set(key: string, value: string): void { set(key: string, value: string): void {

View File

@@ -4,9 +4,9 @@ import {
GlobalStateService, GlobalStateService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { ServersService } from '../cloud';
import { DesktopApiService } from '../desktop-api'; import { DesktopApiService } from '../desktop-api';
import { I18n } from '../i18n'; import { I18n } from '../i18n';
import { UserDBService } from '../userspace';
import { EditorSetting } from './entities/editor-setting'; import { EditorSetting } from './entities/editor-setting';
import { CurrentUserDBEditorSettingProvider } from './impls/user-db'; import { CurrentUserDBEditorSettingProvider } from './impls/user-db';
import { EditorSettingProvider } from './provider/editor-setting-provider'; import { EditorSettingProvider } from './provider/editor-setting-provider';
@@ -21,7 +21,7 @@ export function configureEditorSettingModule(framework: Framework) {
.service(EditorSettingService) .service(EditorSettingService)
.entity(EditorSetting, [EditorSettingProvider]) .entity(EditorSetting, [EditorSettingProvider])
.impl(EditorSettingProvider, CurrentUserDBEditorSettingProvider, [ .impl(EditorSettingProvider, CurrentUserDBEditorSettingProvider, [
UserDBService, ServersService,
GlobalState, GlobalState,
]); ]);
} }

View File

@@ -5,7 +5,7 @@ import {
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { AuthService } from '../cloud'; import { WorkspaceServerService } from '../cloud';
import { FavoriteList } from './entities/favorite-list'; import { FavoriteList } from './entities/favorite-list';
import { FavoriteService } from './services/favorite'; import { FavoriteService } from './services/favorite';
import { import {
@@ -27,7 +27,11 @@ export function configureFavoriteModule(framework: Framework) {
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(FavoriteService) .service(FavoriteService)
.entity(FavoriteList, [FavoriteStore]) .entity(FavoriteList, [FavoriteStore])
.store(FavoriteStore, [AuthService, WorkspaceDBService, WorkspaceService]) .store(FavoriteStore, [
WorkspaceDBService,
WorkspaceService,
WorkspaceServerService,
])
.service(MigrationFavoriteItemsAdapter, [WorkspaceService]) .service(MigrationFavoriteItemsAdapter, [WorkspaceService])
.service(CompatibleFavoriteItemsAdapter, [FavoriteService]); .service(CompatibleFavoriteItemsAdapter, [FavoriteService]);
} }

View File

@@ -3,7 +3,7 @@ import type { WorkspaceDBService, WorkspaceService } from '@toeverything/infra';
import { LiveData, Store } from '@toeverything/infra'; import { LiveData, Store } from '@toeverything/infra';
import { map } from 'rxjs'; import { map } from 'rxjs';
import type { AuthService } from '../../cloud'; import { AuthService, type WorkspaceServerService } from '../../cloud';
import type { FavoriteSupportType } from '../constant'; import type { FavoriteSupportType } from '../constant';
import { isFavoriteSupportType } from '../constant'; import { isFavoriteSupportType } from '../constant';
@@ -14,27 +14,31 @@ export interface FavoriteRecord {
} }
export class FavoriteStore extends Store { export class FavoriteStore extends Store {
authService = this.workspaceServerService.server?.scope.get(AuthService);
constructor( constructor(
private readonly authService: AuthService,
private readonly workspaceDBService: WorkspaceDBService, private readonly workspaceDBService: WorkspaceDBService,
private readonly workspaceService: WorkspaceService private readonly workspaceService: WorkspaceService,
private readonly workspaceServerService: WorkspaceServerService
) { ) {
super(); super();
} }
private get userdataDB$() { private get userdataDB$() {
return this.authService.session.account$.map(account => { // if is local workspace or no account, use __local__ userdata
// if is local workspace or no account, use __local__ userdata // sometimes we may have cloud workspace but no account for a short time, we also use __local__ userdata
// sometimes we may have cloud workspace but no account for a short time, we also use __local__ userdata if (
if ( this.workspaceService.workspace.meta.flavour === WorkspaceFlavour.LOCAL ||
this.workspaceService.workspace.meta.flavour === !this.authService
WorkspaceFlavour.LOCAL || ) {
!account return new LiveData(this.workspaceDBService.userdataDB('__local__'));
) { } else {
return this.workspaceDBService.userdataDB('__local__'); return this.authService.session.account$.map(account => {
} if (!account) {
return this.workspaceDBService.userdataDB(account.id); return this.workspaceDBService.userdataDB('__local__');
}); }
return this.workspaceDBService.userdataDB(account.id);
});
}
} }
watchIsLoading() { watchIsLoading() {

View File

@@ -2,7 +2,6 @@ export type { Member } from './entities/members';
export { WorkspaceMembersService } from './services/members'; export { WorkspaceMembersService } from './services/members';
export { WorkspacePermissionService } from './services/permission'; export { WorkspacePermissionService } from './services/permission';
import { GraphQLService } from '@affine/core/modules/cloud';
import { import {
type Framework, type Framework,
WorkspaceScope, WorkspaceScope,
@@ -10,6 +9,7 @@ import {
WorkspacesService, WorkspacesService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { WorkspaceServerService } from '../cloud';
import { WorkspaceMembers } from './entities/members'; import { WorkspaceMembers } from './entities/members';
import { WorkspacePermission } from './entities/permission'; import { WorkspacePermission } from './entities/permission';
import { WorkspaceMembersService } from './services/members'; import { WorkspaceMembersService } from './services/members';
@@ -25,9 +25,9 @@ export function configurePermissionsModule(framework: Framework) {
WorkspacesService, WorkspacesService,
WorkspacePermissionStore, WorkspacePermissionStore,
]) ])
.store(WorkspacePermissionStore, [GraphQLService]) .store(WorkspacePermissionStore, [WorkspaceServerService])
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore]) .entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
.service(WorkspaceMembersService) .service(WorkspaceMembersService)
.store(WorkspaceMembersStore, [GraphQLService]) .store(WorkspaceMembersStore, [WorkspaceServerService])
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService]); .entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService]);
} }

View File

@@ -1,10 +1,10 @@
import { getMembersByWorkspaceIdQuery } from '@affine/graphql'; import { getMembersByWorkspaceIdQuery } from '@affine/graphql';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../../cloud'; import type { WorkspaceServerService } from '../../cloud';
export class WorkspaceMembersStore extends Store { export class WorkspaceMembersStore extends Store {
constructor(private readonly graphqlService: GraphQLService) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
@@ -14,7 +14,10 @@ export class WorkspaceMembersStore extends Store {
take: number, take: number,
signal?: AbortSignal signal?: AbortSignal
) { ) {
const data = await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getMembersByWorkspaceIdQuery, query: getMembersByWorkspaceIdQuery,
variables: { variables: {
workspaceId, workspaceId,

View File

@@ -1,14 +1,17 @@
import type { GraphQLService } from '@affine/core/modules/cloud'; import type { WorkspaceServerService } from '@affine/core/modules/cloud';
import { getIsOwnerQuery, leaveWorkspaceMutation } from '@affine/graphql'; import { getIsOwnerQuery, leaveWorkspaceMutation } from '@affine/graphql';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
export class WorkspacePermissionStore extends Store { export class WorkspacePermissionStore extends Store {
constructor(private readonly graphqlService: GraphQLService) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
async fetchIsOwner(workspaceId: string, signal?: AbortSignal) { async fetchIsOwner(workspaceId: string, signal?: AbortSignal) {
const isOwner = await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const isOwner = await this.workspaceServerService.server.gql({
query: getIsOwnerQuery, query: getIsOwnerQuery,
variables: { variables: {
workspaceId, workspaceId,
@@ -23,7 +26,10 @@ export class WorkspacePermissionStore extends Store {
* @param workspaceName for send email * @param workspaceName for send email
*/ */
async leaveWorkspace(workspaceId: string, workspaceName: string) { async leaveWorkspace(workspaceId: string, workspaceName: string) {
await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: leaveWorkspaceMutation, query: leaveWorkspaceMutation,
variables: { variables: {
workspaceId, workspaceId,

View File

@@ -1,12 +1,12 @@
export { WorkspaceQuotaService } from './services/quota'; export { WorkspaceQuotaService } from './services/quota';
import { GraphQLService } from '@affine/core/modules/cloud';
import { import {
type Framework, type Framework,
WorkspaceScope, WorkspaceScope,
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { WorkspaceServerService } from '../cloud';
import { WorkspaceQuota } from './entities/quota'; import { WorkspaceQuota } from './entities/quota';
import { WorkspaceQuotaService } from './services/quota'; import { WorkspaceQuotaService } from './services/quota';
import { WorkspaceQuotaStore } from './stores/quota'; import { WorkspaceQuotaStore } from './stores/quota';
@@ -15,6 +15,6 @@ export function configureQuotaModule(framework: Framework) {
framework framework
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(WorkspaceQuotaService) .service(WorkspaceQuotaService)
.store(WorkspaceQuotaStore, [GraphQLService]) .store(WorkspaceQuotaStore, [WorkspaceServerService])
.entity(WorkspaceQuota, [WorkspaceService, WorkspaceQuotaStore]); .entity(WorkspaceQuota, [WorkspaceService, WorkspaceQuotaStore]);
} }

View File

@@ -1,14 +1,17 @@
import type { GraphQLService } from '@affine/core/modules/cloud'; import type { WorkspaceServerService } from '@affine/core/modules/cloud';
import { workspaceQuotaQuery } from '@affine/graphql'; import { workspaceQuotaQuery } from '@affine/graphql';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
export class WorkspaceQuotaStore extends Store { export class WorkspaceQuotaStore extends Store {
constructor(private readonly graphqlService: GraphQLService) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
async fetchWorkspaceQuota(workspaceId: string, signal?: AbortSignal) { async fetchWorkspaceQuota(workspaceId: string, signal?: AbortSignal) {
const data = await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: workspaceQuotaQuery, query: workspaceQuotaQuery,
variables: { variables: {
id: workspaceId, id: workspaceId,

View File

@@ -12,7 +12,7 @@ import {
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { FetchService, GraphQLService } from '../cloud'; import { RawFetchProvider, WorkspaceServerService } from '../cloud';
import { ShareDocsList } from './entities/share-docs-list'; import { ShareDocsList } from './entities/share-docs-list';
import { ShareInfo } from './entities/share-info'; import { ShareInfo } from './entities/share-info';
import { ShareReader } from './entities/share-reader'; import { ShareReader } from './entities/share-reader';
@@ -27,10 +27,10 @@ export function configureShareDocsModule(framework: Framework) {
framework framework
.service(ShareReaderService) .service(ShareReaderService)
.entity(ShareReader, [ShareReaderStore]) .entity(ShareReader, [ShareReaderStore])
.store(ShareReaderStore, [FetchService]) .store(ShareReaderStore, [RawFetchProvider])
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(ShareDocsListService, [WorkspaceService]) .service(ShareDocsListService, [WorkspaceService])
.store(ShareDocsStore, [GraphQLService]) .store(ShareDocsStore, [WorkspaceServerService])
.entity(ShareDocsList, [ .entity(ShareDocsList, [
WorkspaceService, WorkspaceService,
ShareDocsStore, ShareDocsStore,
@@ -39,5 +39,5 @@ export function configureShareDocsModule(framework: Framework) {
.scope(DocScope) .scope(DocScope)
.service(ShareInfoService) .service(ShareInfoService)
.entity(ShareInfo, [WorkspaceService, DocService, ShareStore]) .entity(ShareInfo, [WorkspaceService, DocService, ShareStore])
.store(ShareStore, [GraphQLService]); .store(ShareStore, [WorkspaceServerService]);
} }

View File

@@ -1,14 +1,17 @@
import type { GraphQLService } from '@affine/core/modules/cloud'; import type { WorkspaceServerService } from '@affine/core/modules/cloud';
import { getWorkspacePublicPagesQuery } from '@affine/graphql'; import { getWorkspacePublicPagesQuery } from '@affine/graphql';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
export class ShareDocsStore extends Store { export class ShareDocsStore extends Store {
constructor(private readonly graphqlService: GraphQLService) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
async getWorkspacesShareDocs(workspaceId: string, signal?: AbortSignal) { async getWorkspacesShareDocs(workspaceId: string, signal?: AbortSignal) {
const data = await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getWorkspacePublicPagesQuery, query: getWorkspacePublicPagesQuery,
variables: { variables: {
workspaceId: workspaceId, workspaceId: workspaceId,

View File

@@ -2,16 +2,20 @@ import { ErrorNames, UserFriendlyError } from '@affine/graphql';
import type { DocMode } from '@blocksuite/affine/blocks'; import type { DocMode } from '@blocksuite/affine/blocks';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
import { type FetchService, isBackendError } from '../../cloud'; import type { RawFetchProvider } from '../../cloud';
import { isBackendError } from '../../cloud';
export class ShareReaderStore extends Store { export class ShareReaderStore extends Store {
constructor(private readonly fetchService: FetchService) { constructor(private readonly rawFetch?: RawFetchProvider) {
super(); super();
} }
async loadShare(workspaceId: string, docId: string) { async loadShare(workspaceId: string, docId: string) {
if (!this.rawFetch) {
throw new Error('No Fetch Service');
}
try { try {
const docResponse = await this.fetchService.fetch( const docResponse = await this.rawFetch.fetch(
`/api/workspaces/${workspaceId}/docs/${docId}` `/api/workspaces/${workspaceId}/docs/${docId}`
); );
const publishMode = docResponse.headers.get( const publishMode = docResponse.headers.get(
@@ -19,7 +23,7 @@ export class ShareReaderStore extends Store {
) as DocMode | null; ) as DocMode | null;
const docBinary = await docResponse.arrayBuffer(); const docBinary = await docResponse.arrayBuffer();
const workspaceResponse = await this.fetchService.fetch( const workspaceResponse = await this.rawFetch.fetch(
`/api/workspaces/${workspaceId}/docs/${workspaceId}` `/api/workspaces/${workspaceId}/docs/${workspaceId}`
); );
const workspaceBinary = await workspaceResponse.arrayBuffer(); const workspaceBinary = await workspaceResponse.arrayBuffer();

View File

@@ -6,10 +6,10 @@ import {
} from '@affine/graphql'; } from '@affine/graphql';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../../cloud'; import type { WorkspaceServerService } from '../../cloud';
export class ShareStore extends Store { export class ShareStore extends Store {
constructor(private readonly gqlService: GraphQLService) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
@@ -18,7 +18,10 @@ export class ShareStore extends Store {
docId: string, docId: string,
signal?: AbortSignal signal?: AbortSignal
) { ) {
const data = await this.gqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getWorkspacePublicPageByIdQuery, query: getWorkspacePublicPageByIdQuery,
variables: { variables: {
pageId: docId, pageId: docId,
@@ -37,7 +40,10 @@ export class ShareStore extends Store {
docMode?: PublicPageMode, docMode?: PublicPageMode,
signal?: AbortSignal signal?: AbortSignal
) { ) {
await this.gqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: publishPageMutation, query: publishPageMutation,
variables: { variables: {
pageId, pageId,
@@ -55,7 +61,10 @@ export class ShareStore extends Store {
pageId: string, pageId: string,
signal?: AbortSignal signal?: AbortSignal
) { ) {
await this.gqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: revokePublicPageMutation, query: revokePublicPageMutation,
variables: { variables: {
pageId, pageId,

View File

@@ -1,12 +1,12 @@
export { WorkspaceShareSettingService } from './services/share-setting'; export { WorkspaceShareSettingService } from './services/share-setting';
import { GraphQLService } from '@affine/core/modules/cloud';
import { import {
type Framework, type Framework,
WorkspaceScope, WorkspaceScope,
WorkspaceService, WorkspaceService,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { WorkspaceServerService } from '../cloud';
import { WorkspaceShareSetting } from './entities/share-setting'; import { WorkspaceShareSetting } from './entities/share-setting';
import { WorkspaceShareSettingService } from './services/share-setting'; import { WorkspaceShareSettingService } from './services/share-setting';
import { WorkspaceShareSettingStore } from './stores/share-setting'; import { WorkspaceShareSettingStore } from './stores/share-setting';
@@ -15,7 +15,7 @@ export function configureShareSettingModule(framework: Framework) {
framework framework
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(WorkspaceShareSettingService) .service(WorkspaceShareSettingService)
.store(WorkspaceShareSettingStore, [GraphQLService]) .store(WorkspaceShareSettingStore, [WorkspaceServerService])
.entity(WorkspaceShareSetting, [ .entity(WorkspaceShareSetting, [
WorkspaceService, WorkspaceService,
WorkspaceShareSettingStore, WorkspaceShareSettingStore,

View File

@@ -1,4 +1,4 @@
import type { GraphQLService } from '@affine/core/modules/cloud'; import type { WorkspaceServerService } from '@affine/core/modules/cloud';
import { import {
getEnableUrlPreviewQuery, getEnableUrlPreviewQuery,
setEnableUrlPreviewMutation, setEnableUrlPreviewMutation,
@@ -6,7 +6,7 @@ import {
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
export class WorkspaceShareSettingStore extends Store { export class WorkspaceShareSettingStore extends Store {
constructor(private readonly graphqlService: GraphQLService) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
@@ -14,7 +14,10 @@ export class WorkspaceShareSettingStore extends Store {
workspaceId: string, workspaceId: string,
signal?: AbortSignal signal?: AbortSignal
) { ) {
const data = await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
const data = await this.workspaceServerService.server.gql({
query: getEnableUrlPreviewQuery, query: getEnableUrlPreviewQuery,
variables: { variables: {
id: workspaceId, id: workspaceId,
@@ -31,7 +34,10 @@ export class WorkspaceShareSettingStore extends Store {
enableUrlPreview: boolean, enableUrlPreview: boolean,
signal?: AbortSignal signal?: AbortSignal
) { ) {
await this.graphqlService.gql({ if (!this.workspaceServerService.server) {
throw new Error('No Server');
}
await this.workspaceServerService.server.gql({
query: setEnableUrlPreviewMutation, query: setEnableUrlPreviewMutation,
variables: { variables: {
id: workspaceId, id: workspaceId,

View File

@@ -1,8 +1,8 @@
import { type Framework, GlobalContextService } from '@toeverything/infra'; import { type Framework, GlobalContextService } from '@toeverything/infra';
import { AuthService } from '../cloud'; import { ServersService } from '../cloud/services/servers';
import { TelemetryService } from './services/telemetry'; import { TelemetryService } from './services/telemetry';
export function configureTelemetryModule(framework: Framework) { export function configureTelemetryModule(framework: Framework) {
framework.service(TelemetryService, [AuthService, GlobalContextService]); framework.service(TelemetryService, [ServersService, GlobalContextService]);
} }

View File

@@ -2,26 +2,31 @@ import { mixpanel } from '@affine/track';
import type { GlobalContextService } from '@toeverything/infra'; import type { GlobalContextService } from '@toeverything/infra';
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra'; import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
import { import { AccountChanged, type AuthAccountInfo, AuthService } from '../../cloud';
AccountChanged,
type AuthAccountInfo,
type AuthService,
} from '../../cloud';
import { AccountLoggedOut } from '../../cloud/services/auth'; import { AccountLoggedOut } from '../../cloud/services/auth';
import type { ServersService } from '../../cloud/services/servers';
@OnEvent(ApplicationStarted, e => e.onApplicationStart) @OnEvent(ApplicationStarted, e => e.onApplicationStart)
@OnEvent(AccountChanged, e => e.updateIdentity) @OnEvent(AccountChanged, e => e.updateIdentity)
@OnEvent(AccountLoggedOut, e => e.onAccountLoggedOut) @OnEvent(AccountLoggedOut, e => e.onAccountLoggedOut)
export class TelemetryService extends Service { export class TelemetryService extends Service {
private readonly authService;
constructor( constructor(
private readonly auth: AuthService, serversService: ServersService,
private readonly globalContextService: GlobalContextService private readonly globalContextService: GlobalContextService
) { ) {
super(); super();
// TODO: support multiple servers
const affineCloudServer = serversService.server$('affine-cloud').value;
if (!affineCloudServer) {
throw new Error('affine-cloud server not found');
}
this.authService = affineCloudServer.scope.get(AuthService);
} }
onApplicationStart() { onApplicationStart() {
const account = this.auth.session.account$.value; const account = this.authService.session.account$.value;
this.updateIdentity(account); this.updateIdentity(account);
this.registerMiddlewares(); this.registerMiddlewares();
} }

View File

@@ -3,6 +3,7 @@ export { UserspaceService as UserDBService } from './services/userspace';
import type { Framework } from '@toeverything/infra'; import type { Framework } from '@toeverything/infra';
import { AuthService, WebSocketService } from '../cloud'; import { AuthService, WebSocketService } from '../cloud';
import { ServerScope } from '../cloud/scopes/server';
import { DesktopApiService } from '../desktop-api/service/desktop-api'; import { DesktopApiService } from '../desktop-api/service/desktop-api';
import { CurrentUserDB } from './entities/current-user-db'; import { CurrentUserDB } from './entities/current-user-db';
import { UserDB } from './entities/user-db'; import { UserDB } from './entities/user-db';
@@ -15,6 +16,7 @@ import { UserspaceService } from './services/userspace';
export function configureUserspaceModule(framework: Framework) { export function configureUserspaceModule(framework: Framework) {
framework framework
.scope(ServerScope)
.service(UserspaceService) .service(UserspaceService)
.entity(CurrentUserDB, [UserspaceService, AuthService]) .entity(CurrentUserDB, [UserspaceService, AuthService])
.entity(UserDB) .entity(UserDB)

View File

@@ -19,6 +19,7 @@ import {
onComplete, onComplete,
OnEvent, OnEvent,
onStart, onStart,
type Workspace,
type WorkspaceEngineProvider, type WorkspaceEngineProvider,
type WorkspaceFlavourProvider, type WorkspaceFlavourProvider,
type WorkspaceMetadata, type WorkspaceMetadata,
@@ -30,13 +31,16 @@ import { nanoid } from 'nanoid';
import { EMPTY, map, mergeMap } from 'rxjs'; import { EMPTY, map, mergeMap } from 'rxjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { import type { Server } from '../../cloud';
import {
AccountChanged,
AuthService, AuthService,
FetchService, FetchService,
GraphQLService, GraphQLService,
WebSocketService, WebSocketService,
WorkspaceServerService,
} from '../../cloud'; } from '../../cloud';
import { AccountChanged } from '../../cloud'; import type { ServersService } from '../../cloud/services/servers';
import type { WorkspaceEngineStorageProvider } from '../providers/engine'; import type { WorkspaceEngineStorageProvider } from '../providers/engine';
import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel'; import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel';
import { CloudAwarenessConnection } from './engine/awareness-cloud'; import { CloudAwarenessConnection } from './engine/awareness-cloud';
@@ -55,16 +59,30 @@ export class CloudWorkspaceFlavourProviderService
extends Service extends Service
implements WorkspaceFlavourProvider implements WorkspaceFlavourProvider
{ {
private readonly authService: AuthService;
private readonly webSocketService: WebSocketService;
private readonly fetchService: FetchService;
private readonly graphqlService: GraphQLService;
private readonly affineCloudServer: Server;
constructor( constructor(
private readonly globalState: GlobalState, private readonly globalState: GlobalState,
private readonly authService: AuthService,
private readonly storageProvider: WorkspaceEngineStorageProvider, private readonly storageProvider: WorkspaceEngineStorageProvider,
private readonly graphqlService: GraphQLService, serversService: ServersService
private readonly webSocketService: WebSocketService,
private readonly fetchService: FetchService
) { ) {
super(); super();
// TODO: support multiple servers
const affineCloudServer = serversService.server$('affine-cloud').value;
if (!affineCloudServer) {
throw new Error('affine-cloud server not found');
}
this.affineCloudServer = affineCloudServer;
this.authService = affineCloudServer.scope.get(AuthService);
this.webSocketService = affineCloudServer.scope.get(WebSocketService);
this.fetchService = affineCloudServer.scope.get(FetchService);
this.graphqlService = affineCloudServer.scope.get(GraphQLService);
} }
flavour: WorkspaceFlavour = WorkspaceFlavour.AFFINE_CLOUD; flavour: WorkspaceFlavour = WorkspaceFlavour.AFFINE_CLOUD;
async deleteWorkspace(id: string): Promise<void> { async deleteWorkspace(id: string): Promise<void> {
@@ -244,6 +262,7 @@ export class CloudWorkspaceFlavourProviderService
); );
return await cloudBlob.get(blob); return await cloudBlob.get(blob);
} }
getEngineProvider(workspaceId: string): WorkspaceEngineProvider { getEngineProvider(workspaceId: string): WorkspaceEngineProvider {
return { return {
getAwarenessConnections: () => { getAwarenessConnections: () => {
@@ -274,6 +293,13 @@ export class CloudWorkspaceFlavourProviderService
}; };
} }
onWorkspaceInitialized(workspace: Workspace): void {
// bind the workspace to the affine cloud server
workspace.scope
.get(WorkspaceServerService)
.bindServer(this.affineCloudServer);
}
private async getIsOwner(workspaceId: string, signal?: AbortSignal) { private async getIsOwner(workspaceId: string, signal?: AbortSignal) {
return ( return (
await this.graphqlService.gql({ await this.graphqlService.gql({

View File

@@ -1,15 +1,10 @@
import {
AuthService,
FetchService,
GraphQLService,
WebSocketService,
} from '@affine/core/modules/cloud';
import { import {
type Framework, type Framework,
GlobalState, GlobalState,
WorkspaceFlavourProvider, WorkspaceFlavourProvider,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { ServersService } from '../cloud/services/servers';
import { DesktopApiService } from '../desktop-api'; import { DesktopApiService } from '../desktop-api';
import { CloudWorkspaceFlavourProviderService } from './impls/cloud'; import { CloudWorkspaceFlavourProviderService } from './impls/cloud';
import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb'; import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb';
@@ -31,11 +26,8 @@ export function configureBrowserWorkspaceFlavours(framework: Framework) {
]) ])
.service(CloudWorkspaceFlavourProviderService, [ .service(CloudWorkspaceFlavourProviderService, [
GlobalState, GlobalState,
AuthService,
WorkspaceEngineStorageProvider, WorkspaceEngineStorageProvider,
GraphQLService, ServersService,
WebSocketService,
FetchService,
]) ])
.impl(WorkspaceFlavourProvider('CLOUD'), p => .impl(WorkspaceFlavourProvider('CLOUD'), p =>
p.get(CloudWorkspaceFlavourProviderService) p.get(CloudWorkspaceFlavourProviderService)

View File

@@ -998,6 +998,7 @@ query serverConfig {
name name
features features
type type
initialized
credentialsRequirement { credentialsRequirement {
...CredentialsRequirement ...CredentialsRequirement
} }

View File

@@ -8,6 +8,7 @@ query serverConfig {
name name
features features
type type
initialized
credentialsRequirement { credentialsRequirement {
...CredentialsRequirement ...CredentialsRequirement
} }

View File

@@ -2,23 +2,3 @@ export * from './error';
export * from './fetcher'; export * from './fetcher';
export * from './graphql'; export * from './graphql';
export * from './schema'; export * from './schema';
import { setupGlobal } from '@affine/env/global';
import { gqlFetcherFactory } from './fetcher';
setupGlobal();
export function getBaseUrl(): string {
if (BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid) {
return BUILD_CONFIG.serverUrlPrefix;
}
if (typeof window === 'undefined') {
// is nodejs
return '';
}
const { protocol, hostname, port } = window.location;
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
}
export const fetcher = gqlFetcherFactory(getBaseUrl() + '/graphql');

View File

@@ -2175,6 +2175,7 @@ export type ServerConfigQuery = {
name: string; name: string;
features: Array<ServerFeature>; features: Array<ServerFeature>;
type: ServerDeploymentType; type: ServerDeploymentType;
initialized: boolean;
credentialsRequirement: { credentialsRequirement: {
__typename?: 'CredentialsRequirementType'; __typename?: 'CredentialsRequirementType';
password: { password: {