Coder Social home page Coder Social logo

kvs's Introduction

KVS Actions Status: test

Key Value storage for Browser, Node.js, and In-Memory.

It is a monorepo for key-value storage.

Motivation

I want to get universal storage library that works on Browser and Node.js.

Previously, I've created localstorage-ponyfill for this purpose. However, Window.localStorage does not work on Web Workers or Service Worker

@kvs/* packages provide async storage API using IndexedDB etc and resolve this issue.

Common Features

KVS libraries provide following common features.

  • Key-Value Storage
  • Async Read, and Write API
    • provide get, set, has, delete, and clear API
  • Migration API
    • Upgrade storage data via version and upgrade method
  • Tiny packages
    • Almost package size is 1kb(gzip)
  • TypeScript
    • All packages are written by TypeScript

Support Browsers

  • A browser that support AsyncIterator
    • It requires ES2018+ supports
  • Chromium-based(Google Chrome and MSEdge), Firefox, and macOS Safari

Packages

If you want to custom implementation, please see @kvs/storage and test it with @kvs/common-test-case.

Usage

@kvs/env support Browser and Node.js. In fact, browser use @kvs/indexeddb and Node.js use @kvs/node-localstorage.

import { KVSIndexedDB, kvsIndexedDB } from "@kvs/env";
(async () => {
    const storage = await kvsEnvStorage({
        name: "database-name",
        version: 1
    });
    await storage.set("a1", "string"); 
    const a1 = await storage.get("a1");
    console.log(a1); // => "string"
})();

API

@kvs/types define common interface.

Each constructor function like kvsEnvStorage return KVS object that has following methods. Also, KVS object define Symbol.asyncIterator, and you can iterate the storage by for await...of.

export type KVS<Schema extends StorageSchema> = {
    /**
     * Returns the value associated to the key.
     * If the key does not exist, returns `undefined`.
     */
    get<K extends StoreNames<Schema>>(key: K): Promise<StoreValue<Schema, K> | undefined>;
    /**
     * Sets the value for the key in the storage. Returns the storage.
     */
    set<K extends StoreNames<Schema>>(key: K, value: StoreValue<Schema, K> | undefined): Promise<KVS<Schema>>;
    /**
     * Returns a boolean asserting whether a value has been associated to the key in the storage.
     */
    has(key: StoreNames<Schema>): Promise<boolean>;
    /**
     * Returns true if an key in the storage existed and has been removed.
     * Returns false if the key does not exist.
     */
    delete(key: StoreNames<Schema>): Promise<boolean>;
    /**
     * Removes all key-value pairs from the storage.
     * Note: clear method does not delete the storage.
     * In other words, after clear(), the storage still has internal metadata like version.
     */
    clear(): Promise<void>;
    /**
     * Drop the storage.
     * It delete all data that includes metadata completely.
     */
    dropInstance(): Promise<void>;
    /*
     * Close the KVS connection
     * DB-like KVS close the connection via this method
     * Of course, localStorage-like KVS implement do nothing. It is just noop function
     */
    close(): Promise<void>;
} & AsyncIterable<[StoreNames<Schema>, StoreValue<Schema, StoreNames<Schema>>]>;

Basic Usage

import assert from "assert";
import { kvsEnvStorage } from "@kvs/env";
(async () => {
    type StorageSchema = {
        a1: string;
        b2: number;
        c3: boolean;
    };
    // open database and initialize it
    const storage = await kvsEnvStorage<StorageSchema>({
        name: "database-name",
        version: 1
    });
    // set
    await storage.set("a1", "string"); // type check
    await storage.set("b2", 42);
    await storage.set("c3", false);
    // has
    console.log(await storage.has("a1")); // => true
    // get
    const a1 = await storage.get("a1"); // a1 will be string type
    const b2 = await storage.get("b2");
    const c3 = await storage.get("c3");
    assert.strictEqual(a1, "string");
    assert.strictEqual(b2, 42);
    assert.strictEqual(c3, false);
    // iterate
    for await (const [key, value] of storage) {
        console.log([key, value]);
    }
    // delete
    await storage.delete("a1");
    // clear all data
    await storage.clear();
})();

Migration

KVS support migration feature. You can define upgrade and use it as migration function.

import { kvsEnvStorage } from "@kvs/env";
(async () => {
    // Defaut version: 1 
    // when update version 1 → 2, call upgrace function
    const storage = await kvsEnvStorage({
        name: "database-name",
        version: 2,
        async upgrade({ kvs, oldVersion }) {
            if (oldVersion < 2) {
                await kvs.set("v1", "v1-migrated-value"); // modify storage as migration
            }
            return;
        }
    });
    assert.strictEqual(await storage.get("v1"), "v1-migrated-value");
})();

First Initializing

When open database at first time, this library also call upgrade function with { oldVersion: 0, newVersion: 1 }. So, You can implement 0 to 1 migration as initializing database.

import { KVSIndexedDB, kvsIndexedDB } from "@kvs/env";
(async () => {
    const storage = await kvsEnvStorage({
        name: "database-name",
        version: 1,
        async upgrade({ kvs, oldVersion, newVersion }) {
            console.log(oldVersion); // => 0
            console.log(newVersion); // => 1
        }
    });
})();

TypeScript

KVS packages support Schema type. It helps you to define a schema of the storage.

import { KVSIndexedDB, kvsIndexedDB } from "@kvs/env";
(async () => {
    type StorageSchema = {
        a1: string;
        b2: number;
        c3: boolean;
    };
    const storage = await kvsEnvStorage<StorageSchema>({
        name: "database-name",
        version: 1
    });
    await storage.set("a1", "string"); // type check
    await storage.set("b2", 42);
    await storage.set("c3", false);
    const a1 = await storage.get("a1"); // a1 will be string type
    const b2 = await storage.get("b2");
    const c3 = await storage.get("c3");
    assert.strictEqual(a1, "string");
    assert.strictEqual(b2, 42);
    assert.strictEqual(c3, false);
})();

Tips: Initial Data

You can also set up initial data using upgrade function. This approach help you to improve Scheme typing.

(async () => {
    type UnixTimeStamp = number;
    type Scheme = {
        timeStamp: UnixTimeStamp
    };
    const storage = await kvsEnvStorage<Scheme>({
        name: "test-data",
        version: 1,
        async upgrade({ kvs, oldVersion, newVersion }) {
            // Initialize data
            // oldVersion is 0 and newVersion is 1 at first time
            if (oldVersion < 1) {
                await kvs.set("timeStamp", Date.now());
            }
        }
    });
    const timeStamp = await storage.get("timeStamp");
    console.log(timeStamp); // => timestamp
})()

Related

  • azu/localstorage-ponyfill
    • It provides storage API based on localStorage API
  • KV Storage
    • This proposal aims to create "async local storage", but it is suspended
    • @kvs project aims to be similar one
  • localForage
    • It has same concept and similar API.
    • However, localForage size is large ~8.8kB(gzipped)
  • Unstorage
    • Unstorage has various cloud storage support
    • However, Unstorage API is too rich for me
    • IndexedDB is not supported yet - Issue: unjs/unstorage#10
  • idb
    • @kvs type interface inspired by idb
    • If you want to use only IndexedDB directly, I recommend to use idb
    • It has low level API for IndexedDB

Changelog

See Releases page.

Development

This repository use Yarn. You need to build before editing each packages.

# install and link
yarn install
# build all package
yarn run build

Running tests

Running test via yarn test command.

yarn test

Contributing

Pull requests and stars are always welcome.

For bugs and feature requests, please create an issue.

  1. Fork it!
  2. Create your feature branch: git checkout -b my-new-feature
  3. Commit your changes: git commit -am 'Add some feature'
  4. Push to the branch: git push origin my-new-feature
  5. Submit a pull request :D

Author

License

MIT © azu

kvs's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

kvs's Issues

table(store) is not generated in indexeddb.

indexeddbでtable(store)が生成されない

便利なプラグインを作成いただきありがとうございます!

データベース'test'が存在しない状態で

kvsIndexedDB({
	name: "test",
	version: 4
});

のようにversionに1より大きい数を指定した場合、
データベース'test'は生成されますが、
table 'kvs'が生成されません。

version: 1を指定した場合は正しく
table 'kvs'が生成されます。

ソースのこの部分を下記のように、oldVersionの判定を追加する形にするとバージョンが大きい場合もテーブルが生成されるようになりました。

if (!newVersion || newVersion <= 1) {

- if (!newVersion || newVersion <= 1) {
+ if (!newVersion || newVersion <= 1 || oldVersion===0) {

現状の挙動は意図されたものでしょうか?
ご確認いただけますと幸いでございます。

Version: 2.1.4
OS: windows10
Browser: Chromium 116.0.5845.96

Safari 14 is hanged on opening IndexedDB

@kvs/indexeddb does not work in a specific case.
It is related to Safari 14 bug.

You may need to use following workaround.

import { KVSIndexedDB, kvsIndexedDB } from "@kvs/env";
import idbReady from 'safari-14-idb-fix';
(async () => {
    await idbReady(); // workaround for safari 14
    const storage = await kvsEnvStorage({
        name: "database-name",
        version: 1
    });
    await storage.set("a1", "string"); 
    const a1 = await storage.get("a1");
    console.log(a1); // => "string"
})();

Iterating a specific kvsEnvStorage returns all key-value pairs for all databases

Let's say I've got two collections:

const A = async () => {
return await kvsEnvStorage({
name: "a_col",
version: 1,
})
}

const B = async () => {
return await kvsEnvStorage({
name: "b_col",
version: 1,
})
}

const storage = await A()
for await (const [key, value] of storage) {
console.log(key, value)
}

Will print items that belong to storage B as well.

Is this an error of my usage, or a bug?

Uncaught TypeError: Failed to resolve module specifier "@kvs/env". Relative references must start with either "/", "./", or "../".

i am trying to run this simple demo code

import { kvsEnvStorage } from "@kvs/env";
const storage = await kvsEnvStorage({
    name: "database-name",
    version: 1
});
await storage.set("a1", "string"); // type check
await storage.set("b2", 42);
await storage.set("c3", false);
const a1 = await storage.get("a1"); // a1 will be string type
const b2 = await storage.get("b2");
const c3 = await storage.get("c3");

but i receive this error:
Uncaught TypeError: Failed to resolve module specifier "@kvs/env". Relative references must start with either "/", "./", or "../".

what am doing wrong here??

Note that i am trying to load the script from browser directly not from node.js. Any help @azu

get function returns JsonValue that needs to be re-cast every time

Hello again!

Here with a suggestion this time.

This is normally non issue but I couldn't find out a way to go with the set(customKey, list) signatures and I have to manually convert JSON to list every time. Maybe implementing .toList or .toDict sort of functions could be a good idea!

Sync Version

It's reasonable that we provide sync version of @kvs/localstorage.
Because sync localstorage usage is handly and fast.

I think that sync version is meaningful because KVS storage support migration feature and TypeScript.
It is useful to develop.

/**
* Sync Version
*/
// export type KVSSync<Schema extends StorageSchema> = {
// clear(): void;
// delete(key: KeyOf<Schema>): boolean;
// get<T extends KeyOf<Schema>>(key: keyof T): Schema[T] | undefined;
// has(key: KeyOf<Schema>): boolean;
// set<T extends KeyOf<Schema>>(key: T, value: Schema[T] | undefined): Schema;
// /*
// * Close the KVS connection
// * DB-like KVS close the connection via this method
// * Of course, localStorage-like KVS implement do nothing. It is just noop function
// */
// close(): void;
// } & Iterable<[KeyOf<Schema>, ValueOf<Schema>]>;
// export type KVSSyncOptions<Schema extends StorageSchema> = {
// name: string;
// version: number;
// upgrade?({ kvs, oldVersion, newVersion }: { kvs: KVS<Schema>; oldVersion: number; newVersion: number }): any;
// };

Need to discuss

  • Iterator is needed?
  • Support target - ES5 or ES2015

Related

first upgrade: 0 → 1

KVS should not call upgrade when the storage is initialized at first.
Instead of it, How to initialize storage or migrate from existing storage?

We should provide a document for first migration from another storage.

Version stays at 1

Version is always 1, no matter what you put in. It also causes the upgrade function to be called every time.

kvsEnvStorage({
  name: 'data',
  version: 2,
  upgrade: (_, old, new) => console.log('I am always called', old, new), // I am always called 1 2
})

Possibility to configure quota with node-localstorage?

I have received this here on a project I have been working on:

QUOTA_EXCEEDED_ERR: Unknown error.
    at LocalStorage.setItem (/node_modules/node-localstorage/LocalStorage.js:215:15)
    at setItem (/node_modules/@kvs/storage/src/storage.ts:34:20)
    at <anonymous> (/node_modules/@kvs/storage/src/storage.ts:156:35)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at run (/some-file.ts:97:17)

It would be good if we could pass something or the same default as a second argument here

storage: new LocalStorage(saveFilePath)

Looking at the node-localstorage implementation here:

https://github.com/lmaccherone/node-localstorage/blob/5498cdbd2e26819351eb505e71aae1feafdad81b/LocalStorage.coffee#L53

https://github.com/lmaccherone/node-localstorage/blob/5498cdbd2e26819351eb505e71aae1feafdad81b/LocalStorage.coffee#L145-L146

5 * 1024 * 1024 is the default value

I did just change the value up to 25 * 1024 * 1024 right in the dependency code for now, but I should probably do a PR for it instead.

Say something like...

export type KvsLocalStorageOptions<Schema extends KvsLocalStorageSchema> = KVSOptions<Schema> & {
    kvsVersionKey?: string;
    storeFilePath?: string;
    storeQuota?: number;
};
export const kvsLocalStorage = async <Schema extends KvsLocalStorageSchema>(
    options: KvsLocalStorageOptions<Schema>
): Promise<KvsStorage<Schema>> => {
    const defaultCacheDir = path.join(appRoot.toString(), ".cache");
    if (!options.storeFilePath) {
        await mkdirp(defaultCacheDir);
    }
    const saveFilePath = options.storeFilePath
        ? options.storeFilePath
        : path.join(defaultCacheDir, "kvs-node-localstorage");
    const storeQuota = options.storeQuota 
        ? options.storeQuota
        : 5 * 1024 * 1024
    return kvsStorage({
        ...options,
        storage: new LocalStorage(saveFilePath, storeQuota)
    });
};

How does this then work with @kvs/env ?

Improve AsyncIterable type

We can improve AsyncIterable typing.
AsyncIterable<[StoreNames<Schema>, StoreValue<Schema, StoreNames<Schema>>]> may be AsyncIterable<[K StoreValue<Schema, K>]>?

Store files as value?

Is it possible to save files? localforage handle files and serialization of objects out of the box.

TypeScript: Schema instead of <K,V>

Currently use <K,V> type for initializing.

export const kvsMemoryStorage = async <K extends KVSStorageKey, V extends JsonValue>(
    options: KvsLocalStorageOptions<K, V>
): Promise<KvsStorage<K, V>> => {
    return kvsStorage({
        ...options,
        storage: localstorage
    });
};

But, Schamea is useful, I Think

Reason

  • KVS needs to initialize. In other words, It is not a singleton
    • LocalStorage is a singleton
  • <K,V> is difficult to use correct type.

Related

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.