mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
refactor(infra): migrate to new infra (#5565)
This commit is contained in:
1
packages/frontend/core/src/modules/collection/index.ts
Normal file
1
packages/frontend/core/src/modules/collection/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './service';
|
||||
164
packages/frontend/core/src/modules/collection/service.ts
Normal file
164
packages/frontend/core/src/modules/collection/service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
24
packages/frontend/core/src/modules/page/current-page.tsx
Normal file
24
packages/frontend/core/src/modules/page/current-page.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
1
packages/frontend/core/src/modules/page/index.ts
Normal file
1
packages/frontend/core/src/modules/page/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './current-page';
|
||||
27
packages/frontend/core/src/modules/services.ts
Normal file
27
packages/frontend/core/src/modules/services.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './atoms';
|
||||
export * from './current-workspace';
|
||||
export * from './properties';
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
@@ -1,2 +1 @@
|
||||
export * from './adapter';
|
||||
export * from './atom';
|
||||
|
||||
Reference in New Issue
Block a user