diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 3bb8edf..0000000 --- a/PLAN.md +++ /dev/null @@ -1,200 +0,0 @@ -# Plan: Pluggable Storage Engine for Test Compatibility - -## Problem Summary - -The test suite fails in Node.js because: -- 10 tests in `spellbook-storage.test.ts` fail with `MissingAPIError: IndexedDB API missing` -- Services like `relay-liveness.ts` and `relay-list-cache.ts` show IndexedDB errors (but handle them gracefully) -- The `db.ts` singleton directly uses Dexie/IndexedDB which isn't available in Node - -## Options Analysis - -### Option A: Polyfill IndexedDB with `fake-indexeddb` (Recommended) - -**Approach**: Use `fake-indexeddb` package to provide IndexedDB API in Node tests. - -**Pros**: -- Minimal code changes (just vitest setup) -- Tests actual Dexie code paths accurately -- One-time setup, zero maintenance -- Dexie officially supports this approach - -**Cons**: -- Not truly "pluggable" - still coupled to Dexie -- Tests slightly slower than pure in-memory - -**Effort**: ~30 minutes - ---- - -### Option B: Full Pluggable Storage Interface - -**Approach**: Abstract all storage operations behind interfaces, implement both DexieStorage and InMemoryStorage. - -**Pros**: -- True flexibility - swap storage backends -- Faster tests with pure in-memory implementation -- Could support alternative backends (SQLite, etc.) - -**Cons**: -- Significant refactoring (~11 files) -- More code to maintain -- Need to handle `useLiveQuery` hook differently (Dexie-specific) -- Adds complexity for marginal benefit - -**Effort**: ~4-6 hours - ---- - -### Option C: Hybrid Approach - -**Approach**: Use `fake-indexeddb` for now, extract storage interfaces incrementally where it matters. - -**Pros**: -- Immediate fix with minimal effort -- Can evolve architecture as needed -- Best of both worlds - -**Effort**: ~30 minutes initial + incremental - ---- - -## Recommended Plan: Option A (with Option C path forward) - -### Step 1: Install fake-indexeddb - -```bash -npm install -D fake-indexeddb -``` - -### Step 2: Create vitest setup file - -Create `src/test/setup.ts`: - -```typescript -import "fake-indexeddb/auto"; -``` - -### Step 3: Update vitest config - -Update `vitest.config.ts`: - -```typescript -export default defineConfig({ - test: { - globals: true, - environment: "node", - setupFiles: ["./src/test/setup.ts"], - }, - // ... -}); -``` - -### Step 4: Verify tests pass - -```bash -npm run test:run -``` - -### Step 5 (Optional): Add database reset helper - -Create `src/test/db-helpers.ts` for consistent test cleanup: - -```typescript -import db from "@/services/db"; - -export async function clearTestDatabase() { - await Promise.all([ - db.profiles.clear(), - db.nip05.clear(), - db.nips.clear(), - db.relayInfo.clear(), - db.relayAuthPreferences.clear(), - db.relayLists.clear(), - db.relayLiveness.clear(), - db.spells.clear(), - db.spellbooks.clear(), - ]); -} -``` - ---- - -## Future: Evolving to Pluggable Storage (Option B details) - -If you later want true pluggability, here's how: - -### 1. Define Storage Interface - -```typescript -// src/storage/types.ts -export interface SpellbookStorage { - save(spellbook: Omit): Promise; - get(id: string): Promise; - getAll(): Promise; - delete(id: string): Promise; - markPublished(id: string, event: SpellbookEvent): Promise; -} -``` - -### 2. Create Implementations - -- `src/storage/dexie/spellbook-storage.ts` - Current Dexie implementation -- `src/storage/memory/spellbook-storage.ts` - In-memory for tests - -### 3. Use Context/DI - -```typescript -// src/storage/context.ts -export const StorageContext = createContext(dexieStorage); - -// In tests - -``` - -### 4. Handle useLiveQuery - -The `useLiveQuery` hook is Dexie-specific. Options: -- Create `useStorageQuery` abstraction with observable pattern -- Keep useLiveQuery for components, mock storage layer for unit tests -- Use integration tests with fake-indexeddb for component testing - ---- - -## Files Affected - -### Option A (Minimal): -- `vitest.config.ts` - Add setupFiles -- `src/test/setup.ts` - New file (2 lines) -- `package.json` - Add fake-indexeddb dev dependency - -### Option B (Full refactor): -- `src/services/db.ts` -- `src/services/spell-storage.ts` -- `src/services/spellbook-storage.ts` -- `src/services/relay-state-manager.ts` -- `src/services/relay-list-cache.ts` -- `src/services/relay-liveness.ts` -- `src/lib/nip11.ts` -- `src/hooks/useNip.ts` -- `src/hooks/useNip05.ts` -- `src/hooks/useProfile.ts` -- `src/hooks/useRelayInfo.ts` -- Plus 5 component files using useLiveQuery - ---- - -## My Recommendation - -**Start with Option A**. It's the pragmatic choice: -1. Fixes tests immediately -2. Tests actual production code paths -3. Dexie's maintainers recommend this approach -4. You can always evolve to Option B later if needed - -The "pluggable storage" architecture (Option B) is valuable when: -- You need to support multiple storage backends in production -- You want ultra-fast unit tests (~100 tests in <100ms) -- You're building a library others will use - -For a Nostr app like Grimoire, `fake-indexeddb` gives you the testing capability without the complexity overhead. diff --git a/package-lock.json b/package-lock.json index 375e66e..60659ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,6 +77,7 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", + "fake-indexeddb": "^6.2.5", "globals": "^15.14.0", "postcss": "^8.4.49", "prettier": "^3.7.4", @@ -5966,6 +5967,16 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index f69413a..4d776be 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", + "fake-indexeddb": "^6.2.5", "globals": "^15.14.0", "postcss": "^8.4.49", "prettier": "^3.7.4", diff --git a/src/services/relay-selection.test.ts b/src/services/relay-selection.test.ts index 2dc8a63..fca0201 100644 --- a/src/services/relay-selection.test.ts +++ b/src/services/relay-selection.test.ts @@ -6,12 +6,15 @@ import { describe, it, expect, beforeEach } from "vitest"; import { selectRelaysForFilter } from "./relay-selection"; import { EventStore } from "applesauce-core"; import type { NostrEvent } from "nostr-tools"; +import relayListCache from "./relay-list-cache"; describe("selectRelaysForFilter", () => { let eventStore: EventStore; - beforeEach(() => { + beforeEach(async () => { eventStore = new EventStore(); + // Clear the relay list cache to ensure test isolation + await relayListCache.clear(); }); describe("fallback behavior", () => { diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..eba54c2 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,7 @@ +/** + * Vitest setup file + * + * Polyfills IndexedDB for Node.js test environment. + * This allows Dexie to work in tests without a browser. + */ +import "fake-indexeddb/auto"; diff --git a/vitest.config.ts b/vitest.config.ts index 2b29d01..3674afd 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ test: { globals: true, environment: "node", + setupFiles: ["./src/test/setup.ts"], }, resolve: { alias: {