182 lines
5.1 KiB
TypeScript
182 lines
5.1 KiB
TypeScript
import { useMemo, useEffect, useState, useCallback } from "react"
|
|
import { TLStoreSnapshot } from "@tldraw/tldraw"
|
|
import { CloudflareNetworkAdapter } from "./CloudflareAdapter"
|
|
import { useAutomergeStoreV2, useAutomergePresence } from "./useAutomergeStoreV2"
|
|
import { TLStoreWithStatus } from "@tldraw/tldraw"
|
|
import { Repo } from "@automerge/automerge-repo"
|
|
|
|
interface AutomergeSyncConfig {
|
|
uri: string
|
|
assets?: any
|
|
shapeUtils?: any[]
|
|
bindingUtils?: any[]
|
|
user?: {
|
|
id: string
|
|
name: string
|
|
}
|
|
}
|
|
|
|
export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus {
|
|
const { uri, user } = config
|
|
|
|
// Extract roomId from URI (e.g., "https://worker.com/connect/room123" -> "room123")
|
|
const roomId = useMemo(() => {
|
|
const match = uri.match(/\/connect\/([^\/]+)$/)
|
|
return match ? match[1] : "default-room"
|
|
}, [uri])
|
|
|
|
// Extract worker URL from URI (remove /connect/roomId part)
|
|
const workerUrl = useMemo(() => {
|
|
return uri.replace(/\/connect\/.*$/, '')
|
|
}, [uri])
|
|
|
|
const [repo] = useState(() => new Repo({
|
|
network: [new CloudflareNetworkAdapter(workerUrl, roomId)]
|
|
}))
|
|
const [handle, setHandle] = useState<any>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
// Initialize Automerge document handle
|
|
useEffect(() => {
|
|
let mounted = true
|
|
|
|
const initializeHandle = async () => {
|
|
try {
|
|
console.log("🔌 Initializing Automerge Repo with NetworkAdapter")
|
|
|
|
if (mounted) {
|
|
// Create a new document - Automerge will generate the proper document ID
|
|
// Force refresh to clear cache
|
|
const handle = repo.create()
|
|
|
|
console.log("Created Automerge handle via Repo:", {
|
|
handleId: handle.documentId,
|
|
isReady: handle.isReady()
|
|
})
|
|
|
|
// Wait for the handle to be ready
|
|
await handle.whenReady()
|
|
|
|
console.log("Automerge handle is ready:", {
|
|
hasDoc: !!handle.doc(),
|
|
docKeys: handle.doc() ? Object.keys(handle.doc()).length : 0
|
|
})
|
|
|
|
setHandle(handle)
|
|
setIsLoading(false)
|
|
}
|
|
} catch (error) {
|
|
console.error("Error initializing Automerge handle:", error)
|
|
if (mounted) {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
initializeHandle()
|
|
|
|
return () => {
|
|
mounted = false
|
|
}
|
|
}, [repo, roomId])
|
|
|
|
// Auto-save to Cloudflare on every change (with debouncing to prevent excessive calls)
|
|
useEffect(() => {
|
|
if (!handle) return
|
|
|
|
let saveTimeout: NodeJS.Timeout
|
|
|
|
const scheduleSave = () => {
|
|
// Clear existing timeout
|
|
if (saveTimeout) clearTimeout(saveTimeout)
|
|
|
|
// Schedule save with a short debounce (500ms) to batch rapid changes
|
|
saveTimeout = setTimeout(async () => {
|
|
try {
|
|
// With Repo, we don't need manual saving - the NetworkAdapter handles sync
|
|
console.log("🔍 Automerge changes detected - NetworkAdapter will handle sync")
|
|
} catch (error) {
|
|
console.error('Error in change-triggered save:', error)
|
|
}
|
|
}, 500)
|
|
}
|
|
|
|
// Listen for changes to the Automerge document
|
|
const changeHandler = (payload: any) => {
|
|
console.log('🔍 Automerge document changed:', {
|
|
hasPatches: !!payload.patches,
|
|
patchCount: payload.patches?.length || 0,
|
|
patches: payload.patches?.map((p: any) => ({
|
|
action: p.action,
|
|
path: p.path,
|
|
value: p.value ? (typeof p.value === 'object' ? 'object' : p.value) : 'undefined'
|
|
}))
|
|
})
|
|
scheduleSave()
|
|
}
|
|
|
|
handle.on('change', changeHandler)
|
|
|
|
return () => {
|
|
handle.off('change', changeHandler)
|
|
if (saveTimeout) clearTimeout(saveTimeout)
|
|
}
|
|
}, [handle])
|
|
|
|
// Get the store from the Automerge document
|
|
const store = useMemo(() => {
|
|
if (!handle?.doc()) {
|
|
return null
|
|
}
|
|
|
|
const doc = handle.doc()
|
|
if (!doc.store) {
|
|
return null
|
|
}
|
|
|
|
return doc.store
|
|
}, [handle])
|
|
|
|
// Get the store with status
|
|
const storeWithStatus = useMemo((): TLStoreWithStatus => {
|
|
if (!store) {
|
|
return {
|
|
status: 'loading' as const
|
|
}
|
|
}
|
|
|
|
return {
|
|
status: 'synced-remote' as const,
|
|
connectionStatus: 'online' as const,
|
|
store
|
|
}
|
|
}, [store, isLoading])
|
|
|
|
// Get presence data (only when handle is ready)
|
|
const userMetadata: { userId: string; name: string; color: string } = (() => {
|
|
if (user && 'userId' in user) {
|
|
return {
|
|
userId: (user as { userId: string; name: string; color?: string }).userId,
|
|
name: (user as { userId: string; name: string; color?: string }).name,
|
|
color: (user as { userId: string; name: string; color?: string }).color || '#000000'
|
|
}
|
|
}
|
|
return {
|
|
userId: user?.id || 'anonymous',
|
|
name: user?.name || 'Anonymous',
|
|
color: '#000000'
|
|
}
|
|
})()
|
|
|
|
const presence = useAutomergePresence({
|
|
handle: handle || null,
|
|
store: store || null,
|
|
userMetadata
|
|
})
|
|
|
|
return {
|
|
...storeWithStatus,
|
|
presence
|
|
} as TLStoreWithStatus & { presence: typeof presence }
|
|
}
|