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

utility-functions.mddocs/

Utility Functions

Helper functions for comparison, property modification, and range operations that extend Dexie's capabilities.

Capabilities

Comparison Function

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:

  1. undefined (always first)
  2. null
  3. Numbers (including NaN, Infinity)
  4. Dates
  5. Strings
  6. Binary data (ArrayBuffer, typed arrays)
  7. Arrays (compared element by element)

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

Property Modification

Atomic property modification utilities for safe concurrent updates.

PropModification Class

/**
 * 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[];
};

Add Function

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

Remove Function

/**
 * 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"))
});

Replace Prefix Function

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

Range Set Utilities

Utilities for working with index ranges and sets for advanced query operations.

RangeSet Class

/**
 * 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 structure

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

Range Merging Functions

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

Advanced Utility Patterns

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

Error Handling and Edge Cases

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