Skip to content

Implement local-first architecture with IndexDB#10

Draft
kertal wants to merge 1 commit into
mainfrom
claude/local-first-indexdb-0zENV
Draft

Implement local-first architecture with IndexDB#10
kertal wants to merge 1 commit into
mainfrom
claude/local-first-indexdb-0zENV

Conversation

@kertal
Copy link
Copy Markdown
Owner

@kertal kertal commented Jan 1, 2026

…ecking

This commit refactors the data fetching and caching logic to follow true local-first principles:

Changes:

  1. Cache Freshness Utilities (src/utils/indexedDB.ts):

    • Added CACHE_TTL_MS constant (30 minutes default)
    • Implemented cacheUtils.isFresh() to check cache age
    • Implemented cacheUtils.matchesQuery() to validate cache against current query
    • Implemented cacheUtils.isValidCache() to combine both checks
  2. Local-First Data Fetching (src/hooks/useGitHubDataFetching.ts):

    • Added eventsMetadata and searchItemsMetadata parameters
    • Check cache freshness BEFORE deciding to block UI
    • If cache is fresh and matches query: non-blocking background refresh
    • If cache is stale or missing: blocking initial load
    • Users see cached data immediately when available
    • API fetches happen in background without locking the UI
  3. Updated App Integration (src/App.tsx):

    • Destructure metadata from useIndexedDBStorage hooks
    • Pass metadata to useGitHubDataFetching for cache validation

Before: API-first with local caching (always blocks UI to fetch) After: Local-first with background sync (shows cache, refreshes behind scenes)

Benefits:

  • Instant data display when cache is fresh (< 30 min old)
  • No UI blocking for background refreshes
  • Better offline experience
  • Clearer data flow that's easier to reason about
  • True local-first architecture as documented

All tests pass. No breaking changes to existing functionality.

@kertal kertal requested a review from Copilot January 1, 2026 18:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a local-first architecture for GitHub data fetching using IndexedDB. Instead of always blocking the UI to fetch fresh data, the application now checks if cached data is fresh (< 30 minutes old) and matches the current query parameters. When fresh cache exists, users see data immediately while a background refresh occurs. When cache is stale or missing, the UI shows a loading state.

Key Changes:

  • Added cache freshness validation utilities (TTL checks and query parameter matching)
  • Refactored data fetching to prioritize cached data when fresh, performing non-blocking background updates
  • Enhanced metadata tracking to support cache validation decisions

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/utils/indexedDB.ts Adds cache TTL constant and utility functions for validating cache freshness and query parameter matching
src/hooks/useGitHubDataFetching.ts Implements local-first logic to check cache validity before deciding whether to block UI or perform background refresh
src/App.tsx Passes metadata from IndexedDB storage hooks to data fetching hook for cache validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +180 to +187
const hasCachedEvents = indexedDBEvents.length > 0 && eventsMetadata;
const hasCachedSearchItems = indexedDBSearchItems.length > 0 && searchItemsMetadata;

// Check if either cache is fresh and matches the current query
const eventsCacheFresh = hasCachedEvents &&
cacheUtils.isValidCache(eventsMetadata!, usernames, startDate, endDate);
const searchItemsCacheFresh = hasCachedSearchItems &&
cacheUtils.isValidCache(searchItemsMetadata!, usernames, startDate, endDate);
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using non-null assertion operator (!) bypasses TypeScript's null safety. The conditional checks hasCachedEvents and hasCachedSearchItems already verify metadata existence, but the assertions make the code fragile if the conditions change. Consider refactoring to pass the metadata directly without assertions, or use optional chaining with a fallback return value from isValidCache when metadata is null.

Suggested change
const hasCachedEvents = indexedDBEvents.length > 0 && eventsMetadata;
const hasCachedSearchItems = indexedDBSearchItems.length > 0 && searchItemsMetadata;
// Check if either cache is fresh and matches the current query
const eventsCacheFresh = hasCachedEvents &&
cacheUtils.isValidCache(eventsMetadata!, usernames, startDate, endDate);
const searchItemsCacheFresh = hasCachedSearchItems &&
cacheUtils.isValidCache(searchItemsMetadata!, usernames, startDate, endDate);
const hasCachedEvents = indexedDBEvents.length > 0 && !!eventsMetadata;
const hasCachedSearchItems = indexedDBSearchItems.length > 0 && !!searchItemsMetadata;
// Check if either cache is fresh and matches the current query
const eventsCacheFresh =
hasCachedEvents && eventsMetadata
? cacheUtils.isValidCache(eventsMetadata, usernames, startDate, endDate)
: false;
const searchItemsCacheFresh =
hasCachedSearchItems && searchItemsMetadata
? cacheUtils.isValidCache(searchItemsMetadata, usernames, startDate, endDate)
: false;

Copilot uses AI. Check for mistakes.
Comment on lines +200 to +203
setLoadingProgress(hasCachedEvents || hasCachedSearchItems ?
'Cache outdated, fetching fresh data...' :
'Starting search...'
);
Copy link

Copilot AI Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progress message logic checks for any cached data (hasCachedEvents || hasCachedSearchItems) but doesn't verify if that cache actually matches the current query parameters. This could show 'Cache outdated' even when the cache is for completely different query parameters. Consider checking if the cache matches the query (not just exists) before displaying the 'Cache outdated' message versus 'Starting search'.

Suggested change
setLoadingProgress(hasCachedEvents || hasCachedSearchItems ?
'Cache outdated, fetching fresh data...' :
'Starting search...'
);
// We don't know if existing cache matches the current query, so use a neutral message
setLoadingProgress('Starting search...');

Copilot uses AI. Check for mistakes.
@kertal
Copy link
Copy Markdown
Owner Author

kertal commented Jan 1, 2026

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 1, 2026

@kertal I've opened a new pull request, #11, to work on those changes. Once the pull request is ready, I'll request review from you.

…ecking

This commit refactors the data fetching and caching logic to follow true local-first principles:

**Changes:**

1. **Cache Freshness Utilities** (src/utils/indexedDB.ts):
   - Added CACHE_TTL_MS constant (30 minutes default)
   - Implemented cacheUtils.isFresh() to check cache age
   - Implemented cacheUtils.matchesQuery() to validate cache against current query
   - Implemented cacheUtils.isValidCache() to combine both checks

2. **Local-First Data Fetching** (src/hooks/useGitHubDataFetching.ts):
   - Added eventsMetadata and searchItemsMetadata parameters
   - Check cache freshness BEFORE deciding to block UI
   - If cache is fresh and matches query: non-blocking background refresh
   - If cache is stale or missing: blocking initial load
   - Users see cached data immediately when available
   - API fetches happen in background without locking the UI

3. **Updated App Integration** (src/App.tsx):
   - Destructure metadata from useIndexedDBStorage hooks
   - Pass metadata to useGitHubDataFetching for cache validation

**Before:** API-first with local caching (always blocks UI to fetch)
**After:** Local-first with background sync (shows cache, refreshes behind scenes)

**Benefits:**
- Instant data display when cache is fresh (< 30 min old)
- No UI blocking for background refreshes
- Better offline experience
- Clearer data flow that's easier to reason about
- True local-first architecture as documented

All tests pass. No breaking changes to existing functionality.
@kertal kertal force-pushed the claude/local-first-indexdb-0zENV branch from 04c2dfc to 806ef15 Compare January 9, 2026 22:43
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying git-vegas with  Cloudflare Pages  Cloudflare Pages

Latest commit: 806ef15
Status: ✅  Deploy successful!
Preview URL: https://fb99170e.git-vegas.pages.dev
Branch Preview URL: https://claude-local-first-indexdb-0.git-vegas.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants