🐛(frontend) fix lost content during sync

The tests e2e highlighted a problem where content
was lost during synchronization. This bug
started to occurs after upgrading Blocknote to
0.41.1 version.
It seems to happen only when the initial document
is empty and 2 users are collaborating, so before
the first minute.
We now initialize the editor only when the y-doc
has attempted to sync. This should ensure that
all updates are applied before the editor
is initialized.
This commit is contained in:
Anthony LC
2025-10-21 15:26:31 +02:00
parent 950d215632
commit 145c688830
4 changed files with 20 additions and 15 deletions

View File

@@ -7,6 +7,7 @@ import {
keyCloakSignIn, keyCloakSignIn,
verifyDocName, verifyDocName,
} from './utils-common'; } from './utils-common';
import { writeInEditor } from './utils-editor';
import { addNewMember, connectOtherUserToDoc } from './utils-share'; import { addNewMember, connectOtherUserToDoc } from './utils-share';
import { createRootSubPage } from './utils-sub-pages'; import { createRootSubPage } from './utils-sub-pages';
@@ -151,18 +152,15 @@ test.describe('Doc Visibility: Restricted', () => {
await verifyDocName(page, docTitle); await verifyDocName(page, docTitle);
await page await writeInEditor({ page, text: 'Hello World' });
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
const docUrl = page.url(); const docUrl = page.url();
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({ const { otherBrowserName, otherPage, cleanup } =
browserName, await connectOtherUserToDoc({
docUrl, browserName,
}); docUrl,
});
await expect( await expect(
otherPage.getByText('Insufficient access rights to view the document.'), otherPage.getByText('Insufficient access rights to view the document.'),
@@ -178,6 +176,8 @@ test.describe('Doc Visibility: Restricted', () => {
await expect(otherPage.getByText('Hello World')).toBeVisible({ await expect(otherPage.getByText('Hello World')).toBeVisible({
timeout: 10000, timeout: 10000,
}); });
await cleanup();
}); });
}); });

View File

@@ -83,10 +83,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { user } = useAuth(); const { user } = useAuth();
const { setEditor } = useEditorStore(); const { setEditor } = useEditorStore();
const { t } = useTranslation(); const { t } = useTranslation();
const { isSynced } = useProviderStore(); const { isSynced: isConnectedToCollabServer } = useProviderStore();
const { isEditable, isLoading } = useIsCollaborativeEditable(doc); const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
const isConnectedToCollabServer = isSynced;
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading; const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
const isDeletedDoc = !!doc.deleted_at; const isDeletedDoc = !!doc.deleted_at;

View File

@@ -25,10 +25,10 @@ interface DocEditorProps {
export const DocEditor = ({ doc, versionId }: DocEditorProps) => { export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const isVersion = !!versionId && typeof versionId === 'string'; const isVersion = !!versionId && typeof versionId === 'string';
const { provider } = useProviderStore(); const { provider, isReady } = useProviderStore();
// TODO: Use skeleton instead of loading // TODO: Use skeleton instead of loading
if (!provider) { if (!provider || !isReady) {
return <Loading />; return <Loading />;
} }

View File

@@ -14,6 +14,7 @@ export interface UseCollaborationStore {
destroyProvider: () => void; destroyProvider: () => void;
provider: HocuspocusProvider | undefined; provider: HocuspocusProvider | undefined;
isConnected: boolean; isConnected: boolean;
isReady: boolean;
isSynced: boolean; isSynced: boolean;
hasLostConnection: boolean; hasLostConnection: boolean;
resetLostConnection: () => void; resetLostConnection: () => void;
@@ -22,6 +23,7 @@ export interface UseCollaborationStore {
const defaultValues = { const defaultValues = {
provider: undefined, provider: undefined,
isConnected: false, isConnected: false,
isReady: false,
isSynced: false, isSynced: false,
hasLostConnection: false, hasLostConnection: false,
}; };
@@ -46,14 +48,18 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
onDisconnect(data) { onDisconnect(data) {
// Attempt to reconnect if the disconnection was clean (initiated by the client or server) // Attempt to reconnect if the disconnection was clean (initiated by the client or server)
if ((data.event as ExtendedCloseEvent).wasClean) { if ((data.event as ExtendedCloseEvent).wasClean) {
provider.connect(); void provider.connect();
} }
}, },
onAuthenticationFailed() {
set({ isReady: true });
},
onStatus: ({ status }) => { onStatus: ({ status }) => {
set((state) => { set((state) => {
const nextConnected = status === WebSocketStatus.Connected; const nextConnected = status === WebSocketStatus.Connected;
return { return {
isConnected: nextConnected, isConnected: nextConnected,
isReady: state.isReady || status === WebSocketStatus.Disconnected,
hasLostConnection: hasLostConnection:
state.isConnected && !nextConnected state.isConnected && !nextConnected
? true ? true
@@ -62,7 +68,7 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
}); });
}, },
onSynced: ({ state }) => { onSynced: ({ state }) => {
set({ isSynced: state }); set({ isSynced: state, isReady: true });
}, },
onClose(data) { onClose(data) {
/** /**