Dexie.js provides a comprehensive event system that allows developers to hook into database operations at multiple levels - database, table, and transaction levels. The event system enables intercepting, modifying, and reacting to database operations, making it possible to implement features like validation, logging, auditing, and data transformation.
The event system consists of three main categories:
Database events are fired during the database lifecycle and can be subscribed to using the db.on() method.
interface DbEvents {
ready: DexieOnReadyEvent;
populate: DexiePopulateEvent;
blocked: DexieEvent;
versionchange: DexieVersionChangeEvent;
close: DexieCloseEvent;
}Fired when the database is ready for use, after successful opening and any version upgrades.
interface DexieOnReadyEvent {
subscribe(fn: (vipDb: Dexie) => any, bSticky?: boolean): void;
unsubscribe(fn: (vipDb: Dexie) => any): void;
fire(vipDb: Dexie): any;
}Usage:
db.on('ready', function (db) {
console.log('Database is ready!');
});
// Subscribe with sticky behavior (survives close/reopen)
db.on('ready', function (db) {
console.log('Database ready (sticky)');
}, true);Fired during database upgrade when tables are being populated for the first time.
interface DexiePopulateEvent {
subscribe(fn: (trans: Transaction) => any): void;
unsubscribe(fn: (trans: Transaction) => any): void;
fire(trans: Transaction): any;
}Usage:
db.on('populate', function (trans) {
// Populate initial data
trans.friends.add({name: 'Alice', age: 25});
trans.friends.add({name: 'Bob', age: 30});
});Fired when database opening is blocked by another connection.
interface DexieEvent {
subscribers: Function[];
fire(...args: any[]): any;
subscribe(fn: (...args: any[]) => any): void;
unsubscribe(fn: (...args: any[]) => any): void;
}Usage:
db.on('blocked', function (event) {
console.warn('Database blocked:', event);
});Fired when another connection requests a version change or database deletion.
interface DexieVersionChangeEvent {
subscribe(fn: (event: IDBVersionChangeEvent) => any): void;
unsubscribe(fn: (event: IDBVersionChangeEvent) => any): void;
fire(event: IDBVersionChangeEvent): any;
}Usage:
db.on('versionchange', function (event) {
if (event.newVersion > 0) {
console.warn('Another tab wants to upgrade the database');
db.close(); // Allow upgrade
} else {
console.warn('Another tab wants to delete the database');
}
});Fired when the database connection is closed.
interface DexieCloseEvent {
subscribe(fn: (event: Event) => any): void;
unsubscribe(fn: (event: Event) => any): void;
fire(event: Event): any;
}Usage:
db.on('close', function (event) {
console.log('Database connection closed');
});Table hooks allow intercepting and modifying CRUD operations on individual tables. They are attached to specific tables and provide powerful capabilities for data transformation and validation.
interface TableHooks<T=any, TKey=IndexableType, TInsertType=T> {
creating: DexieEvent;
reading: DexieEvent;
updating: DexieEvent;
deleting: DexieEvent;
}Fired before an object is created (inserted) into the table.
interface CreatingHookContext<T, Key> {
onsuccess?: (primKey: Key) => void;
onerror?: (err: any) => void;
}
function creatingHook(
this: CreatingHookContext<T, TKey>,
primKey: TKey,
obj: T,
transaction: Transaction
): void | undefined | TKey;Usage:
// Subscribe to creating hook
db.friends.hook('creating', function (primKey, obj, trans) {
// Add timestamp
obj.created = new Date();
// Add audit trail
trans.auditLog.add({
table: 'friends',
operation: 'create',
objectId: primKey,
timestamp: new Date()
});
// Set success callback
this.onsuccess = function (primKey) {
console.log('Created object with key:', primKey);
};
// Set error callback
this.onerror = function (err) {
console.error('Failed to create object:', err);
};
// Return custom primary key (optional)
// return customPrimKey;
});Fired after an object is read from the table, allowing transformation of the returned data.
function readingHook(obj: T): T | any;Usage:
db.friends.hook('reading', function (obj) {
// Add computed properties
obj.displayName = `${obj.firstName} ${obj.lastName}`;
// Return transformed object
return obj;
});
// Chaining reading hooks
db.friends.hook('reading', function (obj) {
// First transformation
return {
...obj,
fullName: `${obj.firstName} ${obj.lastName}`
};
});
db.friends.hook('reading', function (obj) {
// Second transformation (receives result from first)
obj.initials = obj.fullName.split(' ').map(n => n[0]).join('');
return obj;
});Fired before an object is updated in the table.
interface UpdatingHookContext<T, Key> {
onsuccess?: (updatedObj: T) => void;
onerror?: (err: any) => void;
}
function updatingHook(
this: UpdatingHookContext<T, TKey>,
modifications: Object,
primKey: TKey,
obj: T,
transaction: Transaction
): any;Usage:
db.friends.hook('updating', function (modifications, primKey, obj, trans) {
// Add last modified timestamp
modifications.lastModified = new Date();
// Add audit trail
trans.auditLog.add({
table: 'friends',
operation: 'update',
objectId: primKey,
changes: modifications,
timestamp: new Date()
});
// Set success callback
this.onsuccess = function (updatedObj) {
console.log('Updated object:', updatedObj);
};
// Return additional modifications
return {
version: (obj.version || 0) + 1
};
});Fired before an object is deleted from the table.
interface DeletingHookContext<T, Key> {
onsuccess?: () => void;
onerror?: (err: any) => void;
}
function deletingHook(
this: DeletingHookContext<T, TKey>,
primKey: TKey,
obj: T,
transaction: Transaction
): any;Usage:
db.friends.hook('deleting', function (primKey, obj, trans) {
// Add audit trail
trans.auditLog.add({
table: 'friends',
operation: 'delete',
objectId: primKey,
deletedObject: obj,
timestamp: new Date()
});
// Soft delete instead of hard delete
trans.friends.put({
...obj,
deleted: true,
deletedAt: new Date()
});
// Prevent the actual delete by throwing an error
throw new Error('Soft delete implemented');
});Transaction events are fired during the transaction lifecycle and can be subscribed to using the transaction.on() method.
interface TransactionEvents {
complete: DexieEvent;
abort: DexieEvent;
error: DexieEvent;
}db.transaction('rw', [db.friends, db.auditLog], function (trans) {
// Subscribe to transaction events
trans.on('complete', function () {
console.log('Transaction completed successfully');
});
trans.on('error', function (error) {
console.error('Transaction failed:', error);
});
trans.on('abort', function () {
console.log('Transaction was aborted');
});
// Perform operations
return trans.friends.add({name: 'Alice'});
});Global events are fired across all Dexie instances and can be subscribed to using Dexie.on().
Fired when any Dexie database is mutated, used by the live query system.
interface ObservabilitySet {
[part: string]: IntervalTree;
}
interface DexieOnStorageMutatedEvent {
subscribe(fn: (parts: ObservabilitySet) => any): void;
unsubscribe(fn: (parts: ObservabilitySet) => any): void;
fire(parts: ObservabilitySet): any;
}Usage:
Dexie.on('storagemutated', function (parts) {
console.log('Database mutation detected:', parts);
});All events support subscription and unsubscription:
// Subscribe
function myHandler(arg1, arg2) {
console.log('Event fired:', arg1, arg2);
}
db.on('ready', myHandler);
db.friends.hook('creating', myHandler);
// Unsubscribe
db.on('ready').unsubscribe(myHandler);
db.friends.hook('creating').unsubscribe(myHandler);Multiple handlers can be attached to the same event. They are executed in the order they were added:
db.friends.hook('reading', function (obj) {
obj.step1 = true;
return obj;
});
db.friends.hook('reading', function (obj) {
obj.step2 = true;
return obj;
});Some hooks can prevent operations by returning specific values or throwing errors:
// Prevent creation by throwing error
db.friends.hook('creating', function (primKey, obj, trans) {
if (!obj.email) {
throw new Error('Email is required');
}
});
// Prevent event chain continuation
db.on('versionchange', function (event) {
// Returning false prevents default behavior
return false;
});db.users.hook('creating', function (primKey, obj, trans) {
if (!obj.email || !obj.email.includes('@')) {
throw new Error('Valid email is required');
}
if (obj.age < 0 || obj.age > 150) {
throw new Error('Age must be between 0 and 150');
}
});const auditHook = function (operation) {
return function (primKey, obj, trans) {
trans.auditLog.add({
table: this.name,
operation,
objectId: primKey,
timestamp: new Date(),
data: operation === 'delete' ? obj : undefined
});
};
};
db.users.hook('creating', auditHook('create'));
db.users.hook('updating', auditHook('update'));
db.users.hook('deleting', auditHook('delete'));db.users.hook('creating', function (primKey, obj, trans) {
// For async operations, use the transaction object
this.onsuccess = function (primKey) {
// Perform async operations after successful creation
fetch('/api/notify-user-created', {
method: 'POST',
body: JSON.stringify({ userId: primKey })
});
};
});class UserManager {
constructor(db) {
this.db = db;
this.handlers = [];
this.setupHooks();
}
setupHooks() {
const creatingHandler = (primKey, obj) => this.onUserCreating(primKey, obj);
this.db.users.hook('creating', creatingHandler);
this.handlers.push(['creating', creatingHandler]);
}
destroy() {
// Clean up all handlers
this.handlers.forEach(([event, handler]) => {
this.db.users.hook(event).unsubscribe(handler);
});
}
}db.friends.hook('creating', function (primKey, obj, trans) {
try {
// Validate and transform data
obj.normalizedName = obj.name.toLowerCase().trim();
this.onsuccess = function (primKey) {
console.log('Successfully created friend:', primKey);
};
this.onerror = function (error) {
console.error('Failed to create friend:', error);
// Could trigger notifications, logging, etc.
};
} catch (error) {
// Synchronous error handling
console.error('Validation error:', error);
throw error;
}
});import { Dexie } from 'dexie';
// Create database
const db = new Dexie('MyDatabase');
// Define schema
db.version(1).stores({
friends: '++id, name, age',
auditLog: '++id, table, operation, timestamp'
});
// Setup hooks and events
db.on('ready', function () {
console.log('Database ready!');
});
db.friends.hook('creating', function (primKey, obj, trans) {
obj.created = new Date();
});
// Open database
await db.open();The Dexie event system provides powerful capabilities for building robust database applications with features like validation, auditing, data transformation, and reactive updates.