@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.nodepnpm add @unikvs/redis.nodeyarn add @unikvs/redis.nodebun add @unikvs/redis.nodenub add @unikvs/redis.nodeaube add @unikvs/redis.nodeioredis 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.
keyPrefixis a unikvs-side namespace prepended to every key. The default is"unikvs:", which limits whatclear()deletes to keys under this prefix. An empty string uses keys as-is. It is not used as the built-inkeyPrefixfeature ofioredisand is not passed through tonew Ioredis.clusteris reserved for future cluster support. v1 supports standalone only, and specifying it makes the constructor throwClusterNotSupportedErrorbeforecloseTimeoutvalidation.allowRepaircontrols whether repair writes are allowed. The default isfalse. When set tofalse, repairwriteandgetWritablecalls fail withRepairNotAllowedError.closeTimeoutis a package-specific extension: the cutoff limit in milliseconds forquit()inclose(). The default is5000. It is not passed tonew Ioredis. Only finite positive numbers are accepted;0, negative numbers,NaN, andInfinitycause the constructor to throwInvalidCloseTimeoutError.lazyConnectis not accepted at the type level. It is always treated astrue, connecting onopen().connectTimeoutis the cutoff limit in milliseconds for the initial connection. The default is5000.- All other properties are passed through to
ioredis, such astls, reconnection options,db,password, andusername.
The main members are as follows.
nameis"Redis".isOpenindicates 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 byconnectTimeout, and on failure it cleans up withdisconnect()and rethrows.close()is async, returnsPromise<void>, and must always be awaited.quit()is cut off bycloseTimeout, and on failure or timeout it callsdisconnect()and recovers normally. Calling it when not open, or calling it twice, is rejected with aTypeError.write,read,exists,delete, andclearhandle 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()usesSCANto enumerate keys matching`${this.keyPrefix}*`and deletes them withDELin batches ofCOUNT 1000. When the prefix is empty, the pattern is"*".- The temporary key for
getWritableis`${dest}.${randomUUID()}.tmp`. It is generated withrandomUUIDfromnode:cryptoand lives under the same prefix, soclear()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
writeorreadbeforeopen(). The internal connection isnulluntil then. Check withisOpen. open()waits until the connection is established before settingisOpento true. If connecting fails, it cleans up withdisconnect(),isOpenstays false, and the error propagates.- Calling
open()while already open closes the old connection and opens a new one. - Concurrent
opencalls, andclosecalls whileopenis 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 incompleteQUITor leftover handles. A second call or a call when not open is rejected with aTypeError.write,read,exists,delete, andclearreject when given an already abortedAbortSignalviasignal.throwIfAborted(). Redis commands cannot be cancelled after they are sent, so in-flight commands are not interrupted.writesaves withSETviaBuffer.from(data). Overwrites replace the whole value.readthrowsKeyNotFoundErrorfor missing keys.getReadablesurfaces the same error when the stream is read.deleteis idempotent and does not error for missing keys.existsreturns the boolean determined from the number returned byiorediswith> 0. Zero-byte values also returntrue.readandgetReadablenormalize theBufferreturned byioredisintonew 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();