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.
constkv=newKv()
- set and get stuff.
awaitkv.set("penguins",123)// setting undefined is the same as delete
awaitkv.get("penguins")// 123
- keys are strings. values can be any structured data.
awaitkv.set("hello",{alpha: 123,bravo: ["bingus"]})
- commit batches of ops, atomically.
awaitkv.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"constkv=newKv(newMemoryMagazine())
- LevelMagazine, nodejs on-disk leveldb.
import{Level}from"level"import{Kv,LevelMagazine}from"@e280/kv"constlevel=newLevel("./kv")constkv=newKv(newLevelMagazine(level))
- IdbMagazine, in-browser indexedDB storage.
import{Kv,IdbMagazine,idbOpen}from"@e280/kv"constidb=awaitidbOpen("kv")constkv=newKv(newIdbMagazine(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"constkv=newKv(newStorageMagazine(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!exportclassMyMagazineimplementsMagazine{asynccommit(ops: Op<Value>[]){/*...*/}asyncgetMany(keys: string[]){/*...*/}async*entries(scan?: Scan){/*...*/}}
scopemakes namespaced Kv instances.constusers=kv.scope("users")constmessages=kv.scope("messages")
awaitusers.set("111","chase")awaitmessages.set("222",["111","yo"])
awaitkv.get("111")// undefined// 👮 parent is blind to child entriesawaitusers.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")// 🥸 equivalentkv.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.
constanimals=kv.scope("animals")constturtles=animals.scope("turtles")constsquirrels=animals.scope("squirrels")
awaitanimals.clear()// 👮 turtles and squirrels are safe
awaitsquirrels.clear()// 👮 turtles are safe
- ☣️
subtreeis dangerous, it allows a parent scope to hurt its children.it returns a special
Subtreeinstance that only hascountandclearmethods.awaitanimals.subtree.count()// count includes animals, turtles, and squirrels
awaitanimals.subtree.clear()// 💀 wipes out the turtles and squirrels. i'm sorry.
- don't forget you can set strict types, on both Kv and scopes.
constkv=newKv<unknown>()constusers=kv.scope<string>("users")constmessages=kv.scope<[author: string,text: string]>("messages")
- 🍋🟩 commits can be cross-scoped, don't miss this!
awaitkv.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.awaitkv.delete("hello")
awaitkv.delete("123","234","345")
haschecks whether a key exists.awaitkv.has("hello")// true
setManysets many key-value pairs at once.awaitkv.setMany([["1","alpha"],["2","bravo"]])
getManyretrieves many values at once.constvalues=awaitkv.getMany(["alpha","bravo"])// [123, undefined]
needretrieves a value, or throws if the value is missing/nullish.constvalue=awaitkv.need("hello")// "world" (or throws error)
needManyretrieves many values, or throws on missing/nullish values.constvalues=awaitkv.needMany(["alpha","bravo"])// [123, 234] (or throws error)
entriesloops over key-value pairs.it's aliased toforawait(const[key,value]ofkv.entries())console.log(key,value)
Symbol.asyncIterator, so you can do this:theforawait(const[key,value]ofkv)console.log(key,value)
entriesmethod accepts scan options.💡 you can useforawait(const[key,value]ofkv.entries({limit: 100,reverse: false,start: "alpha",// inclusiveend: "omega",// exclusive}))console.log(key,value)
collecthelper from@e280/stzto get entries/keys/values as an array:import{collect}from"@e280/stz"constentries=awaitcollect(kv)// [["123", "alpha"], ["234", "bravo"]]constkeys=awaitcollect(kv.keys())// ["123", "234"]
keysandvalues.(accepts scan options)forawait(constkeyofkv.keys())console.log(key)forawait(constvalueofkv.values())console.log(value)
countthe number of entries in this scope. (accepts scan options)awaitkv.count()// 123
cleardeletes everything in this scope. (accepts scan options)awaitkv.clear()
cellmakes a little cubby, for storing a single value.(it implements
@e280/stz'sCubbytype)constmuffins=kv.cell<number>("muffins")
awaitmuffins.set(99)
awaitmuffins.has()// true or falseawaitmuffins.get()// number or undefinedawaitmuffins.need()// number or throws error
you can pass typedawaitmuffins.delete()
Cell<X>instances all around your app.import{Cell}from"@e280/kv"classMuffinCaptain{constructor(publicmuffins: Cell<number>){}}
