mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-18 14:56:59 +08:00
fix(infra): compatibility fix for space prefix (#4912)
It seems there are some cases that [this upstream PR](https://github.com/toeverything/blocksuite/pull/4747) will cause data loss.
Because of some historical reasons, the page id could be different with its doc id.
It might be caused by subdoc migration in the following (not 100% sure if all white screen issue is caused by it) 0714c12703/packages/common/infra/src/blocksuite/index.ts (L538-L540)
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
The provided fix in the PR will patch the `spaces` field of the root doc so that after 0.10 the page doc can still be found in the `spaces` map. It shall apply to both of the idb & sqlite datasources.
Special thanks to @lawvs 's db file for investigation!
This commit is contained in:
@@ -51,7 +51,7 @@
|
||||
"builder-util-runtime": "^9.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^27.0.0",
|
||||
"electron-log": "^5.0.0-rc.1",
|
||||
"electron-log": "^5.0.0",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.19.4",
|
||||
|
||||
@@ -54,7 +54,6 @@ function spawnOrReloadElectron() {
|
||||
if (code && code !== 0) {
|
||||
console.log(`Electron exited with code ${code}`);
|
||||
}
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shell } from 'electron';
|
||||
import { app } from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
export const logger = log.scope('main');
|
||||
@@ -12,3 +13,7 @@ export async function revealLogFile() {
|
||||
const filePath = getLogFilePath();
|
||||
return await shell.openPath(filePath);
|
||||
}
|
||||
|
||||
app.on('before-quit', () => {
|
||||
log.transports.console.level = false;
|
||||
});
|
||||
|
||||
@@ -10,6 +10,9 @@ import { updaterSubjects } from './event';
|
||||
const mode = process.env.NODE_ENV;
|
||||
const isDev = mode === 'development';
|
||||
|
||||
// skip auto update in dev mode & internal
|
||||
const disabled = buildType === 'internal' || isDev;
|
||||
|
||||
export const quitAndInstall = async () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
};
|
||||
@@ -17,7 +20,7 @@ export const quitAndInstall = async () => {
|
||||
let lastCheckTime = 0;
|
||||
export const checkForUpdates = async (force = true) => {
|
||||
// check every 30 minutes (1800 seconds) at most
|
||||
if (force || lastCheckTime + 1000 * 1800 < Date.now()) {
|
||||
if (!disabled && (force || lastCheckTime + 1000 * 1800 < Date.now())) {
|
||||
lastCheckTime = Date.now();
|
||||
return await autoUpdater.checkForUpdates();
|
||||
}
|
||||
@@ -25,8 +28,7 @@ export const checkForUpdates = async (force = true) => {
|
||||
};
|
||||
|
||||
export const registerUpdater = async () => {
|
||||
// skip auto update in dev mode & internal
|
||||
if (buildType === 'internal' || isDev) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +45,6 @@ export const registerUpdater = async () => {
|
||||
channel: buildType,
|
||||
// hack for custom provider
|
||||
provider: 'custom' as 'github',
|
||||
// @ts-expect-error - just ignore for now
|
||||
repo: buildType !== 'internal' ? 'AFFiNE' : 'AFFiNE-Releases',
|
||||
owner: 'toeverything',
|
||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||
|
||||
@@ -76,12 +76,25 @@ describe('useBlockSuitePagePreview', () => {
|
||||
page.getBlockByFlavour('affine:note')[0].id
|
||||
);
|
||||
const hook = renderHook(() => useAtomValue(useBlockSuitePagePreview(page)));
|
||||
expect(hook.result.current).toBe('\nHello, world!');
|
||||
expect(hook.result.current).toBe('Hello, world!');
|
||||
page.transact(() => {
|
||||
page.getBlockById(id)!.text!.insert('Test', 0);
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
hook.rerender();
|
||||
expect(hook.result.current).toBe('\nTestHello, world!');
|
||||
expect(hook.result.current).toBe('TestHello, world!');
|
||||
|
||||
// Insert before
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('First block!'),
|
||||
},
|
||||
page.getBlockByFlavour('affine:note')[0].id,
|
||||
0
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
hook.rerender();
|
||||
expect(hook.result.current).toBe('First block! TestHello, world!');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,49 @@
|
||||
import type { ParagraphBlockModel } from '@blocksuite/blocks/models';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
const MAX_PREVIEW_LENGTH = 150;
|
||||
const MAX_SEARCH_BLOCK_COUNT = 30;
|
||||
|
||||
const weakMap = new WeakMap<Page, Atom<string>>();
|
||||
|
||||
export const getPagePreviewText = (page: Page) => {
|
||||
// TODO this is incorrect, since the order of blocks is not guaranteed
|
||||
const paragraphBlocks = page.getBlockByFlavour(
|
||||
'affine:paragraph'
|
||||
) as ParagraphBlockModel[];
|
||||
const text = paragraphBlocks
|
||||
.slice(0, 10)
|
||||
.map(block => block.text.toString())
|
||||
.join('\n');
|
||||
return text.slice(0, 300);
|
||||
const pageRoot = page.root;
|
||||
if (!pageRoot) {
|
||||
return '';
|
||||
}
|
||||
const preview: string[] = [];
|
||||
// DFS
|
||||
const queue = [pageRoot];
|
||||
let previewLenNeeded = MAX_PREVIEW_LENGTH;
|
||||
let count = MAX_SEARCH_BLOCK_COUNT;
|
||||
while (queue.length && previewLenNeeded > 0 && count-- > 0) {
|
||||
const block = queue.shift();
|
||||
if (!block) {
|
||||
console.error('Unexpected empty block');
|
||||
break;
|
||||
}
|
||||
if (block.children) {
|
||||
queue.unshift(...block.children);
|
||||
}
|
||||
if (block.role !== 'content') {
|
||||
continue;
|
||||
}
|
||||
if (block.text) {
|
||||
const text = block.text.toString();
|
||||
if (!text.length) {
|
||||
continue;
|
||||
}
|
||||
previewLenNeeded -= text.length;
|
||||
preview.push(text);
|
||||
} else {
|
||||
// image/attachment/bookmark
|
||||
const type = block.flavour.split('affine:')[1] ?? null;
|
||||
previewLenNeeded -= type.length + 2;
|
||||
type && preview.push(`[${type}]`);
|
||||
}
|
||||
}
|
||||
return preview.join(' ');
|
||||
};
|
||||
|
||||
const emptyAtom = atom<string>('');
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user