Spaces:
Sleeping
Sleeping
| export const Registry = { | |
| // App instance reference - set by the main app during initialization | |
| app: null, | |
| // Track recently deleted items to avoid re-syncing them (KV eventual consistency workaround) | |
| recentlyDeleted: new Set(), | |
| setApp(appInstance) { | |
| this.app = appInstance; | |
| // Load recently deleted from localStorage | |
| try { | |
| const deleted = JSON.parse(localStorage.getItem('crm_recently_deleted') || '[]'); | |
| // Only keep items deleted in the last 5 minutes | |
| const fiveMinutesAgo = Date.now() - 5 * 60 * 1000; | |
| const valid = deleted.filter(d => d.time > fiveMinutesAgo); | |
| this.recentlyDeleted = new Set(valid.map(d => d.id)); | |
| localStorage.setItem('crm_recently_deleted', JSON.stringify(valid)); | |
| } catch (e) { | |
| this.recentlyDeleted = new Set(); | |
| } | |
| }, | |
| markAsDeleted(id) { | |
| this.recentlyDeleted.add(id); | |
| try { | |
| const deleted = JSON.parse(localStorage.getItem('crm_recently_deleted') || '[]'); | |
| deleted.push({ id, time: Date.now() }); | |
| localStorage.setItem('crm_recently_deleted', JSON.stringify(deleted)); | |
| } catch (e) {} | |
| }, | |
| getApp() { | |
| // Fallback to window.App for backwards compatibility | |
| return this.app || window.App; | |
| }, | |
| getToken() { return localStorage.getItem('tldraw_auth_token'); }, | |
| getUsername() { return localStorage.getItem('tldraw_auth_username'); }, | |
| // Helper to get API URL (supports bundled mode) | |
| apiUrl(path) { | |
| return window.Config ? window.Config.apiUrl(path) : path; | |
| }, | |
| async sync() { | |
| const token = this.getToken(); | |
| if (!token) return; // Anonymous users don't sync registry | |
| try { | |
| // 1. Fetch Cloud Registry | |
| const res = await fetch(this.apiUrl('/api/color_rm/registry'), { | |
| headers: { 'Authorization': `Bearer ${token}` } | |
| }); | |
| if (!res.ok) { | |
| if (res.status === 401) { | |
| console.warn("Registry: Auth token invalid/expired, skipping sync."); | |
| return; // Silent fail for expired tokens | |
| } | |
| throw new Error("Registry fetch failed"); | |
| } | |
| const data = await res.json(); | |
| console.log('Registry Sync Data:', data); | |
| const cloudProjects = data.projects || []; | |
| const cloudFolders = data.folders || []; | |
| // Filter out recently deleted items (KV eventual consistency workaround) | |
| const filteredProjects = cloudProjects.filter(p => !this.recentlyDeleted.has(p.id)); | |
| const filteredFolders = cloudFolders.filter(f => !this.recentlyDeleted.has(f.id)); | |
| if (filteredProjects.length !== cloudProjects.length) { | |
| console.log(`Registry: Filtered out ${cloudProjects.length - filteredProjects.length} recently deleted projects`); | |
| } | |
| const cloudIds = new Set(filteredProjects.map(p => p.id)); | |
| const cloudFolderIds = new Set(filteredFolders.map(f => f.id)); | |
| // 2. Merge into Local DB | |
| const app = this.getApp(); | |
| if (!app || !app.db) { | |
| console.warn("Registry: App DB not ready."); | |
| return; | |
| } | |
| // --- SYNC SESSIONS (use separate transaction) --- | |
| const localSessions = await new Promise((resolve) => { | |
| const tx = app.db.transaction('sessions', 'readonly'); | |
| const r = tx.objectStore('sessions').getAll(); | |
| r.onsuccess = () => resolve(r.result || []); | |
| r.onerror = () => resolve([]); | |
| }); | |
| const localMap = new Map(localSessions.map(p => [p.id, p])); | |
| for (const cp of filteredProjects) { | |
| const local = localMap.get(cp.id); | |
| if (!local) { | |
| // New project from cloud (metadata only) | |
| await app.dbPut('sessions', { | |
| id: cp.id, | |
| name: cp.name, | |
| pageCount: cp.pageCount, | |
| lastMod: cp.lastMod, | |
| ownerId: cp.ownerId, | |
| baseFileName: cp.baseFileName, | |
| idx: 0, | |
| bookmarks: [], | |
| clipboardBox: [], | |
| state: null, | |
| folderId: cp.folderId || null, // Sync folderId | |
| isCloudBackedUp: true | |
| }); | |
| } else { | |
| // Update existing | |
| let changed = false; | |
| if (cp.lastMod > (local.lastMod || 0)) { | |
| local.name = cp.name; | |
| local.pageCount = cp.pageCount; | |
| local.lastMod = cp.lastMod; | |
| local.ownerId = cp.ownerId; | |
| local.folderId = cp.folderId || null; // Update folderId | |
| changed = true; | |
| } | |
| // Mark as backed up | |
| if (!local.isCloudBackedUp) { | |
| local.isCloudBackedUp = true; | |
| changed = true; | |
| } | |
| if (changed) await app.dbPut('sessions', local); | |
| } | |
| } | |
| // --- SYNC FOLDERS (use separate transaction) --- | |
| const localFolders = await new Promise(r => { | |
| const tx = app.db.transaction('folders', 'readonly'); | |
| const req = tx.objectStore('folders').getAll(); | |
| req.onsuccess = () => r(req.result || []); | |
| req.onerror = () => r([]); | |
| }); | |
| const localFolderMap = new Map(localFolders.map(f => [f.id, f])); | |
| for (const cf of filteredFolders) { | |
| const lf = localFolderMap.get(cf.id); | |
| if (!lf) { | |
| await app.dbPut('folders', cf); | |
| } else { | |
| // Simple overwrite for now (last write wins on server usually) | |
| await app.dbPut('folders', cf); | |
| } | |
| } | |
| // -------------------- | |
| // --- Clean up local projects that were deleted from cloud --- | |
| // Only do this for projects owned by this user | |
| const userId = localStorage.getItem('color_rm_user_id') || (app.liveSync && app.liveSync.userId); | |
| for (const [id, local] of localMap) { | |
| // If project was synced to cloud but now missing, delete locally | |
| if (local.isCloudBackedUp && !cloudIds.has(id)) { | |
| // Verify ownership before deleting | |
| if (local.ownerId === userId) { | |
| console.log("Registry: Removing orphaned local project:", id); | |
| // Delete session | |
| await new Promise(r => { | |
| const tx = app.db.transaction('sessions', 'readwrite'); | |
| tx.objectStore('sessions').delete(id); | |
| tx.oncomplete = () => r(); | |
| tx.onerror = () => r(); | |
| }); | |
| // Delete pages | |
| try { | |
| const tx = app.db.transaction('pages', 'readwrite'); | |
| const store = tx.objectStore('pages'); | |
| const index = store.index('sessionId'); | |
| const pages = await new Promise(r => { | |
| const req = index.getAll(id); | |
| req.onsuccess = () => r(req.result || []); | |
| req.onerror = () => r([]); | |
| }); | |
| pages.forEach(pg => store.delete(pg.id)); | |
| } catch (e) {} | |
| } | |
| } | |
| } | |
| } catch (e) { | |
| console.warn("Registry sync error:", e); | |
| } | |
| }, | |
| async upsert(project) { | |
| const token = this.getToken(); | |
| if (!token) return; | |
| // Prepare lightweight metadata object | |
| const payload = { | |
| id: project.id, | |
| name: project.name, | |
| pageCount: project.pageCount, | |
| lastMod: project.lastMod, | |
| ownerId: project.ownerId, | |
| baseFileName: project.baseFileName, | |
| folderId: project.folderId || null | |
| }; | |
| fetch(this.apiUrl('/api/color_rm/registry'), { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ project: payload }) | |
| }) | |
| .then(res => { | |
| if (res.status === 401) { | |
| console.warn("Registry: Auth token expired on upsert."); | |
| return; | |
| } | |
| if (res.ok) { | |
| // Mark as backed up locally | |
| const app = this.getApp(); | |
| if (app && app.dbGet) { | |
| app.dbGet('sessions', project.id).then(s => { | |
| if (s) { | |
| s.isCloudBackedUp = true; | |
| app.dbPut('sessions', s).then(() => { | |
| // Refresh list if dashboard is open | |
| if(document.getElementById('dashboardModal') && document.getElementById('dashboardModal').style.display === 'flex') { | |
| app.loadSessionList(); | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| } | |
| }) | |
| .catch(e => console.warn("Registry upsert failed:", e)); | |
| }, | |
| async saveFolder(folder) { | |
| const token = this.getToken(); | |
| if (!token) return; | |
| // Payload for folder update | |
| const payload = { | |
| id: folder.id, | |
| name: folder.name, | |
| parentId: folder.parentId || null, | |
| ownerId: folder.ownerId | |
| }; | |
| fetch(this.apiUrl('/api/color_rm/registry/folder'), { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ folder: payload }) | |
| }).catch(e => console.warn("Folder save failed:", e)); | |
| }, | |
| async deleteFolder(folderId) { | |
| const token = this.getToken(); | |
| if (!token) return; | |
| // Mark as deleted locally FIRST to prevent re-sync | |
| this.markAsDeleted(folderId); | |
| try { | |
| const res = await fetch(this.apiUrl(`/api/color_rm/registry/folder/${folderId}`), { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${token}` } | |
| }); | |
| if (res.ok) { | |
| console.log("Registry: Folder deleted from cloud:", folderId); | |
| } | |
| } catch (e) { | |
| console.warn("Folder delete failed:", e); | |
| } | |
| }, | |
| async delete(projectId) { | |
| const token = this.getToken(); | |
| if (!token) return; | |
| // Mark as deleted locally FIRST to prevent re-sync | |
| this.markAsDeleted(projectId); | |
| try { | |
| // 1. Delete from registry (KV) | |
| const res = await fetch(this.apiUrl(`/api/color_rm/registry/${projectId}`), { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${token}` } | |
| }); | |
| if (res.status === 401) { | |
| console.warn("Registry: Auth token expired on delete."); | |
| } else if (res.ok) { | |
| console.log("Registry: Project deleted from cloud registry:", projectId); | |
| } | |
| // 2. Delete base file from R2 bucket | |
| const baseRes = await fetch(this.apiUrl(`/api/color_rm/base_file/${projectId}`), { | |
| method: 'DELETE', | |
| headers: { 'Authorization': `Bearer ${token}` } | |
| }); | |
| if (baseRes.ok) { | |
| console.log("Registry: Base file deleted from R2:", projectId); | |
| } | |
| } catch (e) { | |
| console.warn("Registry delete failed:", e); | |
| } | |
| } | |
| }; | |