Event handling, error management, and testing utilities for comprehensive IndexedDB simulation.
Base request interface for all asynchronous IndexedDB operations.
interface IDBRequest extends EventTarget {
/** Operation result (available when readyState is "done") */
readonly result: any;
/** Error that occurred (null if successful) */
readonly error: DOMException | null;
/** Source object that initiated the request */
readonly source: IDBObjectStore | IDBIndex | IDBCursor | null;
/** Transaction this request belongs to */
readonly transaction: IDBTransaction | null;
/** Current state of the request */
readonly readyState: "pending" | "done";
/** Success event handler */
onsuccess: ((event: Event) => void) | null;
/** Error event handler */
onerror: ((event: Event) => void) | null;
}Usage Examples:
import "fake-indexeddb/auto";
// Basic request handling
const tx = db.transaction("products", "readonly");
const store = tx.objectStore("products");
const request = store.get("LAP001");
request.onsuccess = (event) => {
console.log("Product found:", event.target.result);
console.log("Request state:", request.readyState); // "done"
};
request.onerror = (event) => {
console.error("Request failed:", event.target.error);
};
// Using addEventListener for multiple handlers
request.addEventListener("success", (event) => {
console.log("First success handler");
});
request.addEventListener("success", (event) => {
console.log("Second success handler");
});Extended request for database opening operations with upgrade and blocking events.
interface IDBOpenDBRequest extends IDBRequest {
/** Database upgrade needed event handler */
onupgradeneeded: ((event: IDBVersionChangeEvent) => void) | null;
/** Database opening blocked event handler */
onblocked: ((event: Event) => void) | null;
}Usage Examples:
// Complete database opening with all events
const request = indexedDB.open("myapp", 3);
request.onupgradeneeded = (event) => {
const db = event.target.result;
console.log(`Upgrading from version ${event.oldVersion} to ${event.newVersion}`);
// Handle schema changes
if (event.oldVersion < 1) {
db.createObjectStore("users", { keyPath: "id" });
}
if (event.oldVersion < 2) {
const usersStore = event.target.transaction.objectStore("users");
usersStore.createIndex("email", "email", { unique: true });
}
if (event.oldVersion < 3) {
db.createObjectStore("settings", { keyPath: "key" });
}
};
request.onblocked = (event) => {
console.warn("Database upgrade blocked by other connections");
// Notify user to close other tabs
showUserMessage("Please close other tabs to continue");
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log("Database opened successfully");
// Set up global error handler
db.onerror = (event) => {
console.error("Database error:", event.target.error);
};
// Handle version changes from other connections
db.onversionchange = (event) => {
console.log("Database version changing, closing connection");
db.close();
showUserMessage("Database updated, please refresh the page");
};
};
request.onerror = (event) => {
console.error("Failed to open database:", event.target.error);
handleDatabaseError(event.target.error);
};Event fired during database version changes and upgrades.
interface IDBVersionChangeEvent extends Event {
/** Previous database version */
readonly oldVersion: number;
/** New database version (null for deletion) */
readonly newVersion: number | null;
}Usage Examples:
// Handle version change event details
request.onupgradeneeded = (event) => {
const db = event.target.result;
const { oldVersion, newVersion } = event;
console.log(`Version change: ${oldVersion} → ${newVersion}`);
// Progressive migration
if (oldVersion === 0) {
console.log("Creating initial database schema");
setupInitialSchema(db);
} else {
console.log("Upgrading existing database");
migrateSchema(db, oldVersion, newVersion);
}
};
function setupInitialSchema(db) {
const usersStore = db.createObjectStore("users", {
keyPath: "id",
autoIncrement: true
});
usersStore.createIndex("email", "email", { unique: true });
usersStore.createIndex("created", "createdAt");
const postsStore = db.createObjectStore("posts", {
keyPath: "id",
autoIncrement: true
});
postsStore.createIndex("author", "authorId");
postsStore.createIndex("published", "publishedAt");
}
function migrateSchema(db, fromVersion, toVersion) {
const transaction = event.target.transaction;
for (let version = fromVersion + 1; version <= toVersion; version++) {
switch (version) {
case 2:
// Add comments store
const commentsStore = db.createObjectStore("comments", {
keyPath: "id",
autoIncrement: true
});
commentsStore.createIndex("post", "postId");
break;
case 3:
// Add tags index to posts
const postsStore = transaction.objectStore("posts");
postsStore.createIndex("tags", "tags", { multiEntry: true });
break;
case 4:
// Data migration
migrateUserData(transaction.objectStore("users"));
break;
}
}
}// Comprehensive event handling with propagation
const tx = db.transaction(["products", "orders"], "readwrite");
// Transaction-level error handler (catches all operation errors)
tx.onerror = (event) => {
console.error("Transaction error:", event.target.error);
// Check if error was handled at operation level
if (event.defaultPrevented) {
console.log("Error was handled by operation");
} else {
console.log("Unhandled operation error, transaction will abort");
}
};
tx.onabort = (event) => {
console.log("Transaction aborted");
// Cleanup or retry logic
handleTransactionAbort();
};
tx.oncomplete = (event) => {
console.log("Transaction completed successfully");
// Success notifications
showSuccessMessage("Data saved successfully");
};
// Operation-level error handling
const productsStore = tx.objectStore("products");
const addRequest = productsStore.add(product);
addRequest.onerror = (event) => {
const error = event.target.error;
if (error.name === "ConstraintError") {
console.log("Product already exists, using put instead");
// Prevent transaction abort
event.preventDefault();
// Retry with put
productsStore.put(product);
} else {
console.error("Unexpected error:", error);
// Let transaction handle the error
}
};Utility function for testing abnormal database closure scenarios.
/**
* Forces abnormal database closure for testing scenarios
* @param db - Database instance to forcibly close
*/
declare function forceCloseDatabase(db: IDBDatabase): void;Usage Examples:
import { forceCloseDatabase } from "fake-indexeddb";
// Test database close event handling
const db = await openDatabase();
db.onclose = (event) => {
console.log("Database was forcibly closed");
// Handle unexpected closure
handleUnexpectedClose();
};
// Simulate abnormal closure (e.g., user clearing browser data)
forceCloseDatabase(db);
// Test application resilience
function testDatabaseResilience(db) {
// Set up close handler
db.addEventListener("close", () => {
console.log("Detected database closure");
// Attempt to reconnect
setTimeout(() => {
reopenDatabase();
}, 1000);
});
// Simulate closure after random delay
setTimeout(() => {
console.log("Simulating unexpected database closure");
forceCloseDatabase(db);
}, Math.random() * 5000 + 1000);
}// Handle specific IndexedDB errors
function handleIndexedDBError(error) {
switch (error.name) {
case "AbortError":
console.log("Operation was aborted");
// Transaction was aborted, possibly due to tab close
break;
case "ConstraintError":
console.log("Constraint violation (unique index, etc.)");
// Handle duplicate key or constraint violation
break;
case "DataError":
console.log("Invalid key or key range");
// Validate and sanitize key data
break;
case "DataCloneError":
console.log("Value cannot be cloned");
// Handle non-serializable objects
break;
case "InvalidAccessError":
console.log("Invalid operation for current state");
// Check transaction state and object store availability
break;
case "InvalidStateError":
console.log("Operation not allowed in current state");
// Verify database and transaction state
break;
case "NotFoundError":
console.log("Object store or index not found");
// Handle missing object stores or indexes
break;
case "ReadOnlyError":
console.log("Attempted write on readonly transaction");
// Create readwrite transaction for modifications
break;
case "TransactionInactiveError":
console.log("Transaction is no longer active");
// Create new transaction for the operation
break;
case "VersionError":
console.log("Invalid version number");
// Handle version conflicts
break;
default:
console.error("Unknown IndexedDB error:", error.name, error.message);
}
}
// Global error handler setup
function setupGlobalErrorHandling(db) {
// Database-level error handler
db.onerror = (event) => {
handleIndexedDBError(event.target.error);
};
// Version change handler
db.onversionchange = (event) => {
console.log("Database version changed by another connection");
// Close current connection
db.close();
// Notify user
showVersionChangeNotification();
};
// Close handler
db.onclose = (event) => {
console.log("Database connection closed");
// Update application state
updateConnectionStatus(false);
// Attempt reconnection if unexpected
if (!event.wasClean) {
attemptReconnection();
}
};
}// Convert IDBRequest to Promise
function requestToPromise(request) {
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// Usage with async/await
async function getProduct(store, id) {
try {
const product = await requestToPromise(store.get(id));
return product;
} catch (error) {
console.error("Failed to get product:", error);
throw error;
}
}
// Transaction as Promise
function transactionToPromise(transaction) {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = (event) => reject(event.target.error);
transaction.onabort = () => reject(new Error("Transaction aborted"));
});
}
// Complete async transaction
async function performAsyncTransaction(db) {
const tx = db.transaction(["products", "orders"], "readwrite");
const productsStore = tx.objectStore("products");
const ordersStore = tx.objectStore("orders");
try {
// Perform operations
await requestToPromise(productsStore.put(product));
await requestToPromise(ordersStore.add(order));
// Wait for transaction completion
await transactionToPromise(tx);
console.log("Transaction completed successfully");
} catch (error) {
console.error("Transaction failed:", error);
throw error;
}
}// Centralized event management
class IndexedDBEventManager {
constructor(db) {
this.db = db;
this.listeners = new Map();
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
this.db.onerror = (event) => {
this.emit("database-error", event.target.error);
};
this.db.onversionchange = (event) => {
this.emit("version-change", event);
};
this.db.onclose = (event) => {
this.emit("database-close", event);
};
}
on(eventType, callback) {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType).push(callback);
}
off(eventType, callback) {
const callbacks = this.listeners.get(eventType);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
emit(eventType, data) {
const callbacks = this.listeners.get(eventType);
if (callbacks) {
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error("Event handler error:", error);
}
});
}
}
}
// Usage
const eventManager = new IndexedDBEventManager(db);
eventManager.on("database-error", (error) => {
console.error("Database error:", error);
showErrorNotification(error.message);
});
eventManager.on("version-change", () => {
console.log("Database version changed");
showVersionChangeDialog();
});
eventManager.on("database-close", () => {
console.log("Database closed");
updateUIState("disconnected");
});// Development helper for monitoring requests
function monitorRequest(request, label = "Request") {
console.log(`${label} started`);
const startTime = performance.now();
request.onsuccess = (event) => {
const duration = performance.now() - startTime;
console.log(`${label} completed in ${duration.toFixed(2)}ms`);
console.log("Result:", event.target.result);
};
request.onerror = (event) => {
const duration = performance.now() - startTime;
console.error(`${label} failed after ${duration.toFixed(2)}ms`);
console.error("Error:", event.target.error);
};
return request;
}
// Usage
const request = store.get("product-123");
monitorRequest(request, "Get Product");
// Transaction monitoring
function monitorTransaction(transaction, label = "Transaction") {
console.log(`${label} started`);
const startTime = performance.now();
transaction.oncomplete = () => {
const duration = performance.now() - startTime;
console.log(`${label} completed in ${duration.toFixed(2)}ms`);
};
transaction.onerror = (event) => {
const duration = performance.now() - startTime;
console.error(`${label} failed after ${duration.toFixed(2)}ms`);
console.error("Error:", event.target.error);
};
transaction.onabort = () => {
const duration = performance.now() - startTime;
console.warn(`${label} aborted after ${duration.toFixed(2)}ms`);
};
return transaction;
}