diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c44c7f..1a2d65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.12.2 + +- Only throw in `.sync()` on closed database if the force parameter is true. +- Make all relevant methods (`.get()`, `.set()`, `.iterate()`, `.listAll()` ...) + type safe using generics +- Update docs + ## 0.12.1 - Add method `.isOpen()` diff --git a/README.md b/README.md index 98582b2..1e816ff 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -# cross/kv: A Fast Key/Value Database for Node, Deno and Bun. +# cross/kv: Fast Key/Value Database for Node, Deno and Bun. [![JSR](https://jsr.io/badges/@cross/kv)](https://jsr.io/@cross/kv) -[![JSR Score](https://jsr.io/badges/@/@cross/kv)](https://jsr.io/@cross/kv) +[![JSR Score](https://jsr.io/badges/@cross/kv)](https://jsr.io/@cross/kv) -An in-memory indexed and file based Key/Value database for JavaScript and -TypeScript, designed for seamless multi-process access and compatibility across -Node.js, Deno, and Bun. +A lightweight, fast, powerful and cross-platform key-value database for Node.js, +Deno, and Bun. ```typescript import { KV } from "@cross/kv"; @@ -35,23 +34,26 @@ db.close(); ## Features -- **Efficient Key-Value Storage:** Rapid storage and retrieval using - hierarchical keys and a high-performance in-memory index. -- **Durable Transactions:** Ensure data integrity and recoverability through an - append-only transaction ledger. -- **Atomic Transactions:** Guarantee data consistency by grouping multiple - operations into a single, indivisible unit. -- **Optimized Storage:** Reclaim disk space and maintain performance through - vacuuming operations. -- **Cross-Platform & Multi-Process:** Built in pure TypeScript, working - seamlessly across Node.js, Deno, and Bun, supporting concurrent access by - multiple processes. -- **Flexible & Customizable:** Store any JavaScript object, subscribe to data - changes, and fine-tune synchronization behavior. +## Features -## Installation +- **Cross-Platform & Multi-Process:** Built with pure TypeScript for seamless + compatibility across Node.js, Deno, and Bun, with built-in support for + concurrent access by multiple processes. +- **Powerful:** Supports hierarchical keys, flexible mid-key range queries, and + real-time data change notifications through `.watch()`. +- **Simple and Fast:** Lightweight and performant storage with an in-memory + index for efficient data retrieval. +- **Durable:** Ensures data integrity and reliability by storing each database + as a single, append-only transaction ledger. +- **Type-Safe:** Leverages TypeScript generics for enhanced type safety when + setting and retrieving values. +- **Atomic Transactions:** Guarantees data consistency by grouping multiple + operations into indivisible units, which also improves performance. +- **Flexible:** Store any serializable JavaScript object (except functions and + WeakMaps), and customize synchronization behavior to optimize for your + specific use case. -Full installation instructions available at +## Installation ```bash # Using npm @@ -72,18 +74,18 @@ bunx jsr add @cross/kv are optional. - `async open(filepath, createIfMissing)` - Opens the KV store. `createIfMissing` defaults to true. - - `async set(key, value)` - Stores a value. - - `async get(key)` - Retrieves a value. - - `async *iterate(query)` - Iterates over entries for a key. + - `async set(key, value)` - Stores a value. + - `async get(key)` - Retrieves a value. + - `async *iterate(query)` - Iterates over entries for a key. - `listKeys(query)` - List all keys under . - - `async listAll(query)` - Gets all entries for a key as an array. + - `async listAll(query)` - Gets all entries for a key as an array. - `async delete(key)` - Deletes a key-value pair. - `async sync()` - Synchronizez the ledger with disk. - - `watch(query, callback, recursive): void` - Registers a callback to be + - `watch(query, callback, recursive): void` - Registers a callback to be called whenever a new transaction matching the given query is added to the database. - - `unwatch(query, callback): void` - Unregisters a previously registered watch - handler. + - `unwatch(query, callback): void` - Unregisters a previously registered + watch handler. - `beginTransaction()` - Starts an atomic transaction. - `async endTransaction()` - Ends an atomic transaction, returns a list of `Errors` if any occurred. @@ -126,9 +128,9 @@ including: ### Queries -Queries are basically keys, but with additional support for ranges, which are +Queries are similar to keys but with additional support for ranges, specified as objects like `{ from: 5, to: 20 }` or `{ from: "a", to: "l" }`. An empty range -(`{}`) means any document. +(`{}`) matches any document. **Example queries** @@ -159,36 +161,24 @@ transaction. ### Single-Process Synchronization -In single-process scenarios, explicit synchronization is unnecessary. You can -disable automatic synchronization by setting the `autoSync` option to false, and -do not have to care about running `.sync()`. This can improve performance when -only one process is accessing the database. +In single-process scenarios, explicit synchronization is often unnecessary. You +can disable automatic synchronization by setting the `autoSync` option to +`false`, eliminating automated `.sync()` calls. This can potentially improve +performance when only one process accesses the database. ### Multi-Process Synchronisation -In multi-process scenarios, synchronization is crucial to ensure data -consistency across different processes. `cross/kv` manages synchronization in -the following ways: - -- **Automatic Index Synchronization:** The index is automatically synchronized - at a set interval (default: 1000ms), ensuring that changes made by other - processes are reflected in all instances within a maximum of `syncIntervalMs` - milliseconds. You can adjust this interval using the `syncIntervalMs` option. +In multi-process scenarios, synchronization is essential for maintaining data +consistency. `cross/kv` offers automatic index synchronization upon each data +insertion and at a configurable interval (default: 1000ms). Customizing this +interval providing fine-grained control over the trade-off between consistency +and performance. For strict consistency guarantees, you can manually call +`.sync()` before reading data. -- **Manual Synchronization for Reads:** When reading data, you have two options: - - - **Accept Potential Inconsistency:** By default, reads do not trigger an - immediate synchronization, which can lead to a small window of inconsistency - if another process has recently written to the database. This is generally - acceptable for most use cases. - - - **Force Synchronization:** For strict consistency, you can manually trigger - synchronization before reading using the `.sync()` method: - - ```ts - await kv.sync(); // Ensure the most up-to-date data - const result = await kv.get(["my", "key"]); // Now read with confidence - ``` +```ts +await kv.sync(); // Ensure the most up-to-date data +const result = await kv.get(["my", "key"]); // Now read with confidence +``` ### Monitoring Synchronization Events diff --git a/deno.json b/deno.json index cf55566..420100e 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@cross/kv", - "version": "0.12.1", + "version": "0.12.2", "exports": { ".": "./mod.ts" }, diff --git a/src/kv.ts b/src/kv.ts index 880a922..4700a40 100644 --- a/src/kv.ts +++ b/src/kv.ts @@ -42,7 +42,7 @@ export interface KVSyncResult { /** * A function that is called when a watched transaction occurs. */ -export interface WatchHandler { +export interface WatchHandler { /** * The query used to filter the transactions. */ @@ -50,7 +50,7 @@ export interface WatchHandler { /** * The callback function that will be called when a transaction matches the query. */ - callback: (transaction: KVTransactionResult) => void; + callback: (transaction: KVTransactionResult) => void; /** * Whether to include child keys */ @@ -95,7 +95,7 @@ export class KV extends EventEmitter { private index: KVIndex = new KVIndex(); private ledger?: KVLedger; private pendingTransactions: KVTransaction[] = []; - private watchHandlers: WatchHandler[] = []; + private watchHandlers: WatchHandler[] = []; // Configuration private ledgerPath?: string; @@ -244,7 +244,7 @@ export class KV extends EventEmitter { */ public async sync(force = false, doLock = true): Promise { // Throw if database isn't open - this.ensureOpen(); + if (force) this.ensureOpen(); // Ensure ledger is open and instance is not closed if (this.ledger?.isClosing() || this.aborted) { @@ -403,11 +403,13 @@ export class KV extends EventEmitter { * @param key - Representation of the key. * @returns A promise that resolves to the retrieved value, or null if not found. */ - public async get(key: KVKey): Promise { + public async get( + key: KVKey, + ): Promise | null> { // Throw if database isn't open this.ensureOpen(); - for await (const entry of this.iterate(key, 1)) { + for await (const entry of this.iterate(key, 1)) { return entry; } return null; @@ -435,10 +437,10 @@ export class KV extends EventEmitter { * const allEntries = await kvStore.list(["users"]); * console.log(allEntries); */ - public async *iterate( + public async *iterate( key: KVQuery, limit?: number, - ): AsyncGenerator { + ): AsyncGenerator> { // Throw if database isn't open this.ensureOpen(); @@ -469,12 +471,14 @@ export class KV extends EventEmitter { * @param key - Representation of the key to query. * @returns A Promise that resolves to an array of all matching data entries. */ - public async listAll(key: KVQuery): Promise { + public async listAll( + key: KVQuery, + ): Promise[]> { // Throw if database isn't open this.ensureOpen(); - const entries: KVTransactionResult[] = []; - for await (const entry of this.iterate(key)) { + const entries: KVTransactionResult[] = []; + for await (const entry of this.iterate(key)) { entries.push(entry); } return entries; @@ -507,10 +511,7 @@ export class KV extends EventEmitter { * @param key - Representation of the key. * @param value - The value to store. */ - public async set( - key: KVKey, - value: any, - ): Promise { + public async set(key: KVKey, value: T): Promise { // Throw if database isn't open this.ensureOpen(); @@ -669,9 +670,9 @@ export class KV extends EventEmitter { * @param query - The query to match against new transactions. * @param callback - The callback function to be called when a match is found. The callback will receive the matching transaction as its argument. */ - public watch( + public watch( query: KVQuery, - callback: (transaction: KVTransactionResult) => void, + callback: (transaction: KVTransactionResult) => void, recursive: boolean = false, ) { this.watchHandlers.push({ query, callback, recursive }); @@ -687,9 +688,9 @@ export class KV extends EventEmitter { * * @returns True on success */ - public unwatch( + public unwatch( query: KVQuery, - callback: (transaction: KVTransactionResult) => void, + callback: (transaction: KVTransactionResult) => void, ): boolean { const newWatchHandlers = this.watchHandlers.filter( (handler) => handler.query !== query || handler.callback !== callback, diff --git a/src/transaction.ts b/src/transaction.ts index c803610..6e41cfc 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -77,7 +77,7 @@ export type KVTransactionData = Uint8Array; /** * Represents a single transaction result from the Key-Value store. */ -export interface KVTransactionResult { +export interface KVTransactionResult { /** * The key associated with the transaction. */ @@ -98,7 +98,7 @@ export interface KVTransactionResult { * For SET operations, this will be the value that was set. * For DELETE operations, this will typically be null or undefined. */ - data: unknown; + data: T; /** * The hash of the raw transaction data. This can be used for @@ -242,7 +242,7 @@ export class KVTransaction { return fullData; } - private getData(): unknown | null { + private getData(): T | null { // Return data, should be validated through create or fromUint8Array if (this.data) { return decode(this.data); @@ -255,7 +255,7 @@ export class KVTransaction { * Converts the transaction to a KVTransactionResult object. * This assumes that the transaction's data is already validated or created correctly. */ - public asResult(): KVTransactionResult { + public asResult(): KVTransactionResult { if ( this.operation === undefined || this.timestamp === undefined || this.hash === undefined @@ -268,7 +268,7 @@ export class KVTransaction { key: this.key!.get() as KVKey, operation: this.operation, timestamp: this.timestamp, - data: this.getData(), + data: this.getData() as T, hash: this.hash, }; } diff --git a/test/kv.test.ts b/test/kv.test.ts index b31f535..d8dd719 100644 --- a/test/kv.test.ts +++ b/test/kv.test.ts @@ -270,7 +270,7 @@ test("KV: iteration with limit", async () => { // Iterate with a limit of 3 const limit = 3; - const results: KVTransactionResult[] = []; + const results: KVTransactionResult[] = []; for await (const entry of kvStore.iterate(["data"], limit)) { results.push(entry); } @@ -383,7 +383,7 @@ test("KV: sync event triggers and reflects data changes", async () => { await kvStore1.open(tempFilePrefix); await kvStore2.open(tempFilePrefix); - let syncedData: KVTransactionResult[] = []; + let syncedData: KVTransactionResult[] = []; // Listen for the "sync" event on the second instance // @ts-ignore ksStore2 is an EventEmitter @@ -415,7 +415,7 @@ test("KV: watch functionality - basic matching", async () => { await kvStore.open(tempFilePrefix); const watchedKey = ["user", "profile"]; - let receivedTransaction: KVTransactionResult | null = null; + let receivedTransaction: KVTransactionResult | null = null; // Watch for a specific key kvStore.watch(watchedKey, (transaction) => { @@ -436,7 +436,7 @@ test("KV: watch functionality - recursive matching", async () => { const kvStore = new KV({ autoSync: false }); await kvStore.open(tempFilePrefix); - const receivedTransactions: KVTransactionResult[] = []; + const receivedTransactions: KVTransactionResult[] = []; const query: KVQuery = ["users"]; @@ -461,7 +461,7 @@ test("KV: watch functionality - range matching", async () => { const kvStore = new KV({ autoSync: false }); await kvStore.open(tempFilePrefix); - const receivedTransactions: KVTransactionResult[] = []; + const receivedTransactions: KVTransactionResult[] = []; kvStore.watch(["scores", { from: 10, to: 20 }], (transaction) => { receivedTransactions.push(transaction);