skip to content
walterra.dev
Table of Contents

Two days after 0.1.0, Eddo is now at 0.3.0. The version jump is intentional - I’ve switched to fixed versioning across all packages in the monorepo, and 0.2.0 was consumed by the release automation testing. So 0.3.0 it is.

This release is about performance and polish.

The Performance Problem

This has been annoying me for a while: toggling time tracking on a todo would cause multi-second UI lag. The Kanban board would freeze, waiting for… something. Same with completing todos or switching views.

I knew the issue was in PouchDB’s map/reduce and query layer - I’d tried to fix it before but couldn’t get the views right. This time I had another go at it.

From MapReduce to Mango Queries

Part of the problem was a regression from the GitHub sync work in 0.1.0: some code paths were triggering allDocs() calls that scanned the entire database. But the deeper issue was the reliance on MapReduce views for queries that Mango handles better.

PouchDB’s MapReduce views have overhead - they need to be built and maintained, and the query syntax is awkward. Mango queries using db.find() with selectors turned out to be more straightforward for the common cases:

// Old: MapReduce view query
const result = await db.query('todos_by_time_tracking_active/byTimeTrackingActive', {
key: null
});
// New: Mango selector
const result = await db.find({
selector: {
version: 'alpha3',
active: { $exists: true, $ne: {} }
}
});

The migration (8c38415) replaced most MapReduce view queries with Mango safeFind() calls. Time tracking lookups, todo fetching by date range - these now use selectors instead of pre-built views.

Some MapReduce views remain for cases where they make sense (like the activity aggregation), but the hot paths are now Mango-based.

Before: Time tracking toggle took 2-3 seconds with visible UI freezing. After: Toggles and status changes feel responsive now.

That said, this isn’t a complete fix. Paging through large lists of todos can still feel sluggish - the indexed views help, but PouchDB in the browser has inherent limitations when dealing with hundreds of documents. It’s a meaningful improvement for the common interactions, not a silver bullet.

TanStack Query Mutations Everywhere

With the performance foundation solid, I migrated all remaining direct PouchDB calls to TanStack Query mutations (f590f19). Previously, todo creation and deletion bypassed Query entirely:

// Old: Direct database call, hope the UI updates
await db.put(newTodo);
// ...manually trigger refetch somewhere?

Now everything goes through mutations:

const createTodo = useCreateTodoMutation();
await createTodo.mutateAsync(newTodo);
// Cache invalidation handled automatically

Benefits:

  • Consistent loading/error states via isPending/isError
  • Automatic cache invalidation when todos change
  • Optimistic updates where appropriate
  • Centralized mutation logic that’s easier to test

The use_profile.ts hook now exposes all API calls as mutations through a mutations object, making the GitHub resync button properly show loading states.

GitHub PR Reviews

A smaller but useful addition: Eddo now syncs pull requests awaiting your review (785f7d4), not just assigned issues.

The GitHub client now runs three queries:

  • is:issue assignee:@me → tagged github:issue
  • is:pr assignee:@me → tagged github:pr
  • is:pr review-requested:@me → tagged github:pr-review

Deduplication handles PRs that appear in multiple queries. Now my daily briefings include “PRs waiting for your review” which is genuinely useful when working across multiple repos.

GitHub API Rate Limiting

Heavy GitHub sync usage was hitting API rate limits, so I added proper rate limit handling (63e3c46):

  • Extract x-ratelimit-* headers from every response
  • Enforce minimum 100ms between requests
  • Exponential backoff on 429 errors (1s, 2s, 4s… up to 3 retries)
  • Warning when remaining quota falls below 20%
  • Display current rate limit status in the UI

The user profile now shows something like “API: 4,847/5,000 remaining (resets in 45 min)” so you know where you stand.

Testcontainers: No More Manual Database Setup

The testing story improved significantly with testcontainers (7325fa0). Previously, running integration tests required:

  1. Install CouchDB locally (or via Docker)
  2. Start the service
  3. Configure credentials
  4. Run tests
  5. Remember to clean up

Now tests spin up their own CouchDB containers:

global-testcontainer-setup.ts
const container = await new CouchDBContainer()
.withCredentials('admin', 'password')
.start();
process.env.COUCHDB_URL = container.getConnectionUrl();

Each test suite gets an ephemeral container. No manual setup, complete isolation, works identically in CI and local development. The ~5 second container startup overhead per suite is worth the convenience.

This let me remove the CouchDB service container from GitHub Actions - tests now handle their own infrastructure.

Fixed Versioning

One meta-change: the monorepo now uses fixed versioning (4150888). All packages share the same version number, and releases are aggregated.

This makes more sense for Eddo since packages are tightly coupled - a change in @eddo/core-shared almost always affects @eddo/web-client. Individual package versions were confusing, now everything moves in lockstep.

What’s Next

The performance work exposed how much the codebase has grown without proper profiling, I should add actual metrics. Flying blind on performance until things get noticeably slow isn’t sustainable. And the deployment story remains “run it yourself” which limits who can actually use this.

But for now, the common interactions feel snappier again. Large dataset performance remains a known limitation - at some point I may need to rethink the architecture or accept that browser-based PouchDB has a ceiling, who knows.

On the other hand it’s nice to see some magic moments happen using the app: PouchDb/CouchDb sync updates views across clients in real-time without full page refreshes needed. Github Sync makes issues and pr-reviews popup in Eddo and I can track progress in one place. And the agentic telegram bot delivers daily briefings.

Full release notes on GitHub.