tiny key-value storage library.
typescript. node or web. memory, leveldb, localstorage, or indexeddb. scoped namespaces and atomic write batches. pass scoped and typed kv instances around to your app components, they don't need to worry about where the data actually lives.
npm install @e280/kvimport {Kv} from "@e280/kv"- make your kv.
const kv = new Kv()
- set and get stuff.
await kv.set("penguins", 123) // setting undefined is the same as delete
await kv.get("penguins") // 123
- keys are strings. values can be any structured data.
await kv.set("hello", {alpha: 123, bravo: ["bingus"]})
- commit batches of ops, atomically.
await kv.commit([ kv.op.set("pangolins", 100), kv.op.delete("bingus"), ])
- MemoryMagazine (default), ephemeral in-memory storage.
memory magazine is slow and non-atomic. it's meant for testing.
import {Kv, MemoryMagazine} from "@e280/kv" const kv = new Kv(new MemoryMagazine())
- LevelMagazine, nodejs on-disk leveldb.
import {Level} from "level" import {Kv, LevelMagazine} from "@e280/kv" const level = new Level("./kv") const kv = new Kv(new LevelMagazine(level))
- IdbMagazine, in-browser indexedDB storage.
import {Kv, IdbMagazine, idbOpen} from "@e280/kv" const idb = await idbOpen("kv") const kv = new Kv(new IdbMagazine(idb))
- StorageMagazine, in-browser localStorage/sessionStorage.
storage magazine is slow and non-atomic. it's meant for small amounts of data.
import {Kv, StorageMagazine} from "@e280/kv" const kv = new Kv(new StorageMagazine(localStorage))
- write your own magazine, you won't believe how easy it is.
see magazines/memory.ts for inspiration.
import {Magazine, Op, Scan, Value} from "@e280/kv" // three methods and you're done! export class MyMagazine implements Magazine { async commit(ops: Op<Value>[]) {/*...*/} async getMany(keys: string[]) {/*...*/} async* entries(scan?: Scan) {/*...*/} }
scopemakes namespaced Kv instances.const users = kv.scope("users") const messages = kv.scope("messages")
await users.set("111", "chase") await messages.set("222", ["111", "yo"])
await kv.get("111") // undefined // ๐ฎ parent is blind to child entries await users.get("222") // undefined // ๐ฎ child is blind to sibling and parent entries
- scopes are nestable, and it's turtles all the way down.
kv.scope("animals").scope("turtles") // ๐ฅธ equivalent kv.scope("animals", "turtles") // ๐ illegal: empty strings, reserved characters "." and ":" kv.scope("", "e280.org", "e280:org")
- all kv operations are isolated to their own scope.
the root kv is a scope like any other.
const animals = kv.scope("animals") const turtles = animals.scope("turtles") const squirrels = animals.scope("squirrels")
await animals.clear() // ๐ฎ turtles and squirrels are safe
await squirrels.clear() // ๐ฎ turtles are safe
- โฃ๏ธ
subtreeis dangerous, it allows a parent scope to hurt its children.it returns a special
Subtreeinstance that only hascountandclearmethods.await animals.subtree.count() // count includes animals, turtles, and squirrels
await animals.subtree.clear() // ๐ wipes out the turtles and squirrels. i'm sorry.
- don't forget you can set strict types, on both Kv and scopes.
const kv = new Kv<unknown>() const users = kv.scope<string>("users") const messages = kv.scope<[author: string, text: string]>("messages")
- ๐โ๐ฉ commits can be cross-scoped, don't miss this!
await kv.commit([ users.op.set("345", "bingus"), messages.op.set("456", ["345", "don't let the raccoons know"]), ])
deletea pair by its key.you can also pass multiple keys.await kv.delete("hello")
await kv.delete("123", "234", "345")
haschecks whether a key exists.await kv.has("hello") // true
setManysets many key-value pairs at once.await kv.setMany([["1", "alpha"], ["2", "bravo"]])
getManyretrieves many values at once.const values = await kv.getMany(["alpha", "bravo"]) // [123, undefined]
needretrieves a value, or throws if the value is missing/nullish.const value = await kv.need("hello") // "world" (or throws error)
needManyretrieves many values, or throws on missing/nullish values.const values = await kv.needMany(["alpha", "bravo"]) // [123, 234] (or throws error)
entriesloops over key-value pairs.it's aliased tofor await (const [key, value] of kv.entries()) console.log(key, value)
Symbol.asyncIterator, so you can do this:thefor await (const [key, value] of kv) console.log(key, value)
entriesmethod accepts scan options.๐ก you can usefor await (const [key, value] of kv.entries({ limit: 100, reverse: false, start: "alpha", // inclusive end: "omega", // exclusive })) console.log(key, value)
collecthelper from@e280/stzto get entries/keys/values as an array:import {collect} from "@e280/stz" const entries = await collect(kv) // [["123", "alpha"], ["234", "bravo"]] const keys = await collect(kv.keys()) // ["123", "234"]
keysandvalues. (accepts scan options)for await (const key of kv.keys()) console.log(key) for await (const value of kv.values()) console.log(value)
countthe number of entries in this scope. (accepts scan options)await kv.count() // 123
cleardeletes everything in this scope. (accepts scan options)await kv.clear()
cellmakes a little cubby, for storing a single value.(it implements
@e280/stz'sCubbytype)const muffins = kv.cell<number>("muffins")
await muffins.set(99)
await muffins.has() // true or false await muffins.get() // number or undefined await muffins.need() // number or throws error
you can pass typedawait muffins.delete()
Cell<X>instances all around your app.import {Cell} from "@e280/kv" class MuffinCaptain { constructor(public muffins: Cell<number>) {} }
