️(frontend) improve UploadFile process

We notices that `context.getChanges` was very
greedy, on a large document with multiple
users collaborating, it caused performance issues.
We change the way that we track a upload by
listening onUploadEnd event instead of tracking
all changes in the document.
When a doc opens, we check if there are any ongoing
uploads and resume them.
We fix as well a race condition that could happen
when multiple collaborators were on a document
during an upload.
This commit is contained in:
Anthony LC
2025-11-28 09:19:08 +01:00
parent f7d4e6810b
commit 9aeedd1d03
3 changed files with 100 additions and 74 deletions

View File

@@ -15,6 +15,7 @@ and this project adheres to
- ⚡️(sw) stop to cache external resources likes videos #1655 - ⚡️(sw) stop to cache external resources likes videos #1655
- 💥(frontend) upgrade to ui-kit v2 - 💥(frontend) upgrade to ui-kit v2
- ⚡️(frontend) improve perf on upload and table of contents #1662
### Fixed ### Fixed

View File

@@ -75,27 +75,31 @@ const UploadLoaderBlockComponent = ({
loopCheckDocMediaStatus(url) loopCheckDocMediaStatus(url)
.then((response) => { .then((response) => {
// Replace the loading block with the resource block (image, audio, video, pdf ...) // Add random delay to reduce collision probability during collaboration
try { const randomDelay = Math.random() * 800;
editor.replaceBlocks( setTimeout(() => {
[block.id], // Replace the loading block with the resource block (image, audio, video, pdf ...)
[ try {
{ editor.replaceBlocks(
type: block.props.blockUploadType, [block.id],
props: { [
url: `${mediaUrl}${response.file}`, {
showPreview: block.props.blockUploadShowPreview, type: block.props.blockUploadType,
name: block.props.blockUploadName, props: {
caption: '', url: `${mediaUrl}${response.file}`,
backgroundColor: 'default', showPreview: block.props.blockUploadShowPreview,
textAlignment: 'left', name: block.props.blockUploadName,
}, caption: '',
} as never, backgroundColor: 'default',
], textAlignment: 'left',
); },
} catch { } as never,
/* During collaboration, another user might have updated the block */ ],
} );
} catch {
/* During collaboration, another user might have updated the block */
}
}, randomDelay);
}) })
.catch((error) => { .catch((error) => {
console.error('Error analyzing file:', error); console.error('Error analyzing file:', error);

View File

@@ -1,3 +1,4 @@
import { Block } from '@blocknote/core';
import { captureException } from '@sentry/nextjs'; import { captureException } from '@sentry/nextjs';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -36,73 +37,93 @@ export const useUploadFile = (docId: string) => {
}; };
}; };
/**
* When we upload a file it can takes some time to analyze it (e.g. virus scan).
* This hook listen to upload end and replace the uploaded block by a uploadLoader
* block to show analyzing status.
* The uploadLoader block will then handle the status display until the analysis is done
* then replaced by the final block (e.g. image, pdf, etc.).
* @param editor
*/
export const useUploadStatus = (editor: DocsBlockNoteEditor) => { export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
const ANALYZE_URL = 'media-check'; const ANALYZE_URL = 'media-check';
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { /**
const unsubscribe = editor.onChange((_, context) => { * Replace the resource block by a uploadLoader block to show analyzing status
const blocksChanges = context.getChanges(); */
const replaceBlockWithUploadLoader = useCallback(
if (!blocksChanges.length) { (block: Block) => {
return;
}
const blockChanges = blocksChanges[0];
if ( if (
blockChanges.source.type !== 'local' || !block ||
blockChanges.type !== 'update' || !('url' in block.props) ||
!('url' in blockChanges.block.props) || ('url' in block.props && !block.props.url.includes(ANALYZE_URL))
('url' in blockChanges.block.props &&
!blockChanges.block.props.url.includes(ANALYZE_URL))
) { ) {
return; return;
} }
const blockUploadUrl = blockChanges.block.props.url; const blockUploadUrl = block.props.url;
const blockUploadType = blockChanges.block.type; const blockUploadType = block.type;
const blockUploadName = blockChanges.block.props.name; const blockUploadName = block.props.name;
const blockUploadShowPreview = const blockUploadShowPreview =
('showPreview' in blockChanges.block.props && ('showPreview' in block.props && block.props.showPreview) || false;
blockChanges.block.props.showPreview) ||
false;
const timeoutId = setTimeout(() => { try {
// Replace the resource block by a uploadLoader block editor.replaceBlocks(
// to show analyzing status [block.id],
try { [
editor.replaceBlocks( {
[blockChanges.block.id], type: 'uploadLoader',
[ props: {
{ information: t('Analyzing file...'),
type: 'uploadLoader', type: 'loading',
props: { blockUploadName,
information: t('Analyzing file...'), blockUploadType,
type: 'loading', blockUploadUrl,
blockUploadName, blockUploadShowPreview,
blockUploadType,
blockUploadUrl,
blockUploadShowPreview,
},
}, },
], },
); ],
} catch (error) { );
captureException(error, { } catch (error) {
extra: { info: 'Error replacing block for upload loader' }, captureException(error, {
}); extra: { info: 'Error replacing block for upload loader' },
} });
}, 250); }
},
[editor, t],
);
useEffect(() => {
const imagesBlocks = editor?.document.filter(
(block) =>
block.type === 'image' && block.props.url.includes(ANALYZE_URL),
);
imagesBlocks.forEach((block) => {
replaceBlockWithUploadLoader(block as Block);
});
}, [editor, replaceBlockWithUploadLoader]);
/**
* Handle upload end to replace the upload block by a uploadLoader
* block to show analyzing status
*/
useEffect(() => {
editor.onUploadEnd((blockId) => {
if (!blockId) {
return;
}
const innerTimeoutId = setTimeout(() => {
const block = editor.getBlock({ id: blockId });
replaceBlockWithUploadLoader(block as Block);
}, 300);
return () => { return () => {
clearTimeout(timeoutId); clearTimeout(innerTimeoutId);
unsubscribe();
}; };
}); });
}, [editor, replaceBlockWithUploadLoader]);
return () => {
unsubscribe();
};
}, [editor, t]);
}; };