@e280/kv v0.0.0-8
🪇 Kv
Json Key-value Storage for TypeScript.
Damn simple typescript database. String keys. Json values.
Kv is an agnostic interface. You insert different drivers, which allows Kv to write data in-memory, or to local storage, or to leveldb, or wherever you want.
Kv does smart stuff, like namespacing, batch operations, and atomic write transactions.
Get started
Install Kv into your project
npm install @e280/kv
Make your Kv instance
Kv uses the in-memory
MemDriver
by defaultimport {Kv} from "@e280/kv" const kv = new Kv()
or alternatively, pop in a
LevelDriver
to use leveldb, a local on-disk database (kinda like sqlite)import {Kv} from "@e280/kv" import {LevelDriver} from "@e280/kv/level" const kv = new Kv(new LevelDriver("path/to/database"))
or alternatively, pop in a
StorageDriver
to use browser localStorageimport {Kv, StorageDriver} from "@e280/kv" const kv = new Kv(new StorageDriver())
Get and set key-value pairs
The most basic thing you can do with Kv, is write and read values using string keys.
await kv.set("101", "hello") await kv.set("102", 123.456) await kv.get("101") // "hello" await kv.get("102") // 123.456 await kv.get("103") // undefined
Kv usage
Example usage walkthrough
so, for my use case, i'm doing stuff like saving user accounts, it might give you an idea of how Kv is meant to be used
// create a kv instance const kv = new Kv() // creating some typed namespaces for which i'll insert records const accounts = kv.namespace<Account>("accounts") const characters = kv.namespace<Character>("characters") // my app's function for adding a character to an account async function addCharacter(accountId: string, character: Character) { // obtain the account const account = await accounts.require(accountId) // actually uses key `accounts:${accountId}` because of the namespace prefix // modifying the data character.ownerId = account.id account.characterIds.push(character.id) // create an atomic write transaction to save the data await kv.transaction(() => [ accounts.write.set(account.id, account), characters.write.set(character.id, character), ]) } // my app's function for listing all characters async function listCharacters(accountId: string) { const account = await accounts.require(accountId) return characters.requires(...account.characterIds) }
Functionality reference
Setting stuff
set
saves key-value pairsawait kv.set("hello", "world")
set
can save any serializable json-friendly javascript crapawait kv.set("hello", {data: ["world"], count: 123.456})
set
will interpretundefined
as the same as a deletion (like json)await kv.set("hello", undefined) // same as deleting "hello"
- like json you can use
null
instead of you want the key to exist
- like json you can use
sets
saves many pairs, as an atomic batchawait kv.sets(["101", "alpha"], ["102", "bravo"])
Getting stuff
get
loads a value (or undefined if the key's not found)await kv.get("101") // "alpha" (or undefined)
gets
loads many values at once (undefined for not-found keys)await kv.gets("101", "102", "103") // ["alpha", "bravo", undefined]
Deleting stuff
del
deletes thingsawait kv.del("hello")
del
can also delete many thingsawait kv.del("101", "102", "103")
Having stuff
has
checks if a key existsawait kv.has("hello") // true (or false)
hasKeys
checks many keysawait kv.hasKeys("101", "102", "103") // [true, true, false]
Fancy stuff
require
gets a value, but throws an error if the key is missingawait kv.require("101") // "world" (or an error is thrown)
requires
gets many things, throws an error if any keys are missingawait kv.requires("101", "102") // ["alpha", {data: 123.45}] (or an error is thrown)
guarantee
gets or creates a thingawait kv.guarantee("hello", () => "world") // "world"
Transactions make you cool and incredible
- make an atomic transaction, where the writes happen all-or-nothing to avoid corruption
// all these succeed or fail together await kv.transaction(write => [ write.del("obsolete:99"), write.set("owners:4", [101, 102]), write.sets( ["records:101", {msg: "lol", owner: 4}], ["records:102", {msg: "lel", owner: 4}], ), ])
- you can use
write.set
,write.sets
, andwrite.del
to schedule write operations into the transaction
- you can use
Namespaces keep things tidy
a namespace is just a Kv instance that has a key prefix assigned
const records = kv.namespace("records") // writes to key "records:123" await records.set("123", "lol")
a namespace can do everything a Kv can do (it is a Kv)
const records = kv.namespace("records") await records.set("124", {data: "bingus"}) await records.transaction(write => [write.del("124")])
yes, you can namespace a namespace — it's turtles all the way down
const records = kv.namespace("records") const owners = records.namespace("owners") const accounts = records.namespace("accounts") // writes to key "records.owners:5" await owners.set("5", "lol") // writes to key "records.accounts:123" await accounts.set("123", "rofl")
you can constrain a namespace with a typescript type
type MyData = {count: number} // provide your type // 👇 const records = kv.namespace<MyData>("records") // now typescript knows `count` is a number const {count} = records.get("123")
you can in fact do transactional writes across multiple namespaces
const records = kv.namespace("records") const owners = records.namespace("owners") const accounts = records.namespace("accounts") await kv.transaction(() => [ owners.write.set("5", {records: [101, 102]}), accounts.write.set("101", {data: "alpha", owner: 5}), accounts.write.set("102", {data: "bravo", owner: 5}), ])
Stores keep you focused
a store is an object that focuses on reading/writing the value of a single key
const login = kv.store<Login>("login") // save data to the store await login.set({token: "lol"}) // load data from the store const {token} = await login.get()
Drivers
- if you want Kv to operate on a new database, it's pretty easy to write a new Driver
- here is the abstract Driver class you'd have to extend
export abstract class Driver { abstract gets(...keys: string[]): Promise<(string | undefined)[]> abstract hasKeys(...keys: string[]): Promise<boolean[]> abstract keys(scan?: Scan): AsyncGenerator<string> abstract entries(scan?: Scan): AsyncGenerator<[string, string]> abstract transaction(...writes: Write[]): Promise<void> }
- then you can just provide your new driver to the Kv constructor, eg
// instance your new driver and give it to Kv const kv = new Kv(new MyDriver())
- see drivers/mem.ts
- see drivers/level.ts
- see drivers/storage.ts
- you can do it!
💖 Made with open source love
- free and open source
- build with us at https://e280.org/ but only if you're cool
- star this on github if you think it's cool