# sql.js Implementation Guide for Claude-Flow **Implementation Date**: 2026-01-03 **Target Version**: Claude-Flow v3.0 **Author**: Research Agent --- ## Overview This guide provides step-by-step implementation instructions for integrating sql.js as a cross-platform SQLite provider alongside better-sqlite3. --- ## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────┐ │ Application Layer │ │ (DatabaseService, MemoryManager, PersistenceManager) │ └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ DatabaseManager / sqlite-wrapper │ │ (Provider Selection & Abstraction) │ └─────────────┬───────────────┬───────────────┬───────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ SQLiteProvider│ │SqlJsProvider│ │ JSONProvider │ │(better-sqlite3)│ │ (sql.js) │ │ (Fallback) │ └─────────────┘ └─────────────┘ └─────────────┘ (Linux/macOS) (Windows/All) (Ultimate FB) ``` --- ## File Modifications Plan ### New Files 1. **`src/memory/backends/sqljs.ts`** - sql.js backend implementation 2. **`src/memory/providers/sqljs-provider.ts`** - Provider wrapper 3. **`src/utils/sqljs-loader.ts`** - WASM loader utility 4. **`tests/unit/memory/sqljs-backend.test.ts`** - Unit tests 5. **`tests/integration/sqljs-integration.test.ts`** - Integration tests ### Modified Files 1. **`src/memory/sqlite-wrapper.js`** - Add sql.js detection and wrapper 2. **`src/core/DatabaseManager.ts`** - Add SqlJsProvider support 3. **`package.json`** - Add sql.js dependency 4. **`tsconfig.json`** - Configure WASM file handling 5. **`.swcrc`** - Update build configuration --- ## Step-by-Step Implementation ### Step 1: Install sql.js Dependency **File**: `/home/user/claude-flow/package.json` ```json { "dependencies": { "sql.js": "^1.13.0", "existing-deps": "..." }, "optionalDependencies": { "better-sqlite3": "^12.2.0", "agentdb": "^1.6.1" } } ``` **Command**: ```bash npm install sql.js@^1.13.0 --save ``` --- ### Step 2: Create sql.js Backend Implementation **File**: `/home/user/claude-flow/src/memory/backends/sqljs.ts` ```typescript /** * sql.js Backend Implementation * Cross-platform SQLite backend using WebAssembly */ import initSqlJs, { Database as SqlJsDatabase } from 'sql.js'; import { promises as fs } from 'fs'; import path from 'path'; import type { IMemoryBackend } from './base.js'; import type { MemoryEntry, MemoryQuery } from '../../utils/types.js'; import type { ILogger } from '../../core/logger.js'; import { MemoryBackendError } from '../../utils/errors.js'; /** * Configuration for sql.js backend */ export interface SqlJsBackendConfig { dbPath: string; autoSave?: boolean; saveInterval?: number; wasmPath?: string; } /** * sql.js-based memory backend with file persistence */ export class SqlJsBackend implements IMemoryBackend { private SQL: any; private db?: SqlJsDatabase; private dbPath: string; private dirty: boolean = false; private saveTimer?: NodeJS.Timeout; private autoSave: boolean; private saveInterval: number; constructor( config: SqlJsBackendConfig, private logger: ILogger ) { this.dbPath = config.dbPath; this.autoSave = config.autoSave ?? true; this.saveInterval = config.saveInterval ?? 30000; // 30 seconds default } async initialize(): Promise { this.logger.info('Initializing sql.js backend', { dbPath: this.dbPath, autoSave: this.autoSave }); try { // Load SQL.js WASM module this.SQL = await initSqlJs({ locateFile: (file) => { // Try multiple paths for WASM file const paths = [ path.join(__dirname, '../../../node_modules/sql.js/dist', file), path.join(process.cwd(), 'node_modules/sql.js/dist', file), path.join(__dirname, '../../..', 'node_modules/sql.js/dist', file) ]; for (const testPath of paths) { try { if (require('fs').existsSync(testPath)) { return testPath; } } catch {} } // Fallback to default return file; } }); // Load existing database or create new one const dir = path.dirname(this.dbPath); await fs.mkdir(dir, { recursive: true }); if (await this.fileExists(this.dbPath)) { await this.loadFromFile(); } else { this.db = new this.SQL.Database(); this.logger.info('Created new sql.js database'); } // Create schema this.createTables(); this.createIndexes(); // Setup auto-save if enabled if (this.autoSave) { this.setupAutoSave(); } this.logger.info('sql.js backend initialized successfully'); } catch (error) { throw new MemoryBackendError('Failed to initialize sql.js backend', { error }); } } private async fileExists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } } private async loadFromFile(): Promise { try { const buffer = await fs.readFile(this.dbPath); this.db = new this.SQL.Database(new Uint8Array(buffer)); this.logger.info('Loaded existing database from file', { dbPath: this.dbPath, size: buffer.length }); } catch (error) { this.logger.warn('Failed to load database file, creating new', { error }); this.db = new this.SQL.Database(); } } private createTables(): void { const sql = ` CREATE TABLE IF NOT EXISTS memory_entries ( id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, session_id TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, context TEXT NOT NULL, timestamp TEXT NOT NULL, tags TEXT NOT NULL, version INTEGER NOT NULL, parent_id TEXT, metadata TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP ) `; try { this.db!.run(sql); } catch (error) { throw new MemoryBackendError('Failed to create tables', { error }); } } private createIndexes(): void { const indexes = [ 'CREATE INDEX IF NOT EXISTS idx_agent_id ON memory_entries(agent_id)', 'CREATE INDEX IF NOT EXISTS idx_session_id ON memory_entries(session_id)', 'CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type)', 'CREATE INDEX IF NOT EXISTS idx_timestamp ON memory_entries(timestamp)', 'CREATE INDEX IF NOT EXISTS idx_parent_id ON memory_entries(parent_id)', ]; for (const sql of indexes) { try { this.db!.run(sql); } catch (error) { this.logger.warn('Failed to create index', { sql, error }); } } } private setupAutoSave(): void { this.logger.debug('Setting up auto-save', { interval: this.saveInterval }); } private markDirty(): void { this.dirty = true; if (this.autoSave) { if (this.saveTimer) clearTimeout(this.saveTimer); this.saveTimer = setTimeout(() => { this.saveToDisk().catch(err => { this.logger.error('Auto-save failed', err); }); }, this.saveInterval); } } private async saveToDisk(): Promise { if (!this.dirty || !this.db) return; try { const data = this.db.export(); await fs.writeFile(this.dbPath, data); this.dirty = false; this.logger.debug('Database saved to disk', { dbPath: this.dbPath, size: data.length }); } catch (error) { this.logger.error('Failed to save database', { error }); throw new MemoryBackendError('Failed to save database', { error }); } } async store(entry: MemoryEntry): Promise { if (!this.db) { throw new MemoryBackendError('Database not initialized'); } const sql = ` INSERT OR REPLACE INTO memory_entries ( id, agent_id, session_id, type, content, context, timestamp, tags, version, parent_id, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; const params = [ entry.id, entry.agentId, entry.sessionId, entry.type, entry.content, JSON.stringify(entry.context), entry.timestamp.toISOString(), JSON.stringify(entry.tags), entry.version, entry.parentId || null, entry.metadata ? JSON.stringify(entry.metadata) : null, ]; try { this.db.run(sql, params); this.markDirty(); } catch (error) { throw new MemoryBackendError('Failed to store entry', { error, entryId: entry.id }); } } async retrieve(id: string): Promise { if (!this.db) { throw new MemoryBackendError('Database not initialized'); } const sql = 'SELECT * FROM memory_entries WHERE id = ?'; try { const result = this.db.exec(sql, [id]); if (result.length === 0 || result[0].values.length === 0) { return undefined; } return this.rowToEntry(result[0]); } catch (error) { throw new MemoryBackendError('Failed to retrieve entry', { error, entryId: id }); } } async update(id: string, entry: MemoryEntry): Promise { // sql.js INSERT OR REPLACE handles updates await this.store(entry); } async delete(id: string): Promise { if (!this.db) { throw new MemoryBackendError('Database not initialized'); } const sql = 'DELETE FROM memory_entries WHERE id = ?'; try { this.db.run(sql, [id]); this.markDirty(); } catch (error) { throw new MemoryBackendError('Failed to delete entry', { error, entryId: id }); } } async query(query: MemoryQuery): Promise { if (!this.db) { throw new MemoryBackendError('Database not initialized'); } const conditions: string[] = []; const params: unknown[] = []; if (query.agentId) { conditions.push('agent_id = ?'); params.push(query.agentId); } if (query.sessionId) { conditions.push('session_id = ?'); params.push(query.sessionId); } if (query.type) { conditions.push('type = ?'); params.push(query.type); } if (query.startTime) { conditions.push('timestamp >= ?'); params.push(query.startTime.toISOString()); } if (query.endTime) { conditions.push('timestamp <= ?'); params.push(query.endTime.toISOString()); } if (query.search) { conditions.push('(content LIKE ? OR tags LIKE ?)'); params.push(`%${query.search}%`, `%${query.search}%`); } if (query.tags && query.tags.length > 0) { const tagConditions = query.tags.map(() => 'tags LIKE ?'); conditions.push(`(${tagConditions.join(' OR ')})`); query.tags.forEach((tag: string) => params.push(`%"${tag}"%`)); } let sql = 'SELECT * FROM memory_entries'; if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); } sql += ' ORDER BY timestamp DESC'; if (query.limit) { sql += ' LIMIT ?'; params.push(query.limit); } if (query.offset) { sql += ' OFFSET ?'; params.push(query.offset); } try { const result = this.db.exec(sql, params); if (result.length === 0) { return []; } return this.rowsToEntries(result[0]); } catch (error) { throw new MemoryBackendError('Failed to query entries', { error, query }); } } async getAllEntries(): Promise { if (!this.db) { throw new MemoryBackendError('Database not initialized'); } const sql = 'SELECT * FROM memory_entries ORDER BY timestamp DESC'; try { const result = this.db.exec(sql); if (result.length === 0) { return []; } return this.rowsToEntries(result[0]); } catch (error) { throw new MemoryBackendError('Failed to get all entries', { error }); } } async getHealthStatus(): Promise<{ healthy: boolean; error?: string; metrics?: Record; }> { if (!this.db) { return { healthy: false, error: 'Database not initialized', }; } try { // Test query this.db.exec('SELECT 1'); // Get metrics const countResult = this.db.exec('SELECT COUNT(*) as count FROM memory_entries'); const entryCount = countResult[0]?.values[0]?.[0] || 0; const data = this.db.export(); const dbSize = data.length; return { healthy: true, metrics: { entryCount: Number(entryCount), dbSizeBytes: dbSize, }, }; } catch (error) { return { healthy: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } async shutdown(): Promise { this.logger.info('Shutting down sql.js backend'); if (this.saveTimer) { clearTimeout(this.saveTimer); } // Final save if (this.dirty) { await this.saveToDisk(); } if (this.db) { this.db.close(); delete this.db; } } private rowToEntry(result: any): MemoryEntry { const columns = result.columns; const values = result.values[0]; const row: Record = {}; columns.forEach((col: string, i: number) => { row[col] = values[i]; }); return this.mapRowToEntry(row); } private rowsToEntries(result: any): MemoryEntry[] { const columns = result.columns; const values = result.values; return values.map((rowValues: any[]) => { const row: Record = {}; columns.forEach((col: string, i: number) => { row[col] = rowValues[i]; }); return this.mapRowToEntry(row); }); } private mapRowToEntry(row: Record): MemoryEntry { const entry: MemoryEntry = { id: row.id as string, agentId: row.agent_id as string, sessionId: row.session_id as string, type: row.type as MemoryEntry['type'], content: row.content as string, context: JSON.parse(row.context as string), timestamp: new Date(row.timestamp as string), tags: JSON.parse(row.tags as string), version: row.version as number, }; if (row.parent_id) { entry.parentId = row.parent_id as string; } if (row.metadata) { entry.metadata = JSON.parse(row.metadata as string); } return entry; } /** * Force immediate save to disk */ async flush(): Promise { await this.saveToDisk(); } /** * Export database as binary */ export(): Uint8Array { if (!this.db) { throw new MemoryBackendError('Database not initialized'); } return this.db.export(); } /** * Get database size in bytes */ getSize(): number { if (!this.db) return 0; return this.db.export().length; } } ``` --- ### Step 3: Update sqlite-wrapper.js **File**: `/home/user/claude-flow/src/memory/sqlite-wrapper.js` Add sql.js detection and provider creation: ```javascript // Add after existing imports let SqlJs = null; let sqlJsAvailable = false; let sqlJsLoadError = null; /** * Try to load sql.js */ async function tryLoadSqlJs() { try { const module = await import('sql.js'); SqlJs = module.default; sqlJsAvailable = true; console.log('✅ sql.js loaded successfully (cross-platform SQLite mode)'); return true; } catch (error) { sqlJsLoadError = error; console.warn('⚠️ sql.js not available:', error.message); return false; } } /** * Check if sql.js is available */ export async function isSqlJsAvailable() { if (sqlJsAvailable !== null) { return sqlJsAvailable; } await tryLoadSqlJs(); return sqlJsAvailable; } /** * Get sql.js constructor */ export async function getSqlJs() { if (!sqlJsAvailable && sqlJsLoadError === null) { await tryLoadSqlJs(); } return SqlJs; } /** * Create database with automatic provider selection */ export async function createDatabase(dbPath, options = {}) { const preferredProvider = options.provider || getRecommendedProvider(); // Try preferred provider first if (preferredProvider === 'better-sqlite3' && await isSQLiteAvailable()) { return createBetterSqlite3Database(dbPath); } if (preferredProvider === 'sql.js' && await isSqlJsAvailable()) { return createSqlJsDatabase(dbPath); } // Fallback chain if (await isSQLiteAvailable()) { return createBetterSqlite3Database(dbPath); } if (await isSqlJsAvailable()) { return createSqlJsDatabase(dbPath); } throw new Error('No SQLite provider available (tried better-sqlite3 and sql.js)'); } /** * Create sql.js database instance */ async function createSqlJsDatabase(dbPath) { const SQL = await getSqlJs(); if (!SQL) { throw new Error('sql.js is not available'); } // Initialize SQL.js const initSql = await SQL({ locateFile: file => { const path = require('path'); return path.join(process.cwd(), 'node_modules/sql.js/dist', file); } }); // Load or create database const fs = require('fs'); let db; if (fs.existsSync(dbPath)) { const buffer = fs.readFileSync(dbPath); db = new initSql.Database(new Uint8Array(buffer)); } else { db = new initSql.Database(); } // Wrap to match better-sqlite3 API return wrapSqlJsDatabase(db, dbPath); } /** * Wrap sql.js database to match better-sqlite3 API */ function wrapSqlJsDatabase(db, dbPath) { let dirty = false; let saveTimer = null; const markDirty = () => { dirty = true; if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(saveToDisk, 5000); }; const saveToDisk = () => { if (!dirty) return; const fs = require('fs'); const data = db.export(); fs.writeFileSync(dbPath, data); dirty = false; }; return { // Mimic better-sqlite3 API prepare: (sql) => { const stmt = db.prepare(sql); return { run: (...params) => { stmt.bind(params); stmt.step(); stmt.reset(); markDirty(); return { changes: 1 }; // Simplified }, get: (...params) => { stmt.bind(params); const result = stmt.step() ? stmt.getAsObject() : null; stmt.reset(); return result; }, all: (...params) => { stmt.bind(params); const results = []; while (stmt.step()) { results.push(stmt.getAsObject()); } stmt.reset(); return results; } }; }, exec: (sql) => { db.run(sql); markDirty(); }, pragma: (pragma) => { // sql.js doesn't support pragma in the same way // Silently ignore for compatibility }, close: () => { if (saveTimer) clearTimeout(saveTimer); saveToDisk(); db.close(); }, // sql.js specific methods export: () => db.export(), _sqljs: true // Marker for detection }; } /** * Get recommended provider based on platform */ function getRecommendedProvider() { if (process.platform === 'win32') { return 'sql.js'; // Windows: prefer sql.js (no compilation) } if (process.platform === 'darwin' && process.arch === 'arm64') { return 'better-sqlite3'; // macOS ARM: try native first } return 'better-sqlite3'; // Linux/Unix: native is usually best } // Pre-load both providers on module import (async () => { await tryLoadSQLite(); await tryLoadSqlJs(); })(); export default { isSQLiteAvailable, isSqlJsAvailable, getSQLiteDatabase, getSqlJs, createDatabase, isWindows, getStorageRecommendations }; ``` --- ### Step 4: Update DatabaseManager.ts **File**: `/home/user/claude-flow/src/core/DatabaseManager.ts` Add SqlJsProvider option: ```typescript // Add import import { SqlJsBackend } from '../memory/backends/sqljs.js'; export class DatabaseManager implements IDatabaseProvider { // ... existing code ... private async initializeSQLiteWithRecovery(): Promise { try { // Try better-sqlite3 first return new SQLiteProvider(this.dbPath); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); // Check for native module issues if (error instanceof NativeModuleError || isNativeModuleVersionError(error)) { console.warn('\n' + (error instanceof NativeModuleError ? error.message : getNativeModuleRecoveryMessage(error))); console.warn(' Attempting fallback to sql.js (cross-platform SQLite)...\n'); // Try sql.js before JSON try { return new SqlJsProvider(this.dbPath, this.logger); } catch (sqlJsError) { console.warn(' sql.js not available, falling back to JSON storage.\n'); } } // Final fallback to JSON this.provider = new JSONProvider(this.dbPath.replace('.sqlite', '.json')); this.dbType = 'json'; return this.provider; } } } /** * sql.js Provider implementation */ class SqlJsProvider implements IDatabaseProvider { private backend: SqlJsBackend; private dbPath: string; constructor(dbPath: string, logger: ILogger) { this.dbPath = dbPath; this.backend = new SqlJsBackend( { dbPath, autoSave: true, saveInterval: 30000 }, logger ); } async initialize(): Promise { await this.backend.initialize(); } async store(key: string, value: any, namespace: string = 'default'): Promise { // Adapt to MemoryEntry format const entry = { id: `${namespace}:${key}`, agentId: namespace, sessionId: 'default', type: 'data' as const, content: typeof value === 'string' ? value : JSON.stringify(value), context: { namespace }, timestamp: new Date(), tags: [], version: 1 }; await this.backend.store(entry); } async retrieve(key: string, namespace: string = 'default'): Promise { const entry = await this.backend.retrieve(`${namespace}:${key}`); if (!entry) return null; try { return JSON.parse(entry.content); } catch { return entry.content; } } async delete(key: string, namespace: string = 'default'): Promise { try { await this.backend.delete(`${namespace}:${key}`); return true; } catch { return false; } } async list(namespace: string = 'default'): Promise { const entries = await this.backend.query({ agentId: namespace }); return entries.map(e => e.id.replace(`${namespace}:`, '')); } async close(): Promise { await this.backend.shutdown(); } } ``` --- ### Step 5: Update Build Configuration **File**: `/.swcrc` ```json { "jsc": { "parser": { "syntax": "typescript", "tsx": false, "decorators": true, "dynamicImport": true }, "target": "es2020", "externalHelpers": false, "keepClassNames": true }, "module": { "type": "es6", "strict": false, "strictMode": true, "lazy": false, "noInterop": false }, "copyFiles": { "enable": true, "files": { "**/*.wasm": true } } } ``` **File**: `/home/user/claude-flow/package.json` (files section) ```json { "files": [ "dist/", "src/", "node_modules/sql.js/dist/sql-wasm.wasm", "node_modules/sql.js/dist/sql-wasm.js" ] } ``` --- ### Step 6: Add Tests **File**: `/home/user/claude-flow/tests/unit/memory/sqljs-backend.test.ts` ```typescript import { SqlJsBackend } from '../../../src/memory/backends/sqljs'; import { MemoryEntry } from '../../../src/utils/types'; import { createMockLogger } from '../../helpers/logger'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as os from 'os'; describe('SqlJsBackend', () => { let backend: SqlJsBackend; let dbPath: string; let logger: any; beforeEach(async () => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sqljs-test-')); dbPath = path.join(tmpDir, 'test.db'); logger = createMockLogger(); backend = new SqlJsBackend( { dbPath, autoSave: false // Disable for tests }, logger ); await backend.initialize(); }); afterEach(async () => { await backend.shutdown(); await fs.remove(path.dirname(dbPath)); }); describe('store and retrieve', () => { it('should store and retrieve an entry', async () => { const entry: MemoryEntry = { id: 'test-1', agentId: 'agent-1', sessionId: 'session-1', type: 'observation', content: 'Test content', context: { key: 'value' }, timestamp: new Date(), tags: ['test', 'demo'], version: 1 }; await backend.store(entry); const retrieved = await backend.retrieve('test-1'); expect(retrieved).toBeDefined(); expect(retrieved?.id).toBe('test-1'); expect(retrieved?.content).toBe('Test content'); expect(retrieved?.tags).toEqual(['test', 'demo']); }); it('should return undefined for non-existent entry', async () => { const retrieved = await backend.retrieve('non-existent'); expect(retrieved).toBeUndefined(); }); it('should update existing entry', async () => { const entry: MemoryEntry = { id: 'test-1', agentId: 'agent-1', sessionId: 'session-1', type: 'observation', content: 'Original content', context: {}, timestamp: new Date(), tags: [], version: 1 }; await backend.store(entry); const updated = { ...entry, content: 'Updated content' }; await backend.update('test-1', updated); const retrieved = await backend.retrieve('test-1'); expect(retrieved?.content).toBe('Updated content'); }); }); describe('query', () => { beforeEach(async () => { const entries: MemoryEntry[] = [ { id: 'entry-1', agentId: 'agent-1', sessionId: 'session-1', type: 'observation', content: 'Content 1', context: {}, timestamp: new Date('2026-01-01'), tags: ['tag1'], version: 1 }, { id: 'entry-2', agentId: 'agent-1', sessionId: 'session-1', type: 'action', content: 'Content 2', context: {}, timestamp: new Date('2026-01-02'), tags: ['tag2'], version: 1 }, { id: 'entry-3', agentId: 'agent-2', sessionId: 'session-2', type: 'observation', content: 'Content 3', context: {}, timestamp: new Date('2026-01-03'), tags: ['tag1'], version: 1 } ]; for (const entry of entries) { await backend.store(entry); } }); it('should query by agentId', async () => { const results = await backend.query({ agentId: 'agent-1' }); expect(results).toHaveLength(2); expect(results[0].agentId).toBe('agent-1'); }); it('should query by type', async () => { const results = await backend.query({ type: 'observation' }); expect(results).toHaveLength(2); expect(results.every(r => r.type === 'observation')).toBe(true); }); it('should support pagination', async () => { const results = await backend.query({ limit: 2, offset: 1 }); expect(results).toHaveLength(2); }); }); describe('file persistence', () => { it('should save to disk and reload', async () => { const entry: MemoryEntry = { id: 'persist-test', agentId: 'agent-1', sessionId: 'session-1', type: 'observation', content: 'Persistent data', context: {}, timestamp: new Date(), tags: [], version: 1 }; await backend.store(entry); await backend.flush(); // Force save await backend.shutdown(); // Create new instance with same path const backend2 = new SqlJsBackend({ dbPath }, logger); await backend2.initialize(); const retrieved = await backend2.retrieve('persist-test'); expect(retrieved?.content).toBe('Persistent data'); await backend2.shutdown(); }); }); describe('health status', () => { it('should return healthy status', async () => { const health = await backend.getHealthStatus(); expect(health.healthy).toBe(true); expect(health.metrics).toBeDefined(); expect(health.metrics?.entryCount).toBeGreaterThanOrEqual(0); }); }); }); ``` --- ## Testing Checklist ### Unit Tests - [ ] SqlJsBackend store/retrieve - [ ] SqlJsBackend query operations - [ ] File persistence - [ ] WASM loading - [ ] Error handling ### Integration Tests - [ ] Provider selection logic - [ ] Fallback chain (better-sqlite3 → sql.js → JSON) - [ ] Database migration between providers - [ ] Concurrent access ### Platform Tests - [ ] Windows 10/11 (npm install, runtime) - [ ] macOS (Intel and ARM64) - [ ] Linux (Ubuntu, Alpine) - [ ] WSL (Windows Subsystem for Linux) ### Performance Tests - [ ] Benchmark sql.js vs better-sqlite3 - [ ] Memory usage comparison - [ ] Large dataset handling (100K+ records) - [ ] Bulk insert performance --- ## Deployment Checklist ### Pre-release - [ ] Update CHANGELOG.md with sql.js integration - [ ] Update README.md with Windows installation instructions - [ ] Create migration guide for existing users - [ ] Add troubleshooting section ### Release - [ ] Publish to npm with `@alpha` tag for testing - [ ] Test in real Windows environment - [ ] Monitor issue tracker for reports - [ ] Collect performance feedback ### Post-release - [ ] Update documentation based on feedback - [ ] Optimize performance bottlenecks - [ ] Consider sql.js as default on all platforms (if successful) --- ## Troubleshooting Guide ### WASM File Not Found **Error**: `Cannot find sql-wasm.wasm` **Solution**: ```javascript // Update locateFile in SqlJsBackend locateFile: (file) => { if (process.env.SQLJS_WASM_PATH) { return path.join(process.env.SQLJS_WASM_PATH, file); } return path.join(__dirname, '../../../node_modules/sql.js/dist', file); } ``` ### Performance Issues **Symptom**: Slow queries (>100ms) **Solutions**: 1. Enable prepared statements caching 2. Use batch transactions 3. Reduce auto-save frequency 4. Consider switching to better-sqlite3 if available ### Memory Leaks **Symptom**: Increasing memory usage **Solutions**: 1. Call `stmt.free()` after prepared statements 2. Limit result set sizes 3. Implement periodic `db.export()` and recreate 4. Monitor with `process.memoryUsage()` --- ## Next Steps 1. Implement SqlJsBackend (Step 2) 2. Update sqlite-wrapper.js (Step 3) 3. Run unit tests 4. Test on Windows 5. Collect feedback 6. Iterate and optimize --- **Document Version**: 1.0 **Last Updated**: 2026-01-03 **Status**: Ready for implementation