From 48eac1e40de5da3eb6f91ccf90fc5bc143fc2cd0 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 7 Jun 2026 02:49:04 +0200 Subject: [PATCH] fix: recover paragraph editor index errors --- .../blockNoteRenderRecovery.test.ts | 14 +++++++++ src/components/blockNoteRenderRecovery.ts | 5 ++++ ...torTransformErrorRecoveryExtension.test.ts | 16 ++++++++-- ...chEditorTransformErrorRecoveryExtension.ts | 29 ++++++++++++++----- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/components/blockNoteRenderRecovery.test.ts b/src/components/blockNoteRenderRecovery.test.ts index 4e76a994..b618b159 100644 --- a/src/components/blockNoteRenderRecovery.test.ts +++ b/src/components/blockNoteRenderRecovery.test.ts @@ -41,6 +41,20 @@ describe('blockNoteRenderRecovery', () => { expect(isRecoverableBlockNoteRenderError(error)).toBe(true) }) + it('recognizes production paragraph index render errors from slash input', () => { + const error = new RangeError('Index 1 out of range for ') + + expect(blockNoteRenderRecoveryReason(error)).toBe('paragraph_index_out_of_range') + expect(isRecoverableBlockNoteRenderError(error)).toBe(true) + }) + + it('recognizes production paragraph index render errors that are plain Error instances', () => { + const error = new Error('Index 1 out of range for ') + + expect(blockNoteRenderRecoveryReason(error)).toBe('paragraph_index_out_of_range') + expect(isRecoverableBlockNoteRenderError(error)).toBe(true) + }) + it('recognizes recovered BlockNote block type mismatch render errors', () => { const error = new Error('Block type does not match') diff --git a/src/components/blockNoteRenderRecovery.ts b/src/components/blockNoteRenderRecovery.ts index 36eea494..98568638 100644 --- a/src/components/blockNoteRenderRecovery.ts +++ b/src/components/blockNoteRenderRecovery.ts @@ -3,10 +3,12 @@ const BLOCKNOTE_BLOCK_TYPE_MISMATCH_ERROR = 'Block type does not match' const BLOCKNOTE_RECOVERY_BOUNDARY_NAME = 'BlockNoteRenderRecoveryBoundary' const RECOVERED_BLOCKNOTE_RENDER_ERROR_MARK = '__tolariaRecoveredBlockNoteRenderError' const BLOCKNOTE_TABLE_ROW_INDEX_ERROR = /^Index \d+ out of range for { expect(isRecoverableEditorTransformError(new Error( 'Index 1 out of range for ', ))).toBe(true) + expect(isRecoverableEditorTransformError(new RangeError( + 'Index 1 out of range for ', + ))).toBe(true) + expect(isRecoverableEditorTransformError(new Error( + 'Index 1 out of range for ', + ))).toBe(true) expect(isRecoverableEditorTransformError(new Error( 'Block with ID 6c1c3bb4-e218-4f00-aaf5-40606852d286 not found', ))).toBe(true) @@ -106,9 +112,6 @@ describe('isRecoverableEditorTransformError', () => { expect(isRecoverableEditorTransformError(new TypeError( "Cannot read properties of null (reading 'append')", ))).toBe(false) - expect(isRecoverableEditorTransformError(new RangeError( - 'Index 1 out of range for ', - ))).toBe(false) expect(isRecoverableEditorTransformError(new Error('unrelated'))).toBe(false) }) }) @@ -218,6 +221,13 @@ describe('installRichEditorTransformErrorRecovery', () => { ) }) + it('recovers production paragraph index transactions from stale slash input', () => { + expectDocumentRepairRecovery( + new RangeError('Index 1 out of range for '), + 'paragraph_position_out_of_range', + ) + }) + it('recovers invalid insertion-depth transactions after note switching and saves', () => { expectDocumentRepairRecovery( new RangeError('Inserted content deeper than insertion position'), diff --git a/src/components/richEditorTransformErrorRecoveryExtension.ts b/src/components/richEditorTransformErrorRecoveryExtension.ts index 50bebf71..aba94fbb 100644 --- a/src/components/richEditorTransformErrorRecoveryExtension.ts +++ b/src/components/richEditorTransformErrorRecoveryExtension.ts @@ -49,11 +49,17 @@ type RecoveryReason = | 'invalid_insertion_depth' | 'mismatched_transaction' | 'null_fragment_append' + | 'paragraph_position_out_of_range' | 'stale_block_reference' | 'stale_transaction' | 'table_position_out_of_range' | 'transform_error' +interface RecoveryReasonMatcher { + matches: (error: unknown) => boolean + reason: RecoveryReason +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null } @@ -92,6 +98,10 @@ function isTablePositionOutOfRangeError(error: unknown): boolean { return error instanceof Error && /^Index \d+ out of range for predicate(error)) } @@ -135,13 +156,7 @@ function recoveryReason( view: RichEditorDispatchView, ): RecoveryReason { if (transactionDocIsStale(transaction, view)) return 'stale_transaction' - if (isMismatchedTransactionError(error)) return 'mismatched_transaction' - if (isStaleBlockReferenceError(error)) return 'stale_block_reference' - if (isInvalidBlockJoinError(error)) return 'invalid_block_join' - if (isInvalidInsertionDepthError(error)) return 'invalid_insertion_depth' - if (isNullFragmentAppendError(error)) return 'null_fragment_append' - if (isTablePositionOutOfRangeError(error)) return 'table_position_out_of_range' - return 'transform_error' + return RECOVERY_REASON_MATCHERS.find(({ matches }) => matches(error))?.reason ?? 'transform_error' } function shouldRepairEditorDocument(error: unknown): boolean {