UNPKG

yq-store

Version:

A high-performance, persistent and in-memory key-value store with TTL support, built for Node.js applications.

734 lines (731 loc) 22.2 kB
/** * @fileoverview A generic, type-safe wrapper around Node.js's EventEmitter. */ /** * Provides a fully type-safe event emitter implementation. * @template TEvents A record mapping event names to their argument tuples. */ export declare class TypedEventEmitter<TEvents extends Record<string, any>> { private emitter; /** * Registers an event listener. Alias for `on`. * @param event The name of the event to listen for. * @param listener The callback function. */ addEventListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this; /** * Registers an event listener. * @param event The name of the event to listen for. * @param listener The callback function. */ on<E extends keyof TEvents>(event: E, listener: TEvents[E]): this; /** * Unregisters an event listener. Alias for `off`. * @param event The name of the event to stop listening to. * @param listener The callback function to remove. */ removeEventListener<E extends keyof TEvents>(event: E, listener: TEvents[E]): this; /** * Unregisters an event listener. * @param event The name of the event to stop listening to. * @param listener The callback function to remove. */ off<E extends keyof TEvents>(event: E, listener: TEvents[E]): this; /** * Emits an event, calling all registered listeners with the provided arguments. * @param event The name of the event to emit. * @param args The arguments to pass to the listeners. * @returns `true` if the event had listeners, `false` otherwise. */ emit<E extends keyof TEvents>(event: E, ...args: Parameters<TEvents[E]>): boolean; } /** * @fileoverview A high-performance, persistent, zero-dependency Key-Value store * with advanced querying and type-safe events. */ /** * Defines the configuration options for creating a new KvStore instance. */ export interface KvStoreOptions { /** * The interval in milliseconds for the store to automatically perform * compaction. Compaction is the process of rewriting the database file to * permanently remove data from expired, deleted, or updated keys. * This saves disk space and speeds up initial load times. * * @remarks * This is a global setting for database maintenance and is different from * the 'ttlSeconds' parameter on the `set` method, which applies a * Time-To-Live expiration to an individual key. * * Set to 0 to disable automatic compaction. * @default 3600000 (1 hour) */ compactionInterval?: number; /** * The default Time-To-Live (TTL) for keys in seconds. If set, each key will * automatically expire after this duration. If not set, keys will persist * until deleted. * * @remarks * This is a per-key setting and overrides the global `compactionInterval`. * If a key-specific TTL is set, it takes precedence over the global setting. * * Set to 0 to disable expiration for a key. * @default 0 (no expiration) */ ttl?: number; /** * Enable soft delete mode. When enabled, deleted entries are marked as deleted * but not physically removed from the database until compaction or manual cleanup. * When disabled, deleted entries are immediately removed from the database. * * @default true */ softDelete?: boolean; /** * Storage configuration. */ storage?: { /** * Storage type * - 'memory': In-memory SQLite database (':memory:') * - 'persistence': File-based SQLite database * @default 'persistence' */ type?: "memory" | "persistence"; /** * Maximum number of entries before LRU eviction kicks in * @default 50000 for memory mode, 1000000 for persistence mode */ maxEntries?: number; /** * Maximum memory usage in bytes (only applies to memory mode) * @default 104857600 (100MB) */ maxMemory?: number; /** * Persistence storage configuration */ persistence?: { /** * Directory path for storing the persistent database files. If not specified, * defaults to the system's temporary directory (os.tmpdir()). The database * filename will be an MD5 hash of __dirname if dbFileName is not provided. * * @remarks * For production use, it's recommended to specify a custom directory path * to ensure data persistence across system reboots. * * @default os.tmpdir() */ dbDir?: string; /** * Custom database file name (without extension) * @default auto-generated */ dbFileName?: string; /** * SQLite configuration */ sqlite?: { /** * Journal mode for SQLite * @default 'WAL' */ journalMode?: "DELETE" | "TRUNCATE" | "PERSIST" | "MEMORY" | "WAL" | "OFF"; /** * Synchronous mode for SQLite * @default 'NORMAL' */ synchronous?: "OFF" | "NORMAL" | "FULL" | "EXTRA"; /** * Cache size in pages (negative value = KB) * @default -10000 (10MB) */ cacheSize?: number; /** * Temporary storage location * @default 'memory' */ tempStore?: "default" | "file" | "memory"; /** * Enable foreign key constraints * @default false */ foreignKeys?: boolean; /** * Busy timeout in milliseconds * @default 30000 */ busyTimeout?: number; }; /** * Vacuum configuration */ vacuum?: { /** * Enable vacuum * @default true */ enabled?: boolean; /** * Vacuum mode * @default 'incremental' */ mode?: "none" | "incremental" | "full"; }; }; }; /** * Logging configuration */ logging?: { /** * Enable logging * @default false */ enabled?: boolean; /** * Log directory path * @default './logs' */ logDir?: string; }; /** * Debug mode configuration */ debug?: { /** * Enable debug mode for detailed logging and performance metrics * @default false */ enabled?: boolean; /** * Enable performance timing logs * @default false */ timing?: boolean; /** * Enable SQL query logging * @default false */ sqlLogging?: boolean; }; } /** * Defines the mapping of event names to their listener function signatures. * This ensures full type safety when using `store.on()` or `store.emit()`. */ export interface KvStoreEvents<T = unknown> { /** Emitted when the store has successfully initialized and is ready for use. */ ready: () => void; /** Emitted when the database is ready for operations. */ "db:ready": () => void; /** Emitted when a new key-value pair is successfully set. */ set: (key: string, value: T) => void; /** Emitted when a key is successfully deleted (either directly or via expiration). */ delete: (key: string) => void; /** Emitted when a key is found to be expired during a `get` or query operation. */ expire: (key: string) => void; /** Emitted when entries are evicted due to LRU or memory limits. */ evict: (key: string | number) => void; /** Emitted when a compaction process is about to begin. */ "compact:start": () => void; /** Emitted when a compaction process has successfully completed. */ "compact:end": (stats: { newSize: number; keysProcessed: number; }) => void; /** Emitted when a recoverable error occurs, such as a corrupted line in the DB file. */ error: (error: Error) => void; /** Emitted when the database file is closed and all resources are cleaned up. */ close: () => void; } /** * Represents the structure of a single entry in the append-only log file. */ export type LogEntry<T> = { key: string; value: T; expiresAt?: number; tombstone?: never; } | { key: string; value?: never; expiresAt?: never; tombstone: true; }; /** * In-memory metadata for a key, pointing to its location on disk. */ export interface IndexMeta { offset: number; size: number; expiresAt?: number; db?: string; } export interface PutBatchOperation { type: "put"; key: string; value: any; ttlSeconds?: number; } export interface DelBatchOperation { type: "del"; key: string; } export type BatchOperation = PutBatchOperation | DelBatchOperation; /** * A high-performance, persistent Key-Value store with SQLite backend. * Supports TTL, soft deletes, compaction, and type-safe event handling. * * @example * ```typescript * const store = await YqStore.create({ * storage: { type: 'persistence' }, * compactionInterval: 3600000, * softDelete: true * }); * * await store.set('key', { data: 'value' }, 60); // TTL of 60 seconds * const value = await store.get('key'); * await store.delete('key'); * await store.close(); * ``` */ export declare class YqStore { /** Configuration options for the store instance */ private readonly options; /** Timer for automatic compaction operations */ private compactionTimer?; /** Timer for automatic eviction of expired entries */ private evictionTimer?; /** Counter for tracking deleted entries (tombstones) */ private tombstoneCount; /** Flag indicating if compaction is currently in progress */ private isCompacting; /** Flag indicating if the store is being closed */ private isClosing; /** Event emitter for store events */ private readonly emitter; /** Storage manager instance handling data persistence */ private readonly storageManager; /** Database name for the storage file */ private dbName; private constructor(); /** * Creates and initializes a new YqStore instance. * * @param options - Configuration options for the store * @returns Promise that resolves to an initialized YqStore instance * * @example * ```typescript * const store = await YqStore.create({ * storage: { * type: 'persistence', * persistence: { dbDir: './data' } * }, * compactionInterval: 3600000 * }); * ``` */ static create(options?: KvStoreOptions): Promise<YqStore>; private initialize; /** * Registers an event listener for the specified event. * * @param event - The event name to listen for * @param listener - The callback function to execute when the event is emitted * @returns This YqStore instance for method chaining * * @example * ```typescript * store.on('ready', () => console.log('Store is ready')); * store.on('error', (error) => console.error('Store error:', error)); * ``` */ on<E extends keyof KvStoreEvents>(event: E, listener: KvStoreEvents[E]): this; /** * Removes an event listener for the specified event. * * @param event - The event name to stop listening for * @param listener - The callback function to remove * @returns This YqStore instance for method chaining */ off<E extends keyof KvStoreEvents>(event: E, listener: KvStoreEvents[E]): this; /** * Alias for the `on` method. Registers an event listener. * * @param event - The event name to listen for * @param listener - The callback function to execute when the event is emitted * @returns This YqStore instance for method chaining */ addEventListener<E extends keyof KvStoreEvents>(event: E, listener: KvStoreEvents[E]): this; /** * Alias for the `off` method. Removes an event listener. * * @param event - The event name to stop listening for * @param listener - The callback function to remove * @returns This YqStore instance for method chaining */ removeEventListener<E extends keyof KvStoreEvents>(event: E, listener: KvStoreEvents[E]): this; /** * Checks if a key exists in the store and is not expired. * * @param key - The key to check for existence * @returns Promise that resolves to true if the key exists and is valid, false otherwise * * @example * ```typescript * const exists = await store.has('myKey'); * if (exists) { * console.log('Key exists'); * } * ``` */ has(key: string): Promise<boolean>; /** * Retrieves a value from the store by its key. * * @template T - The expected type of the stored value * @param key - The key to retrieve the value for * @returns Promise that resolves to the stored value or null if not found/expired * * @example * ```typescript * const user = await store.get<User>('user:123'); * if (user) { * console.log('User found:', user.name); * } * ``` */ get<T>(key: string): Promise<T | null>; /** * Stores a value in the store with an optional TTL (Time To Live). * * @template T - The type of the value being stored * @param key - The key to store the value under * @param value - The value to store * @param ttlSeconds - Optional TTL in seconds. If provided, the key will expire after this duration * @returns Promise that resolves when the value is stored * * @example * ```typescript * // Store without TTL * await store.set('user:123', { name: 'John', age: 30 }); * * // Store with 60 second TTL * await store.set('session:abc', { token: 'xyz' }, 60); * ``` */ set<T>(key: string, value: T, ttlSeconds?: number): Promise<void>; /** * Deletes a key from the store. * * @param key - The key to delete * @returns Promise that resolves to true if the key was deleted, false if it didn't exist * * @example * ```typescript * const wasDeleted = await store.delete('user:123'); * if (wasDeleted) { * console.log('User deleted successfully'); * } * ``` */ delete(key: string): Promise<boolean>; /** * Gets the number of active entries in the store. * * @returns Promise that resolves to the count of active entries * * @example * ```typescript * const count = await store.getSize(); * ``` */ getSize(): Promise<number>; /** * Gets the number of entries in the store by type. * * @param type - The type of entries to count: 'active', 'deleted', 'expired', or 'all' * @returns Promise that resolves to the count of entries * * @example * ```typescript * const activeCount = await store.getSize('active'); * const totalCount = await store.getSize('all'); * ``` */ getSize(type: "active" | "deleted" | "expired" | "all"): Promise<number>; /** * Gets the number of active entries in the store with prefix filtering. * * @param options - Filtering options with prefix * @param options.prefix - Only count entries with keys that start with this prefix * @returns Promise that resolves to the count of entries matching the prefix * * @example * ```typescript * const userCount = await store.getSize({ prefix: 'user:' }); * ``` */ getSize(options: { prefix: string; }): Promise<number>; /** * Gets the number of entries in the store with suffix filtering. * * @param options - Filtering options with suffix * @param options.suffix - Only count entries with keys that end with this suffix * @returns Promise that resolves to the count of entries * * @example * ```typescript * const sessionCount = await store.getSize({ suffix: ':session' }); * ``` */ getSize(options: { suffix: string; }): Promise<number>; /** * Gets the number of entries in the store by type with prefix filtering. * * @param type - The type of entries to count: 'active', 'deleted', 'expired', or 'all' * @param options - Filtering options with prefix * @param options.prefix - Only count entries with keys that start with this prefix * @returns Promise that resolves to the count of entries * * @example * ```typescript * const activeUserCount = await store.getSize('active', { prefix: 'user:' }); * ``` */ getSize(type: "active" | "deleted" | "expired" | "all", options: { prefix: string; }): Promise<number>; /** * Gets the number of entries in the store by type with suffix filtering. * * @param type - The type of entries to count: 'active', 'deleted', 'expired', or 'all' * @param options - Filtering options with suffix * @param options.suffix - Only count entries with keys that end with this suffix * @returns Promise that resolves to the count of entries * * @example * ```typescript * const activeSessionCount = await store.getSize('active', { suffix: ':session' }); * ``` */ getSize(type: "active" | "deleted" | "expired" | "all", options: { suffix: string; }): Promise<number>; /** * Lists all keys in the store. * * @returns Promise that resolves to an array of all keys * * @example * ```typescript * const allKeys = await store.listKeys(); * ``` */ listKeys(): Promise<string[]>; /** * Lists all keys in the store that start with the specified prefix. * * @param options - Filtering options with prefix * @param options.prefix - Only return keys that start with this prefix * @returns Promise that resolves to an array of matching keys * * @example * ```typescript * const userKeys = await store.listKeys({ prefix: 'user:' }); * ``` */ listKeys(options: { prefix: string; }): Promise<string[]>; /** * Lists all keys in the store that end with the specified suffix. * * @param options - Filtering options with suffix * @param options.suffix - Only return keys that end with this suffix * @returns Promise that resolves to an array of matching keys * * @example * ```typescript * const sessionKeys = await store.listKeys({ suffix: ':session' }); * ``` */ listKeys(options: { suffix: string; }): Promise<string[]>; /** * Iterates over all key-value pairs in the store that match the given criteria. * * @template T - The expected type of the stored values * @param callback - Function to call for each key-value pair * @param options - Filtering and limiting options * @param options.prefix - Only iterate over keys that start with this prefix * @param options.suffix - Only iterate over keys that end with this suffix * @param options.limit - Maximum number of entries to iterate over * @returns Promise that resolves when iteration is complete * * @example * ```typescript * await store.forEach<User>((key, user) => { * console.log(`User ${key}: ${user.name}`); * }, { prefix: 'user:', limit: 100 }); * ``` */ forEach<T>(callback: (key: string, value: T) => void, options?: { prefix?: string; suffix?: string; limit?: number; }): Promise<void>; /** * Retrieves a list of key-value pairs that match the given criteria with pagination support. * * @template T - The expected type of the stored values * @param options - Filtering, limiting, and pagination options * @param options.prefix - Only return entries with keys that start with this prefix * @param options.suffix - Only return entries with keys that end with this suffix * @param options.limit - Maximum number of entries to return (default: 100) * @param options.page - Page number for pagination (default: 1) * @returns Promise that resolves to an array of key-value pairs * * @example * ```typescript * // Get first 50 users * const users = await store.list<User>({ * prefix: 'user:', * limit: 50, * page: 1 * }); * * users.forEach(({ key, value }) => { * console.log(`${key}: ${value.name}`); * }); * ``` */ list<T>(options?: { prefix?: string; suffix?: string; limit?: number; page?: number; }): Promise<Array<{ key: string; value: T; }>>; /** * Permanently removes all soft-deleted entries from the store. * * @returns Promise that resolves to the number of entries that were permanently deleted * * @example * ```typescript * const deletedCount = await store.clearSoftDeletedEntries(); * console.log(`Permanently deleted ${deletedCount} entries`); * ``` */ clearSoftDeletedEntries(): Promise<number>; /** * Permanently removes all expired entries from the store. * An entry is considered expired if its TTL has elapsed. * This method can be called manually to free up storage space. * * @returns Promise that resolves to the number of expired entries that were removed * * @throws {Error} If there is an error accessing the storage * * @example * ```typescript * // Remove all expired entries and get count * const removedCount = await store.clearExpiredEntries(); * console.log(`Removed ${removedCount} expired entries`); * ``` * * @see {@link clearSoftDeletedEntries} for removing soft-deleted entries */ clearExpiredEntries(): Promise<number>; /** * Manually triggers compaction of the store to reclaim space and improve performance. * Compaction removes soft-deleted entries and optimizes the storage structure. * * @returns Promise that resolves when compaction is complete * * @example * ```typescript * // Manually trigger compaction * await store.compact(); * console.log('Store compaction completed'); * ``` */ compact(): Promise<void>; private checkAndTriggerCompaction; /** * Executes multiple operations atomically in a single transaction. * * @param operations - Array of operations to execute * @returns Promise that resolves when all operations are complete * * @example * ```typescript * await store.batch([ * { type: 'put', key: 'user:1', value: { name: 'John' } }, * { type: 'put', key: 'user:2', value: { name: 'Jane' } }, * { type: 'del', key: 'user:3' } * ]); * ``` */ batch(operations: BatchOperation[]): Promise<void>; /** * Creates a new transaction for batching multiple operations. * Transactions provide ACID guarantees and can be committed or rolled back. * * @returns Transaction object with put, del, commit, and rollback methods * * @example * ```typescript * const tx = store.createTransaction(); * try { * tx.put('user:1', { name: 'John' }); * tx.put('user:2', { name: 'Jane' }); * tx.del('user:3'); * await tx.commit(); * } catch (error) { * tx.rollback(); * throw error; * } * ``` */ createTransaction(): { put<T>(key: string, value: T, ttlSeconds?: number): void; del(key: string): void; commit(): Promise<void>; rollback(): void; }; /** * Closes the store and releases all resources. * This should be called when the store is no longer needed. * * @returns Promise that resolves when the store is fully closed * * @example * ```typescript * // Gracefully close the store * await store.close(); * console.log('Store closed successfully'); * ``` */ close(): Promise<void>; private checkAndTriggerEviction; private evictExpiredEntries; } export { YqStore as default, }; export {};