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

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