A high-performance, type-safe SQLite-based cache for Node.js applications with built-in TTL (Time To Live), size limits, and schema validation.
- ๐ High Performance: Built on Node.js native SQLite driver
- ๐ Type Safe: Full TypeScript support with generic types
- โฐ TTL Support: Automatic expiration with configurable time-to-live
- ๐ Size Limits: LRU-style eviction when max entries exceeded
- ๐ Schema Validation: JSON Schema validation using TypeBox
- ๐ชต Built-in Logging: Configurable logging with colorful output
- ๐ Iterable: Full Map-like interface with iteration support
- ๐พ Persistent: Data survives application restarts
- ๐งน Auto Cleanup: Automatic cleanup of expired entries
- Node.js >= 22.5.0 (required for native SQLite driver)
npm install sqlite-cacheimport { SQLiteCache } from "sqlite-cache"
import { Type } from "@sinclair/typebox"
// Define your data schema
const UserSchema = Type.Object({
id: Type.Number(),
name: Type.String(),
email: Type.String(),
active: Type.Boolean()
})
type User = {
id: number
name: string
email: string
active: boolean
}
// Create cache instance
const cache = new SQLiteCache<string, User>({
path: "./cache.db",
schema: UserSchema,
ttl: 60_000, // 1 minute
max: 1000, // Maximum 1000 entries
log: true // Enable logging
})
// Use the cache
const user: User = {
id: 1,
name: "John Doe",
email: "[email protected]",
active: true
}
cache.set("user:1", user)
const retrieved = cache.get("user:1")
console.log(retrieved) // { id: 1, name: 'John Doe', ... }
// Clean up
cache.close()new SQLiteCache<Key extends string, Value extends object>(options: CacheOptions)| Option | Type | Required | Default | Description |
|---|---|---|---|---|
path |
string |
โ | - | Path to SQLite database file |
schema |
TSchema |
โ | - | TypeBox JSON schema for validation |
ttl |
number |
โ | 60000 |
Time to live in milliseconds |
max |
number |
โ | 100 |
Maximum number of entries |
log |
boolean | LogOptions |
โ | false |
Logging configuration |
interface LogOptions {
prefix?: string // Log prefix (default: '[SQLiteCache]')
timestamp?: boolean // Include timestamps (default: true)
}Store a value in the cache with the given key.
cache.set("user:123", { id: 123, name: "Alice", email: "[email protected]", active: true })Retrieve a value from the cache. Returns null if not found or expired.
const user = cache.get("user:123")
if (user) {
console.log(user.name)
}Check if a key exists and is not expired.
if (cache.has("user:123")) {
console.log("User exists in cache")
}Remove a specific key from the cache. Returns true if the key existed.
const wasDeleted = cache.delete("user:123")Remove all entries from the cache.
cache.clear()Close the database connection. Call this when your application shuts down.
cache.close()Get the current number of valid (non-expired) entries in the cache.
console.log(`Cache contains ${cache.size} entries`)The cache implements the iterable protocol and provides Map-like iteration methods:
Iterate over all valid keys.
for (const key of cache.keys()) {
console.log(key)
}Iterate over all valid values.
for (const user of cache.values()) {
console.log(user.name)
}Iterate over all valid key-value pairs.
for (const [key, user] of cache.entries()) {
console.log(`${key}: ${user.name}`)
}Execute a callback for each valid entry.
cache.forEach((user, key) => {
console.log(`Processing ${user.name} with key ${key}`)
})Direct iteration support.
for (const [key, user] of cache) {
console.log(`${key}: ${user.name}`)
}import { Type } from "@sinclair/typebox"
const ProductSchema = Type.Object({
id: Type.Number(),
name: Type.String(),
price: Type.Number(),
description: Type.Optional(Type.String()),
metadata: Type.Optional(
Type.Object({
category: Type.String(),
tags: Type.Array(Type.String()),
inStock: Type.Boolean()
})
)
})
type Product = {
id: number
name: string
price: number
description?: string
metadata?: {
category: string
tags: string[]
inStock: boolean
}
}
const productCache = new SQLiteCache<string, Product>({
path: "./products.db",
schema: ProductSchema,
ttl: 300_000, // 5 minutes
max: 5000,
log: {
prefix: "[ProductCache]",
timestamp: true
}
})try {
const cache = new SQLiteCache({
path: "/invalid/path/cache.db",
schema: UserSchema
})
} catch (error) {
console.error("Failed to initialize cache:", error.message)
}For testing or temporary caching, you can use an in-memory database:
const tempCache = new SQLiteCache<string, User>({
path: ":memory:",
schema: UserSchema,
ttl: 30_000
})// Batch insert
const users = [
{ id: 1, name: "Alice", email: "[email protected]", active: true },
{ id: 2, name: "Bob", email: "[email protected]", active: false },
{ id: 3, name: "Charlie", email: "[email protected]", active: true }
]
users.forEach((user, index) => {
cache.set(`user:${user.id}`, user)
})
// Batch retrieve
const cachedUsers = users.map((user) => cache.get(`user:${user.id}`)).filter((user) => user !== null)
console.log(`Retrieved ${cachedUsers.length} users from cache`)- Database Location: Use SSD storage for better performance
- TTL Strategy: Shorter TTL reduces memory usage but increases cache misses
- Max Entries: Higher limits improve hit rates but use more storage
- Schema Complexity: Simpler schemas serialize/deserialize faster
- Key Design: Use consistent, predictable key patterns for better organization
Always close the cache when your application shuts down:
process.on("SIGINT", () => {
cache.close()
process.exit(0)
})Use consistent key patterns:
// Good
cache.set("user:123", user)
cache.set("product:456", product)
cache.set("session:abc123", session)
// Avoid
cache.set("123", user)
cache.set("prod456", product)
cache.set("sessionabc123", session)Design schemas that match your data structure:
// Prefer this
const UserSchema = Type.Object({
id: Type.Number(),
profile: Type.Object({
name: Type.String(),
email: Type.String()
})
})
// Over this (less structured)
const UserSchema = Type.Object({
id: Type.Number(),
name: Type.String(),
email: Type.String()
// ... many flat properties
})Handle potential errors gracefully:
const user = cache.get("user:123")
if (!user) {
// Cache miss - fetch from primary data source
const user = await fetchUserFromDB(123)
cache.set("user:123", user)
return user
}
return userError: "Node version must be 22.5.0 or higher"
Solution: Upgrade to Node.js 22.5.0 or later, which includes the native SQLite driver.
Error: Failed to connect to database
Solutions:
- Ensure the directory exists for file-based databases
- Check file permissions
- Verify disk space availability
- Use
:memory:for testing
Error: Data does not match schema
Solutions:
- Verify your TypeBox schema matches your data structure
- Check for required vs optional fields
- Ensure type compatibility (string vs number, etc.)
- Use appropriate TTL values
- Monitor cache hit/miss ratios
- Consider database file location (SSD vs HDD)
- Optimize key naming patterns
- Fork the repository
- Create a feature branch:
git checkout -b feature/new-feature - Make your changes and add tests
- Run tests:
npm test - Commit your changes:
git commit -am 'Add new feature' - Push to the branch:
git push origin feature/new-feature - Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
- ๐ Issues: GitHub Issues
- ๐ Sponsor: Patreon
- Initial release with basic caching functionality
- TTL support with automatic cleanup
- Size limits with LRU-style eviction
- TypeBox schema validation
- Full TypeScript support
- Configurable logging
- Map-like iteration interface
Made with โค๏ธ by xcfio