export class MultiTabDatabase {
constructor(Database, dbName, options = {}) {
this.Database = Database;
this.dbName = dbName;
this.db = null;
this.refreshCallbacks = [];
this.autoSync = options.autoSync !== false;
this.waitForLeadership = options.waitForLeadership || false;
this.syncIntervalMs = options.syncIntervalMs || 0;
this.syncIntervalId = null;
}
async init() {
this.db = await this.Database.newDatabase(this.dbName);
this.db.onDataChange((changeType) => {
console.log(`[MultiTabDatabase] Data change received: ${changeType}`);
this._triggerRefreshCallbacks();
});
this.beforeUnloadHandler = async () => {
console.log('[MultiTabDatabase] Page unloading, cleaning up leader election');
try {
await this.db.close();
} catch (error) {
console.error('[MultiTabDatabase] Error during cleanup:', error);
}
};
window.addEventListener('beforeunload', this.beforeUnloadHandler);
if (this.syncIntervalMs > 0) {
this.syncIntervalId = setInterval(async () => {
try {
await this.db.sync();
} catch (error) {
console.error('[MultiTabDatabase] Auto-sync error:', error);
}
}, this.syncIntervalMs);
}
console.log(`[MultiTabDatabase] Initialized database: ${this.dbName}`);
}
async isLeader() {
return await this.db.isLeader();
}
async waitForLeadership(timeoutMs = 5000) {
return await this.db.waitForLeadership();
}
async requestLeadership() {
return await this.db.requestLeadership();
}
async getLeaderInfo() {
return await this.db.getLeaderInfo();
}
async write(sql, params = []) {
const isLeader = await this.isLeader();
if (!isLeader) {
if (this.waitForLeadership) {
console.log('[MultiTabDatabase] Not leader, waiting for leadership...');
await this.waitForLeadership();
} else {
throw new Error(
'Cannot write: This tab is not the leader. ' +
'Use db.waitForLeadership() or db.requestLeadership() to become leader, ' +
'or set waitForLeadership: true in options.'
);
}
}
let result;
if (params.length > 0) {
result = await this.db.executeWithParams(sql, params);
} else {
result = await this.db.execute(sql);
}
if (this.autoSync) {
await this.db.sync();
}
console.log('[MultiTabDatabase] Write completed and synced');
return result;
}
async query(sql, params = []) {
if (params.length > 0) {
return await this.db.executeWithParams(sql, params);
} else {
return await this.db.execute(sql);
}
}
async execute(sql, params = []) {
const isWriteOp = this._isWriteOperation(sql);
if (isWriteOp) {
return await this.write(sql, params);
} else {
return await this.query(sql, params);
}
}
async sync() {
return await this.db.sync();
}
onRefresh(callback) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
this.refreshCallbacks.push(callback);
}
offRefresh(callback) {
const index = this.refreshCallbacks.indexOf(callback);
if (index > -1) {
this.refreshCallbacks.splice(index, 1);
}
}
async close() {
if (this.syncIntervalId) {
clearInterval(this.syncIntervalId);
this.syncIntervalId = null;
}
if (this.db) {
await this.db.close();
this.db = null;
}
this.refreshCallbacks = [];
console.log(`[MultiTabDatabase] Closed database: ${this.dbName}`);
}
async enableOptimisticUpdates(enabled) {
return await this.db.enableOptimisticUpdates(enabled);
}
async isOptimisticMode() {
return await this.db.isOptimisticMode();
}
async trackOptimisticWrite(sql) {
return await this.db.trackOptimisticWrite(sql);
}
async getPendingWritesCount() {
return await this.db.getPendingWritesCount();
}
async clearOptimisticWrites() {
return await this.db.clearOptimisticWrites();
}
async enableCoordinationMetrics(enabled) {
return await this.db.enableCoordinationMetrics(enabled);
}
async isCoordinationMetricsEnabled() {
return await this.db.isCoordinationMetricsEnabled();
}
async recordLeadershipChange(becameLeader) {
return await this.db.recordLeadershipChange(becameLeader);
}
async recordNotificationLatency(latencyMs) {
return await this.db.recordNotificationLatency(latencyMs);
}
async recordWriteConflict() {
return await this.db.recordWriteConflict();
}
async recordFollowerRefresh() {
return await this.db.recordFollowerRefresh();
}
async getCoordinationMetrics() {
return await this.db.getCoordinationMetrics();
}
async resetCoordinationMetrics() {
return await this.db.resetCoordinationMetrics();
}
_isWriteOperation(sql) {
const upper = sql.trim().toUpperCase();
return (
upper.startsWith('INSERT') ||
upper.startsWith('UPDATE') ||
upper.startsWith('DELETE') ||
upper.startsWith('REPLACE')
);
}
_triggerRefreshCallbacks() {
for (const callback of this.refreshCallbacks) {
try {
callback();
} catch (err) {
console.error('[MultiTabDatabase] Error in refresh callback:', err);
}
}
}
}
export default MultiTabDatabase;