️(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,6 +75,9 @@ const UploadLoaderBlockComponent = ({
loopCheckDocMediaStatus(url) loopCheckDocMediaStatus(url)
.then((response) => { .then((response) => {
// Add random delay to reduce collision probability during collaboration
const randomDelay = Math.random() * 800;
setTimeout(() => {
// Replace the loading block with the resource block (image, audio, video, pdf ...) // Replace the loading block with the resource block (image, audio, video, pdf ...)
try { try {
editor.replaceBlocks( editor.replaceBlocks(
@@ -96,6 +99,7 @@ const UploadLoaderBlockComponent = ({
} catch { } catch {
/* During collaboration, another user might have updated the block */ /* 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,44 +37,40 @@ 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(() => {
// Replace the resource block by a uploadLoader block
// to show analyzing status
try { try {
editor.replaceBlocks( editor.replaceBlocks(
[blockChanges.block.id], [block.id],
[ [
{ {
type: 'uploadLoader', type: 'uploadLoader',
@@ -93,16 +90,40 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
extra: { info: 'Error replacing block for upload loader' }, 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]);
}; };