ブラウザとNode.jsで動く1kBのキーバリューストレージライブラリを書いた
azu/kvsというブラウザとNode.jsで動くファイルサイズが小さいキーバリューストレージを作りました。
モチベーション
ファイルサイズが小さくIndexedDBを使っていて、Node.jsでも透過的に同じAPIで利用できるライブラリが必要となったため作りました。
textlint-editorというアプリを書いていて、キャッシュストレージとしてlocalstorage-ponyfillを使っていました。 しかし、localstorage-ponyfillはブラウザとNode.jsで透過的に動くストレージライブラリですが、LocalStorageベースとなっています。 textlint-editorでは、スクリプトをWeb Workerで動かすため同期的なAPIであるLocalStorageは利用できません。
そのため、IndexedDBベースでシンプルなキーバリューストレージを扱えるものが必要でした。
有名所ではlocalForageがこの条件を満たしますが、ファイルサイズが9kb弱(min+gzip)あります。
10kb(gzip)はUIを作るような主要ライブラリのサイズ感なので、1-2kb(gzip)程度のキーバリューストレージが欲しかったです。
フレームワークとかのライブラリに許されるのは、170 - 130 = 40kb(gzip+minify)として、
— azu (@azu_re) September 28, 2020
大体1つのページに機能が豊富とかUIを作るような主要なライブラリは2~4つあるとしたら、
大きめの1つのライブラリに許されるファイルサイズは 10kb ~ 20kb ぐらい(gzip+min)
色々探してもなかったのでKVSというライブラリを作りました。
KVS Packages
azu/kvsはいくつかのパッケージを集めたmonorepoになっています。 基本的にはどれのパッケージも同じインターフェースですが、読み書きする先がIndexedDB、LocalStorage、ファイル、メモリなど実装が異なっています。
基本的には@kvs/envというパッケージを使えば、 自動的にブラウザとNode.jsに合わせたパッケージを使い分けしてくれます。
- Universal
- @kvs/env: Use suitable storage for platform
- Use IndexedDB for Browser, and Use node-localstorage for Node.js
- @kvs/env: Use suitable storage for platform
- Browser
- @kvs/indexeddb: Use IndexedDB
- For WebWorker and ServiceWorker
- @kvs/localstorage: Use localStorage
- For Browser
- @kvs/indexeddb: Use IndexedDB
- Node.js
- @kvs/node-localstorage: Use node-localstorage
- For Node.js
- @kvs/node-localstorage: Use node-localstorage
- In-Memory
- @kvs/memorystorage: In-Memory Storage
- For debug and testing
- @kvs/memorystorage: In-Memory Storage
- Sync Version
- @kvs/storage-sync: Sync version of @kvs/localstorage
また自分で任意のバックエンドでStorageも作成できます。 インターフェイスとして@kvs/storageとテストケースを提供する@kvs/common-test-caseがあるので利用してください。
Features
azu/kvsは基本的に次の機能を持っています。
- Key-ValueなAPI
get
,set
,has
,delete
,clear
- 列挙はAsync Iteratorを使うので
for await of
で列挙します
- バージョン管理とマイグレーションAPI
version
の指定とupgrade
メソッドを実装することでマイグレーションができます- 初めてデータベースを作るときも
0
から1
へとupgrade
されるので初期データも作成できます
- 小さなファイルサイズ
- 基本的に1kb(min+gzip)程度のファイルサイズです
- https://bundlephobia.com/result?p=@kvs/env
- TypeScript
- TypeScriptでデータベースのスキーマを設定できます
- 単純なKey-Valueな型より強めの制約を作れます
Examples
基本的なTypeScriptでのサンプルコードは次のような感じです。
StorageSchema
というように、storageのインスタンスを作るときスキーマの型を渡します。
こうすることで、get
やset
などもすべて型が付いた状態になり、スキーマに定義されていないKey-Valueはコンパイルエラーになります。
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();
})();
このスキーマ型はidbを意識した作りです。 (idbはキーバリューストレージではなく、IndexedDBの薄いラッパーが目的のライブラリ)
localForageのようなgetItem
やsetItem
でGenericsを渡す方式では、
ストレージに入ってる値の一覧がインターフェイスからわからない問題や書き忘れるとanyとなるため、最初にスキーマで定義するようにしています。
// localforage での例
import localforage from "localforage"
(async () => {
await localforage.setItem<string>('key', 'value')
const value = localforage.getItem<string>('key')
console.log(typeof value === "string"); // => true
})();
また、@kvs/envでのデータのマイグレーションは次のように書けます。
一度データを作るとversion
が1
となるので、version
を上げればupgrade
メソッドが呼ばれます。
upgrade
メソッドにマイグレーションの処理を書く仕組みです。
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");
})();
おわりに
ファイルサイズが1KB程度でIndexedDBに対応していてTypeScriptで書けるキーバリューストレージとして@kvs/envを作りました。
マイグレーションの仕組みが思っていたよりも便利で、またScehma
で型定義できると結構TypeScriptで使いやすいです。
IteratorをサポートしていないIE11でも動くようなもっとシンプルなlocalStorage
ラッパーも同じ仕組みがあると便利なのかもしれません。
次のIssueがあるので、興味がある人はやってみてください。
Related
- azu/localstorage-ponyfill: Universal LocalStorage for browser and Node.js.
- 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)
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。