mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user