@langchain/langgraph-checkpoint-redis 1.0.3 → 1.0.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.cjs","names":["BaseCheckpointSaver","WRITE_KEYS_ZSET_PREFIX","escapeRediSearchTagValue"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\nimport { WRITE_KEYS_ZSET_PREFIX } from \"./constants.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Check if writes already exist for this checkpoint (handles putWrites-before-put ordering)\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writesExist = await this.client.exists(zsetKey);\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: writesExist ? \"true\" : \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shallow.cjs","names":["BaseCheckpointSaver","WRITE_KEYS_ZSET_PREFIX","escapeRediSearchTagValue"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\nimport { WRITE_KEYS_ZSET_PREFIX } from \"./constants.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Check if writes already exist for this checkpoint (handles putWrites-before-put ordering)\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writesExist = await this.client.exists(zsetKey);\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: writesExist ? \"true\" : \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
package/dist/shallow.js CHANGED
@@ -2,7 +2,6 @@ import { escapeRediSearchTagValue } from "./utils.js";
2
2
  import { WRITE_KEYS_ZSET_PREFIX } from "./constants.js";
3
3
  import { BaseCheckpointSaver, uuid6 } from "@langchain/langgraph-checkpoint";
4
4
  import { createClient } from "redis";
5
-
6
5
  //#region src/shallow.ts
7
6
  function deterministicStringify(obj) {
8
7
  if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
@@ -402,7 +401,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
402
401
  }
403
402
  }
404
403
  };
405
-
406
404
  //#endregion
407
405
  export { ShallowRedisSaver };
406
+
408
407
  //# sourceMappingURL=shallow.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.js","names":[],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\nimport { WRITE_KEYS_ZSET_PREFIX } from \"./constants.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Check if writes already exist for this checkpoint (handles putWrites-before-put ordering)\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writesExist = await this.client.exists(zsetKey);\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: writesExist ? \"true\" : \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shallow.js","names":[],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\nimport { WRITE_KEYS_ZSET_PREFIX } from \"./constants.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n _newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Check if writes already exist for this checkpoint (handles putWrites-before-put ordering)\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writesExist = await this.client.exists(zsetKey);\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(metadata),\n checkpoint_ts: Date.now(),\n has_writes: writesExist ? \"true\" : \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n"],"mappings":"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"}
package/dist/store.cjs CHANGED
@@ -1,9 +1,8 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_utils = require('./utils.cjs');
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_utils = require("./utils.cjs");
3
3
  let _langchain_langgraph_checkpoint = require("@langchain/langgraph-checkpoint");
4
4
  let redis = require("redis");
5
5
  let uuid = require("uuid");
6
-
7
6
  //#region src/store.ts
8
7
  function isPutOperation(op) {
9
8
  return "value" in op && "namespace" in op && "key" in op;
@@ -606,7 +605,6 @@ var RedisStore = class RedisStore {
606
605
  }
607
606
  }
608
607
  };
609
-
610
608
  //#endregion
611
609
  exports.FilterBuilder = FilterBuilder;
612
610
  exports.RedisStore = RedisStore;
@@ -614,4 +612,5 @@ exports.isGetOperation = isGetOperation;
614
612
  exports.isListNamespacesOperation = isListNamespacesOperation;
615
613
  exports.isPutOperation = isPutOperation;
616
614
  exports.isSearchOperation = isSearchOperation;
615
+
617
616
  //# sourceMappingURL=store.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.cjs","names":["InvalidNamespaceError","escapeRediSearchTagValue"],"sources":["../src/store.ts"],"sourcesContent":["import { createClient, createCluster } from \"redis\";\n\n/** A conventional Redis connection. */\nexport type RedisClientConnection = ReturnType<typeof createClient>;\n\n/** A clustered Redis connection. */\nexport type RedisClusterConnection = ReturnType<typeof createCluster>;\n\n/** A Redis connection, clustered or conventional. */\nexport type RedisConnection = RedisClientConnection | RedisClusterConnection;\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n type GetOperation,\n InvalidNamespaceError,\n type ListNamespacesOperation,\n type Operation,\n type PutOperation,\n type SearchOperation,\n} from \"@langchain/langgraph-checkpoint\";\n\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\n// Type guard functions for operations\nexport function isPutOperation(op: Operation): op is PutOperation {\n return \"value\" in op && \"namespace\" in op && \"key\" in op;\n}\n\nexport function isGetOperation(op: Operation): op is GetOperation {\n return (\n \"namespace\" in op &&\n \"key\" in op &&\n !(\"value\" in op) &&\n !(\"namespacePrefix\" in op) &&\n !(\"matchConditions\" in op)\n );\n}\n\nexport function isSearchOperation(op: Operation): op is SearchOperation {\n return \"namespacePrefix\" in op;\n}\n\nexport function isListNamespacesOperation(\n op: Operation\n): op is ListNamespacesOperation {\n return \"matchConditions\" in op;\n}\n\n// Filter types for advanced search operations\nexport interface FilterOperators {\n $eq?: any;\n $ne?: any;\n $gt?: number;\n $gte?: number;\n $lt?: number;\n $lte?: number;\n $in?: any[];\n $nin?: any[];\n $exists?: boolean;\n}\n\nexport type FilterValue = any | FilterOperators;\nexport type Filter = Record<string, FilterValue>;\n\n/**\n * Internal class for evaluating filters against documents.\n * Supports MongoDB-style query operators.\n */\nclass FilterBuilder {\n /**\n * Evaluates if a document matches the given filter criteria.\n * Supports advanced operators like $gt, $lt, $in, etc.\n */\n static matchesFilter(doc: Record<string, any>, filter: Filter): boolean {\n for (const [key, filterValue] of Object.entries(filter)) {\n if (!this.matchesFieldFilter(doc, key, filterValue)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Builds a Redis Search query string from filter criteria.\n * Note: This is limited by RediSearch capabilities and may not support all operators.\n */\n static buildRedisSearchQuery(\n filter: Filter,\n prefix?: string\n ): { query: string; useClientFilter: boolean } {\n let queryParts: string[] = [];\n let useClientFilter = false;\n\n // Add prefix filter if provided\n if (prefix) {\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n queryParts.push(`@prefix:(${tokens.join(\" \")})`);\n }\n }\n\n // Check if we have complex operators that require client-side filtering\n for (const [_key, value] of Object.entries(filter)) {\n if (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).some((k) => k.startsWith(\"$\"))\n ) {\n // Complex operators require client-side filtering\n useClientFilter = true;\n break;\n }\n }\n\n // If no prefix, at least search all documents\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n return {\n query: queryParts.join(\" \"),\n useClientFilter,\n };\n }\n\n private static matchesFieldFilter(\n doc: Record<string, any>,\n key: string,\n filterValue: FilterValue\n ): boolean {\n // Handle nested keys (e.g., \"user.name\")\n const actualValue = this.getNestedValue(doc, key);\n\n // Check if it's an operator object\n if (\n typeof filterValue === \"object\" &&\n filterValue !== null &&\n !Array.isArray(filterValue) &&\n Object.keys(filterValue).some((k) => k.startsWith(\"$\"))\n ) {\n // Handle operator object\n return this.matchesOperators(actualValue, filterValue as FilterOperators);\n } else {\n // Simple equality check\n return this.isEqual(actualValue, filterValue);\n }\n }\n\n private static matchesOperators(\n actualValue: any,\n operators: FilterOperators\n ): boolean {\n for (const [operator, operatorValue] of Object.entries(operators)) {\n if (!this.matchesOperator(actualValue, operator, operatorValue)) {\n return false;\n }\n }\n return true;\n }\n\n private static matchesOperator(\n actualValue: any,\n operator: string,\n operatorValue: any\n ): boolean {\n switch (operator) {\n case \"$eq\":\n return this.isEqual(actualValue, operatorValue);\n\n case \"$ne\":\n return !this.isEqual(actualValue, operatorValue);\n\n case \"$gt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) > Number(operatorValue)\n );\n\n case \"$gte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) >= Number(operatorValue)\n );\n\n case \"$lt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) < Number(operatorValue)\n );\n\n case \"$lte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) <= Number(operatorValue)\n );\n\n case \"$in\":\n if (!Array.isArray(operatorValue)) return false;\n return operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$nin\":\n if (!Array.isArray(operatorValue)) return false;\n return !operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$exists\": {\n const exists = actualValue !== undefined;\n return operatorValue ? exists : !exists;\n }\n\n default:\n // Unknown operator, return false for safety\n return false;\n }\n }\n\n private static isEqual(a: any, b: any): boolean {\n // Handle null and undefined\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a === undefined || b === undefined) return false;\n\n // Handle arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((val, idx) => this.isEqual(val, b[idx]));\n }\n if (Array.isArray(a) || Array.isArray(b)) {\n // Check if non-array value exists in array\n const arr = Array.isArray(a) ? a : b;\n const val = Array.isArray(a) ? b : a;\n return arr.includes(val);\n }\n\n // Handle objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => this.isEqual(a[key], b[key]));\n }\n\n // Primitive comparison (with type coercion for numbers)\n return a == b;\n }\n\n private static getNestedValue(obj: any, path: string): any {\n const keys = path.split(\".\");\n let current = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = current[key];\n }\n\n return current;\n }\n}\n\nexport interface Item {\n value: any;\n key: string;\n namespace: string[];\n created_at: Date;\n updated_at: Date;\n}\n\nexport interface SearchItem extends Item {\n score?: number;\n}\n\ninterface StoreDocument {\n key: string;\n prefix: string;\n value: any;\n created_at: number;\n updated_at: number;\n}\n\ninterface VectorDocument {\n prefix: string;\n key: string;\n field_name: string;\n embedding: number[];\n created_at: number;\n updated_at: number;\n}\n\nexport interface IndexConfig {\n dims: number;\n embed?: any;\n distanceType?: \"cosine\" | \"l2\" | \"ip\"; // cosine, L2 (Euclidean), inner product\n fields?: string[];\n vectorStorageType?: string;\n similarityThreshold?: number; // Minimum similarity score for results\n}\n\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n\nexport interface StoreConfig {\n index?: IndexConfig;\n ttl?: TTLConfig;\n}\n\nconst REDIS_KEY_SEPARATOR = \":\";\nconst STORE_PREFIX = \"store\";\nconst STORE_VECTOR_PREFIX = \"store_vectors\";\n\nconst SCHEMAS = [\n {\n index: \"store\",\n prefix: STORE_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n {\n index: \"store_vectors\",\n prefix: STORE_VECTOR_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.embedding\": { type: \"VECTOR\", AS: \"embedding\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n];\n\nexport class RedisStore {\n private readonly client: RedisConnection;\n private readonly indexConfig?: IndexConfig;\n private readonly ttlConfig?: TTLConfig;\n private readonly embeddings?: any;\n\n constructor(client: RedisConnection, config?: StoreConfig) {\n this.client = client;\n this.indexConfig = config?.index;\n this.ttlConfig = config?.ttl;\n\n if (this.indexConfig?.embed) {\n this.embeddings = this.indexConfig.embed;\n }\n }\n\n static async fromConnString(\n connString: string,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createClient({ url: connString });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n async setup(): Promise<void> {\n // Create store index\n try {\n await this.client.ft.create(SCHEMAS[0].index, SCHEMAS[0].schema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[0].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create store index:\", error.message);\n }\n }\n\n // Create vector index if configured\n if (this.indexConfig) {\n const dims = this.indexConfig.dims;\n const distanceMetric =\n this.indexConfig.distanceType === \"cosine\"\n ? \"COSINE\"\n : this.indexConfig.distanceType === \"l2\"\n ? \"L2\"\n : this.indexConfig.distanceType === \"ip\"\n ? \"IP\"\n : \"COSINE\";\n\n // Build schema with correct vector syntax\n const vectorSchema: Record<string, any> = {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n };\n\n // Add vector field with correct syntax\n vectorSchema[\"$.embedding\"] = {\n type: \"VECTOR\",\n ALGORITHM: \"FLAT\",\n TYPE: \"FLOAT32\",\n DIM: dims,\n DISTANCE_METRIC: distanceMetric,\n AS: \"embedding\",\n };\n\n try {\n await this.client.ft.create(SCHEMAS[1].index, vectorSchema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[1].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create vector index:\", error.message);\n }\n }\n }\n }\n\n async get(\n namespace: string[],\n key: string,\n options?: { refreshTTL?: boolean }\n ): Promise<Item | null> {\n const prefix = namespace.join(\".\");\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n // Handle empty string as a special case\n let query: string;\n if (key === \"\") {\n // For empty keys, search by prefix and filter results\n query = prefixQuery;\n } else {\n const escapedKey = this.escapeTagValue(key);\n query = `(${prefixQuery}) (@key:{${escapedKey}})`;\n }\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: key === \"\" ? 100 : 1 },\n });\n\n if (!results || !results.documents || results.documents.length === 0) {\n return null;\n }\n\n // For empty key, filter to find the exact match\n if (key === \"\") {\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n if (jsonDoc.key === \"\" && jsonDoc.prefix === prefix) {\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n }\n }\n return null;\n }\n\n const doc = results.documents[0];\n const jsonDoc = doc.value as unknown as StoreDocument;\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return null;\n }\n throw error;\n }\n }\n\n async put(\n namespace: string[],\n key: string,\n value: any,\n options?: { ttl?: number; index?: boolean | string[] }\n ): Promise<void> {\n // Validate namespace for put operations\n this.validateNamespace(namespace);\n const prefix = namespace.join(\".\");\n const docId = uuidv4();\n // Use high-resolution time for better timestamp precision\n const now = Date.now() * 1000000 + Math.floor(performance.now() * 1000); // Microseconds + nanoseconds component\n let createdAt = now; // Will be overridden if document exists\n\n // Delete existing document if it exists\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n const escapedKey = this.escapeTagValue(key);\n const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;\n try {\n const existing = await this.client.ft.search(\"store\", existingQuery, {\n LIMIT: { from: 0, size: 1 },\n });\n\n if (existing && existing.documents && existing.documents.length > 0) {\n const oldDocId = existing.documents[0].id;\n // Preserve the original created_at timestamp\n const existingDoc = await this.client.json.get(oldDocId);\n if (\n existingDoc &&\n typeof existingDoc === \"object\" &&\n \"created_at\" in existingDoc\n ) {\n createdAt = (existingDoc as any).created_at;\n }\n await this.client.del(oldDocId);\n\n // Also delete associated vector if it exists\n if (this.indexConfig) {\n const oldUuid = oldDocId.split(\":\").pop();\n const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;\n try {\n await this.client.del(oldVectorKey);\n } catch (error) {\n // Vector might not exist\n }\n }\n }\n } catch (error) {\n // Index might not exist yet\n }\n\n // Handle delete operation\n if (value === null) {\n return;\n }\n\n // Store the document\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const doc = {\n prefix,\n key,\n value,\n created_at: createdAt,\n updated_at: now,\n };\n\n await this.client.json.set(storeKey, \"$\", doc);\n\n // Handle embeddings if configured\n if (this.indexConfig && this.embeddings && options?.index !== false) {\n const fieldsToIndex =\n options && Array.isArray(options.index)\n ? options.index\n : this.indexConfig.fields || [\"text\"];\n const textsToEmbed = [];\n const fieldNames = [];\n\n for (const field of fieldsToIndex) {\n if (value[field]) {\n textsToEmbed.push(value[field]);\n fieldNames.push(field);\n }\n }\n\n if (textsToEmbed.length > 0) {\n const embeddings = await this.embeddings.embedDocuments(textsToEmbed);\n\n for (let i = 0; i < embeddings.length; i++) {\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const vectorDoc: VectorDocument = {\n prefix,\n key,\n field_name: fieldNames[i],\n embedding: embeddings[i],\n created_at: now,\n updated_at: now,\n };\n\n await this.client.json.set(vectorKey, \"$\", vectorDoc as any);\n\n // Apply TTL to vector key if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(vectorKey, ttlSeconds);\n }\n }\n }\n }\n\n // Apply TTL if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(storeKey, ttlSeconds);\n }\n }\n\n async delete(namespace: string[], key: string): Promise<void> {\n await this.put(namespace, key, null);\n }\n\n async search(\n namespacePrefix: string[],\n options?: {\n filter?: Filter;\n query?: string;\n limit?: number;\n offset?: number;\n refreshTTL?: boolean;\n similarityThreshold?: number;\n }\n ): Promise<SearchItem[]> {\n const prefix = namespacePrefix.join(\".\");\n const limit = options?.limit || 10;\n const offset = options?.offset || 0;\n\n // Handle vector search if query is provided\n if (options?.query && this.indexConfig && this.embeddings) {\n const [embedding] = await this.embeddings.embedDocuments([options.query]);\n\n // Build KNN query\n // For prefix search, use wildcard since we want to match any document starting with this prefix\n let queryStr = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : \"*\";\n const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);\n\n try {\n // Use KNN query with proper syntax\n const results = await this.client.ft.search(\n \"store_vectors\",\n `(${queryStr})=>[KNN ${limit} @embedding $BLOB]`,\n {\n PARAMS: {\n BLOB: vectorBytes,\n },\n DIALECT: 2,\n LIMIT: { from: offset, size: limit },\n RETURN: [\"prefix\", \"key\", \"__embedding_score\"],\n }\n );\n\n // Get matching store documents\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const docUuid = doc.id.split(\":\").pop();\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n\n const storeDoc = (await this.client.json.get(\n storeKey\n )) as StoreDocument | null;\n if (storeDoc) {\n // Apply advanced filter if provided\n if (options.filter) {\n if (\n !FilterBuilder.matchesFilter(\n storeDoc.value || {},\n options.filter\n )\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options.refreshTTL) {\n await this.refreshItemTTL(storeKey);\n await this.refreshItemTTL(doc.id);\n }\n\n const score = (doc.value as any)?.__embedding_score\n ? this.calculateSimilarityScore(\n parseFloat((doc.value as any).__embedding_score as string)\n )\n : 0;\n\n // Apply similarity threshold if specified\n const threshold =\n options.similarityThreshold ??\n this.indexConfig?.similarityThreshold;\n if (threshold !== undefined && score < threshold) {\n continue;\n }\n\n items.push({\n value: storeDoc.value,\n key: storeDoc.key,\n namespace: storeDoc.prefix.split(\".\"),\n created_at: new Date(storeDoc.created_at / 1000000),\n updated_at: new Date(storeDoc.updated_at / 1000000),\n score,\n });\n }\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n // Regular search without vectors\n let queryStr = \"*\";\n if (prefix) {\n // For prefix search, we need to match all tokens from the namespace prefix\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n // Match all tokens to ensure we get the right prefix\n queryStr = `@prefix:(${tokens.join(\" \")})`;\n }\n }\n\n try {\n const results = await this.client.ft.search(\"store\", queryStr, {\n LIMIT: { from: offset, size: limit },\n SORTBY: { BY: \"created_at\", DIRECTION: \"DESC\" },\n });\n\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n\n // Apply advanced filter\n if (options?.filter) {\n if (\n !FilterBuilder.matchesFilter(jsonDoc.value || {}, options.filter)\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(doc.id);\n }\n\n items.push({\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n });\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async listNamespaces(options?: {\n prefix?: string[];\n suffix?: string[];\n maxDepth?: number;\n limit?: number;\n offset?: number;\n }): Promise<string[][]> {\n let query = \"*\";\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: 1000 }, // Get many to deduplicate\n RETURN: [\"prefix\"],\n });\n\n // Extract unique namespaces and filter\n const namespaceSet = new Set<string>();\n for (const doc of results.documents) {\n const prefix = (doc.value as unknown as StoreDocument).prefix;\n const parts = prefix.split(\".\");\n\n // Apply prefix filter if specified\n if (options?.prefix) {\n // Check if this namespace starts with the specified prefix\n if (parts.length < options.prefix.length) continue;\n\n let matches = true;\n for (let i = 0; i < options.prefix.length; i++) {\n if (parts[i] !== options.prefix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply suffix filter if specified\n if (options?.suffix) {\n // Check if this namespace ends with the specified suffix\n if (parts.length < options.suffix.length) continue;\n\n let matches = true;\n const startIdx = parts.length - options.suffix.length;\n for (let i = 0; i < options.suffix.length; i++) {\n if (parts[startIdx + i] !== options.suffix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply max depth\n if (options?.maxDepth) {\n const truncated = parts.slice(0, options.maxDepth);\n namespaceSet.add(truncated.join(\".\"));\n } else {\n namespaceSet.add(prefix);\n }\n }\n\n // Convert to array of arrays and sort\n let namespaces = Array.from(namespaceSet)\n .map((ns) => ns.split(\".\"))\n .sort((a, b) => a.join(\".\").localeCompare(b.join(\".\")));\n\n // Apply pagination\n if (options?.offset || options?.limit) {\n const offset = options.offset || 0;\n const limit = options.limit || 10;\n namespaces = namespaces.slice(offset, offset + limit);\n }\n\n return namespaces;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async batch(ops: Operation[]): Promise<any[]> {\n const results: any[] = new Array(ops.length).fill(null);\n\n // Process operations in order to maintain dependencies\n for (let idx = 0; idx < ops.length; idx++) {\n const op = ops[idx];\n\n // Execute operation based on type guards\n if (isPutOperation(op)) {\n // TypeScript now knows op is PutOperation\n await this.put(op.namespace, op.key, op.value);\n results[idx] = null;\n } else if (isSearchOperation(op)) {\n // TypeScript now knows op is SearchOperation\n results[idx] = await this.search(op.namespacePrefix, {\n filter: op.filter,\n query: op.query,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isListNamespacesOperation(op)) {\n // TypeScript now knows op is ListNamespacesOperation\n let prefix: string[] | undefined = undefined;\n let suffix: string[] | undefined = undefined;\n\n if (op.matchConditions) {\n for (const condition of op.matchConditions) {\n if (condition.matchType === \"prefix\") {\n prefix = condition.path;\n } else if (condition.matchType === \"suffix\") {\n suffix = condition.path;\n }\n }\n }\n\n results[idx] = await this.listNamespaces({\n prefix,\n suffix,\n maxDepth: op.maxDepth,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isGetOperation(op)) {\n // TypeScript now knows op is GetOperation\n results[idx] = await this.get(op.namespace, op.key);\n } else {\n // This should never happen with proper Operation type\n throw new Error(`Unknown operation type: ${JSON.stringify(op)}`);\n }\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Get statistics about the store.\n * Returns document counts and other metrics.\n */\n async getStatistics(): Promise<{\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n }> {\n const stats: {\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n } = {\n totalDocuments: 0,\n namespaceCount: 0,\n };\n\n try {\n // Get total document count\n const countResult = await this.client.ft.search(\"store\", \"*\", {\n LIMIT: { from: 0, size: 0 },\n });\n stats.totalDocuments = countResult.total || 0;\n\n // Get unique namespace count\n const namespaces = await this.listNamespaces({ limit: 1000 });\n stats.namespaceCount = namespaces.length;\n\n // Get vector document count if index is configured\n if (this.indexConfig) {\n try {\n const vectorResult = await this.client.ft.search(\n \"store_vectors\",\n \"*\",\n {\n LIMIT: { from: 0, size: 0 },\n }\n );\n stats.vectorDocuments = vectorResult.total || 0;\n } catch (error) {\n // Vector index might not exist\n stats.vectorDocuments = 0;\n }\n\n // Get index info\n try {\n stats.indexInfo = await this.client.ft.info(\"store\");\n } catch (error) {\n // Index info might not be available\n }\n }\n } catch (error: any) {\n if (!error.message?.includes(\"no such index\")) {\n throw error;\n }\n }\n\n return stats;\n }\n\n private validateNamespace(namespace: string[]): void {\n if (namespace.length === 0) {\n throw new InvalidNamespaceError(\"Namespace cannot be empty.\");\n }\n for (const label of namespace) {\n // Runtime check for JavaScript users (TypeScript already ensures this)\n // This check is for runtime safety when called from JavaScript\n // noinspection SuspiciousTypeOfGuard\n if (typeof label !== \"string\") {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${String(\n label\n )}' found in ${namespace}. Namespace labels must be strings.`\n );\n }\n if (label.includes(\".\")) {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`\n );\n }\n if (label === \"\") {\n throw new InvalidNamespaceError(\n `Namespace labels cannot be empty strings. Got ${label} in ${namespace}`\n );\n }\n }\n if (namespace[0] === \"langgraph\") {\n throw new InvalidNamespaceError(\n `Root label for namespace cannot be \"langgraph\". Got: ${namespace}`\n );\n }\n }\n\n private async refreshItemTTL(docId: string): Promise<void> {\n if (this.ttlConfig?.defaultTTL) {\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n await this.client.expire(docId, ttlSeconds);\n\n // Also refresh vector key if it exists\n const docUuid = docId.split(\":\").pop();\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n try {\n await this.client.expire(vectorKey, ttlSeconds);\n } catch (error) {\n // Vector key might not exist\n }\n }\n }\n\n private escapeTagValue(value: string): string {\n // Delegate to shared utility for RediSearch TAG field escaping\n return escapeRediSearchTagValue(value);\n }\n\n /**\n * Calculate similarity score based on the distance metric.\n * Converts raw distance to a normalized similarity score [0,1].\n */\n private calculateSimilarityScore(distance: number): number {\n const metric = this.indexConfig?.distanceType || \"cosine\";\n\n switch (metric) {\n case \"cosine\":\n // Cosine distance is in range [0,2], convert to similarity [0,1]\n return Math.max(0, 1 - distance / 2);\n\n case \"l2\":\n // L2 (Euclidean) distance, use exponential decay\n // Similarity = e^(-distance)\n return Math.exp(-distance);\n\n case \"ip\":\n // Inner product can be negative, use sigmoid function\n // Similarity = 1 / (1 + e^(-distance))\n return 1 / (1 + Math.exp(-distance));\n\n default:\n // Default to cosine similarity\n return Math.max(0, 1 - distance / 2);\n }\n }\n}\n\n// Export FilterBuilder for testing purposes\nexport { FilterBuilder };\n"],"mappings":"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"}
1
+ {"version":3,"file":"store.cjs","names":["InvalidNamespaceError","escapeRediSearchTagValue"],"sources":["../src/store.ts"],"sourcesContent":["import { createClient, createCluster } from \"redis\";\n\n/** A conventional Redis connection. */\nexport type RedisClientConnection = ReturnType<typeof createClient>;\n\n/** A clustered Redis connection. */\nexport type RedisClusterConnection = ReturnType<typeof createCluster>;\n\n/** A Redis connection, clustered or conventional. */\nexport type RedisConnection = RedisClientConnection | RedisClusterConnection;\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n type GetOperation,\n InvalidNamespaceError,\n type ListNamespacesOperation,\n type Operation,\n type PutOperation,\n type SearchOperation,\n} from \"@langchain/langgraph-checkpoint\";\n\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\n// Type guard functions for operations\nexport function isPutOperation(op: Operation): op is PutOperation {\n return \"value\" in op && \"namespace\" in op && \"key\" in op;\n}\n\nexport function isGetOperation(op: Operation): op is GetOperation {\n return (\n \"namespace\" in op &&\n \"key\" in op &&\n !(\"value\" in op) &&\n !(\"namespacePrefix\" in op) &&\n !(\"matchConditions\" in op)\n );\n}\n\nexport function isSearchOperation(op: Operation): op is SearchOperation {\n return \"namespacePrefix\" in op;\n}\n\nexport function isListNamespacesOperation(\n op: Operation\n): op is ListNamespacesOperation {\n return \"matchConditions\" in op;\n}\n\n// Filter types for advanced search operations\nexport interface FilterOperators {\n $eq?: any;\n $ne?: any;\n $gt?: number;\n $gte?: number;\n $lt?: number;\n $lte?: number;\n $in?: any[];\n $nin?: any[];\n $exists?: boolean;\n}\n\nexport type FilterValue = any | FilterOperators;\nexport type Filter = Record<string, FilterValue>;\n\n/**\n * Internal class for evaluating filters against documents.\n * Supports MongoDB-style query operators.\n */\nclass FilterBuilder {\n /**\n * Evaluates if a document matches the given filter criteria.\n * Supports advanced operators like $gt, $lt, $in, etc.\n */\n static matchesFilter(doc: Record<string, any>, filter: Filter): boolean {\n for (const [key, filterValue] of Object.entries(filter)) {\n if (!this.matchesFieldFilter(doc, key, filterValue)) {\n return false;\n }\n }\n return true;\n }\n\n /**\n * Builds a Redis Search query string from filter criteria.\n * Note: This is limited by RediSearch capabilities and may not support all operators.\n */\n static buildRedisSearchQuery(\n filter: Filter,\n prefix?: string\n ): { query: string; useClientFilter: boolean } {\n let queryParts: string[] = [];\n let useClientFilter = false;\n\n // Add prefix filter if provided\n if (prefix) {\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n queryParts.push(`@prefix:(${tokens.join(\" \")})`);\n }\n }\n\n // Check if we have complex operators that require client-side filtering\n for (const [_key, value] of Object.entries(filter)) {\n if (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).some((k) => k.startsWith(\"$\"))\n ) {\n // Complex operators require client-side filtering\n useClientFilter = true;\n break;\n }\n }\n\n // If no prefix, at least search all documents\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n return {\n query: queryParts.join(\" \"),\n useClientFilter,\n };\n }\n\n private static matchesFieldFilter(\n doc: Record<string, any>,\n key: string,\n filterValue: FilterValue\n ): boolean {\n // Handle nested keys (e.g., \"user.name\")\n const actualValue = this.getNestedValue(doc, key);\n\n // Check if it's an operator object\n if (\n typeof filterValue === \"object\" &&\n filterValue !== null &&\n !Array.isArray(filterValue) &&\n Object.keys(filterValue).some((k) => k.startsWith(\"$\"))\n ) {\n // Handle operator object\n return this.matchesOperators(actualValue, filterValue as FilterOperators);\n } else {\n // Simple equality check\n return this.isEqual(actualValue, filterValue);\n }\n }\n\n private static matchesOperators(\n actualValue: any,\n operators: FilterOperators\n ): boolean {\n for (const [operator, operatorValue] of Object.entries(operators)) {\n if (!this.matchesOperator(actualValue, operator, operatorValue)) {\n return false;\n }\n }\n return true;\n }\n\n private static matchesOperator(\n actualValue: any,\n operator: string,\n operatorValue: any\n ): boolean {\n switch (operator) {\n case \"$eq\":\n return this.isEqual(actualValue, operatorValue);\n\n case \"$ne\":\n return !this.isEqual(actualValue, operatorValue);\n\n case \"$gt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) > Number(operatorValue)\n );\n\n case \"$gte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) >= Number(operatorValue)\n );\n\n case \"$lt\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) < Number(operatorValue)\n );\n\n case \"$lte\":\n return (\n actualValue !== undefined &&\n actualValue !== null &&\n Number(actualValue) <= Number(operatorValue)\n );\n\n case \"$in\":\n if (!Array.isArray(operatorValue)) return false;\n return operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$nin\":\n if (!Array.isArray(operatorValue)) return false;\n return !operatorValue.some((val) => this.isEqual(actualValue, val));\n\n case \"$exists\": {\n const exists = actualValue !== undefined;\n return operatorValue ? exists : !exists;\n }\n\n default:\n // Unknown operator, return false for safety\n return false;\n }\n }\n\n private static isEqual(a: any, b: any): boolean {\n // Handle null and undefined\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (a === undefined || b === undefined) return false;\n\n // Handle arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n return a.every((val, idx) => this.isEqual(val, b[idx]));\n }\n if (Array.isArray(a) || Array.isArray(b)) {\n // Check if non-array value exists in array\n const arr = Array.isArray(a) ? a : b;\n const val = Array.isArray(a) ? b : a;\n return arr.includes(val);\n }\n\n // Handle objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every((key) => this.isEqual(a[key], b[key]));\n }\n\n // Primitive comparison (with type coercion for numbers)\n return a == b;\n }\n\n private static getNestedValue(obj: any, path: string): any {\n const keys = path.split(\".\");\n let current = obj;\n\n for (const key of keys) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = current[key];\n }\n\n return current;\n }\n}\n\nexport interface Item {\n value: any;\n key: string;\n namespace: string[];\n created_at: Date;\n updated_at: Date;\n}\n\nexport interface SearchItem extends Item {\n score?: number;\n}\n\ninterface StoreDocument {\n key: string;\n prefix: string;\n value: any;\n created_at: number;\n updated_at: number;\n}\n\ninterface VectorDocument {\n prefix: string;\n key: string;\n field_name: string;\n embedding: number[];\n created_at: number;\n updated_at: number;\n}\n\nexport interface IndexConfig {\n dims: number;\n embed?: any;\n distanceType?: \"cosine\" | \"l2\" | \"ip\"; // cosine, L2 (Euclidean), inner product\n fields?: string[];\n vectorStorageType?: string;\n similarityThreshold?: number; // Minimum similarity score for results\n}\n\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n\nexport interface StoreConfig {\n index?: IndexConfig;\n ttl?: TTLConfig;\n}\n\nconst REDIS_KEY_SEPARATOR = \":\";\nconst STORE_PREFIX = \"store\";\nconst STORE_VECTOR_PREFIX = \"store_vectors\";\n\nconst SCHEMAS = [\n {\n index: \"store\",\n prefix: STORE_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n {\n index: \"store_vectors\",\n prefix: STORE_VECTOR_PREFIX + REDIS_KEY_SEPARATOR,\n schema: {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.embedding\": { type: \"VECTOR\", AS: \"embedding\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n },\n },\n];\n\nexport class RedisStore {\n private readonly client: RedisConnection;\n private readonly indexConfig?: IndexConfig;\n private readonly ttlConfig?: TTLConfig;\n private readonly embeddings?: any;\n\n constructor(client: RedisConnection, config?: StoreConfig) {\n this.client = client;\n this.indexConfig = config?.index;\n this.ttlConfig = config?.ttl;\n\n if (this.indexConfig?.embed) {\n this.embeddings = this.indexConfig.embed;\n }\n }\n\n static async fromConnString(\n connString: string,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createClient({ url: connString });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n config?: StoreConfig\n ): Promise<RedisStore> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const store = new RedisStore(client, config);\n await store.setup();\n return store;\n }\n\n async setup(): Promise<void> {\n // Create store index\n try {\n await this.client.ft.create(SCHEMAS[0].index, SCHEMAS[0].schema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[0].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create store index:\", error.message);\n }\n }\n\n // Create vector index if configured\n if (this.indexConfig) {\n const dims = this.indexConfig.dims;\n const distanceMetric =\n this.indexConfig.distanceType === \"cosine\"\n ? \"COSINE\"\n : this.indexConfig.distanceType === \"l2\"\n ? \"L2\"\n : this.indexConfig.distanceType === \"ip\"\n ? \"IP\"\n : \"COSINE\";\n\n // Build schema with correct vector syntax\n const vectorSchema: Record<string, any> = {\n \"$.prefix\": { type: \"TEXT\", AS: \"prefix\" },\n \"$.key\": { type: \"TAG\", AS: \"key\" },\n \"$.field_name\": { type: \"TAG\", AS: \"field_name\" },\n \"$.created_at\": { type: \"NUMERIC\", AS: \"created_at\" },\n \"$.updated_at\": { type: \"NUMERIC\", AS: \"updated_at\" },\n };\n\n // Add vector field with correct syntax\n vectorSchema[\"$.embedding\"] = {\n type: \"VECTOR\",\n ALGORITHM: \"FLAT\",\n TYPE: \"FLOAT32\",\n DIM: dims,\n DISTANCE_METRIC: distanceMetric,\n AS: \"embedding\",\n };\n\n try {\n await this.client.ft.create(SCHEMAS[1].index, vectorSchema as any, {\n ON: \"JSON\",\n PREFIX: SCHEMAS[1].prefix,\n });\n } catch (error: any) {\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\"Failed to create vector index:\", error.message);\n }\n }\n }\n }\n\n async get(\n namespace: string[],\n key: string,\n options?: { refreshTTL?: boolean }\n ): Promise<Item | null> {\n const prefix = namespace.join(\".\");\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n // Handle empty string as a special case\n let query: string;\n if (key === \"\") {\n // For empty keys, search by prefix and filter results\n query = prefixQuery;\n } else {\n const escapedKey = this.escapeTagValue(key);\n query = `(${prefixQuery}) (@key:{${escapedKey}})`;\n }\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: key === \"\" ? 100 : 1 },\n });\n\n if (!results || !results.documents || results.documents.length === 0) {\n return null;\n }\n\n // For empty key, filter to find the exact match\n if (key === \"\") {\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n if (jsonDoc.key === \"\" && jsonDoc.prefix === prefix) {\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n }\n }\n return null;\n }\n\n const doc = results.documents[0];\n const jsonDoc = doc.value as unknown as StoreDocument;\n const docId = doc.id;\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(docId);\n }\n\n return {\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n };\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return null;\n }\n throw error;\n }\n }\n\n async put(\n namespace: string[],\n key: string,\n value: any,\n options?: { ttl?: number; index?: boolean | string[] }\n ): Promise<void> {\n // Validate namespace for put operations\n this.validateNamespace(namespace);\n const prefix = namespace.join(\".\");\n const docId = uuidv4();\n // Use high-resolution time for better timestamp precision\n const now = Date.now() * 1000000 + Math.floor(performance.now() * 1000); // Microseconds + nanoseconds component\n let createdAt = now; // Will be overridden if document exists\n\n // Delete existing document if it exists\n // For TEXT fields, we need to match all tokens (split by dots and hyphens)\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n const prefixQuery =\n tokens.length > 0 ? `@prefix:(${tokens.join(\" \")})` : \"*\";\n\n // For TAG fields in curly braces, escape special characters\n const escapedKey = this.escapeTagValue(key);\n const existingQuery = `(${prefixQuery}) (@key:{${escapedKey}})`;\n try {\n const existing = await this.client.ft.search(\"store\", existingQuery, {\n LIMIT: { from: 0, size: 1 },\n });\n\n if (existing && existing.documents && existing.documents.length > 0) {\n const oldDocId = existing.documents[0].id;\n // Preserve the original created_at timestamp\n const existingDoc = await this.client.json.get(oldDocId);\n if (\n existingDoc &&\n typeof existingDoc === \"object\" &&\n \"created_at\" in existingDoc\n ) {\n createdAt = (existingDoc as any).created_at;\n }\n await this.client.del(oldDocId);\n\n // Also delete associated vector if it exists\n if (this.indexConfig) {\n const oldUuid = oldDocId.split(\":\").pop();\n const oldVectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${oldUuid}`;\n try {\n await this.client.del(oldVectorKey);\n } catch (error) {\n // Vector might not exist\n }\n }\n }\n } catch (error) {\n // Index might not exist yet\n }\n\n // Handle delete operation\n if (value === null) {\n return;\n }\n\n // Store the document\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const doc = {\n prefix,\n key,\n value,\n created_at: createdAt,\n updated_at: now,\n };\n\n await this.client.json.set(storeKey, \"$\", doc);\n\n // Handle embeddings if configured\n if (this.indexConfig && this.embeddings && options?.index !== false) {\n const fieldsToIndex =\n options && Array.isArray(options.index)\n ? options.index\n : this.indexConfig.fields || [\"text\"];\n const textsToEmbed = [];\n const fieldNames = [];\n\n for (const field of fieldsToIndex) {\n if (value[field]) {\n textsToEmbed.push(value[field]);\n fieldNames.push(field);\n }\n }\n\n if (textsToEmbed.length > 0) {\n const embeddings = await this.embeddings.embedDocuments(textsToEmbed);\n\n for (let i = 0; i < embeddings.length; i++) {\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docId}`;\n const vectorDoc: VectorDocument = {\n prefix,\n key,\n field_name: fieldNames[i],\n embedding: embeddings[i],\n created_at: now,\n updated_at: now,\n };\n\n await this.client.json.set(vectorKey, \"$\", vectorDoc as any);\n\n // Apply TTL to vector key if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(vectorKey, ttlSeconds);\n }\n }\n }\n }\n\n // Apply TTL if configured\n const ttlMinutes = options?.ttl || this.ttlConfig?.defaultTTL;\n if (ttlMinutes) {\n const ttlSeconds = Math.floor(ttlMinutes * 60);\n await this.client.expire(storeKey, ttlSeconds);\n }\n }\n\n async delete(namespace: string[], key: string): Promise<void> {\n await this.put(namespace, key, null);\n }\n\n async search(\n namespacePrefix: string[],\n options?: {\n filter?: Filter;\n query?: string;\n limit?: number;\n offset?: number;\n refreshTTL?: boolean;\n similarityThreshold?: number;\n }\n ): Promise<SearchItem[]> {\n const prefix = namespacePrefix.join(\".\");\n const limit = options?.limit || 10;\n const offset = options?.offset || 0;\n\n // Handle vector search if query is provided\n if (options?.query && this.indexConfig && this.embeddings) {\n const [embedding] = await this.embeddings.embedDocuments([options.query]);\n\n // Build KNN query\n // For prefix search, use wildcard since we want to match any document starting with this prefix\n let queryStr = prefix ? `@prefix:${prefix.split(/[.-]/)[0]}*` : \"*\";\n const vectorBytes = Buffer.from(new Float32Array(embedding).buffer);\n\n try {\n // Use KNN query with proper syntax\n const results = await this.client.ft.search(\n \"store_vectors\",\n `(${queryStr})=>[KNN ${limit} @embedding $BLOB]`,\n {\n PARAMS: {\n BLOB: vectorBytes,\n },\n DIALECT: 2,\n LIMIT: { from: offset, size: limit },\n RETURN: [\"prefix\", \"key\", \"__embedding_score\"],\n }\n );\n\n // Get matching store documents\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const docUuid = doc.id.split(\":\").pop();\n const storeKey = `${STORE_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n\n const storeDoc = (await this.client.json.get(\n storeKey\n )) as StoreDocument | null;\n if (storeDoc) {\n // Apply advanced filter if provided\n if (options.filter) {\n if (\n !FilterBuilder.matchesFilter(\n storeDoc.value || {},\n options.filter\n )\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options.refreshTTL) {\n await this.refreshItemTTL(storeKey);\n await this.refreshItemTTL(doc.id);\n }\n\n const score = (doc.value as any)?.__embedding_score\n ? this.calculateSimilarityScore(\n parseFloat((doc.value as any).__embedding_score as string)\n )\n : 0;\n\n // Apply similarity threshold if specified\n const threshold =\n options.similarityThreshold ??\n this.indexConfig?.similarityThreshold;\n if (threshold !== undefined && score < threshold) {\n continue;\n }\n\n items.push({\n value: storeDoc.value,\n key: storeDoc.key,\n namespace: storeDoc.prefix.split(\".\"),\n created_at: new Date(storeDoc.created_at / 1000000),\n updated_at: new Date(storeDoc.updated_at / 1000000),\n score,\n });\n }\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n // Regular search without vectors\n let queryStr = \"*\";\n if (prefix) {\n // For prefix search, we need to match all tokens from the namespace prefix\n const tokens = prefix.split(/[.-]/).filter((t) => t.length > 0);\n if (tokens.length > 0) {\n // Match all tokens to ensure we get the right prefix\n queryStr = `@prefix:(${tokens.join(\" \")})`;\n }\n }\n\n try {\n const results = await this.client.ft.search(\"store\", queryStr, {\n LIMIT: { from: offset, size: limit },\n SORTBY: { BY: \"created_at\", DIRECTION: \"DESC\" },\n });\n\n const items: SearchItem[] = [];\n for (const doc of results.documents) {\n const jsonDoc = doc.value as unknown as StoreDocument;\n\n // Apply advanced filter\n if (options?.filter) {\n if (\n !FilterBuilder.matchesFilter(jsonDoc.value || {}, options.filter)\n ) {\n continue;\n }\n }\n\n // Refresh TTL if requested\n if (options?.refreshTTL) {\n await this.refreshItemTTL(doc.id);\n }\n\n items.push({\n value: jsonDoc.value,\n key: jsonDoc.key,\n namespace: jsonDoc.prefix.split(\".\"),\n created_at: new Date(jsonDoc.created_at / 1000000),\n updated_at: new Date(jsonDoc.updated_at / 1000000),\n });\n }\n\n return items;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async listNamespaces(options?: {\n prefix?: string[];\n suffix?: string[];\n maxDepth?: number;\n limit?: number;\n offset?: number;\n }): Promise<string[][]> {\n let query = \"*\";\n\n try {\n const results = await this.client.ft.search(\"store\", query, {\n LIMIT: { from: 0, size: 1000 }, // Get many to deduplicate\n RETURN: [\"prefix\"],\n });\n\n // Extract unique namespaces and filter\n const namespaceSet = new Set<string>();\n for (const doc of results.documents) {\n const prefix = (doc.value as unknown as StoreDocument).prefix;\n const parts = prefix.split(\".\");\n\n // Apply prefix filter if specified\n if (options?.prefix) {\n // Check if this namespace starts with the specified prefix\n if (parts.length < options.prefix.length) continue;\n\n let matches = true;\n for (let i = 0; i < options.prefix.length; i++) {\n if (parts[i] !== options.prefix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply suffix filter if specified\n if (options?.suffix) {\n // Check if this namespace ends with the specified suffix\n if (parts.length < options.suffix.length) continue;\n\n let matches = true;\n const startIdx = parts.length - options.suffix.length;\n for (let i = 0; i < options.suffix.length; i++) {\n if (parts[startIdx + i] !== options.suffix[i]) {\n matches = false;\n break;\n }\n }\n if (!matches) continue;\n }\n\n // Apply max depth\n if (options?.maxDepth) {\n const truncated = parts.slice(0, options.maxDepth);\n namespaceSet.add(truncated.join(\".\"));\n } else {\n namespaceSet.add(prefix);\n }\n }\n\n // Convert to array of arrays and sort\n let namespaces = Array.from(namespaceSet)\n .map((ns) => ns.split(\".\"))\n .sort((a, b) => a.join(\".\").localeCompare(b.join(\".\")));\n\n // Apply pagination\n if (options?.offset || options?.limit) {\n const offset = options.offset || 0;\n const limit = options.limit || 10;\n namespaces = namespaces.slice(offset, offset + limit);\n }\n\n return namespaces;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n return [];\n }\n throw error;\n }\n }\n\n async batch(ops: Operation[]): Promise<any[]> {\n const results: any[] = new Array(ops.length).fill(null);\n\n // Process operations in order to maintain dependencies\n for (let idx = 0; idx < ops.length; idx++) {\n const op = ops[idx];\n\n // Execute operation based on type guards\n if (isPutOperation(op)) {\n // TypeScript now knows op is PutOperation\n await this.put(op.namespace, op.key, op.value);\n results[idx] = null;\n } else if (isSearchOperation(op)) {\n // TypeScript now knows op is SearchOperation\n results[idx] = await this.search(op.namespacePrefix, {\n filter: op.filter,\n query: op.query,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isListNamespacesOperation(op)) {\n // TypeScript now knows op is ListNamespacesOperation\n let prefix: string[] | undefined = undefined;\n let suffix: string[] | undefined = undefined;\n\n if (op.matchConditions) {\n for (const condition of op.matchConditions) {\n if (condition.matchType === \"prefix\") {\n prefix = condition.path;\n } else if (condition.matchType === \"suffix\") {\n suffix = condition.path;\n }\n }\n }\n\n results[idx] = await this.listNamespaces({\n prefix,\n suffix,\n maxDepth: op.maxDepth,\n limit: op.limit,\n offset: op.offset,\n });\n } else if (isGetOperation(op)) {\n // TypeScript now knows op is GetOperation\n results[idx] = await this.get(op.namespace, op.key);\n } else {\n // This should never happen with proper Operation type\n throw new Error(`Unknown operation type: ${JSON.stringify(op)}`);\n }\n }\n\n return results;\n }\n\n async close(): Promise<void> {\n await this.client.quit();\n }\n\n /**\n * Get statistics about the store.\n * Returns document counts and other metrics.\n */\n async getStatistics(): Promise<{\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n }> {\n const stats: {\n totalDocuments: number;\n namespaceCount: number;\n vectorDocuments?: number;\n indexInfo?: Record<string, any>;\n } = {\n totalDocuments: 0,\n namespaceCount: 0,\n };\n\n try {\n // Get total document count\n const countResult = await this.client.ft.search(\"store\", \"*\", {\n LIMIT: { from: 0, size: 0 },\n });\n stats.totalDocuments = countResult.total || 0;\n\n // Get unique namespace count\n const namespaces = await this.listNamespaces({ limit: 1000 });\n stats.namespaceCount = namespaces.length;\n\n // Get vector document count if index is configured\n if (this.indexConfig) {\n try {\n const vectorResult = await this.client.ft.search(\n \"store_vectors\",\n \"*\",\n {\n LIMIT: { from: 0, size: 0 },\n }\n );\n stats.vectorDocuments = vectorResult.total || 0;\n } catch (error) {\n // Vector index might not exist\n stats.vectorDocuments = 0;\n }\n\n // Get index info\n try {\n stats.indexInfo = await this.client.ft.info(\"store\");\n } catch (error) {\n // Index info might not be available\n }\n }\n } catch (error: any) {\n if (!error.message?.includes(\"no such index\")) {\n throw error;\n }\n }\n\n return stats;\n }\n\n private validateNamespace(namespace: string[]): void {\n if (namespace.length === 0) {\n throw new InvalidNamespaceError(\"Namespace cannot be empty.\");\n }\n for (const label of namespace) {\n // Runtime check for JavaScript users (TypeScript already ensures this)\n // This check is for runtime safety when called from JavaScript\n // noinspection SuspiciousTypeOfGuard\n if (typeof label !== \"string\") {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${String(\n label\n )}' found in ${namespace}. Namespace labels must be strings.`\n );\n }\n if (label.includes(\".\")) {\n throw new InvalidNamespaceError(\n `Invalid namespace label '${label}' found in ${namespace}. Namespace labels cannot contain periods ('.').`\n );\n }\n if (label === \"\") {\n throw new InvalidNamespaceError(\n `Namespace labels cannot be empty strings. Got ${label} in ${namespace}`\n );\n }\n }\n if (namespace[0] === \"langgraph\") {\n throw new InvalidNamespaceError(\n `Root label for namespace cannot be \"langgraph\". Got: ${namespace}`\n );\n }\n }\n\n private async refreshItemTTL(docId: string): Promise<void> {\n if (this.ttlConfig?.defaultTTL) {\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n await this.client.expire(docId, ttlSeconds);\n\n // Also refresh vector key if it exists\n const docUuid = docId.split(\":\").pop();\n const vectorKey = `${STORE_VECTOR_PREFIX}${REDIS_KEY_SEPARATOR}${docUuid}`;\n try {\n await this.client.expire(vectorKey, ttlSeconds);\n } catch (error) {\n // Vector key might not exist\n }\n }\n }\n\n private escapeTagValue(value: string): string {\n // Delegate to shared utility for RediSearch TAG field escaping\n return escapeRediSearchTagValue(value);\n }\n\n /**\n * Calculate similarity score based on the distance metric.\n * Converts raw distance to a normalized similarity score [0,1].\n */\n private calculateSimilarityScore(distance: number): number {\n const metric = this.indexConfig?.distanceType || \"cosine\";\n\n switch (metric) {\n case \"cosine\":\n // Cosine distance is in range [0,2], convert to similarity [0,1]\n return Math.max(0, 1 - distance / 2);\n\n case \"l2\":\n // L2 (Euclidean) distance, use exponential decay\n // Similarity = e^(-distance)\n return Math.exp(-distance);\n\n case \"ip\":\n // Inner product can be negative, use sigmoid function\n // Similarity = 1 / (1 + e^(-distance))\n return 1 / (1 + Math.exp(-distance));\n\n default:\n // Default to cosine similarity\n return Math.max(0, 1 - distance / 2);\n }\n }\n}\n\n// Export FilterBuilder for testing purposes\nexport { FilterBuilder };\n"],"mappings":"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"}