refactor(infra): migrate to new infra (#5565)

This commit is contained in:
EYHN
2024-01-30 07:16:39 +00:00
parent 1e3499c323
commit 329fc19852
170 changed files with 2007 additions and 4354 deletions

View File

@@ -0,0 +1 @@
export * from './service';

View File

@@ -0,0 +1,164 @@
import type {
Collection,
DeleteCollectionInfo,
DeletedCollection,
} from '@affine/env/filter';
import type { Workspace } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
import { Observable } from 'rxjs';
import { Array as YArray } from 'yjs';
const SETTING_KEY = 'setting';
const COLLECTIONS_KEY = 'collections';
const COLLECTIONS_TRASH_KEY = 'collections_trash';
export class CollectionService {
constructor(private readonly workspace: Workspace) {}
private get doc() {
return this.workspace.blockSuiteWorkspace.doc;
}
private get setting() {
return this.workspace.blockSuiteWorkspace.doc.getMap(SETTING_KEY);
}
private get collectionsYArray(): YArray<Collection> | undefined {
return this.setting.get(COLLECTIONS_KEY) as YArray<Collection>;
}
private get collectionsTrashYArray(): YArray<DeletedCollection> | undefined {
return this.setting.get(COLLECTIONS_TRASH_KEY) as YArray<DeletedCollection>;
}
readonly collections = LiveData.from(
new Observable<Collection[]>(subscriber => {
subscriber.next(this.collectionsYArray?.toArray() ?? []);
const fn = () => {
subscriber.next(this.collectionsYArray?.toArray() ?? []);
};
this.setting.observeDeep(fn);
return () => {
this.setting.unobserveDeep(fn);
};
}),
[]
);
readonly collectionsTrash = LiveData.from(
new Observable<DeletedCollection[]>(subscriber => {
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
const fn = () => {
subscriber.next(this.collectionsTrashYArray?.toArray() ?? []);
};
this.setting.observeDeep(fn);
return () => {
this.setting.unobserveDeep(fn);
};
}),
[]
);
addCollection(...collections: Collection[]) {
if (!this.setting.has(COLLECTIONS_KEY)) {
this.setting.set(COLLECTIONS_KEY, new YArray());
}
this.doc.transact(() => {
this.collectionsYArray?.insert(0, collections);
});
}
updateCollection(id: string, updater: (value: Collection) => Collection) {
if (this.collectionsYArray) {
updateFirstOfYArray(
this.collectionsYArray,
v => v.id === id,
v => {
return updater(v);
}
);
}
}
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
const collectionsYArray = this.collectionsYArray;
if (!collectionsYArray) {
return;
}
const set = new Set(ids);
this.workspace.blockSuiteWorkspace.doc.transact(() => {
const indexList: number[] = [];
const list: Collection[] = [];
collectionsYArray.forEach((collection, i) => {
if (set.has(collection.id)) {
set.delete(collection.id);
indexList.unshift(i);
list.push(JSON.parse(JSON.stringify(collection)));
}
});
indexList.forEach(i => {
collectionsYArray.delete(i);
});
if (!this.collectionsTrashYArray) {
this.setting.set(COLLECTIONS_TRASH_KEY, new YArray());
}
const collectionsTrashYArray = this.collectionsTrashYArray;
if (!collectionsTrashYArray) {
return;
}
collectionsTrashYArray.insert(
0,
list.map(collection => ({
userId: info?.userId,
userName: info ? info.userName : 'Local User',
collection,
}))
);
if (collectionsTrashYArray.length > 10) {
collectionsTrashYArray.delete(10, collectionsTrashYArray.length - 10);
}
});
}
private deletePagesFromCollection(
collection: Collection,
idSet: Set<string>
) {
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
if (newAllowList.length !== collection.allowList.length) {
this.updateCollection(collection.id, old => {
return {
...old,
allowList: newAllowList,
};
});
}
}
deletePagesFromCollections(ids: string[]) {
const idSet = new Set(ids);
this.doc.transact(() => {
this.collections.value.forEach(collection => {
this.deletePagesFromCollection(collection, idSet);
});
});
}
}
const updateFirstOfYArray = <T>(
array: YArray<T>,
p: (value: T) => boolean,
update: (value: T) => T
) => {
array.doc?.transact(() => {
for (let i = 0; i < array.length; i++) {
const ele = array.get(i);
if (p(ele)) {
array.delete(i);
array.insert(i, [update(ele)]);
return;
}
}
});
};

View File

@@ -0,0 +1,42 @@
import type { Page } from '@toeverything/infra';
import {
LiveData,
ServiceCollection,
type ServiceProvider,
ServiceProviderContext,
useLiveData,
useService,
useServiceOptional,
} from '@toeverything/infra';
import type React from 'react';
import { CurrentPageService } from '../../page';
import { CurrentWorkspaceService } from '../../workspace';
export const GlobalScopeProvider: React.FC<
React.PropsWithChildren<{ provider: ServiceProvider }>
> = ({ provider: rootProvider, children }) => {
const currentWorkspaceService = useService(CurrentWorkspaceService, {
provider: rootProvider,
});
const workspaceProvider = useLiveData(
currentWorkspaceService.currentWorkspace
)?.services;
const currentPageService = useServiceOptional(CurrentPageService, {
provider: workspaceProvider ?? ServiceCollection.EMPTY.provider(),
});
const pageProvider = useLiveData(
currentPageService?.currentPage ?? new LiveData<Page | null>(null)
)?.services;
return (
<ServiceProviderContext.Provider
value={pageProvider ?? workspaceProvider ?? rootProvider}
>
{children}
</ServiceProviderContext.Provider>
);
};

View File

@@ -0,0 +1,32 @@
import type { GlobalCache } from '@toeverything/infra';
import { Observable } from 'rxjs';
export class LocalStorageGlobalCache implements GlobalCache {
prefix = 'cache:';
get<T>(key: string): T | null {
const json = localStorage.getItem(this.prefix + key);
return json ? JSON.parse(json) : null;
}
watch<T>(key: string): Observable<T | null> {
return new Observable<T | null>(subscriber => {
const json = localStorage.getItem(this.prefix + key);
const first = json ? JSON.parse(json) : null;
subscriber.next(first);
const channel = new BroadcastChannel(this.prefix + key);
channel.addEventListener('message', event => {
subscriber.next(event.data);
});
return () => {
channel.close();
};
});
}
set<T>(key: string, value: T | null): void {
localStorage.setItem(this.prefix + key, JSON.stringify(value));
const channel = new BroadcastChannel(this.prefix + key);
channel.postMessage(value);
channel.close();
}
}

View File

@@ -0,0 +1,24 @@
import type { Page } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
/**
* service to manage current page
*/
export class CurrentPageService {
currentPage = new LiveData<Page | null>(null);
/**
* open page, current page will be set to the page
* @param page
*/
openPage(page: Page) {
this.currentPage.next(page);
}
/**
* close current page, current page will be null
*/
closePage() {
this.currentPage.next(null);
}
}

View File

@@ -0,0 +1 @@
export * from './current-page';

View File

@@ -0,0 +1,27 @@
import {
GlobalCache,
type ServiceCollection,
Workspace,
WorkspaceScope,
} from '@toeverything/infra';
import { CollectionService } from './collection';
import { LocalStorageGlobalCache } from './infra-web/storage';
import { CurrentPageService } from './page';
import {
CurrentWorkspaceService,
WorkspacePropertiesAdapter,
} from './workspace';
export function configureBusinessServices(services: ServiceCollection) {
services.add(CurrentWorkspaceService);
services
.scope(WorkspaceScope)
.add(CurrentPageService)
.add(WorkspacePropertiesAdapter, [Workspace])
.add(CollectionService, [Workspace]);
}
export function configureWebInfraServices(services: ServiceCollection) {
services.addImpl(GlobalCache, LocalStorageGlobalCache);
}

View File

@@ -1,57 +0,0 @@
import { DebugLogger } from '@affine/debug';
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
import { workspaceManager } from '@affine/workspace-impl';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { Observable } from 'rxjs';
const logger = new DebugLogger('affine:workspace:atom');
// readonly atom for workspace manager, currently only one workspace manager is supported
export const workspaceManagerAtom = atom(() => workspaceManager);
// workspace metadata list, use rxjs to push updates
export const workspaceListAtom = atomWithObservable<WorkspaceMetadata[]>(
get => {
const workspaceManager = get(workspaceManagerAtom);
return new Observable<WorkspaceMetadata[]>(subscriber => {
subscriber.next(workspaceManager.list.workspaceList);
return workspaceManager.list.onStatusChanged.on(status => {
subscriber.next(status.workspaceList);
}).dispose;
});
},
{
initialValue: [],
}
);
// workspace list loading status, if is false, UI can display not found page when workspace id is not in the list.
export const workspaceListLoadingStatusAtom = atomWithObservable<boolean>(
get => {
const workspaceManager = get(workspaceManagerAtom);
return new Observable<boolean>(subscriber => {
subscriber.next(workspaceManager.list.status.loading);
return workspaceManager.list.onStatusChanged.on(status => {
subscriber.next(status.loading);
}).dispose;
});
},
{
initialValue: true,
}
);
// current workspace
export const currentWorkspaceAtom = atom<Workspace | null>(null);
// wait for current workspace, if current workspace is null, it will suspend
export const waitForCurrentWorkspaceAtom = atom(get => {
const currentWorkspace = get(currentWorkspaceAtom);
if (!currentWorkspace) {
// suspended
logger.info('suspended for current workspace');
return new Promise<Workspace>(_ => {});
}
return currentWorkspace;
});

View File

@@ -0,0 +1,24 @@
import type { Workspace } from '@toeverything/infra';
import { LiveData } from '@toeverything/infra/livedata';
/**
* service to manage current workspace
*/
export class CurrentWorkspaceService {
currentWorkspace = new LiveData<Workspace | null>(null);
/**
* open workspace, current workspace will be set to the workspace
* @param workspace
*/
openWorkspace(workspace: Workspace) {
this.currentWorkspace.next(workspace);
}
/**
* close current workspace, current workspace will be null
*/
closeWorkspace() {
this.currentWorkspace.next(null);
}
}

View File

@@ -1,2 +1,2 @@
export * from './atoms';
export * from './current-workspace';
export * from './properties';

View File

@@ -1,6 +1,7 @@
// the adapter is to bridge the workspace rootdoc & native js bindings
import { createYProxy, type Workspace, type Y } from '@blocksuite/store';
import { createYProxy, type Y } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra';
import { defaultsDeep } from 'lodash-es';
import {
@@ -29,7 +30,7 @@ export class WorkspacePropertiesAdapter {
constructor(private readonly workspace: Workspace) {
// check if properties exists, if not, create one
const rootDoc = workspace.doc;
const rootDoc = workspace.blockSuiteWorkspace.doc;
this.properties = rootDoc.getMap(AFFINE_PROPERTIES_ID);
this.proxy = createYProxy(this.properties);
@@ -56,7 +57,9 @@ export class WorkspacePropertiesAdapter {
name: 'Tags',
source: 'system',
type: PagePropertyType.Tags,
options: this.workspace.meta.properties.tags?.options ?? [], // better use a one time migration
options:
this.workspace.blockSuiteWorkspace.meta.properties.tags
?.options ?? [], // better use a one time migration
},
},
},

View File

@@ -1,21 +0,0 @@
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
import { waitForCurrentWorkspaceAtom } from '../atoms';
import { WorkspacePropertiesAdapter } from './adapter';
// todo: remove the inner atom when workspace is closed by using workspaceAdapterAtomFamily.remove
export const workspaceAdapterAtomFamily = atomFamily(
(workspace: BlockSuiteWorkspace) => {
return atom(async () => {
await workspace.doc.whenLoaded;
return new WorkspacePropertiesAdapter(workspace);
});
}
);
export const currentWorkspacePropertiesAdapterAtom = atom(async get => {
const workspace = await get(waitForCurrentWorkspaceAtom);
return get(workspaceAdapterAtomFamily(workspace.blockSuiteWorkspace));
});

View File

@@ -1,2 +1 @@
export * from './adapter';
export * from './atom';