Skip to content
UniKVS
Esc
↑↓navigate↵open⌘Jpreview
On this page

Middleware

@unikvs/core IMiddleware: hooks and contexts implemented by middleware.

Overview

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

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

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

Type arguments

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

Member Required Description
name required Plugin name
isOpen required Whether initialized

open(c) and close(c) are optional. As with storage lifecycles, implement them only when needed.

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

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

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. Narrow by action for type-safe access to each operation’s key, repair, or destinationKey.

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

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.

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.

encode and decode

Data conversion and its inverse. source is the input; assign the result to destination.

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

Stream conversion and its inverse. Unlike data conversion, source and destination are IReadableStream.

next

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

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.

Context aliases

The IMiddleware namespace provides aliases of pipeline contexts.

Alias Entity
IMiddleware.MapKeyContext IMapKeyContext
IMiddleware.OpenContext IOpenContext
IMiddleware.CloseContext ICloseContext
IMiddleware.UseContext ISetContext | IGetContext | IStreamContext | IHasContext | IDeleteContext | ICopyContext | IMoveContext | IClearContext
IMiddleware.EncodeContext<T> IEncodeContext<T, T>
IMiddleware.DecodeContext<T> IDecodeContext<T, T>
IMiddleware.EncodeStreamContext<T> IEncodeStreamContext<T, T>
IMiddleware.DecodeStreamContext<T> IDecodeStreamContext<T, T>

Example

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

Was this page helpful?