From 525d8c8417c1895110b0f3cd69bfcd823d6e254b Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 25 Mar 2026 12:03:12 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(y-provider)=20destroy=20Y.Doc=20in?= =?UTF-8?q?stances=20after=20each=20convert=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 7 +++++-- .../y-provider/__tests__/convert.test.ts | 14 ++++++++++++-- .../y-provider/src/handlers/convertHandler.ts | 17 ++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 063a52fc..b22f544c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/frontend/servers/y-provider/__tests__/convert.test.ts b/src/frontend/servers/y-provider/__tests__/convert.test.ts index a31f4b58..5acba310 100644 --- a/src/frontend/servers/y-provider/__tests__/convert.test.ts +++ b/src/frontend/servers/y-provider/__tests__/convert.test.ts @@ -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); }); }); diff --git a/src/frontend/servers/y-provider/src/handlers/convertHandler.ts b/src/frontend/servers/y-provider/src/handlers/convertHandler.ts index e77b7e52..89a22c33 100644 --- a/src/frontend/servers/y-provider/src/handlers/convertHandler.ts +++ b/src/frontend/servers/y-provider/src/handlers/convertHandler.ts @@ -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],