Skip to content

e280/kv

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

53 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸͺ‡ 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 default
    import {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 localStorage
    import {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 scopes for which i'll insert records
    const accounts = kv.scope<Account>("accounts")
    const characters = kv.scope<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 scope 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 pairs
    await kv.set("hello", "world")
  • set can save any serializable json-friendly javascript crap
    await kv.set("hello", {data: ["world"], count: 123.456})
  • set will interpret undefined 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
  • sets saves many pairs, as an atomic batch
    await 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 things
    await kv.del("hello")
  • del can also delete many things
    await kv.del("101", "102", "103")

Having stuff

  • has checks if a key exists
    await kv.has("hello")
      // true (or false)
  • hasKeys checks many keys
    await kv.hasKeys("101", "102", "103")
      // [true, true, false]

Fancy stuff

  • require gets a value, but throws an error if the key is missing
    await kv.require("101")
      // "world" (or an error is thrown)
  • requires gets many things, throws an error if any keys are missing
    await kv.requires("101", "102")
      // ["alpha", {data: 123.45}] (or an error is thrown)
  • guarantee gets or creates a thing
    await 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, and write.del to schedule write operations into the transaction

Scopes keep things tidy

  • a scope is just a Kv instance that has a key prefix assigned
    const records = kv.scope("records")
    
    // writes to key "records:123"
    await records.set("123", "lol")
  • a scope can do everything a Kv can do (it is a Kv)
    const records = kv.scope("records")
    await records.set("124", {data: "bingus"})
    await records.transaction(write => [write.del("124")])
  • yes, you can scope a scope β€” it's turtles all the way down
    const records = kv.scope("records")
    const owners = records.scope("owners")
    const accounts = records.scope("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 scope with a typescript type
    type MyData = {count: number}
    
      //                  provide your type
      //                           πŸ‘‡
    const records = kv.scope<MyData>("records")
    
    // now typescript knows `count` is a number
    const {count} = records.get("123")
  • you can in fact do transactional writes across multiple scopes
    const records = kv.scope("records")
    const owners = records.scope("owners")
    const accounts = records.scope("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}),
    ])
  • scopes automatically place a ":" delimiter to separate namespaces from subkeys
    const records = kv.scope("records")
    const alpha = records.scope("alpha")
    
    await records.set(1, "hello")
      // writes to key "records:1"
    
    await alpha.set(2, "hello")
      // writes to key "records.alpha:2"
    • no keys will collide between records.keys() and alpha.keys()
    • however, you can deliberately flatten a scope, which allows you to select all keys across all sub scopes
      const flatRecords = records.flatten()
      
      for await (const key of records.keys())
        console.log(key)
          // "records:1"
          // "records.alpha:2"

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

About

key-value storage for typescript

Resources

License

Stars

Watchers

Forks