From a86b783f587c6e18248edcaf7bdc3c39ab36dee8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 12:11:33 +0000 Subject: [PATCH 1/2] docs: add plan for pluggable storage engine Analyze test failures due to missing IndexedDB in Node environment. Present two options: fake-indexeddb polyfill vs full storage abstraction. Recommend fake-indexeddb as pragmatic solution. --- PLAN.md | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..3bb8edf --- /dev/null +++ b/PLAN.md @@ -0,0 +1,200 @@ +# 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. From 729c83011e0909627416d7baef72316a76298f1c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 12:18:38 +0000 Subject: [PATCH 2/2] fix: add IndexedDB polyfill for test environment Use fake-indexeddb to provide IndexedDB API in Node.js test environment. This fixes 10 failing tests in spellbook-storage.test.ts that were previously blocked by missing IndexedDB. Changes: - Add fake-indexeddb as dev dependency - Create vitest setup file that imports the polyfill - Update vitest.config.ts to use setup file - Fix relay-selection.test.ts to clear cache between tests for isolation --- PLAN.md | 200 --------------------------- package-lock.json | 11 ++ package.json | 1 + src/services/relay-selection.test.ts | 5 +- src/test/setup.ts | 7 + vitest.config.ts | 1 + 6 files changed, 24 insertions(+), 201 deletions(-) delete mode 100644 PLAN.md create mode 100644 src/test/setup.ts 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: {