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
5 changes: 5 additions & 0 deletions .changeset/warm-pandas-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@chat-adapter/state-redis": patch
---

Fix `createRedisState` typings so `url` appears in IntelliSense and the factory accepts an existing Redis client.
26 changes: 26 additions & 0 deletions packages/state-redis/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Logger } from "chat";
import type { RedisClientType } from "redis";
import { describe, expect, it, vi } from "vitest";
import { createRedisState, RedisStateAdapter } from "./index";

Expand All @@ -22,6 +23,31 @@ describe("RedisStateAdapter", () => {
expect(adapter).toBeInstanceOf(RedisStateAdapter);
});

it("should create an adapter with url and default logger", () => {
const adapter = createRedisState({
url: "redis://localhost:6379",
});

expect(adapter).toBeInstanceOf(RedisStateAdapter);
});

it("should accept an existing redis client", () => {
const client = {
close: vi.fn(),
connect: vi.fn(),
isOpen: true,
on: vi.fn(),
} as unknown as RedisClientType;

const adapter = createRedisState({
client,
logger: mockLogger,
});

expect(adapter).toBeInstanceOf(RedisStateAdapter);
expect(adapter.getClient()).toBe(client);
});

it("should have appendToList method", () => {
const adapter = createRedisState({
url: "redis://localhost:6379",
Expand Down
66 changes: 55 additions & 11 deletions packages/state-redis/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,31 @@ export interface RedisStateAdapterOptions {
/** Key prefix for all Redis keys (default: "chat-sdk") */
keyPrefix?: string;
/** Logger instance for error reporting */
logger: Logger;
logger?: Logger;
/** Redis connection URL (e.g., redis://localhost:6379) */
url: string;
}

export interface RedisStateClientOptions {
/** Existing redis client instance */
client: RedisClientType;
/** Key prefix for all Redis keys (default: "chat-sdk") */
keyPrefix?: string;
/** Logger instance for error reporting */
logger?: Logger;
}

export interface CreateRedisStateOptions {
/** Existing redis client instance */
client?: RedisClientType;
/** Key prefix for all Redis keys (default: "chat-sdk") */
keyPrefix?: string;
/** Logger instance for error reporting */
logger?: Logger;
/** Redis connection URL (e.g., redis://localhost:6379) */
url?: string;
}

/**
* Redis state adapter for production use.
*
Expand All @@ -21,13 +41,20 @@ export class RedisStateAdapter implements StateAdapter {
private readonly client: RedisClientType;
private readonly keyPrefix: string;
private readonly logger: Logger;
private readonly ownsClient: boolean;
private connected = false;
private connectPromise: Promise<void> | null = null;

constructor(options: RedisStateAdapterOptions) {
this.client = createClient({ url: options.url });
constructor(options: RedisStateAdapterOptions | RedisStateClientOptions) {
if ("client" in options) {
this.client = options.client;
this.ownsClient = false;
} else {
this.client = createClient({ url: options.url });
this.ownsClient = true;
}
this.keyPrefix = options.keyPrefix || "chat-sdk";
this.logger = options.logger;
this.logger = options.logger ?? new ConsoleLogger("info").child("redis");

// Handle connection errors
this.client.on("error", (err) => {
Expand All @@ -50,8 +77,15 @@ export class RedisStateAdapter implements StateAdapter {

// Reuse existing connection attempt to avoid race conditions
if (!this.connectPromise) {
this.connectPromise = this.client.connect().then(() => {
this.connectPromise = (async () => {
if (!this.client.isOpen) {
await this.client.connect();
}

this.connected = true;
})().catch((error) => {
this.connectPromise = null;
throw error;
});
}

Expand All @@ -60,7 +94,9 @@ export class RedisStateAdapter implements StateAdapter {

async disconnect(): Promise<void> {
if (this.connected) {
await this.client.close();
if (this.ownsClient) {
await this.client.close();
}
this.connected = false;
this.connectPromise = null;
}
Expand Down Expand Up @@ -321,18 +357,26 @@ function generateToken(): string {
}

export function createRedisState(
options?: Partial<RedisStateAdapterOptions>
options: CreateRedisStateOptions = {}
): RedisStateAdapter {
const url = options?.url ?? process.env.REDIS_URL;
if (options.client) {
return new RedisStateAdapter({
client: options.client,
keyPrefix: options.keyPrefix,
logger: options.logger,
});
}

const url = options.url ?? process.env.REDIS_URL;
if (!url) {
throw new Error(
"Redis url is required. Set REDIS_URL or provide it in options."
"Redis url is required. Set REDIS_URL or provide url in options."
);
}
const resolved: RedisStateAdapterOptions = {
url,
keyPrefix: options?.keyPrefix,
logger: options?.logger ?? new ConsoleLogger("info").child("redis"),
keyPrefix: options.keyPrefix,
logger: options.logger,
};
return new RedisStateAdapter(resolved);
}