---
title: "Middleware"
description: "@unikvs/core IMiddleware: hooks and contexts implemented by middleware."
---

## Overview [#overview]

`IMiddleware` is the interface middleware implements. Only `name` and `isOpen` are required; every hook is optional. Implement just the hooks you need.

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

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

## Type arguments [#type-params]

| Type argument  | Default | Corresponding hooks                |
| -------------- | ------- | ---------------------------------- |
| `TEncodeData`  | `any`   | `encode` input and `decode` output |
| `TDecodeData`  | `any`   | `decode` input and `encode` output |
| `TEncodeChunk` | `any`   | `encodeStream` in/out              |
| `TDecodeChunk` | `any`   | `decodeStream` in/out              |

`encode` converts raw data to the stored form; `decode` reverses it. Data and stream conversions have separate type arguments, so values and chunks can have different types.

## Identity [#identity]

| Member   | Required | Description         |
| -------- | -------- | ------------------- |
| `name`   | required | Plugin name         |
| `isOpen` | required | Whether initialized |

`open(c)` and `close(c)` are optional. As with [storage lifecycles](/unikvs/en/packages/core/storage#identity), implement them only when needed.

## Hooks [#hooks]

| Hook           | Arguments                     | Description                                                   |
| -------------- | ----------------------------- | ------------------------------------------------------------- |
| `mapKey`       | `MapKeyContext`               | Transforms the key, returns `string`                          |
| `open`         | `OpenContext`                 | Initialization                                                |
| `close`        | `CloseContext`                | Teardown                                                      |
| `use`          | `UseContext`, `next`          | Hook before storage operations (wraps downstream `use` hooks) |
| `afterUse`     | `UseContext`, `UseOutcome`    | Notification after storage I/O settles                        |
| `encode`       | `EncodeContext`, `next`       | Write-data conversion                                         |
| `decode`       | `DecodeContext`, `next`       | Read-data reverse conversion                                  |
| `encodeStream` | `EncodeStreamContext`, `next` | Write-stream conversion                                       |
| `decodeStream` | `DecodeStreamContext`, `next` | Read-stream reverse conversion                                |

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

Transforms the key itself. Synchronously returns a `string`. Unlike other hooks, it has no `next`.

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

Hook that runs before storage operations (`set` / `get` / `stream` / `has` / `delete` / `copy` / `move` / `clear`). `use` runs before storage I/O, and `next()` runs only downstream `use` hooks. The storage operation runs after all `use` hooks complete, so `use` cannot observe whether it succeeds. Receive the outcome of mutating operations with [`afterUse`](#hooks-after-use). Narrow by `action` for type-safe access to each operation's `key`, `repair`, or `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` is a notification-only hook called once after storage I/O settles for mutating operations (`set` / `delete` / `copy` / `move` / `clear`). It is not called for reads (`get` / `stream` / `has`). Repair writes performed by `get` / `stream` are treated as part of the read and do not call `afterUse`.

Unlike `use`, it can observe whether the storage operation succeeded. It runs after every target has finished, while the key and I/O locks are still held. `outcome` and `outcome.targets` are frozen before they are passed to the hook.

:::warning[KVS operations on the same key can deadlock]
`afterUse` runs while the key and I/O locks are held, so calling KVS operations on the same key from `afterUse` can deadlock.
:::

```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` is `success`, `error`, or `aborted`. Each entry in `outcome.targets` has a target `name`, `index`, and `status`. A middleware applied to only one branch receives only that branch's targets. The status is `committed` (write or delete confirmed), `notFound` (key absent), `skipped` (reserved; current UniKvs does not return it for an executed target), or `failed` (failure). Only `failed` targets carry `error`.

A `committed` target does not imply the whole operation succeeded. When only some targets fail, or when an operation is aborted after storage has settled, targets that actually committed are still reported as `committed`. `aborted` means the operation as a whole was aborted; targets already committed remain `committed`. Decide whether the operation is usable from the per-target results, not from `outcome.status` alone. This lets metrics and cleanup decisions use the real per-target result.

:::note[Repair during reads and afterUse]
When `get` / `stream` runs with `repair`, its write-back goes directly to storage as part of the read operation. `afterUse` is not called, so a middleware that stages write metadata in `encode` / `encodeStream` cannot commit that metadata on the reading instance. For example, TTL stream expiry is not stored in-band, so a stream repaired through `get` / `stream` does not gain a TTL record on the reading instance.
:::

:::warning[Notification only]
`afterUse` cannot change the operation result. If the hook throws, the error is logged and the original success or failure is preserved. Use `use` to reject an operation.
:::

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

Data conversion and its inverse. `source` is the input; assign the result to `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>` equals `IEncodeContext<string, string>`. With a single type argument, input and output resolve to the same type.

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

Stream conversion and its inverse. Unlike data conversion, `source` and `destination` are [`IReadableStream`](/unikvs/en/packages/core/stream).

## `next` [#next]

`use` / `encode` / `decode` / `encodeStream` / `decodeStream` advance downstream by calling `next()`. `INext` takes no arguments and returns `Promise<void>`.

```ts
public async use(c: IMiddleware.UseContext, next: IMiddleware.INext): Promise<void> {
  await next(); // advance downstream
}
```

Write pre/post processing around `next()`. The return is `PromiseLike<void>` — always async, never `MaybePromise`.

:::warning[Do not mix sync and async]
`open` / `close` return `MaybePromise<void>`, but the `use` family returns `PromiseLike<void>`. Calls before `open()` or for unsupported operations throw the appropriate error from each plugin.
:::

## Context aliases [#contexts]

The `IMiddleware` namespace provides aliases of [pipeline contexts](/unikvs/en/packages/core/pipeline-context#contexts).

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

## Example [#example]

**Key mapping**

```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}`;
  }
}
```

**Around processing**

```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}`);
  }
}
```

**Data conversion**

```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();
  }
}
```
