feat: support migration (#2852)

This commit is contained in:
Alex Yang
2023-06-26 15:55:44 +08:00
committed by GitHub
parent 002e64c819
commit 8e82d1e02c
14 changed files with 395 additions and 119 deletions
+2 -1
View File
@@ -5,7 +5,7 @@ import 'fake-indexeddb/auto';
import { initEmptyPage } from '@affine/env/blocksuite'; import { initEmptyPage } from '@affine/env/blocksuite';
import type { LocalIndexedDBBackgroundProvider } from '@affine/env/workspace'; import type { LocalIndexedDBBackgroundProvider } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import { import {
rootCurrentWorkspaceIdAtom, rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom, rootWorkspacesMetadataAtom,
@@ -96,6 +96,7 @@ describe('currentWorkspace atom', () => {
{ {
id: workspaceId, id: workspaceId,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.SubDoc,
}, },
]); ]);
_cleanupBlockSuiteWorkspaceCache(); _cleanupBlockSuiteWorkspaceCache();
+26 -22
View File
@@ -1,6 +1,6 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import type { RootWorkspaceMetadataV2 } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomFamily, atomWithStorage } from 'jotai/utils'; import { atomFamily, atomWithStorage } from 'jotai/utils';
@@ -13,7 +13,7 @@ const logger = new DebugLogger('web:atoms');
// workspace necessary atoms // workspace necessary atoms
// todo(himself65): move this to the workspace package // todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => { rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] { function createFirst(): RootWorkspaceMetadataV2[] {
const Plugins = Object.values(WorkspaceAdapters).sort( const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority (a, b) => a.loadPriority - b.loadPriority
); );
@@ -24,29 +24,33 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
({ ({
id, id,
flavour: Plugin.flavour, flavour: Plugin.flavour,
} satisfies RootWorkspaceMetadata) // new workspace should all support sub-doc feature
version: WorkspaceVersion.SubDoc,
} satisfies RootWorkspaceMetadataV2)
); );
}).filter((ids): ids is RootWorkspaceMetadata => !!ids); }).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
} }
const abortController = new AbortController(); const abortController = new AbortController();
// next tick to make sure the hydration is correct if (!environment.isServer) {
const id = setTimeout(() => { // next tick to make sure the hydration is correct
setAtom(metadata => { setTimeout(() => {
if (abortController.signal.aborted) return metadata; setAtom(metadata => {
if ( if (abortController.signal.aborted) return metadata;
metadata.length === 0 && if (
localStorage.getItem('is-first-open') === null metadata.length === 0 &&
) { localStorage.getItem('is-first-open') === null
localStorage.setItem('is-first-open', 'false'); ) {
const newMetadata = createFirst(); localStorage.setItem('is-first-open', 'false');
logger.info('create first workspace', newMetadata); const newMetadata = createFirst();
return newMetadata; logger.info('create first workspace', newMetadata);
} return newMetadata;
return metadata; }
}); return metadata;
}, 0); });
}, 0);
}
if (environment.isDesktop) { if (environment.isDesktop) {
window.apis?.workspace window.apis?.workspace
@@ -56,6 +60,7 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
const newMetadata = workspaceIDs.map(w => ({ const newMetadata = workspaceIDs.map(w => ({
id: w[0], id: w[0],
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
version: undefined,
})); }));
setAtom(metadata => { setAtom(metadata => {
return [ return [
@@ -70,7 +75,6 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
} }
return () => { return () => {
clearTimeout(id);
abortController.abort(); abortController.abort();
}; };
}; };
+75 -51
View File
@@ -18,60 +18,73 @@ const logger = new DebugLogger('web:atoms:root');
/** /**
* Fetch all workspaces from the Plugin CRUD * Fetch all workspaces from the Plugin CRUD
*/ */
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => { export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
const { WorkspaceAdapters } = await import('../adapters/workspace'); async (get, { signal }) => {
const flavours: string[] = Object.values(WorkspaceAdapters).map( const { WorkspaceAdapters } = await import('../adapters/workspace');
plugin => plugin.flavour const flavours: string[] = Object.values(WorkspaceAdapters).map(
); plugin => plugin.flavour
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
.filter(
workspace => flavours.includes(workspace.flavour)
// TODO: remove this when we remove the legacy cloud
)
.filter(workspace =>
!config.enableLegacyCloud
? workspace.flavour !== WorkspaceFlavour.AFFINE
: true
); );
const workspaces = await Promise.all( const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
jotaiWorkspaces.map(workspace => { .filter(
const plugin = workspace => flavours.includes(workspace.flavour)
WorkspaceAdapters[workspace.flavour as keyof typeof WorkspaceAdapters]; // TODO: remove this when we remove the legacy cloud
assertExists(plugin); )
const { CRUD } = plugin; .filter(workspace =>
return CRUD.get(workspace.id).then(workspace => { !config.enableLegacyCloud
if (workspace === null) { ? workspace.flavour !== WorkspaceFlavour.AFFINE
console.warn( : true
'workspace is null. this should not happen. If you see this error, please report it to the developer.' );
); if (jotaiWorkspaces.some(meta => meta.version === undefined)) {
} // wait until all workspaces have migrated to v2
return workspace; await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject);
setTimeout(resolve, 1000);
}).catch(() => {
// do nothing
}); });
})
).then(workspaces =>
workspaces.filter(
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] =>
workspace !== null
)
);
const workspaceProviders = workspaces.map(workspace =>
workspace.blockSuiteWorkspace.providers.filter(
(provider): provider is ActiveDocProvider =>
'active' in provider && provider.active
)
);
const promises: Promise<void>[] = [];
for (const providers of workspaceProviders) {
for (const provider of providers) {
provider.sync();
promises.push(provider.whenReady);
} }
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspaceAdapters[
workspace.flavour as keyof typeof WorkspaceAdapters
];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id).then(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
return workspace;
});
})
).then(workspaces =>
workspaces.filter(
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] =>
workspace !== null
)
);
const workspaceProviders = workspaces.map(workspace =>
workspace.blockSuiteWorkspace.providers.filter(
(provider): provider is ActiveDocProvider =>
'active' in provider && provider.active
)
);
const promises: Promise<void>[] = [];
for (const providers of workspaceProviders) {
for (const provider of providers) {
provider.sync();
promises.push(provider.whenReady);
}
}
// we will wait for all the necessary providers to be ready
await Promise.all(promises);
logger.info('workspaces', workspaces);
return workspaces;
} }
// we will wait for all the necessary providers to be ready );
await Promise.all(promises);
logger.info('workspaces', workspaces);
return workspaces;
});
/** /**
* This will throw an error if the workspace is not found, * This will throw an error if the workspace is not found,
@@ -79,7 +92,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
* use `rootCurrentWorkspaceIdAtom` instead * use `rootCurrentWorkspaceIdAtom` instead
*/ */
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>( export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async get => { async (get, { signal }) => {
const { WorkspaceAdapters } = await import('../adapters/workspace'); const { WorkspaceAdapters } = await import('../adapters/workspace');
const metadata = get(rootWorkspacesMetadataAtom); const metadata = get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom); const targetId = get(rootCurrentWorkspaceIdAtom);
@@ -92,6 +105,17 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
if (!targetWorkspace) { if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`); throw new Error(`cannot find the workspace with id ${targetId}.`);
} }
if (!targetWorkspace.version) {
// wait until the workspace has migrated to v2
await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject);
setTimeout(resolve, 1000);
}).catch(() => {
// do nothing
});
}
const workspace = await WorkspaceAdapters[targetWorkspace.flavour].CRUD.get( const workspace = await WorkspaceAdapters[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id targetWorkspace.id
); );
+77
View File
@@ -1,4 +1,18 @@
import { migrateToSubdoc } from '@affine/env/blocksuite';
import { config, setupGlobal } from '@affine/env/config'; import { config, setupGlobal } from '@affine/env/config';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/plugin-infra/manager';
import { WorkspaceAdapters } from '../adapters/workspace';
setupGlobal(); setupGlobal();
@@ -32,3 +46,66 @@ if (!environment.isDesktop && !environment.isServer) {
writable: false, writable: false,
}); });
} }
rootStore.sub(rootWorkspacesMetadataAtom, () => {
const metadata = rootStore.get(rootWorkspacesMetadataAtom);
metadata.forEach(oldMeta => {
if (!oldMeta.version) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(workspace.id, doc, {
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness,
}) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
rootStore.set(rootWorkspacesMetadataAtom, metadata =>
metadata.map(newMeta =>
newMeta.id === oldMeta.id
? {
...newMeta,
version: WorkspaceVersion.SubDoc,
}
: newMeta
)
);
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
await migrateLocalBlobStorage(workspace.id, newId);
rootStore.set(rootWorkspacesMetadataAtom, metadata => [
...metadata
.map(newMeta => (newMeta.id === oldMeta.id ? null : newMeta))
.filter((meta): meta is RootWorkspaceMetadata => !!meta),
{
id: newId,
flavour: oldMeta.flavour,
version: WorkspaceVersion.SubDoc,
},
]);
};
// create a new workspace and push it to metadata
upgrade().catch(console.error);
}
});
});
@@ -1,5 +1,6 @@
import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import type { WorkspaceRegistry } from '@affine/env/workspace'; import type { WorkspaceRegistry } from '@affine/env/workspace';
import { WorkspaceVersion } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -7,7 +8,7 @@ import { useCallback } from 'react';
import { WorkspaceAdapters } from '../adapters/workspace'; import { WorkspaceAdapters } from '../adapters/workspace';
/** /**
* Transform workspace from one flavour to another * Transform workspace from one flavor to another
* *
* The logic here is to delete the old workspace and create a new one. * The logic here is to delete the old workspace and create a new one.
*/ */
@@ -29,6 +30,7 @@ export function useTransformWorkspace() {
workspaces.splice(idx, 1, { workspaces.splice(idx, 1, {
id: newId, id: newId,
flavour: to, flavour: to,
version: WorkspaceVersion.SubDoc,
}); });
return [...workspaces]; return [...workspaces];
}); });
+3 -1
View File
@@ -1,5 +1,5 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud'; import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -34,6 +34,7 @@ export function useAppHelper() {
{ {
id: workspaceId, id: workspaceId,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.SubDoc,
}, },
]); ]);
logger.debug('imported local workspace', workspaceId); logger.debug('imported local workspace', workspaceId);
@@ -54,6 +55,7 @@ export function useAppHelper() {
{ {
id, id,
flavour: WorkspaceFlavour.LOCAL, flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.SubDoc,
}, },
]); ]);
logger.debug('created local workspace', id); logger.debug('created local workspace', id);
+7 -1
View File
@@ -267,7 +267,13 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
); );
}); });
} }
items.push(...item.map(x => ({ id: x.id, flavour: x.flavour }))); items.push(
...item.map(x => ({
id: x.id,
flavour: x.flavour,
version: undefined,
}))
);
} catch (e) { } catch (e) {
logger.error('list data error:', e); logger.error('list data error:', e);
} }
+118 -41
View File
@@ -1,58 +1,119 @@
import { BlockSuiteEditor } from '@affine/component/block-suite-editor'; import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import { migrateToSubdoc } from '@affine/env/blocksuite'; import type {
LocalIndexedDBDownloadProvider,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { Workspace } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store';
import { NoSsr } from '@mui/material'; import { NoSsr } from '@mui/material';
import {
atom,
createStore,
Provider,
useAtom,
useAtomValue,
useSetAtom,
} from 'jotai';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { use, useCallback } from 'react'; import { Suspense, use, useCallback } from 'react';
import * as Y from 'yjs';
const { default: json } = await import('@affine-test/fixtures/output.json');
const workspace = new Workspace({ const store = createStore();
id: 'test-migration',
isSSR: typeof window === 'undefined', const workspaceIdsAtom = atom<Promise<string[]>>(async () => {
if (typeof window === 'undefined') {
return [];
} else {
const idb = await import('idb');
const db = await idb.openDB('affine-local', 1);
return (await db
.transaction('workspace')
.objectStore('workspace')
.getAllKeys()) as string[];
}
}); });
const finalWorkspace = new Workspace({ const targetIdAtom = atom<string | null>(null);
id: 'test-migration-final',
isSSR: typeof window === 'undefined',
});
finalWorkspace.register(AffineSchemas).register(__unstableSchemas); const workspaceAtom = atom<Promise<Workspace>>(async get => {
workspace.register(AffineSchemas).register(__unstableSchemas); const id = get(targetIdAtom);
if (!id) {
if (typeof window !== 'undefined') { throw new Error('no id');
const length = Object.keys(json).length;
const binary = new Uint8Array(length);
for (let i = 0; i < length; i++) {
binary[i] = (json as any)[i];
} }
Y.applyUpdate(workspace.doc, binary); const workspace = new Workspace({
{ id,
// invoke data isSSR: typeof window === 'undefined',
workspace.doc.getMap('space:hello-world');
workspace.doc.getMap('space:meta');
}
const newDoc = migrateToSubdoc(workspace.doc);
Y.applyUpdate(finalWorkspace.doc, Y.encodeStateAsUpdate(newDoc));
finalWorkspace.doc.subdocs.forEach(finalSubdoc => {
newDoc.subdocs.forEach(subdoc => {
if (subdoc.guid === finalSubdoc.guid) {
Y.applyUpdate(finalSubdoc, Y.encodeStateAsUpdate(subdoc));
}
});
}); });
}
const MigrationInner = () => { workspace.register(AffineSchemas).register(__unstableSchemas);
const page = finalWorkspace.getPage('hello-world'); const provider = createIndexedDBDownloadProvider(
workspace.id,
workspace.doc,
{
awareness: workspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const localWorkspace = {
id: workspace.id,
blockSuiteWorkspace: workspace,
flavour: WorkspaceFlavour.LOCAL,
} satisfies LocalWorkspace;
const newWorkspace = upgradeV1ToV2(localWorkspace);
await migrateLocalBlobStorage(localWorkspace.id, newWorkspace.id);
newWorkspace.blockSuiteWorkspace;
return newWorkspace.blockSuiteWorkspace;
});
const pageIdAtom = atom('hello-world');
const PageListSelect = () => {
const workspace = useAtomValue(workspaceAtom);
const setPageId = useSetAtom(pageIdAtom);
return (
<ul>
{workspace.meta.pageMetas.map(meta => (
<li
key={meta.id}
onClick={() => {
setPageId(meta.id);
}}
>
{meta.id}
</li>
))}
</ul>
);
};
const WorkspaceInner = () => {
const workspace = useAtomValue(workspaceAtom);
const pageId = useAtomValue(pageIdAtom);
const page = workspace.getPage(pageId);
const onInit = useCallback(() => {}, []); const onInit = useCallback(() => {}, []);
if (!page) { if (!page) {
return <>loading...</>; return <PageListSelect />;
} }
if (!page.loaded) { if (!page.loaded) {
use(page.waitForLoaded()); use(page.waitForLoaded());
} }
return (
<>
<PageListSelect />
<BlockSuiteEditor page={page} mode="page" onInit={onInit} />;
</>
);
};
const MigrationInner = () => {
const ids = useAtomValue(workspaceIdsAtom);
const [id, setId] = useAtom(targetIdAtom);
return ( return (
<div <div
style={{ style={{
@@ -60,15 +121,31 @@ const MigrationInner = () => {
height: '100vh', height: '100vh',
}} }}
> >
<BlockSuiteEditor page={page} mode="page" onInit={onInit} /> <ul>
{ids.map(id => (
<li
onClick={() => {
setId(id);
}}
key={id}
>
{id}
</li>
))}
</ul>
<Suspense fallback="loading...">{id && <WorkspaceInner />}</Suspense>
</div> </div>
); );
}; };
export default function MigrationPage(): ReactElement { export default function MigrationPage(): ReactElement {
return ( return (
<NoSsr> <Provider store={store}>
<MigrationInner /> <NoSsr>
</NoSsr> <Suspense>
<MigrationInner />
</Suspense>
</NoSsr>
</Provider>
); );
} }
+1
View File
@@ -130,6 +130,7 @@ function migrateMeta(oldDoc: Y.Doc, newDoc: Y.Doc) {
meta.set('workspaceVersion', 1); meta.set('workspaceVersion', 1);
meta.set('blockVersions', blockVersions); meta.set('blockVersions', blockVersions);
meta.set('pages', pages); meta.set('pages', pages);
meta.set('name', originalMeta.get('name') as string);
updateBlockVersions(blockVersions); updateBlockVersions(blockVersions);
const mapList = originalPages.map(page => { const mapList = originalPages.map(page => {
+4
View File
@@ -10,6 +10,10 @@ import type { FC, PropsWithChildren } from 'react';
import type { View } from './filter'; import type { View } from './filter';
import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud'; import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud';
export enum WorkspaceVersion {
SubDoc = 2,
}
export enum WorkspaceSubPath { export enum WorkspaceSubPath {
ALL = 'all', ALL = 'all',
SETTING = 'setting', SETTING = 'setting',
+1
View File
@@ -6,6 +6,7 @@
"./blob": "./src/blob/index.ts", "./blob": "./src/blob/index.ts",
"./utils": "./src/utils.ts", "./utils": "./src/utils.ts",
"./type": "./src/type.ts", "./type": "./src/type.ts",
"./migration": "./src/migration/index.ts",
"./local/crud": "./src/local/crud.ts", "./local/crud": "./src/local/crud.ts",
"./providers": "./src/providers/index.ts", "./providers": "./src/providers/index.ts",
"./affine/*": "./src/affine/*.ts", "./affine/*": "./src/affine/*.ts",
+15 -1
View File
@@ -1,13 +1,27 @@
import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace';
import type { WorkspaceVersion } from '@affine/env/workspace';
import type { EditorContainer } from '@blocksuite/editor'; import type { EditorContainer } from '@blocksuite/editor';
import { atom } from 'jotai'; import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
import Router from 'next/router'; import Router from 'next/router';
export type RootWorkspaceMetadata = { export type RootWorkspaceMetadataV2 = {
id: string; id: string;
flavour: WorkspaceFlavour; flavour: WorkspaceFlavour;
version: WorkspaceVersion;
}; };
export type RootWorkspaceMetadataV1 = {
id: string;
flavour: WorkspaceFlavour;
// force type check
version: undefined;
};
export type RootWorkspaceMetadata =
| RootWorkspaceMetadataV1
| RootWorkspaceMetadataV2;
// #region root atoms // #region root atoms
// root primitive atom that stores the necessary data for the whole app // root primitive atom that stores the necessary data for the whole app
// be careful when you use this atom, // be careful when you use this atom,
+12
View File
@@ -64,6 +64,18 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
WorkspaceFlavour.LOCAL WorkspaceFlavour.LOCAL
); );
BlockSuiteWorkspace.Y.applyUpdate(blockSuiteWorkspace.doc, binary); BlockSuiteWorkspace.Y.applyUpdate(blockSuiteWorkspace.doc, binary);
doc.getSubdocs().forEach(subdoc => {
blockSuiteWorkspace.doc.getSubdocs().forEach(newDoc => {
if (subdoc.guid === newDoc.guid) {
BlockSuiteWorkspace.Y.applyUpdate(
newDoc,
BlockSuiteWorkspace.Y.encodeStateAsUpdate(subdoc)
);
}
});
});
const persistence = createIndexedDBProvider(blockSuiteWorkspace.doc); const persistence = createIndexedDBProvider(blockSuiteWorkspace.doc);
persistence.connect(); persistence.connect();
await persistence.whenSynced.then(() => { await persistence.whenSynced.then(() => {
+51
View File
@@ -0,0 +1,51 @@
import { migrateToSubdoc } from '@affine/env/blocksuite';
import type { LocalWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { nanoid, Workspace } from '@blocksuite/store';
import { createIndexeddbStorage } from '@blocksuite/store';
const Y = Workspace.Y;
export function upgradeV1ToV2(oldWorkspace: LocalWorkspace): LocalWorkspace {
const oldDoc = oldWorkspace.blockSuiteWorkspace.doc;
const newDoc = migrateToSubdoc(oldDoc);
if (newDoc === oldDoc) {
console.warn('do not need update');
return oldWorkspace;
} else {
const id = nanoid();
const newBlockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
id,
WorkspaceFlavour.LOCAL
);
Y.applyUpdate(newBlockSuiteWorkspace.doc, Y.encodeStateAsUpdate(newDoc));
newDoc.getSubdocs().forEach(subdoc => {
newBlockSuiteWorkspace.doc.getSubdocs().forEach(newDoc => {
if (subdoc.guid === newDoc.guid) {
Y.applyUpdate(newDoc, Y.encodeStateAsUpdate(subdoc));
}
});
});
console.log(newBlockSuiteWorkspace.doc.toJSON());
return {
blockSuiteWorkspace: newBlockSuiteWorkspace,
flavour: WorkspaceFlavour.LOCAL,
id,
};
}
}
export async function migrateLocalBlobStorage(from: string, to: string) {
const fromStorage = createIndexeddbStorage(from);
const toStorage = createIndexeddbStorage(to);
const keys = await fromStorage.crud.list();
for (const key of keys) {
const value = await fromStorage.crud.get(key);
if (!value) {
console.warn('cannot find blob:', key);
continue;
}
await toStorage.crud.set(key, value);
}
}