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
7 changes: 7 additions & 0 deletions .changeset/hungry-dodos-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"bentocache": patch
"@bentocache/otel": patch
"@bentocache/docs": patch
---

feat(drivers): add Bun-native Redis, SQLite, and Postgres adapters
105 changes: 105 additions & 0 deletions docs/content/docs/cache_drivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,44 @@ const bento = new BentoCache({
|--------------|-------------------------------------------------------------------------------|---------|
| `connection` | The connection options to use to connect to Redis or an instance of `ioredis` | N/A |

### Bun Redis

**L2 driver, Bun runtime only.** Uses Bun's native `RedisClient` — no `ioredis` dependency. If you're running under Bun, this avoids the JavaScript-protocol implementation of `ioredis` in favor of Bun's Zig-backed client.

```ts
import { BentoCache, bentostore } from 'bentocache'
import { bunRedisDriver } from 'bentocache/drivers/bun_redis'

const bento = new BentoCache({
default: 'redis',
stores: {
redis: bentostore().useL2Layer(bunRedisDriver({
connection: 'redis://127.0.0.1:6379'
}))
}
})
```

You can also pass an existing `RedisClient` to reuse a connection:

```ts
import { RedisClient } from 'bun'

const client = new RedisClient('redis://127.0.0.1:6379')

const bento = new BentoCache({
default: 'redis',
stores: {
redis: bentostore().useL2Layer(bunRedisDriver({ connection: client }))
}
})
```

| Option | Description | Default |
|--------------|----------------------------------------------------------------------------------------------|---------|
| `connection` | A connection URL string, a `Bun.RedisOptions` object, or an existing `Bun.RedisClient`. | N/A |
| `options` | Extra `Bun.RedisOptions` forwarded to the `RedisClient` constructor. | N/A |

## Filesystem

The filesystem driver will store your cache in a distributed way in several files/folders on your filesystem.
Expand Down Expand Up @@ -259,3 +297,70 @@ export const bento = new BentoCache({
}
})
```

### Bun SQLite

**L2 driver, Bun runtime only.** Talks to `bun:sqlite` directly using prepared statements — no `better-sqlite3` native binding required.

```ts
import { BentoCache, bentostore } from 'bentocache'
import { bunSqliteDriver } from 'bentocache/drivers/bun_sqlite'

const bento = new BentoCache({
default: 'sqlite',
stores: {
sqlite: bentostore().useL2Layer(bunSqliteDriver({
connection: './cache.sqlite3'
}))
}
})
```

You can also pass an existing `Database` instance:

```ts
import { Database } from 'bun:sqlite'

const db = new Database('./cache.sqlite3')

const bento = new BentoCache({
default: 'sqlite',
stores: {
sqlite: bentostore().useL2Layer(bunSqliteDriver({ connection: db }))
}
})
```

Inherits the common SQL driver options (`tableName`, `autoCreateTable`, `pruneInterval`).

| Option | Description | Default |
|--------------|--------------------------------------------------------------------------------------|---------|
| `connection` | A filename (`':memory:'` for in-memory) or an existing `bun:sqlite` `Database`. | N/A |
| `options` | Open flags forwarded to `new Database(...)` (`readonly`, `create`, `readwrite`, …). | N/A |

### Bun Postgres

**L2 driver, Bun runtime only.** Uses Bun's native `SQL` Postgres client — no `pg` dependency.

```ts
import { BentoCache, bentostore } from 'bentocache'
import { bunPostgresDriver } from 'bentocache/drivers/bun_postgres'

const bento = new BentoCache({
default: 'pg',
stores: {
pg: bentostore().useL2Layer(bunPostgresDriver({
connection: 'postgres://user:pass@localhost:5432/app',
pruneInterval: '1h',
}))
}
})
```

You can also pass an existing `SQL` instance (e.g. `Bun.sql`) to reuse a connection.

Postgres has no native TTL, so set `pruneInterval` if you want expired entries cleaned automatically — same caveat as the existing Knex/Kysely Postgres path.

| Option | Description | Default |
|--------------|--------------------------------------------------------------------------|---------|
| `connection` | A Postgres connection URL string, or an existing `Bun.SQL` instance. | N/A |
5 changes: 5 additions & 0 deletions docs/content/docs/quick_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ pnpm add bentocache
// title: yarn
yarn add bentocache
```

```sh
// title: bun
bun add bentocache
```
:::


Expand Down
5 changes: 5 additions & 0 deletions docs/content/docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ pnpm add @bentocache/otel
// title: yarn
yarn add @bentocache/otel
```

```sh
// title: bun
bun add @bentocache/otel
```
:::

## Basic setup
Expand Down
5 changes: 5 additions & 0 deletions packages/bentocache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
"./drivers/knex": "./build/src/drivers/database/adapters/knex.js",
"./drivers/kysely": "./build/src/drivers/database/adapters/kysely.js",
"./drivers/orchid": "./build/src/drivers/database/adapters/orchid.js",
"./drivers/bun_redis": "./build/src/drivers/bun_redis.js",
"./drivers/bun_sqlite": "./build/src/drivers/database/adapters/bun_sqlite.js",
"./drivers/bun_postgres": "./build/src/drivers/database/adapters/bun_postgres.js",
"./types": "./build/src/types/main.js",
"./plugins/*": "./build/plugins/*.js",
"./test_suite": "./build/src/test_suite.js"
Expand Down Expand Up @@ -57,6 +60,7 @@
"typecheck": "tsc --noEmit",
"lint": "eslint .",
"quick:test": "cross-env NODE_NO_WARNINGS=1 node --enable-source-maps --loader=ts-node/esm bin/test.ts",
"quick:test:bun": "bun bin/test.ts",
"pretest": "pnpm lint",
"test": "c8 pnpm quick:test",
"build": "pnpm clean && tsup-node",
Expand Down Expand Up @@ -101,6 +105,7 @@
"devDependencies": {
"@aws-sdk/client-dynamodb": "^3.1019.0",
"@types/better-sqlite3": "^7.6.13",
"@types/bun": "^1.2.0",
"@types/pg": "^8.20.0",
"better-sqlite3": "^12.8.0",
"dayjs": "^1.11.20",
Expand Down
103 changes: 103 additions & 0 deletions packages/bentocache/src/drivers/bun_redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { RedisClient } from 'bun'

import { BaseDriver } from './base_driver.js'
import type { BunRedisConfig, CreateDriverResult, L2CacheDriver } from '../types/main.js'

export function bunRedisDriver(options: BunRedisConfig): CreateDriverResult<BunRedisDriver> {
return { options, factory: (config: BunRedisConfig) => new BunRedisDriver(config) }
}

export class BunRedisDriver extends BaseDriver implements L2CacheDriver {
type = 'l2' as const
#connection: RedisClient
declare config: BunRedisConfig

constructor(config: BunRedisConfig) {
super(config)

if (config.connection instanceof RedisClient) {
this.#connection = config.connection
return
}

if (typeof config.connection === 'string') {
this.#connection = new RedisClient(config.connection, config.options)
return
}

const { url, ...rest } = config.connection
this.#connection = new RedisClient(url, { ...rest, ...config.options })
}

getConnection() {
return this.#connection
}

namespace(namespace: string) {
return new BunRedisDriver({
...this.config,
connection: this.#connection,
prefix: this.createNamespacePrefix(namespace),
})
}

async get(key: string) {
const result = await this.#connection.get(this.getItemKey(key))
return result ?? undefined
}

async pull(key: string) {
const result = (await this.#connection.send('GETDEL', [this.getItemKey(key)])) as string | null
return result ?? undefined
}

async set(key: string, value: string, ttl?: number) {
const itemKey = this.getItemKey(key)

if (!ttl) {
const result = (await this.#connection.send('SET', [itemKey, value])) as string | null
return result === 'OK'
}

const result = (await this.#connection.send('SET', [itemKey, value, 'PX', String(ttl)])) as
| string
| null
return result === 'OK'
}

async clear() {
let cursor = '0'
const pattern = this.prefix ? `${this.prefix}:*` : '*'

do {
const [nextCursor, keys] = (await this.#connection.send('SCAN', [
cursor,
'MATCH',
pattern,
'COUNT',
'1000',
])) as [string, string[]]

if (keys.length) await this.#connection.send('UNLINK', keys)
cursor = nextCursor
} while (cursor !== '0')
}

async delete(key: string) {
const deleted = (await this.#connection.send('UNLINK', [this.getItemKey(key)])) as number
return deleted > 0
}

async deleteMany(keys: string[]) {
if (keys.length === 0) return true
await this.#connection.send(
'UNLINK',
keys.map((key) => this.getItemKey(key)),
)
return true
}

async disconnect() {
this.#connection.close()
}
}
93 changes: 93 additions & 0 deletions packages/bentocache/src/drivers/database/adapters/bun_postgres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { SQL } from 'bun'

import { DatabaseDriver } from '../database.js'
import type { BunPostgresConfig, CreateDriverResult, DatabaseAdapter } from '../../../types/main.js'

export function bunPostgresDriver(options: BunPostgresConfig): CreateDriverResult<DatabaseDriver> {
return {
options,
factory: (config: BunPostgresConfig) => {
const adapter = new BunPostgresAdapter(config)
return new DatabaseDriver(adapter, config)
},
}
}

export class BunPostgresAdapter implements DatabaseAdapter {
#sql: SQL
#tableName!: string

constructor(config: BunPostgresConfig) {
this.#sql = config.connection instanceof SQL ? config.connection : new SQL(config.connection)
}

setTableName(tableName: string) {
this.#tableName = tableName
}

#table() {
return this.#sql(this.#tableName)
}

async get(key: string) {
const rows = await this.#sql<
Array<{ value: string; expires_at: number | string | null }>
>`SELECT value, expires_at FROM ${this.#table()} WHERE key = ${key}`

const row = rows[0]
if (!row) return

return {
value: row.value,
expiresAt:
row.expires_at !== null && row.expires_at !== undefined ? Number(row.expires_at) : null,
}
}

async set(row: { key: string; value: any; expiresAt: Date | null }) {
const expiresAt = row.expiresAt?.getTime() ?? null

await this.#sql`
INSERT INTO ${this.#table()} (key, value, expires_at)
VALUES (${row.key}, ${row.value}, ${expiresAt})
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at
`
}

async delete(key: string) {
const result = await this.#sql`DELETE FROM ${this.#table()} WHERE key = ${key}`
return (result.count ?? 0) > 0
}

async deleteMany(keys: string[]) {
let count = 0
for (const key of keys) {
const result = await this.#sql`DELETE FROM ${this.#table()} WHERE key = ${key}`
count += result.count ?? 0
}
return count
}

async createTableIfNotExists() {
await this.#sql`
CREATE TABLE IF NOT EXISTS ${this.#table()} (
key VARCHAR(255) PRIMARY KEY NOT NULL,
value TEXT,
expires_at BIGINT
)
`
}

async pruneExpiredEntries() {
await this.#sql`DELETE FROM ${this.#table()} WHERE expires_at < ${Date.now()}`
}

async clear(prefix: string) {
await this.#sql`DELETE FROM ${this.#table()} WHERE key LIKE ${`${prefix}%`}`
}

async disconnect() {
await this.#sql.end()
}
}
Loading