@comic-vine/sqlite-store
Version:
SQLite store implementations for Comic Vine client caching, deduplication, and rate limiting using Drizzle ORM
658 lines (506 loc) • 18.2 kB
Markdown
# @comic-vine/sqlite-store
SQLite store implementations for Comic Vine client caching, deduplication, and rate limiting using Drizzle ORM.
## Installation
```bash
npm install @comic-vine/sqlite-store @comic-vine/client
```
## Usage
```typescript
import Database from 'better-sqlite3';
import ComicVine from '@comic-vine/client';
import {
SQLiteCacheStore,
SQLiteDedupeStore,
SQLiteRateLimitStore,
SqliteAdaptiveRateLimitStore, // NEW: Intelligent persistent rate limiting
} from '@comic-vine/sqlite-store';
// Create ONE shared connection which every store will use.
const sharedDb = new Database('./comic-vine.db');
const client = new ComicVine({
apiKey: 'your-api-key',
stores: {
cache: new SQLiteCacheStore({ database: sharedDb }),
dedupe: new SQLiteDedupeStore({ database: sharedDb }),
rateLimit: new SqliteAdaptiveRateLimitStore({ database: sharedDb }), // Recommended
},
});
// Use client normally - data persists with intelligent rate limiting
const issue = await client.issue.retrieve(1, { priority: 'user' });
```
## Key Features
- **Shared Database Support**: All stores can use the same SQLite database file/connection
- **Object-based Configuration**: Clean, type-safe constructor parameters
- **Connection Management**: Automatic handling of database connection lifecycle
- **Type Safety**: Full TypeScript support with exported option interfaces
## Store Implementations
### SQLiteCacheStore
Provides persistent caching with TTL support using SQLite database.
```typescript
import type { SQLiteCacheStoreOptions } from '@comic-vine/sqlite-store';
const cacheStore = new SQLiteCacheStore({
database: './cache.db', // File path, ':memory:', or Database instance
cleanupIntervalMs: 60_000, // Cleanup expired items every minute (default)
maxEntrySizeBytes: 5 * 1024 * 1024, // 5MB max per entry (default)
});
```
**Features:**
- Persistent storage across application restarts
- TTL-based expiration with automatic cleanup
- Database-level ACID transactions
- Efficient indexing for fast lookups
- Automatic schema creation and migration
- Size limits to prevent SQLite length violations
- Shared database connection support
### SQLiteDedupeStore
Prevents duplicate concurrent requests using SQLite for coordination.
```typescript
import type { SQLiteDedupeStoreOptions } from '@comic-vine/sqlite-store';
const dedupeStore = new SQLiteDedupeStore({
database: './dedupe.db', // File path, ':memory:', or Database instance
jobTimeoutMs: 300_000, // 5 minute timeout for jobs (default)
cleanupIntervalMs: 60_000, // Cleanup interval (default: 1 minute)
});
```
**Features:**
- Cross-process deduplication support
- Persistent job state tracking
- Automatic cleanup of expired jobs
- Promise-based waiting with database coordination
- Error state persistence and recovery
- Configurable job timeouts and cleanup intervals
### SQLiteRateLimitStore
Implements sliding window rate limiting with SQLite persistence.
```typescript
import type { SQLiteRateLimitStoreOptions } from '@comic-vine/sqlite-store';
const rateLimitStore = new SQLiteRateLimitStore({
database: './rate-limits.db', // File path, ':memory:', or Database instance
defaultConfig: { limit: 100, windowMs: 60_000 }, // Default: 100 req/min
resourceConfigs: new Map([
['issues', { limit: 50, windowMs: 60_000 }], // Custom per-resource limits
['characters', { limit: 200, windowMs: 60_000 }],
]),
});
```
**Features:**
- Persistent rate limit tracking
- Per-resource configuration
- Sliding window algorithm
- Cross-process rate limit enforcement
- Automatic cleanup of expired records
- Flexible default and per-resource configurations
### SqliteAdaptiveRateLimitStore (Recommended)
Advanced rate limiting with intelligent priority-based capacity allocation that persists across application restarts and supports cross-process coordination.
```typescript
import type { SqliteAdaptiveRateLimitStoreOptions } from '@comic-vine/sqlite-store';
const adaptiveStore = new SqliteAdaptiveRateLimitStore({
database: './comic-vine.db', // Shared database for all stores
adaptiveConfig: {
// Activity detection
highActivityThreshold: 10, // Requests/15min to trigger priority mode
moderateActivityThreshold: 3, // Requests/15min for moderate activity
// Timing behavior
monitoringWindowMs: 15 * 60 * 1000, // 15 minutes monitoring window
sustainedInactivityThresholdMs: 30 * 60 * 1000, // 30min for full background scale-up
recalculationIntervalMs: 30000, // Recalculate every 30 seconds
// Capacity limits
maxUserScaling: 2.0, // Maximum user capacity multiplier
minUserReserved: 5, // Minimum guaranteed user requests
backgroundPauseOnIncreasingTrend: true, // Pause background on user surge
},
});
```
**Unique SQLite Features:**
- **Cross-Process Coordination**: Multiple application instances share the same rate limiting state
- **Persistent Activity Tracking**: Rate limiting history survives application restarts
- **Recovery Support**: Handles application crashes gracefully with timeout recovery
- **Performance Optimized**: Hybrid approach with in-memory metrics and SQLite persistence
- **Migration Support**: Automatically upgrades database schema with priority column
**How SQLite Adaptive Rate Limiting Works:**
```typescript
// Process A (web server)
const webClient = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({ database: sharedDb }),
},
});
// Process B (background worker) - shares the same rate limiting state
const workerClient = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({ database: sharedDb }),
},
});
// Web server requests get priority during high activity
const userCharacter = await webClient.character.retrieve(1443, {
priority: 'user',
});
// Background worker respects the adaptive allocation
const backgroundVolumes = await workerClient.volume.list({
priority: 'background', // May be throttled if web server is active
});
```
**Persistence Benefits:**
```typescript
// Application restart scenario
const client = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({
database: './comic-vine.db',
}),
},
});
// Rate limiting history is preserved:
// - Recent activity patterns remain tracked
// - Capacity allocation continues from where it left off
// - No "cold start" penalty on rate limiting behavior
```
## How Stores Work Together
The SQLite stores provide persistent versions of the same request optimization flow:
### Request Processing Order
1. **Cache Store**: Checks SQLite cache table for existing response
2. **Dedupe Store**: Uses SQLite to coordinate concurrent requests across processes
3. **Rate Limit Store**: Enforces API limits using persistent SQLite tracking
```typescript
// Example: Cross-process request coordination
// Process A makes request
const issueA = await clientA.issue.retrieve(1); // Cache miss → dedupe register → rate limit → API call
// Process B (same or different process) makes same request concurrently
const issueB = await clientB.issue.retrieve(1); // Cache miss → dedupe wait → shares result from Process A
```
### Persistence Benefits
Unlike in-memory stores, SQLite stores survive application restarts:
- **Cache**: Cached responses remain available after restart
- **Dedupe**: In-progress requests survive process crashes (with timeout recovery)
- **Rate Limit**: Rate limit history persists, preventing limit bypassing via restart
### Cross-Process Coordination
SQLite stores enable multiple processes to share the same optimization state:
```typescript
// Multiple processes can safely share rate limits and cache
const sharedDb = new Database('./shared-stores.db');
// Process 1
const client1 = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SQLiteRateLimitStore({ database: sharedDb }),
},
});
// Process 2 - shares the same rate limits
const client2 = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SQLiteRateLimitStore({ database: sharedDb }),
},
});
```
## Shared Database Configuration
**Recommended**: Use a single database for all stores to reduce file handles and improve performance:
```typescript
import Database from 'better-sqlite3';
// Single database approach (recommended)
const db = new Database('./comic-vine-stores.db');
const stores = {
cache: new SQLiteCacheStore({ database: db }),
dedupe: new SQLiteDedupeStore({ database: db }),
rateLimit: new SqliteAdaptiveRateLimitStore({ database: db }), // Adaptive rate limiting
};
// The stores automatically create their own tables within the shared database
```
**Cross-Process Setup (Multiple Application Instances):**
```typescript
// All processes share the same database file for coordination
const sharedDbPath = './shared-comic-vine.db';
// Process 1: Web Server
const webClient = new ComicVine({
apiKey: 'your-api-key',
stores: {
cache: new SQLiteCacheStore({ database: sharedDbPath }),
rateLimit: new SqliteAdaptiveRateLimitStore({ database: sharedDbPath }),
},
});
// Process 2: Background Worker
const workerClient = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({ database: sharedDbPath }),
},
});
// Both processes coordinate through the shared SQLite database
```
**Alternative**: Separate databases per store:
```typescript
// Separate database files (alternative approach)
const stores = {
cache: new SQLiteCacheStore({ database: './cache.db' }),
dedupe: new SQLiteDedupeStore({ database: './dedupe.db' }),
rateLimit: new SQLiteRateLimitStore({ database: './rate-limits.db' }),
};
```
## Database Schema
The SQLite stores use the following schema:
### Cache Table
```sql
CREATE TABLE cache (
hash TEXT PRIMARY KEY,
value BLOB NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
```
### Dedupe Jobs Table
```sql
CREATE TABLE dedupe_jobs (
hash TEXT PRIMARY KEY,
job_id TEXT NOT NULL,
status TEXT NOT NULL, -- 'pending', 'completed', 'failed'
result BLOB,
error TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### Rate Limits Table
**Enhanced Schema (Adaptive Rate Limiting):**
```sql
CREATE TABLE rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resource TEXT NOT NULL,
timestamp INTEGER NOT NULL,
priority TEXT NOT NULL DEFAULT 'background' -- NEW: Priority tracking
);
-- Optimized indexes for adaptive rate limiting
CREATE INDEX IF NOT EXISTS idx_rate_limit_resource ON rate_limits(resource);
CREATE INDEX IF NOT EXISTS idx_rate_limit_timestamp ON rate_limits(timestamp);
CREATE INDEX IF NOT EXISTS idx_rate_limit_resource_priority_timestamp
ON rate_limits(resource, priority, timestamp);
```
**Legacy Schema (Traditional Rate Limiting):**
```sql
CREATE TABLE rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
resource TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
```
The adaptive rate limiting store automatically migrates existing databases by adding the `priority` column with a default value of `'background'`, ensuring backward compatibility with existing rate limiting data.
## Configuration
### Database Paths
- Use file paths for persistent storage: `'./my-cache.db'`
- Use `':memory:'` for in-memory databases (faster, but not persistent)
- Relative paths are resolved from the current working directory
- Pass existing `Database` instances for connection sharing
### Performance Tuning
```typescript
// For high-throughput applications
const sharedDb = new Database('./data/comic-vine.db');
const stores = {
cache: new SQLiteCacheStore({
database: sharedDb,
cleanupIntervalMs: 300_000, // Less frequent cleanup
maxEntrySizeBytes: 10 * 1024 * 1024, // 10MB max entries
}),
dedupe: new SQLiteDedupeStore({
database: sharedDb,
jobTimeoutMs: 600_000, // Longer timeout for slow operations
cleanupIntervalMs: 300_000, // Less frequent cleanup
}),
rateLimit: new SQLiteRateLimitStore({
database: sharedDb,
defaultConfig: { limit: 1000, windowMs: 60_000 }, // Higher limits
}),
};
```
## TypeScript Support
All stores export their option interfaces for type safety:
```typescript
import type {
SQLiteCacheStoreOptions,
SQLiteDedupeStoreOptions,
SQLiteRateLimitStoreOptions,
SqliteAdaptiveRateLimitStoreOptions, // NEW: Adaptive rate limiting options
} from '@comic-vine/sqlite-store';
// Type-safe configuration for traditional rate limiting
const rateLimitOptions: SQLiteRateLimitStoreOptions = {
database: './rate-limits.db',
defaultConfig: { limit: 200, windowMs: 3600000 },
resourceConfigs: new Map([['issues', { limit: 100, windowMs: 3600000 }]]),
};
// Type-safe configuration for adaptive rate limiting
const adaptiveOptions: SqliteAdaptiveRateLimitStoreOptions = {
database: './comic-vine.db',
adaptiveConfig: {
highActivityThreshold: 15,
sustainedInactivityThresholdMs: 45 * 60 * 1000,
maxUserScaling: 1.5,
},
};
const adaptiveStore = new SqliteAdaptiveRateLimitStore(adaptiveOptions);
```
## Deployment Considerations
### Docker
```dockerfile
# Ensure SQLite files are persisted
VOLUME ["/app/data"]
# Your application
COPY . /app
WORKDIR /app
# Use shared database path
ENV COMIC_VINE_DB_PATH=/app/data/comic-vine.db
```
### File Permissions
Ensure the application has read/write permissions to the database directory:
```bash
# Create data directory with correct permissions
mkdir -p ./data
chmod 755 ./data
```
### Backup and Maintenance
```bash
# Backup shared database
cp comic-vine.db comic-vine.db.backup
# Vacuum database to optimize performance
sqlite3 comic-vine.db "VACUUM;"
# Check database integrity
sqlite3 comic-vine.db "PRAGMA integrity_check;"
```
### Production Considerations for Adaptive Rate Limiting
**Multi-Instance Deployments:**
```typescript
// Docker Compose setup with shared volume
// docker-compose.yml
services:
web:
build: .
volumes:
- ./data:/app/data
environment:
- COMIC_VINE_DB_PATH=/app/data/shared.db
worker:
build: .
volumes:
- ./data:/app/data # Same shared volume
environment:
- COMIC_VINE_DB_PATH=/app/data/shared.db
// Application code
const client = new ComicVine({
apiKey: process.env.COMIC_VINE_API_KEY,
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({
database: process.env.COMIC_VINE_DB_PATH,
}),
},
});
```
**Load Balancer Scenarios:**
```typescript
// Each application instance behind a load balancer
// shares the same rate limiting state through SQLite
// Instance 1 (handles user requests)
const webInstance1 = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({
database: '/shared/comic-vine.db',
}),
},
});
// Instance 2 (handles background jobs)
const backgroundInstance = new ComicVine({
apiKey: 'your-api-key',
stores: {
rateLimit: new SqliteAdaptiveRateLimitStore({
database: '/shared/comic-vine.db', // Same database
}),
},
});
// Adaptive system coordinates across all instances
```
## API Reference
All stores implement the same interfaces as the in-memory versions:
- `CacheStore`
- `DedupeStore`
- `RateLimitStore`
Additional methods specific to SQLite stores:
### SQLiteCacheStore
```typescript
// Get database statistics
const stats = await cacheStore.getStats();
// { totalItems: 1250, expiredItems: 23, databaseSizeKB: 512 }
// Manual cleanup
await cacheStore.cleanup();
// Close database connection
await cacheStore.close();
```
### SQLiteDedupeStore
```typescript
// Get job statistics
const stats = await dedupeStore.getStats();
// { totalJobs: 15, pendingJobs: 3, completedJobs: 10, failedJobs: 2, expiredJobs: 0 }
// Manual cleanup
await dedupeStore.cleanup();
// Close database connection
await dedupeStore.close();
```
### SQLiteRateLimitStore
```typescript
// Get rate limiting statistics
const stats = await rateLimitStore.getStats();
// { totalRequests: 1543, uniqueResources: 8, rateLimitedResources: ['issues'] }
// Configure resource-specific limits
rateLimitStore.setResourceConfig('volumes', { limit: 25, windowMs: 60000 });
// Manual cleanup
await rateLimitStore.cleanup();
// Close database connection
await rateLimitStore.close();
```
### SqliteAdaptiveRateLimitStore
```typescript
// Get comprehensive statistics including adaptive metrics
const stats = await adaptiveStore.getStats();
// {
// totalRequests: 2157,
// uniqueResources: 12,
// rateLimitedResources: ['issues'],
// adaptiveMetrics: {
// averageUserActivity: 8.5,
// backgroundUtilization: 0.75,
// priorityModeActivations: 14
// }
// }
// Check current adaptive status
const status = await adaptiveStore.getStatus('characters');
console.log(status.adaptive);
// {
// userReserved: 140,
// backgroundMax: 60,
// backgroundPaused: false,
// recentUserActivity: 7,
// reason: "Moderate user activity - dynamic scaling (1.4x user capacity)"
// }
// Configure resource-specific traditional limits (if needed)
adaptiveStore.setResourceConfig('videos', { limit: 50, windowMs: 3600000 });
// Manual cleanup (cleans both priority and traditional rate limit data)
await adaptiveStore.cleanup();
// Close database connection
await adaptiveStore.close();
```
## Performance Characteristics
- **Persistence**: Data survives application restarts and crashes
- **Concurrency**: SQLite handles concurrent access with appropriate locking
- **Memory**: Lower memory usage compared to in-memory stores
- **Latency**: Slightly higher latency due to disk I/O
- **Scalability**: Suitable for moderate to high traffic applications
## Use Cases
Perfect for:
- Production applications
- Multi-instance deployments
- Long-running processes
- Applications requiring data persistence
- Serverless environments with persistent storage
- Docker containers with volume mounts
## Dependencies
- `drizzle-orm`: Type-safe SQL query builder
- `better-sqlite3`: Fast SQLite3 binding for Node.js
## License
MIT