refactor: support suspense mode in workspaces (#1304)

This commit is contained in:
Himself65
2023-03-04 20:11:15 -06:00
committed by GitHub
parent dd6bee68cb
commit 9a199eb9a1
27 changed files with 713 additions and 652 deletions

View File

@@ -1,172 +1,25 @@
import { Workspace } from '@affine/datacenter';
import { config, getEnvironment } from '@affine/env';
import { nanoid } from '@blocksuite/store';
import { useCallback, useMemo, useSyncExternalStore } from 'react';
import useSWR from 'swr';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { lockMutex } from '../atoms';
import { createLocalProviders } from '../blocksuite';
import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms';
import { WorkspacePlugins } from '../plugins';
import { QueryKey } from '../plugins/affine/fetcher';
import { kStoreKey } from '../plugins/local';
import { LocalPlugin } from '../plugins/local';
import { LocalWorkspace, RemWorkspace, RemWorkspaceFlavour } from '../shared';
import { createEmptyBlockSuiteWorkspace } from '../utils';
// fixme(himself65): refactor with jotai atom using async
export const dataCenter = {
workspaces: [] as RemWorkspace[],
isLoaded: false,
callbacks: new Set<() => void>(),
};
export function vitestRefreshWorkspaces() {
dataCenter.workspaces = [];
dataCenter.callbacks.clear();
}
declare global {
// eslint-disable-next-line no-var
var dataCenter: {
workspaces: RemWorkspace[];
isLoaded: boolean;
callbacks: Set<() => void>;
};
}
globalThis.dataCenter = dataCenter;
function createRemLocalWorkspace(name: string) {
const id = nanoid();
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
id,
(_: string) => undefined
);
blockSuiteWorkspace.meta.setName(name);
const workspace: LocalWorkspace = {
flavour: RemWorkspaceFlavour.LOCAL,
blockSuiteWorkspace: blockSuiteWorkspace,
providers: [...createLocalProviders(blockSuiteWorkspace)],
id,
};
if (config.enableIndexedDBProvider) {
let ids: string[];
try {
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
if (!Array.isArray(ids)) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
} catch (e) {
localStorage.setItem(kStoreKey, '[]');
ids = [];
}
ids.push(id);
localStorage.setItem(kStoreKey, JSON.stringify(ids));
}
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
dataCenter.callbacks.forEach(cb => cb());
return id;
}
const emptyWorkspaces: RemWorkspace[] = [];
export async function refreshDataCenter(signal?: AbortSignal) {
dataCenter.isLoaded = false;
dataCenter.callbacks.forEach(cb => cb());
if (getEnvironment().isServer) {
return;
}
// fixme(himself65): `prefetchWorkspace` is not used
// use `config.enablePlugin = ['affine', 'local']` instead
// if (!config.prefetchWorkspace) {
// console.info('prefetchNecessaryData: skip prefetching');
// return;
// }
const plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
// prefetch data in order
for (const plugin of plugins) {
console.info('prefetchNecessaryData: plugin', plugin.flavour);
try {
if (signal?.aborted) {
break;
}
const oldData = dataCenter.workspaces;
await plugin.prefetchData(dataCenter, signal);
const newData = dataCenter.workspaces;
if (!Object.is(oldData, newData)) {
console.info('prefetchNecessaryData: data changed');
}
} catch (e) {
console.error('error prefetch data', plugin.flavour, e);
}
}
dataCenter.isLoaded = true;
dataCenter.callbacks.forEach(cb => cb());
}
export function useWorkspaces(): RemWorkspace[] {
return useSyncExternalStore(
useCallback(onStoreChange => {
dataCenter.callbacks.add(onStoreChange);
return () => {
dataCenter.callbacks.delete(onStoreChange);
};
}, []),
useCallback(() => dataCenter.workspaces, []),
useCallback(() => emptyWorkspaces, [])
);
}
export function useWorkspacesIsLoaded(): boolean {
return useSyncExternalStore(
useCallback(onStoreChange => {
dataCenter.callbacks.add(onStoreChange);
return () => {
dataCenter.callbacks.delete(onStoreChange);
};
}, []),
useCallback(() => dataCenter.isLoaded, []),
useCallback(() => true, [])
);
}
export function useSyncWorkspaces() {
return useSWR<Workspace[]>(QueryKey.getWorkspaces, {
fallbackData: [],
revalidateOnReconnect: true,
revalidateOnFocus: false,
revalidateOnMount: true,
revalidateIfStale: false,
});
}
async function deleteWorkspace(workspaceId: string) {
return lockMutex(async () => {
console.warn('deleting workspace');
const idx = dataCenter.workspaces.findIndex(
workspace => workspace.id === workspaceId
);
if (idx === -1) {
throw new Error('workspace not found');
}
try {
const [workspace] = dataCenter.workspaces.splice(idx, 1);
// @ts-expect-error
await WorkspacePlugins[workspace.flavour].deleteWorkspace(workspace);
dataCenter.callbacks.forEach(cb => cb());
} catch (e) {
console.error('error deleting workspace', e);
}
});
return useAtomValue(workspacesAtom);
}
export function useWorkspacesHelper() {
return useMemo(
() => ({
createWorkspacePage: (workspaceId: string, pageId: string) => {
const workspace = dataCenter.workspaces.find(
const workspaces = useWorkspaces();
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
return {
createWorkspacePage: useCallback(
(workspaceId: string, pageId: string) => {
const workspace = workspaces.find(
ws => ws.id === workspaceId
) as LocalWorkspace;
if (workspace && 'blockSuiteWorkspace' in workspace) {
@@ -175,9 +28,46 @@ export function useWorkspacesHelper() {
throw new Error('cannot create page. blockSuiteWorkspace not found');
}
},
createRemLocalWorkspace,
deleteWorkspace,
}),
[]
);
[workspaces]
),
createLocalWorkspace: useCallback(
async (name: string): Promise<string> => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(),
_ => undefined
);
blockSuiteWorkspace.meta.setName(name);
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
set(workspaces => [
...workspaces,
{
id,
flavour: RemWorkspaceFlavour.LOCAL,
},
]);
return id;
},
[set]
),
deleteWorkspace: useCallback(
async (workspaceId: string) => {
const targetJotaiWorkspace = jotaiWorkspaces.find(
ws => ws.id === workspaceId
);
const targetWorkspace = workspaces.find(ws => ws.id === workspaceId);
if (!targetJotaiWorkspace || !targetWorkspace) {
throw new Error('page cannot be found');
}
// delete workspace from plugin
await WorkspacePlugins[targetWorkspace.flavour].CRUD.delete(
// fixme: type casting
targetWorkspace as any
);
// delete workspace from jotai storage
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
},
[jotaiWorkspaces, set, workspaces]
),
};
}