Async API Design Analysis for @photostructure/sqlite
Executive Summary
This document analyzes options for adding asynchronous API support to the @photostructure/sqlite library, which currently provides a synchronous SQLite interface matching Node.js's built-in sqlite module. After careful analysis, we recommend creating a separate package for the async API rather than integrating it into the existing library.
Current State
What We Have
The @photostructure/sqlite library provides:
Synchronous API matching Node.js's experimental SQLite module exactly
DatabaseSync and StatementSync classes for blocking operations
Native C++ implementation using N-API/node-addon-api
Full SQLite functionality including user-defined functions, aggregates, and more
Cross-platform support with prebuilds for major platforms
Technical Foundation
The library is built on:
SQLite amalgamation (sqlite3.c) compiled directly into the addon
Node-addon-api for C++ to JavaScript bindings
Synchronous execution model where all operations block the JavaScript thread
The Challenge
SQLite's C API is fundamentally synchronous. Operations like sqlite3_step(), sqlite3_exec(), and sqlite3_prepare() block until completion. To provide an async API, we need to:
Move SQLite operations to worker threads
Manage callbacks/promises for result delivery
Handle concurrent access safely
Maintain proper connection lifecycle
Design Options Analysis
Option 1: Integrated Async API in Existing Library
Add async classes alongside sync classes in the same package:
// Same package exports both APIs export { DatabaseSync, StatementSync } from"./sync"; export { Database, Statement } from"./async";
Pros:
Single package installation
Shared C++ compilation and SQLite binary
Code reuse for common functionality
Easier migration between sync/async
Cons:
API confusion (which to use when?)
Complex TypeScript definitions
Risk of breaking sync API when adding async
Larger package size for all users
Testing complexity increases significantly
Option 2: Separate Async Package (Recommended)
Create a new package @photostructure/sqlite-async:
Statement objects must track their parent Database
2. Error Handling
Errors in Execute() are automatically converted to promise rejections
SQLite error messages must be copied (not referenced)
Need to handle both SQLite errors and system errors
3. Performance
Worker thread overhead vs blocking main thread
Connection pool tuning
Consider prepared statement caching
4. Compatibility
Support same SQLite compile options
Match Node.js sqlite error behaviors
Provide migration guide from sync API
Testing Strategy
Port existing tests - Adapt sync tests to async
Concurrency tests - Verify thread safety
Performance benchmarks - Compare with sync API
Stress tests - Connection pool limits
Integration tests - Real-world usage patterns
Migration Guide (Future)
For users moving from sync to async:
// Sync (current) constdb = newDatabaseSync(":memory:"); conststmt = db.prepare("SELECT * FROM users WHERE id = ?"); constuser = stmt.get(userId);
// Async (new) constdb = awaitDatabase.open(":memory:"); conststmt = awaitdb.prepare("SELECT * FROM users WHERE id = ?"); constuser = awaitstmt.get(userId);
Open Questions
Package naming: @photostructure/sqlite-async or @photostructure/async-sqlite?
API style: Mirror better-sqlite3's async API or create our own?
Default pool size: What's appropriate for typical use cases?
Transaction API: Provide high-level transaction helpers?
Conclusion
Creating a separate async package is the recommended approach. It provides the clearest path forward, aligns with Node.js's design philosophy, and minimizes risk to existing users. The AsyncWorker pattern from node-addon-api provides a solid foundation for implementation.