fix(infra): avoid data loss (#6111)

This commit is contained in:
EYHN
2024-03-14 06:27:49 +00:00
parent d2bad68b74
commit b9fc848824
6 changed files with 106 additions and 6 deletions

View File

@@ -13,6 +13,9 @@ export function fixWorkspaceVersion(rootDoc: YDoc) {
* Blocksuite just set the value, do nothing else. * Blocksuite just set the value, do nothing else.
*/ */
function doFix() { function doFix() {
if (meta.size === 0) {
return;
}
const workspaceVersion = meta.get('workspaceVersion'); const workspaceVersion = meta.get('workspaceVersion');
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) { if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
transact( transact(

View File

@@ -68,15 +68,17 @@ export class SyncEngine {
this.onStatusChange.emit(s); this.onStatusChange.emit(s);
} }
isRootDocLoaded = LiveData.from( isRootDocLoaded = LiveData.from(
new Observable(observer => { new Observable<boolean>(observer => {
observer.next( observer.next(
this.status.local [this.status?.local, ...(this.status?.remotes ?? [])].some(
? this.status.local.step > SyncPeerStep.LoadingRootDoc p => p?.rootDocLoaded === true
: false )
); );
this.onStatusChange.on(status => { this.onStatusChange.on(status => {
observer.next( observer.next(
status.local ? status.local.step > SyncPeerStep.LoadingRootDoc : false [status?.local, ...(status?.remotes ?? [])].some(
p => p?.rootDocLoaded === true
)
); );
}); });
}), }),

View File

@@ -20,6 +20,7 @@ export interface SyncPeerStatus {
pendingPullUpdates: number; pendingPullUpdates: number;
pendingPushUpdates: number; pendingPushUpdates: number;
lastError: string | null; lastError: string | null;
rootDocLoaded: boolean;
} }
/** /**
@@ -56,6 +57,7 @@ export class SyncPeer {
pendingPullUpdates: 0, pendingPullUpdates: 0,
pendingPushUpdates: 0, pendingPushUpdates: 0,
lastError: null, lastError: null,
rootDocLoaded: false,
}; };
onStatusChange = new Slot<SyncPeerStatus>(); onStatusChange = new Slot<SyncPeerStatus>();
readonly abort = new AbortController(); readonly abort = new AbortController();
@@ -122,6 +124,7 @@ export class SyncPeer {
pendingPullUpdates: 0, pendingPullUpdates: 0,
pendingPushUpdates: 0, pendingPushUpdates: 0,
lastError: 'Retrying sync after 5 seconds', lastError: 'Retrying sync after 5 seconds',
rootDocLoaded: this.status.rootDocLoaded,
}; };
await Promise.race([ await Promise.race([
new Promise<void>(resolve => { new Promise<void>(resolve => {
@@ -295,6 +298,13 @@ export class SyncPeer {
(await this.storage.pull(doc.guid, encodeStateVector(doc))) ?? {}; (await this.storage.pull(doc.guid, encodeStateVector(doc))) ?? {};
throwIfAborted(abort); throwIfAborted(abort);
if (docData !== undefined && doc.guid === this.rootDoc.guid) {
this.status = {
...this.status,
rootDocLoaded: true,
};
}
if (docData) { if (docData) {
applyUpdate(doc, docData, 'load'); applyUpdate(doc, docData, 'load');
} }
@@ -400,6 +410,7 @@ export class SyncPeer {
pendingPushUpdates: pendingPushUpdates:
this.state.pushUpdatesQueue.length + (this.state.pushingUpdate ? 1 : 0), this.state.pushUpdatesQueue.length + (this.state.pushingUpdate ? 1 : 0),
lastError, lastError,
rootDocLoaded: this.status.rootDocLoaded,
}; };
} }

View File

@@ -12,6 +12,8 @@ import { base64ToUint8Array, uint8ArrayToBase64 } from '../utils/base64';
const logger = new DebugLogger('affine:storage:socketio'); const logger = new DebugLogger('affine:storage:socketio');
(window as any)._TEST_SIMULATE_SYNC_LAG = Promise.resolve();
export class AffineSyncStorage implements SyncStorage { export class AffineSyncStorage implements SyncStorage {
name = 'affine-cloud'; name = 'affine-cloud';
@@ -57,6 +59,9 @@ export class AffineSyncStorage implements SyncStorage {
docId: string, docId: string,
state: Uint8Array state: Uint8Array
): Promise<{ data: Uint8Array; state?: Uint8Array } | null> { ): Promise<{ data: Uint8Array; state?: Uint8Array } | null> {
// for testing
await (window as any)._TEST_SIMULATE_SYNC_LAG;
const stateVector = state ? await uint8ArrayToBase64(state) : undefined; const stateVector = state ? await uint8ArrayToBase64(state) : undefined;
logger.debug('doc-load-v2', { logger.debug('doc-load-v2', {

View File

@@ -20,6 +20,13 @@ export class SQLiteSyncStorage implements SyncStorage {
); );
if (update) { if (update) {
if (
update.byteLength === 0 ||
(update.byteLength === 2 && update[0] === 0 && update[1] === 0)
) {
return null;
}
return { return {
data: update, data: update,
state: encodeStateVectorFromUpdate(update), state: encodeStateVectorFromUpdate(update),

View File

@@ -15,7 +15,10 @@ import {
waitForEditorLoad, waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic'; } from '@affine-test/kit/utils/page-logic';
import { clickUserInfoCard } from '@affine-test/kit/utils/setting'; import { clickUserInfoCard } from '@affine-test/kit/utils/setting';
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar'; import {
clickSideBarCurrentWorkspaceBanner,
clickSideBarSettingButton,
} from '@affine-test/kit/utils/sidebar';
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
@@ -294,3 +297,72 @@ test('can sync svg between different browsers', async ({ page, browser }) => {
expect(svg2).toEqual(svg1); expect(svg2).toEqual(svg1);
} }
}); });
test('When the first sync is not completed, should always show loading', async ({
page,
browser,
}) => {
await page.reload();
await waitForEditorLoad(page);
await createLocalWorkspace(
{
name: 'test',
},
page
);
await enableCloudWorkspace(page);
await clickNewPageButton(page);
await waitForEditorLoad(page);
const title = getBlockSuiteEditorTitle(page);
await title.pressSequentially('TEST TITLE', {
delay: 50,
});
const context = await browser.newContext();
await skipOnboarding(context);
const page2 = await context.newPage();
await loginUser(page2, user.email);
// simulate sync stuck
await page2.evaluate(() => {
(window as any)._TEST_SIMULATE_SYNC_LAG = new Promise(() => {});
});
const localWorkspaceUrl = page2.url();
await clickSideBarCurrentWorkspaceBanner(page2);
await page2.getByTestId('workspace-card').getByText('test').click(); // enter "test" workspace
await page2.waitForTimeout(1000);
await expect(
page2.getByTestId('page-list-item').getByText('TEST TITLE')
).not.toBeVisible(); // should be loading
// Simulate user refresh and re-enter workspace, should still be loading
await page2.goto(localWorkspaceUrl);
// setup sync lag
await page2.evaluate(() => {
(window as any).resolveSyncLag = null;
(window as any)._TEST_SIMULATE_SYNC_LAG = new Promise(resolve => {
(window as any).resolveSyncLag = resolve;
});
});
await clickSideBarCurrentWorkspaceBanner(page2);
await page2.getByTestId('workspace-card').getByText('test').click(); // enter "test" workspace
await page2.waitForTimeout(1000);
await expect(
page2.getByTestId('page-list-item').getByText('TEST TITLE')
).not.toBeVisible(); // should be loading
await page2.evaluate(() => {
(window as any).resolveSyncLag();
}); // start syncing
await page2.getByTestId('page-list-item').getByText('TEST TITLE').click(); // should be able to click page
await waitForEditorLoad(page2);
expect(await getBlockSuiteEditorTitle(page2).innerText()).toContain(
'TEST TITLE'
);
});