Skip to main content

Real-Time Normalized Cache with TanStack Query

The useNormalizedQuery hook provides instant, event-driven cache updates with server-side filtering. Built with 2025 best practices including runtime validation (Valibot), type-safe merging (ts-deepmerge), and proper subscription cleanup.

Overview

useNormalizedQuery wraps TanStack Query to add real-time capabilities:
  • Instant updates across all devices via WebSocket events
  • Server-side filtering reduces network traffic by 90%+
  • Client-side safety ensures correctness even with unrelated events
  • Proper cleanup prevents connection leaks
  • Runtime validation catches malformed events
  • Type-safe merging preserves data integrity

Architecture

Basic Usage

Directory Listing

What happens:
  1. Initial query fetches directory listing
  2. Hook subscribes to file events for this path (exact mode)
  3. When files are created/updated, events arrive instantly
  4. Cache updates atomically
  5. UI re-renders with new data

Media View (Recursive)

Global Resources

Single Resource Queries

API Reference

Options

Path Filtering Modes

Exact Mode (Default)

Only events for files directly in the specified directory:
Behavior:
  • File in /Photos/image.jpg → ✓ Included
  • File in /Photos/Vacation/beach.jpg → ✗ Excluded
  • Directory /Photos/Vacation → ✗ Excluded

Recursive Mode

All events for files anywhere under the specified directory:
Behavior:
  • File in /Photos/image.jpg → ✓ Included
  • File in /Photos/Vacation/beach.jpg → ✓ Included
  • File in /Photos/Vacation/Cruise/pic.jpg → ✓ Included

Server-Side Filtering

How It Works

Each hook creates a filtered subscription on the backend:
Backend applies filters before sending events:
  1. resource_type matches?
  2. library_id matches?
  3. path_scope matches? (with include_descendants mode)
  4. resourceId matches? (if specified)
Result: Only matching events are transmitted over the network.

Filter Logic

Exact Mode:
Recursive Mode:

Client-Side Safety Filtering

Even with server-side filtering, the client applies a safety filter to batch events:
This ensures correctness even if server-side filtering has edge cases.

Event Types

ResourceChanged (Single)

ResourceChangedBatch (Multiple)

ResourceDeleted

Refresh (Invalidate All)

Triggers queryClient.invalidateQueries() to refetch all data.

Deep Merge Behavior

Uses ts-deepmerge for type-safe, configurable merging:

No-Merge Fields

Some fields should be replaced entirely, not merged:

Runtime Validation

All events are validated with Valibot before processing:

Subscription Multiplexing

Multiple hooks with identical filters automatically share a single backend subscription:
How it works:
  1. First hook creates subscription with filter {resource_type: "location", library_id: "abc"}
  2. Subscription manager generates key from filter: {"resource_type":"location","library_id":"abc"}
  3. Second hook with same filter reuses existing subscription
  4. Events broadcast to all listeners
  5. When both unmount, subscription cleaned up automatically
Benefits:
  • Eliminates duplicate subscriptions during render cycles
  • Reduces backend load (fewer Unix socket connections)
  • Faster subscription setup (reuses existing connection)
  • Automatic reference counting prevents premature cleanup

Subscription Cleanup

Subscriptions are properly cleaned up when components unmount:
Cleanup process:
  1. React calls cleanup function
  2. Frontend stops listening to events
  3. Tauri sends Unsubscribe request to daemon
  4. Daemon closes subscription
  5. Unix socket connection closed
Result: No connection leaks, no memory leaks.

Performance

Event Reduction

Connection Management

  • Multiplexing: Multiple hooks with identical filters share one backend subscription
  • Reference counting: Subscriptions cleaned up when last hook unmounts
  • Deduplication: Eliminates duplicate subscriptions during render cycles
  • Monitoring: Check client.getSubscriptionStats() for active subscriptions

Testing

Test Coverage

Rust (Backend):
  • 9/9 event filtering tests passing
  • Validates exact vs recursive modes
  • Tests all path types (Physical, Content, Cloud, Sidecar)
TypeScript (Frontend):
  • 5/5 integration tests passing
  • Uses real backend event fixtures
  • Validates filtering and cache updates
  • Proves correctness with actual production code

Run Tests

Best Practices

Always Scope File Queries

Use Correct Mode for View Type

Combine with TanStack Query Options

Advanced Usage

Content-Addressed Files

Files use Content-based sd_path but have Physical paths in alternate_paths:

Multiple Instances

Multiple files with same content have different IDs:

Debugging

Enable Logging

Monitor Subscriptions

Frontend subscription stats:

Inspect Cache

Migration

From useLibraryQuery

Backward Compatibility

The old useNormalizedCache name is aliased:

Technical Details

Exported Functions

Core logic is exported for testing:

Runtime Dependencies

  • ts-deepmerge - Type-safe deep merging
  • valibot - Runtime event validation
  • tiny-invariant - Assertion helpers
  • type-fest - TypeScript utilities
  • @tanstack/react-query - Core caching

Subscription Lifecycle

Common Patterns

List with Real-Time Updates

Directory with Instant File Appearance

Inspector with Sidecar Updates

Summary

useNormalizedQuery provides production-grade real-time caching:
  • Server-side filtering (90%+ event reduction)
  • Client-side safety (validates and filters)
  • Proper cleanup (no connection leaks)
  • Runtime validation (catches bad events)
  • Type-safe merging (preserves data)
  • Comprehensive tests (9 Rust + 5 TypeScript)
  • TanStack Query compatible (all features work)
  • Cross-device sync (instant updates everywhere)
Use it for any query where data can change and you want instant updates without manual refetching.