revert: loadWorkspace unexpected behavior (#1172)

This commit is contained in:
Himself65
2023-02-21 20:44:18 -06:00
committed by GitHub
parent 86346b284e
commit 0b072da346
49 changed files with 1225 additions and 2198 deletions

View File

@@ -2,13 +2,11 @@ import { ThemeMode } from '../types';
export class LocalStorageThemeHelper {
name = 'Affine-theme-mode';
callback = new Set<() => void>();
get = (): ThemeMode | null => {
return localStorage.getItem(this.name) as ThemeMode | null;
};
set = (mode: ThemeMode) => {
localStorage.setItem(this.name, mode);
this.callback.forEach(cb => cb());
};
}

View File

@@ -6,6 +6,17 @@ const _initializeDataCenter = () => {
return () => {
if (!_dataCenterInstance) {
_dataCenterInstance = DataCenter.init();
_dataCenterInstance.then(dc => {
try {
if (window) {
(window as any).dc = dc;
}
} catch (_) {
// ignore
}
return dc;
});
}
return _dataCenterInstance;
@@ -14,7 +25,7 @@ const _initializeDataCenter = () => {
export const getDataCenter = _initializeDataCenter();
export { DataCenter };
export type { DataCenter };
export * from './message';
export { AffineProvider } from './provider/affine';
export * from './provider/affine/apis';

View File

@@ -1,24 +0,0 @@
{
"name": "@affine/store",
"private": true,
"main": "./src/index.ts",
"dependencies": {
"@affine/datacenter": "workspace:*",
"@affine/debug": "workspace:*",
"@blocksuite/blocks": "0.4.1",
"@blocksuite/editor": "0.4.1",
"@blocksuite/global": "0.4.1",
"@blocksuite/react": "0.4.1",
"@blocksuite/store": "0.4.1",
"lit": "^2.6.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"swr": "^2.0.3",
"yjs": "^13.5.46",
"zustand": "^4.3.3"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11"
}
}

View File

@@ -1,61 +0,0 @@
import { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import { Page, Workspace } from '@blocksuite/store';
import { GlobalActionsCreator } from '..';
export interface BlockSuiteState {
currentWorkspace: Workspace | null;
editor: EditorContainer | null;
currentPage: Page | null;
blockHub: BlockHub | null;
}
export const createBlockSuiteState = (): BlockSuiteState => ({
currentWorkspace: null,
currentPage: null,
blockHub: null,
editor: null,
});
export interface BlockSuiteActions {
loadPage: (pageId: string) => void;
setEditor: (editor: EditorContainer) => void;
setWorkspace: (workspace: Workspace) => void;
setBlockHub: (blockHub: BlockHub) => void;
}
export const createBlockSuiteActions: GlobalActionsCreator<
BlockSuiteActions
> = (set, get) => ({
setWorkspace: workspace => {
set({
currentWorkspace: workspace,
});
},
setEditor: editor => {
set({
editor,
});
},
loadPage: pageId => {
const { currentWorkspace } = get();
if (currentWorkspace === null) {
console.warn('currentWorkspace is null');
return;
}
const page = currentWorkspace.getPage(pageId);
if (page === null) {
console.warn('cannot find page ', pageId);
return;
}
set({
currentPage: page,
});
},
setBlockHub: blockHub => {
set({
blockHub,
});
},
});

View File

@@ -1,225 +0,0 @@
import { getDataCenter, WorkspaceUnit } from '@affine/datacenter';
import { DataCenter } from '@affine/datacenter';
import { Disposable, DisposableGroup } from '@blocksuite/global/utils';
import type { PageMeta as StorePageMeta } from '@blocksuite/store';
import React, { useEffect } from 'react';
import useSWR from 'swr';
const DEFAULT_WORKSPACE_NAME = 'Demo Workspace';
export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
return dataCenter.createWorkspace({
name: DEFAULT_WORKSPACE_NAME,
});
};
declare global {
// eslint-disable-next-line no-var
var dataCenterPromise: Promise<DataCenter>;
// eslint-disable-next-line no-var
var dc: DataCenter;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
let dataCenterPromise: Promise<DataCenter> = null!;
if (!globalThis.dataCenterPromise) {
dataCenterPromise = getDataCenter();
dataCenterPromise.then(dataCenter => {
globalThis.dc = dataCenter;
return dataCenter;
});
} else {
dataCenterPromise = globalThis.dataCenterPromise;
}
export { dataCenterPromise };
export interface PageMeta extends StorePageMeta {
favorite: boolean;
trash: boolean;
trashDate: number;
updatedDate: number;
mode: 'edgeless' | 'page';
}
import { GlobalActionsCreator, useGlobalStateApi } from '..';
export type DataCenterState = {
currentDataCenterWorkspace: WorkspaceUnit | null;
dataCenterPageList: PageMeta[];
blobDataSynced: boolean;
};
export type DataCenterActions = {
loadWorkspace: (
workspaceId: string,
signal?: AbortSignal
) => Promise<WorkspaceUnit | null>;
};
export const createDataCenterState = (): DataCenterState => ({
currentDataCenterWorkspace: null,
dataCenterPageList: [],
blobDataSynced: false,
});
export const createDataCenterActions: GlobalActionsCreator<
DataCenterActions
> = (set, get) => ({
loadWorkspace: async (workspaceId, signal) => {
const dataCenter = await dataCenterPromise;
const { currentDataCenterWorkspace } = get();
if (!dataCenter.workspaces.find(v => v.id.toString() === workspaceId)) {
return null;
}
if (workspaceId === currentDataCenterWorkspace?.id) {
return currentDataCenterWorkspace;
}
const workspace = await dataCenter.loadWorkspace(workspaceId);
if (!workspace) {
return null;
}
if (signal?.aborted) {
// do not update state if aborted
return null;
}
let isOwner;
if (workspace.provider === 'local') {
// isOwner is useful only in the cloud
isOwner = true;
} else {
const userInfo = get().user; // We must ensure workspace.owner exists, then ensure id same.
isOwner = userInfo?.id === workspace.owner?.id;
}
const pageList =
(workspace.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
if (workspace.blocksuiteWorkspace) {
set({
currentWorkspace: workspace.blocksuiteWorkspace,
});
}
set({
isOwner,
currentDataCenterWorkspace: workspace,
dataCenterPageList: pageList,
});
return workspace;
},
});
export function useDataCenter() {
const { data } = useSWR<DataCenter>(['datacenter'], {
fallbackData: DataCenter.initEmpty(),
});
return data as DataCenter;
}
export function useDataCenterWorkspace(
workspaceId: string | null
): WorkspaceUnit | null {
const { data } = useSWR<WorkspaceUnit | null>(['datacenter', workspaceId], {
fallbackData: null,
});
return data ?? null;
}
export function useDataCenterPublicWorkspace(workspaceId: string | null) {
const { data, error } = useSWR<WorkspaceUnit | null>(
['datacenter', workspaceId, 'public'],
{
fallbackData: null,
}
);
return {
workspace: data ?? null,
error,
} as const;
}
export function DataCenterPreloader({ children }: React.PropsWithChildren) {
const api = useGlobalStateApi();
// init user info from datacenter
useEffect(() => {
dataCenterPromise.then(async dataCenter => {
const user = await dataCenter.getUserInfo();
if (!api.getState().user) {
api.setState({ user });
}
});
}, []);
//# region effect for updating workspace page list
useEffect(() => {
return api.subscribe(
store => store.currentDataCenterWorkspace,
currentWorkspace => {
const disposableGroup = new DisposableGroup();
disposableGroup.add(
currentWorkspace?.blocksuiteWorkspace?.meta.pagesUpdated.on(() => {
if (
Array.isArray(
currentWorkspace.blocksuiteWorkspace?.meta.pageMetas
)
) {
api.setState({
dataCenterPageList: currentWorkspace.blocksuiteWorkspace?.meta
.pageMetas as PageMeta[],
});
}
})
);
return () => {
disposableGroup.dispose();
};
}
);
}, [api]);
//# endregion
//# region effect for blobDataSynced
useEffect(
() =>
api.subscribe(
store => store.currentDataCenterWorkspace,
workspace => {
if (!workspace?.blocksuiteWorkspace) {
return;
}
const controller = new AbortController();
const blocksuiteWorkspace = workspace.blocksuiteWorkspace;
let syncChangeDisposable: Disposable | undefined;
async function subscribe() {
const blobStorage = await blocksuiteWorkspace.blobs;
if (controller.signal.aborted) {
return;
}
syncChangeDisposable =
blobStorage?.signals.onBlobSyncStateChange.on(() => {
if (controller.signal.aborted) {
syncChangeDisposable?.dispose();
return;
} else {
api.setState({
blobDataSynced: blobStorage?.uploading,
});
}
});
}
subscribe();
return () => {
controller.abort();
syncChangeDisposable?.dispose();
};
}
),
[api]
);
//# endregion
return <>{children}</>;
}

View File

@@ -1,128 +0,0 @@
import { assertEquals } from '@blocksuite/global/utils';
import type React from 'react';
import { createContext, useContext, useMemo } from 'react';
import { preload, SWRConfig, SWRConfiguration } from 'swr';
import { createStore, StateCreator, useStore } from 'zustand';
import { combine, subscribeWithSelector } from 'zustand/middleware';
import type { UseBoundStore } from 'zustand/react';
import {
BlockSuiteActions,
BlockSuiteState,
createBlockSuiteActions,
createBlockSuiteState,
} from './blocksuite';
import {
createDataCenterActions,
createDataCenterState,
createDefaultWorkspace,
DataCenterActions,
dataCenterPromise,
DataCenterState,
} from './datacenter';
import {
createUserActions,
createUserState,
UserActions,
UserState,
} from './user';
export type GlobalActionsCreator<Actions, Store = GlobalState> = StateCreator<
Store,
[['zustand/subscribeWithSelector', unknown]],
[],
Actions
>;
export interface GlobalState
extends BlockSuiteState,
UserState,
DataCenterState {}
export interface GlobalActions
extends BlockSuiteActions,
UserActions,
DataCenterActions {}
const create = () =>
createStore(
subscribeWithSelector(
combine<GlobalState, GlobalActions>(
{
...createBlockSuiteState(),
...createUserState(),
...createDataCenterState(),
},
/* deepscan-disable TOO_MANY_ARGS */
(set, get, api) => ({
...createBlockSuiteActions(set, get, api),
...createUserActions(set, get, api),
...createDataCenterActions(set, get, api),
})
/* deepscan-enable TOO_MANY_ARGS */
)
)
);
type Store = ReturnType<typeof create>;
const GlobalStateContext = createContext<Store | null>(null);
export const useGlobalStateApi = () => {
const api = useContext(GlobalStateContext);
if (!api) {
throw new Error('cannot find modal context');
}
return api;
};
export const useGlobalState: UseBoundStore<Store> = ((
selector: Parameters<UseBoundStore<Store>>[0],
equals: Parameters<UseBoundStore<Store>>[1]
) => {
const api = useGlobalStateApi();
return useStore(api, selector, equals);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any;
export type DataKey =
| ['datacenter', string | null, 'public' | undefined]
| ['datacenter'];
const swrFetcher = async (keys: DataKey) => {
assertEquals(keys[0], 'datacenter');
if (keys.length === 1) {
return await dataCenterPromise.then(async dataCenter => {
if (dataCenter.workspaces.length === 0) {
await createDefaultWorkspace(dataCenter);
}
return dataCenter;
});
} else {
if (keys[1] === null) {
return null;
}
const dataCenter = await dataCenterPromise;
if (keys[2] === 'public') {
return dataCenter.loadPublicWorkspace(keys[1]);
}
return dataCenter.loadWorkspace(keys[1]);
}
};
preload(['datacenter'], swrFetcher);
const swrConfig: SWRConfiguration = {
fetcher: swrFetcher,
suspense: true,
};
export const GlobalAppProvider: React.FC<React.PropsWithChildren> =
function ModelProvider({ children }) {
return (
<SWRConfig value={swrConfig}>
<GlobalStateContext.Provider value={useMemo(() => create(), [])}>
{children}
</GlobalStateContext.Provider>
</SWRConfig>
);
};

View File

@@ -1,67 +0,0 @@
import { User } from '@affine/datacenter';
import { DebugLogger } from '@affine/debug';
import { GlobalActionsCreator } from '..';
import { dataCenterPromise } from '../datacenter';
export interface UserState {
user: User | null;
isOwner: boolean;
}
export interface UserActions {
login: () => Promise<User | null>;
logout: () => Promise<void>;
}
export const createUserState = (): UserState => ({
// initialized in DataCenterLoader (restore from localStorage)
user: null,
isOwner: false,
});
const logger = new DebugLogger('store:user');
export const createUserActions: GlobalActionsCreator<UserActions> = (
set,
get
) => {
return {
login: async () => {
const { currentDataCenterWorkspace: workspace } = get();
const dataCenter = await dataCenterPromise;
try {
await dataCenter.login();
const user = (await dataCenter.getUserInfo()) as User;
if (!user) {
// Add ErrorBoundary
throw new Error('User info not found');
}
let isOwner;
if (workspace?.provider === 'local') {
// isOwner is useful only in the cloud
isOwner = true;
} else {
isOwner = user.id === workspace?.owner?.id;
}
set({ user, isOwner });
logger.debug('login success', user);
return user;
} catch (error) {
logger.error('login failed', error);
return null; // login failed
}
},
logout: async () => {
const dataCenter = await dataCenterPromise;
await dataCenter.logout();
logger.debug('logout success');
set({ user: null });
},
};
};

View File

@@ -1,9 +0,0 @@
export * from './app';
export type { PageMeta } from './app/datacenter';
export { createDefaultWorkspace, DataCenterPreloader } from './app/datacenter';
export {
dataCenterPromise,
useDataCenter,
useDataCenterPublicWorkspace,
useDataCenterWorkspace,
} from './app/datacenter';

View File

@@ -1,38 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import { DataCenter, getDataCenter } from '@affine/datacenter';
import {
createDefaultWorkspace,
GlobalAppProvider,
useDataCenter,
useGlobalState,
} from '@affine/store';
import { render } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
describe('App Store', () => {
test('init', async () => {
const dataCenterPromise = getDataCenter();
const dataCenter = await dataCenterPromise;
await createDefaultWorkspace(dataCenter);
const Inner = () => {
const state = useGlobalState();
const dataCenter = useDataCenter();
expect(state).toBeTypeOf('object');
expect(dataCenter).toBeInstanceOf(DataCenter);
return <div>Test2</div>;
};
const App = () => (
<GlobalAppProvider>
<div>Test1</div>
<Inner />
</GlobalAppProvider>
);
const app = render(<App />);
app.getByText('Test2');
});
});