feat(core): desktop multiple server support (#8979)

This commit is contained in:
EYHN
2024-12-03 05:51:09 +00:00
parent af81c95b85
commit 8963826463
137 changed files with 2052 additions and 1694 deletions

View File

@@ -33,7 +33,7 @@ export class EventBus {
});
for (const handler of handlers.values()) {
this.on(handler.event.id, handler.handler);
this.on(handler.event, handler.handler);
}
}
@@ -41,23 +41,25 @@ export class EventBus {
return this.parent?.root ?? this;
}
on<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
if (!this.listeners[id]) {
this.listeners[id] = [];
on<T>(event: FrameworkEvent<T>, listener: (event: T) => void) {
if (!this.listeners[event.id]) {
this.listeners[event.id] = [];
}
this.listeners[id].push(listener);
const off = this.parent?.on(id, listener);
this.listeners[event.id].push(listener);
const off = this.parent?.on(event, listener);
return () => {
this.off(id, listener);
this.off(event, listener);
off?.();
};
}
off<T>(id: string, listener: (event: FrameworkEvent<T>) => void) {
if (!this.listeners[id]) {
off<T>(event: FrameworkEvent<T>, listener: (event: T) => void) {
if (!this.listeners[event.id]) {
return;
}
this.listeners[id] = this.listeners[id].filter(l => l !== listener);
this.listeners[event.id] = this.listeners[event.id].filter(
l => l !== listener
);
}
emit<T>(event: FrameworkEvent<T>, payload: T) {

View File

@@ -216,6 +216,15 @@ export const AFFINE_FLAGS = {
configurable: false,
defaultState: isMobile,
},
enable_multiple_cloud_servers: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name',
description:
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description',
configurable: isDesktopEnvironment,
defaultState: false,
},
} satisfies { [key in string]: FlagInfo };
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;

View File

@@ -8,6 +8,9 @@ export class GlobalContext extends Entity {
memento = new MemoryMemento();
workspaceId = this.define<string>('workspaceId');
workspaceFlavour = this.define<string>('workspaceFlavour');
serverId = this.define<string>('serverId');
/**
* is in doc page

View File

@@ -1,4 +1,3 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { describe, expect, test } from 'vitest';
import { Framework } from '../../../framework';
@@ -22,7 +21,7 @@ describe('Workspace System', () => {
expect(workspaceService.list.workspaces$.value.length).toBe(0);
const workspace = workspaceService.open({
metadata: await workspaceService.create(WorkspaceFlavour.LOCAL),
metadata: await workspaceService.create('local'),
});
expect(workspace.workspace).toBeInstanceOf(Workspace);

View File

@@ -1,21 +1,32 @@
import { combineLatest, map, of, switchMap } from 'rxjs';
import { Entity } from '../../../framework';
import { LiveData } from '../../../livedata';
import type { WorkspaceFlavourProvider } from '../providers/flavour';
import type { WorkspaceMetadata } from '../metadata';
import type { WorkspaceFlavoursService } from '../services/flavours';
export class WorkspaceList extends Entity {
workspaces$ = new LiveData(this.providers.map(p => p.workspaces$))
.map(v => {
return v;
})
.flat()
.map(workspaces => {
return workspaces.flat();
});
isRevalidating$ = new LiveData(
this.providers.map(p => p.isRevalidating$ ?? new LiveData(false))
)
.flat()
.map(isLoadings => isLoadings.some(isLoading => isLoading));
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
this.flavoursService.flavours$.pipe(
switchMap(flavours =>
combineLatest(flavours.map(flavour => flavour.workspaces$)).pipe(
map(workspaces => workspaces.flat())
)
)
),
[]
);
isRevalidating$ = LiveData.from<boolean>(
this.flavoursService.flavours$.pipe(
switchMap(flavours =>
combineLatest(
flavours.map(flavour => flavour.isRevalidating$ ?? of(false))
).pipe(map(isLoadings => isLoadings.some(isLoading => isLoading)))
)
),
false
);
workspace$(id: string) {
return this.workspaces$.map(workspaces =>
@@ -23,12 +34,14 @@ export class WorkspaceList extends Entity {
);
}
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
super();
}
revalidate() {
this.providers.forEach(provider => provider.revalidate?.());
this.flavoursService.flavours$.value.forEach(provider => {
provider.revalidate?.();
});
}
waitForRevalidation(signal?: AbortSignal) {

View File

@@ -12,6 +12,7 @@ import {
} from '../../../livedata';
import type { WorkspaceMetadata } from '../metadata';
import type { WorkspaceFlavourProvider } from '../providers/flavour';
import type { WorkspaceFlavoursService } from '../services/flavours';
import type { WorkspaceProfileCacheStore } from '../stores/profile-cache';
import type { Workspace } from './workspace';
@@ -47,12 +48,14 @@ export class WorkspaceProfile extends Entity<{ metadata: WorkspaceMetadata }> {
constructor(
private readonly cache: WorkspaceProfileCacheStore,
providers: WorkspaceFlavourProvider[]
flavoursService: WorkspaceFlavoursService
) {
super();
this.provider =
providers.find(p => p.flavour === this.props.metadata.flavour) ?? null;
flavoursService.flavours$.value.find(
p => p.flavour === this.props.metadata.flavour
) ?? null;
}
private setProfile(info: WorkspaceProfileInfo) {

View File

@@ -5,7 +5,8 @@ export { getAFFiNEWorkspaceSchema } from './global-schema';
export type { WorkspaceMetadata } from './metadata';
export type { WorkspaceOpenOptions } from './open-options';
export type { WorkspaceEngineProvider } from './providers/flavour';
export { WorkspaceFlavourProvider } from './providers/flavour';
export type { WorkspaceFlavourProvider } from './providers/flavour';
export { WorkspaceFlavoursProvider } from './providers/flavour';
export { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
export { WorkspaceScope } from './scopes/workspace';
export { WorkspaceService } from './services/workspace';
@@ -21,12 +22,13 @@ import {
WorkspaceLocalCacheImpl,
WorkspaceLocalStateImpl,
} from './impls/storage';
import { WorkspaceFlavourProvider } from './providers/flavour';
import { WorkspaceFlavoursProvider } from './providers/flavour';
import { WorkspaceLocalCache, WorkspaceLocalState } from './providers/storage';
import { WorkspaceScope } from './scopes/workspace';
import { WorkspaceDestroyService } from './services/destroy';
import { WorkspaceEngineService } from './services/engine';
import { WorkspaceFactoryService } from './services/factory';
import { WorkspaceFlavoursService } from './services/flavours';
import { WorkspaceListService } from './services/list';
import { WorkspaceProfileService } from './services/profile';
import { WorkspaceRepositoryService } from './services/repo';
@@ -34,12 +36,12 @@ import { WorkspaceTransformService } from './services/transform';
import { WorkspaceService } from './services/workspace';
import { WorkspacesService } from './services/workspaces';
import { WorkspaceProfileCacheStore } from './stores/profile-cache';
import { TestingWorkspaceLocalProvider } from './testing/testing-provider';
import { TestingWorkspaceFlavoursProvider } from './testing/testing-provider';
export function configureWorkspaceModule(framework: Framework) {
framework
.service(WorkspacesService, [
[WorkspaceFlavourProvider],
WorkspaceFlavoursService,
WorkspaceListService,
WorkspaceProfileService,
WorkspaceTransformService,
@@ -47,22 +49,23 @@ export function configureWorkspaceModule(framework: Framework) {
WorkspaceFactoryService,
WorkspaceDestroyService,
])
.service(WorkspaceDestroyService, [[WorkspaceFlavourProvider]])
.service(WorkspaceFlavoursService, [[WorkspaceFlavoursProvider]])
.service(WorkspaceDestroyService, [WorkspaceFlavoursService])
.service(WorkspaceListService)
.entity(WorkspaceList, [[WorkspaceFlavourProvider]])
.entity(WorkspaceList, [WorkspaceFlavoursService])
.service(WorkspaceProfileService)
.store(WorkspaceProfileCacheStore, [GlobalCache])
.entity(WorkspaceProfile, [
WorkspaceProfileCacheStore,
[WorkspaceFlavourProvider],
WorkspaceFlavoursService,
])
.service(WorkspaceFactoryService, [[WorkspaceFlavourProvider]])
.service(WorkspaceFactoryService, [WorkspaceFlavoursService])
.service(WorkspaceTransformService, [
WorkspaceFactoryService,
WorkspaceDestroyService,
])
.service(WorkspaceRepositoryService, [
[WorkspaceFlavourProvider],
WorkspaceFlavoursService,
WorkspaceProfileService,
])
.scope(WorkspaceScope)
@@ -82,8 +85,8 @@ export function configureWorkspaceModule(framework: Framework) {
export function configureTestingWorkspaceProvider(framework: Framework) {
framework.impl(
WorkspaceFlavourProvider('LOCAL'),
TestingWorkspaceLocalProvider,
WorkspaceFlavoursProvider('LOCAL'),
TestingWorkspaceFlavoursProvider,
[GlobalState]
);
}

View File

@@ -1,7 +1,5 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
export type WorkspaceMetadata = {
id: string;
flavour: WorkspaceFlavour;
flavour: string;
initialized?: boolean;
};

View File

@@ -1,4 +1,3 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
import type { DocCollection } from '@blocksuite/affine/store';
import { createIdentifier } from '../../../framework';
@@ -22,7 +21,7 @@ export interface WorkspaceEngineProvider {
}
export interface WorkspaceFlavourProvider {
flavour: WorkspaceFlavour;
flavour: string;
deleteWorkspace(id: string): Promise<void>;
@@ -60,5 +59,9 @@ export interface WorkspaceFlavourProvider {
onWorkspaceInitialized?(workspace: Workspace): void;
}
export const WorkspaceFlavourProvider =
createIdentifier<WorkspaceFlavourProvider>('WorkspaceFlavourProvider');
export interface WorkspaceFlavoursProvider {
workspaceFlavours$: LiveData<WorkspaceFlavourProvider[]>;
}
export const WorkspaceFlavoursProvider =
createIdentifier<WorkspaceFlavoursProvider>('WorkspaceFlavoursProvider');

View File

@@ -1,14 +1,16 @@
import { Service } from '../../../framework';
import type { WorkspaceMetadata } from '../metadata';
import type { WorkspaceFlavourProvider } from '../providers/flavour';
import type { WorkspaceFlavoursService } from './flavours';
export class WorkspaceDestroyService extends Service {
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
super();
}
deleteWorkspace = async (metadata: WorkspaceMetadata) => {
const provider = this.providers.find(p => p.flavour === metadata.flavour);
const provider = this.flavoursService.flavours$.value.find(
p => p.flavour === metadata.flavour
);
if (!provider) {
throw new Error(`Unknown workspace flavour: ${metadata.flavour}`);
}

View File

@@ -1,12 +1,11 @@
import type { WorkspaceFlavour } from '@affine/env/workspace';
import type { DocCollection } from '@blocksuite/affine/store';
import { Service } from '../../../framework';
import type { BlobStorage, DocStorage } from '../../../sync';
import type { WorkspaceFlavourProvider } from '../providers/flavour';
import type { WorkspaceFlavoursService } from './flavours';
export class WorkspaceFactoryService extends Service {
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
constructor(private readonly flavoursService: WorkspaceFlavoursService) {
super();
}
@@ -17,14 +16,16 @@ export class WorkspaceFactoryService extends Service {
* @returns workspace id
*/
create = async (
flavour: WorkspaceFlavour,
flavour: string,
initial: (
docCollection: DocCollection,
blobStorage: BlobStorage,
docStorage: DocStorage
) => Promise<void> = () => Promise.resolve()
) => {
const provider = this.providers.find(x => x.flavour === flavour);
const provider = this.flavoursService.flavours$.value.find(
x => x.flavour === flavour
);
if (!provider) {
throw new Error(`Unknown workspace flavour: ${flavour}`);
}

View File

@@ -0,0 +1,18 @@
import { combineLatest, map } from 'rxjs';
import { Service } from '../../../framework';
import { LiveData } from '../../../livedata';
import type { WorkspaceFlavoursProvider } from '../providers/flavour';
export class WorkspaceFlavoursService extends Service {
constructor(private readonly providers: WorkspaceFlavoursProvider[]) {
super();
}
flavours$ = LiveData.from(
combineLatest(this.providers.map(p => p.workspaceFlavours$)).pipe(
map(flavours => flavours.flat())
),
[]
);
}

View File

@@ -5,11 +5,9 @@ import { ObjectPool } from '../../../utils';
import type { Workspace } from '../entities/workspace';
import { WorkspaceInitialized } from '../events';
import type { WorkspaceOpenOptions } from '../open-options';
import type {
WorkspaceEngineProvider,
WorkspaceFlavourProvider,
} from '../providers/flavour';
import type { WorkspaceEngineProvider } from '../providers/flavour';
import { WorkspaceScope } from '../scopes/workspace';
import type { WorkspaceFlavoursService } from './flavours';
import type { WorkspaceProfileService } from './profile';
import { WorkspaceService } from './workspace';
@@ -17,7 +15,7 @@ const logger = new DebugLogger('affine:workspace-repository');
export class WorkspaceRepositoryService extends Service {
constructor(
private readonly providers: WorkspaceFlavourProvider[],
private readonly flavoursService: WorkspaceFlavoursService,
private readonly profileRepo: WorkspaceProfileService
) {
super();
@@ -83,7 +81,7 @@ export class WorkspaceRepositoryService extends Service {
logger.info(
`open workspace [${openOptions.metadata.flavour}] ${openOptions.metadata.id} `
);
const flavourProvider = this.providers.find(
const flavourProvider = this.flavoursService.flavours$.value.find(
p => p.flavour === openOptions.metadata.flavour
);
const provider =

View File

@@ -1,4 +1,3 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { assertEquals } from '@blocksuite/affine/global/utils';
import { applyUpdate } from 'yjs';
@@ -26,12 +25,12 @@ export class WorkspaceTransformService extends Service {
local: Workspace,
accountId: string
): Promise<WorkspaceMetadata> => {
assertEquals(local.flavour, WorkspaceFlavour.LOCAL);
assertEquals(local.flavour, 'local');
const localDocStorage = local.engine.doc.storage.behavior;
const newMetadata = await this.factory.create(
WorkspaceFlavour.AFFINE_CLOUD,
'affine-cloud',
async (docCollection, blobStorage, docStorage) => {
const rootDocBinary = await localDocStorage.doc.get(
local.docCollection.doc.guid

View File

@@ -1,8 +1,8 @@
import { Service } from '../../../framework';
import type { WorkspaceMetadata } from '..';
import type { WorkspaceFlavourProvider } from '../providers/flavour';
import type { WorkspaceDestroyService } from './destroy';
import type { WorkspaceFactoryService } from './factory';
import type { WorkspaceFlavoursService } from './flavours';
import type { WorkspaceListService } from './list';
import type { WorkspaceProfileService } from './profile';
import type { WorkspaceRepositoryService } from './repo';
@@ -14,7 +14,7 @@ export class WorkspacesService extends Service {
}
constructor(
private readonly providers: WorkspaceFlavourProvider[],
private readonly flavoursService: WorkspaceFlavoursService,
private readonly listService: WorkspaceListService,
private readonly profileRepo: WorkspaceProfileService,
private readonly transform: WorkspaceTransformService,
@@ -46,7 +46,7 @@ export class WorkspacesService extends Service {
}
async getWorkspaceBlob(meta: WorkspaceMetadata, blob: string) {
return await this.providers
return await this.flavoursService.flavours$.value
.find(x => x.flavour === meta.flavour)
?.getWorkspaceBlob(meta.id, blob);
}

View File

@@ -1,4 +1,3 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { DocCollection, nanoid } from '@blocksuite/affine/store';
import { map } from 'rxjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
@@ -19,21 +18,17 @@ import type { WorkspaceMetadata } from '../metadata';
import type {
WorkspaceEngineProvider,
WorkspaceFlavourProvider,
WorkspaceFlavoursProvider,
} from '../providers/flavour';
export class TestingWorkspaceLocalProvider
extends Service
implements WorkspaceFlavourProvider
{
flavour: WorkspaceFlavour = WorkspaceFlavour.LOCAL;
class TestingWorkspaceLocalProvider implements WorkspaceFlavourProvider {
flavour = 'local';
store = wrapMemento(this.globalStore, 'testing/');
workspaceListStore = wrapMemento(this.store, 'workspaces/');
docStorage = new MemoryDocStorage(wrapMemento(this.store, 'docs/'));
constructor(private readonly globalStore: GlobalState) {
super();
}
constructor(private readonly globalStore: GlobalState) {}
async deleteWorkspace(id: string): Promise<void> {
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
@@ -48,7 +43,7 @@ export class TestingWorkspaceLocalProvider
) => Promise<void>
): Promise<WorkspaceMetadata> {
const id = nanoid();
const meta = { id, flavour: WorkspaceFlavour.LOCAL };
const meta = { id, flavour: 'local' };
const blobStorage = new MemoryBlobStorage(
wrapMemento(this.store, id + '/blobs/')
@@ -75,7 +70,7 @@ export class TestingWorkspaceLocalProvider
const list = this.workspaceListStore.get<WorkspaceMetadata[]>('list') ?? [];
this.workspaceListStore.set('list', [...list, meta]);
return { id, flavour: WorkspaceFlavour.LOCAL };
return { id, flavour: 'local' };
}
workspaces$ = LiveData.from<WorkspaceMetadata[]>(
this.workspaceListStore
@@ -132,3 +127,15 @@ export class TestingWorkspaceLocalProvider
};
}
}
export class TestingWorkspaceFlavoursProvider
extends Service
implements WorkspaceFlavoursProvider
{
constructor(private readonly globalStore: GlobalState) {
super();
}
workspaceFlavours$ = new LiveData<WorkspaceFlavourProvider[]>([
new TestingWorkspaceLocalProvider(this.globalStore),
]);
}