Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

141 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

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

๐Ÿช‡ 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"
    
    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.
    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) {/*...*/}
    }
    see magazines/memory.ts for inspiration.

๐Ÿช‡ kv scopes.

  • scope makes 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
  • โ˜ฃ๏ธ 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.

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

๐Ÿช‡ more kv methods.

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

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

    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
    await muffins.delete()
    you can pass typed Cell<X> instances all around your app.
    import {Cell} from "@e280/kv"
    
    class MuffinCaptain {
      constructor(public muffins: Cell<number>) {}
    }



https://e280.org/

About

๐Ÿช‡ tiny key-value storage library

Resources

Stars

Watchers

Forks

Releases

Used by

Contributors

Languages