Helper functions for comparison, property modification, and range operations that extend Dexie's capabilities.
IndexedDB-compatible comparison function for sorting and ordering operations.
/**
* Compares two values using IndexedDB comparison semantics
* @param a - First value to compare
* @param b - Second value to compare
* @returns -1 if a < b, 0 if a === b, 1 if a > b
*/
function cmp(a: any, b: any): number;Comparison Order:
undefined (always first)nullNaN, Infinity)Usage Examples:
import { cmp } from "dexie";
// Basic comparisons
console.log(cmp(1, 2)); // -1
console.log(cmp(2, 1)); // 1
console.log(cmp(1, 1)); // 0
// String comparison
console.log(cmp("apple", "banana")); // -1
console.log(cmp("zebra", "apple")); // 1
// Date comparison
const date1 = new Date("2023-01-01");
const date2 = new Date("2023-01-02");
console.log(cmp(date1, date2)); // -1
// Mixed type comparison
console.log(cmp(null, undefined)); // 1
console.log(cmp(1, "1")); // -1 (number before string)
console.log(cmp(new Date(), 1)); // 1 (date after number)
// Array comparison (element by element)
console.log(cmp([1, 2], [1, 3])); // -1
console.log(cmp([1, 2, 3], [1, 2])); // 1
// Use in sorting
const values = [undefined, null, 3, "hello", new Date(), [1, 2]];
values.sort(cmp);
console.log(values); // [undefined, null, 3, Date, "hello", [1, 2]]
// Custom sorting with cmp
const friends = await db.friends.toArray();
friends.sort((a, b) => cmp(a.name, b.name));
// Binary search using cmp
function binarySearch<T>(arr: T[], target: T): number {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const comparison = cmp(arr[mid], target);
if (comparison === 0) return mid;
if (comparison < 0) left = mid + 1;
else right = mid - 1;
}
return -1;
}Atomic property modification utilities for safe concurrent updates.
/**
* Represents an atomic property modification operation
*/
class PropModification {
constructor(spec: PropModSpec);
/**
* Executes the modification on a value
* @param value - Current value to modify
* @returns Modified value
*/
execute(value: any): any;
}
type PropModSpec = {
replacePrefix?: [string, string];
add?: number | bigint | any[];
remove?: number | bigint | any[];
};/**
* Creates a modification that adds to numeric values or appends to arrays
* @param value - Number/bigint to add, or array elements to append
* @returns PropModification for atomic addition
*/
function add(value: number | bigint | any[]): PropModification;Usage Examples:
import { add } from "dexie";
// Increment numeric values
await db.users.update(1, {
age: add(1), // Increment age by 1
score: add(100), // Add 100 to score
balance: add(-50) // Subtract 50 from balance
});
// Add to arrays
await db.users.update(1, {
tags: add(["new-tag", "another-tag"]), // Append to array
interests: add(["photography"]) // Append single item as array
});
// BigInt support
await db.accounts.update(1, {
bigIntValue: add(BigInt("999999999999999999"))
});
// Conditional addition
const user = await db.users.get(1);
if (user && user.level < 10) {
await db.users.update(1, {
experience: add(50),
level: user.experience + 50 >= 1000 ? add(1) : user.level
});
}
// Bulk updates with addition
const updates = activeUsers.map(userId => ({
key: userId,
changes: { loginCount: add(1), lastLogin: Date.now() }
}));
await db.users.bulkUpdate(updates);/**
* Creates a modification that subtracts from numeric values or removes from arrays
* @param value - Number/bigint to subtract, or array elements to remove
* @returns PropModification for atomic subtraction/removal
*/
function remove(value: number | bigint | any[]): PropModification;Usage Examples:
import { remove } from "dexie";
// Subtract numeric values
await db.users.update(1, {
credits: remove(10), // Subtract 10 credits
lives: remove(1), // Remove 1 life
debt: remove(-100) // Add 100 (remove negative)
});
// Remove from arrays
await db.users.update(1, {
tags: remove(["old-tag", "deprecated"]), // Remove specific items
blockedUsers: remove([userId]) // Remove user from blocked list
});
// Remove with safety checks
const user = await db.users.get(1);
if (user && user.credits >= 50) {
await db.users.update(1, { credits: remove(50) });
}
// Conditional array removal
await db.posts.where("id").equals(postId).modify(post => {
if (post.tags && post.tags.includes("draft")) {
post.tags = remove(["draft"]).execute(post.tags);
}
});
// BigInt subtraction
await db.accounts.update(1, {
bigBalance: remove(BigInt("1000000000000000"))
});/**
* Creates a modification that replaces string prefixes
* @param oldPrefix - Prefix to replace
* @param newPrefix - New prefix to use
* @returns PropModification for atomic prefix replacement
*/
function replacePrefix(oldPrefix: string, newPrefix: string): PropModification;Usage Examples:
import { replacePrefix } from "dexie";
// Replace URL prefixes
await db.resources.update(1, {
imageUrl: replacePrefix("http://", "https://"),
thumbnailUrl: replacePrefix("http://", "https://")
});
// Update file paths
await db.documents.where("path").startsWith("/old/path/").modify({
path: replacePrefix("/old/path/", "/new/path/")
});
// Namespace changes
await db.settings.where("key").startsWith("app.v1.").modify({
key: replacePrefix("app.v1.", "app.v2.")
});
// Bulk prefix replacement
const urlUpdates = await db.images.where("url").startsWith("http://").primaryKeys();
const updates = urlUpdates.map(id => ({
key: id,
changes: { url: replacePrefix("http://", "https://") }
}));
await db.images.bulkUpdate(updates);
// Conditional prefix replacement
await db.users.toCollection().modify(user => {
if (user.website && user.website.startsWith("http://")) {
user.website = replacePrefix("http://", "https://").execute(user.website);
}
});Utilities for working with index ranges and sets for advanced query operations.
/**
* Represents a set of key ranges for efficient range operations
*/
class RangeSet {
constructor(fromOrTree?: any, to?: any);
/**
* Adds a range or key to the set
* @param rangeOrKey - Range object or single key
* @returns New RangeSet with added range
*/
add(rangeOrKey: any): RangeSet;
/**
* Adds a single key to the set
* @param key - Key to add
* @returns New RangeSet with added key
*/
addKey(key: any): RangeSet;
/**
* Adds multiple keys to the set
* @param keys - Array of keys to add
* @returns New RangeSet with added keys
*/
addKeys(keys: any[]): RangeSet;
/**
* Checks if the set contains a specific key
* @param key - Key to check
* @returns True if key is in the set
*/
hasKey(key: any): boolean;
}
type IntervalTree = any; // Internal tree structureUsage Examples:
import { RangeSet } from "dexie";
// Create range sets
const ageRanges = new RangeSet()
.add({ from: 18, to: 25 }) // Young adults
.add({ from: 35, to: 45 }); // Middle-aged
// Add specific keys
const specificAges = new RangeSet()
.addKey(21)
.addKey(30)
.addKeys([40, 50, 60]);
// Check key membership
console.log(ageRanges.hasKey(20)); // true (in 18-25 range)
console.log(ageRanges.hasKey(30)); // false (not in ranges)
// Use with queries (conceptual - actual implementation may vary)
const filteredFriends = await db.friends
.where("age")
.anyOf(specificAges.toArray()) // Convert to array for anyOf
.toArray();/**
* Merges ranges from one IntervalTree into another
* @param target - Target tree to merge into
* @param newSet - Source tree to merge from
*/
function mergeRanges(target: IntervalTree, newSet: IntervalTree): void;
/**
* Checks if two range sets overlap
* @param rangeSet1 - First range set
* @param rangeSet2 - Second range set
* @returns True if ranges overlap
*/
function rangesOverlap(rangeSet1: IntervalTree, rangeSet2: IntervalTree): boolean;Usage Examples:
import { mergeRanges, rangesOverlap } from "dexie";
// Merge range sets
const weekdayHours = createRangeSet([[9, 17]]); // Business hours
const weekendHours = createRangeSet([[10, 14]]); // Weekend hours
mergeRanges(weekdayHours, weekendHours);
// weekdayHours now contains both ranges
// Check for overlaps
const morningShift = createRangeSet([[6, 14]]);
const eveningShift = createRangeSet([[14, 22]]);
console.log(rangesOverlap(morningShift, eveningShift)); // true (overlap at 14)
// Practical usage in scheduling
function hasScheduleConflict(newEvent: Event, existingEvents: Event[]): boolean {
const newEventRange = createRangeSet([[newEvent.startTime, newEvent.endTime]]);
return existingEvents.some(event => {
const eventRange = createRangeSet([[event.startTime, event.endTime]]);
return rangesOverlap(newEventRange, eventRange);
});
}
// Range-based filtering
function findEventsInTimeRanges(events: Event[], timeRanges: RangeSet): Event[] {
return events.filter(event => {
return timeRanges.hasKey(event.startTime) ||
timeRanges.hasKey(event.endTime);
});
}Combining utilities for complex operations.
Usage Examples:
// Atomic counter with bounds
async function incrementWithBounds(
table: Table,
key: any,
field: string,
increment: number,
max: number
) {
const record = await table.get(key);
if (record && record[field] + increment <= max) {
await table.update(key, { [field]: add(increment) });
return true;
}
return false;
}
// Batch score updates
async function updateScores(userScores: { userId: number; points: number }[]) {
const updates = userScores.map(({ userId, points }) => ({
key: userId,
changes: {
totalScore: add(points),
lastUpdated: Date.now()
}
}));
await db.users.bulkUpdate(updates);
}
// Safe array manipulation
async function addTagIfNotExists(userId: number, tag: string) {
const user = await db.users.get(userId);
if (user && (!user.tags || !user.tags.includes(tag))) {
await db.users.update(userId, { tags: add([tag]) });
}
}
// URL normalization
async function normalizeUrls() {
await db.resources.toCollection().modify(resource => {
if (resource.url) {
// Replace multiple prefixes
let url = resource.url;
url = replacePrefix("http://", "https://").execute(url);
url = replacePrefix("www.", "").execute(url);
resource.url = url;
}
});
}
// Complex range queries with cmp
function findRecordsInCustomRange<T>(
records: T[],
field: keyof T,
min: any,
max: any
): T[] {
return records.filter(record => {
const value = record[field];
return cmp(value, min) >= 0 && cmp(value, max) <= 0;
});
}
// Efficient sorting with cmp
function sortByMultipleFields<T>(
records: T[],
fields: (keyof T)[]
): T[] {
return records.sort((a, b) => {
for (const field of fields) {
const result = cmp(a[field], b[field]);
if (result !== 0) return result;
}
return 0;
});
}Handling edge cases and errors in utility function usage.
Usage Examples:
// Safe property modification
async function safeUpdate(table: Table, key: any, changes: any) {
try {
const result = await table.update(key, changes);
if (result === 0) {
console.warn("No record found with key:", key);
}
return result;
} catch (error) {
console.error("Update failed:", error);
throw error;
}
}
// Validate before PropModification
async function safeIncrement(userId: number, field: string, amount: number) {
const user = await db.users.get(userId);
if (!user) {
throw new Error("User not found");
}
if (typeof user[field] !== "number") {
throw new Error(`Field ${field} is not a number`);
}
await db.users.update(userId, { [field]: add(amount) });
}
// Handle array modifications safely
async function safeArrayAdd(table: Table, key: any, field: string, items: any[]) {
const record = await table.get(key);
if (!record) return false;
// Ensure field is an array
if (!Array.isArray(record[field])) {
record[field] = [];
await table.put(record);
}
await table.update(key, { [field]: add(items) });
return true;
}
// Comparison with type checking
function safeCompare(a: any, b: any): number {
try {
return cmp(a, b);
} catch (error) {
console.warn("Comparison failed, falling back to string comparison:", error);
return String(a).localeCompare(String(b));
}
}