refactor!: next generation AFFiNE code structure (#1176)

This commit is contained in:
Himself65
2023-03-01 01:40:01 -06:00
committed by GitHub
parent 2dcccc772c
commit e0481d29ad
270 changed files with 8308 additions and 6829 deletions
+80
View File
@@ -0,0 +1,80 @@
import { Unreachable } from '../../components/affine/affine-error-eoundary';
import {
AffineRemoteUnSyncedWorkspace,
RemWorkspaceFlavour,
transformToAffineSyncedWorkspace,
} from '../../shared';
import { apis } from '../../shared/apis';
import { createEmptyBlockSuiteWorkspace } from '../../utils';
type Query = (typeof QueryKey)[keyof typeof QueryKey];
export const fetcher = async (
query:
| Query
| [Query, string, boolean]
| [Query, string]
| [Query, string, string]
) => {
if (query === QueryKey.getUser) {
return apis.auth.user ?? null;
}
if (Array.isArray(query)) {
if (query[0] === QueryKey.downloadWorkspace) {
if (typeof query[2] !== 'boolean') {
throw new Unreachable();
}
return apis.downloadWorkspace(query[1], query[2]);
} else if (query[0] === QueryKey.getMembers) {
return apis.getWorkspaceMembers({
id: query[1],
});
} else if (query[0] === QueryKey.getUserByEmail) {
if (typeof query[2] !== 'string') {
throw new Unreachable();
}
return apis.getUserByEmail({
workspace_id: query[1],
email: query[2],
});
}
} else {
if (query === QueryKey.getWorkspaces) {
return apis.getWorkspaces().then(workspaces => {
return workspaces.map(workspace => {
const remWorkspace: AffineRemoteUnSyncedWorkspace = {
...workspace,
flavour: RemWorkspaceFlavour.AFFINE,
firstBinarySynced: false,
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
workspace.id,
(k: string) =>
// fixme: token could be expired
({ api: '/api/workspace', token: apis.auth.token }[k])
),
syncBinary: async () => {
const binary = await apis.downloadWorkspace(
workspace.id,
workspace.public
);
if (remWorkspace.firstBinarySynced) {
return null;
}
return transformToAffineSyncedWorkspace(remWorkspace, binary);
},
};
return remWorkspace;
});
});
}
return (apis as any)[query]();
}
};
export const QueryKey = {
getUser: 'getUser',
getWorkspaces: 'getWorkspaces',
downloadWorkspace: 'downloadWorkspace',
getMembers: 'getMembers',
getUserByEmail: 'getUserByEmail',
} as const;
+198
View File
@@ -0,0 +1,198 @@
import { assertEquals } from '@blocksuite/store';
import React from 'react';
import { preload } from 'swr';
import { z } from 'zod';
import { createAffineProviders } from '../../blocksuite';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { PageDetailEditor } from '../../components/page-detail-editor';
import {
AffineRemoteSyncedWorkspace,
AffineRemoteUnSyncedWorkspace,
BlockSuiteWorkspace,
LoadPriority,
RemWorkspaceFlavour,
} from '../../shared';
import { apis } from '../../shared/apis';
import { createEmptyBlockSuiteWorkspace } from '../../utils';
import { WorkspacePlugin } from '..';
import { fetcher, QueryKey } from './fetcher';
const kAffineLocal = 'affine-local-storage';
const schema = z.object({
id: z.string(),
firstBinarySynced: z.boolean(),
type: z.number(),
public: z.boolean(),
permission: z.number(),
create_at: z.number(),
});
export const AffinePlugin: WorkspacePlugin<RemWorkspaceFlavour.AFFINE> = {
flavour: RemWorkspaceFlavour.AFFINE,
loadPriority: LoadPriority.HIGH,
createWorkspace: async (blockSuiteWorkspace: BlockSuiteWorkspace) => {
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
blockSuiteWorkspace.doc
);
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
return id;
},
deleteWorkspace: async workspace => {
await apis.deleteWorkspace({
id: workspace.id,
});
if (workspace.firstBinarySynced) {
workspace.providers.forEach(p => p.cleanup());
}
},
prefetchData: async dataCenter => {
if (localStorage.getItem(kAffineLocal)) {
const localData = JSON.parse(localStorage.getItem(kAffineLocal) || '[]');
if (Array.isArray(localData)) {
const workspacesDump = localData
.map((item: any) => {
const result = schema.safeParse(item);
if (result.success) {
return result.data;
}
return null;
})
.filter(Boolean) as z.infer<typeof schema>[];
const workspaces = workspacesDump.map(workspace => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspace.id,
(k: string) =>
// fixme: token could be expired
({ api: '/api/workspace', token: apis.auth.token }[k])
);
const affineWorkspace: AffineRemoteSyncedWorkspace = {
...workspace,
firstBinarySynced: true,
blockSuiteWorkspace,
providers: [...createAffineProviders(blockSuiteWorkspace)],
flavour: RemWorkspaceFlavour.AFFINE,
syncBinary: async () => {
return { ...affineWorkspace };
},
};
return affineWorkspace;
});
// fixme: refactor to a function
workspaces.forEach(workspace => {
const exist = dataCenter.workspaces.findIndex(
ws => ws.id === workspace.id
);
if (exist !== -1) {
dataCenter.workspaces.splice(exist, 1, workspace);
dataCenter.workspaces = [...dataCenter.workspaces];
} else {
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
}
});
dataCenter.callbacks.forEach(cb => cb());
} else {
localStorage.removeItem(kAffineLocal);
}
}
const promise: Promise<AffineRemoteUnSyncedWorkspace[]> = preload(
QueryKey.getWorkspaces,
fetcher
);
return promise
.then(async workspaces => {
const promises = workspaces.map(workspace => {
assertEquals(workspace.flavour, RemWorkspaceFlavour.AFFINE);
if (!workspace.firstBinarySynced) {
return workspace.syncBinary();
}
return workspace;
});
return Promise.all(promises)
.then(workspaces => {
workspaces.forEach(workspace => {
if (workspace === null) {
return;
}
const exist = dataCenter.workspaces.findIndex(
ws => ws.id === workspace.id
);
if (exist !== -1) {
dataCenter.workspaces.splice(exist, 1, workspace);
dataCenter.workspaces = [...dataCenter.workspaces];
} else {
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
}
});
return workspaces;
})
.then(ws => {
const workspaces = ws.filter(
Boolean
) as AffineRemoteSyncedWorkspace[];
const dump = workspaces.map(workspace => {
return {
id: workspace.id,
firstBinarySynced: workspace.firstBinarySynced,
type: workspace.type,
public: workspace.public,
permission: workspace.permission,
create_at: workspace.create_at,
} satisfies z.infer<typeof schema>;
});
localStorage.setItem(kAffineLocal, JSON.stringify(dump));
});
})
.catch(error => {
console.error(error);
});
},
PageDetail: ({ currentWorkspace, currentPageId }) => {
if (!currentWorkspace.firstBinarySynced) {
return <div>Loading</div>;
}
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
}
return (
<>
<PageDetailEditor
pageId={currentPageId}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage }) => {
return (
<BlockSuitePageList
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
},
SettingsDetail: ({
currentWorkspace,
onChangeTab,
currentTab,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<WorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onChangeTab={onChangeTab}
currentTab={currentTab}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
};
+90
View File
@@ -0,0 +1,90 @@
import { assertExists } from '@blocksuite/store';
import React from 'react';
import { refreshDataCenter } from '../hooks/use-workspaces';
import {
BlockSuiteWorkspace,
FlavourToWorkspace,
LoadPriority,
RemWorkspace,
RemWorkspaceFlavour,
SettingPanel,
} from '../shared';
import { AffinePlugin } from './affine';
import { LocalPlugin } from './local';
type UIBaseProps<Flavour extends RemWorkspaceFlavour> = {
currentWorkspace: FlavourToWorkspace[Flavour];
};
type SettingProps<Flavour extends RemWorkspaceFlavour> =
UIBaseProps<Flavour> & {
currentTab: SettingPanel;
onChangeTab: (tab: SettingPanel) => void;
onDeleteWorkspace: () => void;
onTransformWorkspace: (targetWorkspaceId: string) => void;
};
type PageDetailProps<Flavour extends RemWorkspaceFlavour> =
UIBaseProps<Flavour> & {
currentPageId: string;
};
type PageListProps<Flavour extends RemWorkspaceFlavour> = {
blockSuiteWorkspace: BlockSuiteWorkspace;
onOpenPage: (pageId: string, newTab?: boolean) => void;
};
type SideBarMenuProps<Flavour extends RemWorkspaceFlavour> =
UIBaseProps<Flavour> & {
setSideBarOpen: (open: boolean) => void;
};
export interface WorkspacePlugin<Flavour extends RemWorkspaceFlavour> {
flavour: Flavour;
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
// Fetch necessary data for the first render
prefetchData: (
dataCenter: {
workspaces: RemWorkspace[];
callbacks: Set<() => void>;
},
signal?: AbortSignal
) => Promise<void>;
createWorkspace: (
blockSuiteWorkspace: BlockSuiteWorkspace
) => Promise<string>;
deleteWorkspace: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
//#region UI
PageDetail: React.FC<PageDetailProps<Flavour>>;
PageList: React.FC<PageListProps<Flavour>>;
SettingsDetail: React.FC<SettingProps<Flavour>>;
//#endregion
}
export const WorkspacePlugins = {
[RemWorkspaceFlavour.AFFINE]: AffinePlugin,
[RemWorkspaceFlavour.LOCAL]: LocalPlugin,
} satisfies {
[Key in RemWorkspaceFlavour]: WorkspacePlugin<Key>;
};
export async function transformWorkspace<
From extends RemWorkspaceFlavour,
To extends RemWorkspaceFlavour
>(from: From, to: To, workspace: FlavourToWorkspace[From]): Promise<string> {
// fixme: type cast
await WorkspacePlugins[from].deleteWorkspace(workspace as any);
const newId = await WorkspacePlugins[to].createWorkspace(
workspace.blockSuiteWorkspace
);
// refresh the data center
dataCenter.workspaces = [];
await refreshDataCenter();
assertExists(dataCenter.workspaces.some(w => w.id === newId));
return newId;
}
+220
View File
@@ -0,0 +1,220 @@
import { assertEquals, uuidv4 } from '@blocksuite/store';
import React from 'react';
import { IndexeddbPersistence } from 'y-indexeddb';
import { createLocalProviders } from '../../blocksuite';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { PageDetailEditor } from '../../components/page-detail-editor';
import {
LoadPriority,
LocalWorkspace,
RemWorkspaceFlavour,
} from '../../shared';
import { config } from '../../shared/env';
import { createEmptyBlockSuiteWorkspace } from '../../utils';
import { WorkspacePlugin } from '..';
export const kStoreKey = 'affine-local-workspace';
export const LocalPlugin: WorkspacePlugin<RemWorkspaceFlavour.LOCAL> = {
flavour: RemWorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW,
createWorkspace: async blockSuiteWorkspace => {
let ids: string[] = [];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
const id = uuidv4();
const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc);
await persistence.whenSynced.then(() => {
persistence.destroy();
});
ids.push(id);
localStorage.setItem(kStoreKey, JSON.stringify(ids));
return id;
},
deleteWorkspace: async workspace => {
const id = workspace.id;
let ids: string[];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
const idx = ids.findIndex(x => x === id);
if (idx === -1) {
throw new Error('cannot find local workspace from localStorage');
}
workspace.providers.forEach(p => p.cleanup());
ids.splice(idx, 1);
assertEquals(
ids.every(id => typeof id === 'string'),
true
);
localStorage.setItem(kStoreKey, JSON.stringify(ids));
},
prefetchData: async (dataCenter, signal) => {
if (typeof window === 'undefined') {
// SSR mode, no local data
return;
}
if (signal?.aborted) {
return;
}
let ids: string[];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
if (config.enableIndexedDBProvider) {
const workspaces = await Promise.all(
ids.map(id => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(id);
const workspace: LocalWorkspace = {
id,
flavour: RemWorkspaceFlavour.LOCAL,
blockSuiteWorkspace: blockSuiteWorkspace,
providers: [...createLocalProviders(blockSuiteWorkspace)],
syncBinary: async () => {
if (!config.enableIndexedDBProvider) {
return {
...workspace,
};
}
const persistence = new IndexeddbPersistence(
blockSuiteWorkspace.room as string,
blockSuiteWorkspace.doc
);
return persistence.whenSynced.then(() => {
persistence.destroy();
return {
...workspace,
};
});
},
};
return workspace.syncBinary();
})
);
workspaces.forEach(workspace => {
if (workspace) {
const exist = dataCenter.workspaces.findIndex(
w => w.id === workspace.id
);
if (exist === -1) {
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
} else {
dataCenter.workspaces[exist] = workspace;
dataCenter.workspaces = [...dataCenter.workspaces];
}
}
});
}
if (dataCenter.workspaces.length === 0) {
if (signal?.aborted) {
return;
}
console.info('no local workspace found, create a new one');
const workspaceId = uuidv4();
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(workspaceId);
blockSuiteWorkspace.meta.setName('Untitled Workspace');
localStorage.setItem(kStoreKey, JSON.stringify([workspaceId]));
blockSuiteWorkspace.createPage(uuidv4());
const workspace: LocalWorkspace = {
id: workspaceId,
flavour: RemWorkspaceFlavour.LOCAL,
blockSuiteWorkspace: blockSuiteWorkspace,
providers: [...createLocalProviders(blockSuiteWorkspace)],
syncBinary: async () => {
if (!config.enableIndexedDBProvider) {
return {
...workspace,
};
}
const persistence = new IndexeddbPersistence(
blockSuiteWorkspace.room as string,
blockSuiteWorkspace.doc
);
return persistence.whenSynced.then(() => {
persistence.destroy();
return {
...workspace,
};
});
},
};
await workspace.syncBinary();
if (signal?.aborted) {
const persistence = new IndexeddbPersistence(
blockSuiteWorkspace.room as string,
blockSuiteWorkspace.doc
);
await persistence.clearData();
return;
}
dataCenter.workspaces = [workspace];
}
},
PageDetail: ({ currentWorkspace, currentPageId }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
}
return (
<>
<PageDetailEditor
pageId={currentPageId}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage }) => {
return (
<BlockSuitePageList
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
);
},
SettingsDetail: ({
currentWorkspace,
onChangeTab,
currentTab,
onDeleteWorkspace,
onTransformWorkspace,
}) => {
return (
<WorkspaceSettingDetail
onDeleteWorkspace={onDeleteWorkspace}
onChangeTab={onChangeTab}
currentTab={currentTab}
workspace={currentWorkspace}
onTransferWorkspace={onTransformWorkspace}
/>
);
},
};