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 c175c0f29a
commit c070e673ad
1 changed files with 77 additions and 2 deletions

View File

@ -1,5 +1,5 @@
import { useMemo, useEffect, useState, useCallback, useRef } from "react" 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 { CloudflareNetworkAdapter } from "./CloudflareAdapter"
import { useAutomergeStoreV2, useAutomergePresence } from "./useAutomergeStoreV2" import { useAutomergeStoreV2, useAutomergePresence } from "./useAutomergeStoreV2"
import { TLStoreWithStatus } from "@tldraw/tldraw" import { TLStoreWithStatus } from "@tldraw/tldraw"
@ -8,6 +8,72 @@ 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"
/**
* 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 { interface AutomergeSyncConfig {
uri: string uri: string
assets?: any assets?: any
@ -311,7 +377,16 @@ export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus
try { try {
const response = await fetch(`${workerUrl}/room/${roomId}`) const response = await fetch(`${workerUrl}/room/${roomId}`)
if (response.ok) { 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 serverShapeCount = serverDoc.store ? Object.values(serverDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0
const serverRecordCount = Object.keys(serverDoc.store || {}).length const serverRecordCount = Object.keys(serverDoc.store || {}).length