Skip to content

Commit a2636e4

Browse files
committed
fix block persistence bug
1 parent bbe5b7b commit a2636e4

10 files changed

Lines changed: 119 additions & 16 deletions

File tree

convex/_generated/api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type * as blockShares_mutations from "../blockShares/mutations.js";
2424
import type * as blockShares_queries from "../blockShares/queries.js";
2525
import type * as blockShares_types from "../blockShares/types.js";
2626
import type * as blocks_blockSchemas from "../blocks/blockSchemas.js";
27+
import type * as blocks_functions_ensureBlocksPersisted from "../blocks/functions/ensureBlocksPersisted.js";
2728
import type * as blocks_functions_extractPlainText from "../blocks/functions/extractPlainText.js";
2829
import type * as blocks_functions_findBlockByBlockNoteId from "../blocks/functions/findBlockByBlockNoteId.js";
2930
import type * as blocks_functions_flattenBlocks from "../blocks/functions/flattenBlocks.js";
@@ -239,6 +240,7 @@ declare const fullApi: ApiFromModules<{
239240
"blockShares/queries": typeof blockShares_queries;
240241
"blockShares/types": typeof blockShares_types;
241242
"blocks/blockSchemas": typeof blocks_blockSchemas;
243+
"blocks/functions/ensureBlocksPersisted": typeof blocks_functions_ensureBlocksPersisted;
242244
"blocks/functions/extractPlainText": typeof blocks_functions_extractPlainText;
243245
"blocks/functions/findBlockByBlockNoteId": typeof blocks_functions_findBlockByBlockNoteId;
244246
"blocks/functions/flattenBlocks": typeof blocks_functions_flattenBlocks;

convex/_test/factories.helper.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { CAMPAIGN_MEMBER_ROLE, CAMPAIGN_MEMBER_STATUS } from '../campaigns/types
22
import { SIDEBAR_ITEM_LOCATION, SIDEBAR_ITEM_TYPES } from '../sidebarItems/types/baseTypes'
33
import { SHARE_STATUS } from '../blockShares/types'
44
import { slugify } from '../common/slug'
5+
import { makeYjsUpdateWithBlocks } from '../yjsSync/__tests__/makeYjsUpdate.helper'
6+
import type { TestBlock } from '../yjsSync/__tests__/makeYjsUpdate.helper'
57
import type { TestConvex } from 'convex-test'
68
import type { Id } from '../_generated/dataModel'
79
import type schema from '../schema'
@@ -552,3 +554,25 @@ export async function setupFolderTree(
552554
}
553555
}
554556
}
557+
558+
export async function syncBlocksToYjs(
559+
t: T,
560+
noteId: Id<'sidebarItems'>,
561+
blocks: Array<TestBlock>,
562+
): Promise<void> {
563+
const update = makeYjsUpdateWithBlocks(blocks)
564+
await t.run(async (ctx) => {
565+
const latest = await ctx.db
566+
.query('yjsUpdates')
567+
.withIndex('by_document_seq', (q: any) => q.eq('documentId', noteId))
568+
.order('desc')
569+
.first()
570+
const seq = (latest?.seq ?? -1) + 1
571+
await ctx.db.insert('yjsUpdates', {
572+
documentId: noteId,
573+
update,
574+
seq,
575+
isSnapshot: false,
576+
})
577+
})
578+
}

convex/blockShares/__tests__/blockShares.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createBlockShare,
1212
createNote,
1313
createSidebarShare,
14+
syncBlocksToYjs,
1415
} from '../../_test/factories.helper'
1516
import { expectNotFound, expectPermissionDenied } from '../../_test/assertions.helper'
1617
import { api } from '../../_generated/api'
@@ -24,6 +25,7 @@ describe('setBlocksShareStatus', () => {
2425
const dmAuth = asDm(ctx)
2526
const { noteId } = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
2627
const { blockNoteId } = await createBlock(t, noteId, ctx.campaignId, ctx.dm.profile._id)
28+
await syncBlocksToYjs(t, noteId, [{ id: blockNoteId, type: 'paragraph' }])
2729

2830
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
2931
campaignId: ctx.campaignId,
@@ -48,6 +50,7 @@ describe('setBlocksShareStatus', () => {
4850
const { blockNoteId } = await createBlock(t, noteId, ctx.campaignId, ctx.dm.profile._id, {
4951
shareStatus: 'all_shared',
5052
})
53+
await syncBlocksToYjs(t, noteId, [{ id: blockNoteId, type: 'paragraph' }])
5154

5255
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
5356
campaignId: ctx.campaignId,
@@ -70,6 +73,7 @@ describe('setBlocksShareStatus', () => {
7073
const dmAuth = asDm(ctx)
7174
const { noteId } = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
7275
const { blockNoteId } = await createBlock(t, noteId, ctx.campaignId, ctx.dm.profile._id)
76+
await syncBlocksToYjs(t, noteId, [{ id: blockNoteId, type: 'paragraph' }])
7377

7478
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
7579
campaignId: ctx.campaignId,
@@ -126,6 +130,7 @@ describe('shareBlocks', () => {
126130
const dmAuth = asDm(ctx)
127131
const { noteId } = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
128132
const { blockNoteId } = await createBlock(t, noteId, ctx.campaignId, ctx.dm.profile._id)
133+
await syncBlocksToYjs(t, noteId, [{ id: blockNoteId, type: 'paragraph' }])
129134

130135
await dmAuth.mutation(api.blockShares.mutations.shareBlocks, {
131136
campaignId: ctx.campaignId,

convex/blockShares/__tests__/blockSharesWithNestedBlocks.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createBlock,
66
createBlockShare,
77
createNote,
8+
syncBlocksToYjs,
89
testBlockNoteId,
910
} from '../../_test/factories.helper'
1011
import { api } from '../../_generated/api'
@@ -28,6 +29,13 @@ describe('share mutations with nested blocks', () => {
2829
depth: 1,
2930
parentBlockId: testBlockNoteId('root'),
3031
})
32+
await syncBlocksToYjs(t, noteId, [
33+
{
34+
id: testBlockNoteId('root'),
35+
type: 'paragraph',
36+
children: [{ id: testBlockNoteId('child'), type: 'paragraph' }],
37+
},
38+
])
3139

3240
await dmAuth.mutation(api.blockShares.mutations.shareBlocks, {
3341
campaignId: ctx.campaignId,
@@ -67,6 +75,19 @@ describe('share mutations with nested blocks', () => {
6775
depth: 2,
6876
parentBlockId: testBlockNoteId('depth-1'),
6977
})
78+
await syncBlocksToYjs(t, noteId, [
79+
{
80+
id: testBlockNoteId('depth-0'),
81+
type: 'paragraph',
82+
children: [
83+
{
84+
id: testBlockNoteId('depth-1'),
85+
type: 'paragraph',
86+
children: [{ id: testBlockNoteId('depth-2'), type: 'paragraph' }],
87+
},
88+
],
89+
},
90+
])
7091

7192
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
7293
campaignId: ctx.campaignId,
@@ -111,6 +132,13 @@ describe('share mutations with nested blocks', () => {
111132
parentBlockId: testBlockNoteId('some-parent'),
112133
shareStatus: 'individually_shared',
113134
})
135+
await syncBlocksToYjs(t, noteId, [
136+
{
137+
id: testBlockNoteId('some-parent'),
138+
type: 'paragraph',
139+
children: [{ id: testBlockNoteId('nested'), type: 'paragraph' }],
140+
},
141+
])
114142

115143
await createBlockShare(t, ctx.dm.profile._id, {
116144
campaignId: ctx.campaignId,

convex/blockShares/__tests__/blockSharingWorkflows.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { describe, expect, it } from 'vitest'
22
import { createTestContext } from '../../_test/setup.helper'
33
import { asDm, setupMultiPlayerContext } from '../../_test/identities.helper'
4-
import { createBlock, createNote, createSidebarShare } from '../../_test/factories.helper'
4+
import {
5+
createBlock,
6+
createNote,
7+
createSidebarShare,
8+
syncBlocksToYjs,
9+
} from '../../_test/factories.helper'
510
import { api } from '../../_generated/api'
611

712
describe('block sharing workflows', () => {
@@ -17,6 +22,10 @@ describe('block sharing workflows', () => {
1722
const note = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
1823
const block1 = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id)
1924
const block2 = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id)
25+
await syncBlocksToYjs(t, note.noteId, [
26+
{ id: block1.blockNoteId, type: 'paragraph' },
27+
{ id: block2.blockNoteId, type: 'paragraph' },
28+
])
2029

2130
await createSidebarShare(t, ctx.dm.profile._id, {
2231
campaignId: ctx.campaignId,
@@ -105,6 +114,7 @@ describe('block sharing workflows', () => {
105114

106115
const note = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
107116
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id)
117+
await syncBlocksToYjs(t, note.noteId, [{ id: block.blockNoteId, type: 'paragraph' }])
108118

109119
await createSidebarShare(t, ctx.dm.profile._id, {
110120
campaignId: ctx.campaignId,
@@ -216,6 +226,7 @@ describe('block sharing workflows', () => {
216226

217227
const note = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
218228
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id)
229+
await syncBlocksToYjs(t, note.noteId, [{ id: block.blockNoteId, type: 'paragraph' }])
219230

220231
await createSidebarShare(t, ctx.dm.profile._id, {
221232
campaignId: ctx.campaignId,
@@ -278,6 +289,7 @@ describe('block sharing workflows', () => {
278289

279290
const note = await createNote(t, ctx.campaignId, ctx.dm.profile._id)
280291
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id)
292+
await syncBlocksToYjs(t, note.noteId, [{ id: block.blockNoteId, type: 'paragraph' }])
281293

282294
await dmAuth.mutation(api.blockShares.mutations.setBlocksShareStatus, {
283295
campaignId: ctx.campaignId,

convex/blockShares/functions/setBlocksShareStatus.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EDIT_HISTORY_ACTION } from '../../editHistory/types'
66
import { SIDEBAR_ITEM_TYPES } from '../../sidebarItems/types/baseTypes'
77
import { setBlockShareStatusHelper } from './blockShareMutations'
88
import { getSidebarItem } from '../../sidebarItems/functions/getSidebarItem'
9+
import { ensureBlocksPersisted } from '../../blocks/functions/ensureBlocksPersisted'
910
import { ERROR_CODE, throwClientError } from '../../errors'
1011
import type { CampaignMutationCtx } from '../../functions'
1112
import type { Id } from '../../_generated/dataModel'
@@ -36,6 +37,8 @@ export const setBlocksShareStatus = async (
3637
requiredLevel: PERMISSION_LEVEL.FULL_ACCESS,
3738
})
3839

40+
await ensureBlocksPersisted(ctx, { noteId })
41+
3942
await asyncMap(blockNoteIds, (blockNoteId) =>
4043
setBlockShareStatusHelper(ctx, {
4144
note,

convex/blockShares/functions/shareBlocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { EDIT_HISTORY_ACTION } from '../../editHistory/types'
66
import { SIDEBAR_ITEM_TYPES } from '../../sidebarItems/types/baseTypes'
77
import { shareBlockWithMemberHelper } from './blockShareMutations'
88
import { getSidebarItem } from '../../sidebarItems/functions/getSidebarItem'
9+
import { ensureBlocksPersisted } from '../../blocks/functions/ensureBlocksPersisted'
910
import { ERROR_CODE, throwClientError } from '../../errors'
1011
import type { CampaignMutationCtx } from '../../functions'
1112
import type { Id } from '../../_generated/dataModel'
@@ -31,6 +32,8 @@ export const shareBlocks = async (
3132
requiredLevel: PERMISSION_LEVEL.FULL_ACCESS,
3233
})
3334

35+
await ensureBlocksPersisted(ctx, { noteId })
36+
3437
await asyncMap(blockNoteIds, (blockNoteId) =>
3538
shareBlockWithMemberHelper(ctx, {
3639
note,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { reconstructYDoc } from '../../yjsSync/functions/reconstructYDoc'
2+
import { yDocToBlocks } from '../../notes/blocknote'
3+
import { saveAllBlocksForNote } from './saveAllBlocksForNote'
4+
import type { CampaignMutationCtx } from '../../functions'
5+
import type { Id } from '../../_generated/dataModel'
6+
7+
export async function ensureBlocksPersisted(
8+
ctx: CampaignMutationCtx,
9+
{ noteId }: { noteId: Id<'sidebarItems'> },
10+
): Promise<void> {
11+
const { doc } = await reconstructYDoc(ctx, noteId)
12+
try {
13+
const blocks = yDocToBlocks(doc, 'document')
14+
await saveAllBlocksForNote(ctx, { noteId, content: blocks })
15+
} finally {
16+
doc.destroy()
17+
}
18+
}

convex/notes/__tests__/noteWorkflows.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createFolder,
1212
createNote,
1313
createSidebarShare,
14+
syncBlocksToYjs,
1415
testBlockNoteId,
1516
} from '../../_test/factories.helper'
1617
import { api } from '../../_generated/api'
@@ -30,6 +31,9 @@ describe('note lifecycle: create, share, edit, block sharing', () => {
3031
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id, {
3132
blockNoteId: testBlockNoteId('secret-block'),
3233
})
34+
await syncBlocksToYjs(t, note.noteId, [
35+
{ id: testBlockNoteId('secret-block'), type: 'paragraph' },
36+
])
3337

3438
await createSidebarShare(t, ctx.dm.profile._id, {
3539
campaignId: ctx.campaignId,
@@ -92,6 +96,9 @@ describe('note lifecycle: create, share, edit, block sharing', () => {
9296
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id, {
9397
blockNoteId: testBlockNoteId('revocable-block'),
9498
})
99+
await syncBlocksToYjs(t, note.noteId, [
100+
{ id: testBlockNoteId('revocable-block'), type: 'paragraph' },
101+
])
95102

96103
await createSidebarShare(t, ctx.dm.profile._id, {
97104
campaignId: ctx.campaignId,
@@ -161,6 +168,7 @@ describe('note lifecycle: create, share, edit, block sharing', () => {
161168
const block = await createBlock(t, noteId, ctx.campaignId, ctx.dm.profile._id, {
162169
blockNoteId: testBlockNoteId('nested-block'),
163170
})
171+
await syncBlocksToYjs(t, noteId, [{ id: testBlockNoteId('nested-block'), type: 'paragraph' }])
164172

165173
const playerNoteWithBlocks = await playerAuth.query(api.notes.queries.getNote, {
166174
campaignId: ctx.campaignId,
@@ -228,6 +236,9 @@ describe('note lifecycle: create, share, edit, block sharing', () => {
228236
const block = await createBlock(t, note.noteId, ctx.campaignId, ctx.dm.profile._id, {
229237
blockNoteId: testBlockNoteId('transition-block'),
230238
})
239+
await syncBlocksToYjs(t, note.noteId, [
240+
{ id: testBlockNoteId('transition-block'), type: 'paragraph' },
241+
])
231242

232243
await createSidebarShare(t, ctx.dm.profile._id, {
233244
campaignId: ctx.campaignId,
@@ -304,6 +315,16 @@ describe('note lifecycle: create, share, edit, block sharing', () => {
304315
parentBlockId: testBlockNoteId('root'),
305316
position: 1,
306317
})
318+
await syncBlocksToYjs(t, note.noteId, [
319+
{
320+
id: testBlockNoteId('root'),
321+
type: 'paragraph',
322+
children: [
323+
{ id: testBlockNoteId('shared-child'), type: 'paragraph' },
324+
{ id: testBlockNoteId('unshared-child'), type: 'paragraph' },
325+
],
326+
},
327+
])
307328

308329
await createSidebarShare(t, ctx.dm.profile._id, {
309330
campaignId: ctx.campaignId,

convex/notes/mutations.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { v } from 'convex/values'
22
import { campaignMutation } from '../functions'
33
import { customBlockValidator } from '../blocks/schema'
4-
import { saveAllBlocksForNote } from '../blocks/functions/saveAllBlocksForNote'
4+
import { ensureBlocksPersisted } from '../blocks/functions/ensureBlocksPersisted'
55
import { checkYjsWriteAccess } from '../yjsSync/functions/checkYjsAccess'
6-
import { reconstructYDoc } from '../yjsSync/functions/reconstructYDoc'
76
import { createNote as createNoteFn } from './functions/createNote'
87
import { updateNote as updateNoteFn } from './functions/updateNote'
9-
import { yDocToBlocks } from './blocknote'
108
import type { Id } from '../_generated/dataModel'
119

1210
export const updateNote = campaignMutation({
@@ -60,18 +58,7 @@ export const persistNoteBlocks = campaignMutation({
6058
returns: v.null(),
6159
handler: async (ctx, { documentId }) => {
6260
await checkYjsWriteAccess(ctx, documentId)
63-
64-
const { doc } = await reconstructYDoc(ctx, documentId)
65-
try {
66-
const blocks = yDocToBlocks(doc, 'document')
67-
68-
await saveAllBlocksForNote(ctx, {
69-
noteId: documentId,
70-
content: blocks,
71-
})
72-
} finally {
73-
doc.destroy()
74-
}
61+
await ensureBlocksPersisted(ctx, { noteId: documentId })
7562

7663
return null
7764
},

0 commit comments

Comments
 (0)