fix: improve index migration to handle all invalid formats

- Added isValidTldrawIndex() function to properly validate tldraw
  fractional indices (e.g., "a1", "a1V" are valid, "b1", "c1" are not)
- Apply migration to IndexedDB data as well as server data
- This fixes ValidationError when loading old data with invalid indices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-11-30 16:19:30 -07:00
parent 179a03057e
commit dba981c2af
1 changed files with 49 additions and 4 deletions

View File

@ -8,6 +8,41 @@ import { DocHandle } from "@automerge/automerge-repo"
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb" import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb"
import { getDocumentId, saveDocumentId } from "./documentIdMapping" import { getDocumentId, saveDocumentId } from "./documentIdMapping"
/**
* Validate if an index is a valid tldraw fractional index
* Valid indices: "a0", "a1", "a1V", "a2", "Zz", etc.
* Invalid indices: "b1", "c2", or any simple letter+number that isn't "a" followed by proper format
*
* tldraw uses fractional indexing where indices are strings that can be compared lexicographically
* The format allows inserting new items between any two existing items without renumbering.
*/
function isValidTldrawIndex(index: string): boolean {
if (!index || typeof index !== 'string') return false
// Valid tldraw indices start with 'a' and can have various formats:
// "a0", "a1", "a1V", "a1Vz", "Zz", etc.
// The key insight is that indices NOT starting with 'a' (like 'b1', 'c1') are invalid
// unless they're the special "Zz" format used for very high indices
// Simple indices like "b1", "c1", "d1" are definitely invalid
if (/^[b-z]\d+$/i.test(index)) {
return false
}
// An index starting with 'a' followed by digits and optional letters is valid
// e.g., "a0", "a1", "a1V", "a10", "a1Vz"
if (/^a\d/.test(index)) {
return true
}
// Other formats like "Zz" are also valid for high indices
if (/^[A-Z]/.test(index)) {
return true
}
return false
}
/** /**
* Migrate old data to fix invalid index values * Migrate old data to fix invalid index values
* tldraw requires indices to be in a specific format (fractional indexing) * tldraw requires indices to be in a specific format (fractional indexing)
@ -28,10 +63,7 @@ function migrateStoreData(store: Record<string, any>): Record<string, any> {
const hasInvalidIndices = shapes.some(([_, record]) => { const hasInvalidIndices = shapes.some(([_, record]) => {
const index = record?.index const index = record?.index
if (!index) return false if (!index) return false
// Valid tldraw indices are like "a1", "a1V", "a2", etc. return !isValidTldrawIndex(index)
// Invalid indices would be like "b1" without proper fractional format
// Simple check: if it's just a letter and number, it might be invalid
return typeof index === 'string' && /^[a-z]\d+$/i.test(index) && !index.startsWith('a')
}) })
if (!hasInvalidIndices) { if (!hasInvalidIndices) {
@ -346,6 +378,19 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
if (localRecordCount > 0) { if (localRecordCount > 0) {
console.log(`Loaded document from IndexedDB: ${localRecordCount} records, ${localShapeCount} shapes`) console.log(`Loaded document from IndexedDB: ${localRecordCount} records, ${localShapeCount} shapes`)
// CRITICAL: Migrate local IndexedDB data to fix any invalid indices
// This ensures shapes with old-format indices like "b1" are fixed
if (localDoc?.store) {
const migratedStore = migrateStoreData(localDoc.store)
if (migratedStore !== localDoc.store) {
console.log('🔄 Applying index migration to local IndexedDB data')
handle.change((doc: any) => {
doc.store = migratedStore
})
}
}
loadedFromLocal = true loadedFromLocal = true
} else { } else {
console.log(`Document found in IndexedDB but is empty, will load from server`) console.log(`Document found in IndexedDB but is empty, will load from server`)