mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
fix(infra): avoid data loss (#6111)
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user