A pure JS in-memory implementation of the IndexedDB API for testing IndexedDB-dependent code in Node.js environments
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
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;
}