@chat-adapter/state-ioredis 4.13.1 → 4.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # @chat-adapter/state-ioredis
2
2
 
3
- Redis state adapter for the [chat](https://github.com/vercel-labs/chat) SDK using [ioredis](https://www.npmjs.com/package/ioredis).
3
+ [![npm version](https://img.shields.io/npm/v/@chat-adapter/state-ioredis)](https://www.npmjs.com/package/@chat-adapter/state-ioredis)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@chat-adapter/state-ioredis)](https://www.npmjs.com/package/@chat-adapter/state-ioredis)
5
+
6
+ Redis state adapter for [Chat SDK](https://chat-sdk.dev/docs) using [ioredis](https://www.npmjs.com/package/ioredis). Use this if you need Redis Cluster or Sentinel support.
4
7
 
5
8
  ## Installation
6
9
 
7
10
  ```bash
8
- npm install chat @chat-adapter/state-ioredis ioredis
11
+ npm install chat @chat-adapter/state-ioredis
9
12
  ```
10
13
 
11
14
  ## Usage
@@ -14,7 +17,7 @@ npm install chat @chat-adapter/state-ioredis ioredis
14
17
  import { Chat } from "chat";
15
18
  import { createIORedisState } from "@chat-adapter/state-ioredis";
16
19
 
17
- const chat = new Chat({
20
+ const bot = new Chat({
18
21
  userName: "mybot",
19
22
  adapters: { /* ... */ },
20
23
  state: createIORedisState({
@@ -23,64 +26,9 @@ const chat = new Chat({
23
26
  });
24
27
  ```
25
28
 
26
- ## Configuration
27
-
28
- | Option | Required | Description |
29
- |--------|----------|-------------|
30
- | `url` | Yes* | Redis connection URL |
31
- | `client` | No | Existing ioredis client instance |
32
- | `keyPrefix` | No | Prefix for all keys (default: `"chat-sdk"`) |
33
-
34
- *Either `url` or `client` is required.
35
-
36
- ### Using Connection URL
37
-
38
- ```typescript
39
- const state = createIORedisState({
40
- url: "redis://localhost:6379",
41
- });
42
- ```
43
-
44
- ### Using Existing Client
45
-
46
- ```typescript
47
- import Redis from "ioredis";
48
-
49
- const client = new Redis("redis://localhost:6379");
50
-
51
- const state = createIORedisState({ client });
52
- ```
53
-
54
- ## When to Use ioredis vs redis
55
-
56
- Use `@chat-adapter/state-ioredis` when:
57
-
58
- - You're already using ioredis in your project
59
- - You need Redis Cluster support
60
- - You need Redis Sentinel support
61
- - You prefer ioredis API
62
-
63
- Use `@chat-adapter/state-redis` when:
29
+ ## Documentation
64
30
 
65
- - You want the official Redis client
66
- - You're starting a new project
67
- - You don't need Cluster/Sentinel
68
-
69
- ## Features
70
-
71
- - Thread subscriptions (persistent)
72
- - Distributed locking (works across instances)
73
- - Automatic reconnection
74
- - Redis Cluster support
75
- - Redis Sentinel support
76
- - Key prefix namespacing
77
-
78
- ## Key Structure
79
-
80
- ```
81
- {keyPrefix}:subscriptions - SET of subscribed thread IDs
82
- {keyPrefix}:lock:{threadId} - Lock key with TTL
83
- ```
31
+ Full documentation at [chat-sdk.dev/docs/state/ioredis](https://chat-sdk.dev/docs/state/ioredis).
84
32
 
85
33
  ## License
86
34
 
package/dist/index.d.ts CHANGED
@@ -2,12 +2,12 @@ import { Logger, StateAdapter, Lock } from 'chat';
2
2
  import Redis from 'ioredis';
3
3
 
4
4
  interface IoRedisStateAdapterOptions {
5
- /** Redis connection URL (e.g., redis://localhost:6379) */
6
- url: string;
7
5
  /** Key prefix for all Redis keys (default: "chat-sdk") */
8
6
  keyPrefix?: string;
9
7
  /** Logger instance for error reporting */
10
8
  logger: Logger;
9
+ /** Redis connection URL (e.g., redis://localhost:6379) */
10
+ url: string;
11
11
  }
12
12
  interface IoRedisStateClientOptions {
13
13
  /** Existing ioredis client instance */
@@ -34,12 +34,12 @@ interface IoRedisStateClientOptions {
34
34
  * ```
35
35
  */
36
36
  declare class IoRedisStateAdapter implements StateAdapter {
37
- private client;
38
- private keyPrefix;
39
- private logger;
37
+ private readonly client;
38
+ private readonly keyPrefix;
39
+ private readonly logger;
40
40
  private connected;
41
41
  private connectPromise;
42
- private ownsClient;
42
+ private readonly ownsClient;
43
43
  constructor(options: IoRedisStateAdapterOptions | IoRedisStateClientOptions);
44
44
  private key;
45
45
  private subscriptionsSetKey;
@@ -48,7 +48,6 @@ declare class IoRedisStateAdapter implements StateAdapter {
48
48
  subscribe(threadId: string): Promise<void>;
49
49
  unsubscribe(threadId: string): Promise<void>;
50
50
  isSubscribed(threadId: string): Promise<boolean>;
51
- listSubscriptions(adapterName?: string): AsyncIterable<string>;
52
51
  acquireLock(threadId: string, ttlMs: number): Promise<Lock | null>;
53
52
  releaseLock(lock: Lock): Promise<void>;
54
53
  extendLock(lock: Lock, ttlMs: number): Promise<boolean>;
package/dist/index.js CHANGED
@@ -72,28 +72,6 @@ var IoRedisStateAdapter = class {
72
72
  );
73
73
  return result === 1;
74
74
  }
75
- async *listSubscriptions(adapterName) {
76
- this.ensureConnected();
77
- let cursor = "0";
78
- do {
79
- const [nextCursor, members] = await this.client.sscan(
80
- this.subscriptionsSetKey(),
81
- cursor,
82
- "COUNT",
83
- 100
84
- );
85
- cursor = nextCursor;
86
- for (const threadId of members) {
87
- if (adapterName) {
88
- if (threadId.startsWith(`${adapterName}:`)) {
89
- yield threadId;
90
- }
91
- } else {
92
- yield threadId;
93
- }
94
- }
95
- } while (cursor !== "0");
96
- }
97
75
  async acquireLock(threadId, ttlMs) {
98
76
  this.ensureConnected();
99
77
  const token = generateToken();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Lock, Logger, StateAdapter } from \"chat\";\nimport Redis from \"ioredis\";\n\nexport interface IoRedisStateAdapterOptions {\n /** Redis connection URL (e.g., redis://localhost:6379) */\n url: string;\n /** Key prefix for all Redis keys (default: \"chat-sdk\") */\n keyPrefix?: string;\n /** Logger instance for error reporting */\n logger: Logger;\n}\n\nexport interface IoRedisStateClientOptions {\n /** Existing ioredis client instance */\n client: Redis;\n /** Key prefix for all Redis keys (default: \"chat-sdk\") */\n keyPrefix?: string;\n /** Logger instance for error reporting */\n logger: Logger;\n}\n\n/**\n * Redis state adapter using ioredis for production use.\n *\n * Provides persistent subscriptions and distributed locking\n * across multiple server instances.\n *\n * @example\n * ```typescript\n * // With URL\n * const state = createIoRedisState({ url: process.env.REDIS_URL });\n *\n * // With existing client\n * const client = new Redis(process.env.REDIS_URL);\n * const state = createIoRedisState({ client });\n * ```\n */\nexport class IoRedisStateAdapter implements StateAdapter {\n private client: Redis;\n private keyPrefix: string;\n private logger: Logger;\n private connected = false;\n private connectPromise: Promise<void> | null = null;\n private ownsClient: boolean;\n\n constructor(options: IoRedisStateAdapterOptions | IoRedisStateClientOptions) {\n if (\"client\" in options) {\n this.client = options.client;\n this.ownsClient = false;\n } else {\n this.client = new Redis(options.url);\n this.ownsClient = true;\n }\n this.keyPrefix = options.keyPrefix || \"chat-sdk\";\n this.logger = options.logger;\n\n // Handle connection errors\n this.client.on(\"error\", (err) => {\n this.logger.error(\"ioredis client error\", { error: err });\n });\n }\n\n private key(type: \"sub\" | \"lock\" | \"cache\", id: string): string {\n return `${this.keyPrefix}:${type}:${id}`;\n }\n\n private subscriptionsSetKey(): string {\n return `${this.keyPrefix}:subscriptions`;\n }\n\n async connect(): Promise<void> {\n // ioredis auto-connects, but we track state for consistency\n if (this.connected) {\n return;\n }\n\n // Reuse existing connection attempt to avoid race conditions\n if (!this.connectPromise) {\n this.connectPromise = new Promise<void>((resolve, reject) => {\n if (this.client.status === \"ready\") {\n this.connected = true;\n resolve();\n return;\n }\n\n this.client.once(\"ready\", () => {\n this.connected = true;\n resolve();\n });\n\n this.client.once(\"error\", (err) => {\n reject(err);\n });\n });\n }\n\n await this.connectPromise;\n }\n\n async disconnect(): Promise<void> {\n if (this.connected && this.ownsClient) {\n await this.client.quit();\n this.connected = false;\n this.connectPromise = null;\n }\n }\n\n async subscribe(threadId: string): Promise<void> {\n this.ensureConnected();\n await this.client.sadd(this.subscriptionsSetKey(), threadId);\n }\n\n async unsubscribe(threadId: string): Promise<void> {\n this.ensureConnected();\n await this.client.srem(this.subscriptionsSetKey(), threadId);\n }\n\n async isSubscribed(threadId: string): Promise<boolean> {\n this.ensureConnected();\n const result = await this.client.sismember(\n this.subscriptionsSetKey(),\n threadId,\n );\n return result === 1;\n }\n\n async *listSubscriptions(adapterName?: string): AsyncIterable<string> {\n this.ensureConnected();\n\n // Use SSCAN for large sets to avoid blocking\n let cursor = \"0\";\n do {\n const [nextCursor, members] = await this.client.sscan(\n this.subscriptionsSetKey(),\n cursor,\n \"COUNT\",\n 100,\n );\n cursor = nextCursor;\n\n for (const threadId of members) {\n if (adapterName) {\n if (threadId.startsWith(`${adapterName}:`)) {\n yield threadId;\n }\n } else {\n yield threadId;\n }\n }\n } while (cursor !== \"0\");\n }\n\n async acquireLock(threadId: string, ttlMs: number): Promise<Lock | null> {\n this.ensureConnected();\n\n const token = generateToken();\n const lockKey = this.key(\"lock\", threadId);\n\n // Use SET NX PX for atomic lock acquisition\n const acquired = await this.client.set(lockKey, token, \"PX\", ttlMs, \"NX\");\n\n if (acquired === \"OK\") {\n return {\n threadId,\n token,\n expiresAt: Date.now() + ttlMs,\n };\n }\n\n return null;\n }\n\n async releaseLock(lock: Lock): Promise<void> {\n this.ensureConnected();\n\n const lockKey = this.key(\"lock\", lock.threadId);\n\n // Use Lua script for atomic check-and-delete\n const script = `\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"del\", KEYS[1])\n else\n return 0\n end\n `;\n\n await this.client.eval(script, 1, lockKey, lock.token);\n }\n\n async extendLock(lock: Lock, ttlMs: number): Promise<boolean> {\n this.ensureConnected();\n\n const lockKey = this.key(\"lock\", lock.threadId);\n\n // Use Lua script for atomic check-and-extend\n const script = `\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"pexpire\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n\n const result = await this.client.eval(\n script,\n 1,\n lockKey,\n lock.token,\n ttlMs.toString(),\n );\n\n return result === 1;\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n const value = await this.client.get(cacheKey);\n\n if (value === null) {\n return null;\n }\n\n try {\n return JSON.parse(value) as T;\n } catch {\n // If parsing fails, return as string\n return value as unknown as T;\n }\n }\n\n async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n const serialized = JSON.stringify(value);\n\n if (ttlMs) {\n await this.client.set(cacheKey, serialized, \"PX\", ttlMs);\n } else {\n await this.client.set(cacheKey, serialized);\n }\n }\n\n async delete(key: string): Promise<void> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n await this.client.del(cacheKey);\n }\n\n private ensureConnected(): void {\n if (!this.connected) {\n throw new Error(\n \"IoRedisStateAdapter is not connected. Call connect() first.\",\n );\n }\n }\n\n /**\n * Get the underlying ioredis client for advanced usage.\n */\n getClient(): Redis {\n return this.client;\n }\n}\n\nfunction generateToken(): string {\n return `ioredis_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n}\n\n/**\n * Create an ioredis state adapter.\n *\n * @example\n * ```typescript\n * // With URL\n * const state = createIoRedisState({ url: process.env.REDIS_URL });\n *\n * // With existing client\n * import Redis from \"ioredis\";\n * const client = new Redis(process.env.REDIS_URL);\n * const state = createIoRedisState({ client });\n * ```\n */\nexport function createIoRedisState(\n options: IoRedisStateAdapterOptions | IoRedisStateClientOptions,\n): IoRedisStateAdapter {\n return new IoRedisStateAdapter(options);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Lock, Logger, StateAdapter } from \"chat\";\nimport Redis from \"ioredis\";\n\nexport interface IoRedisStateAdapterOptions {\n /** Key prefix for all Redis keys (default: \"chat-sdk\") */\n keyPrefix?: string;\n /** Logger instance for error reporting */\n logger: Logger;\n /** Redis connection URL (e.g., redis://localhost:6379) */\n url: string;\n}\n\nexport interface IoRedisStateClientOptions {\n /** Existing ioredis client instance */\n client: Redis;\n /** Key prefix for all Redis keys (default: \"chat-sdk\") */\n keyPrefix?: string;\n /** Logger instance for error reporting */\n logger: Logger;\n}\n\n/**\n * Redis state adapter using ioredis for production use.\n *\n * Provides persistent subscriptions and distributed locking\n * across multiple server instances.\n *\n * @example\n * ```typescript\n * // With URL\n * const state = createIoRedisState({ url: process.env.REDIS_URL });\n *\n * // With existing client\n * const client = new Redis(process.env.REDIS_URL);\n * const state = createIoRedisState({ client });\n * ```\n */\nexport class IoRedisStateAdapter implements StateAdapter {\n private readonly client: Redis;\n private readonly keyPrefix: string;\n private readonly logger: Logger;\n private connected = false;\n private connectPromise: Promise<void> | null = null;\n private readonly ownsClient: boolean;\n\n constructor(options: IoRedisStateAdapterOptions | IoRedisStateClientOptions) {\n if (\"client\" in options) {\n this.client = options.client;\n this.ownsClient = false;\n } else {\n this.client = new Redis(options.url);\n this.ownsClient = true;\n }\n this.keyPrefix = options.keyPrefix || \"chat-sdk\";\n this.logger = options.logger;\n\n // Handle connection errors\n this.client.on(\"error\", (err) => {\n this.logger.error(\"ioredis client error\", { error: err });\n });\n }\n\n private key(type: \"sub\" | \"lock\" | \"cache\", id: string): string {\n return `${this.keyPrefix}:${type}:${id}`;\n }\n\n private subscriptionsSetKey(): string {\n return `${this.keyPrefix}:subscriptions`;\n }\n\n async connect(): Promise<void> {\n // ioredis auto-connects, but we track state for consistency\n if (this.connected) {\n return;\n }\n\n // Reuse existing connection attempt to avoid race conditions\n if (!this.connectPromise) {\n this.connectPromise = new Promise<void>((resolve, reject) => {\n if (this.client.status === \"ready\") {\n this.connected = true;\n resolve();\n return;\n }\n\n this.client.once(\"ready\", () => {\n this.connected = true;\n resolve();\n });\n\n this.client.once(\"error\", (err) => {\n reject(err);\n });\n });\n }\n\n await this.connectPromise;\n }\n\n async disconnect(): Promise<void> {\n if (this.connected && this.ownsClient) {\n await this.client.quit();\n this.connected = false;\n this.connectPromise = null;\n }\n }\n\n async subscribe(threadId: string): Promise<void> {\n this.ensureConnected();\n await this.client.sadd(this.subscriptionsSetKey(), threadId);\n }\n\n async unsubscribe(threadId: string): Promise<void> {\n this.ensureConnected();\n await this.client.srem(this.subscriptionsSetKey(), threadId);\n }\n\n async isSubscribed(threadId: string): Promise<boolean> {\n this.ensureConnected();\n const result = await this.client.sismember(\n this.subscriptionsSetKey(),\n threadId\n );\n return result === 1;\n }\n\n async acquireLock(threadId: string, ttlMs: number): Promise<Lock | null> {\n this.ensureConnected();\n\n const token = generateToken();\n const lockKey = this.key(\"lock\", threadId);\n\n // Use SET NX PX for atomic lock acquisition\n const acquired = await this.client.set(lockKey, token, \"PX\", ttlMs, \"NX\");\n\n if (acquired === \"OK\") {\n return {\n threadId,\n token,\n expiresAt: Date.now() + ttlMs,\n };\n }\n\n return null;\n }\n\n async releaseLock(lock: Lock): Promise<void> {\n this.ensureConnected();\n\n const lockKey = this.key(\"lock\", lock.threadId);\n\n // Use Lua script for atomic check-and-delete\n const script = `\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"del\", KEYS[1])\n else\n return 0\n end\n `;\n\n await this.client.eval(script, 1, lockKey, lock.token);\n }\n\n async extendLock(lock: Lock, ttlMs: number): Promise<boolean> {\n this.ensureConnected();\n\n const lockKey = this.key(\"lock\", lock.threadId);\n\n // Use Lua script for atomic check-and-extend\n const script = `\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"pexpire\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n\n const result = await this.client.eval(\n script,\n 1,\n lockKey,\n lock.token,\n ttlMs.toString()\n );\n\n return result === 1;\n }\n\n async get<T = unknown>(key: string): Promise<T | null> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n const value = await this.client.get(cacheKey);\n\n if (value === null) {\n return null;\n }\n\n try {\n return JSON.parse(value) as T;\n } catch {\n // If parsing fails, return as string\n return value as unknown as T;\n }\n }\n\n async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n const serialized = JSON.stringify(value);\n\n if (ttlMs) {\n await this.client.set(cacheKey, serialized, \"PX\", ttlMs);\n } else {\n await this.client.set(cacheKey, serialized);\n }\n }\n\n async delete(key: string): Promise<void> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n await this.client.del(cacheKey);\n }\n\n private ensureConnected(): void {\n if (!this.connected) {\n throw new Error(\n \"IoRedisStateAdapter is not connected. Call connect() first.\"\n );\n }\n }\n\n /**\n * Get the underlying ioredis client for advanced usage.\n */\n getClient(): Redis {\n return this.client;\n }\n}\n\nfunction generateToken(): string {\n return `ioredis_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n}\n\n/**\n * Create an ioredis state adapter.\n *\n * @example\n * ```typescript\n * // With URL\n * const state = createIoRedisState({ url: process.env.REDIS_URL });\n *\n * // With existing client\n * import Redis from \"ioredis\";\n * const client = new Redis(process.env.REDIS_URL);\n * const state = createIoRedisState({ client });\n * ```\n */\nexport function createIoRedisState(\n options: IoRedisStateAdapterOptions | IoRedisStateClientOptions\n): IoRedisStateAdapter {\n return new IoRedisStateAdapter(options);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chat-adapter/state-ioredis",
3
- "version": "4.13.1",
3
+ "version": "4.13.3",
4
4
  "description": "ioredis state adapter for chat (production)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,16 +17,16 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "ioredis": "^5.4.1",
20
- "chat": "4.13.1"
20
+ "chat": "4.13.3"
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "git+https://github.com/vercel-labs/chat.git",
24
+ "url": "git+https://github.com/vercel/chat.git",
25
25
  "directory": "packages/state-ioredis"
26
26
  },
27
- "homepage": "https://github.com/vercel-labs/chat#readme",
27
+ "homepage": "https://github.com/vercel/chat#readme",
28
28
  "bugs": {
29
- "url": "https://github.com/vercel-labs/chat/issues"
29
+ "url": "https://github.com/vercel/chat/issues"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"
@@ -48,10 +48,9 @@
48
48
  "scripts": {
49
49
  "build": "tsup",
50
50
  "dev": "tsup --watch",
51
- "test": "vitest run",
51
+ "test": "vitest run --coverage",
52
52
  "test:watch": "vitest",
53
53
  "typecheck": "tsc --noEmit",
54
- "lint": "biome check src",
55
54
  "clean": "rm -rf dist"
56
55
  }
57
56
  }