🐛(y-provider) destroy Y.Doc instances after each convert request

The Yjs reader and writer in `convertHandler.ts`
were creating `Y.Doc`instances on every request
without calling `.destroy()`, causing a slow heap
leak that could crash the server.

Fixed by wrapping both sites in `try/finally`
blocks that call `ydoc.destroy()`.
Regression tests added to assert `destroy` is
called the expected number of times per request path.
This commit is contained in:
Anthony LC 2026-03-25 12:03:12 +01:00
parent c886cbb41d
commit 525d8c8417
No known key found for this signature in database
3 changed files with 31 additions and 7 deletions

View file

@ -13,10 +13,13 @@ and this project adheres to
### Changed
- 💄(frontend) improve comments highlights #1961
- ♿️(frontend) improve BoxButton a11y and native button semantics
#2103
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
- ♿️(frontend) improve language picker accessibility #2069
### Fixed
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
## [v4.8.3] - 2026-03-23
### Changed

View file

@ -1,6 +1,6 @@
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import request from 'supertest';
import { describe, expect, test, vi } from 'vitest';
import { afterEach, describe, expect, test, vi } from 'vitest';
import * as Y from 'yjs';
vi.mock('../src/env', async (importOriginal) => {
@ -62,7 +62,11 @@ const expectedBlocks = [
console.error = vi.fn();
describe('Server Tests', () => {
describe('Conversion Testing', () => {
afterEach(() => {
vi.clearAllMocks();
});
test('POST /api/convert with incorrect API key responds with 401', async () => {
const app = initApp();
@ -170,6 +174,7 @@ describe('Server Tests', () => {
});
test('POST /api/convert BlockNote to Yjs', async () => {
const destroySpy = vi.spyOn(Y.Doc.prototype, 'destroy');
const app = initApp();
const editor = ServerBlockNoteEditor.create();
const blocks = await editor.tryParseMarkdownToBlocks(expectedMarkdown);
@ -192,6 +197,7 @@ describe('Server Tests', () => {
const decodedBlocks = editor.yDocToBlocks(ydoc, 'document-store');
expect(decodedBlocks).toStrictEqual(expectedBlocks);
expect(destroySpy).toHaveBeenCalledTimes(1);
});
test('POST /api/convert BlockNote to HTML', async () => {
@ -253,6 +259,7 @@ describe('Server Tests', () => {
});
test('POST /api/convert Yjs to JSON', async () => {
const destroySpy = vi.spyOn(Y.Doc.prototype, 'destroy');
const app = initApp();
const editor = ServerBlockNoteEditor.create();
const blocks = await editor.tryParseMarkdownToBlocks(expectedMarkdown);
@ -272,6 +279,7 @@ describe('Server Tests', () => {
);
expect(response.body).toBeInstanceOf(Array);
expect(response.body).toStrictEqual(expectedBlocks);
expect(destroySpy).toHaveBeenCalledTimes(1);
});
test('POST /api/convert Markdown to JSON', async () => {
@ -293,6 +301,7 @@ describe('Server Tests', () => {
});
test('POST /api/convert with invalid Yjs content returns 400', async () => {
const destroySpy = vi.spyOn(Y.Doc.prototype, 'destroy');
const app = initApp();
const response = await request(app)
.post('/api/convert')
@ -304,5 +313,6 @@ describe('Server Tests', () => {
expect(response.status).toBe(400);
expect(response.body).toStrictEqual({ error: 'Invalid content' });
expect(destroySpy).toHaveBeenCalledTimes(1);
});
});

View file

@ -60,8 +60,12 @@ const readers: InputReader[] = [
supportedContentTypes: [ContentTypes.YJS, ContentTypes.OctetStream],
read: async (data) => {
const ydoc = new Y.Doc();
Y.applyUpdate(ydoc, data);
return editor.yDocToBlocks(ydoc, 'document-store') as PartialBlock[];
try {
Y.applyUpdate(ydoc, data);
return editor.yDocToBlocks(ydoc, 'document-store') as PartialBlock[];
} finally {
ydoc.destroy();
}
},
},
{
@ -77,7 +81,14 @@ const writers: OutputWriter[] = [
},
{
supportedContentTypes: [ContentTypes.YJS, ContentTypes.OctetStream],
write: async (blocks) => Y.encodeStateAsUpdate(createYDocument(blocks)),
write: async (blocks) => {
const ydoc = createYDocument(blocks);
try {
return Y.encodeStateAsUpdate(ydoc);
} finally {
ydoc.destroy();
}
},
},
{
supportedContentTypes: [ContentTypes.Markdown, ContentTypes.XMarkdown],