UNPKG

@shaxpir/sharedb-storage-sqlite

Version:

Shared SQLite storage components for ShareDB adapters

333 lines (289 loc) 10.1 kB
/** * Base SQLite storage implementation for ShareDB * * This class implements the DurableStorage interface from ShareDB, * providing SQLite-based persistence for offline-first applications. */ var errors = require('./utils/errors'); var ERROR_CODES = errors.ERROR_CODES; var wrapError = errors.wrapError; var wrapCallback = errors.wrapCallback; var Formatted = require('./utils/formatted'); module.exports = SqliteStorage; function SqliteStorage(options) { if (!options.adapter) { throw errors.createShareDBError('SqliteStorage requires an adapter', ERROR_CODES.NO_ADAPTER); } this.adapter = options.adapter; this.schemaStrategy = options.schemaStrategy || this.createDefaultStrategy(); this.debug = options.debug || false; this.ready = false; this.initializationPromise = null; } /** * Create a default schema strategy if none provided * Subclasses can override this to provide platform-specific defaults */ SqliteStorage.prototype.createDefaultStrategy = function() { throw errors.createShareDBError('No schema strategy provided and no default available', ERROR_CODES.INVALID_SCHEMA_STRATEGY); }; /** * Initialize the storage system */ SqliteStorage.prototype.initialize = function(callback) { var self = this; if (this.ready) { // If already initialized, read and return current inventory this.schemaStrategy.readInventory(this.adapter, wrapCallback(callback, ERROR_CODES.DB_QUERY_FAILED)); return; } if (this.initializationPromise) { this.initializationPromise .then(function(inventory) { callback(null, inventory); }) .catch(function(error) { callback(error); }); return; } this.initializationPromise = this.initializeAsync(); this.initializationPromise .then(function(inventory) { self.ready = true; callback(null, inventory); }) .catch(function(error) { console.error('[SqliteStorage] Initialization error:', error); callback(wrapError(error, ERROR_CODES.NOT_INITIALIZED)); }); }; SqliteStorage.prototype.initializeAsync = function() { var self = this; var startTime = Date.now(); return this.adapter.connect() .then(function() { self.debug && console.log('[SqliteStorage] Database connected'); return new Promise(function(resolve, reject) { self.schemaStrategy.initializeSchema(self.adapter, function(error) { if (error) reject(error); else resolve(); }); }); }) .then(function() { self.debug && console.log('[SqliteStorage] Schema initialized'); return new Promise(function(resolve, reject) { self.schemaStrategy.readInventory(self.adapter, function(error, inventory) { if (error) reject(error); else resolve(inventory); }); }); }) .then(function(inventory) { self.debug && console.log('[SqliteStorage] Inventory loaded'); var elapsed = Date.now() - startTime; self.debug && console.log('[SqliteStorage] Initialized in ' + elapsed + 'ms'); return inventory; }); }; /** * Read a single record from storage */ SqliteStorage.prototype.readRecord = function(storeName, id, callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } // Map storeName to type for schema strategy var type = storeName === 'meta' ? 'meta' : 'docs'; var collection; var docId; if (storeName === 'meta') { collection = null; docId = id; } else if (storeName === 'docs' && id.indexOf('/') !== -1) { // When storeName is 'docs' and id contains a slash, it's a compound key // Split it into collection and simple ID try { var parts = Formatted.split(id); collection = parts.collection; docId = parts.docId; } catch (error) { // Invalid compound key format callback(wrapError(error, ERROR_CODES.UNKNOWN)); return; } } else { // Invalid usage - DurableStore should only call with 'meta' or 'docs' with compound keys var errorMsg = 'Invalid readRecord call: storeName=' + storeName + ', id=' + id + '. Expected either storeName="meta" or storeName="docs" with compound key (collection/id)'; callback(errors.createShareDBError(errorMsg, ERROR_CODES.UNKNOWN)); return; } this.schemaStrategy.readRecord(this.adapter, type, collection, docId, function(error, record) { if (error) { callback(wrapError(error, ERROR_CODES.RECORD_NOT_FOUND)); } else { // Return with standard error-first callback pattern callback(null, record ? record.payload : null); } }); }; /** * Read all records from a store */ SqliteStorage.prototype.readAllRecords = function(storeName, callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } var type = storeName === 'meta' ? 'meta' : 'docs'; var collection = storeName === 'meta' ? null : storeName; this.schemaStrategy.readAllRecords(this.adapter, type, collection, wrapCallback(callback, ERROR_CODES.DB_QUERY_FAILED)); }; /** * Read multiple records by ID (bulk operation) */ SqliteStorage.prototype.readRecordsBulk = function(storeName, ids, callback) { var self = this; if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } if (!this.schemaStrategy.readRecordsBulk) { // Fall back to individual reads if bulk not supported var promises = ids.map(function(id) { return self.schemaStrategy.readRecord(self.adapter, 'docs', storeName, id); }); Promise.all(promises) .then(function(records) { var validRecords = records.filter(function(r) { return r !== null; }); callback(null, validRecords); }) .catch(function(error) { callback(error); }); return; } this.schemaStrategy.readRecordsBulk(this.adapter, 'docs', storeName, ids, wrapCallback(callback, ERROR_CODES.BULK_READ_FAILED)); }; /** * Write records to storage */ SqliteStorage.prototype.writeRecords = function(records, callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } this.schemaStrategy.writeRecords(this.adapter, records, wrapCallback(callback, ERROR_CODES.WRITE_FAILED)); }; /** * Delete a record from storage */ SqliteStorage.prototype.deleteRecord = function(storeName, id, callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } var type = storeName === 'meta' ? 'meta' : 'docs'; var collection = storeName === 'meta' ? null : storeName; this.schemaStrategy.deleteRecord(this.adapter, type, collection, id, wrapCallback(callback, ERROR_CODES.DELETE_FAILED)); }; /** * Clear all records from a specific store */ SqliteStorage.prototype.clearStore = function(storeName, callback) { var self = this; if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } if (this.schemaStrategy.clearStore) { this.schemaStrategy.clearStore(this.adapter, storeName, wrapCallback(callback, ERROR_CODES.CLEAR_FAILED)); } else { // Default implementation: read all and delete each this.readAllRecords(storeName, function(error, records) { if (error) { callback(error); return; } var deletePromises = (records || []).map(function(record) { return new Promise(function(resolve, reject) { self.deleteRecord(storeName, record.id, function(err) { if (err) reject(err); else resolve(); }); }); }); Promise.all(deletePromises) .then(function() { callback(null); }) .catch(function(error) { callback(error); }); }); } }; /** * Clear all data from storage */ SqliteStorage.prototype.clearAll = function(callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } if (this.schemaStrategy.clearAll) { this.schemaStrategy.clearAll(this.adapter, wrapCallback(callback, ERROR_CODES.CLEAR_FAILED)); } else { // Default: delete all tables this.schemaStrategy.deleteAllTables(this.adapter, wrapCallback(callback, ERROR_CODES.CLEAR_FAILED)); } }; /** * Close the storage connection */ SqliteStorage.prototype.close = function(callback) { var self = this; this.adapter.disconnect() .then(function() { self.ready = false; callback(null); }) .catch(function(error) { callback(error); }); }; /** * Check if storage is ready */ SqliteStorage.prototype.isReady = function() { return this.ready; }; /** * Ensure storage is ready (throw if not) */ SqliteStorage.prototype.ensureReady = function() { if (!this.ready) { throw new Error('Storage not initialized. Call initialize() first.'); } }; /** * Update inventory for a specific document */ SqliteStorage.prototype.updateInventory = function(collection, docId, version, operation, callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } this.schemaStrategy.updateInventoryItem(this.adapter, collection, docId, version, operation, wrapCallback(callback, ERROR_CODES.WRITE_FAILED)); }; /** * Read the full inventory */ SqliteStorage.prototype.readInventory = function(callback) { if (!this.ready) { callback(errors.createShareDBError('Storage not initialized', ERROR_CODES.NOT_INITIALIZED)); return; } this.schemaStrategy.readInventory(this.adapter, wrapCallback(callback, ERROR_CODES.DB_QUERY_FAILED)); }; /** * Delete the database (if supported by adapter) */ SqliteStorage.prototype.deleteDatabase = function(callback) { this.schemaStrategy.deleteAllTables(this.adapter, callback); };