kevy4.0

WebAssembly上のkevy

kevyはブラウザの中で、コンパイルが通るだけの珍品ではなく本物のストアとして動きます。npmパッケージ@goliajp/kevyは、wasm32-unknown-unknown向けにコンパイルしたエンジン(KV+TTL+カウンタ+スキャン+pub/sub)を手書きのESモジュールローダーの後ろに載せて出荷し、OPFS(IndexedDBフォールバック付き)による永続化と、タブをまたぐpub/subを備えます。同じクレート群はwasm32-wasip1向けにもビルドできるので、Rust APIはwasmtimewasmerやエッジランタイムでも動きます。

ライブで試せます。kevy.golia.jpのデモは、まさにこのモジュールの上のブラウザREPLです——コマンド、リロードを生き延びるOPFS永続化、タブ間pub/subを、バックエンドなしで。

クイックスタート(ブラウザ)

npm install @goliajp/kevy
import { open } from "@goliajp/kevy";

const db = await open({ persist: { name: "app" } });

db.set("greeting", "hello");
db.set("session", "abc123", { ttlMs: 60_000 });
db.getText("greeting");            // "hello"
db.incrby("visits");               // 1, 2, 3, ...
db.keys("user:*");

// Pub/sub — including other tabs of the same origin:
const off = db.subscribe("events", (payload, channel) => {
  console.log(channel, new TextDecoder().decode(payload));
});
db.publish("events", "hi from this or any other tab");

await db.flush();                  // durability barrier

書き込みはkevyの追記専用ログとしてストレージへストリームされ、同じpersist.nameでの次回open()時にリプレイされます。パッケージは6ファイル(パック後約165 KB)です。wasmモジュール、ローダー、OPFSワーカー、手書きのTypeScript型定義、そしていつものREADMEとマニフェスト。境界のどちら側も依存ゼロです。

ローダーAPI

open(options)はモジュールをインスタンス化し、(永続化が有効なら)保存済みログをリプレイし、tickタイマーとタブ間ブリッジを開始します。オプションは次の通りです。

オプションデフォルト意味
wasmローダーの隣のkevy.wasmモジュールのソース。URL、ArrayBufferUint8ArrayResponse、またはコンパイル済みWebAssembly.Module
persistfalse(インメモリ){ name, backend }nameごとに1つのログ。backend"auto"(OPFS、IndexedDBフォールバック)、"opfs""idb"
broadcasttrueBroadcastChannelによるタブ間pub/subブリッジ
namepersist.nameまたは"kevy"インスタンス名。ストレージファイルとブロードキャストチャネルの両方をスコープする
tickMs100TTL掃除+イベントポーリングの周期。0でタイマー無効——自分でtick()を呼ぶ

返されるKevyハンドル(完全なシグネチャはkevy.d.ts):

メソッド意味論
set(key, value, { ttlMs? })SET。オプションで期限付き
get(key) / getText(key)GETをUint8Arrayで/UTF-8テキストで。不在または期限切れならundefined
del(key) / exists(key)DEL / EXISTS
expire(key, ttlMs) / persist(key) / pttl(key)PEXPIRE / PERSIST / PTTL(-1はTTLなし、-2はキーなし)
incrby(key, delta?)INCRBY。新しい値を返す
dbsize() / flushall()DBSIZE / FLUSHALL
keys(pattern?, limit?)Redisグロブ付きKEYS。上限も指定可
tick()手動のTTL掃除+イベントポーリング1回。期限切れキー数を返す
subscribe(channel, cb) / psubscribe(pattern, cb)SUBSCRIBE / PSUBSCRIBE。どちらも購読解除関数を返す
publish(channel, payload)ローカル購読者へ、(ブリッジ有効なら)同一オリジンの他タブへもPUBLISH。ローカル受信者数を返す
flush()永続性バリア。保留中の書き込みフレームをストレージへフラッシュ
compact()ストレージを、生きているキー空間のコンパクション済みイメージとして書き直す
close()フラッシュし、ブリッジ/タイマー/ストレージを畳み、インスタンスを解放

キーと値はどこでもstring | Uint8Array | ArrayBufferです。文字列は境界でUTF-8エンコードされ、バイトビューはそのまま通ります——値は本物のバイナリであって、文字列ではありません。

バインディングの仕組み(wasm-bindgen不使用)

kevyの依存ゼロの掟はツールチェーンにも及びます。境界のどちら側にもバインディングジェネレータはありません。kevy-wasmクレートはフラットな手書きC ABI——29個のextern "C"シンボル——をエクスポートし、pkg/kevy.jsはTypedArray境界、UTF-8コーデック、永続化ポンプ、タブ間ブリッジを自前で持つ手書きローダーです。規約は次の通りです。

エクスポート面はコア(open/close/alloc/free/clock/tick/結果バッファ)、KV(kevy_setkevy_getkevy_delkevy_existskevy_expirekevy_persistkevy_pttlkevy_incrbykevy_dbsizekevy_flushallkevy_keysなど)、pub/sub(kevy_subscribekevy_psubscribekevy_unsubscribekevy_publishkevy_poll_events)、AOFポンプ(kevy_aof_frames_outkevy_aof_frame_inkevy_aof_dump)に分かれます。同梱のローダーがあなたのホストに合わないなら、このABIこそがサポートされる統合ポイントです——クレートのドキュメントがすべてのシンボルとパック済みバイトフォーマットを規定しています。

永続化——ネイティブkevyとバイト互換の本物のログ

ブラウザにファイルシステムはないので、永続性はホスト仲介です。永続化が有効なら、すべての書き込みは、kevyのAOFがディスクに保存するのと同じRESPマルチバルクフレーム(kevy-persistフォーマット——persistence.md)としてもエンコードされます。ローダーは保留中のフレームをマイクロタスクごとに1回ストレージへポンプするので、同期的な書き込みバーストのコストはストレージ追記1回です。await db.flush()が永続性バリアで、flush()の解決はバックエンドがディスクへフラッシュしたことを意味します。

ブラウザのタブが書いたログは、そのままネイティブのkevyでリプレイできます——同じマジックヘッダ、同じフレームです。.aofをOPFSからコピーしてネイティブの組み込みストア(またはサーバー)に向ければ、キー空間が戻ってきます。逆も成り立ちます。入りのポンプはネイティブが書いたログを受け付けます。壊れた末尾はネイティブのリプレイ契約に従います——無傷のプレフィックスが適用され、末尾は捨てられ、次のコンパクションがライブ状態からストレージを書き直します。

バックエンドは2つあり、backend: "auto"では自動選択されます。

コンパクションは自動です。追記されたログがmax(512 KB, 前回イメージの4倍)を超えると、ローダーはストレージを生きているキー空間のコンパクション済みイメージとして書き直します(AOFリライトのブラウザ側等価物)。compact()はスナップショットやエクスポートの前に1回を強制します。

localStorageは意図的にバックエンドにしていません。約5 MBのクォータ、書き込みのたびにメインスレッドをブロックする同期API、UTF-16文字列限定のストレージ——書き込みログとしては失格です。

タブ間pub/sub

publish()は同じタブ内の購読者へはエンジン経由で配送し、(ブリッジ有効なら)インスタンスごとのBroadcastChannelでフレームを同一オリジンの他のすべてのタブへブロードキャストし、各タブのローカル購読者が受け取ります。配送はat-most-once・バックログなしです——メッセージを受け取るのはその時点で開いているタブだけで、後から開いたタブへのリプレイはありません。これはkevyサーバーのpub/sub(pubsub.md)と同じ契約なので、片方の面に向けて書いたコードはもう片方でも同じように振る舞います。

クロック、tick、TTL

wasm32-unknown-unknownにはスレッドもOSクロックもないので、エンジンは手動TTLリーパーとホスト供給クロックで動きます。ローダーは各エントリポイントの前にDate.now()をABI経由で供給し、タイマー(デフォルト100 ms)でtick()を回します。帰結は次の通りです。

パフォーマンス

方法論・環境・生の数字の全体はbench/WASM-BENCH.md(ヘッドレスChromeハーネス、3回実行の中央値、16バイト値、1kおよび100kキー)にあります。Webアプリが実際に持っているストレージとの比較:

軸(n=100k)kevy-wasmvs IndexedDBvs localStorage
ポイント読み取り(ops/s)1.67 M77×0.48×
ポイント書き込み(ops/s)1.79 M189×4.9×
バッチロード(ms)60.836×速い4.5×速い
スキャン、約10%一致(ms)5.646×速い5.7×速い
永続書き込み(ops/s)785 k(OPFS)12.6–17.4×約2×
再起動から利用可能まで(ms)1292.7×速い遅い(下記参照)

ヘッドラインと、正直な但し書き:

Rustの顔——WASI、エッジランタイム、直接組み込み

エンジンそのもの——kevy-embeddedとその依存クロージャ(kevy-storekevy-persistkevy-hashkevy-byteskevy-mapkevy-resp)——は両方のwasmターゲットでビルドでき、CIがこのリスト全体+kevy-wasmをプッシュごとにゲートします。ネットワークリアクタのクレート(kevy-rtkevy-syskevy-uring)は意図的にクロージャの外に置かれ、wasmビルドはクリーンに保たれます。

ターゲットコマンド備考
wasm32-unknown-unknowncargo build -p kevy-wasm --target wasm32-unknown-unknown --releaseブラウザ成果物(kevy_wasm.wasm)。npmローダーがこれをインスタンス化する。独自ホストならC ABIを直接叩く。
wasm32-unknown-unknown(Rust API)cargo build -p kevy-embedded --target wasm32-unknown-unknownスレッドなし・OSクロックなし。Config::with_ttl_reaper_manual()で開き、set_clock_nsset_wall_clock_msを供給し、ホストのループからStore::tick()を呼ぶ。
wasm32-wasip1cargo build -p kevy-embedded --target wasm32-wasip1InstantSystemTimeが動く——クロック供給は不要。std::fsはpreopenしたディレクトリに対して動くので、Config::with_persist("/data")wasmtime --dir=/dataで本物のAOF永続性が得られる。スレッドは依然ないので手動リーパーのまま。

wasm32-unknown-unknownでのRust直接組み込み:

use kevy_embedded::{Config, Store, set_clock_ns, set_wall_clock_ms};

let store = Store::open(Config::default().with_ttl_reaper_manual())?;

// Host feeds the clock before time-sensitive work…
set_clock_ns(now_ms_from_host().saturating_mul(1_000_000));
set_wall_clock_ms(now_ms_from_host());

store.set(b"hello", b"world")?;
let v = store.get(b"hello")?;   // Some(b"world".to_vec())

// …and drives expiry at its own cadence:
let _stats = store.tick();

エラーはKevyResultKevyErrorで、argvスタイルの書き込みメソッドは借用スライスを取ります。ネイティブとまったく同じです——kevy-embeddedのドキュメントを参照してください。

Cloudflare Workersなどのエッジアイソレートはブラウザのレシピに従います。アイソレートごとに1インスタンス、クロックソースはDate.now()tick()は遅延実行かスケジュールされたハンドラから。アイソレート再起動をまたぐ永続性には、ハンドラからプラットフォームの永続ストアへ書き込みをミラーします(AOFポンプがフレームをくれます)。アイソレートの内側では、kevyがホットなインメモリ層です。

FAQ

ブラウザでフルのコマンド面は動きますか? npmパッケージが公開するのはKV+TTL+カウンタ+スキャン+pub/subの切り出しです——ブラウザのストアに必要な面を、意図的に小さく保っています(wasmモジュールは非圧縮で約425 KB)。wasmターゲット上のRust APIは、コンパイル時に有効化したkevy-embeddedフィーチャーの全面を公開します。

永続化されたデータはポータブルですか? はい——標準のkevy AOFです。ブラウザ→ネイティブも、ネイティブ→ブラウザも、どちらもリプレイできます。フォーマット契約はpersistence.mdを参照してください。

共有メモリスレッド(+atomics)は? 同梱モジュールはシングルスレッドで、これはブラウザ系のすべてのターゲットに合致します。ホストがスレッドを提供する場所ではエンジンはスレッドセーフですが、手動tick()モデルがサポートされるパスのままです。

BroadcastChannelではなくSharedWorkerは? 単一オーナーのSharedWorkerトポロジは理論上はきれいですが、利用可能な範囲が狭い(特にAndroid Chromeに不在)。BroadcastChannelブリッジが、同じ実測レイテンシ級のまま互換性で勝ちました。

なぜwasm-bindgenを使わないのですか? kevyはサーバーも組み込みもブラウザも、サードパーティ依存ゼロで出荷します。バインディングジェネレータはビルド時依存になり、境界レイアウトの所有権を持ってしまいます。手書きのABIなら、契約は明示的で、バージョン付きで、監査可能なままです。