---
title: "ミドルウェア"
description: "@unikvs/core の IMiddleware。ミドルウェアが実装するフックとコンテキストを説明します。"
---

## 概要 [#overview]

`IMiddleware` はミドルウェアが実装するインターフェースです。`name` と `isOpen` のみ必須で、フックはすべて任意です。必要なフックだけを実装します。

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

type MyMiddleware = IMiddleware<string, string, ArrayBuffer, ArrayBuffer>;
```

## 型引数 [#type-params]

| 型引数         | 既定値 | 対応するフック                    |
| -------------- | ------ | --------------------------------- |
| `TEncodeData`  | `any`  | `encode` の入力と `decode` の出力 |
| `TDecodeData`  | `any`  | `decode` の入力と `encode` の出力 |
| `TEncodeChunk` | `any`  | `encodeStream` の入出力           |
| `TDecodeChunk` | `any`  | `decodeStream` の入出力           |

`encode` で生データを保存形式に変換し、`decode` で逆変換します。データ変換とストリーム変換で型引数が分かれているため、値とチャンクの型が異なるプラグインも表現できます。

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

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

`open(c)` と `close(c)` は任意です。[ストレージのライフサイクル](/unikvs/packages/core/storage#identity)と同じく、必要な場合のみ実装します。

## フック [#hooks]

| フック         | 引数                          | 説明                                               |
| -------------- | ----------------------------- | -------------------------------------------------- |
| `mapKey`       | `MapKeyContext`               | キーを変換して `string` を返す                     |
| `open`         | `OpenContext`                 | 初期化処理                                         |
| `close`        | `CloseContext`                | 終了処理                                           |
| `use`          | `UseContext`, `next`          | ストレージ操作の前段のフック (後段の `use` を包む) |
| `afterUse`     | `UseContext`, `UseOutcome`    | ストレージ I/O の確定後の通知                      |
| `encode`       | `EncodeContext`, `next`       | 書き込みデータの変換                               |
| `decode`       | `DecodeContext`, `next`       | 読み取りデータの逆変換                             |
| `encodeStream` | `EncodeStreamContext`, `next` | 書き込みストリームの変換                           |
| `decodeStream` | `DecodeStreamContext`, `next` | 読み取りストリームの逆変換                         |

### `mapKey` [#hooks-map-key]

キーそのものを変換します。同期的に `string` を返します。他のフックと異なり `next` はありません。

### `use` [#hooks-use]

ストレージ操作 (`set`・`get`・`stream`・`has`・`delete`・`copy`・`move`・`clear`) の前段で実行されるフックです。`use` はストレージ I/O の前に実行され、`next()` が実行するのは後段の `use` フックまでです。ストレージ操作はすべての `use` フックが完了した後に実行されるため、`use` からストレージ操作の成否は観測できません。書き込み系の操作の結果は [`afterUse`](#hooks-after-use) で受け取ります。`action` で絞り込むと、各操作の `key` や `repair`、`destinationKey` に型安全に触れます。

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

export class Timing implements IMiddleware {
  public readonly name = "Timing";
  public isOpen = false;

  public async use(c: IMiddleware.UseContext, next: IMiddleware.INext): Promise<void> {
    const startedAt = Date.now();
    await next();
    console.log(`use chain for ${c.action} took ${Date.now() - startedAt}ms`);
  }
}
```

### `afterUse` [#hooks-after-use]

`afterUse` は、書き込み系の操作 (`set`・`delete`・`copy`・`move`・`clear`) でストレージ I/O が確定した後に 1 回だけ呼ばれる通知専用のフックです。読み取り系の操作 (`get`・`stream`・`has`) では呼ばれません。`get`・`stream` の `repair` による書き戻しは読み取りの一部として扱われ、`afterUse` は呼ばれません。

`use` と異なり、ストレージ操作の成否を観測できます。キーロックと I/O ロックを保持したまま、すべての宛先の処理が終わってから実行されます。`outcome.targets` と `outcome` は読み取り専用に凍結されて渡されます。

:::warning[同じキーへの KVS 操作はデッドロックします]
`afterUse` はキーロックと I/O ロックを保持したまま実行されるため、同じキーに対する KVS 操作を呼び出すとデッドロックする可能性があります。
:::

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

export class Audit implements IMiddleware {
  public readonly name = "Audit";
  public isOpen = false;

  public afterUse(c: IMiddleware.UseContext, outcome: IMiddleware.UseOutcome): void {
    console.log({
      action: c.action,
      status: outcome.status,
      targets: outcome.targets,
    });
  }
}
```

`outcome.status` は `success`・`error`・`aborted` のいずれかです。`outcome.targets` には宛先ごとの `name`・`index`・`status` が入ります。分岐にだけ適用したミドルウェアには、その分岐の宛先だけが渡されます。`status` は `committed` (書き込み・削除などの確定)、`notFound` (キー不在)、`skipped` (予約済み。現在の UniKvs は実行した宛先について返しません)、`failed` (失敗) のいずれかで、`failed` のときだけ `error` を持ちます。

`committed` は操作全体が成功したときだけ現れるわけではありません。一部の宛先だけが失敗した場合や、ストレージの確定後に操作が中断された場合でも、実際に確定した宛先は `committed` として渡されます。`aborted` は操作全体が中断されたことを表し、その時点で確定済みの宛先は `committed` のまま残ります。成功として扱えるかは `outcome.status` ではなく、`targets` の実結果で判断してください。そのため、メトリクスの集計や後片付けのように、宛先ごとの実結果を使う処理を書けます。

:::note[読み取り時の repair と afterUse]
`get`・`stream` で `repair` を有効にしたときの書き戻しは、読み取り操作の一部としてストレージへ直接書き込まれます。`afterUse` は呼ばれないため、`encode`・`encodeStream` で書き込み用のメタデータを操作内に保存するミドルウェアは、そのメタデータを読み取り側のインスタンスへ確定できません。たとえば TTL のストリーム期限は帯域内に埋め込まれないため、repair で書き戻したストリームの期限は読み取り側の記録に残りません。
:::

:::warning[通知専用です]
`afterUse` は操作の結果を変更できません。フックが投げた例外はログに記録され、操作の成功・失敗はそのまま維持されます。操作を拒否したいときは `use` を使ってください。
:::

### `encode` と `decode` [#hooks-encode-decode]

データの変換と逆変換です。`source` が入力で、`destination` に結果を代入します。

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

export class UpperCase implements IMiddleware<string> {
  public readonly name = "UpperCase";
  public isOpen = false;

  public async encode(
    c: IMiddleware.EncodeContext<string>,
    next: IMiddleware.INext,
  ): Promise<void> {
    c.destination = c.source.toUpperCase();
    await next();
  }

  public async decode(
    c: IMiddleware.DecodeContext<string>,
    next: IMiddleware.INext,
  ): Promise<void> {
    c.destination = c.source.toLowerCase();
    await next();
  }
}
```

`EncodeContext<string>` は `IEncodeContext<string, string>` と等価です。型引数を 1 つだけ渡すと、入出力が同じ型として解決されます。

### `encodeStream` と `decodeStream` [#hooks-encode-decode-stream]

ストリームの変換と逆変換です。データ変換と異なり、`source` と `destination` は [`IReadableStream`](/unikvs/packages/core/stream) です。

## `next` [#next]

`use`・`encode`・`decode`・`encodeStream`・`decodeStream` は `next()` を呼ぶと後段へ進みます。`INext` は引数なしで `Promise<void>` を返します。

```ts
public async use(c: IMiddleware.UseContext, next: IMiddleware.INext): Promise<void> {
  await next(); // 後段へ進む
}
```

`next()` の前後で前処理と後処理を書けます。戻り値は `PromiseLike<void>` で、`MaybePromise` ではなく必ず非同期として扱われます。

:::warning[同期・非同期の混在に注意]
`open`・`close` の戻り値は `MaybePromise<void>` ですが、`use` 系の戻り値は `PromiseLike<void>` です。`open()` 前の呼び出しや未対応の操作は各プラグインが適切なエラーを投げます。
:::

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

`IMiddleware` 名前空間は [パイプラインコンテキスト](/unikvs/packages/core/pipeline-context#contexts) の別名を提供します。

| 別名                                 | 実体                                                                                                                             |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `IMiddleware.MapKeyContext`          | [`IMapKeyContext`](/unikvs/packages/core/pipeline-context#imapkeycontext)                                                                            |
| `IMiddleware.OpenContext`            | [`IOpenContext`](/unikvs/packages/core/pipeline-context#iopencontext)                                                                                |
| `IMiddleware.CloseContext`           | [`ICloseContext`](/unikvs/packages/core/pipeline-context#iclosecontext)                                                                              |
| `IMiddleware.UseContext`             | `ISetContext \| IGetContext \| IStreamContext \| IHasContext \| IDeleteContext \| ICopyContext \| IMoveContext \| IClearContext` |
| `IMiddleware.EncodeContext<T>`       | [`IEncodeContext<T, T>`](/unikvs/packages/core/pipeline-context#iencodecontext)                                                                      |
| `IMiddleware.DecodeContext<T>`       | [`IDecodeContext<T, T>`](/unikvs/packages/core/pipeline-context#idecodecontext)                                                                      |
| `IMiddleware.EncodeStreamContext<T>` | [`IEncodeStreamContext<T, T>`](/unikvs/packages/core/pipeline-context#iencodestreamcontext)                                                          |
| `IMiddleware.DecodeStreamContext<T>` | [`IDecodeStreamContext<T, T>`](/unikvs/packages/core/pipeline-context#idecodestreamcontext)                                                          |

## 実装例 [#example]

**キー変換**

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

export class PrefixKey implements IMiddleware {
  public readonly name = "PrefixKey";
  public isOpen = false;

  public mapKey(c: IMiddleware.MapKeyContext): string {
    return `app:${c.key}`;
  }
}
```

**前後処理**

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

export class PrefixKey implements IMiddleware {
  public readonly name = "PrefixKey";
  public isOpen = false;

  public async use(c: IMiddleware.UseContext, next: IMiddleware.INext): Promise<void> {
    console.log(`start: ${c.action}`);
    await next();
    console.log(`end: ${c.action}`);
  }
}
```

**データ変換**

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

export class UpperCase implements IMiddleware<string> {
  public readonly name = "UpperCase";
  public isOpen = false;

  public async encode(
    c: IMiddleware.EncodeContext<string>,
    next: IMiddleware.INext,
  ): Promise<void> {
    c.destination = c.source.toUpperCase();
    await next();
  }

  public async decode(
    c: IMiddleware.DecodeContext<string>,
    next: IMiddleware.INext,
  ): Promise<void> {
    c.destination = c.source.toLowerCase();
    await next();
  }
}
```
