️(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
- 💥(frontend) upgrade to ui-kit v2
- ⚡️(frontend) improve perf on upload and table of contents #1662
### Fixed

View File

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

View File

@@ -1,3 +1,4 @@
import { Block } from '@blocknote/core';
import { captureException } from '@sentry/nextjs';
import { useCallback, useEffect } from 'react';
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) => {
const ANALYZE_URL = 'media-check';
const { t } = useTranslation();
useEffect(() => {
const unsubscribe = editor.onChange((_, context) => {
const blocksChanges = context.getChanges();
if (!blocksChanges.length) {
return;
}
const blockChanges = blocksChanges[0];
/**
* Replace the resource block by a uploadLoader block to show analyzing status
*/
const replaceBlockWithUploadLoader = useCallback(
(block: Block) => {
if (
blockChanges.source.type !== 'local' ||
blockChanges.type !== 'update' ||
!('url' in blockChanges.block.props) ||
('url' in blockChanges.block.props &&
!blockChanges.block.props.url.includes(ANALYZE_URL))
!block ||
!('url' in block.props) ||
('url' in block.props && !block.props.url.includes(ANALYZE_URL))
) {
return;
}
const blockUploadUrl = blockChanges.block.props.url;
const blockUploadType = blockChanges.block.type;
const blockUploadName = blockChanges.block.props.name;
const blockUploadUrl = block.props.url;
const blockUploadType = block.type;
const blockUploadName = block.props.name;
const blockUploadShowPreview =
('showPreview' in blockChanges.block.props &&
blockChanges.block.props.showPreview) ||
false;
('showPreview' in block.props && block.props.showPreview) || false;
const timeoutId = setTimeout(() => {
// Replace the resource block by a uploadLoader block
// to show analyzing status
try {
editor.replaceBlocks(
[blockChanges.block.id],
[
{
type: 'uploadLoader',
props: {
information: t('Analyzing file...'),
type: 'loading',
blockUploadName,
blockUploadType,
blockUploadUrl,
blockUploadShowPreview,
},
try {
editor.replaceBlocks(
[block.id],
[
{
type: 'uploadLoader',
props: {
information: t('Analyzing file...'),
type: 'loading',
blockUploadName,
blockUploadType,
blockUploadUrl,
blockUploadShowPreview,
},
],
);
} catch (error) {
captureException(error, {
extra: { info: 'Error replacing block for upload loader' },
});
}
}, 250);
},
],
);
} catch (error) {
captureException(error, {
extra: { info: 'Error replacing block for upload loader' },
});
}
},
[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 () => {
clearTimeout(timeoutId);
unsubscribe();
clearTimeout(innerTimeoutId);
};
});
return () => {
unsubscribe();
};
}, [editor, t]);
}, [editor, replaceBlockWithUploadLoader]);
};