fix(core): properties adapter reactivitiy issue (#5661)

This commit is contained in:
Peng Xiao
2024-01-23 01:41:44 +00:00
parent 03b60a63cd
commit ecdb5b3407
6 changed files with 51 additions and 34 deletions
@@ -13,9 +13,9 @@ import { createAffineCloudBlobStorage } from '@affine/workspace-impl';
import { assertEquals } from '@blocksuite/global/utils'; import { assertEquals } from '@blocksuite/global/utils';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import { revertUpdate } from '@toeverything/y-indexeddb'; import { revertUpdate } from '@toeverything/y-indexeddb';
import { useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import useSWRImmutable from 'swr/immutable'; import useSWRImmutable from 'swr/immutable';
import { applyUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { import {
useMutateQueryResource, useMutateQueryResource,
@@ -105,7 +105,7 @@ const snapshotFetcher = async (
const workspaceMap = new Map<string, Workspace>(); const workspaceMap = new Map<string, Workspace>();
// assume the workspace is a cloud workspace since the history feature is only enabled for cloud workspace // assume the workspace is a cloud workspace since the history feature is only enabled for cloud workspace
const getOrCreateWorkspace = (workspaceId: string) => { const getOrCreateShellWorkspace = (workspaceId: string) => {
let workspace = workspaceMap.get(workspaceId); let workspace = workspaceMap.get(workspaceId);
if (!workspace) { if (!workspace) {
const blobStorage = createAffineCloudBlobStorage(workspaceId); const blobStorage = createAffineCloudBlobStorage(workspaceId);
@@ -120,6 +120,7 @@ const getOrCreateWorkspace = (workspaceId: string) => {
schema: globalBlockSuiteSchema, schema: globalBlockSuiteSchema,
}); });
workspaceMap.set(workspaceId, workspace); workspaceMap.set(workspaceId, workspace);
workspace.doc.emit('sync', []);
} }
return workspace; return workspace;
}; };
@@ -143,17 +144,17 @@ export const usePageHistory = (
// workspace id + page id + timestamp + snapshot -> Page (to be used for rendering in blocksuite editor) // workspace id + page id + timestamp + snapshot -> Page (to be used for rendering in blocksuite editor)
export const useSnapshotPage = ( export const useSnapshotPage = (
workspaceId: string, workspace: Workspace,
pageDocId: string, pageDocId: string,
ts?: string ts?: string
) => { ) => {
const snapshot = usePageHistory(workspaceId, pageDocId, ts); const snapshot = usePageHistory(workspace.id, pageDocId, ts);
const page = useMemo(() => { const page = useMemo(() => {
if (!ts) { if (!ts) {
return; return;
} }
const pageId = pageDocId + '-' + ts; const pageId = pageDocId + '-' + ts;
const historyShellWorkspace = getOrCreateWorkspace(workspaceId); const historyShellWorkspace = getOrCreateShellWorkspace(workspace.id);
let page = historyShellWorkspace.getPage(pageId); let page = historyShellWorkspace.getPage(pageId);
if (!page && snapshot) { if (!page && snapshot) {
page = historyShellWorkspace.createPage({ page = historyShellWorkspace.createPage({
@@ -169,7 +170,15 @@ export const useSnapshotPage = (
.catch(console.error); // must load before applyUpdate .catch(console.error); // must load before applyUpdate
} }
return page ?? undefined; return page ?? undefined;
}, [pageDocId, snapshot, ts, workspaceId]); }, [pageDocId, snapshot, ts, workspace]);
useEffect(() => {
const historyShellWorkspace = getOrCreateShellWorkspace(workspace.id);
// apply the rootdoc's update to the current workspace
// this makes sure the page reference links are not deleted ones in the preview
const update = encodeStateAsUpdate(workspace.doc);
applyUpdate(historyShellWorkspace.doc, update);
}, [workspace]);
return page; return page;
}; };
@@ -5,6 +5,7 @@ import { ConfirmModal, Modal } from '@affine/component/ui/modal';
import { openSettingModalAtom, type PageMode } from '@affine/core/atoms'; import { openSettingModalAtom, type PageMode } from '@affine/core/atoms';
import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner'; import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-owner';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useBlockSuiteWorkspacePageTitle } from '@affine/core/hooks/use-block-suite-workspace-page-title';
import { useUserSubscription } from '@affine/core/hooks/use-subscription'; import { useUserSubscription } from '@affine/core/hooks/use-subscription';
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace'; import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
import { timestampToLocalTime } from '@affine/core/utils'; import { timestampToLocalTime } from '@affine/core/utils';
@@ -418,7 +419,7 @@ const PageHistoryManager = ({
return workspace.getPage(pageId)?.spaceDoc.guid ?? pageId; return workspace.getPage(pageId)?.spaceDoc.guid ?? pageId;
}, [pageId, workspace]); }, [pageId, workspace]);
const snapshotPage = useSnapshotPage(workspaceId, pageDocId, activeVersion); const snapshotPage = useSnapshotPage(workspace, pageDocId, activeVersion);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@@ -440,10 +441,7 @@ const PageHistoryManager = ({
const defaultPreviewPageMode = useAtomValue(currentModeAtom); const defaultPreviewPageMode = useAtomValue(currentModeAtom);
const [mode, setMode] = useState<PageMode>(defaultPreviewPageMode); const [mode, setMode] = useState<PageMode>(defaultPreviewPageMode);
const title = useMemo( const title = useBlockSuiteWorkspacePageTitle(workspace, pageId);
() => workspace.getPage(pageId)?.meta.title || t['Untitled'](),
[pageId, t, workspace]
);
const [showRestoreConfirmModal, setShowRestoreConfirmModal] = useState(false); const [showRestoreConfirmModal, setShowRestoreConfirmModal] = useState(false);
@@ -51,6 +51,8 @@ beforeEach(async () => {
blockSuiteWorkspace = workspace.blockSuiteWorkspace; blockSuiteWorkspace = workspace.blockSuiteWorkspace;
blockSuiteWorkspace.doc.emit('sync', []);
const initPage = async (page: Page) => { const initPage = async (page: Page) => {
await page.waitForLoaded(); await page.waitForLoaded();
expect(page).not.toBeNull(); expect(page).not.toBeNull();
@@ -1,20 +1,25 @@
import type { Workspace } from '@blocksuite/store'; import type { Workspace } from '@blocksuite/store';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useEffect, useMemo, useReducer } from 'react'; import { useEffect, useState } from 'react';
import type { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
import { import {
currentWorkspacePropertiesAdapterAtom, currentWorkspacePropertiesAdapterAtom,
WorkspacePropertiesAdapter, workspaceAdapterAtomFamily,
} from '../modules/workspace/properties'; } from '../modules/workspace/properties';
function getProxy<T extends object>(obj: T) {
return new Proxy(obj, {});
}
const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => { const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
const [, forceUpdate] = useReducer(c => c + 1, 0); const [proxy, setProxy] = useState(adapter);
useEffect(() => { useEffect(() => {
// todo: track which properties are used and then filter by property path change // todo: track which properties are used and then filter by property path change
// using Y.YEvent.path // using Y.YEvent.path
function observe() { function observe() {
forceUpdate(); setProxy(getProxy(adapter));
} }
adapter.properties.observeDeep(observe); adapter.properties.observeDeep(observe);
return () => { return () => {
@@ -22,7 +27,7 @@ const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
}; };
}, [adapter]); }, [adapter]);
return adapter; return proxy;
}; };
export function useCurrentWorkspacePropertiesAdapter() { export function useCurrentWorkspacePropertiesAdapter() {
@@ -31,9 +36,6 @@ export function useCurrentWorkspacePropertiesAdapter() {
} }
export function useWorkspacePropertiesAdapter(workspace: Workspace) { export function useWorkspacePropertiesAdapter(workspace: Workspace) {
const adapter = useMemo( const adapter = useAtomValue(workspaceAdapterAtomFamily(workspace));
() => new WorkspacePropertiesAdapter(workspace),
[workspace]
);
return useReactiveAdapter(adapter); return useReactiveAdapter(adapter);
} }
@@ -1,16 +1,21 @@
import type { Workspace } from '@affine/workspace/workspace'; import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import { atomWithObservable } from 'jotai/utils'; import { atom } from 'jotai';
import { filter, map, of } from 'rxjs'; import { atomFamily } from 'jotai/utils';
import { currentWorkspaceAtom } from '../atoms'; import { waitForCurrentWorkspaceAtom } from '../atoms';
import { WorkspacePropertiesAdapter } from './adapter'; import { WorkspacePropertiesAdapter } from './adapter';
export const currentWorkspacePropertiesAdapterAtom = // todo: remove the inner atom when workspace is closed by using workspaceAdapterAtomFamily.remove
atomWithObservable<WorkspacePropertiesAdapter>(get => { export const workspaceAdapterAtomFamily = atomFamily(
return of(get(currentWorkspaceAtom)).pipe( (workspace: BlockSuiteWorkspace) => {
filter((workspace): workspace is Workspace => !!workspace), return atom(async () => {
map(workspace => { await workspace.doc.whenLoaded;
return new WorkspacePropertiesAdapter(workspace.blockSuiteWorkspace); return new WorkspacePropertiesAdapter(workspace);
}) });
); }
}); );
export const currentWorkspacePropertiesAdapterAtom = atom(async get => {
const workspace = await get(waitForCurrentWorkspaceAtom);
return get(workspaceAdapterAtomFamily(workspace.blockSuiteWorkspace));
});
@@ -99,6 +99,7 @@ export const loader: LoaderFunction = async ({ params }) => {
assertDownloadResponse(response); assertDownloadResponse(response);
const { arrayBuffer } = response; const { arrayBuffer } = response;
applyUpdate(workspace.doc, new Uint8Array(arrayBuffer)); applyUpdate(workspace.doc, new Uint8Array(arrayBuffer));
workspace.doc.emit('sync', []);
} }
const page = workspace.getPage(pageId); const page = workspace.getPage(pageId);
assertExists(page, 'cannot find page'); assertExists(page, 'cannot find page');