CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-dexie

A minimalistic wrapper for IndexedDB providing reactive queries, transactions, and schema management

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

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

Install with Tessl CLI

npx tessl i tessl/npm-dexie

docs

database-management.md

error-handling.md

events.md

index.md

live-queries.md

query-building.md

schema-management.md

table-operations.md

utility-functions.md

tile.json