Skip to content

Commit c6aa726

Browse files
committed
fix: move @blocknote/core usage to Node.js actions to fix Convex V8 SyntaxError
The notes mutations imported @blocknote/core (a browser-oriented library) at the module level, causing SyntaxError in Convex's V8 isolate when any note mutation was loaded. This moves BlockNoteEditor usage to "use node" actions that run in Node.js instead. - Create convex/notes/nodeActions.ts ("use node") for persistNoteBlocks and initializeNoteContent - Create convex/notes/internalMutations.ts for saveNoteBlocksInternal and pushYjsUpdateInternal - Create convex/yjsSync/internalQueries.ts for getUpdatesInternal - Remove @blocknote/core runtime imports from mutations.ts and createNote.ts - persistNoteBlocks now schedules a Node.js action via ctx.scheduler - createNote with content creates empty Yjs doc then schedules content init https://claude.ai/code/session_01J9nErHs2JYcyBnj6zFY2Kv
1 parent bd55064 commit c6aa726

5 files changed

Lines changed: 178 additions & 41 deletions

File tree

convex/notes/functions/createNote.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import * as Y from 'yjs'
2-
import { BlockNoteEditor } from '@blocknote/core'
3-
import { blocksToYDoc } from '@blocknote/core/yjs'
41
import { saveTopLevelBlocksForNote } from '../../blocks/functions/saveTopLevelBlocksForNote'
52
import {
63
findUniqueSidebarItemSlug,
@@ -12,8 +9,7 @@ import {
129
SIDEBAR_ITEM_TYPES,
1310
} from '../../sidebarItems/types/baseTypes'
1411
import { createYjsDocument } from '../../yjsSync/functions/createYjsDocument'
15-
import { uint8ToArrayBuffer } from '../../yjsSync/functions/uint8ToArrayBuffer'
16-
import { editorSchema } from '../editorSpecs'
12+
import { internal } from '../../_generated/api'
1713
import type { AuthMutationCtx } from '../../functions'
1814
import type { Id } from '../../_generated/dataModel'
1915
import type { CustomBlock } from '../editorSpecs'
@@ -73,22 +69,18 @@ export async function createNote(
7369
await saveTopLevelBlocksForNote(ctx, { noteId, content })
7470
}
7571

76-
let initialState: ArrayBuffer | undefined
72+
// Create empty Yjs document. If content was provided, schedule a Node.js
73+
// action to convert the blocks into a Yjs update (requires @blocknote/core
74+
// which can't run in the Convex V8 isolate).
75+
await createYjsDocument(ctx, { documentId: noteId })
76+
7777
if (content && content.length > 0) {
78-
const editor = BlockNoteEditor.create({
79-
schema: editorSchema,
80-
_headless: true,
81-
})
82-
let doc: Y.Doc | undefined
83-
try {
84-
doc = blocksToYDoc(editor, content)
85-
initialState = uint8ToArrayBuffer(Y.encodeStateAsUpdate(doc))
86-
} finally {
87-
doc?.destroy()
88-
editor._tiptapEditor.destroy()
89-
}
78+
await ctx.scheduler.runAfter(
79+
0,
80+
internal.notes.nodeActions.initializeNoteContent,
81+
{ noteId, content },
82+
)
9083
}
9184

92-
await createYjsDocument(ctx, { documentId: noteId, initialState })
9385
return { noteId, slug: uniqueSlug }
9486
}

convex/notes/internalMutations.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { v } from 'convex/values'
2+
import { internalMutation } from '../_generated/server'
3+
import { customBlockValidator } from '../blocks/schema'
4+
import { yjsDocumentIdValidator } from '../yjsSync/schema'
5+
import { saveTopLevelBlocksForNote } from '../blocks/functions/saveTopLevelBlocksForNote'
6+
import { authenticate } from '../functions'
7+
import type { CustomBlock } from './editorSpecs'
8+
9+
export const saveNoteBlocksInternal = internalMutation({
10+
args: {
11+
noteId: v.id('notes'),
12+
content: v.array(customBlockValidator),
13+
},
14+
handler: async (ctx, { noteId, content }) => {
15+
const user = await authenticate(ctx)
16+
await saveTopLevelBlocksForNote(
17+
{ ...ctx, user },
18+
{ noteId, content: content as Array<CustomBlock> },
19+
)
20+
},
21+
})
22+
23+
export const pushYjsUpdateInternal = internalMutation({
24+
args: {
25+
documentId: yjsDocumentIdValidator,
26+
update: v.bytes(),
27+
},
28+
handler: async (ctx, { documentId, update }) => {
29+
const latest = await ctx.db
30+
.query('yjsUpdates')
31+
.withIndex('by_document_seq', (q) => q.eq('documentId', documentId))
32+
.order('desc')
33+
.first()
34+
35+
const seq = (latest?.seq ?? -1) + 1
36+
37+
await ctx.db.insert('yjsUpdates', {
38+
documentId,
39+
update,
40+
seq,
41+
isSnapshot: false,
42+
})
43+
},
44+
})

convex/notes/mutations.ts

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { v } from 'convex/values'
2-
import { BlockNoteEditor } from '@blocknote/core'
3-
import { yDocToBlocks } from '@blocknote/core/yjs'
42
import { authMutation } from '../functions'
3+
import { internal } from '../_generated/api'
54
import { customBlockValidator } from '../blocks/schema'
6-
import { saveTopLevelBlocksForNote } from '../blocks/functions/saveTopLevelBlocksForNote'
75
import { checkYjsWriteAccess } from '../yjsSync/functions/checkYjsAccess'
8-
import { reconstructYDoc } from '../yjsSync/functions/reconstructYDoc'
96
import { createNote as createNoteFn } from './functions/createNote'
107
import { updateNote as updateNoteFn } from './functions/updateNote'
118
import { updateNoteContent as updateNoteContentFn } from './functions/updateNoteContent'
12-
import { editorSchema } from './editorSpecs'
139
import type { Id } from '../_generated/dataModel'
1410

1511
export const updateNote = authMutation({
@@ -72,23 +68,11 @@ export const persistNoteBlocks = authMutation({
7268
handler: async (ctx, { documentId }) => {
7369
await checkYjsWriteAccess(ctx, documentId)
7470

75-
const { doc } = await reconstructYDoc(ctx, documentId)
76-
let editor: ReturnType<typeof BlockNoteEditor.create> | undefined
77-
try {
78-
editor = BlockNoteEditor.create({
79-
schema: editorSchema,
80-
_headless: true,
81-
})
82-
const blocks = yDocToBlocks(editor, doc)
83-
84-
await saveTopLevelBlocksForNote(ctx, {
85-
noteId: documentId,
86-
content: blocks,
87-
})
88-
} finally {
89-
doc.destroy()
90-
editor?._tiptapEditor.destroy()
91-
}
71+
await ctx.scheduler.runAfter(
72+
0,
73+
internal.notes.nodeActions.persistNoteBlocksNode,
74+
{ documentId },
75+
)
9276

9377
return null
9478
},

convex/notes/nodeActions.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use node'
2+
3+
import * as Y from 'yjs'
4+
import { v } from 'convex/values'
5+
import { BlockNoteEditor } from '@blocknote/core'
6+
import { blocksToYDoc, yDocToBlocks } from '@blocknote/core/yjs'
7+
import { internalAction } from '../_generated/server'
8+
import { internal } from '../_generated/api'
9+
import { customBlockValidator } from '../blocks/schema'
10+
import { editorSchema } from './editorSpecs'
11+
import type { CustomBlock } from './editorSpecs'
12+
13+
function uint8ToArrayBuffer(uint8: Uint8Array): ArrayBuffer {
14+
if (uint8.byteOffset === 0 && uint8.byteLength === uint8.buffer.byteLength) {
15+
return uint8.buffer as ArrayBuffer
16+
}
17+
return uint8.buffer.slice(
18+
uint8.byteOffset,
19+
uint8.byteOffset + uint8.byteLength,
20+
) as ArrayBuffer
21+
}
22+
23+
/**
24+
* Converts blocks to a Yjs update and pushes it as the initial state
25+
* for a newly created note. Runs in Node.js to support @blocknote/core.
26+
*/
27+
export const initializeNoteContent = internalAction({
28+
args: {
29+
noteId: v.id('notes'),
30+
content: v.array(customBlockValidator),
31+
},
32+
handler: async (ctx, { noteId, content }) => {
33+
if (content.length === 0) return
34+
35+
const editor = BlockNoteEditor.create({
36+
schema: editorSchema,
37+
_headless: true,
38+
})
39+
let doc: Y.Doc | undefined
40+
try {
41+
doc = blocksToYDoc(editor, content as Array<CustomBlock>)
42+
const update = uint8ToArrayBuffer(Y.encodeStateAsUpdate(doc))
43+
await ctx.runMutation(
44+
internal.notes.internalMutations.pushYjsUpdateInternal,
45+
{
46+
documentId: noteId,
47+
update,
48+
},
49+
)
50+
} finally {
51+
doc?.destroy()
52+
editor._tiptapEditor.destroy()
53+
}
54+
},
55+
})
56+
57+
/**
58+
* Reads the Yjs document for a note, converts it to blocks, and saves them.
59+
* Runs in Node.js to support @blocknote/core.
60+
*/
61+
export const persistNoteBlocksNode = internalAction({
62+
args: {
63+
documentId: v.id('notes'),
64+
},
65+
handler: async (ctx, { documentId }) => {
66+
const updates = await ctx.runQuery(
67+
internal.yjsSync.internalQueries.getUpdatesInternal,
68+
{ documentId },
69+
)
70+
71+
const doc = new Y.Doc()
72+
let editor: ReturnType<typeof BlockNoteEditor.create> | undefined
73+
try {
74+
for (const entry of updates) {
75+
Y.applyUpdate(doc, new Uint8Array(entry.update))
76+
}
77+
78+
editor = BlockNoteEditor.create({
79+
schema: editorSchema,
80+
_headless: true,
81+
})
82+
const blocks = yDocToBlocks(editor, doc)
83+
84+
await ctx.runMutation(
85+
internal.notes.internalMutations.saveNoteBlocksInternal,
86+
{ noteId: documentId, content: blocks },
87+
)
88+
} finally {
89+
doc.destroy()
90+
editor?._tiptapEditor.destroy()
91+
}
92+
},
93+
})

convex/yjsSync/internalQueries.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { v } from 'convex/values'
2+
import { internalQuery } from '../_generated/server'
3+
import { yjsDocumentIdValidator } from './schema'
4+
5+
export const getUpdatesInternal = internalQuery({
6+
args: {
7+
documentId: yjsDocumentIdValidator,
8+
},
9+
returns: v.array(
10+
v.object({
11+
seq: v.number(),
12+
update: v.bytes(),
13+
}),
14+
),
15+
handler: async (ctx, { documentId }) => {
16+
const rows = await ctx.db
17+
.query('yjsUpdates')
18+
.withIndex('by_document_seq', (q) => q.eq('documentId', documentId))
19+
.order('asc')
20+
.collect()
21+
22+
return rows.map((row) => ({ seq: row.seq, update: row.update }))
23+
},
24+
})

0 commit comments

Comments
 (0)