Skip to content

Repository files navigation

🪇 kv

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/kv
import{Kv}from"@e280/kv"

🪇 kv is easy.

  • 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"),])

🪇 plug in your favorite kv magazine.

  • 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.
    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){/*...*/}}
    see magazines/memory.ts for inspiration.

🪇 kv scopes.

  • scope makes 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
  • ☣️ subtree is dangerous, it allows a parent scope to hurt its children.

    it returns a special Subtree instance that only has count and clear methods.

    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"]),])

🪇 more kv methods.

  • delete a pair by its key.
    awaitkv.delete("hello")
    you can also pass multiple keys.
    awaitkv.delete("123","234","345")
  • has checks whether a key exists.
    awaitkv.has("hello")// true
  • setMany sets many key-value pairs at once.
    awaitkv.setMany([["1","alpha"],["2","bravo"]])
  • getMany retrieves many values at once.
    constvalues=awaitkv.getMany(["alpha","bravo"])// [123, undefined]
  • need retrieves a value, or throws if the value is missing/nullish.
    constvalue=awaitkv.need("hello")// "world" (or throws error)
  • needMany retrieves many values, or throws on missing/nullish values.
    constvalues=awaitkv.needMany(["alpha","bravo"])// [123, 234] (or throws error)
  • entries loops over key-value pairs.
    forawait(const[key,value]ofkv.entries())console.log(key,value)
    it's aliased to Symbol.asyncIterator, so you can do this:
    forawait(const[key,value]ofkv)console.log(key,value)
    the entries method accepts scan options.
    forawait(const[key,value]ofkv.entries({limit: 100,reverse: false,start: "alpha",// inclusiveend: "omega",// exclusive}))console.log(key,value)
    💡 you can use collect helper from @e280/stz to 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"]
  • keys and values.(accepts scan options)
    forawait(constkeyofkv.keys())console.log(key)forawait(constvalueofkv.values())console.log(value)
  • count the number of entries in this scope. (accepts scan options)
    awaitkv.count()// 123
  • clear deletes everything in this scope. (accepts scan options)
    awaitkv.clear()
  • cell makes a little cubby, for storing a single value.

    (it implements @e280/stz's Cubby type)

    constmuffins=kv.cell<number>("muffins")
    awaitmuffins.set(99)
    awaitmuffins.has()// true or falseawaitmuffins.get()// number or undefinedawaitmuffins.need()// number or throws error
    awaitmuffins.delete()
    you can pass typed Cell<X> instances all around your app.
    import{Cell}from"@e280/kv"classMuffinCaptain{constructor(publicmuffins: Cell<number>){}}



https://e280.org/

About

🪇 tiny key-value storage library

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages