Skip to content

247 dexiejs migration#248

Merged
InfinityBowman merged 11 commits into
mainfrom
247-dexiejs-migration
Jan 8, 2026
Merged

247 dexiejs migration#248
InfinityBowman merged 11 commits into
mainfrom
247-dexiejs-migration

Conversation

@InfinityBowman
Copy link
Copy Markdown
Owner

@InfinityBowman InfinityBowman commented Jan 8, 2026

Summary by CodeRabbit

  • Refactor

    • Consolidated multiple client-side databases into a single unified storage system for improved efficiency and performance.
  • Documentation

    • Removed internal audit and planning documents. Added comprehensive migration documentation for ongoing architecture improvements.

✏️ Tip: You can customize this high-level summary in your review settings.

@InfinityBowman InfinityBowman linked an issue Jan 8, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 8, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

This PR consolidates multiple IndexedDB databases into a unified Dexie-based storage layer (db.js), migrating from idb and y-indexeddb to dexie and y-dexie. All persistence layers (query cache, form state, PDF/avatar caching, Y.js project storage) now use the shared Dexie instance. Documentation audits are removed; a new Zod OpenAPI Hono migration plan is added.

Changes

Cohort / File(s) Summary
Documentation Deletions (Audits)
packages/docs/audits/dexie-evaluation-2026-01.md, local-first-roadmap.md, local-first-sync-strategy.md, migration-strategy-sequencing-2026-01.md, single-domain-consolidation-2026-01.md, zod-openapi-hono-codecov-evaluation-2026-01.md
Complete removal of six audit documents covering Dexie evaluation, local-first strategy, domain consolidation, and migration planning; totaling 1,923 lines deleted.
Documentation Updates
packages/docs/glossary.md, packages/web/README.md
Updated glossary and README to reflect unified Dexie database schema (projects, pdfs, avatars, formStates, queryCache, localChecklists, localChecklistPdfs, ops) replacing five separate IndexedDB stores.
New Plan Documentation
packages/docs/plans/zod-openapi-hono-migration.md
New 719-line migration plan detailing phased approach to migrate Hono API from plain Zod to @hono/zod-openapi with infrastructure setup, pilot, and rollout phases.
Dependency Updates
packages/web/package.json
Replaced idb and y-indexeddb with dexie (^4.2.1) and y-dexie (^4.2.2); added dev dependency fake-indexeddb (^6.2.5).
Core Database Layer
packages/web/src/primitives/db.js
New 188-line unified Dexie database schema with typed models and exports: db singleton, deleteProjectData(), clearAllData(). Consolidates projects, PDFs, ops, avatars, form states, and query cache into single CoratesDB instance.
Cache & Persistence Refactoring
packages/web/src/primitives/pdfCache.js
Migrated from low-level IndexedDB transactions to Dexie queries (get/put/delete/bulkDelete/where); simplified eviction logic and cache size calculation; all public function signatures preserved.
Cache & Persistence Refactoring
packages/web/src/primitives/avatarCache.js
Replaced IndexedDB transactions with Dexie operations (db.avatars); all seven exported functions retain their signatures while relying on shared db instance.
Cache & Persistence Refactoring
packages/web/src/lib/queryPersister.js
Removed custom IndexedDB setup; now persists TanStack Query client state to db.queryCache using Dexie put/get/delete operations instead of manual transactions.
Cache & Persistence Refactoring
packages/web/src/lib/formStatePersistence.js
Switched from IndexedDB to db.formStates table; added new hasPendingFormState() export; inline expiration checks in getFormState(); bulk cleanup via Dexie delete.
Y.js Persistence Integration
packages/web/src/primitives/useProject/index.js
Replaced y-indexeddb IndexeddbPersistence with Dexie-backed DexieYProvider; project local data cleanup now uses deleteProjectData() instead of direct IndexedDB database deletion.
Auth & Store Updates
packages/web/src/api/better-auth-store.js
Signout cleanup now calls clearAllData() (imported from db.js) instead of clearAvatarCache(), clearing all data types.
Store Refactoring
packages/web/src/stores/localChecklistsStore.js
Migrated from custom IndexedDB wrapper to direct Dexie access (db.localChecklists, db.localChecklistPdfs); transaction-based operations replaced with Dexie put/get/delete calls.
Comprehensive Test Coverage
packages/web/src/primitives/__tests__/db.test.js
New 373-line test suite validating database initialization, CRUD operations on all tables (projects, PDFs, ops, avatars, form states, query cache), indexing, bulk deletes, and cross-table effects.
Comprehensive Test Coverage
packages/web/src/primitives/__tests__/pdfCache.test.js, avatarCache.test.js, queryPersister.test.js, formStatePersistence.test.js
New test suites (+454 lines combined) covering caching, retrieval, expiry, size limits, eviction, isolation per projectId, persistence/restore cycles, and cleanup scenarios.
Test Expectations Update
packages/web/src/lib/__tests__/error-utils.test.js
Error message expectation updated from "Project not found" to "This project could not be found".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Dexiejs migration #247 — Main issue implements broad migration from y-indexeddb/idb to Dexie across multiple persistence layers (db.js, queryPersister, formStatePersistence, pdfCache, avatarCache, useProject).

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references issue 247 and concisely identifies the primary change: migration to Dexie.js for unified client-side database storage, replacing multiple IndexedDB implementations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@InfinityBowman InfinityBowman self-assigned this Jan 8, 2026
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Jan 8, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
corates-workers-prod cf44c44 Jan 08 2026, 03:10 AM

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @packages/web/src/primitives/__tests__/db.test.js:
- Around line 14-25: The comment claiming "Reset modules to get fresh database
instance" is misleading because dynamic import() does not clear module cache;
update the beforeEach comment to state that test isolation is achieved via the
afterEach cleanup (which uses deleteProjectData/clearAllData) OR add
vi.resetModules() before the dynamic import to truly reset the module cache and
get a fresh db instance; adjust the comment accordingly and keep references to
the beforeEach block, db, deleteProjectData, and clearAllData so reviewers can
locate the change.

In @packages/web/src/primitives/db.js:
- Around line 131-148: The formStates store needs a projectId index and the
deleteProjectData routine must remove project-scoped form states; update the
schema in the DB versioning (the formStates definition used when calling
this.version(...).stores) to include projectId as an indexed field (e.g., add
projectId to the formStates key list), and inside deleteProjectData call the
formStates deletion path using
db.formStates.where('projectId').equals(projectId).delete() to remove all
formStates tied to the deleted project.
🧹 Nitpick comments (6)
packages/web/src/api/better-auth-store.js (1)

45-51: Remove unused import.

The clearAvatarCache import on line 48 is no longer used after migrating to clearAllData(). The other avatar-related functions are still needed for caching during authentication.

♻️ Proposed fix to remove unused import
 import {
   fetchAndCacheAvatar,
   getCachedAvatar,
-  clearAvatarCache,
   pruneExpiredAvatars,
 } from '@/primitives/avatarCache.js';
packages/web/src/primitives/db.js (2)

163-168: Consider expanding project cleanup to include ops and formStates.

The deleteProjectData function currently removes the project and its PDFs, but doesn't clean up:

  • ops table entries that may reference this project
  • formStates entries associated with this project (for 'addStudies' forms)

While these may not cause correctness issues, they represent orphaned data that consumes storage space.

♻️ Expand cleanup to include ops and formStates
 export async function deleteProjectData(projectId) {
-  await db.transaction('rw', [db.projects, db.pdfs], async () => {
+  await db.transaction('rw', [db.projects, db.pdfs, db.ops, db.formStates], async () => {
     await db.projects.delete(projectId);
     await db.pdfs.where('projectId').equals(projectId).delete();
+    // Clean up orphaned operation queue entries
+    // Note: ops table doesn't have projectId index, so we need to scan
+    const projectOps = await db.ops.filter(op => 
+      op.endpoint?.includes(projectId) || op.payload?.projectId === projectId
+    ).toArray();
+    await db.ops.bulkDelete(projectOps.map(op => op.id));
+    
+    // Clean up form states for addStudies forms
+    await db.formStates.where('projectId').equals(projectId).delete();
   });
 }

Note: This assumes formStates has a projectId field as indicated in the FormStateRow typedef. If ops cleanup is too expensive without an index, consider adding a projectId index to the ops table in a future version.


163-188: Consider adding error handling for data operations.

Both deleteProjectData and clearAllData perform critical data operations but don't include error handling. If these operations fail (e.g., due to quota exceeded, IndexedDB corruption), the caller won't be notified.

Consider wrapping these in try-catch blocks or documenting that callers should handle potential Dexie exceptions.

♻️ Add error handling example
 export async function deleteProjectData(projectId) {
+  try {
     await db.transaction('rw', [db.projects, db.pdfs], async () => {
       await db.projects.delete(projectId);
       await db.pdfs.where('projectId').equals(projectId).delete();
     });
+  } catch (error) {
+    console.error(`Failed to delete project data for ${projectId}:`, error);
+    throw error; // Re-throw to let caller handle
+  }
 }

Alternatively, document that these functions may throw Dexie errors and callers should handle them appropriately.

packages/web/src/primitives/__tests__/pdfCache.test.js (1)

133-152: LRU eviction test relies on timing delay.

The 10ms setTimeout for timestamp differentiation may be flaky in slow CI environments. Consider using explicit cachedAt values by directly inserting records via db.pdfs.put() instead of relying on cachePdf timing.

♻️ Proposed fix for deterministic timestamp ordering
   describe('LRU eviction', () => {
     it('evicts oldest entries when cache limit exceeded', async () => {
-      // Cache limit is 200MB, so we test with smaller files but verify ordering
-      const data = new ArrayBuffer(1000);
-
-      // Cache multiple entries with different timestamps
-      await cachePdf('project-1', 'study-1', 'old.pdf', data);
-      await new Promise(r => setTimeout(r, 10)); // Small delay for cachedAt difference
-      await cachePdf('project-1', 'study-2', 'newer.pdf', data);
+      // Insert directly with explicit cachedAt values for deterministic ordering
+      await db.pdfs.bulkPut([
+        {
+          id: 'project-1:study-1:old.pdf',
+          projectId: 'project-1',
+          studyId: 'study-1',
+          fileName: 'old.pdf',
+          data: new ArrayBuffer(1000),
+          size: 1000,
+          cachedAt: 1000,
+        },
+        {
+          id: 'project-1:study-2:newer.pdf',
+          projectId: 'project-1',
+          studyId: 'study-2',
+          fileName: 'newer.pdf',
+          data: new ArrayBuffer(1000),
+          size: 1000,
+          cachedAt: 2000,
+        },
+      ]);
 
       // Verify both are cached
       expect(await getCachedPdf('project-1', 'study-1', 'old.pdf')).not.toBeNull();
packages/web/src/primitives/pdfCache.js (1)

51-54: Consider performance for large caches.

getTotalCacheSize() loads all entries via toArray() to sum sizes. For a 200MB cache with many small files, this could load significant data into memory. Consider using Dexie's each() to iterate without loading all data at once, or store a running total in a separate metadata record.

♻️ Alternative using each() for memory efficiency
 async function getTotalCacheSize() {
-  const all = await db.pdfs.toArray();
-  return all.reduce((sum, entry) => sum + (entry.size || 0), 0);
+  let total = 0;
+  await db.pdfs.each(entry => {
+    total += entry.size || 0;
+  });
+  return total;
 }
packages/web/src/lib/queryPersister.js (1)

1-55: Clean migration to Dexie, with one minor suggestion.

The migration from IndexedDB to Dexie is well-executed with proper API usage and updated documentation. All persister methods correctly interact with db.queryCache.

♻️ Consider using nullish coalescing operator for consistency

Line 37 uses the logical OR operator (||) which could theoretically return null if the cached data is a falsy value (0, false, empty string). While TanStack Query cache data should always be an object, using the nullish coalescing operator (??) would be more explicit and consistent with the pattern used in avatarCache.js line 47.

-        return record?.data || null;
+        return record?.data ?? null;

This only checks for null or undefined rather than all falsy values.

Comment on lines +14 to +25
beforeEach(async () => {
// Reset modules to get fresh database instance
const module = await import('../db.js');
db = module.db;
deleteProjectData = module.deleteProjectData;
clearAllData = module.clearAllData;

// Ensure database is open
if (!db.isOpen()) {
await db.open();
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the full test file to understand the implementation
cat -n packages/web/src/primitives/__tests__/db.test.js | head -50

Repository: InfinityBowman/corates

Length of output: 1874


🏁 Script executed:

#!/bin/bash
# Find vitest configuration
fd -t f "vitest.config" packages/web -o packages/

Repository: InfinityBowman/corates

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Look for vitest.config in common locations
find . -name "vitest.config.*" -o -name "vite.config.*" | grep -E "(vitest|vite)" | head -5

Repository: InfinityBowman/corates

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Read the vite.config.js for packages/web
cat -n packages/web/vite.config.js

Repository: InfinityBowman/corates

Length of output: 3299


🏁 Script executed:

#!/bin/bash
# Also check if there's a vitest.config in packages/web root
ls -la packages/web/ | grep -E "vitest|vite"

Repository: InfinityBowman/corates

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Read the db.js file to understand how the database instance is created
cat -n packages/web/src/primitives/db.js | head -60

Repository: InfinityBowman/corates

Length of output: 2452


🏁 Script executed:

#!/bin/bash
# Read the rest of db.js to see how the database instance is created
cat -n packages/web/src/primitives/db.js | tail -n +60

Repository: InfinityBowman/corates

Length of output: 4893


🏁 Script executed:

#!/bin/bash
# Check if there are other test files in the codebase using similar patterns
rg "beforeEach.*import\(" packages/ --type js -B 2 -A 3

Repository: InfinityBowman/corates

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Check if there are other references to resetModules or module reset in tests
rg "resetModules|resetModuleRegistry" packages/web --type js

Repository: InfinityBowman/corates

Length of output: 311


🏁 Script executed:

#!/bin/bash
# Check the setup.js to see if there's any module isolation configuration
cat packages/web/src/__tests__/setup.js 2>/dev/null || echo "File not found"

Repository: InfinityBowman/corates

Length of output: 2074


🏁 Script executed:

#!/bin/bash
# Check projectStore.test.js to see how it uses vi.resetModules()
cat -n packages/web/src/primitives/__tests__/projectStore.test.js | head -40

Repository: InfinityBowman/corates

Length of output: 1580


Remove misleading comment or add vi.resetModules() before import.

The comment at line 15 ("Reset modules to get fresh database instance") is misleading. Dynamic import() alone does not reset the module cache; the db singleton is shared across all tests. Test isolation currently works through the comprehensive afterEach cleanup (lines 27-39), not the dynamic import.

Either:

  1. Update the comment to reflect that cleanup provides isolation, not the import
  2. Add vi.resetModules() before the import (like in projectStore.test.js) if you want truly fresh instances per test

The current approach works, but the code comment should accurately describe what's happening.

🤖 Prompt for AI Agents
In @packages/web/src/primitives/__tests__/db.test.js around lines 14 - 25, The
comment claiming "Reset modules to get fresh database instance" is misleading
because dynamic import() does not clear module cache; update the beforeEach
comment to state that test isolation is achieved via the afterEach cleanup
(which uses deleteProjectData/clearAllData) OR add vi.resetModules() before the
dynamic import to truly reset the module cache and get a fresh db instance;
adjust the comment accordingly and keep references to the beforeEach block, db,
deleteProjectData, and clearAllData so reviewers can locate the change.

Comment on lines +131 to +148
this.version(1).stores({
// Y.Doc stored as 'ydoc' property via y-dexie
projects: 'id, orgId, updatedAt, ydoc: Y.Doc',
// PDF cache with LRU eviction by cachedAt
pdfs: 'id, projectId, studyId, cachedAt',
// Operation queue with compound index for efficient pending queries
ops: '++id, idempotencyKey, status, createdAt, [status+createdAt]',
// Avatar cache with expiry by cachedAt
avatars: 'userId, cachedAt',
// Form state persistence for OAuth redirects
formStates: 'key, type, timestamp',
// Local checklists for offline practice
localChecklists: 'id, createdAt, updatedAt',
// PDFs associated with local checklists
localChecklistPdfs: 'checklistId, updatedAt',
// TanStack Query cache persistence
queryCache: 'key',
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

y-dexie v4 Y.Doc schema syntax Dexie storage

💡 Result:

Summary — y-dexie (v4) + Y.Doc schema + how Dexie stores Y.Doc

  • What y-dexie is: y-dexie is the Dexie add‑on that provides Y.js (Y.Doc) storage and a Dexie Y provider for loading/releasing Y.Doc instances in Dexie-managed objects. Use it by adding the addon when creating the Dexie instance. [1]

  • Schema syntax (how to declare Y.Doc fields): declare a property in your stores() schema with the type Y.Doc (example):
    db.version(1).stores({
    friends: ++id, name, notes: Y.Doc, age
    })
    This tells Dexie/y-dexie that the property (notes) is a Y.Doc property (virtual on objects; persisted in its own dedicated table). [1][2]

  • Behavior / rules (key points):

    • Y.Doc properties exist on all returned objects (as non-enumerable prototype properties) and are never null/undefined. [2]
    • Physical Y.Doc update data are stored in a separate IndexedDB table(s) managed by y-dexie (not inline with the object row). [2]
    • Y.Doc props are readonly on query results (you cannot replace them with table.update()); to edit use Y.js APIs on the document instance. [2]
    • You must load the document before using it via the provided provider/hook:
      • Imperative: const provider = DexieYProvider.load(doc); await provider.whenLoaded; ... DexieYProvider.release(doc). [2]
      • React: useDocument(doc) / useLiveQuery helpers available in dexie-react-hooks integration. [3]
  • Migration / packaging note: Dexie moved built-in Y.js support into the separate y-dexie add-on (from earlier dexie@4.1.x experimental support). Install/import y-dexie and pass it as an addon when creating the Dexie instance. Also update schema declarations to use Y.Doc. [2]

Selected sources

  • Dexie docs — y-dexie overview & examples. [1]
  • Dexie GitHub release notes / migration details showing Y.Doc schema syntax and DexieYProvider usage. [2]
  • dexie-react-hooks useDocument sample (React hook for consuming Y.Doc). [3]

If you want, I can paste a minimal code example (Dexie + y-dexie + Yjs) and show how to add/get a record and load its Y.Doc.


🏁 Script executed:

cd packages/web/src/primitives && head -200 db.js | tail -100

Repository: InfinityBowman/corates

Length of output: 2898


🏁 Script executed:

cd packages/web/src && rg -A 3 "db\.ops\." | head -50

Repository: InfinityBowman/corates

Length of output: 2768


🏁 Script executed:

cd packages/web/src && rg -A 3 "db\.formStates\." | head -50

Repository: InfinityBowman/corates

Length of output: 2033


🏁 Script executed:

cd packages/web/src/primitives && grep -A 15 "typedef.*OpQueueRow" db.js

Repository: InfinityBowman/corates

Length of output: 733


🏁 Script executed:

cd packages/web/src/lib && head -60 formStatePersistence.js

Repository: InfinityBowman/corates

Length of output: 1656


Remove ops cleanup suggestion; add formStates cleanup for project deletion.

The ydoc: Y.Doc schema syntax is correct for y-dexie v4—y-dexie automatically manages Y.Doc persistence in separate IndexedDB tables and handles cleanup when the parent object is deleted. The ops table does not store projectId (operations are app-wide), so they don't require per-project cleanup.

However, deleteProjectData should also delete form states associated with the project:

await db.formStates.where('projectId').equals(projectId).delete();

Form state entries can be project-specific (when saved during "add studies" workflows) and should be cleaned up to avoid orphaned data, even though the 24-hour expiry provides eventual cleanup.

🤖 Prompt for AI Agents
In @packages/web/src/primitives/db.js around lines 131 - 148, The formStates
store needs a projectId index and the deleteProjectData routine must remove
project-scoped form states; update the schema in the DB versioning (the
formStates definition used when calling this.version(...).stores) to include
projectId as an indexed field (e.g., add projectId to the formStates key list),
and inside deleteProjectData call the formStates deletion path using
db.formStates.where('projectId').equals(projectId).delete() to remove all
formStates tied to the deleted project.

@InfinityBowman InfinityBowman merged commit 19a00e5 into main Jan 8, 2026
2 of 3 checks passed
@InfinityBowman InfinityBowman deleted the 247-dexiejs-migration branch January 8, 2026 03:08
@github-actions github-actions Bot restored the 247-dexiejs-migration branch January 8, 2026 03:09
@InfinityBowman InfinityBowman deleted the 247-dexiejs-migration branch March 29, 2026 03:08
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.

Dexiejs migration

2 participants