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

live-queries.mddocs/

Live Queries

Reactive query system that automatically updates when underlying data changes, following the TC39 Observable proposal.

Capabilities

LiveQuery Function

Creates a reactive query that automatically re-executes when dependent data changes.

/**
 * Creates a reactive query that automatically updates when data changes
 * @param querier - Function that returns data or a Promise of data
 * @returns Observable that emits updated results
 */
function liveQuery<T>(querier: () => T | Promise<T>): Observable<T>;

interface Observable<T> {
  /** Subscribe to value changes */
  subscribe(observer: Observer<T> | ((value: T) => void)): Subscription;
}

interface Observer<T> {
  /** Called when new value is available */
  next: (value: T) => void;
  /** Called when an error occurs */
  error?: (error: any) => void;
  /** Called when observable completes (rarely used) */
  complete?: () => void;
}

interface Subscription {
  /** Stop receiving updates */
  unsubscribe(): void;
}

Usage Examples:

import { liveQuery } from "dexie";

// Basic live query
const friends$ = liveQuery(() => db.friends.toArray());

// Subscribe with function
friends$.subscribe(friends => {
  console.log("Friends updated:", friends);
});

// Subscribe with observer object
friends$.subscribe({
  next: friends => console.log("Friends:", friends),
  error: err => console.error("Query failed:", err)
});

// Query with parameters
function getFriendsByAge(minAge: number) {
  return liveQuery(() => 
    db.friends.where("age").aboveOrEqual(minAge).toArray()
  );
}

const adults$ = getFriendsByAge(18);
adults$.subscribe(adults => {
  console.log(`${adults.length} adults found`);
});

Complex Live Queries

Live queries can include any database operations and will automatically track dependencies.

Usage Examples:

// Complex query with joins
const postsWithAuthors$ = liveQuery(async () => {
  const posts = await db.posts.orderBy("createdAt").reverse().limit(10).toArray();
  
  // Fetch authors for each post
  const postsWithAuthors = await Promise.all(
    posts.map(async post => {
      const author = await db.users.get(post.authorId);
      return { ...post, author };
    })
  );
  
  return postsWithAuthors;
});

// Aggregation query
const statistics$ = liveQuery(async () => {
  const [totalUsers, totalPosts, activeUsers] = await Promise.all([
    db.users.count(),
    db.posts.count(),
    db.users.where("lastActive").above(Date.now() - 24 * 60 * 60 * 1000).count()
  ]);
  
  return { totalUsers, totalPosts, activeUsers };
});

// Conditional query
const userDashboard$ = liveQuery(async () => {
  const currentUser = await db.users.get(currentUserId);
  if (!currentUser) return null;
  
  const [posts, friends, messages] = await Promise.all([
    db.posts.where("authorId").equals(currentUserId).toArray(),
    db.friendships.where("userId").equals(currentUserId).toArray(),
    db.messages.where("recipientId").equals(currentUserId).count()
  ]);
  
  return { user: currentUser, posts, friends, unreadMessages: messages };
});

Subscription Management

Managing subscriptions for memory leaks prevention and lifecycle control.

Usage Examples:

// Store subscription for cleanup
let subscription: Subscription;

function startWatching() {
  const friends$ = liveQuery(() => db.friends.toArray());
  subscription = friends$.subscribe(friends => {
    updateUI(friends);
  });
}

function stopWatching() {
  if (subscription) {
    subscription.unsubscribe();
    subscription = null;
  }
}

// Auto-cleanup with component lifecycle
class FriendsComponent {
  private subscription?: Subscription;
  
  onMount() {
    const friends$ = liveQuery(() => db.friends.toArray());
    this.subscription = friends$.subscribe(friends => {
      this.friends = friends;
    });
  }
  
  onDestroy() {
    this.subscription?.unsubscribe();
  }
}

// Multiple subscriptions management
class Dashboard {
  private subscriptions: Subscription[] = [];
  
  onMount() {
    // Subscribe to multiple live queries
    const friends$ = liveQuery(() => db.friends.toArray());
    const posts$ = liveQuery(() => db.posts.limit(10).toArray());
    const stats$ = liveQuery(() => this.calculateStats());
    
    this.subscriptions.push(
      friends$.subscribe(friends => this.friends = friends),
      posts$.subscribe(posts => this.posts = posts),
      stats$.subscribe(stats => this.stats = stats)
    );
  }
  
  onDestroy() {
    // Cleanup all subscriptions
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions = [];
  }
  
  private async calculateStats() {
    return {
      friendCount: await db.friends.count(),
      postCount: await db.posts.count(),
      messageCount: await db.messages.count()
    };
  }
}

Error Handling

Handling errors in live queries and reactive updates.

Usage Examples:

// Error handling with observer
const friends$ = liveQuery(() => {
  // This might fail if database is closed
  return db.friends.toArray();
});

friends$.subscribe({
  next: friends => {
    console.log("Friends loaded:", friends.length);
  },
  error: error => {
    console.error("Failed to load friends:", error);
    if (error.name === "DatabaseClosedError") {
      // Handle database closed scenario
      reopenDatabase();
    }
  }
});

// Graceful degradation
const robustQuery$ = liveQuery(async () => {
  try {
    const friends = await db.friends.toArray();
    return { success: true, data: friends, error: null };
  } catch (error) {
    console.warn("Query failed, returning empty result:", error);
    return { success: false, data: [], error: error.message };
  }
});

robustQuery$.subscribe(result => {
  if (result.success) {
    updateUI(result.data);
  } else {
    showError(result.error);
  }
});

// Retry on failure
function createResilientQuery<T>(queryFn: () => Promise<T>, retries = 3): Observable<T> {
  return liveQuery(async () => {
    let lastError;
    
    for (let i = 0; i <= retries; i++) {
      try {
        return await queryFn();
      } catch (error) {
        lastError = error;
        if (i < retries) {
          await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
        }
      }
    }
    
    throw lastError;
  });
}

Framework Integration

Live queries integrate well with popular frontend frameworks.

React Integration

// Using dexie-react-hooks
import { useLiveQuery } from "dexie-react-hooks";

function FriendsComponent() {
  const friends = useLiveQuery(() => db.friends.toArray(), []);
  const friendCount = useLiveQuery(() => db.friends.count(), 0);
  
  if (!friends) return <div>Loading...</div>;
  
  return (
    <div>
      <h2>Friends ({friendCount})</h2>
      {friends.map(friend => (
        <div key={friend.id}>{friend.name} - {friend.age}</div>
      ))}
    </div>
  );
}

// Custom React hook
function useTodos(completed?: boolean) {
  return useLiveQuery(async () => {
    let query = db.todos.orderBy("createdAt").reverse();
    if (completed !== undefined) {
      query = query.filter(todo => todo.completed === completed);
    }
    return query.toArray();
  }, [completed]);
}

Vue Integration

// Vue 3 Composition API
import { ref, onUnmounted } from "vue";

function useLiveQuery<T>(queryFn: () => Promise<T>, defaultValue: T) {
  const data = ref(defaultValue);
  const loading = ref(true);
  const error = ref(null);
  
  const subscription = liveQuery(queryFn).subscribe({
    next: (result) => {
      data.value = result;
      loading.value = false;
      error.value = null;
    },
    error: (err) => {
      error.value = err;
      loading.value = false;
    }
  });
  
  onUnmounted(() => {
    subscription.unsubscribe();
  });
  
  return { data, loading, error };
}

// Usage in Vue component
export default {
  setup() {
    const { data: friends, loading, error } = useLiveQuery(
      () => db.friends.toArray(),
      []
    );
    
    return { friends, loading, error };
  }
};

Svelte Integration

// Svelte store integration
import { readable } from "svelte/store";

function createLiveQueryStore<T>(queryFn: () => Promise<T>, initialValue: T) {
  return readable(initialValue, (set) => {
    const subscription = liveQuery(queryFn).subscribe(set);
    return () => subscription.unsubscribe();
  });
}

// Usage in Svelte component
const friends = createLiveQueryStore(() => db.friends.toArray(), []);
const friendCount = createLiveQueryStore(() => db.friends.count(), 0);

// In component template:
// {#each $friends as friend}
//   <div>{friend.name}</div>
// {/each}

Performance Considerations

Optimizing live queries for better performance.

Usage Examples:

// Avoid creating new live queries in render loops
// Bad:
function Component({ userId }) {
  const posts = liveQuery(() => db.posts.where("authorId").equals(userId).toArray());
  // Creates new query on every render
}

// Good:
function Component({ userId }) {
  const posts = useLiveQuery(() => 
    db.posts.where("authorId").equals(userId).toArray(), 
    [userId] // Dependency array
  );
}

// Use specific queries instead of filtering in memory
// Bad:
const activeUsers$ = liveQuery(async () => {
  const allUsers = await db.users.toArray();
  return allUsers.filter(user => user.active);
});

// Good:
const activeUsers$ = liveQuery(() => 
  db.users.where("active").equals(1).toArray()
);

// Debounce rapid updates
function createDebouncedLiveQuery<T>(
  queryFn: () => Promise<T>, 
  delay: number = 100
): Observable<T> {
  let timeoutId: NodeJS.Timeout;
  let lastResult: T;
  
  return liveQuery(async () => {
    const result = await queryFn();
    
    return new Promise<T>((resolve) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        lastResult = result;
        resolve(result);
      }, delay);
    });
  });
}

// Limit query scope
// Instead of querying all data and filtering client-side,
// use indexed queries to limit database reads
const recentPosts$ = liveQuery(() =>
  db.posts
    .where("createdAt")
    .above(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
    .limit(50)
    .toArray()
);

Advanced Patterns

Complex reactive patterns using live queries.

Usage Examples:

// Computed live queries (derived data)
const friendStats$ = liveQuery(async () => {
  const friends = await db.friends.toArray();
  
  const ageGroups = friends.reduce((acc, friend) => {
    const ageGroup = Math.floor(friend.age / 10) * 10;
    acc[ageGroup] = (acc[ageGroup] || 0) + 1;
    return acc;
  }, {} as Record<number, number>);
  
  return {
    total: friends.length,
    averageAge: friends.reduce((sum, f) => sum + f.age, 0) / friends.length,
    ageGroups
  };
});

// Combining multiple live queries
function combineLatest<T, U>(
  obs1: Observable<T>, 
  obs2: Observable<U>
): Observable<[T, U]> {
  return new Observable(observer => {
    let value1: T, value2: U;
    let has1 = false, has2 = false;
    
    const sub1 = obs1.subscribe(v => {
      value1 = v;
      has1 = true;
      if (has2) observer.next([value1, value2]);
    });
    
    const sub2 = obs2.subscribe(v => {
      value2 = v;
      has2 = true;
      if (has1) observer.next([value1, value2]);
    });
    
    return () => {
      sub1.unsubscribe();
      sub2.unsubscribe();
    };
  });
}

// Usage
const friends$ = liveQuery(() => db.friends.toArray());
const posts$ = liveQuery(() => db.posts.toArray());

const combined$ = combineLatest(friends$, posts$);
combined$.subscribe(([friends, posts]) => {
  console.log(`${friends.length} friends, ${posts.length} posts`);
});

// Transform live query results
function mapLiveQuery<T, U>(
  source: Observable<T>, 
  mapper: (value: T) => U
): Observable<U> {
  return new Observable(observer => {
    return source.subscribe({
      next: value => observer.next(mapper(value)),
      error: err => observer.error(err),
      complete: () => observer.complete()
    });
  });
}

// Usage
const friendNames$ = mapLiveQuery(
  liveQuery(() => db.friends.toArray()),
  friends => friends.map(f => f.name)
);

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