fix: migrate invalid shape indices in old data

Adds migrateStoreData() function to fix ValidationError when loading
old data with invalid index keys (e.g., 'b1' instead of fractional
indices like 'a1V'). The migration detects invalid indices and
regenerates valid ones using tldraw's getIndexAbove().

🤖 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-29 22:40:21 -08:00
parent 25357871d8
commit 3f0fb1f85d
1 changed files with 77 additions and 2 deletions

View File

@ -1,5 +1,5 @@
import { useMemo, useEffect, useState, useCallback, useRef } from "react"
import { TLStoreSnapshot, InstancePresenceRecordType } from "@tldraw/tldraw"
import { TLStoreSnapshot, InstancePresenceRecordType, getIndexAbove, IndexKey } from "@tldraw/tldraw"
import { CloudflareNetworkAdapter } from "./CloudflareAdapter"
import { useAutomergeStoreV2, useAutomergePresence } from "./useAutomergeStoreV2"
import { TLStoreWithStatus } from "@tldraw/tldraw"
@ -8,6 +8,72 @@ import { DocHandle } from "@automerge/automerge-repo"
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb"
import { getDocumentId, saveDocumentId } from "./documentIdMapping"
/**
* Migrate old data to fix invalid index values
* tldraw requires indices to be in a specific format (fractional indexing)
* Old data may have simple indices like "b1" which are invalid
*/
function migrateStoreData(store: Record<string, any>): Record<string, any> {
if (!store) return store
const migratedStore: Record<string, any> = {}
let currentIndex: IndexKey = 'a1' as IndexKey // Start with a valid index
// Sort shapes by their old index to maintain relative ordering
const entries = Object.entries(store)
const shapes = entries.filter(([_, record]) => record?.typeName === 'shape')
const nonShapes = entries.filter(([_, record]) => record?.typeName !== 'shape')
// Check if any shapes have invalid indices
const hasInvalidIndices = shapes.some(([_, record]) => {
const index = record?.index
if (!index) return false
// Valid tldraw indices are like "a1", "a1V", "a2", etc.
// 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) {
// No migration needed
return store
}
console.log('🔄 Migrating store data: fixing invalid shape indices')
// Copy non-shape records as-is
for (const [id, record] of nonShapes) {
migratedStore[id] = record
}
// Sort shapes by their original index (alphabetically) to maintain order
shapes.sort((a, b) => {
const indexA = a[1]?.index || ''
const indexB = b[1]?.index || ''
return indexA.localeCompare(indexB)
})
// Regenerate valid indices for shapes
for (const [id, record] of shapes) {
const migratedRecord = { ...record }
// Generate a new valid index
try {
currentIndex = getIndexAbove(currentIndex)
} catch {
// Fallback if getIndexAbove fails - generate simple sequential index
const num = parseInt(currentIndex.slice(1) || '1') + 1
currentIndex = `a${num}` as IndexKey
}
migratedRecord.index = currentIndex
migratedStore[id] = migratedRecord
}
console.log(`✅ Migrated ${shapes.length} shapes with new indices`)
return migratedStore
}
interface AutomergeSyncConfig {
uri: string
assets?: any
@ -311,7 +377,16 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
try {
const response = await fetch(`${workerUrl}/room/${roomId}`)
if (response.ok) {
const serverDoc = await response.json() as TLStoreSnapshot
let serverDoc = await response.json() as TLStoreSnapshot
// Migrate server data to fix any invalid indices
if (serverDoc.store) {
serverDoc = {
...serverDoc,
store: migrateStoreData(serverDoc.store)
}
}
const serverShapeCount = serverDoc.store ? Object.values(serverDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0
const serverRecordCount = Object.keys(serverDoc.store || {}).length