Open Beta版になったDeno KVの世界にダイブする

Denoには[Deno KV](https://deno.com/kv)と呼ばれるデータをディスク上に永続的に保持する組み込みのキーバリューストアがあります。これによってDenoではサービスとシステムの再起動をまたいだデータの保存とアクセスが可能となります。

この機能はこれまでClosed Beta版として公開されていましたが、2023年9月6日にOpen Beta版として公開されました。

この記事ではそんなOpenされたばかりのDeno KVについて紹介します。

:::note
Open Beta版が始まったばかりで実験中の機能です。バージョンアップによって内容が変更される可能性があるので気をつけてください。
:::

## 特徴
### 簡単なセットアップ
ローカル環境であってもDeno Deploy上であってもDeno KVは以下のように手間なく利用可能です。
```ts
await Deno.openKv();
```
Deno Deploy以外の環境からDeno Deploy上のリソースを使いたい場合はurlを入力する必要があります。
```ts
await Deno.openKv("https://api.deno.com/databases/XXX/connect");
```

### JavaScript用のデータベース
Deno KVはJavaScriptもしくはTypeScriptで利用すること前提で作られているので、文字列や数値のような単調な値の他にも`Map`や`undefined`、`Date`のようなJavaScriptで扱えるオブジェクトをそのまま値として使うことができます。
さらに、APIもPromiseベースで直感的に実装されているのでデータの操作が容易です。
```ts
undefined;
null;
true;
false;
42;
-42.5;
42n;
"hello";
new Uint8Array([1, 2, 3]);
[1, 2, 3];
{ a: 1, b: 2, c: 3 };
new Map([["a", 1], ["b", 2], ["c", 3]]);
new Set([1, 2, 3]);
new Date("2023-04-23");
/abc/;
const a = {};
const b = { a };
a.b = b;

// Denoがサポートするbigintとして表される64ビットの符号なし整数
new Deno.KvU64(42n);
```

### 簡単に拡張可能
Deno KVは[FoundationDB](https://www.foundationdb.org/)上に構築されており、毎秒数百万のオペレーションを処理することができます。エンタープライズへの移行もゼロコンフィグ、ゼロプロビジョニング、ゼロオーケストレーションで行えます。

### ACIDトランザクション
Deno KVではデータの一貫性、整合性、耐久性を確保した一連の操作にトランザクションを貼れます。
一連の操作が成功した場合はデータを永続化し、失敗した場合はロールバックされるのでデータが常に正確に保ちます。
```ts
await kv.atomic()
  .check(senderRes)
  .check(receiverRes)
  .set(senderKey, newSenderBalance)
  .set(receiverKey, newReceiverBalance)
  .commit();
```

### 読み取りの精度
データの読み取りは通常プライマリーリージョンから最新のデータを読み取ったものを返します。データの読み取りに強い整合性を求めない場合に最も近いリージョンで読み取った最後の結果を返すように指定できます。
```ts
await kv.get(key, { consistency: "eventual" });
```

## 使ってみる
現在Deno KVの利用には、実行時に`--unstable`フラグを設ける必要があります。実機で試す時は気をつけてください。
```sh:例
deno run --unstable --allow-net main.ts
```

### 構造
キーは`KvKey`という型として実装されています。`KvKey`は`readonly KvKeyPart[]`という型になっています。さらに`KvKeyPart`は`Uint8Array`、`string`、`number`、`bigint`、`boolean`の5つの型のユニオンです。
つまり、`['hello', 'world', 123]`のように一連のシーケンスでキーを表します。

バリューは特徴でも紹介しましたが、JavaScriptのオブジェクトも含んださまざまな値を扱えます。
`undefined`
`null`
`boolean`
`number`
`string`
`bigint`
`Uint8Array`
`Array`
`Object`
`Map`
`Set`
`Date`
`RegExp`
`Deno.KvU64`
値の循環参照もサポートされていて、かなり自由度が高いです。

### Deno KVを開く
全ての操作は`Deno.Kv`APIを介して行います。このAPIは`Deno.openKv`関数をを呼び出すことで利用できます。

```ts
const kv = await Deno.openKv();
```
`openKv`関数の第一引数には接続先を与えられます。Denoをデプロイする先がDeno Deployではない、もしくは異なるプロジェクトのDeno KVを利用する場合はパスを与えて接続先をカスタマイズします。

### 操作
主要な操作について紹介します。
#### get
`get`は指定したキーに対応するバリューとバージョンを返します。
`kv`から`get`を呼び出して型引数でバリューの型を決めます。
```ts
await kv.get<string>(['hello', 'deno']);
```
`get`の第2引数には`options`を渡せます。`options`は現在`consistency`をキーとするオブジェクトを渡すことができ、`strong`と`eventual`で取得する値の精度を決めます。デフォルトでは`strong`となっており、`eventual`にすると一番近いリージョンから取得した値を返します。
```ts
await kv.get<string>(
  ['hello', 'deno'],
  {
    consistency: 'eventual',
  },
);
```

得られる型は`Promise<KvEntryMaybe>`となっています。これは以下のように定義されています。
```ts
type KvEntry<T> = { key: KvKey; value: T; versionstamp: string };
type KvEntryMaybe<T> = KvEntry<T> | {
  key: KvKey;
  value: null;
  versionstamp: null;
};
```
`key`には指定したキーを、`value`には取得した値のバリューを指定した型`T`を、`versionstamp`にはバージョン情報を`string`で返しています。値がなかった場合は`value`と`versionstamp`に`null`を詰めて返します。

詳細は紹介しませんが、`getMany`によって複数のキーに対応する複数の値を取得することも可能です。
```ts
await kv.getMany<[string, string, string]>([
  ['hello', 'node'],
  ['hello', 'deno'],
  ['hello', 'ts'],
]);
```

#### list
`list`は指定したセレクターに一致した値を返します。`KvListIterator`というイテレーターとして実装されており、イテレータからは先ほど`get`で見た`KvEntry`が渡されます。
イテレーターから取り出す値はキーを元に定められた順番で規則正しく並んでいます。
まずは大雑把に型を元に`Uint8Array`、`string`、`number`、`bigint`、`boolean`の順番で並べられます。そして型の中でもそれぞれに定義された順番で並べられます。`string`だとUTF-8エンコーディングのバイト順に並べられます。

```ts
kv.list<string>({ prefix: ["hello"] });
```
第一引数には`KvListSelector`で表現されるセレクターを渡します。
```ts
type KvListSelector =
  | { prefix: KvKey }
  | { prefix: KvKey; start: KvKey }
  | { prefix: KvKey; end: KvKey }
  | { start: KvKey; end: KvKey };
```
`prefix`はそれと完全に一致したキーで始まる値を取り出します。`start`や`end`はキーの範囲を指定します。
例えば`KvListSelector`を`{ start: ['hello', 'a'], end: ['hello', 'e'] }`のようにした場合を考えます。この時はキーが`['hello', 'deno']`の値が取り出されます。`['hello', 'node']`や`['hello', 'ts']`は`node`や`ts`の箇所で範囲外となるので取り出されないです。
`end`に指定したキーは含まれないことに注意してください。`end`が`['hello', 'd']`の場合は`['hello', 'deno']`は引っかかりません。

そして任意で第2引数を`KvListOptions`として渡せます。
```ts
type KvListOptions = {
  limit?: number;
  cursor?: string;
  reverse?: boolean;
  consistency?: KvConsistencyLevel;
  batchSize?: number;
}
```
`limit`は名前の通り最大の取得件数を指定します。`cursor`は取得位置を指定します。`reverse`は逆順で返すように指定します。`consistency`は`get`と同じです。
`batchSize`は裏側でデータを取得する時のサイズを指定します。デフォルトでは500になっています。直接は見えない設定ですがこの取得単位でデータの整合性が担保され、データ間の整合性は担保されないことに気をつけてください。

返ってきたイテレーターは(当然ですが)以下のようにループさせて取得します。
```ts
const iter = kv.list<string>({ prefix: ["hello"] });
for await (const res of iter) console.log(res.value);
```

#### set
`set`は値の作成または更新を行います。
```ts
await kv.set(["users", "alex"], "alex");
```
キーを第1引数に、バリューを第2引数に渡します。
第3引数には任意で期限を渡せます。オブジェクトで`expireIn`をキーとしてms単位で数値を渡すことで生存期間設定します。
```ts
await kv.set(['hello', "deno"], 'good!', { expireIn: 5 });
```
返り値は`Promise<KvCommitResult>`です。以下のようになっていて、結果と`versionstamp`を渡してくれます。
```ts
type KvCommitResult = {
  ok: true;
  versionstamp: string;
}
```

#### delete
`delete`は値の削除です。
```ts
await kv.delete(['hello', "deno"]);
```
第1引数にキーを渡して対象の値が存在すれば削除、なければ何もしません。返り値は`Promise<void>`なので何も返しません。

### トランザクション
Deno KVでトランザクションを張るには`atomic`関数を使います。
`kv.atomic()`でトランザクションを開始して`AtomicOperation`クラスを提供します。
`AtomicOperation`クラスは`check`や`set`、`commit`などのメソッドを持ちます(他にも色々な操作メソッドを持っています)。

`check`は取得した値が現在も最新であることを確認します。取得した値は引数に`AtomicCheck`のような形で渡します。
```ts
type AtomicCheck = {
  key: KvKey;
  versionstamp: string | null;
}
```
以下のようにして事前に取得したキーが`['hello', 'deno']`の値が最新であることを確かめます。
```ts
const hd = await kv.get<string>(['hello', 'deno']);
kv.atomic().check(hd)
```
結果として自身(`AtommicCheck`)を返して次に繋げます。

引数を調整して複数チェックできます。
```ts
const hd = await kv.get<string>(['hello', 'deno']);
// 意味ないですが、例として
kv.atomic().check(hd, hd, hd)
```

`set`は先ほど見た`set`と同じような引数を渡して値の作成または更新を行います。`check`同様に返り値は自身(`AtommicCheck`)を返します。

`commit`は`atomic`を呼び出してから`commit`が呼ばれるまでの一連の操作を実行します。
以下の例では事前に取得した値が最新であることを確かめてから作成・更新します。
```ts
const key = ['hello', 'deno']
const hd = await kv.get<string>(key);
await kv.atomic()
  .check(hd)
  .set(key, 'good')
  .commit();
```
`check`で矛盾した場合や作成・更新に失敗した場合はこれまでの操作を棄却します。
成功の有無は`commit`の返り値から判断可能です。型は`Promise<KvCommitResult | KvCommitError>`のようになっていて、成功した時は`KvCommitResult`を返し、失敗した時は`KvCommitError`を返します。
```ts
type KvCommitResult = {
  ok: true;
  versionstamp: string;
}

type KvCommitError = {
  ok: false;
}
```

## さいごに
Deno KVの基本的な操作を紹介しました。Denoのエコシステムがどんどん発達していることに喜びを感じます。
かなり重要な機能だと思うので本番のリリースが楽しみです。