deepbase
Version:
⚡ DeepBase - Fastest and simplest way to add persistence to your projects.
441 lines (360 loc) • 12.5 kB
JavaScript
import { DeepBaseDriver } from './DeepBaseDriver.js';
/**
* Helper to wrap a promise with a timeout
* @param {Promise} promise - The promise to wrap
* @param {number} ms - Timeout in milliseconds
* @param {string} operation - Name of the operation (for error messages)
* @returns {Promise} Promise that rejects on timeout
*/
function withTimeout(promise, ms, operation = 'operation') {
if (!ms || ms <= 0) {
return promise;
}
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${operation} timed out after ${ms}ms`)), ms)
)
]);
}
/**
* Dynamically import JsonDriver only when needed
* @param {object} options - Options for JsonDriver
* @returns {Promise<DeepBaseDriver>} JsonDriver instance
*/
async function createJsonDriver(options = {}) {
try {
const { JsonDriver } = await import('deepbase-json');
return new JsonDriver(options);
} catch (error) {
throw new Error(
'JsonDriver not available. ' +
'Please provide a driver or install deepbase-json: npm install deepbase-json'
);
}
}
export class DeepBase {
constructor(drivers = [], {writeAll, readFirst, failOnPrimaryError, lazyConnect, timeout, readTimeout, writeTimeout, ...opts} = {}) {
// Support backward compatibility: new DeepBase({ name: "db" })
// If first argument is a plain object (not a driver), treat it as JsonDriver options
if (!Array.isArray(drivers) && !(drivers instanceof DeepBaseDriver) &&
typeof drivers === 'object' && drivers !== null) {
// First argument is options object for JsonDriver
// Store options and create driver lazily
this._jsonDriverOptions = drivers;
drivers = [];
} else {
// Normalize to array
drivers = Array.isArray(drivers) ? drivers : [drivers];
}
// Store original drivers array
this._initialDrivers = drivers;
this._driversInitialized = false;
this.drivers = drivers;
this.opts = {
writeAll: writeAll !== false, // Write to all drivers by default
readFirst: readFirst !== false, // Read from first available
failOnPrimaryError: failOnPrimaryError !== false,
lazyConnect: lazyConnect !== false, // Auto-connect on first operation by default
timeout: timeout || 0, // Global timeout in ms (0 = disabled)
readTimeout: readTimeout || timeout || 0, // Read operations timeout in ms
writeTimeout: writeTimeout || timeout || 0, // Write operations timeout in ms
connectTimeout: opts.connectTimeout || timeout || 0, // Connection timeout in ms
...opts
};
}
async _initializeDrivers() {
if (this._driversInitialized) {
return;
}
// If we need to create a JsonDriver from options
if (this._jsonDriverOptions) {
const jsonDriver = await createJsonDriver(this._jsonDriverOptions);
this.drivers = [jsonDriver];
} else if (this.drivers.length === 0) {
// No drivers provided, try to create default JsonDriver
const jsonDriver = await createJsonDriver();
this.drivers = [jsonDriver];
}
// Validate drivers
for (const driver of this.drivers) {
if (!(driver instanceof DeepBaseDriver)) {
throw new Error('All drivers must extend DeepBaseDriver');
}
}
this._driversInitialized = true;
}
async connect() {
await this._initializeDrivers();
const operation = async () => {
const results = await Promise.allSettled(
this.drivers.map(driver => driver.connect())
);
const errors = results
.filter(r => r.status === 'rejected')
.map((r, i) => ({ driver: this.drivers[i], error: r.reason }));
if (errors.length > 0 && this.opts.failOnPrimaryError &&
results[0].status === 'rejected') {
throw errors[0].error;
}
return {
connected: results.filter(r => r.status === 'fulfilled').length,
total: this.drivers.length
};
};
// Use timeout for connection (default to general timeout)
const connectTimeout = this.opts.connectTimeout || this.opts.timeout;
return withTimeout(operation(), connectTimeout, 'connect()');
}
async _ensureConnected() {
// Initialize drivers first if needed
if (!this._driversInitialized) {
await this._initializeDrivers();
}
// Only auto-connect if lazyConnect is enabled
if (!this.opts.lazyConnect) {
return;
}
// Check if all drivers are connected
const needsConnection = this.drivers.some(driver => !driver._connected);
if (needsConnection) {
await this.connect();
}
}
async disconnect() {
await Promise.allSettled(
this.drivers.map(driver => driver.disconnect())
);
}
async _readFromDrivers(method, args) {
if (this.opts.readFirst) {
// Try drivers in order until one succeeds
for (const driver of this.drivers) {
try {
return await driver[method](...args);
} catch (error) {
// If this is the last driver, throw the error
if (driver === this.drivers[this.drivers.length - 1]) {
throw error;
}
// Otherwise, continue to next driver
}
}
} else {
// Race: return first successful response
return Promise.any(
this.drivers.map(driver => driver[method](...args))
);
}
}
async _writeToDrivers(method, args) {
if (this.opts.writeAll) {
// Write to all drivers
const results = await Promise.allSettled(
this.drivers.map(driver => driver[method](...args))
);
// Check if primary driver succeeded
if (this.opts.failOnPrimaryError && results[0].status === 'rejected') {
throw results[0].reason;
}
// Return result from primary driver
return results[0].status === 'fulfilled' ? results[0].value : null;
} else {
// Write only to primary driver
return this.drivers[0][method](...args);
}
}
async _runReadOperation(operationName, driverMethod, args) {
await this._ensureConnected();
return withTimeout(
this._readFromDrivers(driverMethod, args),
this.opts.readTimeout,
operationName
);
}
async _runWriteOperation(operationName, driverMethod, args) {
await this._ensureConnected();
return withTimeout(
this._writeToDrivers(driverMethod, args),
this.opts.writeTimeout,
operationName
);
}
async get(...args) {
return this._runReadOperation('get()', 'get', args);
}
getSync(...args) {
if (!this.drivers || this.drivers.length === 0) {
throw new Error('No drivers. Call connect() first or add drivers.');
}
return this.drivers[0].getSync(...args);
}
async set(...args) {
return this._runWriteOperation('set()', 'set', args);
}
async del(...args) {
return this._runWriteOperation('del()', 'del', args);
}
async inc(...args) {
return this._runWriteOperation('inc()', 'inc', args);
}
async dec(...args) {
return this._runWriteOperation('dec()', 'dec', args);
}
async add(...args) {
await this._ensureConnected();
const operation = async () => {
const value = args[args.length - 1];
const keys = args.slice(0, -1);
// Generate ID once so all drivers share the same key
const id = this.drivers[0].nanoid();
// Use set() which already handles writeAll
await this.set(...keys, id, value);
return [...keys, id];
};
return withTimeout(operation(), this.opts.writeTimeout, 'add()');
}
async upd(...args) {
return this._runWriteOperation('upd()', 'upd', args);
}
async first(...args) {
return this._runReadOperation('first()', 'first', args);
}
async last(...args) {
return this._runReadOperation('last()', 'last', args);
}
async pop(...args) {
await this._ensureConnected();
const operation = async () => {
const lastKey = await this.last(...args);
if (lastKey === undefined) {
return undefined;
}
const value = await this.get(...args, lastKey);
await this.del(...args, lastKey);
return value;
};
return withTimeout(operation(), this.opts.writeTimeout, 'pop()');
}
async shift(...args) {
await this._ensureConnected();
const operation = async () => {
const firstKey = await this.first(...args);
if (firstKey === undefined) {
return undefined;
}
const value = await this.get(...args, firstKey);
await this.del(...args, firstKey);
return value;
};
return withTimeout(operation(), this.opts.writeTimeout, 'shift()');
}
async keys(...args) {
const r = await this.get(...args);
return (r !== null && typeof r === "object") ? Object.keys(r) : [];
}
async values(...args) {
const r = await this.get(...args);
return (r !== null && typeof r === "object") ? Object.values(r) : [];
}
async entries(...args) {
const r = await this.get(...args);
return (r !== null && typeof r === "object") ? Object.entries(r) : [];
}
async len(...args) {
const k = await this.keys(...args);
return k.length;
}
/**
* Migrate/Sync data from one driver to another
* @param {number} fromIndex - Source driver index (default: 0)
* @param {number} toIndex - Target driver index (default: 1)
* @param {object} opts - Migration options
* @returns {object} Migration statistics
*/
async migrate(fromIndex = 0, toIndex = 1, opts = {}) {
await this._ensureConnected();
if (!this.drivers[fromIndex]) {
throw new Error(`Source driver at index ${fromIndex} not found`);
}
if (!this.drivers[toIndex]) {
throw new Error(`Target driver at index ${toIndex} not found`);
}
const sourceDriver = this.drivers[fromIndex];
const targetDriver = this.drivers[toIndex];
const options = {
clear: opts.clear !== false, // Clear target before migration
batchSize: opts.batchSize || 100,
onProgress: opts.onProgress || (() => {}),
...opts
};
// Clear target if requested
if (options.clear) {
await targetDriver.del();
}
// Get all data from source
const sourceData = await sourceDriver.get();
if (!sourceData || typeof sourceData !== 'object') {
return { migrated: 0, errors: 0 };
}
// Migrate data recursively
let migrated = 0;
let errors = 0;
async function migrateObject(obj, path = []) {
const entries = Object.entries(obj);
for (const [key, value] of entries) {
const currentPath = [...path, key];
try {
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// If it's an object, recurse
await migrateObject(value, currentPath);
} else {
// Write leaf value
await targetDriver.set(...currentPath, value);
migrated++;
// Progress callback
if (migrated % options.batchSize === 0) {
options.onProgress({ migrated, errors, current: currentPath.join('.') });
}
}
} catch (error) {
errors++;
console.error(`❌ Error migrating ${currentPath.join('.')}: ${error.message}`);
}
}
}
await migrateObject(sourceData);
return { migrated, errors };
}
/**
* Sync all drivers - copies data from primary to all others
* @param {object} opts - Sync options
* @returns {object} Sync statistics per driver
*/
async syncAll(opts = {}) {
if (this.drivers.length < 2) {
throw new Error('Need at least 2 drivers to sync');
}
const results = [];
for (let i = 1; i < this.drivers.length; i++) {
const result = await this.migrate(0, i, opts);
results.push({ driverIndex: i, ...result });
}
return results;
}
/**
* Get driver by index
* @param {number} index - Driver index
* @returns {DeepBaseDriver} Driver instance
*/
getDriver(index = 0) {
return this.drivers[index];
}
/**
* Get all drivers
* @returns {DeepBaseDriver[]} Array of driver instances
*/
getDrivers() {
return this.drivers;
}
}