Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/2.drivers/0.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ icon: icon-park-outline:hard-disk
::
::card
---
icon: simple-icons:bun
to: /drivers/bun-redis
title: Bun Redis
color: gray
---
Store data in Redis using Bun's native RedisClient.
::
::card
---
icon: ph:database
to: /drivers/database
title: SQL Database
Expand Down
76 changes: 76 additions & 0 deletions docs/2.drivers/bun-redis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
icon: simple-icons:bun
---

# Bun Redis

> Store data in Redis using Bun's native RedisClient.

## Usage

**Driver name:** `bun-redis`

::read-more{to="https://bun.sh/docs/runtime/redis"}
Learn more about Bun's Redis client.
::

::note
This driver uses Bun's native [`RedisClient`](https://bun.sh/docs/runtime/redis) for high-performance Redis operations with automatic reconnection, connection pooling, and auto-pipelining.
::

::warning
This driver requires the [Bun runtime](https://bun.sh). It will not work in Node.js or other JavaScript runtimes.
::

Usage with Redis URL:

```js
import { createStorage } from "unstorage";
import bunRedisDriver from "unstorage/drivers/bun-redis";

const storage = createStorage({
driver: bunRedisDriver({
base: "unstorage",
url: "redis://localhost:6379",
}),
});
```

Usage with connection options:

```js
const storage = createStorage({
driver: bunRedisDriver({
base: "unstorage",
url: "redis://localhost:6379",
connectionTimeout: 10000,
autoReconnect: true,
maxRetries: 10,
enableAutoPipelining: true,
}),
});
```

Usage without URL (uses environment variables `REDIS_URL` or `VALKEY_URL`):

```js
const storage = createStorage({
driver: bunRedisDriver({
base: "unstorage",
}),
});
```

**Options:**

- `base`: Optional prefix to use for all keys. Can be used for namespacing.
- `url`: Url to use for connecting to redis. Takes precedence over other connection options. Has the format `redis://<REDIS_USER>:<REDIS_PASSWORD>@<REDIS_HOST>:<REDIS_PORT>`
- `ttl`: Default TTL for all items in **seconds**.
- `scanCount`: How many keys to scan at once ([redis documentation](https://redis.io/docs/latest/commands/scan/#the-count-option)).
- `preConnect`: Whether to initialize the redis instance immediately. Otherwise, it will be initialized on the first read/write call. Default: `false`.

See [Bun RedisClient options](https://bun.sh/docs/runtime/redis) for all available connection options.

**Transaction options:**

- `ttl`: Supported for `setItem(key, value, { ttl: number /* seconds */ })`
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
"uploadthing": "^7.7.4",
"vite": "^7.2.4",
"vitest": "^4.0.12",
"wrangler": "^4.49.1"
"wrangler": "^4.49.1",
"bun-types": "^1.3.0"
},
"peerDependencies": {
"@azure/app-configuration": "^1.9.0",
Expand All @@ -108,7 +109,8 @@
"lru-cache": "^11.2.2",
"mongodb": "^6|^7",
"ofetch": "*",
"uploadthing": "^7.7.4"
"uploadthing": "^7.7.4",
"bun-types": "^1.3.0"
},
"peerDependenciesMeta": {
"@azure/app-configuration": {
Expand Down Expand Up @@ -179,6 +181,9 @@
},
"uploadthing": {
"optional": true
},
"bun-types": {
"optional": true
}
},
"packageManager": "pnpm@10.23.0",
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/_drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { AzureCosmosOptions as AzureCosmosOptions } from "unstorage/drivers
import type { AzureKeyVaultOptions as AzureKeyVaultOptions } from "unstorage/drivers/azure-key-vault";
import type { AzureStorageBlobOptions as AzureStorageBlobOptions } from "unstorage/drivers/azure-storage-blob";
import type { AzureStorageTableOptions as AzureStorageTableOptions } from "unstorage/drivers/azure-storage-table";
import type { RedisOptions as BunRedisOptions } from "unstorage/drivers/bun-redis";
import type { CapacitorPreferencesOptions as CapacitorPreferencesOptions } from "unstorage/drivers/capacitor-preferences";
import type { KVOptions as CloudflareKVBindingOptions } from "unstorage/drivers/cloudflare-kv-binding";
import type { KVHTTPOptions as CloudflareKVHttpOptions } from "unstorage/drivers/cloudflare-kv-http";
Expand Down Expand Up @@ -33,7 +34,7 @@ import type { VercelBlobOptions as VercelBlobOptions } from "unstorage/drivers/v
import type { VercelKVOptions as VercelKVOptions } from "unstorage/drivers/vercel-kv";
import type { VercelCacheOptions as VercelRuntimeCacheOptions } from "unstorage/drivers/vercel-runtime-cache";

export type BuiltinDriverName = "azure-app-configuration" | "azureAppConfiguration" | "azure-cosmos" | "azureCosmos" | "azure-key-vault" | "azureKeyVault" | "azure-storage-blob" | "azureStorageBlob" | "azure-storage-table" | "azureStorageTable" | "capacitor-preferences" | "capacitorPreferences" | "cloudflare-kv-binding" | "cloudflareKVBinding" | "cloudflare-kv-http" | "cloudflareKVHttp" | "cloudflare-r2-binding" | "cloudflareR2Binding" | "db0" | "deno-kv-node" | "denoKVNode" | "deno-kv" | "denoKV" | "fs-lite" | "fsLite" | "fs" | "github" | "http" | "indexedb" | "localstorage" | "lru-cache" | "lruCache" | "memory" | "mongodb" | "netlify-blobs" | "netlifyBlobs" | "null" | "overlay" | "planetscale" | "redis" | "s3" | "session-storage" | "sessionStorage" | "uploadthing" | "upstash" | "vercel-blob" | "vercelBlob" | "vercel-kv" | "vercelKV" | "vercel-runtime-cache" | "vercelRuntimeCache";
export type BuiltinDriverName = "azure-app-configuration" | "azureAppConfiguration" | "azure-cosmos" | "azureCosmos" | "azure-key-vault" | "azureKeyVault" | "azure-storage-blob" | "azureStorageBlob" | "azure-storage-table" | "azureStorageTable" | "bun-redis" | "bunRedis" | "capacitor-preferences" | "capacitorPreferences" | "cloudflare-kv-binding" | "cloudflareKVBinding" | "cloudflare-kv-http" | "cloudflareKVHttp" | "cloudflare-r2-binding" | "cloudflareR2Binding" | "db0" | "deno-kv-node" | "denoKVNode" | "deno-kv" | "denoKV" | "fs-lite" | "fsLite" | "fs" | "github" | "http" | "indexedb" | "localstorage" | "lru-cache" | "lruCache" | "memory" | "mongodb" | "netlify-blobs" | "netlifyBlobs" | "null" | "overlay" | "planetscale" | "redis" | "s3" | "session-storage" | "sessionStorage" | "uploadthing" | "upstash" | "vercel-blob" | "vercelBlob" | "vercel-kv" | "vercelKV" | "vercel-runtime-cache" | "vercelRuntimeCache";

export type BuiltinDriverOptions = {
"azure-app-configuration": AzureAppConfigurationOptions;
Expand All @@ -46,6 +47,8 @@ export type BuiltinDriverOptions = {
"azureStorageBlob": AzureStorageBlobOptions;
"azure-storage-table": AzureStorageTableOptions;
"azureStorageTable": AzureStorageTableOptions;
"bun-redis": BunRedisOptions;
"bunRedis": BunRedisOptions;
"capacitor-preferences": CapacitorPreferencesOptions;
"capacitorPreferences": CapacitorPreferencesOptions;
"cloudflare-kv-binding": CloudflareKVBindingOptions;
Expand Down Expand Up @@ -98,6 +101,8 @@ export const builtinDrivers = {
"azureStorageBlob": "unstorage/drivers/azure-storage-blob",
"azure-storage-table": "unstorage/drivers/azure-storage-table",
"azureStorageTable": "unstorage/drivers/azure-storage-table",
"bun-redis": "unstorage/drivers/bun-redis",
"bunRedis": "unstorage/drivers/bun-redis",
"capacitor-preferences": "unstorage/drivers/capacitor-preferences",
"capacitorPreferences": "unstorage/drivers/capacitor-preferences",
"cloudflare-kv-binding": "unstorage/drivers/cloudflare-kv-binding",
Expand Down
129 changes: 129 additions & 0 deletions src/drivers/bun-redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/// <reference types="bun-types" />

import { type RedisOptions as _RedisOptions, RedisClient } from "bun";
import { defineDriver, joinKeys } from "./utils/index.ts";

export interface RedisOptions extends _RedisOptions {
/**
* Optional prefix to use for all keys. Can be used for namespacing.
*/
base?: string;

/**
* Url to use for connecting to redis. Takes precedence over `host` option. Has the format `redis://<REDIS_USER>:<REDIS_PASSWORD>@<REDIS_HOST>:<REDIS_PORT>`
*/
url?: string;

/**
* Default TTL for all items in seconds.
*/
ttl?: number;

/**
* How many keys to scan at once.
*
* [redis documentation](https://redis.io/docs/latest/commands/scan/#the-count-option)
*/
scanCount?: number;

/**
* Whether to initialize the redis instance immediately.
* Otherwise, it will be initialized on the first read/write call.
* @default false
*/
preConnect?: boolean;
}

const DRIVER_NAME = "bun-redis";

export default defineDriver((opts: RedisOptions) => {
let redisClient: RedisClient;
const getRedisClient = () => {
if (redisClient) {
return redisClient;
}
const { url, ...redisOpts } = opts;
if (url) {
redisClient = new RedisClient(url, redisOpts);
} else {
redisClient = new RedisClient();
}
return redisClient;
};

const base = (opts.base || "").replace(/:$/, "");
const p = (...keys: string[]) => joinKeys(base, ...keys);
const d = (key: string) => (base ? key.replace(`${base}:`, "") : key);

if (opts.preConnect) {
try {
getRedisClient();
} catch {
// Silent fail
}
}

const scan = async (pattern: string): Promise<string[]> => {
const client = getRedisClient();
const keys: string[] = [];
let cursor = "0";
do {
const [nextCursor, scanKeys] = opts.scanCount
? await client.scan(cursor, "MATCH", pattern, "COUNT", opts.scanCount)
: await client.scan(cursor, "MATCH", pattern);
cursor = nextCursor;
keys.push(...scanKeys);
} while (cursor !== "0");
return keys;
};

return {
name: DRIVER_NAME,
options: opts,
getInstance: getRedisClient,
async hasItem(key) {
const result = await getRedisClient().exists(p(key));
return Boolean(result);
},
async getItem(key) {
const value = await getRedisClient().get(p(key));
return value ?? null;
},
async getItems(items) {
const keys = items.map((item) => p(item.key));
const data = await getRedisClient().mget(...keys);

return keys.map((key, index) => {
return {
key: d(key),
value: data[index] ?? null,
};
});
},
async setItem(key, value, tOptions) {
const ttl = tOptions?.ttl ?? opts.ttl;
if (ttl) {
await getRedisClient().set(p(key), value, "EX", String(ttl));
} else {
await getRedisClient().set(p(key), value);
}
},
async removeItem(key) {
await getRedisClient().del(p(key));
},
async getKeys(base) {
const keys = await scan(p(base, "*"));
return keys.map((key) => d(key));
},
async clear(base) {
const keys = await scan(p(base, "*"));
if (keys.length === 0) {
return;
}
await getRedisClient().del(...keys);
},
dispose() {
return getRedisClient().close();
},
};
});
50 changes: 50 additions & 0 deletions test/drivers/bun-redis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { RedisClient } from "bun";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import bunRedisDriver from "../../src/drivers/bun-redis.ts";
import { testDriver } from "./utils.ts";

describe("drivers: bun-redis", () => {
let testRedisClient: RedisClient;

beforeAll(async () => {
testRedisClient = new RedisClient("redis://localhost:6379");
try {
await testRedisClient.connect();
} catch {
// Redis might not be available, tests will be skipped
}
});

afterAll(async () => {
testRedisClient?.close();
});

const driver = bunRedisDriver({
base: "test:",
url: "redis://localhost:6379",
});

testDriver({
driver,
additionalTests(ctx) {
it("verify stored keys", async () => {
await ctx.storage.setItem("s1:a", "test_data");
await ctx.storage.setItem("s2:a", "test_data");
await ctx.storage.setItem("s3:a?q=1", "test_data");

const keys = await testRedisClient.keys("test:*");
expect(keys).toMatchInlineSnapshot(`
[
"test:s1:a",
"test:s2:a",
"test:s3:a",
]
`);
});

it("exposes instance", () => {
expect(driver.getInstance?.()).toBeInstanceOf(RedisClient);
});
},
});
});