---
title: "ストレージ"
description: "@unikvs/core の IStorage。保存先プラグインが実装するインターフェース、型引数、必須と任意のメンバー、実装例を説明します。"
---

## 概要 [#overview]

`IStorage` は保存先プラグインが実装するインターフェースです。`name` と `isOpen`、CRUD 操作を持ち、ライフサイクル、`copy`・`move`、ストリーム取得は任意です。4 つの型引数でデータとチャンクの型を調整します。

```ts
import type { IStorage } from "@unikvs/core";

type BinaryStorage = IStorage<Uint8Array>;
type ObjectStorage = IStorage<{ readonly id: number }>;
type FullStorage = IStorage<Uint8Array, string, ArrayBuffer, number>;
```

## 型引数 [#type-params]

| 型引数        | 既定値        | 対応する操作                |
| ------------- | ------------- | --------------------------- |
| `TWriteData`  | `any`         | `write(c, data)` の `data`  |
| `TReadData`   | `TWriteData`  | `read(c)` の戻り値          |
| `TWriteChunk` | `any`         | `getWritable(c)` のチャンク |
| `TReadChunk`  | `TWriteChunk` | `getReadable(c)` のチャンク |

`TReadData` の既定値は `TWriteData`、`TReadChunk` の既定値は `TWriteChunk` です。読み書きで型が変わらない場合はデータの型引数 (`TWriteData`) のみを指定し、ストリームを使わない場合はチャンクの型引数を省略できます。

```ts
// 読み取りは文字列、書き込みはバイト列
type TextReadStorage = IStorage<Uint8Array, string>;

// チャンクまで個別に指定
type ChunkedStorage = IStorage<Uint8Array, string, ArrayBuffer, number>;
```

チャンク型とストリーム型の関係は[ストリーム](/unikvs/packages/core/stream#chunks)を参照してください。

## 基本プロパティー [#identity]

| メンバー | 必須 | 説明               |
| -------- | ---- | ------------------ |
| `name`   | 必須 | プラグイン名       |
| `isOpen` | 必須 | 初期化済みかどうか |

`open(c)` と `close(c)` は任意です。必要な初期化や後始末がある場合のみ実装します。呼び出し側は `isOpen` で状態を確認します。

## CRUD [#crud]

| メンバー | 必須 | シグネチャ                                                  | 説明                     |
| -------- | ---- | ----------------------------------------------------------- | ------------------------ |
| `write`  | 必須 | `(c: WriteContext, data: TWriteData) => MaybePromise<void>` | データを保存する         |
| `read`   | 必須 | `(c: ReadContext) => MaybePromise<TReadData>`               | データを取得する         |
| `exists` | 必須 | `(c: ExistsContext) => MaybePromise<boolean>`               | キーの有無を返す         |
| `delete` | 必須 | `(c: DeleteContext) => MaybePromise<void>`                  | キーを削除する           |
| `clear`  | 必須 | `(c: ClearContext) => MaybePromise<void>`                   | すべてのデータを削除する |

すべてのメソッドは `MaybePromise` (`T | PromiseLike<T>`) を返すため、同期実装も非同期実装も可能です。

:::warning[見つからないキーはエラーを投げます]
`read` はキーがない場合に [`KeyNotFoundError`](/unikvs/packages/core/errors#error-key-not-found) を投げます。`undefined` を返したり `exists` 相当の判定を埋め込んだりせず、見つからない場合の後処理 (読み取り時の書き戻しなど) は呼び出し側に任せます。
:::

## コピーと移動 [#transfer]

| メンバー | 必須 | シグネチャ                               | 説明                     |
| -------- | ---- | ---------------------------------------- | ------------------------ |
| `copy`   | 任意 | `(c: CopyContext) => MaybePromise<void>` | キーを別のキーへ複製する |
| `move`   | 任意 | `(c: MoveContext) => MaybePromise<void>` | キーを別のキーへ移動する |

`CopyContext` と `MoveContext` は、コピー元・移動元のキーを `key`、コピー先・移動先のキーを `destinationKey` に持ちます。コピー先・移動先に既存の値がある場合は上書きします。

`move` は移動先への書き込みが成功した後に移動元を削除します。移動先への書き込みに失敗した場合は移動元を残してください。コピー元・移動元が存在しない場合は [`KeyNotFoundError`](/unikvs/packages/core/errors#error-key-not-found) を投げます。`key` と `destinationKey` が同じ場合は、存在確認だけを行い、値を削除しません。

`copy` または `move` を省略した場合、UniKvs 内部の `UniKvsStorage` が `read`・`write`・`delete` を組み合わせて同じ動作を再現します。ストレージ固有の効率的な API がある場合だけ実装します。

:::tip[ネイティブ API を優先する]
ストレージに複製や名前変更の API がある場合は、読み書きを組み合わせる代わりにそれらを使うと効率的です。API がない場合は実装を省略でき、UniKvs が `read`・`write`・`delete` で動作を再現します。
:::

## ストリーム取得 [#streams]

| メンバー      | 必須 | シグネチャ                                                              | 説明                           |
| ------------- | ---- | ----------------------------------------------------------------------- | ------------------------------ |
| `getWritable` | 任意 | `(c: GetWritableContext) => MaybePromise<IWritableStream<TWriteChunk>>` | 書き込み用ストリームを取得する |
| `getReadable` | 任意 | `(c: GetReadableContext) => MaybePromise<IReadableStream<TReadChunk>>`  | 読み取り用ストリームを取得する |

ストリームに対応しないストレージは実装を省略します。対応宣言は型ではなく実行時の有無で判定されるため、未対応の操作が呼ばれた場合は各プラグインが適切なエラーを投げます。

`getWritable` のコンテキストは `ISetContext` と同じ形状で、`getReadable` のコンテキストは `IStreamContext` と同じ形状です ([コンテキストの別名](#contexts))。開始前に中断済みのシグナルで呼ばれた場合は [`StorageAbortedError`](/unikvs/packages/core/errors#error-storage-aborted) で即座に拒否します。

## 他ストレージの失敗 [#other-write-error]

複数のストレージへ並列に書き込む際、他の保存先で失敗があっても各ストレージに通知されます。

| メンバー            | 必須 | 説明                                                   |
| ------------------- | ---- | ------------------------------------------------------ |
| `onOtherWriteError` | 任意 | 複数ストレージ書き込み時に他ストレージの失敗を受け取る |

引数は `(c: OtherWriteErrorHandlerContext, errors: readonly IOtherWriteError[])` です。`OtherWriteErrorHandlerContext` は `WriteContext | GetWritableContext` で、書き込み系のどちらの経路で失敗したかを示します。

`IOtherWriteError` の形状は次のとおりです。

| Prop | Type | Default | Description |
| - | - | - | - |
| `plugin` | `[#iotherwriteerror-plugin] "storage"` | - | 固定値です。 |
| `errors` | `[#iotherwriteerror-errors] readonly { plugin: "storage", reason: unknown }[]` | - | 他ストレージごとの失敗理由です。 |

## コンテキストの別名 [#contexts]

`IStorage` 名前空間は [パイプラインコンテキスト](/unikvs/packages/core/pipeline-context#contexts) の別名を提供します。プラグイン側は `IStorage.*` を使うと意図が明確になります。

| 別名                                     | 実体                                                  | 用途                |
| ---------------------------------------- | ----------------------------------------------------- | ------------------- |
| `IStorage.OpenContext`                   | [`IOpenContext`](/unikvs/packages/core/pipeline-context#iopencontext)     | `open`              |
| `IStorage.CloseContext`                  | [`ICloseContext`](/unikvs/packages/core/pipeline-context#iclosecontext)   | `close`             |
| `IStorage.WriteContext`                  | `ISetContext \| ICopyContext \| IMoveContext`         | `write`             |
| `IStorage.GetWritableContext`            | [`ISetContext`](/unikvs/packages/core/pipeline-context#isetcontext)       | `getWritable`       |
| `IStorage.ReadContext`                   | `IGetContext \| ICopyContext \| IMoveContext`         | `read`              |
| `IStorage.GetReadableContext`            | [`IStreamContext`](/unikvs/packages/core/pipeline-context#istreamcontext) | `getReadable`       |
| `IStorage.ExistsContext`                 | [`IHasContext`](/unikvs/packages/core/pipeline-context#ihascontext)       | `exists`            |
| `IStorage.DeleteContext`                 | `IDeleteContext \| ICopyContext \| IMoveContext`      | `delete`            |
| `IStorage.CopyContext`                   | `ICopyContext \| IMoveContext`                        | `copy`              |
| `IStorage.MoveContext`                   | [`IMoveContext`](/unikvs/packages/core/pipeline-context#imovecontext)     | `move`              |
| `IStorage.ClearContext`                  | [`IClearContext`](/unikvs/packages/core/pipeline-context#iclearcontext)   | `clear`             |
| `IStorage.OtherWriteErrorHandlerContext` | `WriteContext \| GetWritableContext`                  | `onOtherWriteError` |

`copy` / `move` のフォールバックでは、`read`・`write`・`delete` に元の操作種別 (`"copy"` または `"move"`) と、呼び出しに応じたキーを持つコンテキストが渡されます。そのため `ReadContext`・`WriteContext`・`DeleteContext` には copy/move コンテキストも含まれます。

## 実装例 [#example]

**同期実装**

```ts
import {
  KeyNotFoundError,
  type IStorage,
} from "@unikvs/core";

export class Memory implements IStorage<string> {
  public readonly name = "Memory";
  public isOpen = false;
  private readonly data = new Map<string, string>();

  public open(): void {
    this.isOpen = true;
  }

  public close(): void {
    this.isOpen = false;
  }

  public write(c: IStorage.WriteContext, value: string): void {
    this.data.set(c.key, value);
  }

  public read(c: IStorage.ReadContext): string {
    const value = this.data.get(c.key);
    if (value === undefined) {
      throw new KeyNotFoundError({ key: c.key });
    }
    return value;
  }

  public exists(c: IStorage.ExistsContext): boolean {
    return this.data.has(c.key);
  }

  public delete(c: IStorage.DeleteContext): void {
    this.data.delete(c.key);
  }

  public copy(c: IStorage.CopyContext): void {
    const value = this.data.get(c.key);
    if (value === undefined) {
      throw new KeyNotFoundError({ key: c.key });
    }
    if (c.key !== c.destinationKey) {
      this.data.set(c.destinationKey, value);
    }
  }

  public move(c: IStorage.MoveContext): void {
    this.copy(c as unknown as IStorage.CopyContext);
    if (c.key !== c.destinationKey) {
      this.data.delete(c.key);
    }
  }

  public clear(): void {
    this.data.clear();
  }
}
```

**非同期実装**

```ts
import {
  KeyNotFoundError,
  type IStorage,
} from "@unikvs/core";

export class AsyncMemory implements IStorage<string> {
  public readonly name = "AsyncMemory";
  public isOpen = false;
  private readonly data = new Map<string, string>();

  public async open(): Promise<void> {
    this.isOpen = true;
  }

  public async close(): Promise<void> {
    this.isOpen = false;
  }

  public async write(c: IStorage.WriteContext, value: string): Promise<void> {
    this.data.set(c.key, value);
  }

  public async read(c: IStorage.ReadContext): Promise<string> {
    const value = this.data.get(c.key);
    if (value === undefined) {
      throw new KeyNotFoundError({ key: c.key });
    }
    return value;
  }

  public async exists(c: IStorage.ExistsContext): Promise<boolean> {
    return this.data.has(c.key);
  }

  public async delete(c: IStorage.DeleteContext): Promise<void> {
    this.data.delete(c.key);
  }

  public async copy(c: IStorage.CopyContext): Promise<void> {
    const value = this.data.get(c.key);
    if (value === undefined) {
      throw new KeyNotFoundError({ key: c.key });
    }
    if (c.key !== c.destinationKey) {
      this.data.set(c.destinationKey, value);
    }
  }

  public async move(c: IStorage.MoveContext): Promise<void> {
    await this.copy(c as unknown as IStorage.CopyContext);
    if (c.key !== c.destinationKey) {
      this.data.delete(c.key);
    }
  }

  public async clear(): Promise<void> {
    this.data.clear();
  }
}
```

実装時の要点は次のとおりです。

1. **型引数を決める**

    保存する値の型を `IStorage<T>` に指定します。読み書きで型が変わる場合やストリームを使う場合は、4 つすべてを指定します。

2. **必須メンバーを実装する**

    `name`・`isOpen` と CRUD の 5 メソッドは必須です。`read` では見つからないキーに `KeyNotFoundError` を投げます。`copy`・`move` は任意で、省略時は `read`・`write`・`delete` によるフォールバックが使われます。

3. **任意メンバーを足す**

    初期化が必要なら `open`・`close`、ストリーム対応なら `getWritable`・`getReadable`、他ストレージの失敗通知が要れば `onOtherWriteError` を実装します。

## API リファレンス [#api]

### `IStorage` [#istorage]

保存先プラグインが実装するインターフェースです。4 つの型引数でデータとチャンクの型を調整します。

| Prop | Type | Default | Description |
| - | - | - | - |
| `name` | `[#istorage-name] string` | - | プラグイン名です。 |
| `isOpen` | `[#istorage-isopen] boolean` | - | 初期化済みかどうかを示します。 |
| `open?` | `[#istorage-open] (c: IStorage.OpenContext) => MaybePromise<void>` | - | 初期化処理を行います。 |
| `close?` | `[#istorage-close] (c: IStorage.CloseContext) => MaybePromise<void>` | - | 終了処理を行います。 |
| `write` | `[#istorage-write] (c: IStorage.WriteContext, data: TWriteData) => MaybePromise<void>` | - | データを保存します。 |
| `read` | `[#istorage-read] (c: IStorage.ReadContext) => MaybePromise<TReadData>` | - | データを取得します。なければ `KeyNotFoundError` を投げます。 |
| `exists` | `[#istorage-exists] (c: IStorage.ExistsContext) => MaybePromise<boolean>` | - | キーの有無を返します。 |
| `delete` | `[#istorage-delete] (c: IStorage.DeleteContext) => MaybePromise<void>` | - | キーを削除します。 |
| `clear` | `[#istorage-clear] (c: IStorage.ClearContext) => MaybePromise<void>` | - | すべてのデータを削除します。 |
| `copy?` | `[#istorage-copy] (c: IStorage.CopyContext) => MaybePromise<void>` | - | キーの値を別のキーへ複製します。省略時は read/write で再現されます。 |
| `move?` | `[#istorage-move] (c: IStorage.MoveContext) => MaybePromise<void>` | - | キーの値を別のキーへ移動します。省略時は copy/delete で再現されます。 |
| `onOtherWriteError?` | `[#istorage-onotherwriteerror] (c: IStorage.OtherWriteErrorHandlerContext, errors: readonly IStorage.IOtherWriteError[]) => MaybePromise<void>` | - | 他ストレージの失敗を受け取ります。 |
