From c302d8f67e3a8350d21c5941e612e7aa529d0f44 Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:19:54 +0545 Subject: [PATCH] feat: kv api --- .../api-reference/commandkit/classes/kv.mdx | 371 +++++++++++ .../commandkit/functions/open-kv.mdx | 32 + .../commandkit/interfaces/kv-options.mdx | 41 ++ .../15-key-value-store/01-introduction.mdx | 191 ++++++ .../02-basic-operations.mdx | 378 +++++++++++ .../15-key-value-store/03-namespaces.mdx | 339 ++++++++++ .../04-advanced-features.mdx | 618 ++++++++++++++++++ packages/commandkit/kv.cjs | 6 + packages/commandkit/kv.d.ts | 1 + packages/commandkit/package.json | 9 +- packages/commandkit/src/kv/kv.ts | 547 ++++++++++++++++ 11 files changed, 2532 insertions(+), 1 deletion(-) create mode 100644 apps/website/docs/api-reference/commandkit/classes/kv.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/open-kv.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/kv-options.mdx create mode 100644 apps/website/docs/guide/15-key-value-store/01-introduction.mdx create mode 100644 apps/website/docs/guide/15-key-value-store/02-basic-operations.mdx create mode 100644 apps/website/docs/guide/15-key-value-store/03-namespaces.mdx create mode 100644 apps/website/docs/guide/15-key-value-store/04-advanced-features.mdx create mode 100644 packages/commandkit/kv.cjs create mode 100644 packages/commandkit/kv.d.ts create mode 100644 packages/commandkit/src/kv/kv.ts diff --git a/apps/website/docs/api-reference/commandkit/classes/kv.mdx b/apps/website/docs/api-reference/commandkit/classes/kv.mdx new file mode 100644 index 00000000..a4d89d40 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/kv.mdx @@ -0,0 +1,371 @@ +--- +title: "KV" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## KV + + + +A key-value store implementation using SQLite + +This class provides a simple, persistent key-value storage solution +with support for namespaces, automatic cleanup, iteration, and expiration. + + + +*Example* + +```typescript +const kv = new KV('data.db'); +kv.set('user:123', JSON.stringify({ name: 'John', age: 30 })); +const user = JSON.parse(kv.get('user:123') || '{}'); + +// Using namespaces +const userKv = kv.namespace('users'); +userKv.set('123', JSON.stringify({ name: 'John' })); +``` + +```ts title="Signature" +class KV implements Disposable, AsyncDisposable { + constructor(path: string | Buffer | URL | DatabaseSync, options: KvOptions = { + enableWAL: true, + namespace: 'commandkit_kv', + }) + isOpen() => boolean; + getDatabase() => DatabaseSync; + close() => void; + [Symbol.dispose]() => ; + [Symbol.asyncDispose]() => ; + get(key: string) => string | undefined; + set(key: string, value: string) => void; + setex(key: string, value: string, ttl: number) => void; + expire(key: string, ttl: number) => boolean; + ttl(key: string) => number; + delete(key: string) => void; + has(key: string) => boolean; + keys() => string[]; + values() => string[]; + count() => number; + clear() => void; + all() => Record; + namespaces() => string[]; + getCurrentNamespace() => string; + namespace(namespace: string) => KV; + [Symbol.iterator]() => Iterator<[string, string]>; + transaction(fn: () => T | Promise) => Promise; +} +``` +* Implements: Disposable, AsyncDisposable + + + +
+ +### constructor + +KvOptions = { + enableWAL: true, + namespace: 'commandkit_kv', + }) => KV`} /> + +Creates a new KV store instance +### isOpen + + boolean`} /> + +Checks if the database connection is open +### getDatabase + + DatabaseSync`} /> + +Gets the underlying SQLite database instance +### close + + void`} /> + +Closes the database connection +### \[Symbol.dispose] + + `} /> + +Disposable implementation - closes the database when disposed +### \[Symbol.asyncDispose] + + `} /> + +AsyncDisposable implementation - closes the database when disposed +### get + + string | undefined`} /> + +Retrieves a value by key + + + +*Example* + +```typescript +const value = kv.get('my-key'); +if (value) { + console.log('Found:', value); +} +``` +### set + + void`} /> + +Sets a key-value pair + + + +*Example* + +```typescript +kv.set('user:123', JSON.stringify({ name: 'John' })); +kv.set('counter', '42'); +``` +### setex + + void`} /> + +Sets a key-value pair with expiration + + + +*Example* + +```typescript +// Set with 1 hour expiration +kv.setex('session:123', 'user_data', 60 * 60 * 1000); + +// Set with 5 minutes expiration +kv.setex('temp:data', 'cached_value', 5 * 60 * 1000); +``` +### expire + + boolean`} /> + +Sets expiration for an existing key + + + +*Example* + +```typescript +kv.set('user:123', 'user_data'); + +// Set 30 minute expiration +if (kv.expire('user:123', 30 * 60 * 1000)) { + console.log('Expiration set successfully'); +} +``` +### ttl + + number`} /> + +Gets the time to live for a key + + + +*Example* + +```typescript +const ttl = kv.ttl('user:123'); +if (ttl > 0) { + console.log(`Key expires in ${ttl}ms`); +} else if (ttl === -2) { + console.log('Key has no expiration'); +} else { + console.log('Key does not exist'); +} +``` +### delete + + void`} /> + +Deletes a key-value pair + + + +*Example* + +```typescript +kv.delete('user:123'); +``` +### has + + boolean`} /> + +Checks if a key exists and is not expired + + + +*Example* + +```typescript +if (kv.has('user:123')) { + console.log('User exists and is not expired'); +} +``` +### keys + + string[]`} /> + +Gets all keys in the current namespace (excluding expired keys) + + + +*Example* + +```typescript +const keys = kv.keys(); +console.log('All keys:', keys); +``` +### values + + string[]`} /> + +Gets all values in the current namespace (excluding expired keys) + + + +*Example* + +```typescript +const values = kv.values(); +console.log('All values:', values); +``` +### count + + number`} /> + +Gets the total number of key-value pairs in the current namespace (excluding expired keys) + + + +*Example* + +```typescript +const count = kv.count(); +console.log(`Total entries: ${count}`); +``` +### clear + + void`} /> + +Removes all key-value pairs from the current namespace + + + +*Example* + +```typescript +kv.clear(); // Removes all entries in current namespace +``` +### all + + Record<string, string>`} /> + +Gets all key-value pairs as an object (excluding expired keys) + + + +*Example* + +```typescript +const all = kv.all(); +console.log('All entries:', all); +// Output: { 'key1': 'value1', 'key2': 'value2' } +``` +### namespaces + + string[]`} /> + +Gets all available namespaces (tables) in the database + + + +*Example* + +```typescript +const namespaces = kv.namespaces(); +console.log('Available namespaces:', namespaces); +``` +### getCurrentNamespace + + string`} /> + +Gets the current namespace name +### namespace + + KV`} /> + +Creates a new KV instance with a different namespace + + + +*Example* + +```typescript +const userKv = kv.namespace('users'); +const configKv = kv.namespace('config'); + +userKv.set('123', 'John Doe'); +configKv.set('theme', 'dark'); +``` +### \[Symbol.iterator] + + Iterator<[string, string]>`} /> + +Iterator implementation for iterating over all non-expired key-value pairs + + + +*Example* + +```typescript +for (const [key, value] of kv) { + console.log(`${key}: ${value}`); +} + +// Or using spread operator +const entries = [...kv]; +``` +### transaction + + Promise<T>`} /> + +Executes a function within a transaction + + + +*Example* + +```typescript +// Synchronous transaction +kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + kv.set('user:456', JSON.stringify({ name: 'Jane' })); + // If any operation fails, all changes are rolled back +}); + +// Async transaction +await kv.transaction(async () => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + await someAsyncOperation(); + kv.set('user:456', JSON.stringify({ name: 'Jane' })); + // If any operation fails, all changes are rolled back +}); +``` + + +
diff --git a/apps/website/docs/api-reference/commandkit/functions/open-kv.mdx b/apps/website/docs/api-reference/commandkit/functions/open-kv.mdx new file mode 100644 index 00000000..42c2a940 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/open-kv.mdx @@ -0,0 +1,32 @@ +--- +title: "OpenKV" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## openKV + + + +Opens a new KV instance + +```ts title="Signature" +function openKV(path: string | Buffer | URL | DatabaseSync = 'commandkit_kv.db', options: KvOptions = { enableWAL: true, namespace: 'commandkit_kv' }): KV +``` +Parameters + +### path + + + +### options + +KvOptions`} /> + diff --git a/apps/website/docs/api-reference/commandkit/interfaces/kv-options.mdx b/apps/website/docs/api-reference/commandkit/interfaces/kv-options.mdx new file mode 100644 index 00000000..e89a56e2 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/kv-options.mdx @@ -0,0 +1,41 @@ +--- +title: "KvOptions" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## KvOptions + + + +Configuration options for the KV store + +```ts title="Signature" +interface KvOptions { + enableWAL?: boolean; + namespace?: string; +} +``` + +
+ +### enableWAL + + + +Enable Write-Ahead Logging for better performance and durability +### namespace + + + +Namespace for the key-value store table + + +
diff --git a/apps/website/docs/guide/15-key-value-store/01-introduction.mdx b/apps/website/docs/guide/15-key-value-store/01-introduction.mdx new file mode 100644 index 00000000..b0be2ef6 --- /dev/null +++ b/apps/website/docs/guide/15-key-value-store/01-introduction.mdx @@ -0,0 +1,191 @@ +--- +title: Key-Value Store +description: Learn how to use CommandKit's built-in key-value store for persistent data storage. +--- + +# Key-Value Store + +CommandKit provides a built-in key-value store implementation using SQLite for persistent data storage. This guide will show you how to use the KV store effectively in your bot for storing configuration, user data, and other persistent information. + +## What is the KV Store? + +The KV store is a simple, persistent key-value storage solution that: + +- **Persistent**: Data is stored in a SQLite database file +- **Namespaced**: Organize data into logical groups using namespaces +- **Type-safe**: Full TypeScript support with proper typing +- **Iterable**: Use standard JavaScript iteration patterns +- **Resource-safe**: Implements disposable patterns for automatic cleanup + +## When to Use the KV Store + +Use the KV store when you need to: + +- Store user preferences and settings +- Cache frequently accessed data persistently +- Store bot configuration that needs to survive restarts +- Keep track of user statistics and progress +- Store temporary data that needs to persist between sessions + +:::warning Node.js Version Requirement +The KV store requires Node.js version that supports the `node:sqlite` module. Make sure you're using a compatible Node.js version. +::: + +## Basic Setup + +The KV store is available directly from the CommandKit package: + +```ts +import { KV, openKV } from 'commandkit/kv'; +``` + +## Quick Start + +Here's a simple example of how to use the KV store: + +```ts +import { openKV } from 'commandkit/kv'; + +// Create a new KV store instance (uses default database file) +const kv = openKV(); + +// Or create with custom database file +const kv = openKV('bot-data.db'); + +// Or create in-memory store for caching +const kv = openKV(':memory:'); + +// Store some data +kv.set('user:123', JSON.stringify({ name: 'John', level: 5 })); + +// Retrieve data +const userData = kv.get('user:123'); +if (userData) { + const user = JSON.parse(userData); + console.log(`User ${user.name} is level ${user.level}`); +} + +// Check if a key exists +if (kv.has('user:123')) { + console.log('User data exists'); +} + +// Get all keys +const allKeys = kv.keys(); +console.log('All stored keys:', allKeys); + +// Clean up when done +kv.close(); +``` + +## Key Features + +### 1. **Namespaces** + +Organize your data into logical groups: + +```ts +const userKv = kv.namespace('users'); +const configKv = kv.namespace('config'); + +userKv.set('123', JSON.stringify({ name: 'John' })); +configKv.set('theme', 'dark'); +``` + +### 2. **Iteration Support** + +Use standard JavaScript iteration patterns: + +```ts +// Iterate over all key-value pairs +for (const [key, value] of kv) { + console.log(`${key}: ${value}`); +} + +// Convert to array +const entries = [...kv]; +``` + +### 3. **Automatic Resource Management** + +The KV store implements disposable patterns: + +```ts +// Using with statement (automatic cleanup) +{ + using kv = openKV(); + kv.set('key', 'value'); +} // kv is automatically closed + +// Using async/await with automatic disposal (fake promise wrapper) +await using kv = openKV(); +kv.set('key', 'value'); +// kv is automatically closed when the block ends +``` + +:::note Async Disposal +The `async using` statement is just a fake promise wrapper around the synchronous `using` statement. The disposal is still synchronous. +::: + +### 4. **Expiration Support** + +Store temporary data with automatic expiration: + +```ts +// Set data with expiration (1 hour) +kv.setex('session:123', 'user_data', 60 * 60 * 1000); + +// Set expiration for existing key (30 minutes) +kv.expire('user:123', 30 * 60 * 1000); + +// Check time to live +const ttl = kv.ttl('user:123'); +if (ttl > 0) { + console.log(`Expires in ${ttl}ms`); +} +``` + +### 5. **Transaction Support** + +Execute multiple operations atomically: + +```ts +kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + kv.set('user:456', JSON.stringify({ name: 'Jane' })); + // If any operation fails, all changes are rolled back +}); +``` + +### 6. **Bulk Operations** + +Perform operations on multiple items: + +```ts +// Get all data as an object (excluding expired keys) +const allData = kv.all(); +console.log('All data:', allData); + +// Get all values (excluding expired keys) +const values = kv.values(); +console.log('All values:', values); + +// Count total entries (excluding expired keys) +const count = kv.count(); +console.log(`Total entries: ${count}`); +``` + +## Best Practices + +1. **Use Meaningful Keys**: Use descriptive key names that make sense in your context +2. **Serialize Complex Data**: Store objects as JSON strings +3. **Use Namespaces**: Organize related data into namespaces +4. **Handle Missing Data**: Always check if data exists before using it +5. **Clean Up Resources**: Use disposable patterns or manually close connections +6. **Backup Important Data**: Regularly backup your database files + +## Next Steps + +- Learn about [basic operations](./02-basic-operations.mdx) +- Explore [namespaces](./03-namespaces.mdx) +- Understand [advanced features](./04-advanced-features.mdx) including transactions diff --git a/apps/website/docs/guide/15-key-value-store/02-basic-operations.mdx b/apps/website/docs/guide/15-key-value-store/02-basic-operations.mdx new file mode 100644 index 00000000..23609aec --- /dev/null +++ b/apps/website/docs/guide/15-key-value-store/02-basic-operations.mdx @@ -0,0 +1,378 @@ +--- +title: Basic Operations +description: Learn the fundamental operations for working with the KV store. +--- + +# Basic Operations + +This guide covers the essential operations you'll use most frequently with the KV store: getting, setting, deleting, and checking for data. + +## Creating a KV Store + +First, let's create a KV store instance: + +```ts +import { openKV } from 'commandkit/kv'; + +// Create with default settings (uses 'commandkit_kv.db') +const kv = openKV(); + +// Create with custom database file +const kv = openKV('my-bot-data.db'); + +// Create in-memory store for caching +const kv = openKV(':memory:'); + +// Create with custom options +const kv = openKV('data.db', { + enableWAL: true, + namespace: 'my_bot', +}); +``` + +## Setting Data + +Use the `set` method to store key-value pairs: + +```ts +// Store simple string values +kv.set('bot_name', 'MyAwesomeBot'); +kv.set('version', '1.0.0'); + +// Store serialized objects +kv.set( + 'user:123', + JSON.stringify({ + name: 'John Doe', + level: 5, + joinDate: new Date().toISOString(), + }), +); + +// Store configuration +kv.set('config:theme', 'dark'); +kv.set('config:language', 'en'); +``` + +## Getting Data + +Use the `get` method to retrieve stored values: + +```ts +// Get simple values +const botName = kv.get('bot_name'); +console.log(botName); // 'MyAwesomeBot' + +// Get and parse JSON data +const userData = kv.get('user:123'); +if (userData) { + const user = JSON.parse(userData); + console.log(`User: ${user.name}, Level: ${user.level}`); +} + +// Handle missing data +const missingData = kv.get('non_existent_key'); +if (missingData === undefined) { + console.log('Key does not exist'); +} +``` + +## Checking for Existence + +Use the `has` method to check if a key exists: + +```ts +// Check if a key exists +if (kv.has('user:123')) { + console.log('User data exists'); +} else { + console.log('User data not found'); +} + +// Use in conditional logic +if (kv.has('config:theme')) { + const theme = kv.get('config:theme'); + console.log(`Current theme: ${theme}`); +} else { + // Set default theme + kv.set('config:theme', 'light'); +} +``` + +## Deleting Data + +Use the `delete` method to remove key-value pairs: + +```ts +// Delete a single key +kv.delete('user:123'); + +// Delete configuration +kv.delete('config:theme'); + +// Check if deletion was successful +if (!kv.has('user:123')) { + console.log('User data successfully deleted'); +} +``` + +## Bulk Operations + +### Getting All Keys + +```ts +// Get all keys in the current namespace +const allKeys = kv.keys(); +console.log('All keys:', allKeys); + +// Filter keys by pattern +const userKeys = allKeys.filter((key) => key.startsWith('user:')); +console.log('User keys:', userKeys); +``` + +### Getting All Values + +```ts +// Get all values +const allValues = kv.values(); +console.log('All values:', allValues); + +// Process all values +allValues.forEach((value) => { + try { + const parsed = JSON.parse(value); + console.log('Parsed value:', parsed); + } catch { + console.log('Raw value:', value); + } +}); +``` + +### Getting All Data + +```ts +// Get all key-value pairs as an object +const allData = kv.all(); +console.log('All data:', allData); + +// Process all data +Object.entries(allData).forEach(([key, value]) => { + console.log(`${key}: ${value}`); +}); +``` + +### Counting Entries + +```ts +// Get total number of entries +const count = kv.count(); +console.log(`Total entries: ${count}`); + +// Check if store is empty +if (count === 0) { + console.log('KV store is empty'); +} +``` + +## Clearing Data + +Use the `clear` method to remove all data from the current namespace: + +```ts +// Clear all data in current namespace +kv.clear(); + +// Verify it's empty +console.log(`Entries after clear: ${kv.count()}`); // 0 +``` + +## Expiration Operations + +### Setting Data with Expiration + +```ts +// Set data with expiration (1 hour) +kv.setex( + 'session:123', + JSON.stringify({ userId: '123', data: 'temp' }), + 60 * 60 * 1000, +); + +// Set data with 5 minutes expiration +kv.setex('temp:data', 'cached_value', 5 * 60 * 1000); + +// Set data with 1 day expiration +kv.setex('daily:stats', JSON.stringify({ count: 100 }), 24 * 60 * 60 * 1000); +``` + +### Setting Expiration for Existing Keys + +```ts +// First set the data +kv.set('user:123', JSON.stringify({ name: 'John', level: 5 })); + +// Then set expiration (30 minutes) +if (kv.expire('user:123', 30 * 60 * 1000)) { + console.log('Expiration set successfully'); +} else { + console.log('Key does not exist'); +} +``` + +### Checking Time to Live + +```ts +const ttl = kv.ttl('user:123'); + +if (ttl > 0) { + console.log(`Key expires in ${ttl}ms (${Math.floor(ttl / 1000)}s)`); +} else if (ttl === -2) { + console.log('Key has no expiration'); +} else { + console.log('Key does not exist or has expired'); +} +``` + +### Automatic Expiration Handling + +```ts +// Set data with expiration +kv.setex('temp:key', 'value', 1000); // 1 second + +// Immediately check - should exist +console.log(kv.has('temp:key')); // true + +// Wait for expiration +setTimeout(() => { + console.log(kv.has('temp:key')); // false + console.log(kv.get('temp:key')); // undefined +}, 1100); +``` + +## Working with Different Data Types + +### Storing Numbers + +```ts +// Store numbers as strings +kv.set('counter', '42'); +kv.set('score', '1500'); + +// Retrieve and convert back to numbers +const counter = parseInt(kv.get('counter') || '0'); +const score = parseInt(kv.get('score') || '0'); +``` + +### Storing Booleans + +```ts +// Store booleans as strings +kv.set('feature_enabled', 'true'); +kv.set('maintenance_mode', 'false'); + +// Retrieve and convert back to booleans +const featureEnabled = kv.get('feature_enabled') === 'true'; +const maintenanceMode = kv.get('maintenance_mode') === 'true'; +``` + +### Storing Dates + +```ts +// Store dates as ISO strings +kv.set('last_backup', new Date().toISOString()); +kv.set('user_created', '2024-01-15T10:30:00.000Z'); + +// Retrieve and parse dates +const lastBackup = new Date(kv.get('last_backup') || ''); +const userCreated = new Date(kv.get('user_created') || ''); +``` + +## Transaction Operations + +### Basic Transaction Usage + +```ts +// Execute multiple operations atomically +kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John', balance: 100 })); + kv.set('user:456', JSON.stringify({ name: 'Jane', balance: 200 })); + + // If any operation fails, all changes are rolled back +}); +``` + +### Transaction with Error Handling + +```ts +try { + kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + + // Simulate an error + throw new Error('Database error'); + + // This won't execute due to the error + kv.set('user:456', JSON.stringify({ name: 'Jane' })); + }); +} catch (error) { + console.error('Transaction failed:', error); + // All changes were automatically rolled back +} +``` + +### Combining Expiration with Transactions + +```ts +kv.transaction(() => { + // Set permanent data + kv.set('user:123', JSON.stringify({ name: 'John', level: 5 })); + + // Set temporary data with expiration + kv.setex('session:123', JSON.stringify({ token: 'abc123' }), 60 * 60 * 1000); + + // If any operation fails, both permanent and temporary data are rolled back +}); +``` + +## Error Handling + +Always handle potential errors when working with the KV store: + +```ts +try { + // Check if database is open + if (!kv.isOpen()) { + console.error('Database is not open'); + return; + } + + // Perform operations + kv.set('key', 'value'); + const value = kv.get('key'); + + if (value === undefined) { + console.log('Key not found'); + } +} catch (error) { + console.error('Error working with KV store:', error); +} finally { + // Always close the connection + kv.close(); +} +``` + +## Best Practices + +1. **Always check for undefined**: The `get` method returns `undefined` for missing keys +2. **Serialize complex data**: Use `JSON.stringify()` for objects and arrays +3. **Use meaningful key names**: Make keys descriptive and consistent +4. **Handle errors gracefully**: Wrap operations in try-catch blocks +5. **Close connections**: Always close the KV store when done +6. **Validate data**: Check data types and structure when retrieving + +## Next Steps + +Now that you understand the basic operations, learn about: + +- [Namespaces](./03-namespaces.mdx) for organizing your data +- [Advanced features](./04-advanced-features.mdx) for more complex use cases diff --git a/apps/website/docs/guide/15-key-value-store/03-namespaces.mdx b/apps/website/docs/guide/15-key-value-store/03-namespaces.mdx new file mode 100644 index 00000000..5a075ffc --- /dev/null +++ b/apps/website/docs/guide/15-key-value-store/03-namespaces.mdx @@ -0,0 +1,339 @@ +--- +title: Namespaces +description: Learn how to use namespaces to organize your KV store data logically. +--- + +# Namespaces + +Namespaces are a powerful feature of the KV store that allows you to organize your data into logical groups. This helps keep your data organized, prevents key conflicts, and makes it easier to manage different types of data. + +## What are Namespaces? + +Namespaces are like separate tables within your SQLite database. Each namespace has its own set of key-value pairs, completely isolated from other namespaces. This means you can have the same key in different namespaces without conflicts. + +## Creating Namespaces + +You can create namespaces in two ways: + +### 1. During Initialization + +```ts +import { openKV } from 'commandkit/kv'; + +// Create KV store with a specific namespace +const kv = openKV('data.db', { + namespace: 'users', +}); + +// This will store data in the 'users' namespace +kv.set('123', JSON.stringify({ name: 'John' })); +``` + +### 2. Using the namespace() Method + +```ts +import { openKV } from 'commandkit/kv'; + +// Create the main KV store +const kv = openKV('data.db'); + +// Create namespace instances +const userKv = kv.namespace('users'); +const configKv = kv.namespace('config'); +const statsKv = kv.namespace('statistics'); +``` + +## Working with Multiple Namespaces + +Here's how to work with different namespaces: + +```ts +import { openKV } from 'commandkit/kv'; + +const kv = openKV('bot-data.db'); + +// Create namespace instances +const userKv = kv.namespace('users'); +const configKv = kv.namespace('config'); +const guildKv = kv.namespace('guilds'); + +// Store data in different namespaces +userKv.set( + '123', + JSON.stringify({ + name: 'John Doe', + level: 5, + joinDate: new Date().toISOString(), + }), +); + +configKv.set('theme', 'dark'); +configKv.set('language', 'en'); +configKv.set('timezone', 'UTC'); + +guildKv.set( + '456', + JSON.stringify({ + name: 'My Discord Server', + memberCount: 1000, + premium: true, + }), +); + +// Each namespace can have the same key without conflicts +userKv.set('settings', JSON.stringify({ theme: 'light' })); +configKv.set('settings', JSON.stringify({ maintenance: false })); +``` + +## Getting Current Namespace + +You can check which namespace you're currently working with: + +```ts +const kv = openKV('data.db', { namespace: 'users' }); +console.log(kv.getCurrentNamespace()); // 'users' + +const configKv = kv.namespace('config'); +console.log(configKv.getCurrentNamespace()); // 'config' +``` + +## Listing All Namespaces + +You can get a list of all available namespaces in your database: + +```ts +const kv = openKV('data.db'); + +// Create some namespaces +kv.namespace('users').set('123', 'user data'); +kv.namespace('config').set('theme', 'dark'); +kv.namespace('guilds').set('456', 'guild data'); + +// Get all namespaces +const namespaces = kv.namespaces(); +console.log('Available namespaces:', namespaces); +// Output: ['users', 'config', 'guilds'] +``` + +## Namespace-Specific Operations + +Each namespace instance has its own set of operations: + +```ts +const kv = openKV('data.db'); +const userKv = kv.namespace('users'); +const configKv = kv.namespace('config'); + +// Each namespace has its own count +console.log(`Users: ${userKv.count()}`); +console.log(`Config items: ${configKv.count()}`); + +// Each namespace has its own keys +console.log('User keys:', userKv.keys()); +console.log('Config keys:', configKv.keys()); + +// Each namespace has its own data +console.log('All users:', userKv.all()); +console.log('All config:', configKv.all()); +``` + +## Common Namespace Patterns + +### User Data Management + +```ts +const kv = openKV('bot-data.db'); + +// User profiles +const userProfiles = kv.namespace('user_profiles'); +userProfiles.set( + '123', + JSON.stringify({ + username: 'john_doe', + level: 5, + experience: 1250, + joinDate: new Date().toISOString(), + }), +); + +// User settings +const userSettings = kv.namespace('user_settings'); +userSettings.set( + '123', + JSON.stringify({ + theme: 'dark', + language: 'en', + notifications: true, + }), +); + +// User statistics +const userStats = kv.namespace('user_stats'); +userStats.set( + '123', + JSON.stringify({ + messagesSent: 150, + commandsUsed: 45, + timeSpent: 3600, + }), +); +``` + +### Guild/Server Management + +```ts +const kv = openKV('bot-data.db'); + +// Guild configurations +const guildConfig = kv.namespace('guild_config'); +guildConfig.set( + '456', + JSON.stringify({ + prefix: '!', + welcomeChannel: '123456789', + modRole: '987654321', + autoRole: '111222333', + }), +); + +// Guild statistics +const guildStats = kv.namespace('guild_stats'); +guildStats.set( + '456', + JSON.stringify({ + memberCount: 1000, + messageCount: 50000, + commandUsage: 2500, + }), +); + +// Guild features +const guildFeatures = kv.namespace('guild_features'); +guildFeatures.set( + '456', + JSON.stringify({ + welcomeMessages: true, + autoModeration: false, + leveling: true, + music: false, + }), +); +``` + +### Bot Configuration + +```ts +const kv = openKV('bot-data.db'); + +// Global bot settings +const botConfig = kv.namespace('bot_config'); +botConfig.set( + 'global', + JSON.stringify({ + defaultPrefix: '!', + maintenanceMode: false, + logLevel: 'info', + backupInterval: 3600, + }), +); + +// Feature flags +const featureFlags = kv.namespace('feature_flags'); +featureFlags.set('ai_commands', 'true'); +featureFlags.set('analytics', 'false'); +featureFlags.set('beta_features', 'true'); + +// API keys and secrets +const secrets = kv.namespace('secrets'); +secrets.set('openai_key', 'sk-...'); +secrets.set('weather_api', 'abc123...'); +``` + +## Namespace Best Practices + +### 1. **Use Descriptive Names** + +```ts +// Good +const userProfiles = kv.namespace('user_profiles'); +const guildSettings = kv.namespace('guild_settings'); +const botConfiguration = kv.namespace('bot_config'); + +// Avoid +const up = kv.namespace('up'); +const gs = kv.namespace('gs'); +const bc = kv.namespace('bc'); +``` + +### 2. **Group Related Data** + +```ts +// Group all user-related data +const userData = kv.namespace('users'); +const userProfiles = kv.namespace('user_profiles'); +const userSettings = kv.namespace('user_settings'); +const userStats = kv.namespace('user_statistics'); + +// Group all guild-related data +const guildData = kv.namespace('guilds'); +const guildConfig = kv.namespace('guild_configuration'); +const guildFeatures = kv.namespace('guild_features'); +``` + +### 3. **Use Consistent Naming** + +```ts +// Be consistent with naming conventions +const userKv = kv.namespace('users'); +const guildKv = kv.namespace('guilds'); +const configKv = kv.namespace('config'); + +// Or use descriptive suffixes +const userProfiles = kv.namespace('user_profiles'); +const guildSettings = kv.namespace('guild_settings'); +const botConfiguration = kv.namespace('bot_configuration'); +``` + +### 4. **Document Your Namespaces** + +```ts +// Create a namespace registry +const namespaces = { + users: kv.namespace('users'), + userProfiles: kv.namespace('user_profiles'), + userSettings: kv.namespace('user_settings'), + guilds: kv.namespace('guilds'), + guildConfig: kv.namespace('guild_config'), + botConfig: kv.namespace('bot_config'), + featureFlags: kv.namespace('feature_flags'), +}; + +// Use the registry +namespaces.users.set('123', 'user data'); +namespaces.guildConfig.set('456', 'guild config'); +``` + +## Namespace Cleanup + +You can clear specific namespaces: + +```ts +const kv = openKV('data.db'); +const userKv = kv.namespace('users'); +const configKv = kv.namespace('config'); + +// Clear only the users namespace +userKv.clear(); + +// Clear only the config namespace +configKv.clear(); + +// Clear all data (all namespaces) +kv.clear(); // This only clears the default namespace +``` + +## Next Steps + +Now that you understand namespaces, learn about: + +- [Advanced features](./04-advanced-features.mdx) like iteration and resource management diff --git a/apps/website/docs/guide/15-key-value-store/04-advanced-features.mdx b/apps/website/docs/guide/15-key-value-store/04-advanced-features.mdx new file mode 100644 index 00000000..b741cdc3 --- /dev/null +++ b/apps/website/docs/guide/15-key-value-store/04-advanced-features.mdx @@ -0,0 +1,618 @@ +--- +title: Advanced Features +description: Learn about advanced KV store features like iteration, resource management, and database access. +--- + +# Advanced Features + +This guide covers advanced features of the KV store, including iteration, resource management, database access, and performance optimization. + +## Iteration Support + +The KV store implements the JavaScript iterator protocol, allowing you to use standard iteration patterns. + +### Basic Iteration + +```ts +import { openKV } from 'commandkit/kv'; + +const kv = openKV('data.db'); + +// Store some data +kv.set('user:1', JSON.stringify({ name: 'Alice', level: 5 })); +kv.set('user:2', JSON.stringify({ name: 'Bob', level: 3 })); +kv.set('user:3', JSON.stringify({ name: 'Charlie', level: 7 })); + +// Iterate over all key-value pairs +for (const [key, value] of kv) { + console.log(`${key}: ${value}`); +} +``` + +### Using Spread Operator + +```ts +// Convert to array of entries +const entries = [...kv]; +console.log('All entries:', entries); + +// Convert to array of keys +const keys = [...kv].map(([key]) => key); +console.log('All keys:', keys); + +// Convert to array of values +const values = [...kv].map(([, value]) => value); +console.log('All values:', values); +``` + +### Filtering During Iteration + +```ts +// Filter entries during iteration +const userEntries = [...kv].filter(([key]) => key.startsWith('user:')); +console.log('User entries:', userEntries); + +// Find specific entries +const aliceEntry = [...kv].find(([key, value]) => { + const user = JSON.parse(value); + return user.name === 'Alice'; +}); +console.log('Alice entry:', aliceEntry); +``` + +### Processing Large Datasets + +```ts +// Process entries in chunks to avoid memory issues +function processInChunks(kv: KV, chunkSize: number = 100) { + const entries = [...kv]; + + for (let i = 0; i < entries.length; i += chunkSize) { + const chunk = entries.slice(i, i + chunkSize); + + // Process chunk + chunk.forEach(([key, value]) => { + const user = JSON.parse(value); + console.log(`Processing ${user.name}...`); + }); + + // Optional: Add delay between chunks + if (i + chunkSize < entries.length) { + // await new Promise(resolve => setTimeout(resolve, 100)); + } + } +} + +processInChunks(kv, 50); +``` + +## Transaction Support + +The KV store provides ACID transaction support using SQLite's built-in transaction capabilities. + +### Basic Transactions + +```ts +import { openKV } from 'commandkit/kv'; + +const kv = openKV(); + +// Execute multiple operations atomically (synchronous) +kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John', balance: 100 })); + kv.set('user:456', JSON.stringify({ name: 'Jane', balance: 200 })); + + // If any operation fails, all changes are rolled back + console.log('Transaction completed successfully'); +}); + +// Execute multiple operations atomically (asynchronous) +await kv.transaction(async () => { + kv.set('user:123', JSON.stringify({ name: 'John', balance: 100 })); + + // You can perform async operations within the transaction + await someAsyncOperation(); + + kv.set('user:456', JSON.stringify({ name: 'Jane', balance: 200 })); + + // If any operation fails, all changes are rolled back + console.log('Async transaction completed successfully'); +}); +``` + +### Transaction with Error Handling + +```ts +// Synchronous transaction with error handling +try { + kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + + // Simulate an error + throw new Error('Something went wrong'); + + // This line won't execute due to the error + kv.set('user:456', JSON.stringify({ name: 'Jane' })); + }); +} catch (error) { + console.error('Transaction failed:', error); + // All changes were automatically rolled back +} + +// Async transaction with error handling +try { + await kv.transaction(async () => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + + // Simulate an async error + await Promise.reject(new Error('Async operation failed')); + + // This line won't execute due to the error + kv.set('user:456', JSON.stringify({ name: 'Jane' })); + }); +} catch (error) { + console.error('Async transaction failed:', error); + // All changes were automatically rolled back +} +``` + +### Nested Operations in Transactions + +```ts +// Synchronous nested operations +kv.transaction(() => { + // All operations within this block are part of the same transaction + const userKv = kv.namespace('users'); + const statsKv = kv.namespace('stats'); + + userKv.set('123', JSON.stringify({ name: 'John', level: 5 })); + statsKv.set('total_users', '1'); + + // Even operations on different namespaces are part of the same transaction + // If any operation fails, all changes are rolled back +}); + +// Async nested operations +await kv.transaction(async () => { + // All operations within this block are part of the same transaction + const userKv = kv.namespace('users'); + const statsKv = kv.namespace('stats'); + + userKv.set('123', JSON.stringify({ name: 'John', level: 5 })); + + // You can perform async operations between KV operations + await validateUserData({ name: 'John', level: 5 }); + + statsKv.set('total_users', '1'); + + // Even operations on different namespaces are part of the same transaction + // If any operation fails, all changes are rolled back +}); +``` + +### Transaction Best Practices + +1. **Keep transactions short**: Long-running transactions can block other operations +2. **Handle errors properly**: Always wrap transactions in try-catch blocks +3. **Use for related operations**: Group operations that should succeed or fail together +4. **Async operations are supported**: You can perform async operations within transactions +5. **Prepared statements**: Transaction commands use prepared statements for better performance + +```ts +// Good: Related operations in a transaction +kv.transaction(() => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + kv.set('user_count', '1'); +}); + +// Good: Async operations in a transaction +await kv.transaction(async () => { + kv.set('user:123', JSON.stringify({ name: 'John' })); + + // Async operations are supported + await validateUserData({ name: 'John' }); + await updateUserStats(); + + kv.set('user_count', '1'); +}); + +// Avoid: Very long-running transactions +kv.transaction(() => { + // Don't do heavy processing here + for (let i = 0; i < 1000000; i++) { + kv.set(`key:${i}`, `value:${i}`); + } +}); +``` + +## Resource Management + +The KV store implements both `Disposable` and `AsyncDisposable` interfaces for automatic resource management. + +### Using the `using` Statement + +```ts +// Synchronous using statement +{ + using kv = openKV('data.db'); + kv.set('key', 'value'); + console.log('Data stored'); +} // kv is automatically closed + +// Async using statement +async function processData() { + await using kv = openKV('data.db'); + + kv.set('user:123', JSON.stringify({ name: 'John' })); + const userData = kv.get('user:123'); + + console.log('User data:', userData); +} // kv is automatically closed when function exits +``` + +### Manual Resource Management + +```ts +const kv = openKV('data.db'); + +try { + // Perform operations + kv.set('key', 'value'); + const value = kv.get('key'); + console.log('Value:', value); +} catch (error) { + console.error('Error:', error); +} finally { + // Always close the connection + kv.close(); +} +``` + +### Checking Connection Status + +```ts +const kv = openKV('data.db'); + +// Check if database is open +if (kv.isOpen()) { + console.log('Database is open and ready'); + + // Perform operations + kv.set('key', 'value'); +} else { + console.error('Database is not open'); +} + +// Close the connection +kv.close(); + +// Check again +if (!kv.isOpen()) { + console.log('Database is now closed'); +} +``` + +## Database Access + +You can access the underlying SQLite database for advanced operations. + +### Getting the Database Instance + +```ts +import { openKV } from 'commandkit/kv'; +import type { DatabaseSync } from 'node:sqlite'; + +const kv = openKV('data.db'); +const db = kv.getDatabase(); + +// Now you can use the SQLite database directly +console.log('Database path:', db.name); +console.log('Database is open:', db.isOpen); +``` + +### Custom SQL Queries + +```ts +const kv = openKV('data.db'); +const db = kv.getDatabase(); + +// Execute custom SQL queries +const result = db + .prepare( + ` + SELECT key, value + FROM commandkit_kv + WHERE key LIKE 'user:%' + ORDER BY key +`, + ) + .all(); + +console.log('User data:', result); + +// Execute raw SQL +db.exec(` + CREATE TABLE IF NOT EXISTS custom_table ( + id INTEGER PRIMARY KEY, + name TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) +`); + +// Insert data into custom table +db.prepare( + ` + INSERT INTO custom_table (name) VALUES (?) +`, +).run('Custom Entry'); +``` + +### Database Backup + +```ts +import { openKV } from 'commandkit/kv'; +import { DatabaseSync } from 'node:sqlite'; +import { copyFileSync } from 'node:fs'; + +const kv = openKV('data.db'); + +// Create a backup +function backupDatabase(sourcePath: string, backupPath: string) { + try { + copyFileSync(sourcePath, backupPath); + console.log(`Backup created: ${backupPath}`); + } catch (error) { + console.error('Backup failed:', error); + } +} + +// Backup the database +backupDatabase('data.db', `data-backup-${Date.now()}.db`); +``` + +## Performance Optimization + +### Write-Ahead Logging (WAL) + +WAL mode is enabled by default and provides better performance for concurrent access: + +```ts +const kv = openKV('data.db', { + enableWAL: true, // Default: true + namespace: 'my_bot', +}); +``` + +### Batch Operations + +For better performance when working with multiple items: + +```ts +const kv = openKV('data.db'); + +// Batch set operations +function batchSet(kv: KV, entries: Array<[string, string]>) { + const db = kv.getDatabase(); + + // Start a transaction + db.exec('BEGIN TRANSACTION'); + + try { + const stmt = db.prepare(` + INSERT OR REPLACE INTO ${kv.getCurrentNamespace()} (key, value) + VALUES (?, ?) + `); + + for (const [key, value] of entries) { + stmt.run(key, value); + } + + // Commit the transaction + db.exec('COMMIT'); + } catch (error) { + // Rollback on error + db.exec('ROLLBACK'); + throw error; + } +} + +// Use batch operations +const entries = [ + ['user:1', JSON.stringify({ name: 'Alice', level: 5 })], + ['user:2', JSON.stringify({ name: 'Bob', level: 3 })], + ['user:3', JSON.stringify({ name: 'Charlie', level: 7 })], +]; + +batchSet(kv, entries); +``` + +### Efficient Data Retrieval + +```ts +const kv = openKV('data.db'); + +// Get multiple specific keys efficiently +function getMultiple(kv: KV, keys: string[]) { + const db = kv.getDatabase(); + const placeholders = keys.map(() => '?').join(','); + + const stmt = db.prepare(` + SELECT key, value + FROM ${kv.getCurrentNamespace()} + WHERE key IN (${placeholders}) + `); + + return stmt.all(keys); +} + +// Use efficient retrieval +const keys = ['user:1', 'user:2', 'user:3']; +const results = getMultiple(kv, keys); +console.log('Multiple results:', results); +``` + +## Error Handling and Recovery + +### Robust Error Handling + +```ts +import { openKV } from 'commandkit/kv'; + +class KVManager { + private kv: KV; + + constructor(dbPath: string) { + this.kv = openKV(dbPath); + } + + async set(key: string, value: string): Promise { + try { + if (!this.kv.isOpen()) { + console.error('Database is not open'); + return false; + } + + this.kv.set(key, value); + return true; + } catch (error) { + console.error('Error setting value:', error); + return false; + } + } + + async get(key: string): Promise { + try { + if (!this.kv.isOpen()) { + console.error('Database is not open'); + return undefined; + } + + return this.kv.get(key); + } catch (error) { + console.error('Error getting value:', error); + return undefined; + } + } + + async has(key: string): Promise { + try { + if (!this.kv.isOpen()) { + return false; + } + + return this.kv.has(key); + } catch (error) { + console.error('Error checking key:', error); + return false; + } + } + + close(): void { + try { + this.kv.close(); + } catch (error) { + console.error('Error closing database:', error); + } + } +} + +// Use the manager +const manager = new KVManager('data.db'); +await manager.set('key', 'value'); +const value = await manager.get('key'); +manager.close(); +``` + +### Data Validation + +```ts +// Validate data before storing +function validateUserData(data: any): boolean { + return ( + typeof data === 'object' && + typeof data.name === 'string' && + typeof data.level === 'number' && + data.level >= 0 + ); +} + +// Safe set operation with validation +function safeSetUser(kv: KV, userId: string, userData: any): boolean { + if (!validateUserData(userData)) { + console.error('Invalid user data'); + return false; + } + + try { + kv.set(`user:${userId}`, JSON.stringify(userData)); + return true; + } catch (error) { + console.error('Error storing user data:', error); + return false; + } +} + +// Use safe operations +const userData = { name: 'John', level: 5 }; +if (safeSetUser(kv, '123', userData)) { + console.log('User data stored successfully'); +} +``` + +## Monitoring and Debugging + +### Database Statistics + +```ts +const kv = openKV('data.db'); + +// Get database statistics +function getDatabaseStats(kv: KV) { + const db = kv.getDatabase(); + + return { + isOpen: kv.isOpen(), + namespace: kv.getCurrentNamespace(), + entryCount: kv.count(), + totalKeys: kv.keys().length, + totalValues: kv.values().length, + databaseName: db.name, + databasePath: db.filename, + }; +} + +const stats = getDatabaseStats(kv); +console.log('Database stats:', stats); +``` + +### Performance Monitoring + +```ts +// Monitor operation performance +function measureOperation(operation: () => T): { + result: T; + duration: number; +} { + const start = performance.now(); + const result = operation(); + const duration = performance.now() - start; + + return { result, duration }; +} + +// Measure different operations +const setResult = measureOperation(() => kv.set('test', 'value')); +console.log(`Set operation took ${setResult.duration.toFixed(2)}ms`); + +const getResult = measureOperation(() => kv.get('test')); +console.log(`Get operation took ${getResult.duration.toFixed(2)}ms`); + +const countResult = measureOperation(() => kv.count()); +console.log(`Count operation took ${countResult.duration.toFixed(2)}ms`); +``` + +## Next Steps + +Now that you understand the advanced features, explore: + +- Integration with your Discord bot commands and events diff --git a/packages/commandkit/kv.cjs b/packages/commandkit/kv.cjs new file mode 100644 index 00000000..52d9b7a9 --- /dev/null +++ b/packages/commandkit/kv.cjs @@ -0,0 +1,6 @@ +const { KV, openKV } = require('./dist/kv/kv.js'); + +module.exports = { + KV, + openKV, +}; diff --git a/packages/commandkit/kv.d.ts b/packages/commandkit/kv.d.ts new file mode 100644 index 00000000..bf346631 --- /dev/null +++ b/packages/commandkit/kv.d.ts @@ -0,0 +1 @@ +export * from './dist/kv/kv'; diff --git a/packages/commandkit/package.json b/packages/commandkit/package.json index 2c1e129b..be08a033 100644 --- a/packages/commandkit/package.json +++ b/packages/commandkit/package.json @@ -44,7 +44,9 @@ "./semaphore.cjs", "./semaphore.d.ts", "./mutex.cjs", - "./mutex.d.ts" + "./mutex.d.ts", + "./kv.cjs", + "./kv.d.ts" ], "exports": { ".": { @@ -136,6 +138,11 @@ "require": "./mutex.cjs", "import": "./mutex.cjs", "types": "./mutex.d.ts" + }, + "./kv": { + "require": "./kv.cjs", + "import": "./kv.cjs", + "types": "./kv.d.ts" } }, "scripts": { diff --git a/packages/commandkit/src/kv/kv.ts b/packages/commandkit/src/kv/kv.ts new file mode 100644 index 00000000..45d20c87 --- /dev/null +++ b/packages/commandkit/src/kv/kv.ts @@ -0,0 +1,547 @@ +import { DatabaseSync, StatementSync } from 'node:sqlite'; + +/** + * Configuration options for the KV store + */ +export interface KvOptions { + /** Enable Write-Ahead Logging for better performance and durability */ + enableWAL?: boolean; + /** Namespace for the key-value store table */ + namespace?: string; +} + +/** + * A key-value store implementation using SQLite + * + * This class provides a simple, persistent key-value storage solution + * with support for namespaces, automatic cleanup, iteration, and expiration. + * + * @example + * ```typescript + * const kv = new KV('data.db'); + * kv.set('user:123', JSON.stringify({ name: 'John', age: 30 })); + * const user = JSON.parse(kv.get('user:123') || '{}'); + * + * // Using namespaces + * const userKv = kv.namespace('users'); + * userKv.set('123', JSON.stringify({ name: 'John' })); + * ``` + */ +export class KV implements Disposable, AsyncDisposable { + private db: DatabaseSync; + private statements: Record = {}; + + /** + * Creates a new KV store instance + * + * @param path - Database file path, buffer, URL, or existing DatabaseSync instance + * @param options - Configuration options for the KV store + */ + public constructor( + path: string | Buffer | URL | DatabaseSync, + private options: KvOptions = { + enableWAL: true, + namespace: 'commandkit_kv', + }, + ) { + this.db = + path instanceof DatabaseSync + ? path + : new DatabaseSync(path, { open: true }); + + if (options.enableWAL) { + this.db.exec(/* sql */ `PRAGMA journal_mode = WAL;`); + } + + this.db + .prepare( + /* sql */ ` + CREATE TABLE IF NOT EXISTS ? ( + key TEXT PRIMARY KEY, + value TEXT, + expires_at INTEGER + ) + `, + ) + .run(options.namespace ?? 'commandkit_kv'); + + this.statements = { + get: this.db.prepare( + /* sql */ `SELECT value, expires_at FROM ? WHERE key = ?`, + ), + set: this.db.prepare( + /* sql */ `INSERT OR REPLACE INTO ? (key, value, expires_at) VALUES (?, ?, ?)`, + ), + setex: this.db.prepare( + /* sql */ `INSERT OR REPLACE INTO ? (key, value, expires_at) VALUES (?, ?, ?)`, + ), + delete: this.db.prepare(/* sql */ `DELETE FROM ? WHERE key = ?`), + has: this.db.prepare( + /* sql */ `SELECT COUNT(*) FROM ? WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)`, + ), + keys: this.db.prepare( + /* sql */ `SELECT key FROM ? WHERE expires_at IS NULL OR expires_at > ?`, + ), + values: this.db.prepare( + /* sql */ `SELECT value FROM ? WHERE expires_at IS NULL OR expires_at > ?`, + ), + clear: this.db.prepare(/* sql */ `DELETE FROM ?`), + count: this.db.prepare( + /* sql */ `SELECT COUNT(*) FROM ? WHERE expires_at IS NULL OR expires_at > ?`, + ), + all: this.db.prepare( + /* sql */ `SELECT key, value FROM ? WHERE expires_at IS NULL OR expires_at > ?`, + ), + expire: this.db.prepare( + /* sql */ `UPDATE ? SET expires_at = ? WHERE key = ?`, + ), + ttl: this.db.prepare(/* sql */ `SELECT expires_at FROM ? WHERE key = ?`), + namespaces: this.db.prepare( + /* sql */ `SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`, + ), + begin: this.db.prepare(/* sql */ `BEGIN TRANSACTION`), + commit: this.db.prepare(/* sql */ `COMMIT`), + rollback: this.db.prepare(/* sql */ `ROLLBACK`), + }; + } + + /** + * Gets the current timestamp in milliseconds + */ + private getCurrentTime(): number { + return Date.now(); + } + + /** + * Checks if the database connection is open + * + * @returns `true` if the database is open, `false` otherwise + */ + public isOpen(): boolean { + return this.db.isOpen; + } + + /** + * Gets the underlying SQLite database instance + * + * @returns The DatabaseSync instance + */ + public getDatabase(): DatabaseSync { + return this.db; + } + + /** + * Closes the database connection + */ + public close(): void { + if (this.db.isOpen) this.db.close(); + } + + /** + * Disposable implementation - closes the database when disposed + */ + public [Symbol.dispose]() { + this.close(); + } + + /** + * AsyncDisposable implementation - closes the database when disposed + */ + public async [Symbol.asyncDispose]() { + this.close(); + } + + /** + * Retrieves a value by key + * + * @param key - The key to retrieve + * @returns The value associated with the key, or `undefined` if not found or expired + * + * @example + * ```typescript + * const value = kv.get('my-key'); + * if (value) { + * console.log('Found:', value); + * } + * ``` + */ + public get(key: string): string | undefined { + const result = this.statements.get.get(this.getCurrentNamespace(), key); + + if (!result) return undefined; + + // Check if the key has expired + if ( + result.expires_at && + Number(result.expires_at) <= this.getCurrentTime() + ) { + this.delete(key); + return undefined; + } + + return result.value as string; + } + + /** + * Sets a key-value pair + * + * @param key - The key to set + * @param value - The value to associate with the key + * + * @example + * ```typescript + * kv.set('user:123', JSON.stringify({ name: 'John' })); + * kv.set('counter', '42'); + * ``` + */ + public set(key: string, value: string): void { + this.statements.set.run(this.getCurrentNamespace(), key, value, null); + } + + /** + * Sets a key-value pair with expiration + * + * @param key - The key to set + * @param value - The value to associate with the key + * @param ttl - Time to live in milliseconds + * + * @example + * ```typescript + * // Set with 1 hour expiration + * kv.setex('session:123', 'user_data', 60 * 60 * 1000); + * + * // Set with 5 minutes expiration + * kv.setex('temp:data', 'cached_value', 5 * 60 * 1000); + * ``` + */ + public setex(key: string, value: string, ttl: number): void { + const expiresAt = this.getCurrentTime() + ttl; + this.statements.setex.run( + this.getCurrentNamespace(), + key, + value, + expiresAt, + ); + } + + /** + * Sets expiration for an existing key + * + * @param key - The key to set expiration for + * @param ttl - Time to live in milliseconds + * @returns `true` if the key exists and expiration was set, `false` otherwise + * + * @example + * ```typescript + * kv.set('user:123', 'user_data'); + * + * // Set 30 minute expiration + * if (kv.expire('user:123', 30 * 60 * 1000)) { + * console.log('Expiration set successfully'); + * } + * ``` + */ + public expire(key: string, ttl: number): boolean { + if (!this.has(key)) return false; + + const expiresAt = this.getCurrentTime() + ttl; + this.statements.expire.run(this.getCurrentNamespace(), expiresAt, key); + return true; + } + + /** + * Gets the time to live for a key + * + * @param key - The key to check + * @returns Time to live in milliseconds, or `-1` if the key doesn't exist, or `-2` if the key has no expiration + * + * @example + * ```typescript + * const ttl = kv.ttl('user:123'); + * if (ttl > 0) { + * console.log(`Key expires in ${ttl}ms`); + * } else if (ttl === -2) { + * console.log('Key has no expiration'); + * } else { + * console.log('Key does not exist'); + * } + * ``` + */ + public ttl(key: string): number { + const result = this.statements.ttl.get(this.getCurrentNamespace(), key); + + if (!result) return -1; // Key doesn't exist + + if (!result.expires_at) return -2; // No expiration + + const remaining = Number(result.expires_at) - this.getCurrentTime(); + return remaining > 0 ? remaining : -1; // Expired or doesn't exist + } + + /** + * Deletes a key-value pair + * + * @param key - The key to delete + * + * @example + * ```typescript + * kv.delete('user:123'); + * ``` + */ + public delete(key: string): void { + this.statements.delete.run(this.getCurrentNamespace(), key); + } + + /** + * Checks if a key exists and is not expired + * + * @param key - The key to check + * @returns `true` if the key exists and is not expired, `false` otherwise + * + * @example + * ```typescript + * if (kv.has('user:123')) { + * console.log('User exists and is not expired'); + * } + * ``` + */ + public has(key: string): boolean { + const result = this.statements.has.get( + this.getCurrentNamespace(), + key, + this.getCurrentTime(), + ); + + return ( + result?.count !== undefined && + result.count !== null && + Number(result.count) > 0 + ); + } + + /** + * Gets all keys in the current namespace (excluding expired keys) + * + * @returns Array of all non-expired keys + * + * @example + * ```typescript + * const keys = kv.keys(); + * console.log('All keys:', keys); + * ``` + */ + public keys(): string[] { + const result = this.statements.keys.all( + this.getCurrentNamespace(), + this.getCurrentTime(), + ); + + return result.map((row) => row.key as string); + } + + /** + * Gets all values in the current namespace (excluding expired keys) + * + * @returns Array of all non-expired values + * + * @example + * ```typescript + * const values = kv.values(); + * console.log('All values:', values); + * ``` + */ + public values(): string[] { + const result = this.statements.values.all( + this.getCurrentNamespace(), + this.getCurrentTime(), + ); + + return result.map((row) => row.value as string); + } + + /** + * Gets the total number of key-value pairs in the current namespace (excluding expired keys) + * + * @returns The count of non-expired key-value pairs + * + * @example + * ```typescript + * const count = kv.count(); + * console.log(`Total entries: ${count}`); + * ``` + */ + public count(): number { + const result = this.statements.count.get( + this.getCurrentNamespace(), + this.getCurrentTime(), + ); + + return Number(result?.count ?? 0); + } + + /** + * Removes all key-value pairs from the current namespace + * + * @example + * ```typescript + * kv.clear(); // Removes all entries in current namespace + * ``` + */ + public clear(): void { + this.statements.clear.run(this.getCurrentNamespace()); + } + + /** + * Gets all key-value pairs as an object (excluding expired keys) + * + * @returns Object with all non-expired key-value pairs + * + * @example + * ```typescript + * const all = kv.all(); + * console.log('All entries:', all); + * // Output: { 'key1': 'value1', 'key2': 'value2' } + * ``` + */ + public all(): Record { + const result = this.statements.all.all( + this.getCurrentNamespace(), + this.getCurrentTime(), + ); + + return Object.fromEntries( + result.map((row) => [row.key as string, row.value as string]), + ); + } + + /** + * Gets all available namespaces (tables) in the database + * + * @returns Array of namespace names + * + * @example + * ```typescript + * const namespaces = kv.namespaces(); + * console.log('Available namespaces:', namespaces); + * ``` + */ + public namespaces(): string[] { + const result = this.statements.namespaces.all(); + + return result.map((row) => row.name as string); + } + + /** + * Gets the current namespace name + * + * @returns The current namespace string + */ + public getCurrentNamespace(): string { + return this.options.namespace ?? 'commandkit_kv'; + } + + /** + * Creates a new KV instance with a different namespace + * + * @param namespace - The namespace to use for the new instance + * @returns A new KV instance with the specified namespace + * + * @example + * ```typescript + * const userKv = kv.namespace('users'); + * const configKv = kv.namespace('config'); + * + * userKv.set('123', 'John Doe'); + * configKv.set('theme', 'dark'); + * ``` + */ + public namespace(namespace: string): KV { + return new KV(this.db, { + enableWAL: this.options.enableWAL, + namespace, + }); + } + + /** + * Iterator implementation for iterating over all non-expired key-value pairs + * + * @returns Iterator yielding [key, value] tuples + * + * @example + * ```typescript + * for (const [key, value] of kv) { + * console.log(`${key}: ${value}`); + * } + * + * // Or using spread operator + * const entries = [...kv]; + * ``` + */ + public *[Symbol.iterator](): Iterator<[string, string]> { + const result = this.statements.all.iterate( + this.getCurrentNamespace(), + this.getCurrentTime(), + ); + + for (const row of result) { + yield [row.key as string, row.value as string]; + } + } + + /** + * Executes a function within a transaction + * + * @param fn - Function to execute within the transaction (can be async) + * @returns The result of the function + * + * @example + * ```typescript + * // Synchronous transaction + * kv.transaction(() => { + * kv.set('user:123', JSON.stringify({ name: 'John' })); + * kv.set('user:456', JSON.stringify({ name: 'Jane' })); + * // If any operation fails, all changes are rolled back + * }); + * + * // Async transaction + * await kv.transaction(async () => { + * kv.set('user:123', JSON.stringify({ name: 'John' })); + * await someAsyncOperation(); + * kv.set('user:456', JSON.stringify({ name: 'Jane' })); + * // If any operation fails, all changes are rolled back + * }); + * ``` + */ + public async transaction(fn: () => T | Promise): Promise { + try { + // Begin transaction + this.statements.begin.run(); + + // Execute the function + const result = await fn(); + + // Commit transaction + this.statements.commit.run(); + + return result; + } catch (error) { + // Rollback transaction on error + this.statements.rollback.run(); + throw error; + } + } +} + +/** + * Opens a new KV instance + * + * @param path - Database file path, buffer, URL, or existing DatabaseSync instance + * @param options - Configuration options for the KV store + * @returns A new KV instance + */ +export function openKV( + path: string | Buffer | URL | DatabaseSync = 'commandkit_kv.db', + options: KvOptions = { enableWAL: true, namespace: 'commandkit_kv' }, +): KV { + return new KV(path, options); +}