npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@e280/kv

v0.1.0

Published

Simple abstract key-value json database

Readme

🪇 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