@chat-adapter/state-ioredis 4.20.2 → 4.22.0

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Logger, StateAdapter, Lock } from 'chat';
1
+ import { Logger, StateAdapter, Lock, QueueEntry } from 'chat';
2
2
  import Redis from 'ioredis';
3
3
 
4
4
  interface IoRedisStateAdapterOptions {
@@ -60,6 +60,9 @@ declare class IoRedisStateAdapter implements StateAdapter {
60
60
  maxLength?: number;
61
61
  ttlMs?: number;
62
62
  }): Promise<void>;
63
+ enqueue(threadId: string, entry: QueueEntry, maxSize: number): Promise<number>;
64
+ dequeue(threadId: string): Promise<QueueEntry | null>;
65
+ queueDepth(threadId: string): Promise<number>;
63
66
  getList<T = unknown>(key: string): Promise<T[]>;
64
67
  private ensureConnected;
65
68
  /**
package/dist/index.js CHANGED
@@ -182,6 +182,43 @@ var IoRedisStateAdapter = class {
182
182
  ttlMs.toString()
183
183
  );
184
184
  }
185
+ async enqueue(threadId, entry, maxSize) {
186
+ this.ensureConnected();
187
+ const queueKey = this.key("queue", threadId);
188
+ const serialized = JSON.stringify(entry);
189
+ const ttlMs = Math.max(entry.expiresAt - Date.now(), 6e4);
190
+ const script = `
191
+ redis.call("rpush", KEYS[1], ARGV[1])
192
+ if tonumber(ARGV[2]) > 0 then
193
+ redis.call("ltrim", KEYS[1], -tonumber(ARGV[2]), -1)
194
+ end
195
+ redis.call("pexpire", KEYS[1], ARGV[3])
196
+ return redis.call("llen", KEYS[1])
197
+ `;
198
+ const result = await this.client.eval(
199
+ script,
200
+ 1,
201
+ queueKey,
202
+ serialized,
203
+ maxSize.toString(),
204
+ ttlMs.toString()
205
+ );
206
+ return result;
207
+ }
208
+ async dequeue(threadId) {
209
+ this.ensureConnected();
210
+ const queueKey = this.key("queue", threadId);
211
+ const value = await this.client.lpop(queueKey);
212
+ if (value === null) {
213
+ return null;
214
+ }
215
+ return JSON.parse(value);
216
+ }
217
+ async queueDepth(threadId) {
218
+ this.ensureConnected();
219
+ const queueKey = this.key("queue", threadId);
220
+ return await this.client.llen(queueKey);
221
+ }
185
222
  async getList(key) {
186
223
  this.ensureConnected();
187
224
  const listKey = `${this.keyPrefix}:list:${key}`;
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 /** 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 forceReleaseLock(threadId: string): Promise<void> {\n this.ensureConnected();\n const lockKey = this.key(\"lock\", threadId);\n await this.client.del(lockKey);\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 setIfNotExists(\n key: string,\n value: unknown,\n ttlMs?: number\n ): Promise<boolean> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n const serialized = JSON.stringify(value);\n\n const result = ttlMs\n ? await this.client.set(cacheKey, serialized, \"PX\", ttlMs, \"NX\")\n : await this.client.set(cacheKey, serialized, \"NX\");\n\n return result === \"OK\";\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 async appendToList(\n key: string,\n value: unknown,\n options?: { maxLength?: number; ttlMs?: number }\n ): Promise<void> {\n this.ensureConnected();\n\n const listKey = `${this.keyPrefix}:list:${key}`;\n const serialized = JSON.stringify(value);\n const maxLength = options?.maxLength ?? 0;\n const ttlMs = options?.ttlMs ?? 0;\n\n // Atomic RPUSH + LTRIM + PEXPIRE via Lua\n const script = `\n redis.call(\"rpush\", KEYS[1], ARGV[1])\n if tonumber(ARGV[2]) > 0 then\n redis.call(\"ltrim\", KEYS[1], -tonumber(ARGV[2]), -1)\n end\n if tonumber(ARGV[3]) > 0 then\n redis.call(\"pexpire\", KEYS[1], tonumber(ARGV[3]))\n end\n return 1\n `;\n\n await this.client.eval(\n script,\n 1,\n listKey,\n serialized,\n maxLength.toString(),\n ttlMs.toString()\n );\n }\n\n async getList<T = unknown>(key: string): Promise<T[]> {\n this.ensureConnected();\n\n const listKey = `${this.keyPrefix}:list:${key}`;\n const values = await this.client.lrange(listKey, 0, -1);\n\n return values.map((v) => JSON.parse(v) as T);\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, QueueEntry, 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\" | \"queue\", 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 forceReleaseLock(threadId: string): Promise<void> {\n this.ensureConnected();\n const lockKey = this.key(\"lock\", threadId);\n await this.client.del(lockKey);\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 setIfNotExists(\n key: string,\n value: unknown,\n ttlMs?: number\n ): Promise<boolean> {\n this.ensureConnected();\n\n const cacheKey = this.key(\"cache\", key);\n const serialized = JSON.stringify(value);\n\n const result = ttlMs\n ? await this.client.set(cacheKey, serialized, \"PX\", ttlMs, \"NX\")\n : await this.client.set(cacheKey, serialized, \"NX\");\n\n return result === \"OK\";\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 async appendToList(\n key: string,\n value: unknown,\n options?: { maxLength?: number; ttlMs?: number }\n ): Promise<void> {\n this.ensureConnected();\n\n const listKey = `${this.keyPrefix}:list:${key}`;\n const serialized = JSON.stringify(value);\n const maxLength = options?.maxLength ?? 0;\n const ttlMs = options?.ttlMs ?? 0;\n\n // Atomic RPUSH + LTRIM + PEXPIRE via Lua\n const script = `\n redis.call(\"rpush\", KEYS[1], ARGV[1])\n if tonumber(ARGV[2]) > 0 then\n redis.call(\"ltrim\", KEYS[1], -tonumber(ARGV[2]), -1)\n end\n if tonumber(ARGV[3]) > 0 then\n redis.call(\"pexpire\", KEYS[1], tonumber(ARGV[3]))\n end\n return 1\n `;\n\n await this.client.eval(\n script,\n 1,\n listKey,\n serialized,\n maxLength.toString(),\n ttlMs.toString()\n );\n }\n\n async enqueue(\n threadId: string,\n entry: QueueEntry,\n maxSize: number\n ): Promise<number> {\n this.ensureConnected();\n\n const queueKey = this.key(\"queue\", threadId);\n const serialized = JSON.stringify(entry);\n\n const ttlMs = Math.max(entry.expiresAt - Date.now(), 60_000);\n\n // Atomic RPUSH + LTRIM + PEXPIRE via Lua\n const script = `\n redis.call(\"rpush\", KEYS[1], ARGV[1])\n if tonumber(ARGV[2]) > 0 then\n redis.call(\"ltrim\", KEYS[1], -tonumber(ARGV[2]), -1)\n end\n redis.call(\"pexpire\", KEYS[1], ARGV[3])\n return redis.call(\"llen\", KEYS[1])\n `;\n\n const result = await this.client.eval(\n script,\n 1,\n queueKey,\n serialized,\n maxSize.toString(),\n ttlMs.toString()\n );\n\n return result as number;\n }\n\n async dequeue(threadId: string): Promise<QueueEntry | null> {\n this.ensureConnected();\n\n const queueKey = this.key(\"queue\", threadId);\n const value = await this.client.lpop(queueKey);\n\n if (value === null) {\n return null;\n }\n\n return JSON.parse(value) as QueueEntry;\n }\n\n async queueDepth(threadId: string): Promise<number> {\n this.ensureConnected();\n\n const queueKey = this.key(\"queue\", threadId);\n return await this.client.llen(queueKey);\n }\n\n async getList<T = unknown>(key: string): Promise<T[]> {\n this.ensureConnected();\n\n const listKey = `${this.keyPrefix}:list:${key}`;\n const values = await this.client.lrange(listKey, 0, -1);\n\n return values.map((v) => JSON.parse(v) as T);\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.20.2",
3
+ "version": "4.22.0",
4
4
  "description": "ioredis state adapter for chat (production)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "ioredis": "^5.4.1",
20
- "chat": "4.20.2"
20
+ "chat": "4.22.0"
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",