UNPKG

delta-sync

Version:

A lightweight framework for bi-directional database synchronization with automatic version tracking and conflict resolution.

459 lines (458 loc) 19.5 kB
// adapters/indexeddb.ts // 基于indexeddb的适配器 export class IndexedDBAdapter { constructor(options = {}) { // 添加表名常量 this.LOCAL_CHANGES_STORE = 'local_data_changes'; this.ATTACHMENT_CHANGES_STORE = 'local_attachment_changes'; this.META_STORE = 'local_meta'; // 新增meta表 this.FILES_STORE = '_files'; this.db = null; this.initPromise = null; this.stores = new Set(); this.dbName = options.dbName || 'deltaSyncDB'; } // 文件存储表 async isAvailable() { return !!window.indexedDB; } async initSync() { if (this.db) return; if (this.initPromise) return this.initPromise; const currentVersion = await this.getCurrentDatabaseVersion(); this.initPromise = new Promise((resolve, reject) => { try { // 使用当前版本打开 const request = indexedDB.open(this.dbName, currentVersion); request.onupgradeneeded = (event) => { const db = request.result; this.createRequiredStores(db, event.oldVersion); }; request.onerror = () => { reject(new Error(`Failed to open IndexedDB database: ${request.error?.message || 'Unknown error'}`)); }; request.onsuccess = () => { this.db = request.result; // 检查是否需要升级以创建新存储 if (!this.db.objectStoreNames.contains(this.FILES_STORE) || !this.db.objectStoreNames.contains(this.LOCAL_CHANGES_STORE) || !this.db.objectStoreNames.contains(this.ATTACHMENT_CHANGES_STORE)) { // 需要升级,关闭连接并重新打开 const newVersion = currentVersion + 1; this.db.close(); const upgradeRequest = indexedDB.open(this.dbName, newVersion); upgradeRequest.onupgradeneeded = (event) => { const db = upgradeRequest.result; this.createRequiredStores(db, event.oldVersion); }; upgradeRequest.onerror = () => { reject(new Error(`Failed to upgrade IndexedDB database: ${upgradeRequest.error?.message || 'Unknown error'}`)); }; upgradeRequest.onsuccess = () => { this.db = upgradeRequest.result; this.setupDbConnection(); resolve(); }; } else { this.setupDbConnection(); resolve(); } }; } catch (error) { reject(new Error(`Failed to initialize IndexedDB: ${error instanceof Error ? error.message : String(error)}`)); } }); return this.initPromise; } // 获取当前数据库版本 async getCurrentDatabaseVersion() { return new Promise((resolve) => { const request = indexedDB.open(this.dbName); request.onsuccess = () => { const version = request.result.version; request.result.close(); resolve(version); }; request.onerror = () => { // 如果数据库不存在,返回版本1 resolve(1); }; }); } // 设置数据库连接 setupDbConnection() { this.db.onversionchange = () => { console.warn('Database version changed. Closing connection.'); this.db?.close(); this.db = null; this.initPromise = null; }; this.stores.clear(); for (let i = 0; i < this.db.objectStoreNames.length; i++) { this.stores.add(this.db.objectStoreNames[i]); } } // 创建必要的存储 createRequiredStores(db, oldVersion) { // 创建meta存储,用于存储各种元数据 if (!db.objectStoreNames.contains(this.META_STORE)) { const metaStore = db.createObjectStore(this.META_STORE, { keyPath: '_delta_id' // 使用_delta_id作为主键 }); // 为meta表创建必要的索引 metaStore.createIndex('_version', '_version', { unique: false }); metaStore.createIndex('type', 'type', { unique: false }); metaStore.createIndex('updatedAt', 'updatedAt', { unique: false }); } // 创建文件存储 if (!db.objectStoreNames.contains(this.FILES_STORE)) { const fileStore = db.createObjectStore(this.FILES_STORE, { keyPath: '_delta_id' }); fileStore.createIndex('createdAt', 'createdAt', { unique: false }); fileStore.createIndex('updatedAt', 'updatedAt', { unique: false }); } // 创建数据变更记录存储 if (!db.objectStoreNames.contains(this.LOCAL_CHANGES_STORE)) { const dataChangesStore = db.createObjectStore(this.LOCAL_CHANGES_STORE, { keyPath: '_delta_id' }); dataChangesStore.createIndex('_version', '_version', { unique: false }); } // 创建附件变更记录存储 if (!db.objectStoreNames.contains(this.ATTACHMENT_CHANGES_STORE)) { const attachmentChangesStore = db.createObjectStore(this.ATTACHMENT_CHANGES_STORE, { keyPath: '_delta_id' }); attachmentChangesStore.createIndex('_version', '_version', { unique: false }); } // 创建笔记存储 if (!db.objectStoreNames.contains('notes')) { const noteStore = db.createObjectStore('notes', { keyPath: '_delta_id' }); noteStore.createIndex('_version', '_version', { unique: false }); } } async ensureStore(storeName) { if (!this.db) { await this.initSync(); } if (this.stores.has(storeName)) { return; } } // 读取指定版本之后的所有数据 async readByVersion(storeName, options) { await this.ensureStore(storeName); const limit = options.limit || Number.MAX_SAFE_INTEGER; const offset = options.offset || 0; const since = options.since || 0; const order = options.order || 'asc'; return new Promise((resolve, reject) => { try { const transaction = this.db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const versionIndex = store.index('_version'); const items = []; let skipped = 0; let processed = 0; let hasMore = false; let request; // 根据排序顺序和since参数决定游标打开方式 if (order === 'desc') { const range = since > 0 ? IDBKeyRange.upperBound(since, true) : null; // true表示不包含边界 request = versionIndex.openCursor(range, 'prev'); } else { const range = since > 0 ? IDBKeyRange.lowerBound(since, true) : null; // true表示不包含边界 request = versionIndex.openCursor(range); } request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { if (skipped < offset) { skipped++; cursor.continue(); } else if (processed < limit) { items.push(cursor.value); processed++; cursor.continue(); } else { hasMore = true; resolve({ items, hasMore }); } } else { resolve({ items, hasMore }); } }; request.onerror = (event) => { reject(new Error(`查询${storeName}时出错: ${event.target.error}`)); }; transaction.onerror = (event) => { reject(new Error(`事务执行出错: ${transaction.error}`)); }; } catch (error) { reject(new Error(`执行查询时发生错误: ${error instanceof Error ? error.message : String(error)}`)); } }); } async readBulk(storeName, ids) { await this.ensureStore(storeName); const transaction = this.db.transaction(storeName, 'readonly'); const store = transaction.objectStore(storeName); const promises = ids.map(id => new Promise((resolve) => { const request = store.get(id); request.onsuccess = () => resolve(request.result); request.onerror = () => { console.error(`Failed to read ${id} from ${storeName}`); resolve(undefined); }; })); const results = await Promise.all(promises); return results.filter(result => result !== undefined); } async putBulk(storeName, items) { if (!items.length) return []; await this.ensureStore(storeName); return new Promise((resolve, reject) => { const transaction = this.db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const itemsWithMeta = items.map(item => { return { ...item, }; }); // 批量保存所有项目 itemsWithMeta.forEach(item => { store.put(item); }); transaction.oncomplete = () => resolve(itemsWithMeta); transaction.onerror = () => reject(new Error(`Failed to put items in ${storeName}`)); }); } async deleteBulk(storeName, ids) { if (!ids.length) return; await this.ensureStore(storeName); return new Promise((resolve, reject) => { const transaction = this.db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); ids.forEach(id => { store.delete(id); }); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(new Error(`Failed to delete items from ${storeName}`)); }); } // 如果数据库未初始化或者store不存在,视为清空成功 async clearStore(storeName) { if (!this.db || !this.db.objectStoreNames.contains(storeName)) { return true; } try { await new Promise((resolve, reject) => { const transaction = this.db.transaction(storeName, 'readwrite'); const store = transaction.objectStore(storeName); const request = store.clear(); request.onsuccess = () => resolve(); request.onerror = (event) => { console.error(`清空store ${storeName}失败:`, event); reject(new Error(`Failed to clear store ${storeName}`)); }; // 增加事务完成的错误处理 transaction.oncomplete = () => resolve(); transaction.onerror = (event) => { console.error(`清空store ${storeName}的事务失败:`, event); reject(new Error(`Transaction failed when clearing store ${storeName}`)); }; }); return true; // 操作成功 } catch (error) { console.error(`清空store ${storeName}时出错:`, error); return false; // 操作失败 } } async readFiles(fileIds) { if (!fileIds.length) return new Map(); await this.ensureFileStore(); const result = new Map(); try { const transaction = this.db.transaction(this.FILES_STORE, 'readonly'); const store = transaction.objectStore(this.FILES_STORE); const promises = fileIds.map(async (fileId) => { return new Promise((resolve) => { const request = store.get(fileId); request.onerror = () => { console.warn(`Failed to read file: ${fileId} - ${request.error?.message || 'Unknown error'}`); result.set(fileId, null); resolve(); }; request.onsuccess = () => { const fileObject = request.result; if (!fileObject || !fileObject.content) { result.set(fileId, null); } else { result.set(fileId, fileObject.content); } resolve(); }; }); }); await Promise.all(promises); return result; } catch (error) { console.error('Batch file read error:', error); for (const fileId of fileIds) { if (!result.has(fileId)) { result.set(fileId, null); } } return result; } } async saveFiles(files) { if (!files.length) return []; await this.ensureFileStore(); const attachments = []; try { const transaction = this.db.transaction(this.FILES_STORE, 'readwrite'); const store = transaction.objectStore(this.FILES_STORE); const now = Date.now(); const promises = files.map(async (file) => { return new Promise((resolve) => { try { const fileContent = file.content; const fileId = file.fileId; // 处理文件名和扩展名 let fileName = fileId; let extension = ''; const lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex !== -1) { extension = fileName.substring(lastDotIndex); fileName = fileName.substring(0, lastDotIndex); } const attachment = { id: fileId, filename: fileName + extension, mimeType: fileContent instanceof Blob ? fileContent.type : 'application/octet-stream', size: fileContent instanceof Blob ? fileContent.size : (fileContent instanceof ArrayBuffer ? fileContent.byteLength : String(fileContent).length), createdAt: now, updatedAt: now, metadata: {} }; const fileObject = { _delta_id: fileId, content: fileContent, createdAt: now, updatedAt: now }; const request = store.put(fileObject); request.onerror = () => { console.error(`Failed to save file: ${fileId} - ${request.error?.message || 'Unknown error'}`); resolve(); }; request.onsuccess = () => { attachments.push(attachment); resolve(); }; } catch (error) { console.error('Error saving individual file:', error); resolve(); } }); }); await Promise.all(promises); return attachments; } catch (error) { console.error('Batch file save error:', error); return attachments; } } async deleteFiles(fileIds) { await this.ensureFileStore(); const result = { deleted: [], failed: [] }; try { const transaction = this.db.transaction(this.FILES_STORE, 'readwrite'); const store = transaction.objectStore(this.FILES_STORE); await Promise.all(fileIds.map(async (fileId) => { try { await new Promise((resolve, reject) => { const getRequest = store.get(fileId); getRequest.onsuccess = () => { if (getRequest.result) { const deleteRequest = store.delete(fileId); deleteRequest.onsuccess = () => { result.deleted.push(fileId); resolve(); }; deleteRequest.onerror = () => { result.failed.push(fileId); resolve(); }; } else { result.failed.push(fileId); resolve(); } }; getRequest.onerror = () => { result.failed.push(fileId); resolve(); }; }); } catch (error) { console.error(`Error deleting file ${fileId}:`, error); result.failed.push(fileId); } })); return result; } catch (error) { console.error('Batch file delete error:', error); return result; } } async ensureFileStore() { await this.ensureStore(this.FILES_STORE); } async close() { if (this.db) { this.db.close(); this.db = null; this.initPromise = null; } } async deleteDatabase() { await this.close(); return new Promise((resolve, reject) => { const request = indexedDB.deleteDatabase(this.dbName); request.onsuccess = () => { this.stores.clear(); resolve(); }; request.onerror = () => reject(new Error(`Failed to delete database ${this.dbName}`)); }); } }