import type { InsertRow } from '@affine/native'; import { SqliteConnection } from '@affine/native'; import type { ByteKVBehavior } from '@toeverything/infra/storage'; import { logger } from '../logger'; /** * A base class for SQLite DB adapter that provides basic methods around updates & blobs */ export class SQLiteAdapter { db: SqliteConnection | null = null; constructor(public readonly path: string) {} async connectIfNeeded() { if (!this.db) { this.db = new SqliteConnection(this.path); await this.db.connect(); logger.info(`[SQLiteAdapter]`, 'connected:', this.path); } return this.db; } async destroy() { const { db } = this; this.db = null; // log after close will sometimes crash the app when quitting logger.info(`[SQLiteAdapter]`, 'destroyed:', this.path); await db?.close(); } async addBlob(key: string, data: Uint8Array) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.addBlob(key, data); } catch (error) { logger.error('addBlob', error); } } async getBlob(key: string) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return null; } const blob = await this.db.getBlob(key); return blob?.data ?? null; } catch (error) { logger.error('getBlob', error); return null; } } async deleteBlob(key: string) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.deleteBlob(key); } catch (error) { logger.error(`${this.path} delete blob failed`, error); } } async getBlobKeys() { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return []; } return await this.db.getBlobKeys(); } catch (error) { logger.error(`getBlobKeys failed`, error); return []; } } async getUpdates(docId?: string) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return []; } return await this.db.getUpdates(docId); } catch (error) { logger.error('getUpdates', error); return []; } } async getAllUpdates() { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return []; } return await this.db.getAllUpdates(); } catch (error) { logger.error('getAllUpdates', error); return []; } } // add a single update to SQLite async addUpdateToSQLite(updates: InsertRow[]) { // batch write instead write per key stroke? try { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } const start = performance.now(); await this.db.insertUpdates(updates); logger.debug( `[SQLiteAdapter] addUpdateToSQLite`, 'length:', updates.length, 'docids', updates.map(u => u.docId), performance.now() - start, 'ms' ); } catch (error) { logger.error('addUpdateToSQLite', this.path, error); } } async deleteUpdates(docId?: string) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.deleteUpdates(docId); } catch (error) { logger.error('deleteUpdates', error); } } async getUpdatesCount(docId?: string) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return 0; } return await this.db.getUpdatesCount(docId); } catch (error) { logger.error('getUpdatesCount', error); return 0; } } async replaceUpdates(docId: string | null | undefined, updates: InsertRow[]) { try { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.replaceUpdates(docId, updates); } catch (error) { logger.error('replaceUpdates', error); } } serverClock: ByteKVBehavior = { get: async key => { if (!this.db) { logger.warn(`${this.path} is not connected`); return null; } const blob = await this.db.getServerClock(key); return blob?.data ?? null; }, set: async (key, data) => { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.setServerClock(key, data); }, keys: async () => { if (!this.db) { logger.warn(`${this.path} is not connected`); return []; } return await this.db.getServerClockKeys(); }, del: async key => { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.delServerClock(key); }, clear: async () => { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.clearServerClock(); }, }; syncMetadata: ByteKVBehavior = { get: async key => { if (!this.db) { logger.warn(`${this.path} is not connected`); return null; } const blob = await this.db.getSyncMetadata(key); return blob?.data ?? null; }, set: async (key, data) => { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.setSyncMetadata(key, data); }, keys: async () => { if (!this.db) { logger.warn(`${this.path} is not connected`); return []; } return await this.db.getSyncMetadataKeys(); }, del: async key => { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.delSyncMetadata(key); }, clear: async () => { if (!this.db) { logger.warn(`${this.path} is not connected`); return; } await this.db.clearSyncMetadata(); }, }; }