251 lines
6.9 KiB
TypeScript
251 lines
6.9 KiB
TypeScript
/**
|
|
* Document ID Mapping Utility
|
|
*
|
|
* Manages the mapping between room IDs (human-readable slugs) and
|
|
* Automerge document IDs (automerge:xxxx format).
|
|
*
|
|
* This is necessary because:
|
|
* - Automerge requires specific document ID formats
|
|
* - We want to persist documents in IndexedDB with consistent IDs
|
|
* - Room IDs are user-friendly slugs that may not match Automerge's format
|
|
*/
|
|
|
|
const DB_NAME = 'canvas-document-mappings'
|
|
const STORE_NAME = 'mappings'
|
|
const DB_VERSION = 1
|
|
|
|
interface DocumentMapping {
|
|
roomId: string
|
|
documentId: string
|
|
createdAt: number
|
|
lastAccessedAt: number
|
|
}
|
|
|
|
let dbInstance: IDBDatabase | null = null
|
|
|
|
/**
|
|
* Open the IndexedDB database for document ID mappings
|
|
*/
|
|
async function openDatabase(): Promise<IDBDatabase> {
|
|
if (dbInstance) return dbInstance
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
|
|
request.onerror = () => {
|
|
console.error('Failed to open document mapping database:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
dbInstance = request.result
|
|
resolve(request.result)
|
|
}
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = (event.target as IDBOpenDBRequest).result
|
|
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
const store = db.createObjectStore(STORE_NAME, { keyPath: 'roomId' })
|
|
store.createIndex('documentId', 'documentId', { unique: true })
|
|
store.createIndex('lastAccessedAt', 'lastAccessedAt', { unique: false })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Get the Automerge document ID for a given room ID
|
|
* Returns null if no mapping exists
|
|
*/
|
|
export async function getDocumentId(roomId: string): Promise<string | null> {
|
|
try {
|
|
const db = await openDatabase()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction(STORE_NAME, 'readonly')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const request = store.get(roomId)
|
|
|
|
request.onerror = () => {
|
|
console.error('Failed to get document mapping:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
const mapping = request.result as DocumentMapping | undefined
|
|
if (mapping) {
|
|
// Update last accessed time in background
|
|
updateLastAccessed(roomId).catch(console.error)
|
|
resolve(mapping.documentId)
|
|
} else {
|
|
resolve(null)
|
|
}
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Error getting document ID:', error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save a mapping between room ID and Automerge document ID
|
|
*/
|
|
export async function saveDocumentId(roomId: string, documentId: string): Promise<void> {
|
|
try {
|
|
const db = await openDatabase()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction(STORE_NAME, 'readwrite')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
|
|
const mapping: DocumentMapping = {
|
|
roomId,
|
|
documentId,
|
|
createdAt: Date.now(),
|
|
lastAccessedAt: Date.now()
|
|
}
|
|
|
|
const request = store.put(mapping)
|
|
|
|
request.onerror = () => {
|
|
console.error('Failed to save document mapping:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
console.log(`Saved document mapping: ${roomId} -> ${documentId}`)
|
|
resolve()
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Error saving document ID:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the last accessed timestamp for a room
|
|
*/
|
|
async function updateLastAccessed(roomId: string): Promise<void> {
|
|
try {
|
|
const db = await openDatabase()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction(STORE_NAME, 'readwrite')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const getRequest = store.get(roomId)
|
|
|
|
getRequest.onerror = () => reject(getRequest.error)
|
|
|
|
getRequest.onsuccess = () => {
|
|
const mapping = getRequest.result as DocumentMapping | undefined
|
|
if (mapping) {
|
|
mapping.lastAccessedAt = Date.now()
|
|
store.put(mapping)
|
|
}
|
|
resolve()
|
|
}
|
|
})
|
|
} catch (error) {
|
|
// Silent fail for background update
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a document mapping (useful for cleanup)
|
|
*/
|
|
export async function deleteDocumentMapping(roomId: string): Promise<void> {
|
|
try {
|
|
const db = await openDatabase()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction(STORE_NAME, 'readwrite')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const request = store.delete(roomId)
|
|
|
|
request.onerror = () => {
|
|
console.error('Failed to delete document mapping:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
console.log(`Deleted document mapping for: ${roomId}`)
|
|
resolve()
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Error deleting document mapping:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all document mappings (useful for debugging/management)
|
|
*/
|
|
export async function getAllMappings(): Promise<DocumentMapping[]> {
|
|
try {
|
|
const db = await openDatabase()
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction(STORE_NAME, 'readonly')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const request = store.getAll()
|
|
|
|
request.onerror = () => {
|
|
console.error('Failed to get all document mappings:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = () => {
|
|
resolve(request.result as DocumentMapping[])
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Error getting all mappings:', error)
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up old document mappings (documents not accessed in X days)
|
|
* This helps manage storage quota
|
|
*/
|
|
export async function cleanupOldMappings(maxAgeDays: number = 30): Promise<number> {
|
|
try {
|
|
const db = await openDatabase()
|
|
const cutoffTime = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000)
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction(STORE_NAME, 'readwrite')
|
|
const store = transaction.objectStore(STORE_NAME)
|
|
const index = store.index('lastAccessedAt')
|
|
const range = IDBKeyRange.upperBound(cutoffTime)
|
|
const request = index.openCursor(range)
|
|
|
|
let deletedCount = 0
|
|
|
|
request.onerror = () => {
|
|
console.error('Failed to cleanup old mappings:', request.error)
|
|
reject(request.error)
|
|
}
|
|
|
|
request.onsuccess = (event) => {
|
|
const cursor = (event.target as IDBRequest).result
|
|
if (cursor) {
|
|
cursor.delete()
|
|
deletedCount++
|
|
cursor.continue()
|
|
} else {
|
|
console.log(`Cleaned up ${deletedCount} old document mappings`)
|
|
resolve(deletedCount)
|
|
}
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Error cleaning up old mappings:', error)
|
|
return 0
|
|
}
|
|
}
|