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

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

View File

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

View File

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

View File

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

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 { FetchService } from './fetch';
import type { ServerConfigService } from './server-config';
import type { ServerService } from './server';
export class CaptchaService extends Service {
needCaptcha$ = this.serverConfigService.serverConfig.features$.map(
needCaptcha$ = this.serverService.server.features$.map(
r => r?.captcha || false
);
challenge$ = new LiveData<string | undefined>(undefined);
@@ -23,7 +23,7 @@ export class CaptchaService extends Service {
error$ = new LiveData<any | undefined>(undefined);
constructor(
private readonly serverConfigService: ServerConfigService,
private readonly serverService: ServerService,
private readonly fetchService: FetchService,
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 { BackendError, NetworkError } from '../error';
import type { FetchProvider } from '../provider/fetch';
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}` : ''}`;
}
import type { RawFetchProvider } from '../provider/fetch';
import type { ServerService } from './server';
const logger = new DebugLogger('affine:fetch');
export type FetchInit = RequestInit & { timeout?: number };
export class FetchService extends Service {
constructor(private readonly fetchProvider: FetchProvider) {
constructor(
private readonly fetchProvider: RawFetchProvider,
private readonly serverService: ServerService
) {
super();
}
rxFetch = (
@@ -55,7 +51,7 @@ export class FetchService extends Service {
}, timeout);
const res = await this.fetchProvider
.fetch(new URL(input, getAffineCloudBaseUrl()), {
.fetch(new URL(input, this.serverService.server.serverMetadata.baseUrl), {
...init,
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 type { WebSocketAuthProvider } from '../provider/websocket-auth';
import { getAffineCloudBaseUrl } from '../services/fetch';
import type { AuthService } from './auth';
import { AccountChanged } from './auth';
import type { ServerService } from './server';
@OnEvent(AccountChanged, e => e.update)
@OnEvent(ApplicationStarted, e => e.update)
export class WebSocketService extends Service {
ioManager: Manager = new Manager(`${getAffineCloudBaseUrl()}/`, {
ioManager: Manager = new Manager(`${this.serverService.server.baseUrl}/`, {
autoConnect: false,
transports: ['websocket'],
secure: location.protocol === 'https:',
@@ -18,7 +18,7 @@ export class WebSocketService extends Service {
auth: this.webSocketAuthProvider
? cb => {
this.webSocketAuthProvider
?.getAuthToken(`${getAffineCloudBaseUrl()}/`)
?.getAuthToken(`${this.serverService.server.baseUrl}/`)
.then(v => {
cb(v ?? {});
})
@@ -31,6 +31,7 @@ export class WebSocketService extends Service {
refCount = 0;
constructor(
private readonly serverService: ServerService,
private readonly authService: AuthService,
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 {
type OauthProvidersQuery,
oauthProvidersQuery,
type ServerConfigQuery,
serverConfigQuery,
ServerFeature,
} from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { ServerConfigType } from '../entities/server-config';
import type { GraphQLService } from '../services/graphql';
export type ServerConfigType = ServerConfigQuery['serverConfig'] &
OauthProvidersQuery['serverConfig'];
export class ServerConfigStore extends Store {
constructor(private readonly gqlService: GraphQLService) {
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 { SubscriptionType } from '../entities/subscription';
import { getAffineCloudBaseUrl } from '../services/fetch';
import type { GraphQLService } from '../services/graphql';
import type { ServerService } from '../services/server';
const SUBSCRIPTION_CACHE_KEY = 'subscription:';
const getDefaultSubscriptionSuccessCallbackLink = (
baseUrl: string,
plan: SubscriptionPlan | null,
scheme?: string
) => {
const path =
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
const urlString = getAffineCloudBaseUrl() + path;
const urlString = baseUrl + path;
const url = new URL(urlString);
if (scheme) {
url.searchParams.set('scheme', scheme);
@@ -39,7 +40,8 @@ export class SubscriptionStore extends Store {
constructor(
private readonly gqlService: GraphQLService,
private readonly globalCache: GlobalCache,
private readonly urlService: UrlService
private readonly urlService: UrlService,
private readonly serverService: ServerService
) {
super();
}
@@ -132,6 +134,7 @@ export class SubscriptionStore extends Store {
successCallbackLink:
input.successCallbackLink ||
getDefaultSubscriptionSuccessCallbackLink(
this.serverService.server.baseUrl,
input.plan,
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;
}