From 8e82d1e02cffdb14c8ae9a35a4ea2060931d1a96 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Mon, 26 Jun 2023 15:55:44 +0800 Subject: [PATCH] feat: support migration (#2852) --- apps/web/src/atoms/__tests__/atom.spec.ts | 3 +- apps/web/src/atoms/index.ts | 48 +++--- apps/web/src/atoms/root.ts | 126 ++++++++------ apps/web/src/bootstrap/index.ts | 77 +++++++++ apps/web/src/hooks/use-transform-workspace.ts | 4 +- apps/web/src/hooks/use-workspaces.ts | 4 +- apps/web/src/layouts/workspace-layout.tsx | 8 +- apps/web/src/pages/_debug/migration.tsx | 159 +++++++++++++----- .../env/src/blocksuite/subdoc-migration.ts | 1 + packages/env/src/workspace.ts | 4 + packages/workspace/package.json | 1 + packages/workspace/src/atom.ts | 16 +- packages/workspace/src/local/crud.ts | 12 ++ packages/workspace/src/migration/index.ts | 51 ++++++ 14 files changed, 395 insertions(+), 119 deletions(-) create mode 100644 packages/workspace/src/migration/index.ts diff --git a/apps/web/src/atoms/__tests__/atom.spec.ts b/apps/web/src/atoms/__tests__/atom.spec.ts index 63849a46f4..b58814d9bd 100644 --- a/apps/web/src/atoms/__tests__/atom.spec.ts +++ b/apps/web/src/atoms/__tests__/atom.spec.ts @@ -5,7 +5,7 @@ import 'fake-indexeddb/auto'; import { initEmptyPage } from '@affine/env/blocksuite'; import type { LocalIndexedDBBackgroundProvider } from '@affine/env/workspace'; -import { WorkspaceFlavour } from '@affine/env/workspace'; +import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; import { rootCurrentWorkspaceIdAtom, rootWorkspacesMetadataAtom, @@ -96,6 +96,7 @@ describe('currentWorkspace atom', () => { { id: workspaceId, flavour: WorkspaceFlavour.LOCAL, + version: WorkspaceVersion.SubDoc, }, ]); _cleanupBlockSuiteWorkspaceCache(); diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts index 7b3cb962b7..02235d1416 100644 --- a/apps/web/src/atoms/index.ts +++ b/apps/web/src/atoms/index.ts @@ -1,6 +1,6 @@ import { DebugLogger } from '@affine/debug'; -import { WorkspaceFlavour } from '@affine/env/workspace'; -import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; +import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace'; +import type { RootWorkspaceMetadataV2 } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { atom } from 'jotai'; import { atomFamily, atomWithStorage } from 'jotai/utils'; @@ -13,7 +13,7 @@ const logger = new DebugLogger('web:atoms'); // workspace necessary atoms // todo(himself65): move this to the workspace package rootWorkspacesMetadataAtom.onMount = setAtom => { - function createFirst(): RootWorkspaceMetadata[] { + function createFirst(): RootWorkspaceMetadataV2[] { const Plugins = Object.values(WorkspaceAdapters).sort( (a, b) => a.loadPriority - b.loadPriority ); @@ -24,29 +24,33 @@ rootWorkspacesMetadataAtom.onMount = setAtom => { ({ id, 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(); - // next tick to make sure the hydration is correct - const id = setTimeout(() => { - setAtom(metadata => { - if (abortController.signal.aborted) return metadata; - if ( - metadata.length === 0 && - localStorage.getItem('is-first-open') === null - ) { - localStorage.setItem('is-first-open', 'false'); - const newMetadata = createFirst(); - logger.info('create first workspace', newMetadata); - return newMetadata; - } - return metadata; - }); - }, 0); + if (!environment.isServer) { + // next tick to make sure the hydration is correct + setTimeout(() => { + setAtom(metadata => { + if (abortController.signal.aborted) return metadata; + if ( + metadata.length === 0 && + localStorage.getItem('is-first-open') === null + ) { + localStorage.setItem('is-first-open', 'false'); + const newMetadata = createFirst(); + logger.info('create first workspace', newMetadata); + return newMetadata; + } + return metadata; + }); + }, 0); + } if (environment.isDesktop) { window.apis?.workspace @@ -56,6 +60,7 @@ rootWorkspacesMetadataAtom.onMount = setAtom => { const newMetadata = workspaceIDs.map(w => ({ id: w[0], flavour: WorkspaceFlavour.LOCAL, + version: undefined, })); setAtom(metadata => { return [ @@ -70,7 +75,6 @@ rootWorkspacesMetadataAtom.onMount = setAtom => { } return () => { - clearTimeout(id); abortController.abort(); }; }; diff --git a/apps/web/src/atoms/root.ts b/apps/web/src/atoms/root.ts index 22e6b49ab4..0bcad3c58c 100644 --- a/apps/web/src/atoms/root.ts +++ b/apps/web/src/atoms/root.ts @@ -18,60 +18,73 @@ const logger = new DebugLogger('web:atoms:root'); /** * Fetch all workspaces from the Plugin CRUD */ -export const workspacesAtom = atom>(async get => { - const { WorkspaceAdapters } = await import('../adapters/workspace'); - 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 +export const workspacesAtom = atom>( + async (get, { signal }) => { + const { WorkspaceAdapters } = await import('../adapters/workspace'); + const flavours: string[] = Object.values(WorkspaceAdapters).map( + plugin => plugin.flavour ); - 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; + 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 + ); + if (jotaiWorkspaces.some(meta => meta.version === undefined)) { + // wait until all workspaces have migrated to v2 + 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[] = []; - 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[] = []; + 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, @@ -79,7 +92,7 @@ export const workspacesAtom = atom>(async get => { * use `rootCurrentWorkspaceIdAtom` instead */ export const rootCurrentWorkspaceAtom = atom>( - async get => { + async (get, { signal }) => { const { WorkspaceAdapters } = await import('../adapters/workspace'); const metadata = get(rootWorkspacesMetadataAtom); const targetId = get(rootCurrentWorkspaceIdAtom); @@ -92,6 +105,17 @@ export const rootCurrentWorkspaceAtom = atom>( if (!targetWorkspace) { 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( targetWorkspace.id ); diff --git a/apps/web/src/bootstrap/index.ts b/apps/web/src/bootstrap/index.ts index 3ac27b251c..0516219dbf 100644 --- a/apps/web/src/bootstrap/index.ts +++ b/apps/web/src/bootstrap/index.ts @@ -1,4 +1,18 @@ +import { migrateToSubdoc } from '@affine/env/blocksuite'; 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(); @@ -32,3 +46,66 @@ if (!environment.isDesktop && !environment.isServer) { 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); + } + }); +}); diff --git a/apps/web/src/hooks/use-transform-workspace.ts b/apps/web/src/hooks/use-transform-workspace.ts index 1ccabba388..6366c54f24 100644 --- a/apps/web/src/hooks/use-transform-workspace.ts +++ b/apps/web/src/hooks/use-transform-workspace.ts @@ -1,5 +1,6 @@ import type { WorkspaceFlavour } from '@affine/env/workspace'; import type { WorkspaceRegistry } from '@affine/env/workspace'; +import { WorkspaceVersion } from '@affine/env/workspace'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { useSetAtom } from 'jotai'; import { useCallback } from 'react'; @@ -7,7 +8,7 @@ import { useCallback } from 'react'; 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. */ @@ -29,6 +30,7 @@ export function useTransformWorkspace() { workspaces.splice(idx, 1, { id: newId, flavour: to, + version: WorkspaceVersion.SubDoc, }); return [...workspaces]; }); diff --git a/apps/web/src/hooks/use-workspaces.ts b/apps/web/src/hooks/use-workspaces.ts index 4ac5517c84..af1441ac4d 100644 --- a/apps/web/src/hooks/use-workspaces.ts +++ b/apps/web/src/hooks/use-workspaces.ts @@ -1,5 +1,5 @@ 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 { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud'; import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; @@ -34,6 +34,7 @@ export function useAppHelper() { { id: workspaceId, flavour: WorkspaceFlavour.LOCAL, + version: WorkspaceVersion.SubDoc, }, ]); logger.debug('imported local workspace', workspaceId); @@ -54,6 +55,7 @@ export function useAppHelper() { { id, flavour: WorkspaceFlavour.LOCAL, + version: WorkspaceVersion.SubDoc, }, ]); logger.debug('created local workspace', id); diff --git a/apps/web/src/layouts/workspace-layout.tsx b/apps/web/src/layouts/workspace-layout.tsx index 52bdda758b..cac84fbd56 100644 --- a/apps/web/src/layouts/workspace-layout.tsx +++ b/apps/web/src/layouts/workspace-layout.tsx @@ -267,7 +267,13 @@ export const WorkspaceLayout: FC = ); }); } - 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) { logger.error('list data error:', e); } diff --git a/apps/web/src/pages/_debug/migration.tsx b/apps/web/src/pages/_debug/migration.tsx index 7f6a1340d9..3f1efe12f9 100644 --- a/apps/web/src/pages/_debug/migration.tsx +++ b/apps/web/src/pages/_debug/migration.tsx @@ -1,58 +1,119 @@ 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 { Workspace } from '@blocksuite/store'; import { NoSsr } from '@mui/material'; +import { + atom, + createStore, + Provider, + useAtom, + useAtomValue, + useSetAtom, +} from 'jotai'; import type { ReactElement } from 'react'; -import { use, useCallback } from 'react'; -import * as Y from 'yjs'; -const { default: json } = await import('@affine-test/fixtures/output.json'); +import { Suspense, use, useCallback } from 'react'; -const workspace = new Workspace({ - id: 'test-migration', - isSSR: typeof window === 'undefined', +const store = createStore(); + +const workspaceIdsAtom = atom>(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({ - id: 'test-migration-final', - isSSR: typeof window === 'undefined', -}); +const targetIdAtom = atom(null); -finalWorkspace.register(AffineSchemas).register(__unstableSchemas); -workspace.register(AffineSchemas).register(__unstableSchemas); - -if (typeof window !== 'undefined') { - const length = Object.keys(json).length; - const binary = new Uint8Array(length); - for (let i = 0; i < length; i++) { - binary[i] = (json as any)[i]; +const workspaceAtom = atom>(async get => { + const id = get(targetIdAtom); + if (!id) { + throw new Error('no id'); } - Y.applyUpdate(workspace.doc, binary); - { - // invoke data - 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 workspace = new Workspace({ + id, + isSSR: typeof window === 'undefined', }); -} -const MigrationInner = () => { - const page = finalWorkspace.getPage('hello-world'); + workspace.register(AffineSchemas).register(__unstableSchemas); + 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 ( +
    + {workspace.meta.pageMetas.map(meta => ( +
  • { + setPageId(meta.id); + }} + > + {meta.id} +
  • + ))} +
+ ); +}; + +const WorkspaceInner = () => { + const workspace = useAtomValue(workspaceAtom); + const pageId = useAtomValue(pageIdAtom); + const page = workspace.getPage(pageId); const onInit = useCallback(() => {}, []); if (!page) { - return <>loading...; + return ; } if (!page.loaded) { use(page.waitForLoaded()); } + return ( + <> + + ; + + ); +}; + +const MigrationInner = () => { + const ids = useAtomValue(workspaceIdsAtom); + const [id, setId] = useAtom(targetIdAtom); return (
{ height: '100vh', }} > - +
    + {ids.map(id => ( +
  • { + setId(id); + }} + key={id} + > + {id} +
  • + ))} +
+ {id && }
); }; export default function MigrationPage(): ReactElement { return ( - - - + + + + + + + ); } diff --git a/packages/env/src/blocksuite/subdoc-migration.ts b/packages/env/src/blocksuite/subdoc-migration.ts index fdcf33d9d5..bf41606bd8 100644 --- a/packages/env/src/blocksuite/subdoc-migration.ts +++ b/packages/env/src/blocksuite/subdoc-migration.ts @@ -130,6 +130,7 @@ function migrateMeta(oldDoc: Y.Doc, newDoc: Y.Doc) { meta.set('workspaceVersion', 1); meta.set('blockVersions', blockVersions); meta.set('pages', pages); + meta.set('name', originalMeta.get('name') as string); updateBlockVersions(blockVersions); const mapList = originalPages.map(page => { diff --git a/packages/env/src/workspace.ts b/packages/env/src/workspace.ts index 438733e64c..7d0dd87110 100644 --- a/packages/env/src/workspace.ts +++ b/packages/env/src/workspace.ts @@ -10,6 +10,10 @@ import type { FC, PropsWithChildren } from 'react'; import type { View } from './filter'; import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud'; +export enum WorkspaceVersion { + SubDoc = 2, +} + export enum WorkspaceSubPath { ALL = 'all', SETTING = 'setting', diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 9ff884d213..fe82a7c56d 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -6,6 +6,7 @@ "./blob": "./src/blob/index.ts", "./utils": "./src/utils.ts", "./type": "./src/type.ts", + "./migration": "./src/migration/index.ts", "./local/crud": "./src/local/crud.ts", "./providers": "./src/providers/index.ts", "./affine/*": "./src/affine/*.ts", diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts index 4fd20e5252..9899ae4a1b 100644 --- a/packages/workspace/src/atom.ts +++ b/packages/workspace/src/atom.ts @@ -1,13 +1,27 @@ import type { WorkspaceFlavour } from '@affine/env/workspace'; +import type { WorkspaceVersion } from '@affine/env/workspace'; import type { EditorContainer } from '@blocksuite/editor'; import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; import Router from 'next/router'; -export type RootWorkspaceMetadata = { +export type RootWorkspaceMetadataV2 = { id: string; flavour: WorkspaceFlavour; + version: WorkspaceVersion; }; + +export type RootWorkspaceMetadataV1 = { + id: string; + flavour: WorkspaceFlavour; + // force type check + version: undefined; +}; + +export type RootWorkspaceMetadata = + | RootWorkspaceMetadataV1 + | RootWorkspaceMetadataV2; + // #region root atoms // root primitive atom that stores the necessary data for the whole app // be careful when you use this atom, diff --git a/packages/workspace/src/local/crud.ts b/packages/workspace/src/local/crud.ts index 3194c4839e..7a0d05a697 100644 --- a/packages/workspace/src/local/crud.ts +++ b/packages/workspace/src/local/crud.ts @@ -64,6 +64,18 @@ export const CRUD: WorkspaceCRUD = { WorkspaceFlavour.LOCAL ); 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); persistence.connect(); await persistence.whenSynced.then(() => { diff --git a/packages/workspace/src/migration/index.ts b/packages/workspace/src/migration/index.ts new file mode 100644 index 0000000000..1077a8e56f --- /dev/null +++ b/packages/workspace/src/migration/index.ts @@ -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); + } +}