diff --git a/apps/core/src/app.tsx b/apps/core/src/app.tsx
index bd3f72a972..bc8783d705 100644
--- a/apps/core/src/app.tsx
+++ b/apps/core/src/app.tsx
@@ -5,117 +5,13 @@ import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
-import type { RouterState } from '@remix-run/router';
-import {
- currentPageIdAtom,
- currentWorkspaceIdAtom,
-} from '@toeverything/plugin-infra/manager';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense, useEffect } from 'react';
-import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+import { RouterProvider } from 'react-router-dom';
-import { historyBaseAtom, MAX_HISTORY } from './atoms/history';
+import { router } from './router';
import createEmotionCache from './utils/create-emotion-cache';
-const router = createBrowserRouter([
- {
- path: '/',
- lazy: () => import('./pages/index'),
- },
- {
- path: '/404',
- lazy: () => import('./pages/404'),
- },
- {
- path: '/workspace/:workspaceId/all',
- lazy: () => import('./pages/workspace/all-page'),
- },
- {
- path: '/workspace/:workspaceId/trash',
- lazy: () => import('./pages/workspace/trash-page'),
- },
- {
- path: '/workspace/:workspaceId/:pageId',
- lazy: () => import('./pages/workspace/detail-page'),
- },
-]);
-
-//#region atoms bootstrap
-
-currentWorkspaceIdAtom.onMount = set => {
- const callback = (state: RouterState) => {
- const value = state.location.pathname.split('/')[2];
- if (value) {
- set(value);
- localStorage.setItem('last_workspace_id', value);
- }
- };
- callback(router.state);
-
- const unsubscribe = router.subscribe(callback);
- return () => {
- unsubscribe();
- };
-};
-
-currentPageIdAtom.onMount = set => {
- const callback = (state: RouterState) => {
- const value = state.location.pathname.split('/')[3];
- if (value) {
- set(value);
- }
- };
- callback(router.state);
-
- const unsubscribe = router.subscribe(callback);
- return () => {
- unsubscribe();
- };
-};
-
-historyBaseAtom.onMount = set => {
- const unsubscribe = router.subscribe(state => {
- set(prev => {
- const url = state.location.pathname;
- console.log('push', url, prev.skip, prev.stack.length, prev.current);
- if (prev.skip) {
- return {
- stack: [...prev.stack],
- current: prev.current,
- skip: false,
- };
- } else {
- if (prev.current < prev.stack.length - 1) {
- const newStack = prev.stack.slice(0, prev.current);
- newStack.push(url);
- if (newStack.length > MAX_HISTORY) {
- newStack.shift();
- }
- return {
- stack: newStack,
- current: newStack.length - 1,
- skip: false,
- };
- } else {
- const newStack = [...prev.stack, url];
- if (newStack.length > MAX_HISTORY) {
- newStack.shift();
- }
- return {
- stack: newStack,
- current: newStack.length - 1,
- skip: false,
- };
- }
- }
- });
- });
- return () => {
- unsubscribe();
- };
-};
-//#endregion
-
const i18n = createI18n();
const cache = createEmotionCache();
@@ -132,6 +28,10 @@ const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
);
};
+const future = {
+ v7_startTransition: true,
+} as const;
+
export const App = memo(function App() {
useEffect(() => {
document.documentElement.lang = i18n.language;
@@ -144,9 +44,11 @@ export const App = memo(function App() {
- }>
-
-
+ }
+ router={router}
+ future={future}
+ />
diff --git a/apps/core/src/atoms/history.ts b/apps/core/src/atoms/history.ts
index 592c72b41d..9d38c8fcf4 100644
--- a/apps/core/src/atoms/history.ts
+++ b/apps/core/src/atoms/history.ts
@@ -1,8 +1,10 @@
import { useAtom } from 'jotai';
-import { atomWithStorage } from 'jotai/utils';
+import { atomWithStorage, createJSONStorage } from 'jotai/utils';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
+import { router } from '../router';
+
export type History = {
stack: string[];
current: number;
@@ -11,11 +13,62 @@ export type History = {
export const MAX_HISTORY = 50;
-export const historyBaseAtom = atomWithStorage('router-history', {
- stack: [],
- current: 0,
- skip: false,
-});
+const historyBaseAtom = atomWithStorage(
+ 'router-history',
+ {
+ stack: [],
+ current: 0,
+ skip: false,
+ },
+ createJSONStorage(() => sessionStorage)
+);
+
+historyBaseAtom.onMount = set => {
+ const unsubscribe = router.subscribe(state => {
+ set(prev => {
+ const url = state.location.pathname;
+
+ // if stack top is the same as current, skip
+ if (prev.stack[prev.current] === url) {
+ return prev;
+ }
+
+ if (prev.skip) {
+ return {
+ stack: [...prev.stack],
+ current: prev.current,
+ skip: false,
+ };
+ } else {
+ if (prev.current < prev.stack.length - 1) {
+ const newStack = prev.stack.slice(0, prev.current);
+ newStack.push(url);
+ if (newStack.length > MAX_HISTORY) {
+ newStack.shift();
+ }
+ return {
+ stack: newStack,
+ current: newStack.length - 1,
+ skip: false,
+ };
+ } else {
+ const newStack = [...prev.stack, url];
+ if (newStack.length > MAX_HISTORY) {
+ newStack.shift();
+ }
+ return {
+ stack: newStack,
+ current: newStack.length - 1,
+ skip: false,
+ };
+ }
+ }
+ });
+ });
+ return () => {
+ unsubscribe();
+ };
+};
export function useHistoryAtom() {
const navigate = useNavigate();
diff --git a/apps/core/src/layouts/workspace-layout.tsx b/apps/core/src/layouts/workspace-layout.tsx
index 246e83c483..fb8f9a5376 100644
--- a/apps/core/src/layouts/workspace-layout.tsx
+++ b/apps/core/src/layouts/workspace-layout.tsx
@@ -10,7 +10,6 @@ import {
ToolContainer,
WorkspaceFallback,
} from '@affine/component/workspace';
-import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
rootBlockHubAtom,
@@ -30,14 +29,11 @@ import {
} from '@dnd-kit/core';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
-import {
- currentPageIdAtom,
- currentWorkspaceIdAtom,
-} from '@toeverything/plugin-infra/manager';
+import { currentWorkspaceIdAtom } from '@toeverything/plugin-infra/manager';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { FC, PropsWithChildren, ReactElement } from 'react';
-import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
-import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import { lazy, Suspense, useCallback, useMemo } from 'react';
+import { useLocation, useParams } from 'react-router-dom';
import { WorkspaceAdapters } from '../adapters/workspace';
import {
@@ -111,20 +107,6 @@ export const CurrentWorkspaceContext = ({
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
- const navigate = useNavigate();
- // fixme(himself65): this is not a good way to handle this,
- // need a better way to check whether this workspace really exist.
- useEffect(() => {
- const id = setTimeout(() => {
- if (!exist) {
- navigate('/');
- globalThis.HALTING_PROBLEM_TIMEOUT <<= 1;
- }
- }, globalThis.HALTING_PROBLEM_TIMEOUT);
- return () => {
- clearTimeout(id);
- };
- }, [exist, metadata.length, navigate]);
if (metadata.length === 0) {
return ;
}
@@ -171,24 +153,10 @@ export const WorkspaceLayout: FC =
export const WorkspaceLayoutInner: FC = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace();
- const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
- const { jumpToPage, openPage } = useNavigateHelper();
+ const { openPage } = useNavigateHelper();
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
- useEffect(() => {
- const page = currentWorkspace.blockSuiteWorkspace.getPage(
- `${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
- );
- if (page && page.meta.jumpOnce) {
- currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
- jumpOnce: false,
- });
- setCurrentPageId(currentPageId);
- jumpToPage(currentWorkspace.id, page.id);
- }
- }, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
-
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace
diff --git a/apps/core/src/pages/index.tsx b/apps/core/src/pages/index.tsx
index a1d5f51694..c2437e9558 100644
--- a/apps/core/src/pages/index.tsx
+++ b/apps/core/src/pages/index.tsx
@@ -1,12 +1,10 @@
import { DebugLogger } from '@affine/debug';
-import { WorkspaceSubPath } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
-import { useAtomValue } from 'jotai';
-import { lazy, useEffect, useRef } from 'react';
-
-import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
-import { useWorkspace } from '../hooks/use-workspace';
+import { rootStore } from '@toeverything/plugin-infra/manager';
+import { lazy } from 'react';
+import type { LoaderFunction } from 'react-router-dom';
+import { redirect } from 'react-router-dom';
const AllWorkspaceModals = lazy(() =>
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
@@ -14,81 +12,35 @@ const AllWorkspaceModals = lazy(() =>
}))
);
-type WorkspaceLoaderProps = {
- id: string;
-};
+const logger = new DebugLogger('index-page');
-const WorkspaceLoader = (props: WorkspaceLoaderProps): null => {
- useWorkspace(props.id);
+export const loader: LoaderFunction = async () => {
+ const meta = await rootStore.get(rootWorkspacesMetadataAtom);
+ const lastId = localStorage.getItem('last_workspace_id');
+ const lastPageId = localStorage.getItem('last_page_id');
+ const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
+ if (target) {
+ const targetWorkspace = getWorkspace(target.id);
+ const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
+ ({ trash }) => !trash
+ );
+ const pageId =
+ nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
+ nonTrashPages.at(0)?.id;
+ if (pageId) {
+ logger.debug('Found target workspace. Jump to page', pageId);
+ return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
+ } else {
+ logger.debug('Found target workspace. Jump to all page');
+ return redirect(`/workspace/${targetWorkspace.id}/all`);
+ }
+ }
return null;
};
-const logger = new DebugLogger('index-page');
-
export const Component = () => {
- const meta = useAtomValue(rootWorkspacesMetadataAtom);
- const navigateHelper = useNavigateHelper();
- const jumpOnceRef = useRef(false);
- useEffect(() => {
- if (jumpOnceRef.current) {
- return;
- }
- const lastId = localStorage.getItem('last_workspace_id');
- const lastPageId = localStorage.getItem('last_page_id');
- const target =
- (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
- if (target) {
- const targetWorkspace = getWorkspace(target.id);
- const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
- ({ trash }) => !trash
- );
- const pageId =
- nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
- nonTrashPages.at(0)?.id;
- if (pageId) {
- logger.debug('Found target workspace. Jump to page', pageId);
- navigateHelper.jumpToPage(
- targetWorkspace.id,
- pageId,
- RouteLogic.REPLACE
- );
- jumpOnceRef.current = true;
- } else {
- const clearId = setTimeout(() => {
- dispose.dispose();
- logger.debug('Found target workspace. Jump to all pages');
- navigateHelper.jumpToSubPath(
- targetWorkspace.id,
- WorkspaceSubPath.ALL,
- RouteLogic.REPLACE
- );
- jumpOnceRef.current = true;
- }, 1000);
- const dispose = targetWorkspace.slots.pageAdded.once(pageId => {
- clearTimeout(clearId);
- navigateHelper.jumpToPage(
- targetWorkspace.id,
- pageId,
- RouteLogic.REPLACE
- );
- jumpOnceRef.current = true;
- });
- return () => {
- clearTimeout(clearId);
- dispose.dispose();
- jumpOnceRef.current = false;
- };
- }
- } else {
- console.warn('No workspace found');
- }
- return;
- }, [meta, navigateHelper]);
return (
<>
- {meta.map(({ id }) => (
-
- ))}
>
);
diff --git a/apps/core/src/pages/workspace/all-page.tsx b/apps/core/src/pages/workspace/all-page.tsx
index 1a88c601f4..b501c81313 100644
--- a/apps/core/src/pages/workspace/all-page.tsx
+++ b/apps/core/src/pages/workspace/all-page.tsx
@@ -1,15 +1,41 @@
import { useCollectionManager } from '@affine/component/page-list';
+import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
-import { useCallback } from 'react';
+import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/plugin-infra/__internal__/workspace';
+import {
+ currentPageIdAtom,
+ rootStore,
+} from '@toeverything/plugin-infra/manager';
+import { useAtom } from 'jotai/react';
+import { useCallback, useEffect } from 'react';
+import type { LoaderFunction } from 'react-router-dom';
+import { redirect } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
-import { WorkspaceLayout } from '../../layouts/workspace-layout';
-const AllPage = () => {
+export const loader: LoaderFunction = async args => {
+ const workspaceId = args.params.workspaceId;
+ assertExists(workspaceId);
+ const workspaceAtom = getActiveBlockSuiteWorkspaceAtom(workspaceId);
+ const workspace = await rootStore.get(workspaceAtom);
+ const page = workspace.getPage(
+ `${workspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
+ );
+ if (page && page.meta.jumpOnce) {
+ workspace.meta.setPageMeta(page.id, {
+ jumpOnce: false,
+ });
+ return redirect(`/workspace/${workspace.id}/${page.id}`);
+ }
+ return null;
+};
+
+export const AllPage = () => {
const { jumpToPage } = useNavigateHelper();
+ const [currentPageId, setCurrentPageId] = useAtom(currentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
const setting = useCollectionManager(currentWorkspace.id);
const onClickPage = useCallback(
@@ -23,6 +49,18 @@ const AllPage = () => {
},
[currentWorkspace, jumpToPage]
);
+ useEffect(() => {
+ const page = currentWorkspace.blockSuiteWorkspace.getPage(
+ `${currentWorkspace.blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`
+ );
+ if (page && page.meta.jumpOnce) {
+ currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(page.id, {
+ jumpOnce: false,
+ });
+ setCurrentPageId(currentPageId);
+ jumpToPage(currentWorkspace.id, page.id);
+ }
+ }, [currentPageId, currentWorkspace, jumpToPage, setCurrentPageId]);
const { PageList, Header } = getUIAdapter(currentWorkspace.flavour);
return (
<>
@@ -42,9 +80,5 @@ const AllPage = () => {
};
export const Component = () => {
- return (
-
-
-
- );
+ return ;
};
diff --git a/apps/core/src/pages/workspace/detail-page.tsx b/apps/core/src/pages/workspace/detail-page.tsx
index 35aa2e1e4e..e830175bfa 100644
--- a/apps/core/src/pages/workspace/detail-page.tsx
+++ b/apps/core/src/pages/workspace/detail-page.tsx
@@ -7,18 +7,21 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
-import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
+import {
+ currentPageIdAtom,
+ rootStore,
+} from '@toeverything/plugin-infra/manager';
import { useAtomValue } from 'jotai';
import { useAtom } from 'jotai/react';
import { type ReactElement, useCallback, useEffect } from 'react';
+import type { LoaderFunction } from 'react-router-dom';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { getUIAdapter } from '../../adapters/workspace';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
-import { WorkspaceLayout } from '../../layouts/workspace-layout';
-const WorkspaceDetailPageImpl = (): ReactElement => {
+const DetailPageImpl = (): ReactElement => {
const { openPage, jumpToSubPath } = useNavigateHelper();
const currentPageId = useAtomValue(currentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
@@ -68,7 +71,7 @@ const WorkspaceDetailPageImpl = (): ReactElement => {
);
};
-const WorkspaceDetailPage = (): ReactElement => {
+export const DetailPage = (): ReactElement => {
const { workspaceId, pageId } = useParams();
const location = useLocation();
const navigate = useNavigate();
@@ -92,6 +95,13 @@ const WorkspaceDetailPage = (): ReactElement => {
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
navigate('/404');
+ } else {
+ // fixme: cleanup jumpOnce in the right time
+ if (page.meta.jumpOnce) {
+ currentWorkspace.blockSuiteWorkspace.setPageMeta(currentPageId, {
+ jumpOnce: false,
+ });
+ }
}
}
}
@@ -110,13 +120,17 @@ const WorkspaceDetailPage = (): ReactElement => {
if (!currentPageId || !page) {
return ;
}
- return ;
+ return ;
+};
+
+export const loader: LoaderFunction = args => {
+ if (args.params.pageId) {
+ localStorage.setItem('last_page_id', args.params.pageId);
+ rootStore.set(currentPageIdAtom, args.params.pageId);
+ }
+ return null;
};
export const Component = () => {
- return (
-
-
-
- );
+ return ;
};
diff --git a/apps/core/src/pages/workspace/index.tsx b/apps/core/src/pages/workspace/index.tsx
new file mode 100644
index 0000000000..f2406b7c7a
--- /dev/null
+++ b/apps/core/src/pages/workspace/index.tsx
@@ -0,0 +1,29 @@
+import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
+import {
+ currentWorkspaceIdAtom,
+ rootStore,
+} from '@toeverything/plugin-infra/manager';
+import type { ReactElement } from 'react';
+import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';
+
+import { WorkspaceLayout } from '../../layouts/workspace-layout';
+
+export const loader: LoaderFunction = async args => {
+ const meta = await rootStore.get(rootWorkspacesMetadataAtom);
+ if (!meta.some(({ id }) => id === args.params.workspaceId)) {
+ return redirect('/404');
+ }
+ if (args.params.workspaceId) {
+ localStorage.setItem('last_workspace_id', args.params.workspaceId);
+ rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
+ }
+ return null;
+};
+
+export const Component = (): ReactElement => {
+ return (
+
+
+
+ );
+};
diff --git a/apps/core/src/pages/workspace/trash-page.tsx b/apps/core/src/pages/workspace/trash-page.tsx
index e6197f003f..85d7d49604 100644
--- a/apps/core/src/pages/workspace/trash-page.tsx
+++ b/apps/core/src/pages/workspace/trash-page.tsx
@@ -6,9 +6,8 @@ import { getUIAdapter } from '../../adapters/workspace';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
-import { WorkspaceLayout } from '../../layouts/workspace-layout';
-const TrashPage = () => {
+export const TrashPage = () => {
const { jumpToPage } = useNavigateHelper();
const [currentWorkspace] = useCurrentWorkspace();
const onClickPage = useCallback(
@@ -44,9 +43,5 @@ const TrashPage = () => {
};
export const Component = () => {
- return (
-
-
-
- );
+ return ;
};
diff --git a/apps/core/src/router.ts b/apps/core/src/router.ts
new file mode 100644
index 0000000000..444e5fe7dc
--- /dev/null
+++ b/apps/core/src/router.ts
@@ -0,0 +1,37 @@
+import { createBrowserRouter } from 'react-router-dom';
+
+export const router = createBrowserRouter(
+ [
+ {
+ path: '/',
+ lazy: () => import('./pages/index'),
+ },
+ {
+ path: '/workspace/:workspaceId',
+ lazy: () => import('./pages/workspace/index'),
+ children: [
+ {
+ path: 'all',
+ lazy: () => import('./pages/workspace/all-page'),
+ },
+ {
+ path: 'trash',
+ lazy: () => import('./pages/workspace/trash-page'),
+ },
+ {
+ path: ':pageId',
+ lazy: () => import('./pages/workspace/detail-page'),
+ },
+ ],
+ },
+ {
+ path: '/404',
+ lazy: () => import('./pages/404'),
+ },
+ ],
+ {
+ future: {
+ v7_normalizeFormMethod: true,
+ },
+ }
+);
diff --git a/apps/electron/src/helper/db/workspace-db-adapter.ts b/apps/electron/src/helper/db/workspace-db-adapter.ts
index 18d929cdd2..63f1bbbb38 100644
--- a/apps/electron/src/helper/db/workspace-db-adapter.ts
+++ b/apps/electron/src/helper/db/workspace-db-adapter.ts
@@ -52,12 +52,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
};
setupListener(docId?: string) {
- logger.debug(
- 'WorkspaceSQLiteDB:setupListener',
- this.workspaceId,
- docId,
- this.getWorkspaceName()
- );
+ logger.debug('WorkspaceSQLiteDB:setupListener', this.workspaceId, docId);
const doc = this.getDoc(docId);
if (doc) {
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts
index fc826a3562..8cc7294130 100644
--- a/packages/workspace/src/atom.ts
+++ b/packages/workspace/src/atom.ts
@@ -1,11 +1,12 @@
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
-import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { BlockHub } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
import { atom } from 'jotai';
import { z } from 'zod';
+import { getOrCreateWorkspace } from './manager';
+
const rootWorkspaceMetadataV1Schema = z.object({
id: z.string(),
flavour: z.nativeEnum(WorkspaceFlavour),
diff --git a/packages/workspace/src/manager/index.ts b/packages/workspace/src/manager/index.ts
index 308eaaecdd..da82455eb7 100644
--- a/packages/workspace/src/manager/index.ts
+++ b/packages/workspace/src/manager/index.ts
@@ -1,10 +1,6 @@
import { isBrowser, isDesktop } from '@affine/env/constant';
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
import { WorkspaceFlavour } from '@affine/env/workspace';
-import {
- createAffineProviders,
- createLocalProviders,
-} from '@affine/workspace/providers';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { DocProviderCreator, StoreOptions } from '@blocksuite/store';
import {
@@ -18,6 +14,7 @@ import type { Transaction } from 'yjs';
import { createStaticStorage } from '../blob/local-static-storage';
import { createSQLiteStorage } from '../blob/sqlite-blob-storage';
+import { createAffineProviders, createLocalProviders } from '../providers';
function setEditorFlags(workspace: Workspace) {
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
diff --git a/packages/workspace/src/providers/datasource-doc-adapter.ts b/packages/workspace/src/providers/datasource-doc-adapter.ts
deleted file mode 100644
index c0af541c75..0000000000
--- a/packages/workspace/src/providers/datasource-doc-adapter.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export interface DatasourceDocAdapter {
- // request diff update from other clients
- queryDocState: (
- guid: string,
- options?: {
- stateVector?: Uint8Array;
- targetClientId?: number;
- }
- ) => Promise;
-
- // send update to the datasource
- sendDocUpdate: (guid: string, update: Uint8Array) => Promise;
-
- // listen to update from the datasource. Returns a function to unsubscribe.
- // this is optional because some datasource might not support it
- onDocUpdate?(
- callback: (guid: string, update: Uint8Array) => void
- ): () => void;
-}
diff --git a/packages/workspace/src/providers/lazy-provider.ts b/packages/workspace/src/providers/lazy-provider.ts
deleted file mode 100644
index 376c61669c..0000000000
--- a/packages/workspace/src/providers/lazy-provider.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import type { PassiveDocProvider } from '@blocksuite/store';
-import {
- applyUpdate,
- type Doc,
- encodeStateAsUpdate,
- encodeStateVectorFromUpdate,
-} from 'yjs';
-
-import type { DatasourceDocAdapter } from './datasource-doc-adapter';
-
-const selfUpdateOrigin = 'lazy-provider-self-origin';
-
-function getDoc(doc: Doc, guid: string): Doc | undefined {
- if (doc.guid === guid) {
- return doc;
- }
- for (const subdoc of doc.subdocs) {
- const found = getDoc(subdoc, guid);
- if (found) {
- return found;
- }
- }
- return undefined;
-}
-
-/**
- * Creates a lazy provider that connects to a datasource and synchronizes a root document.
- */
-export const createLazyProvider = (
- rootDoc: Doc,
- datasource: DatasourceDocAdapter
-): Omit => {
- let connected = false;
- const pendingMap = new Map(); // guid -> pending-updates
- const disposableMap = new Map void>>();
- let datasourceUnsub: (() => void) | undefined;
-
- async function syncDoc(doc: Doc) {
- const guid = doc.guid;
- // perf: optimize me
- const currentUpdate = encodeStateAsUpdate(doc);
-
- const remoteUpdate = await datasource.queryDocState(guid, {
- stateVector: encodeStateVectorFromUpdate(currentUpdate),
- });
-
- const updates = [currentUpdate];
- pendingMap.set(guid, []);
-
- if (remoteUpdate) {
- applyUpdate(doc, remoteUpdate, selfUpdateOrigin);
- const newUpdate = encodeStateAsUpdate(
- doc,
- encodeStateVectorFromUpdate(remoteUpdate)
- );
- updates.push(newUpdate);
- await datasource.sendDocUpdate(guid, newUpdate);
- }
- }
-
- function setupDocListener(doc: Doc) {
- const disposables = new Set<() => void>();
- disposableMap.set(doc.guid, disposables);
- const updateHandler = async (update: Uint8Array, origin: unknown) => {
- if (origin === selfUpdateOrigin) {
- return;
- }
- datasource.sendDocUpdate(doc.guid, update).catch(console.error);
- };
-
- const subdocLoadHandler = (event: { loaded: Set }) => {
- event.loaded.forEach(subdoc => {
- connectDoc(subdoc).catch(console.error);
- });
- };
-
- doc.on('update', updateHandler);
- doc.on('subdocs', subdocLoadHandler);
- // todo: handle destroy?
- disposables.add(() => {
- doc.off('update', updateHandler);
- doc.off('subdocs', subdocLoadHandler);
- });
- }
-
- function setupDatasourceListeners() {
- datasourceUnsub = datasource.onDocUpdate?.((guid, update) => {
- const doc = getDoc(rootDoc, guid);
- if (doc) {
- applyUpdate(doc, update);
- //
- if (pendingMap.has(guid)) {
- pendingMap.get(guid)?.forEach(update => applyUpdate(doc, update));
- pendingMap.delete(guid);
- }
- } else {
- // This case happens when the father doc is not yet updated,
- // so that the child doc is not yet created.
- // We need to put it into cache so that it can be applied later.
- console.warn('idb: doc not found', guid);
- pendingMap.set(guid, (pendingMap.get(guid) ?? []).concat(update));
- }
- });
- }
-
- // when a subdoc is loaded, we need to sync it with the datasource and setup listeners
- async function connectDoc(doc: Doc) {
- setupDocListener(doc);
- await syncDoc(doc);
- await Promise.all(
- [...doc.subdocs]
- .filter(subdoc => subdoc.shouldLoad)
- .map(subdoc => connectDoc(subdoc))
- );
- }
-
- function disposeAll() {
- disposableMap.forEach(disposables => {
- disposables.forEach(dispose => dispose());
- });
- disposableMap.clear();
- }
-
- function connect() {
- connected = true;
-
- // root doc should be already loaded,
- // but we want to populate the cache for later update events
- connectDoc(rootDoc).catch(console.error);
- setupDatasourceListeners();
- }
-
- async function disconnect() {
- connected = false;
- disposeAll();
- datasourceUnsub?.();
- datasourceUnsub = undefined;
- }
-
- return {
- get connected() {
- return connected;
- },
- passive: true,
- connect,
- disconnect,
- };
-};
diff --git a/packages/workspace/src/providers/sqlite-providers.ts b/packages/workspace/src/providers/sqlite-providers.ts
index 6344056f48..b3e651dc91 100644
--- a/packages/workspace/src/providers/sqlite-providers.ts
+++ b/packages/workspace/src/providers/sqlite-providers.ts
@@ -2,6 +2,7 @@ import type {
SQLiteDBDownloadProvider,
SQLiteProvider,
} from '@affine/env/workspace';
+import { getDoc } from '@affine/y-provider';
import { assertExists } from '@blocksuite/global/utils';
import type { DocProviderCreator } from '@blocksuite/store';
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
@@ -19,6 +20,28 @@ type SubDocsEvent = {
loaded: Set;
};
+// workaround: there maybe new updates before SQLite is connected
+// we need to exchange them with the SQLite db
+// will be removed later when we have lazy load doc provider
+const syncDiff = async (rootDoc: Doc, subdocId?: string) => {
+ try {
+ const workspaceId = rootDoc.guid;
+ const doc = subdocId ? getDoc(rootDoc, subdocId) : rootDoc;
+ if (!doc) {
+ logger.error('doc not found', workspaceId, subdocId);
+ return;
+ }
+ const update = await window.apis?.db.getDocAsUpdates(workspaceId, subdocId);
+ const diff = Y.encodeStateAsUpdate(
+ doc,
+ Y.encodeStateVectorFromUpdate(update)
+ );
+ await window.apis.db.applyDocUpdate(workspaceId, diff, subdocId);
+ } catch (err) {
+ logger.error('failed to sync diff', err);
+ }
+};
+
/**
* A provider that is responsible for syncing updates the workspace with the local SQLite database.
*/
@@ -74,6 +97,9 @@ export const createSQLiteProvider: DocProviderCreator = (
};
function trackDoc(doc: Doc) {
+ syncDiff(rootDoc, rootDoc !== doc ? doc.guid : undefined).catch(
+ logger.error
+ );
doc.on('update', createOrHandleUpdate(doc));
doc.on('subdocs', createOrGetHandleSubDocs(doc));
doc.subdocs.forEach(doc => {
@@ -93,6 +119,9 @@ export const createSQLiteProvider: DocProviderCreator = (
let connected = false;
const connect = () => {
+ if (connected) {
+ return;
+ }
logger.info('connecting sqlite provider', id);
trackDoc(rootDoc);
@@ -161,7 +190,7 @@ export const createSQLiteDBDownloadProvider: DocProviderCreator = (
});
async function syncUpdates(doc: Doc) {
- logger.info('syncing updates from sqlite', id);
+ logger.info('syncing updates from sqlite', doc.guid);
const subdocId = doc.guid === id ? undefined : doc.guid;
const updates = await apis.db.getDocAsUpdates(id, subdocId);
@@ -173,7 +202,10 @@ export const createSQLiteDBDownloadProvider: DocProviderCreator = (
Y.applyUpdate(doc, updates, sqliteOrigin);
}
- const mergedUpdates = Y.encodeStateAsUpdate(doc);
+ const mergedUpdates = Y.encodeStateAsUpdate(
+ doc,
+ Y.encodeStateVectorFromUpdate(updates)
+ );
// also apply updates to sqlite
await apis.db.applyDocUpdate(id, mergedUpdates, subdocId);
diff --git a/packages/y-provider/src/index.ts b/packages/y-provider/src/index.ts
index 2397bb712e..33a55725d7 100644
--- a/packages/y-provider/src/index.ts
+++ b/packages/y-provider/src/index.ts
@@ -1,2 +1,3 @@
export * from './lazy-provider';
export * from './types';
+export * from './utils';
diff --git a/tests/affine-local/e2e/local-first-collections-items.spec.ts b/tests/affine-local/e2e/local-first-collections-items.spec.ts
index 41f6a60a6f..9ecb7b6f1c 100644
--- a/tests/affine-local/e2e/local-first-collections-items.spec.ts
+++ b/tests/affine-local/e2e/local-first-collections-items.spec.ts
@@ -42,6 +42,7 @@ const createAndPinCollection = async (
await page.getByTestId('collection-bar-option-pin').click();
await page.waitForTimeout(100);
};
+
test('Show collections items in sidebar', async ({ page }) => {
await createAndPinCollection(page);
const collections = page.getByTestId('collections');
diff --git a/tests/affine-local/e2e/router.spec.ts b/tests/affine-local/e2e/router.spec.ts
index 0d969a9666..a632abc374 100644
--- a/tests/affine-local/e2e/router.spec.ts
+++ b/tests/affine-local/e2e/router.spec.ts
@@ -15,10 +15,9 @@ test('goto not found page', async ({ page }) => {
test('goto not found workspace', async ({ page }) => {
await openHomePage(page);
await waitEditorLoad(page);
- const currentUrl = page.url();
// if doesn't wait for timeout, data won't be saved into indexedDB
await page.waitForTimeout(1000);
await page.goto(new URL('/workspace/invalid/all', webUrl).toString());
- await waitEditorLoad(page);
- expect(page.url()).toEqual(currentUrl);
+ await page.waitForTimeout(1000);
+ expect(page.url()).toBe(new URL('/404', webUrl).toString());
});