diff --git a/packages/common/infra/src/blocksuite/index.ts b/packages/common/infra/src/blocksuite/index.ts index 5e3abe6a9d..335d137331 100644 --- a/packages/common/infra/src/blocksuite/index.ts +++ b/packages/common/infra/src/blocksuite/index.ts @@ -2,7 +2,7 @@ import type { Page, PageMeta, Workspace } from '@blocksuite/store'; import { createIndexeddbStorage } from '@blocksuite/store'; import type { createStore, WritableAtom } from 'jotai/vanilla'; import type { Doc } from 'yjs'; -import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs'; +import { Array as YArray, Doc as YDoc, Map as YMap, transact } from 'yjs'; export async function initEmptyPage(page: Page, title?: string) { await page.waitForLoaded(); @@ -537,6 +537,7 @@ function migrateBlocks( const originalBlocks = oldDoc.getMap(spaceId) as YMap; const subdoc = new YDoc(); spaces.set(newId, subdoc); + subdoc.guid = id; const blocks = subdoc.getMap('blocks'); Array.from(originalBlocks.entries()).forEach(([key, value]) => { const blockData = value.clone(); @@ -602,6 +603,7 @@ export async function forceUpgradePages( const versions = meta.get('blockVersions') as YMap; const schema = options.getSchema(); const oldVersions = versions.toJSON(); + guidCompatibilityFix(rootDoc); spaces.forEach((space: Doc) => { try { schema.upgradePage(0, oldVersions, space); @@ -623,6 +625,7 @@ async function upgradeV2ToV3(options: UpgradeOptions): Promise { const meta = rootDoc.getMap('meta') as YMap; const versions = meta.get('blockVersions') as YMap; const schema = options.getSchema(); + guidCompatibilityFix(rootDoc); spaces.forEach((space: Doc) => { schema.upgradePage( 0, @@ -654,6 +657,39 @@ async function upgradeV2ToV3(options: UpgradeOptions): Promise { return true; } +// patch root doc's space guid compatibility issue +// +// in version 0.10, page id in spaces no longer has prefix "space:" +// The data flow for fetching a doc's updates is: +// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid` +// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId +// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces` +// - when fetching the rows of this doc using the doc id === page id, +// it will return empty since there is no updates associated with the page id +export function guidCompatibilityFix(rootDoc: YDoc) { + let changed = false; + transact(rootDoc, () => { + const spaces = rootDoc.getMap('spaces') as YMap; + spaces.forEach((doc: YDoc, pageId: string) => { + if (pageId.includes(':')) { + const newPageId = pageId.split(':').at(-1) ?? pageId; + const newDoc = new YDoc(); + // clone the original doc. yjs is not happy to use the same doc instance + applyUpdate(newDoc, encodeStateAsUpdate(doc)); + newDoc.guid = doc.guid; + spaces.set(newPageId, newDoc); + // should remove the old doc, otherwise we will do it again in the next run + spaces.delete(pageId); + changed = true; + console.debug( + `fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}` + ); + } + }); + }); + return changed; +} + export enum WorkspaceVersion { // v1 is treated as undefined SubDoc = 2, diff --git a/packages/common/y-provider/src/lazy-provider.ts b/packages/common/y-provider/src/lazy-provider.ts index ee59b4c01e..f1c1a2ec2c 100644 --- a/packages/common/y-provider/src/lazy-provider.ts +++ b/packages/common/y-provider/src/lazy-provider.ts @@ -98,38 +98,21 @@ export const createLazyProvider = ( async function syncDoc(doc: Doc) { const guid = doc.guid; { - // backport from `@blocksuite/store` - const prefixId = guid.startsWith('space:') ? guid.slice(6) : guid; - const possible1 = `${rootDoc.guid}:space:${prefixId}`; - const possible2 = `space:${prefixId}`; - const update1 = await datasource.queryDocState(possible1); - const update2 = await datasource.queryDocState(possible2); + const update = await datasource.queryDocState(guid); let hasUpdate = false; if ( - update1 && - update1.missing.length !== 2 && - update1.missing[0] !== 0 && - update1.missing[1] !== 0 + update && + update.missing.length !== 2 && + update.missing[0] !== 0 && + update.missing[1] !== 0 ) { - applyUpdate(doc, update1.missing, origin); - hasUpdate = true; - } - if ( - update2 && - update2.missing.length !== 2 && - update2.missing[0] !== 0 && - update2.missing[1] !== 0 - ) { - applyUpdate(doc, update2.missing, origin); + applyUpdate(doc, update.missing, origin); hasUpdate = true; } if (hasUpdate) { await datasource.sendDocUpdate( guid, - encodeStateAsUpdate( - doc, - update1 ? update1.state : update2 ? update2.state : undefined - ) + encodeStateAsUpdate(doc, update ? update.state : undefined) ); } } diff --git a/packages/frontend/electron/src/helper/db/base-db-adapter.ts b/packages/frontend/electron/src/helper/db/base-db-adapter.ts index 32acd64bdc..30d3fe4737 100644 --- a/packages/frontend/electron/src/helper/db/base-db-adapter.ts +++ b/packages/frontend/electron/src/helper/db/base-db-adapter.ts @@ -5,7 +5,7 @@ import { } from '@affine/native'; import { WorkspaceVersion } from '@toeverything/infra/blocksuite'; -import { migrateToLatest } from '../db/migration'; +import { applyGuidCompatibilityFix, migrateToLatest } from '../db/migration'; import { logger } from '../logger'; /** @@ -29,6 +29,7 @@ export abstract class BaseSQLiteAdapter { if (maxVersion !== WorkspaceVersion.Surface) { await migrateToLatest(this.path, WorkspaceVersion.Surface); } + await applyGuidCompatibilityFix(this.db); logger.info(`[SQLiteAdapter:${this.role}]`, 'connected:', this.path); } return this.db; diff --git a/packages/frontend/electron/src/helper/db/migration.ts b/packages/frontend/electron/src/helper/db/migration.ts index 12a0a7aaf5..8673a5d869 100644 --- a/packages/frontend/electron/src/helper/db/migration.ts +++ b/packages/frontend/electron/src/helper/db/migration.ts @@ -6,6 +6,7 @@ import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { Schema } from '@blocksuite/store'; import { forceUpgradePages, + guidCompatibilityFix, migrateToSubdoc, WorkspaceVersion, } from '@toeverything/infra/blocksuite'; @@ -119,3 +120,21 @@ async function replaceRows( }) ); } + +export const applyGuidCompatibilityFix = async (db: SqliteConnection) => { + const oldRows = await db.getUpdates(undefined); + + const rootDoc = new YDoc(); + oldRows.forEach(row => applyUpdate(rootDoc, row.data)); + + // see comments of guidCompatibilityFix + guidCompatibilityFix(rootDoc); + + // todo: backup? + await db.replaceUpdates(undefined, [ + { + docId: undefined, + data: encodeStateAsUpdate(rootDoc), + }, + ]); +}; diff --git a/packages/frontend/hooks/src/use-block-suite-workspace-page.ts b/packages/frontend/hooks/src/use-block-suite-workspace-page.ts index 8b0cb27e42..4df7360604 100644 --- a/packages/frontend/hooks/src/use-block-suite-workspace-page.ts +++ b/packages/frontend/hooks/src/use-block-suite-workspace-page.ts @@ -85,7 +85,7 @@ export function loadPage(page: Page, priority = 0) { logger.debug('page loaded', page.id); // we do not know how long it takes to load a page here // so that we just use 300ms timeout as the default page processing time - await awaitForTimeout(1000); + await awaitForTimeout(300); } else { // do nothing if it is already loaded }