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 🙏

© 2024 – Pkg Stats / Ryan Hefner

aokv

v0.0.2

Published

Append-only key-value store

Downloads

11

Readme

This is an append-only key-value store. It is a key-value store that you can write to (but only by appending), and then, after finishing writing, read back like any normal key-value store.

What and why?

The main use case of this is to deal with the fragmented world of storing data from a browser.

You can store data in localStorage, an indexed DB, or the origin-private filesystem, but that storage is extremely fragile. It gets destroyed when the browser feels like it. That's bad for the web app, and bad for the user.

You can store data in cloud storage, but that requires a whole extra interface and sometimes-onerous auditing and licensing. Plus, it's not really a solution to storing user data if you don't allow them to store it themselves.

On Chrome, you can store data in a local directory, by using showDirectoryPicker. This is one of the better options, but has some fragilities:

  • It's a Chrome-specific option (plus all the Chomealikes).
  • The user interface can be extremely confusing.
  • The createWritable interface for files is fragile to early cancellation: if you cancel writing (e.g. by navigating away from the page), the intermediary file is deleted. So, you need to make sure to write many small files, instead of several large files or any streaming files.

(The previous two options can be implemented by my own nonlocalForage )

Or, you can download the data with something like StreamSaver.js. But, if your data isn't naturally file-like, or has many subparts, what exactly do you put into that stream?

AOKV is the answer to that question. It provides an interface for using a stream of bytes as a write-only key-value store. That stream can be streamed as a downloaded file with something like StreamSaver.js. The user can then select that file later (and get a File object), and AOKV provides an interface to use that as a read-only key-value store. The data is saved eagerly, so early interruption still yields a valid store.

Note that if you're using this with a download, as suggested above, and the download is either explicitly canceled or implicitly canceled (e.g., due to running out of disk space), most browsers delete the intermediary file, so all data is lost. Oh well, can't fix everything...

How

aokv.js exposes an object AOKV (if importing or using require, this object is the export). AOKV.AOKVW is a class for writing AOKV streams, and AOKV.AOKVR is a class for reading AOKV streams.

aokvr.js exposes AOKVR with only the reading side (AOKVR.AOKVR), and aokvw.js exposes AOKVW with only the writing side (AOKVW.AOKVW). You can also import this module as "aokv/read" to get only the reading side, or "aokv/write" to get only the writing side.

Writing

Create an AOKV writer instance with w = new AOKV.AOKVW();.

AOKVW takes an optional parameter, an object describing file options:

{
    /**
     * Optional identifier to distinguish your application's AOKV files from
     * other AOKV files.
     */
    fileId?: number,

    /**
     * Optional function to compress a data chunk.
     */
    compress?: (x: Uint8Array) => Promise<Uint8Array>
}

The file ID, if used, is to distinguish your application's AOKV files from other AOKV files. You must use the same ID for writing and reading.

The optional compression function, if present, will be used to compress each entry in the store.

The AOKVW object exposes its output as the field stream (e.g., w.stream), which is a ReadableStream of Uint8Array chunks. You should start reading from this stream as soon as you create the AOKVW, so data doesn't buffer.

To set an item in the store, use await w.setItem(key, value);. The key must be a string, and the value can be anything JSON-serializable, or any ArrayBuffer or TypedArray. This and many other names are chosen to be familiar to users of localForage

await w.removeItem(key); is provided to “remove” an item from the store, but it's important to note that nothing can truly be removed, since the store is only ever appended to. Instead, this is just a convenience function to set the item to null, as getItem (below) returns null for items that are not in the store.

AOKVW also provides a size method, which returns the amount of data that's been written to the stream so far, in bytes, e.g., w.size(). You do not need to await w.size().

Because AOKV files are append-only stores, you should be mindful of how you use them. If you set the same key over and over again, you will take a lot of space, because the previous, discarded values are all still saved. The size is monotonically increasing.

To end the stream, use await w.end(). This is technically optional, as truncated AOKV files are valid, but probably useful for whatever you're using to read the stream.

Reading

Create an AOKV reader instance with r = new AOKV.AOKVR({...});. The options object is mandatory, and has the following form:

{
    /**
     * Total size of the file, in bytes, if known.
     */
    size?: number,

    /**
     * Function for reading from the input.
     */
    pread: preadT,

    /**
     * Optional identifier to distinguish your application's AOKV files from
     * other AOKV files. Must match the write ID.
     */
    fileId?: number,

    /**
     * Optional function to decompress. Must match the write compression.
     */
    decompress?: (x: Uint8Array) => Promise<Uint8Array>
}

If you know the file's size, you should provide it, as it will speed up indexing.

pread is a function of the form (count: number, offset: number) => Promise<Uint8Array | null> which should read count bytes from offset, returning the read data as a Uint8Array. A short read or null are acceptable returns for end-of-file. size is the size of the file, in bytes.

The file ID, if present, should be the same as used in AOKVW, and decompress, if present, should be the reverse of compress in AOKVW.

As it is common to use AOKVR with Blobs (or Files, which are a subtype of Blob), a convenience function is provided to create a pread for Blobs, AOKV.blobToPread. Use it like so: r = new AOKV.AOKVR({size: file.size, pread: AOKV.blobToPread(file)});.

Once you've created the AOKVR instance, before accessing data, you must index the file. Do so with await r.index();. r.index has some options to control how it validates that this is an AOKV file, but they should usually be left as default.

After indexing, there are two accessors available. Use r.keys() to get an array of all the keys in the store. It is not necessary to await r.keys(), as the indexing process makes the list of available keys eagerly.

Use await r.getItem(key) to get the item associated with the given key. This function will return null if the key is not set, if the data for this key was truncated, or (of course) if it was set to null.

Because AOKV files are append-only stores, every value assigned to any key is technically available in the file. The reader interface only exposes the last one (which is the standard behavior of a key-value store).

Format

AOKV files are written in native endianness, so typically little-endian.

An AOKV file consists of a sequence of AOKV blocks. There is no header to the entire AOKV file; instead, an AOKV file can be recognized by the header to the first block in the file. There are two types of AOKV blocks: key-value pair blocks, and index blocks.

Each block consists of a header, content, and footer. The header consists of three 32-bit unsigned integers. The first two are just identification magic, and the third is the size of the entire block, including the header and footer. The first magic word is always 0x564b4f41. Note that in little-endian, the first value is the ASCII string "AOKV".

The footer is the distance, in bytes, back from the footer itself to the nearest index block.

KVP blocks

The content of a key-value pair block consists of the length of the key in bytes, the key, and a body.

The key length is encoded as a 32-bit unsigned integer.

The key is simply a UTF-8 string.

The length of the body is inferred from the length of the block and the length of the key.

The body consists of the length of the descriptor in bytes, the descriptor, and a “post”. The length of the descriptor is written as a 32-bit unsigned integer.

The descriptor is a serialized JSON object with the following format:

interface Descriptor {
    /**
     * Type of the serialized data.
     */
    t: SerType,

    /**
     * If typed array or array buffer, type of the typed array.
     */
    a?: string,

    /**
     * If JSON, the data itself.
     */
    d?: any
}

The t field is 0 for JSON, 1 for a TypedArray, and 2 for an ArrayBuffer. If the serialized data is JSON, then its entire serialized value is in the descriptor (the d field), and the post is absent.

If the serialized value is a TypedArray, then the a field specifies (by string) which type, e.g. "Uint8ClampedArray". The post is the raw data in the typed array. Only the accessible portion is stored, not the entire ArrayBuffer.

If the serialized value is an ArrayBuffer, then neither a or d are used in the descriptor, and the post is the raw data in the buffer.

If compression is used, the body is compressed. The header and key are not, for fast indexing.

Even if compression is used, the data is written uncompressed if compression didn't actually reduce the size of the body. Because every descriptor starts with {, it is possible to determine if a body is compressed by checking if the fifth byte is {. Because this is the method to check for compression, if the compression function happens to output a byte sequence in which the fifth byte is {, then it isn't used, and the data is written uncompressed, even if compression would have reduced the size.

Index blocks

An index block is an index of all of the key-value pairs written so far.

The content of an index block is the index, which is a JSON-encoded mapping of keys to [size, offset] pairs. The sizes and offsets are absolute.

It is possible to recreate any AOKV file's index without an index block, but for large files, it is much faster to recreate with it.

Indices can be compressed, like the body of KVP blocks.