or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

database-management.mderror-handling.mdevents.mdindex.mdlive-queries.mdquery-building.mdschema-management.mdtable-operations.mdutility-functions.md
tile.json

events.mddocs/

Events and Hooks

Overview

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: Lifecycle events for database operations (opening, closing, version changes)
  • Table Hooks: CRUD operation hooks for individual tables (creating, reading, updating, deleting)
  • Transaction Events: Transaction lifecycle events (complete, abort, error)
  • Global Events: Cross-database events like storage mutations for live queries

Database Events

Database events are fired during the database lifecycle and can be subscribed to using the db.on() method.

Event Types

interface DbEvents {
  ready: DexieOnReadyEvent;
  populate: DexiePopulateEvent;
  blocked: DexieEvent;
  versionchange: DexieVersionChangeEvent;
  close: DexieCloseEvent;
}

ready

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);

populate

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});
});

blocked

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);
});

versionchange

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');
  }
});

close

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

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.

Hook Types

interface TableHooks<T=any, TKey=IndexableType, TInsertType=T> {
  creating: DexieEvent;
  reading: DexieEvent;
  updating: DexieEvent;
  deleting: DexieEvent;
}

creating

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;
});

reading

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;
});

updating

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
  };
});

deleting

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

Transaction events are fired during the transaction lifecycle and can be subscribed to using the transaction.on() method.

Event Types

interface TransactionEvents {
  complete: DexieEvent;
  abort: DexieEvent;
  error: DexieEvent;
}

Usage

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

Global events are fired across all Dexie instances and can be subscribed to using Dexie.on().

storagemutated

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);
});

Event Management

Subscribing and Unsubscribing

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);

Event Chaining

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;
});

Preventing Operations

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;
});

Best Practices

1. Use Hooks for Data Validation

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');
  }
});

2. Implement Audit Trails

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'));

3. Handle Async Operations in Hooks

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 })
    });
  };
});

4. Clean Up Event Listeners

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);
    });
  }
}

5. Error Handling in Event Handlers

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 and Setup

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.