---
title: "Storage"
description: "@unikvs/core IStorage: the interface destination plugins implement, its type arguments, required and optional members, with examples."
---

## Overview [#overview]

`IStorage` is the interface destination plugins implement. It has `name`, `isOpen`, and CRUD operations; lifecycle, `copy`/`move`, and stream acquisition are optional. Four type arguments tune data and chunk types.

```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 arguments [#type-params]

| Type argument | Default       | Corresponding operation    |
| ------------- | ------------- | -------------------------- |
| `TWriteData`  | `any`         | `data` of `write(c, data)` |
| `TReadData`   | `TWriteData`  | return of `read(c)`        |
| `TWriteChunk` | `any`         | chunk of `getWritable(c)`  |
| `TReadChunk`  | `TWriteChunk` | chunk of `getReadable(c)`  |

`TReadData` defaults to `TWriteData`, and `TReadChunk` defaults to `TWriteChunk`. When read/write types match, specify only the data type argument (`TWriteData`); when you use no streams, omit the chunk type arguments.

```ts
// writes are bytes, reads are strings
type TextReadStorage = IStorage<Uint8Array, string>;

// specify down to chunks
type ChunkedStorage = IStorage<Uint8Array, string, ArrayBuffer, number>;
```

For the chunk/stream-type relationship, see [Streams](/unikvs/en/packages/core/stream#chunks).

## Identity [#identity]

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

`open(c)` and `close(c)` are optional. Implement them only when you need setup or teardown. Callers check state via `isOpen`.

## CRUD [#crud]

| Member   | Required | Signature                                                   | Description          |
| -------- | -------- | ----------------------------------------------------------- | -------------------- |
| `write`  | required | `(c: WriteContext, data: TWriteData) => MaybePromise<void>` | Stores data          |
| `read`   | required | `(c: ReadContext) => MaybePromise<TReadData>`               | Fetches data         |
| `exists` | required | `(c: ExistsContext) => MaybePromise<boolean>`               | Returns key presence |
| `delete` | required | `(c: DeleteContext) => MaybePromise<void>`                  | Deletes a key        |
| `clear`  | required | `(c: ClearContext) => MaybePromise<void>`                   | Deletes all data     |

Every method returns `MaybePromise` (`T | PromiseLike<T>`), so sync and async implementations both work.

:::warning[Missing keys throw]
`read` throws [`KeyNotFoundError`](/unikvs/en/packages/core/errors#error-key-not-found) for missing keys. Do not return `undefined` or embed an `exists`-like check; leave post-missing handling (such as read-time repair) to the caller.
:::

## Copy and move [#transfer]

| Member | Required | Signature                                | Description                 |
| ------ | -------- | ---------------------------------------- | --------------------------- |
| `copy` | optional | `(c: CopyContext) => MaybePromise<void>` | Copies a key to another key |
| `move` | optional | `(c: MoveContext) => MaybePromise<void>` | Moves a key to another key  |

`CopyContext` and `MoveContext` hold the source key in `key` and the destination key in `destinationKey`. An existing destination value is overwritten.

`move` removes the source only after the destination write succeeds. If the destination write fails, leave the source in place. If the source does not exist, throw [`KeyNotFoundError`](/unikvs/en/packages/core/errors#error-key-not-found). When `key` and `destinationKey` are the same, only verify that the source exists and do not delete it.

When `copy` or `move` is omitted, the internal `UniKvsStorage` reproduces the behavior with `read`, `write`, and `delete`. Implement them only when the backend has a more efficient native operation.

:::tip[Prefer native APIs]
If the backend provides copy or rename operations, prefer them over combining reads and writes. Otherwise, omit the implementation and let UniKvs reproduce it with `read`, `write`, and `delete`.
:::

## Stream acquisition [#streams]

| Member        | Required | Signature                                                               | Description                |
| ------------- | -------- | ----------------------------------------------------------------------- | -------------------------- |
| `getWritable` | optional | `(c: GetWritableContext) => MaybePromise<IWritableStream<TWriteChunk>>` | Acquires a writable stream |
| `getReadable` | optional | `(c: GetReadableContext) => MaybePromise<IReadableStream<TReadChunk>>`  | Acquires a readable stream |

Storages without stream support omit the implementation. Support is detected by runtime presence, not by types, so unimplemented operations must stay absent rather than throwing "unsupported".

:::note[Reject when aborted before start]
When called with an already-aborted signal, fail immediately with [`StorageAbortedError`](/unikvs/en/packages/core/errors#error-storage-aborted). See [Streams › Stream acquisition](/unikvs/en/packages/core/stream) and the stream example below.
:::

## Multi-storage writes [#other-write-error]

| Member              | Required | Description                                               |
| ------------------- | -------- | --------------------------------------------------------- |
| `onOtherWriteError` | optional | Receives other storages' failures on multi-storage writes |

Arguments are `(c: OtherWriteErrorHandlerContext, errors: readonly IOtherWriteError[])`. `OtherWriteErrorHandlerContext` is `WriteContext | GetWritableContext`, indicating which write path failed.

`IOtherWriteError` has the following shape:

| Prop | Type | Default | Description |
| - | - | - | - |
| `plugin` | `[#iotherwriteerror-plugin] "storage"` | - | Fixed value. |
| `errors` | `[#iotherwriteerror-errors] readonly { plugin: "storage", reason: unknown }[]` | - | Failure reason per other storage. |

## Context aliases [#contexts]

The `IStorage` namespace provides aliases of [pipeline contexts](/unikvs/en/packages/core/pipeline-context#contexts). Using `IStorage.*` on the plugin side clarifies intent.

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

During the `copy` / `move` fallback, `read`, `write`, and `delete` receive contexts with the original action (`"copy"` or `"move"`) and the key appropriate for the call. Therefore `ReadContext`, `WriteContext`, and `DeleteContext` also include the copy/move contexts.

## Example [#example]

**Sync**

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

**Async**

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

Implementation points:

1. **Pick type arguments**

    Declare the stored value type as `IStorage<T>`. When read/write types differ or you use streams, specify all four.

2. **Implement required members**

    `name`, `isOpen`, and the 5 CRUD methods are required. `read` throws `KeyNotFoundError` for missing keys. `copy` and `move` are optional; when omitted, `read`/`write`/`delete` provide the fallback.

3. **Add optional members**

    Implement `open` / `close` only with setup needs, `getWritable` / `getReadable` for streams, and `onOtherWriteError` when you need other storages' failure reports.

## API reference [#api]

### `IStorage` [#istorage]

Interface implemented by destination plugins. Four type arguments tune data and chunk types.

| Prop | Type | Default | Description |
| - | - | - | - |
| `name` | `[#istorage-name] string` | - | Plugin name. |
| `isOpen` | `[#istorage-isopen] boolean` | - | Whether initialized. |
| `open?` | `[#istorage-open] (c: IStorage.OpenContext) => MaybePromise<void>` | - | Runs initialization. |
| `close?` | `[#istorage-close] (c: IStorage.CloseContext) => MaybePromise<void>` | - | Runs teardown. |
| `write` | `[#istorage-write] (c: IStorage.WriteContext, data: TWriteData) => MaybePromise<void>` | - | Stores data. |
| `read` | `[#istorage-read] (c: IStorage.ReadContext) => MaybePromise<TReadData>` | - | Fetches data. Throws `KeyNotFoundError` when missing. |
| `exists` | `[#istorage-exists] (c: IStorage.ExistsContext) => MaybePromise<boolean>` | - | Returns key presence. |
| `delete` | `[#istorage-delete] (c: IStorage.DeleteContext) => MaybePromise<void>` | - | Deletes a key. |
| `clear` | `[#istorage-clear] (c: IStorage.ClearContext) => MaybePromise<void>` | - | Deletes all data. |
| `copy?` | `[#istorage-copy] (c: IStorage.CopyContext) => MaybePromise<void>` | - | Copies a key to another key. Falls back to read/write when omitted. |
| `move?` | `[#istorage-move] (c: IStorage.MoveContext) => MaybePromise<void>` | - | Moves a key to another key. Falls back to copy/delete when omitted. |
| `onOtherWriteError?` | `[#istorage-onotherwriteerror] (c: IStorage.OtherWriteErrorHandlerContext, errors: readonly IStorage.IOtherWriteError[]) => MaybePromise<void>` | - | Receives other storages' failures. |
