mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 19:46:32 +08:00
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:
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useNavigationType,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../cloud';
|
||||
import { AuthService, ServersService } from '../../cloud';
|
||||
import type { DesktopApi } from '../entities/electron-api';
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.setupStartListener)
|
||||
@@ -140,10 +140,19 @@ export class DesktopApiService extends Service {
|
||||
private setupAuthRequestEvent() {
|
||||
this.events.ui.onAuthenticationRequest(({ method, payload }) => {
|
||||
(async () => {
|
||||
const authService = this.framework.get(AuthService);
|
||||
if (!(await this.api.handler.ui.isActiveTab())) {
|
||||
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) {
|
||||
case 'magic-link': {
|
||||
const { email, token } = payload;
|
||||
|
||||
@@ -2,21 +2,29 @@ import type { GlobalState } from '@toeverything/infra';
|
||||
import { Service } from '@toeverything/infra';
|
||||
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';
|
||||
|
||||
export class CurrentUserDBEditorSettingProvider
|
||||
extends Service
|
||||
implements EditorSettingProvider
|
||||
{
|
||||
currentUserDB$ = this.userDBService.currentUserDB.db$;
|
||||
private readonly currentUserDB$;
|
||||
fallback = new GlobalStateEditorSettingProvider(this.globalState);
|
||||
|
||||
constructor(
|
||||
public readonly userDBService: UserDBService,
|
||||
public readonly serversService: ServersService,
|
||||
public readonly globalState: GlobalState
|
||||
) {
|
||||
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 {
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
GlobalStateService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { ServersService } from '../cloud';
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { I18n } from '../i18n';
|
||||
import { UserDBService } from '../userspace';
|
||||
import { EditorSetting } from './entities/editor-setting';
|
||||
import { CurrentUserDBEditorSettingProvider } from './impls/user-db';
|
||||
import { EditorSettingProvider } from './provider/editor-setting-provider';
|
||||
@@ -21,7 +21,7 @@ export function configureEditorSettingModule(framework: Framework) {
|
||||
.service(EditorSettingService)
|
||||
.entity(EditorSetting, [EditorSettingProvider])
|
||||
.impl(EditorSettingProvider, CurrentUserDBEditorSettingProvider, [
|
||||
UserDBService,
|
||||
ServersService,
|
||||
GlobalState,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { AuthService } from '../cloud';
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { FavoriteList } from './entities/favorite-list';
|
||||
import { FavoriteService } from './services/favorite';
|
||||
import {
|
||||
@@ -27,7 +27,11 @@ export function configureFavoriteModule(framework: Framework) {
|
||||
.scope(WorkspaceScope)
|
||||
.service(FavoriteService)
|
||||
.entity(FavoriteList, [FavoriteStore])
|
||||
.store(FavoriteStore, [AuthService, WorkspaceDBService, WorkspaceService])
|
||||
.store(FavoriteStore, [
|
||||
WorkspaceDBService,
|
||||
WorkspaceService,
|
||||
WorkspaceServerService,
|
||||
])
|
||||
.service(MigrationFavoriteItemsAdapter, [WorkspaceService])
|
||||
.service(CompatibleFavoriteItemsAdapter, [FavoriteService]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { WorkspaceDBService, WorkspaceService } from '@toeverything/infra';
|
||||
import { LiveData, Store } from '@toeverything/infra';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import type { AuthService } from '../../cloud';
|
||||
import { AuthService, type WorkspaceServerService } from '../../cloud';
|
||||
import type { FavoriteSupportType } from '../constant';
|
||||
import { isFavoriteSupportType } from '../constant';
|
||||
|
||||
@@ -14,27 +14,31 @@ export interface FavoriteRecord {
|
||||
}
|
||||
|
||||
export class FavoriteStore extends Store {
|
||||
authService = this.workspaceServerService.server?.scope.get(AuthService);
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly workspaceDBService: WorkspaceDBService,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly workspaceServerService: WorkspaceServerService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private get userdataDB$() {
|
||||
return this.authService.session.account$.map(account => {
|
||||
// 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
|
||||
if (
|
||||
this.workspaceService.workspace.meta.flavour ===
|
||||
WorkspaceFlavour.LOCAL ||
|
||||
!account
|
||||
) {
|
||||
return this.workspaceDBService.userdataDB('__local__');
|
||||
}
|
||||
return this.workspaceDBService.userdataDB(account.id);
|
||||
});
|
||||
// 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
|
||||
if (
|
||||
this.workspaceService.workspace.meta.flavour === WorkspaceFlavour.LOCAL ||
|
||||
!this.authService
|
||||
) {
|
||||
return new LiveData(this.workspaceDBService.userdataDB('__local__'));
|
||||
} else {
|
||||
return this.authService.session.account$.map(account => {
|
||||
if (!account) {
|
||||
return this.workspaceDBService.userdataDB('__local__');
|
||||
}
|
||||
return this.workspaceDBService.userdataDB(account.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watchIsLoading() {
|
||||
|
||||
@@ -2,7 +2,6 @@ export type { Member } from './entities/members';
|
||||
export { WorkspaceMembersService } from './services/members';
|
||||
export { WorkspacePermissionService } from './services/permission';
|
||||
|
||||
import { GraphQLService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type Framework,
|
||||
WorkspaceScope,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { WorkspaceMembers } from './entities/members';
|
||||
import { WorkspacePermission } from './entities/permission';
|
||||
import { WorkspaceMembersService } from './services/members';
|
||||
@@ -25,9 +25,9 @@ export function configurePermissionsModule(framework: Framework) {
|
||||
WorkspacesService,
|
||||
WorkspacePermissionStore,
|
||||
])
|
||||
.store(WorkspacePermissionStore, [GraphQLService])
|
||||
.store(WorkspacePermissionStore, [WorkspaceServerService])
|
||||
.entity(WorkspacePermission, [WorkspaceService, WorkspacePermissionStore])
|
||||
.service(WorkspaceMembersService)
|
||||
.store(WorkspaceMembersStore, [GraphQLService])
|
||||
.store(WorkspaceMembersStore, [WorkspaceServerService])
|
||||
.entity(WorkspaceMembers, [WorkspaceMembersStore, WorkspaceService]);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getMembersByWorkspaceIdQuery } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../../cloud';
|
||||
import type { WorkspaceServerService } from '../../cloud';
|
||||
|
||||
export class WorkspaceMembersStore extends Store {
|
||||
constructor(private readonly graphqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ export class WorkspaceMembersStore extends Store {
|
||||
take: number,
|
||||
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,
|
||||
variables: {
|
||||
workspaceId,
|
||||
|
||||
@@ -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 { Store } from '@toeverything/infra';
|
||||
|
||||
export class WorkspacePermissionStore extends Store {
|
||||
constructor(private readonly graphqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
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,
|
||||
variables: {
|
||||
workspaceId,
|
||||
@@ -23,7 +26,10 @@ export class WorkspacePermissionStore extends Store {
|
||||
* @param workspaceName for send email
|
||||
*/
|
||||
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,
|
||||
variables: {
|
||||
workspaceId,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export { WorkspaceQuotaService } from './services/quota';
|
||||
|
||||
import { GraphQLService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type Framework,
|
||||
WorkspaceScope,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { WorkspaceQuota } from './entities/quota';
|
||||
import { WorkspaceQuotaService } from './services/quota';
|
||||
import { WorkspaceQuotaStore } from './stores/quota';
|
||||
@@ -15,6 +15,6 @@ export function configureQuotaModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceQuotaService)
|
||||
.store(WorkspaceQuotaStore, [GraphQLService])
|
||||
.store(WorkspaceQuotaStore, [WorkspaceServerService])
|
||||
.entity(WorkspaceQuota, [WorkspaceService, WorkspaceQuotaStore]);
|
||||
}
|
||||
|
||||
@@ -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 { Store } from '@toeverything/infra';
|
||||
|
||||
export class WorkspaceQuotaStore extends Store {
|
||||
constructor(private readonly graphqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
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,
|
||||
variables: {
|
||||
id: workspaceId,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { FetchService, GraphQLService } from '../cloud';
|
||||
import { RawFetchProvider, WorkspaceServerService } from '../cloud';
|
||||
import { ShareDocsList } from './entities/share-docs-list';
|
||||
import { ShareInfo } from './entities/share-info';
|
||||
import { ShareReader } from './entities/share-reader';
|
||||
@@ -27,10 +27,10 @@ export function configureShareDocsModule(framework: Framework) {
|
||||
framework
|
||||
.service(ShareReaderService)
|
||||
.entity(ShareReader, [ShareReaderStore])
|
||||
.store(ShareReaderStore, [FetchService])
|
||||
.store(ShareReaderStore, [RawFetchProvider])
|
||||
.scope(WorkspaceScope)
|
||||
.service(ShareDocsListService, [WorkspaceService])
|
||||
.store(ShareDocsStore, [GraphQLService])
|
||||
.store(ShareDocsStore, [WorkspaceServerService])
|
||||
.entity(ShareDocsList, [
|
||||
WorkspaceService,
|
||||
ShareDocsStore,
|
||||
@@ -39,5 +39,5 @@ export function configureShareDocsModule(framework: Framework) {
|
||||
.scope(DocScope)
|
||||
.service(ShareInfoService)
|
||||
.entity(ShareInfo, [WorkspaceService, DocService, ShareStore])
|
||||
.store(ShareStore, [GraphQLService]);
|
||||
.store(ShareStore, [WorkspaceServerService]);
|
||||
}
|
||||
|
||||
@@ -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 { Store } from '@toeverything/infra';
|
||||
|
||||
export class ShareDocsStore extends Store {
|
||||
constructor(private readonly graphqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
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,
|
||||
variables: {
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@@ -2,16 +2,20 @@ import { ErrorNames, UserFriendlyError } from '@affine/graphql';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
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 {
|
||||
constructor(private readonly fetchService: FetchService) {
|
||||
constructor(private readonly rawFetch?: RawFetchProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
async loadShare(workspaceId: string, docId: string) {
|
||||
if (!this.rawFetch) {
|
||||
throw new Error('No Fetch Service');
|
||||
}
|
||||
try {
|
||||
const docResponse = await this.fetchService.fetch(
|
||||
const docResponse = await this.rawFetch.fetch(
|
||||
`/api/workspaces/${workspaceId}/docs/${docId}`
|
||||
);
|
||||
const publishMode = docResponse.headers.get(
|
||||
@@ -19,7 +23,7 @@ export class ShareReaderStore extends Store {
|
||||
) as DocMode | null;
|
||||
const docBinary = await docResponse.arrayBuffer();
|
||||
|
||||
const workspaceResponse = await this.fetchService.fetch(
|
||||
const workspaceResponse = await this.rawFetch.fetch(
|
||||
`/api/workspaces/${workspaceId}/docs/${workspaceId}`
|
||||
);
|
||||
const workspaceBinary = await workspaceResponse.arrayBuffer();
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../../cloud';
|
||||
import type { WorkspaceServerService } from '../../cloud';
|
||||
|
||||
export class ShareStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ export class ShareStore extends Store {
|
||||
docId: string,
|
||||
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,
|
||||
variables: {
|
||||
pageId: docId,
|
||||
@@ -37,7 +40,10 @@ export class ShareStore extends Store {
|
||||
docMode?: PublicPageMode,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
await this.gqlService.gql({
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
await this.workspaceServerService.server.gql({
|
||||
query: publishPageMutation,
|
||||
variables: {
|
||||
pageId,
|
||||
@@ -55,7 +61,10 @@ export class ShareStore extends Store {
|
||||
pageId: string,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
await this.gqlService.gql({
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
await this.workspaceServerService.server.gql({
|
||||
query: revokePublicPageMutation,
|
||||
variables: {
|
||||
pageId,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export { WorkspaceShareSettingService } from './services/share-setting';
|
||||
|
||||
import { GraphQLService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type Framework,
|
||||
WorkspaceScope,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { WorkspaceServerService } from '../cloud';
|
||||
import { WorkspaceShareSetting } from './entities/share-setting';
|
||||
import { WorkspaceShareSettingService } from './services/share-setting';
|
||||
import { WorkspaceShareSettingStore } from './stores/share-setting';
|
||||
@@ -15,7 +15,7 @@ export function configureShareSettingModule(framework: Framework) {
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
.service(WorkspaceShareSettingService)
|
||||
.store(WorkspaceShareSettingStore, [GraphQLService])
|
||||
.store(WorkspaceShareSettingStore, [WorkspaceServerService])
|
||||
.entity(WorkspaceShareSetting, [
|
||||
WorkspaceService,
|
||||
WorkspaceShareSettingStore,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GraphQLService } from '@affine/core/modules/cloud';
|
||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
getEnableUrlPreviewQuery,
|
||||
setEnableUrlPreviewMutation,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
export class WorkspaceShareSettingStore extends Store {
|
||||
constructor(private readonly graphqlService: GraphQLService) {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,10 @@ export class WorkspaceShareSettingStore extends Store {
|
||||
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: getEnableUrlPreviewQuery,
|
||||
variables: {
|
||||
id: workspaceId,
|
||||
@@ -31,7 +34,10 @@ export class WorkspaceShareSettingStore extends Store {
|
||||
enableUrlPreview: boolean,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
await this.graphqlService.gql({
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
await this.workspaceServerService.server.gql({
|
||||
query: setEnableUrlPreviewMutation,
|
||||
variables: {
|
||||
id: workspaceId,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type Framework, GlobalContextService } from '@toeverything/infra';
|
||||
|
||||
import { AuthService } from '../cloud';
|
||||
import { ServersService } from '../cloud/services/servers';
|
||||
import { TelemetryService } from './services/telemetry';
|
||||
|
||||
export function configureTelemetryModule(framework: Framework) {
|
||||
framework.service(TelemetryService, [AuthService, GlobalContextService]);
|
||||
framework.service(TelemetryService, [ServersService, GlobalContextService]);
|
||||
}
|
||||
|
||||
@@ -2,26 +2,31 @@ import { mixpanel } from '@affine/track';
|
||||
import type { GlobalContextService } from '@toeverything/infra';
|
||||
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
|
||||
|
||||
import {
|
||||
AccountChanged,
|
||||
type AuthAccountInfo,
|
||||
type AuthService,
|
||||
} from '../../cloud';
|
||||
import { AccountChanged, type AuthAccountInfo, AuthService } from '../../cloud';
|
||||
import { AccountLoggedOut } from '../../cloud/services/auth';
|
||||
import type { ServersService } from '../../cloud/services/servers';
|
||||
|
||||
@OnEvent(ApplicationStarted, e => e.onApplicationStart)
|
||||
@OnEvent(AccountChanged, e => e.updateIdentity)
|
||||
@OnEvent(AccountLoggedOut, e => e.onAccountLoggedOut)
|
||||
export class TelemetryService extends Service {
|
||||
private readonly authService;
|
||||
constructor(
|
||||
private readonly auth: AuthService,
|
||||
serversService: ServersService,
|
||||
private readonly globalContextService: GlobalContextService
|
||||
) {
|
||||
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() {
|
||||
const account = this.auth.session.account$.value;
|
||||
const account = this.authService.session.account$.value;
|
||||
this.updateIdentity(account);
|
||||
this.registerMiddlewares();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export { UserspaceService as UserDBService } from './services/userspace';
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { AuthService, WebSocketService } from '../cloud';
|
||||
import { ServerScope } from '../cloud/scopes/server';
|
||||
import { DesktopApiService } from '../desktop-api/service/desktop-api';
|
||||
import { CurrentUserDB } from './entities/current-user-db';
|
||||
import { UserDB } from './entities/user-db';
|
||||
@@ -15,6 +16,7 @@ import { UserspaceService } from './services/userspace';
|
||||
|
||||
export function configureUserspaceModule(framework: Framework) {
|
||||
framework
|
||||
.scope(ServerScope)
|
||||
.service(UserspaceService)
|
||||
.entity(CurrentUserDB, [UserspaceService, AuthService])
|
||||
.entity(UserDB)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
onComplete,
|
||||
OnEvent,
|
||||
onStart,
|
||||
type Workspace,
|
||||
type WorkspaceEngineProvider,
|
||||
type WorkspaceFlavourProvider,
|
||||
type WorkspaceMetadata,
|
||||
@@ -30,13 +31,16 @@ import { nanoid } from 'nanoid';
|
||||
import { EMPTY, map, mergeMap } from 'rxjs';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import type {
|
||||
import type { Server } from '../../cloud';
|
||||
import {
|
||||
AccountChanged,
|
||||
AuthService,
|
||||
FetchService,
|
||||
GraphQLService,
|
||||
WebSocketService,
|
||||
WorkspaceServerService,
|
||||
} from '../../cloud';
|
||||
import { AccountChanged } from '../../cloud';
|
||||
import type { ServersService } from '../../cloud/services/servers';
|
||||
import type { WorkspaceEngineStorageProvider } from '../providers/engine';
|
||||
import { BroadcastChannelAwarenessConnection } from './engine/awareness-broadcast-channel';
|
||||
import { CloudAwarenessConnection } from './engine/awareness-cloud';
|
||||
@@ -55,16 +59,30 @@ export class CloudWorkspaceFlavourProviderService
|
||||
extends Service
|
||||
implements WorkspaceFlavourProvider
|
||||
{
|
||||
private readonly authService: AuthService;
|
||||
private readonly webSocketService: WebSocketService;
|
||||
private readonly fetchService: FetchService;
|
||||
private readonly graphqlService: GraphQLService;
|
||||
private readonly affineCloudServer: Server;
|
||||
|
||||
constructor(
|
||||
private readonly globalState: GlobalState,
|
||||
private readonly authService: AuthService,
|
||||
private readonly storageProvider: WorkspaceEngineStorageProvider,
|
||||
private readonly graphqlService: GraphQLService,
|
||||
private readonly webSocketService: WebSocketService,
|
||||
private readonly fetchService: FetchService
|
||||
serversService: ServersService
|
||||
) {
|
||||
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;
|
||||
|
||||
async deleteWorkspace(id: string): Promise<void> {
|
||||
@@ -244,6 +262,7 @@ export class CloudWorkspaceFlavourProviderService
|
||||
);
|
||||
return await cloudBlob.get(blob);
|
||||
}
|
||||
|
||||
getEngineProvider(workspaceId: string): WorkspaceEngineProvider {
|
||||
return {
|
||||
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) {
|
||||
return (
|
||||
await this.graphqlService.gql({
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
AuthService,
|
||||
FetchService,
|
||||
GraphQLService,
|
||||
WebSocketService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import {
|
||||
type Framework,
|
||||
GlobalState,
|
||||
WorkspaceFlavourProvider,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { ServersService } from '../cloud/services/servers';
|
||||
import { DesktopApiService } from '../desktop-api';
|
||||
import { CloudWorkspaceFlavourProviderService } from './impls/cloud';
|
||||
import { IndexedDBBlobStorage } from './impls/engine/blob-indexeddb';
|
||||
@@ -31,11 +26,8 @@ export function configureBrowserWorkspaceFlavours(framework: Framework) {
|
||||
])
|
||||
.service(CloudWorkspaceFlavourProviderService, [
|
||||
GlobalState,
|
||||
AuthService,
|
||||
WorkspaceEngineStorageProvider,
|
||||
GraphQLService,
|
||||
WebSocketService,
|
||||
FetchService,
|
||||
ServersService,
|
||||
])
|
||||
.impl(WorkspaceFlavourProvider('CLOUD'), p =>
|
||||
p.get(CloudWorkspaceFlavourProviderService)
|
||||
|
||||
Reference in New Issue
Block a user