refactor(core): use manual upgrade to replace auto migration when web setup (#5022)

1. Split logic in `packages/common/infra/src/blocksuite/index.ts` to multiple single files
2. Move migration logic from setup to upgrade module, to prevent auto migration problems and loading problem
This commit is contained in:
Joooye_34
2023-11-23 02:26:06 +00:00
parent 3710bcdc14
commit 4c8d54b3a7
21 changed files with 947 additions and 1026 deletions

View File

@@ -13,10 +13,7 @@ import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
@@ -47,7 +44,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
buildShowcaseWorkspace(blockSuiteWorkspace, {
schema: globalBlockSuiteSchema,
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,

View File

@@ -1,129 +1,18 @@
import { setupGlobal } from '@affine/env/global';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { assertExists } from '@blocksuite/global/utils';
import {
migrateLocalBlobStorage,
migrateWorkspace,
WorkspaceVersion,
} from '@toeverything/infra/blocksuite';
import { downloadBinary, overwriteBinary } from '@toeverything/y-indexeddb';
import type { createStore } from 'jotai/vanilla';
import { nanoid } from 'nanoid';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { WorkspaceAdapters } from '../adapters/workspace';
import { performanceLogger } from '../shared';
const performanceSetupLogger = performanceLogger.namespace('setup');
async function tryMigration() {
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (oldMeta.flavour === WorkspaceFlavour.LOCAL) {
let doc: YDoc;
const options = {
getCurrentRootDoc: async () => {
doc = new YDoc({
guid: oldMeta.id,
});
const downloadWorkspace = async (doc: YDoc): Promise<void> => {
const binary = await downloadBinary(doc.guid);
if (binary) {
applyUpdate(doc, binary);
}
await Promise.all(
[...doc.subdocs.values()].map(subdoc =>
downloadWorkspace(subdoc)
)
);
};
await downloadWorkspace(doc);
return doc;
},
createWorkspace: async () =>
getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL),
getSchema: () => globalBlockSuiteSchema,
};
promises.push(
migrateWorkspace(
'version' in oldMeta ? oldMeta.version : undefined,
options
).then(async status => {
if (typeof status !== 'boolean') {
const adapter = WorkspaceAdapters[oldMeta.flavour];
const oldWorkspace = await adapter.CRUD.get(oldMeta.id);
const newId = await adapter.CRUD.create(status);
assertExists(
oldWorkspace,
'workspace should exist after migrate'
);
await adapter.CRUD.delete(oldWorkspace.blockSuiteWorkspace);
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.Surface,
};
await migrateLocalBlobStorage(status.id, newId);
console.log('workspace migrated', oldMeta.id, newId);
} else if (status) {
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.Surface,
};
const overWrite = async (doc: YDoc): Promise<void> => {
await overwriteBinary(doc.guid, encodeStateAsUpdate(doc));
return Promise.all(
[...doc.subdocs.values()].map(subdoc => overWrite(subdoc))
).then();
};
await overWrite(doc);
console.log('workspace migrated', oldMeta.id);
}
})
);
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(e => {
console.error('migration failed', e);
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
}
export function createFirstAppData(store: ReturnType<typeof createStore>) {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
@@ -136,7 +25,6 @@ export function createFirstAppData(store: ReturnType<typeof createStore>) {
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
version: WorkspaceVersion.DatabaseV3,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
@@ -163,9 +51,6 @@ export async function setup(store: ReturnType<typeof createStore>) {
performanceSetupLogger.info('setup global');
setupGlobal();
performanceSetupLogger.info('try migration');
await tryMigration();
performanceSetupLogger.info('get root workspace meta');
// do not read `rootWorkspacesMetadataAtom` before migration
await store.get(rootWorkspacesMetadataAtom);

View File

@@ -1,42 +1,122 @@
import { forceUpgradePages } from '@toeverything/infra/blocksuite';
import { useCallback, useState } from 'react';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Workspace } from '@blocksuite/store';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
import {
migrateLocalBlobStorage,
migrateWorkspace,
} from '@toeverything/infra/blocksuite';
import { nanoid } from 'nanoid';
import { useState } from 'react';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { WorkspaceAdapters } from '../../adapters/workspace';
import { useCurrentSyncEngine } from '../../hooks/current/use-current-sync-engine';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
export type UpgradeState = 'pending' | 'upgrading' | 'done' | 'error';
export function useUpgradeWorkspace() {
function applyDoc(target: YDoc, result: YDoc) {
applyUpdate(target, encodeStateAsUpdate(result));
for (const targetSubDoc of target.subdocs.values()) {
const resultSubDocs = Array.from(result.subdocs.values());
const resultSubDoc = resultSubDocs.find(
item => item.guid === targetSubDoc.guid
);
if (resultSubDoc) {
applyDoc(targetSubDoc, resultSubDoc);
}
}
}
export function useUpgradeWorkspace(migration: MigrationPoint) {
const [state, setState] = useState<UpgradeState>('pending');
const [error, setError] = useState<Error | null>(null);
const [newWorkspaceId, setNewWorkspaceId] = useState<string | null>(null);
const [workspace] = useCurrentWorkspace();
const syncEngine = useCurrentSyncEngine();
const rootStore = getCurrentStore();
const upgradeWorkspace = useCallback(() => {
const upgradeWorkspace = useAsyncCallback(async () => {
setState('upgrading');
setError(null);
(async () => {
try {
// Migration need to wait for root doc and all subdocs loaded.
await syncEngine?.waitForSynced();
await forceUpgradePages({
getCurrentRootDoc: async () => workspace.blockSuiteWorkspace.doc,
getSchema: () => workspace.blockSuiteWorkspace.schema,
// Clone a new doc to prevent change events.
const clonedDoc = new YDoc({
guid: workspace.blockSuiteWorkspace.doc.guid,
});
applyDoc(clonedDoc, workspace.blockSuiteWorkspace.doc);
const schema = workspace.blockSuiteWorkspace.schema;
let newWorkspace: Workspace | null = null;
const resultDoc = await migrateWorkspace(migration, {
doc: clonedDoc,
schema,
createWorkspace: () => {
// Migrate to subdoc version need to create a new workspace.
// It will only happened for old local workspace.
newWorkspace = getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL);
return Promise.resolve(newWorkspace);
},
});
if (newWorkspace) {
const localMetaString =
localStorage.getItem('jotai-workspaces') ?? '[]';
const localMetadataList = JSON.parse(
localMetaString
) as RootWorkspaceMetadata[];
const currentLocalMetadata = localMetadataList.find(
item => item.id === workspace.id
);
const flavour = currentLocalMetadata?.flavour ?? WorkspaceFlavour.LOCAL;
// Legacy logic moved from `setup.ts`.
// It works well before, should be refactor or remove in the future.
const adapter = WorkspaceAdapters[flavour];
const newId = await adapter.CRUD.create(newWorkspace);
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(newId);
await rootStore.get(workspaceAtom); // Trigger provider sync to persist data.
await adapter.CRUD.delete(workspace.blockSuiteWorkspace);
await migrateLocalBlobStorage(workspace.id, newId);
setNewWorkspaceId(newId);
const index = localMetadataList.findIndex(
meta => meta.id === workspace.id
);
localMetadataList[index] = {
...currentLocalMetadata,
id: newId,
flavour,
};
localStorage.setItem(
'jotai-workspaces',
JSON.stringify(localMetadataList)
);
localStorage.setItem('last_workspace_id', newId);
localStorage.removeItem('last_page_id');
} else {
applyDoc(workspace.blockSuiteWorkspace.doc, resultDoc);
}
await syncEngine?.waitForSynced();
setState('done');
})().catch((e: any) => {
} catch (e: any) {
console.error(e);
setError(e);
setState('error');
});
}, [
workspace.blockSuiteWorkspace.doc,
workspace.blockSuiteWorkspace.schema,
syncEngine,
]);
}
}, [rootStore, workspace, syncEngine, migration]);
return [state, error, upgradeWorkspace] as const;
return [state, error, upgradeWorkspace, newWorkspaceId] as const;
}

View File

@@ -1,8 +1,10 @@
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
import { useCallback, useMemo } from 'react';
import { pathGenerator } from '../../shared';
import * as styles from './upgrade.css';
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
@@ -32,11 +34,18 @@ function UpgradeIcon({ upgradeState }: { upgradeState: UpgradeState }) {
);
}
interface WorkspaceUpgradeProps {
migration: MigrationPoint;
}
/**
* TODO: Help info is not implemented yet.
*/
export const WorkspaceUpgrade = function MigrationFallback() {
const [upgradeState, , upgradeWorkspace] = useUpgradeWorkspace();
export const WorkspaceUpgrade = function WorkspaceUpgrade(
props: WorkspaceUpgradeProps
) {
const [upgradeState, , upgradeWorkspace, newWorkspaceId] =
useUpgradeWorkspace(props.migration);
const t = useAFFiNEI18N();
const refreshPage = useCallback(() => {
@@ -45,6 +54,12 @@ export const WorkspaceUpgrade = function MigrationFallback() {
const onButtonClick = useMemo(() => {
if (upgradeState === 'done') {
if (newWorkspaceId) {
return () => {
window.location.replace(pathGenerator.all(newWorkspaceId));
};
}
return refreshPage;
}
@@ -53,7 +68,7 @@ export const WorkspaceUpgrade = function MigrationFallback() {
}
return undefined;
}, [upgradeState, upgradeWorkspace, refreshPage]);
}, [upgradeState, upgradeWorkspace, refreshPage, newWorkspaceId]);
return (
<div className={styles.layout}>

View File

@@ -2,10 +2,7 @@ import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
@@ -76,7 +73,6 @@ export function useAppHelper() {
WorkspaceFlavour.LOCAL
);
await buildShowcaseWorkspace(blockSuiteWorkspace, {
schema: globalBlockSuiteSchema,
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,

View File

@@ -27,6 +27,7 @@ import {
} from '@dnd-kit/core';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { PropsWithChildren, ReactNode } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
@@ -112,12 +113,12 @@ export const CurrentWorkspaceContext = ({
};
type WorkspaceLayoutProps = {
incompatible?: boolean;
migration?: MigrationPoint;
};
export const WorkspaceLayout = function WorkspacesSuspense({
children,
incompatible = false,
migration,
}: PropsWithChildren<WorkspaceLayoutProps>) {
return (
<AdapterProviderWrapper>
@@ -128,7 +129,7 @@ export const WorkspaceLayout = function WorkspacesSuspense({
<CurrentWorkspaceModals />
</Suspense>
<Suspense fallback={<WorkspaceFallback />}>
<WorkspaceLayoutInner incompatible={incompatible}>
<WorkspaceLayoutInner migration={migration}>
{children}
</WorkspaceLayoutInner>
</Suspense>
@@ -139,7 +140,7 @@ export const WorkspaceLayout = function WorkspacesSuspense({
export const WorkspaceLayoutInner = ({
children,
incompatible = false,
migration,
}: PropsWithChildren<WorkspaceLayoutProps>) => {
const [currentWorkspace] = useCurrentWorkspace();
const { openPage } = useNavigateHelper();
@@ -262,7 +263,11 @@ export const WorkspaceLayoutInner = ({
padding={appSettings.clientBorder}
inTrashPage={inTrashPage}
>
{incompatible ? <WorkspaceUpgrade /> : children}
{migration ? (
<WorkspaceUpgrade migration={migration} />
) : (
children
)}
<ToolContainer inTrashPage={inTrashPage}>
<RootBlockHub />
<HelpIsland showList={pageId ? undefined : showList} />

View File

@@ -1,4 +1,3 @@
import { WorkspaceFlavour } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import {
@@ -6,7 +5,11 @@ import {
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import { guidCompatibilityFix } from '@toeverything/infra/blocksuite';
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
import {
checkWorkspaceCompatibility,
guidCompatibilityFix,
} from '@toeverything/infra/blocksuite';
import { useSetAtom } from 'jotai';
import { type ReactElement, useEffect } from 'react';
import {
@@ -49,22 +52,9 @@ export const loader: LoaderFunction = async args => {
const workspace = await rootStore.get(workspaceAtom);
workspaceLoaderLogger.info('workspace loaded');
if (currentMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return (() => {
guidCompatibilityFix(workspace.doc);
const blockVersions = workspace.meta.blockVersions;
if (!blockVersions) {
return true;
}
for (const [flavour, schema] of workspace.schema.flavourSchemaMap) {
if (blockVersions[flavour] !== schema.version) {
return true;
}
}
return false;
})();
}
return null;
guidCompatibilityFix(workspace.doc);
return checkWorkspaceCompatibility(workspace);
};
export const Component = (): ReactElement => {
@@ -81,10 +71,10 @@ export const Component = (): ReactElement => {
}
}, [params, setCurrentWorkspaceId]);
const incompatible = useLoaderData();
const migration = useLoaderData() as MigrationPoint | undefined;
return (
<AffineErrorBoundary height="100vh">
<WorkspaceLayout incompatible={!!incompatible}>
<WorkspaceLayout migration={migration}>
<Outlet />
</WorkspaceLayout>
</AffineErrorBoundary>