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

@unikvs/redis.node

Learn how to use the storage plugin that saves byte sequences to Redis with ioredis on Node.js.

Overview

@unikvs/redis.node is a storage plugin that saves data to Redis using ioredis@^6.0.0. Keys are namespaced with a prefix.Node.js only

It handles byte sequences only. write accepts a Uint8Array<ArrayBuffer> and read returns a Uint8Array<ArrayBuffer>. Bytes are stored as-is in Redis string values and fetched with getBuffer. The driver is ioredis, and it is Node.js only.

Install it as follows.

npm install @unikvs/redis.node
pnpm add @unikvs/redis.node
yarn add @unikvs/redis.node
bun add @unikvs/redis.node
nub add @unikvs/redis.node
aube add @unikvs/redis.node

ioredis is listed in dependencies, so it is installed automatically. It depends on @unikvs/core.

Usage

The central class is Redis.

import { Redis } from "@unikvs/redis.node";
public constructor(url?: string, options?: RedisStorageOptions)

The second argument is optional.

url is the connection target. The resolution order is the url argument, REDIS_URL, VALKEY_URL, and the default 127.0.0.1:6379 when unspecified. An empty string is treated as unspecified. The format requires redis:// / rediss://; bare forms such as a bare hostname are not supported.

options is Omit<RedisOptions, "keyPrefix" | "lazyConnect"> plus package-specific fields.

  • keyPrefix is a unikvs-side namespace prepended to every key. The default is "unikvs:", which limits what clear() deletes to keys under this prefix. An empty string uses keys as-is. It is not used as the built-in keyPrefix feature of ioredis and is not passed through to new Ioredis.
  • cluster is reserved for future cluster support. v1 supports standalone only, and specifying it makes the constructor throw ClusterNotSupportedError before closeTimeout validation.
  • allowRepair controls whether repair writes are allowed. The default is false. When set to false, repair write and getWritable calls fail with RepairNotAllowedError.
  • closeTimeout is a package-specific extension: the cutoff limit in milliseconds for quit() in close(). The default is 5000. It is not passed to new Ioredis. Only finite positive numbers are accepted; 0, negative numbers, NaN, and Infinity cause the constructor to throw InvalidCloseTimeoutError.
  • lazyConnect is not accepted at the type level. It is always treated as true, connecting on open().
  • connectTimeout is the cutoff limit in milliseconds for the initial connection. The default is 5000.
  • All other properties are passed through to ioredis, such as tls, reconnection options, db, password, and username.

The main members are as follows.

  • name is "Redis".
  • isOpen indicates whether the storage is open.
  • open() creates the client and establishes the connection. When already open, it closes and reopens. The initial connection is cut off by connectTimeout, and on failure it cleans up with disconnect() and rethrows.
  • close() is async, returns Promise<void>, and must always be awaited. quit() is cut off by closeTimeout, and on failure or timeout it calls disconnect() and recovers normally. Calling it when not open, or calling it twice, is rejected with a TypeError.
  • write, read, exists, delete, and clear handle data.
import { Redis } from "@unikvs/redis.node";

const storage = new Redis("redis://localhost:6379", { keyPrefix: "myapp:" });
await storage.open();

await storage.write({
  key: "hello.bin",
  data: new TextEncoder().encode("hello"),
});

const data = await storage.read({ key: "hello.bin" });
console.log(new TextDecoder().decode(data));

await storage.close();

Key Layout

With the default prefix, the key hello.bin becomes the Redis key unikvs:hello.bin. No conversion or escaping beyond the prefix is applied.

unikvs:hello.bin
unikvs:greeting.bin
  • Keys are built with `${this.keyPrefix}${key}`.
  • Redis keys are binary-safe, so unlike @unikvs/fs.node, keys containing slashes, spaces, or : work as-is.
  • clear() uses SCAN to enumerate keys matching `${this.keyPrefix}*` and deletes them with DEL in batches of COUNT 1000. When the prefix is empty, the pattern is "*".
  • The temporary key for getWritable is `${dest}.${randomUUID()}.tmp`. It is generated with randomUUID from node:crypto and lives under the same prefix, so clear() also removes temporary keys left behind by aborts.

Streams

Both readable and writable streams are supported.

Obtain a writable stream with the following method.

public getWritable(
  args: Pick<IStorage.GetWritableArgs, "key" | "signal" | "vars">,
): WritableStream<Uint8Array<ArrayBuffer>>

It creates a temporary key with an empty string value on start and appends each chunk with APPEND via Buffer.from(chunk). It uses swap-on-close: data is replaced with the final key via RENAME on close. On failure or abort, it cleans up with del(tmp), preserving existing data. RENAME is atomic, so readers never observe partially written contents. Concurrent writes to the same key don’t collide thanks to unique temporary keys, and the last completed RENAME wins.

Obtain a readable stream with the following method.

public getReadable(
  args: Pick<IStorage.GetReadableArgs, "key" | "signal">,
): ReadableStream<Uint8Array<ArrayBuffer>>

Redis GET returns the whole value at once, so the bytes from getBuffer are emitted as a single chunk.

Notes

  • Node.js only. The driver is ioredis.
  • Don’t call write or read before open(). The internal connection is null until then. Check with isOpen.
  • open() waits until the connection is established before setting isOpen to true. If connecting fails, it cleans up with disconnect(), isOpen stays false, and the error propagates.
  • Calling open() while already open closes the old connection and opens a new one.
  • Concurrent open calls, and close calls while open is waiting to connect, are forbidden. Serialize them on the caller side.
  • close() is async and must always be awaited. Leaving the returned promise floating causes an incomplete QUIT or leftover handles. A second call or a call when not open is rejected with a TypeError.
  • write, read, exists, delete, and clear reject when given an already aborted AbortSignal via signal.throwIfAborted(). Redis commands cannot be cancelled after they are sent, so in-flight commands are not interrupted.
  • write saves with SET via Buffer.from(data). Overwrites replace the whole value.
  • read throws KeyNotFoundError for missing keys. getReadable surfaces the same error when the stream is read.
  • delete is idempotent and does not error for missing keys.
  • exists returns the boolean determined from the number returned by ioredis with > 0. Zero-byte values also return true.
  • read and getReadable normalize the Buffer returned by ioredis into new Uint8Array(data).
  • Persistence follows the Redis server configuration. Servers with RDB and AOF disabled lose data on restart.

Errors

Error Condition
KeyNotFoundError Thrown when reading a missing key with read or getReadable. A shared error from @unikvs/core.
ClusterNotSupportedError Thrown when constructed with cluster. v1 supports standalone only.
InvalidCloseTimeoutError Thrown when closeTimeout is not a finite positive number.
ConnectTimeoutError Thrown when the initial connection in open() exceeds connectTimeout.
CloseTimeoutError Thrown when closing exceeds closeTimeout.
import { KeyNotFoundError, Redis } from "@unikvs/redis.node";

const storage = new Redis();

try {
  await storage.open();
  await storage.read({ key: "missing.bin" });
} catch (error) {
  if (error instanceof KeyNotFoundError) {
    console.error(`Key ${error.meta.key} was not found`);
  } else {
    throw error;
  }
} finally {
  if (storage.isOpen) {
    await storage.close();
  }
}

Connection, authentication, and server-side rejections propagate the raw errors from ioredis as-is.

Examples

A minimal working example storing to redis://localhost:6379.

import { Redis } from "@unikvs/redis.node";

const storage = new Redis("redis://localhost:6379", { keyPrefix: "unikvs-example:" });
await storage.open();

const key = "greeting.bin";
await storage.write({
  key,
  data: new TextEncoder().encode("hello, Redis"),
});

if (await storage.exists({ key })) {
  const data = await storage.read({ key });
  console.log(new TextDecoder().decode(data));
}

const writer = storage.getWritable({ key: "stream.bin" }).getWriter();
await writer.write(new Uint8Array([1, 2, 3]));
await writer.write(new Uint8Array([4, 5, 6]));
await writer.close();

await storage.delete({ key });
await storage.clear();
await storage.close();

Was this page helpful?