mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 03:26:47 +08: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:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user