From 9f58917c3923e631f1a3af47838aa76f7fdd371a Mon Sep 17 00:00:00 2001 From: CC Worker Date: Mon, 1 Jun 2026 06:02:19 +0000 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20sanitize=20snapshot=20session.cu?= =?UTF-8?q?rrentPageId=20before=20loadSnapshot=20=E2=80=94=20stale=20snaps?= =?UTF-8?q?hots=20with=20missing=20pages=20caused=20currentPageId=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pre-load: if session.currentPageId not in snapshot pages, reset to first available page - Post-load: if store instance.currentPageId still invalid, clear store for fresh start - On loadSnapshot error: clear store instead of silently failing with corrupt state - Root cause: teacher1 had a saved snapshot with currentPageId pointing to a deleted page --- src/services/tldraw/snapshotService.ts | 31 +++++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/services/tldraw/snapshotService.ts b/src/services/tldraw/snapshotService.ts index 1f0712d..84c26ae 100644 --- a/src/services/tldraw/snapshotService.ts +++ b/src/services/tldraw/snapshotService.ts @@ -1,5 +1,5 @@ // External imports -import { TLStore, getSnapshot, Editor, loadSnapshot } from '@tldraw/tldraw'; +import { TLStore, getSnapshot, Editor, loadSnapshot, TLINSTANCE_ID } from '@tldraw/tldraw'; import logger from '../../debugConfig'; import { SharedStoreService } from './sharedStoreService'; @@ -109,17 +109,35 @@ export class NavigationSnapshotService { return; } + // Sanitize session.currentPageId against pages actually in the document + const docStore = (snap.document as { store?: Record })?.store ?? {}; + const pageIds = Object.values(docStore).filter((r) => (r as { typeName: string }).typeName === 'page').map((r) => r.id); + const session = snap.session as { currentPageId?: string } | undefined; + if (session?.currentPageId && pageIds.length > 0 && !pageIds.includes(session.currentPageId)) { + logger.warn('snapshot-service', '⚠️ session.currentPageId not in snapshot pages — resetting to first page', { + currentPageId: session.currentPageId, availablePages: pageIds + }); + session.currentPageId = pageIds[0]; + } + const snapshotCopy = { schemaVersion: snap.schemaVersion || (snap.document as { schema?: { schemaVersion?: unknown } })?.schema?.schemaVersion, document: snap.document, session: snap.session, }; + const targetStore = editor ? editor.store : store; try { - if (editor) { - loadSnapshot(editor.store, snapshotCopy as any); - } else { - loadSnapshot(store, snapshotCopy as any); + loadSnapshot(targetStore, snapshotCopy as any); + // Post-load validation: ensure instance.currentPageId is still valid + const instance = targetStore.get(TLINSTANCE_ID); + if (instance) { + const pages = targetStore.allRecords().filter((r) => (r as { typeName: string }).typeName === 'page'); + const pageValid = pages.some((p) => p.id === instance.currentPageId); + if (!pageValid) { + logger.warn('snapshot-service', '⚠️ Post-load: currentPageId invalid — clearing corrupt store', { currentPageId: instance.currentPageId }); + targetStore.clear(); + } } logger.debug('snapshot-service', '✅ Snapshot loaded successfully'); } catch (err) { @@ -128,7 +146,8 @@ export class NavigationSnapshotService { if (isSchemaMigration) { logger.debug('snapshot-service', 'ℹ️ Schema migration warning (non-critical)', { error: msg }); } else { - logger.warn('snapshot-service', '⚠️ Unexpected loadSnapshot error', { error: msg }); + logger.warn('snapshot-service', '⚠️ Unexpected loadSnapshot error — clearing store to allow fresh start', { error: msg }); + targetStore.clear(); } }