@langchain/langgraph-checkpoint-redis 1.0.2 → 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.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n TASKS,\n maxChannelVersion,\n copyCheckpoint,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient, createCluster } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\n// Type for Redis client - supports both standalone and cluster\nexport type RedisClientType =\n | ReturnType<typeof createClient>\n | ReturnType<typeof createCluster>;\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_blobs\",\n prefix: \"checkpoint_blob:\",\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 \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.version\": { type: \"TAG\", AS: \"version\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\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\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\ninterface CheckpointDocument {\n thread_id: string;\n checkpoint_ns: string;\n checkpoint_id: string;\n parent_checkpoint_id: string | null;\n checkpoint: Checkpoint & {\n channel_values?: Record<string, any>;\n channel_blobs?: Record<string, { __blob__: boolean; key: string }>;\n };\n metadata: CheckpointMetadata;\n checkpoint_ts: number;\n has_writes: string;\n source?: string;\n step?: number;\n [key: string]: any; // Allow additional fields for metadata\n}\n\nexport class RedisSaver extends BaseCheckpointSaver {\n private client: RedisClientType;\n private ttlConfig?: TTLConfig;\n\n constructor(client: RedisClientType, 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<RedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new RedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n ttlConfig?: TTLConfig\n ): Promise<RedisSaver> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const saver = new RedisSaver(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 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 let key: string;\n let jsonDoc: CheckpointDocument | null;\n\n if (checkpointId) {\n // Get specific checkpoint\n key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n } else {\n // Get latest checkpoint - need to search\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n if (keys.length === 0) {\n return undefined;\n }\n\n // Sort by key to get latest\n keys.sort();\n key = keys[keys.length - 1];\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n }\n\n if (!jsonDoc) {\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 // Load checkpoint with pending writes\n const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(\n jsonDoc\n );\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\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 const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Copy checkpoint and filter channel_values to only include changed channels\n const storedCheckpoint = copyCheckpoint(checkpoint);\n\n // If newVersions is provided and has keys, only store those channels that changed\n // If newVersions is empty {}, store no channel values\n // If newVersions is not provided (undefined), keep all channel_values as-is\n if (storedCheckpoint.channel_values && newVersions !== undefined) {\n if (Object.keys(newVersions).length === 0) {\n // Empty newVersions means no channels changed - store empty channel_values\n storedCheckpoint.channel_values = {};\n } else {\n // Only store the channels that are in newVersions\n const filteredChannelValues: Record<string, any> = {};\n for (const channel of Object.keys(newVersions)) {\n if (channel in storedCheckpoint.channel_values) {\n filteredChannelValues[channel] =\n storedCheckpoint.channel_values[channel];\n }\n }\n storedCheckpoint.channel_values = filteredChannelValues;\n }\n }\n // If newVersions is undefined, keep all channel_values as-is (for backward compatibility)\n\n // Structure matching Python implementation\n const jsonDoc: CheckpointDocument = {\n thread_id: threadId,\n // Store empty namespace as \"__empty__\" for RediSearch compatibility\n checkpoint_ns: checkpointNs === \"\" ? \"__empty__\" : checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: storedCheckpoint,\n metadata: metadata,\n checkpoint_ts: Date.now(),\n has_writes: \"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 as any);\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 *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // If filter is provided (even if empty), use search functionality\n if (options?.filter !== undefined) {\n // Check if we have null values in the filter which RediSearch can't handle\n const hasNullFilter = Object.values(options.filter).some(\n (v) => v === null\n );\n\n // Build search query\n const queryParts: string[] = [];\n\n // Add thread_id constraint if provided\n if (config?.configurable?.thread_id) {\n const threadId = config.configurable.thread_id.replace(\n /[-.@]/g,\n \"\\\\$&\"\n );\n queryParts.push(`(@thread_id:{${threadId}})`);\n }\n\n // Add checkpoint_ns constraint if provided\n if (config?.configurable?.checkpoint_ns !== undefined) {\n const checkpointNs = config.configurable.checkpoint_ns;\n if (checkpointNs === \"\") {\n // Empty string needs special handling in RediSearch\n // We'll store it as \"__empty__\" in the index\n queryParts.push(`(@checkpoint_ns:{__empty__})`);\n } else {\n const escapedNs = checkpointNs.replace(/[-.@]/g, \"\\\\$&\");\n queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);\n }\n }\n\n // Skip metadata filters in search query when 'before' parameter is used\n // We'll apply them after the before filtering to get correct results\n if (!options?.before && options?.filter) {\n // Add metadata filters (but skip null values)\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 } else if (\n typeof value === \"object\" &&\n Object.keys(value).length === 0\n ) {\n // Skip empty objects\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 // Fetch more results than the limit to handle post-filtering for 'before'\n // When no thread_id is specified but 'before' is used, we need to fetch all results\n const fetchLimit =\n options?.before && !config?.configurable?.thread_id\n ? 1000 // Fetch many results for global search with 'before' filtering\n : options?.before\n ? limit * 10\n : limit;\n\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: fetchLimit },\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n let documents = results.documents;\n\n let yieldedCount = 0;\n\n for (const doc of documents) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n // UUID6 IDs are time-sortable, so string comparison works for ordering\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = doc.value.checkpoint_id as string;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n const jsonDoc = doc.value;\n\n // Apply metadata filters manually (either for null filters or when before parameter was used)\n let matches = true;\n if ((hasNullFilter || options?.before) && options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n // This should only match explicit null, not missing fields\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n\n // Search succeeded, return without falling through\n return;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall through to regular listing\n } else {\n throw error;\n }\n }\n\n // If search failed due to missing index, fall through to regular listing\n if (config?.configurable?.thread_id) {\n // Fall back to regular listing with manual filtering when thread_id is specified\n const threadId = config.configurable.thread_id;\n const checkpointNs = config.configurable.checkpoint_ns ?? \"\";\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n keys.sort().reverse();\n\n let filteredKeys = keys;\n\n // Handle 'before' parameter\n if (options?.before?.configurable?.checkpoint_id) {\n const beforeThreadId =\n options.before.configurable.thread_id || threadId;\n const beforeCheckpointNs =\n options.before.configurable.checkpoint_ns ?? checkpointNs;\n const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;\n\n const beforeIndex = keys.indexOf(beforeKey);\n if (beforeIndex > 0) {\n // Return all items that come after the found index (i.e., before in time)\n filteredKeys = keys.slice(beforeIndex + 1);\n } else if (beforeIndex === 0) {\n // Nothing before the first item (most recent)\n filteredKeys = [];\n }\n // If not found, return all\n }\n\n const limit = options?.limit ?? 10;\n const limitedKeys = filteredKeys.slice(0, limit);\n\n for (const key of limitedKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n // Check if metadata matches filter\n let matches = true;\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (filterValue === null) {\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n\n if (!matches) continue;\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n }\n }\n } else {\n // Fall back to global search when thread_id is undefined\n // This is needed for validation tests that search globally with 'before' parameter\n const globalPattern =\n config?.configurable?.checkpoint_ns !== undefined\n ? `checkpoint:*:${\n config.configurable.checkpoint_ns === \"\"\n ? \"__empty__\"\n : config.configurable.checkpoint_ns\n }:*`\n : \"checkpoint:*\";\n\n const allKeys = await (this.client as any).keys(globalPattern);\n const allDocuments: { key: string; doc: CheckpointDocument }[] = [];\n\n // Load all matching documents\n for (const key of allKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n allDocuments.push({ key, doc: jsonDoc });\n }\n }\n\n // Sort by timestamp (descending) to match the search behavior\n allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);\n\n let yieldedCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const { doc: jsonDoc } of allDocuments) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = jsonDoc.checkpoint_id;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n // Apply metadata filters manually\n let matches = true;\n if (options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n }\n\n return;\n }\n\n // Regular listing without filter - use search with empty filter instead\n // This ensures consistent behavior between filter={} and filter=undefined\n const searchOptions: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n } = {\n ...options,\n filter: {} as CheckpointMetadata,\n };\n\n // Delegate to the search path\n yield* this.list(config, searchOptions);\n return;\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 // Collect write keys for sorted set tracking\n const writeKeys: string[] = [];\n\n // Use high-resolution timestamp to ensure unique ordering across putWrites calls\n const baseTimestamp = performance.now() * 1000; // Microsecond precision\n\n // Store each write as a separate indexed JSON document\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 timestamp: baseTimestamp,\n global_idx: baseTimestamp + idx, // Add microseconds for sub-millisecond ordering\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc as any);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Use timestamp + idx for scoring to maintain correct order\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = baseTimestamp + 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 // Apply TTL to write keys and zset\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n // Get the current document and update it\n const currentDoc = (await this.client.json.get(\n checkpointKey\n )) as CheckpointDocument | null;\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc as any);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const checkpointKeys = await (this.client as any).keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `writes:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const writesKeys = await (this.client as any).keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to load channel blobs (simplified - no blob support for now)\n // private async loadChannelBlobs(\n // checkpoint: Checkpoint & { channel_blobs?: any }\n // ): Promise<Checkpoint> {\n // // Since we're not using blobs anymore, just return the checkpoint as-is\n // return checkpoint;\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 // Search for all write documents for this checkpoint\n const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;\n const writeKeys = await (this.client as any).keys(pattern);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const writeDocuments: any[] = [];\n for (const writeKey of writeKeys) {\n const writeDoc = (await this.client.json.get(writeKey)) as any;\n if (writeDoc) {\n writeDocuments.push(writeDoc);\n }\n }\n\n // Sort by global_idx (which represents insertion order across all putWrites calls)\n // This matches how SQLite would naturally order by insertion time + idx\n writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeDoc of writeDocuments) {\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 return pendingWrites;\n }\n\n // Helper method to load checkpoint with pending writes\n private async loadCheckpointWithWrites(jsonDoc: any): Promise<{\n checkpoint: Checkpoint;\n pendingWrites?: Array<[string, string, any]>;\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 // Migrate pending sends ONLY for OLD checkpoint versions (v < 4) with parents\n // Modern checkpoints (v >= 4) should NEVER have pending sends migrated\n if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {\n // Convert back from \"__empty__\" to empty string for migration\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n await this.migratePendingSends(\n checkpoint,\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.parent_checkpoint_id\n );\n }\n\n // Load this checkpoint's own pending writes (but don't migrate them)\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n // Convert back from \"__empty__\" to empty string for key lookup\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.checkpoint_id\n );\n }\n\n return { checkpoint, pendingWrites };\n }\n\n // Migrate pending sends from parent checkpoint (matches SQLite implementation)\n private async migratePendingSends(\n checkpoint: Checkpoint,\n threadId: string,\n checkpointNs: string,\n parentCheckpointId: string\n ): Promise<void> {\n // Load pending writes from parent checkpoint that have TASKS channel\n const parentWrites = await this.loadPendingWrites(\n threadId,\n checkpointNs,\n parentCheckpointId\n );\n\n if (!parentWrites || parentWrites.length === 0) {\n return;\n }\n\n // Filter for TASKS channel writes only\n const taskWrites = parentWrites.filter(([, channel]) => channel === TASKS);\n\n if (taskWrites.length === 0) {\n return;\n }\n\n // Collect all task values in order\n const allTasks: any[] = [];\n for (const [, , value] of taskWrites) {\n allTasks.push(value);\n }\n\n // Add pending sends to checkpoint\n checkpoint.channel_values ??= {};\n checkpoint.channel_values[TASKS] = allTasks;\n\n // Add to versions (matches SQLite logic)\n checkpoint.channel_versions[TASKS] =\n Object.keys(checkpoint.channel_versions).length > 0\n ? maxChannelVersion(...Object.values(checkpoint.channel_versions))\n : 1;\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 // Convert back from \"__empty__\" to empty string\n const checkpointNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n\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: checkpointNs,\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: checkpointNs,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\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 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 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 as any, {\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\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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n TASKS,\n maxChannelVersion,\n copyCheckpoint,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient, createCluster } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\nimport { WRITE_KEYS_ZSET_PREFIX } from \"./constants.js\";\n\n// Type for Redis client - supports both standalone and cluster\nexport type RedisClientType =\n | ReturnType<typeof createClient>\n | ReturnType<typeof createCluster>;\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_blobs\",\n prefix: \"checkpoint_blob:\",\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 \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.version\": { type: \"TAG\", AS: \"version\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\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\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\ninterface CheckpointDocument {\n thread_id: string;\n checkpoint_ns: string;\n checkpoint_id: string;\n parent_checkpoint_id: string | null;\n checkpoint: Checkpoint & {\n channel_values?: Record<string, any>;\n channel_blobs?: Record<string, { __blob__: boolean; key: string }>;\n };\n metadata: CheckpointMetadata;\n checkpoint_ts: number;\n has_writes: string;\n source?: string;\n step?: number;\n [key: string]: any; // Allow additional fields for metadata\n}\n\nexport class RedisSaver extends BaseCheckpointSaver {\n private client: RedisClientType;\n private ttlConfig?: TTLConfig;\n\n constructor(client: RedisClientType, 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<RedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new RedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n ttlConfig?: TTLConfig\n ): Promise<RedisSaver> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const saver = new RedisSaver(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 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 let key: string;\n let jsonDoc: CheckpointDocument | null;\n\n if (checkpointId) {\n // Get specific checkpoint\n key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n } else {\n // Get latest checkpoint - need to search\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n if (keys.length === 0) {\n return undefined;\n }\n\n // Sort by key to get latest\n keys.sort();\n key = keys[keys.length - 1];\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n }\n\n if (!jsonDoc) {\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 // Load checkpoint with pending writes\n const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(\n jsonDoc\n );\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\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 const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Copy checkpoint and filter channel_values to only include changed channels\n const storedCheckpoint = copyCheckpoint(checkpoint);\n\n // If newVersions is provided and has keys, only store those channels that changed\n // If newVersions is empty {}, store no channel values\n // If newVersions is not provided (undefined), keep all channel_values as-is\n if (storedCheckpoint.channel_values && newVersions !== undefined) {\n if (Object.keys(newVersions).length === 0) {\n // Empty newVersions means no channels changed - store empty channel_values\n storedCheckpoint.channel_values = {};\n } else {\n // Only store the channels that are in newVersions\n const filteredChannelValues: Record<string, any> = {};\n for (const channel of Object.keys(newVersions)) {\n if (channel in storedCheckpoint.channel_values) {\n filteredChannelValues[channel] =\n storedCheckpoint.channel_values[channel];\n }\n }\n storedCheckpoint.channel_values = filteredChannelValues;\n }\n }\n // If newVersions is undefined, keep all channel_values as-is (for backward compatibility)\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: CheckpointDocument = {\n thread_id: threadId,\n // Store empty namespace as \"__empty__\" for RediSearch compatibility\n checkpoint_ns: checkpointNs === \"\" ? \"__empty__\" : checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: storedCheckpoint,\n metadata: 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 as any);\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 *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // If filter is provided (even if empty), use search functionality\n if (options?.filter !== undefined) {\n // Check if we have null values in the filter which RediSearch can't handle\n const hasNullFilter = Object.values(options.filter).some(\n (v) => v === null\n );\n\n // Build search query\n const queryParts: string[] = [];\n\n // Add thread_id constraint if provided\n if (config?.configurable?.thread_id) {\n const threadId = escapeRediSearchTagValue(\n config.configurable.thread_id\n );\n queryParts.push(`(@thread_id:{${threadId}})`);\n }\n\n // Add checkpoint_ns constraint if provided\n if (config?.configurable?.checkpoint_ns !== undefined) {\n const checkpointNs = config.configurable.checkpoint_ns;\n if (checkpointNs === \"\") {\n // Empty string needs special handling in RediSearch\n // We'll store it as \"__empty__\" in the index\n queryParts.push(`(@checkpoint_ns:{__empty__})`);\n } else {\n const escapedNs = escapeRediSearchTagValue(checkpointNs);\n queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);\n }\n }\n\n // Skip metadata filters in search query when 'before' parameter is used\n // We'll apply them after the before filtering to get correct results\n if (!options?.before && options?.filter) {\n // Add metadata filters (but skip null values)\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 } else if (\n typeof value === \"object\" &&\n Object.keys(value).length === 0\n ) {\n // Skip empty objects\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 // Fetch more results than the limit to handle post-filtering for 'before'\n // When no thread_id is specified but 'before' is used, we need to fetch all results\n const fetchLimit =\n options?.before && !config?.configurable?.thread_id\n ? 1000 // Fetch many results for global search with 'before' filtering\n : options?.before\n ? limit * 10\n : limit;\n\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: fetchLimit },\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n let documents = results.documents;\n\n let yieldedCount = 0;\n\n for (const doc of documents) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n // UUID6 IDs are time-sortable, so string comparison works for ordering\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = doc.value.checkpoint_id as string;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n const jsonDoc = doc.value;\n\n // Apply metadata filters manually (either for null filters or when before parameter was used)\n let matches = true;\n if ((hasNullFilter || options?.before) && options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n // This should only match explicit null, not missing fields\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n\n // Search succeeded, return without falling through\n return;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall through to regular listing\n } else {\n throw error;\n }\n }\n\n // If search failed due to missing index, fall through to regular listing\n if (config?.configurable?.thread_id) {\n // Fall back to regular listing with manual filtering when thread_id is specified\n const threadId = config.configurable.thread_id;\n const checkpointNs = config.configurable.checkpoint_ns ?? \"\";\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n keys.sort().reverse();\n\n let filteredKeys = keys;\n\n // Handle 'before' parameter\n if (options?.before?.configurable?.checkpoint_id) {\n const beforeThreadId =\n options.before.configurable.thread_id || threadId;\n const beforeCheckpointNs =\n options.before.configurable.checkpoint_ns ?? checkpointNs;\n const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;\n\n const beforeIndex = keys.indexOf(beforeKey);\n if (beforeIndex > 0) {\n // Return all items that come after the found index (i.e., before in time)\n filteredKeys = keys.slice(beforeIndex + 1);\n } else if (beforeIndex === 0) {\n // Nothing before the first item (most recent)\n filteredKeys = [];\n }\n // If not found, return all\n }\n\n const limit = options?.limit ?? 10;\n const limitedKeys = filteredKeys.slice(0, limit);\n\n for (const key of limitedKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n // Check if metadata matches filter\n let matches = true;\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (filterValue === null) {\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n\n if (!matches) continue;\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n }\n }\n } else {\n // Fall back to global search when thread_id is undefined\n // This is needed for validation tests that search globally with 'before' parameter\n const globalPattern =\n config?.configurable?.checkpoint_ns !== undefined\n ? `checkpoint:*:${\n config.configurable.checkpoint_ns === \"\"\n ? \"__empty__\"\n : config.configurable.checkpoint_ns\n }:*`\n : \"checkpoint:*\";\n\n const allKeys = await (this.client as any).keys(globalPattern);\n const allDocuments: { key: string; doc: CheckpointDocument }[] = [];\n\n // Load all matching documents\n for (const key of allKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n allDocuments.push({ key, doc: jsonDoc });\n }\n }\n\n // Sort by timestamp (descending) to match the search behavior\n allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);\n\n let yieldedCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const { doc: jsonDoc } of allDocuments) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = jsonDoc.checkpoint_id;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n // Apply metadata filters manually\n let matches = true;\n if (options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n }\n\n return;\n }\n\n // Regular listing without filter - use search with empty filter instead\n // This ensures consistent behavior between filter={} and filter=undefined\n const searchOptions: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n } = {\n ...options,\n filter: {} as CheckpointMetadata,\n };\n\n // Delegate to the search path\n yield* this.list(config, searchOptions);\n return;\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 // Collect write keys for sorted set tracking\n const writeKeys: string[] = [];\n\n // Use high-resolution timestamp to ensure unique ordering across putWrites calls\n const baseTimestamp = performance.now() * 1000; // Microsecond precision\n\n // Store each write as a separate indexed JSON document\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 timestamp: baseTimestamp,\n global_idx: baseTimestamp + idx, // Add microseconds for sub-millisecond ordering\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc as any);\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 // Use timestamp + idx for scoring to maintain correct order\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = baseTimestamp + 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 // Apply TTL to write keys and zset\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n // Get the current document and update it\n const currentDoc = (await this.client.json.get(\n checkpointKey\n )) as CheckpointDocument | null;\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc as any);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const checkpointKeys = await (this.client as any).keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `writes:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const writesKeys = await (this.client as any).keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to load channel blobs (simplified - no blob support for now)\n // private async loadChannelBlobs(\n // checkpoint: Checkpoint & { channel_blobs?: any }\n // ): Promise<Checkpoint> {\n // // Since we're not using blobs anymore, just return the checkpoint as-is\n // return checkpoint;\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 // Search for all write documents for this checkpoint\n const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;\n const writeKeys = await (this.client as any).keys(pattern);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const writeDocuments: any[] = [];\n for (const writeKey of writeKeys) {\n const writeDoc = (await this.client.json.get(writeKey)) as any;\n if (writeDoc) {\n writeDocuments.push(writeDoc);\n }\n }\n\n // Sort by global_idx (which represents insertion order across all putWrites calls)\n // This matches how SQLite would naturally order by insertion time + idx\n writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeDoc of writeDocuments) {\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 return pendingWrites;\n }\n\n // Helper method to load checkpoint with pending writes\n private async loadCheckpointWithWrites(jsonDoc: any): Promise<{\n checkpoint: Checkpoint;\n pendingWrites?: Array<[string, string, any]>;\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 // Migrate pending sends ONLY for OLD checkpoint versions (v < 4) with parents\n // Modern checkpoints (v >= 4) should NEVER have pending sends migrated\n if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {\n // Convert back from \"__empty__\" to empty string for migration\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n await this.migratePendingSends(\n checkpoint,\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.parent_checkpoint_id\n );\n }\n\n // Load this checkpoint's own pending writes (but don't migrate them)\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n // Convert back from \"__empty__\" to empty string for key lookup\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.checkpoint_id\n );\n }\n\n return { checkpoint, pendingWrites };\n }\n\n // Migrate pending sends from parent checkpoint (matches SQLite implementation)\n private async migratePendingSends(\n checkpoint: Checkpoint,\n threadId: string,\n checkpointNs: string,\n parentCheckpointId: string\n ): Promise<void> {\n // Load pending writes from parent checkpoint that have TASKS channel\n const parentWrites = await this.loadPendingWrites(\n threadId,\n checkpointNs,\n parentCheckpointId\n );\n\n if (!parentWrites || parentWrites.length === 0) {\n return;\n }\n\n // Filter for TASKS channel writes only\n const taskWrites = parentWrites.filter(([, channel]) => channel === TASKS);\n\n if (taskWrites.length === 0) {\n return;\n }\n\n // Collect all task values in order\n const allTasks: any[] = [];\n for (const [, , value] of taskWrites) {\n allTasks.push(value);\n }\n\n // Add pending sends to checkpoint\n checkpoint.channel_values ??= {};\n checkpoint.channel_values[TASKS] = allTasks;\n\n // Add to versions (matches SQLite logic)\n checkpoint.channel_versions[TASKS] =\n Object.keys(checkpoint.channel_versions).length > 0\n ? maxChannelVersion(...Object.values(checkpoint.channel_versions))\n : 1;\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 // Convert back from \"__empty__\" to empty string\n const checkpointNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n\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: checkpointNs,\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: checkpointNs,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\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 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 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 as any, {\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\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"],"mappings":"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"}
package/dist/shallow.cjs CHANGED
@@ -1,8 +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
+ const require_constants = require("./constants.cjs");
3
4
  let _langchain_langgraph_checkpoint = require("@langchain/langgraph-checkpoint");
4
5
  let redis = require("redis");
5
-
6
6
  //#region src/shallow.ts
7
7
  function deterministicStringify(obj) {
8
8
  if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
@@ -138,6 +138,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
138
138
  channel_values: checkpoint.channel_values || {},
139
139
  channel_blobs: void 0
140
140
  };
141
+ const zsetKey = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
142
+ const writesExist = await this.client.exists(zsetKey);
141
143
  const jsonDoc = {
142
144
  thread_id: threadId,
143
145
  checkpoint_ns: checkpointNs,
@@ -146,7 +148,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
146
148
  checkpoint: checkpointCopy,
147
149
  metadata: this.sanitizeMetadata(metadata),
148
150
  checkpoint_ts: Date.now(),
149
- has_writes: "false"
151
+ has_writes: writesExist ? "true" : "false"
150
152
  };
151
153
  this.addSearchableMetadataFields(jsonDoc, metadata);
152
154
  await this.client.json.set(key, "$", jsonDoc);
@@ -275,7 +277,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
275
277
  await this.client.json.set(writeKey, "$", writeDoc);
276
278
  }
277
279
  if (writeKeys.length > 0) {
278
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
280
+ const zsetKey = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
279
281
  const zaddArgs = {};
280
282
  writeKeys.forEach((key, idx) => {
281
283
  zaddArgs[key] = idx;
@@ -302,7 +304,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
302
304
  const writesPattern = `checkpoint_write:${threadId}:*`;
303
305
  const writesKeys = await this.client.keys(writesPattern);
304
306
  if (writesKeys.length > 0) await this.client.del(writesKeys);
305
- const zsetPattern = `write_keys_zset:${threadId}:*`;
307
+ const zsetPattern = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:*`;
306
308
  const zsetKeys = await this.client.keys(zsetPattern);
307
309
  if (zsetKeys.length > 0) await this.client.del(zsetKeys);
308
310
  }
@@ -341,7 +343,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
341
343
  for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") console.warn(`Failed to set TTL for key ${keys[i]}:`, results[i].reason);
342
344
  }
343
345
  async loadPendingWrites(threadId, checkpointNs, checkpointId) {
344
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
346
+ const zsetKey = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
345
347
  const writeKeys = await this.client.zRange(zsetKey, 0, -1);
346
348
  if (writeKeys.length === 0) return;
347
349
  const pendingWrites = [];
@@ -374,7 +376,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
374
376
  const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
375
377
  const oldWriteKeys = await this.client.keys(writePattern);
376
378
  if (oldWriteKeys.length > 0) await this.client.del(oldWriteKeys);
377
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;
379
+ const zsetKey = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${oldCheckpointId}`;
378
380
  await this.client.del(zsetKey);
379
381
  const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
380
382
  const oldBlobKeys = await this.client.keys(blobPattern);
@@ -400,7 +402,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_che
400
402
  }
401
403
  }
402
404
  };
403
-
404
405
  //#endregion
405
406
  exports.ShallowRedisSaver = ShallowRedisSaver;
407
+
406
408
  //# sourceMappingURL=shallow.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.cjs","names":["BaseCheckpointSaver","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\";\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 // 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: \"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:${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:${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:${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:${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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.d.cts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\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 declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n private addSearchableMetadataFields;\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;UAAwBD,2BAAAA;UAAiCI,qBAAAA;UAC3CA,QAAAA;UACzBA,iBAAAA;UAboCV,wBAAAA;EAAmB,QAAA,oBAAA"}
1
+ {"version":3,"file":"shallow.d.cts","names":[],"sources":["../src/shallow.ts"],"mappings":";;;;UAeiB,SAAA;EACf,UAAA;EACA,aAAA;AAAA;;;;AAoEF;;;;;;cAAa,iBAAA,SAA0B,mBAAA;EAAA,QAC7B,MAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAY,MAAA,OAAa,SAAA,GAAY,SAAA;EAAA,OAMxB,OAAA,CACX,GAAA,UACA,SAAA,GAAY,SAAA,GACX,OAAA,CAAQ,iBAAA;EAQL,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,UAAA;EAKrC,GAAA,CACJ,MAAA,EAAQ,cAAA,EACR,UAAA,EAAY,UAAA,EACZ,QAAA,EAAU,kBAAA,EACV,YAAA,EAAc,eAAA,GACb,OAAA,CAAQ,cAAA;EA6EL,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,eAAA;EA8CzC,IAAA,CACL,MAAA,EAAQ,cAAA,SACR,OAAA,GAAU,qBAAA;IAA0B,MAAA,GAAS,kBAAA;EAAA,IAC5C,cAAA,CAAe,eAAA;EAiJZ,SAAA,CACJ,MAAA,EAAQ,cAAA,EACR,MAAA,EAAQ,YAAA,IACR,MAAA,WACC,OAAA;EAwEG,YAAA,CAAa,QAAA,WAAmB,OAAA;EA0BhC,GAAA,CAAA,GAAO,OAAA;EAAA,QAKL,2BAAA;EAAA,QA0BM,qBAAA;EAAA,QAmCA,QAAA;EAAA,QAoBA,iBAAA;EAAA,QAiCN,wBAAA;EAAA,QA4BM,oBAAA;EAAA,QAyBN,gBAAA;EAAA,QAeM,aAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"shallow.d.ts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\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 declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n private addSearchableMetadataFields;\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;UAAwBD,2BAAAA;UAAiCI,qBAAAA;UAC3CA,QAAAA;UACzBA,iBAAAA;UAboCV,wBAAAA;EAAmB,QAAA,oBAAA"}
1
+ {"version":3,"file":"shallow.d.ts","names":[],"sources":["../src/shallow.ts"],"mappings":";;;;UAeiB,SAAA;EACf,UAAA;EACA,aAAA;AAAA;;;;AAoEF;;;;;;cAAa,iBAAA,SAA0B,mBAAA;EAAA,QAC7B,MAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAY,MAAA,OAAa,SAAA,GAAY,SAAA;EAAA,OAMxB,OAAA,CACX,GAAA,UACA,SAAA,GAAY,SAAA,GACX,OAAA,CAAQ,iBAAA;EAQL,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,UAAA;EAKrC,GAAA,CACJ,MAAA,EAAQ,cAAA,EACR,UAAA,EAAY,UAAA,EACZ,QAAA,EAAU,kBAAA,EACV,YAAA,EAAc,eAAA,GACb,OAAA,CAAQ,cAAA;EA6EL,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,eAAA;EA8CzC,IAAA,CACL,MAAA,EAAQ,cAAA,SACR,OAAA,GAAU,qBAAA;IAA0B,MAAA,GAAS,kBAAA;EAAA,IAC5C,cAAA,CAAe,eAAA;EAiJZ,SAAA,CACJ,MAAA,EAAQ,cAAA,EACR,MAAA,EAAQ,YAAA,IACR,MAAA,WACC,OAAA;EAwEG,YAAA,CAAa,QAAA,WAAmB,OAAA;EA0BhC,GAAA,CAAA,GAAO,OAAA;EAAA,QAKL,2BAAA;EAAA,QA0BM,qBAAA;EAAA,QAmCA,QAAA;EAAA,QAoBA,iBAAA;EAAA,QAiCN,wBAAA;EAAA,QA4BM,oBAAA;EAAA,QAyBN,gBAAA;EAAA,QAeM,aAAA;AAAA"}
package/dist/shallow.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { escapeRediSearchTagValue } from "./utils.js";
2
+ import { WRITE_KEYS_ZSET_PREFIX } from "./constants.js";
2
3
  import { BaseCheckpointSaver, uuid6 } from "@langchain/langgraph-checkpoint";
3
4
  import { createClient } from "redis";
4
-
5
5
  //#region src/shallow.ts
6
6
  function deterministicStringify(obj) {
7
7
  if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
@@ -137,6 +137,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
137
137
  channel_values: checkpoint.channel_values || {},
138
138
  channel_blobs: void 0
139
139
  };
140
+ const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
141
+ const writesExist = await this.client.exists(zsetKey);
140
142
  const jsonDoc = {
141
143
  thread_id: threadId,
142
144
  checkpoint_ns: checkpointNs,
@@ -145,7 +147,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
145
147
  checkpoint: checkpointCopy,
146
148
  metadata: this.sanitizeMetadata(metadata),
147
149
  checkpoint_ts: Date.now(),
148
- has_writes: "false"
150
+ has_writes: writesExist ? "true" : "false"
149
151
  };
150
152
  this.addSearchableMetadataFields(jsonDoc, metadata);
151
153
  await this.client.json.set(key, "$", jsonDoc);
@@ -274,7 +276,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
274
276
  await this.client.json.set(writeKey, "$", writeDoc);
275
277
  }
276
278
  if (writeKeys.length > 0) {
277
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
279
+ const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
278
280
  const zaddArgs = {};
279
281
  writeKeys.forEach((key, idx) => {
280
282
  zaddArgs[key] = idx;
@@ -301,7 +303,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
301
303
  const writesPattern = `checkpoint_write:${threadId}:*`;
302
304
  const writesKeys = await this.client.keys(writesPattern);
303
305
  if (writesKeys.length > 0) await this.client.del(writesKeys);
304
- const zsetPattern = `write_keys_zset:${threadId}:*`;
306
+ const zsetPattern = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:*`;
305
307
  const zsetKeys = await this.client.keys(zsetPattern);
306
308
  if (zsetKeys.length > 0) await this.client.del(zsetKeys);
307
309
  }
@@ -340,7 +342,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
340
342
  for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") console.warn(`Failed to set TTL for key ${keys[i]}:`, results[i].reason);
341
343
  }
342
344
  async loadPendingWrites(threadId, checkpointNs, checkpointId) {
343
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
345
+ const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
344
346
  const writeKeys = await this.client.zRange(zsetKey, 0, -1);
345
347
  if (writeKeys.length === 0) return;
346
348
  const pendingWrites = [];
@@ -373,7 +375,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
373
375
  const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
374
376
  const oldWriteKeys = await this.client.keys(writePattern);
375
377
  if (oldWriteKeys.length > 0) await this.client.del(oldWriteKeys);
376
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;
378
+ const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${oldCheckpointId}`;
377
379
  await this.client.del(zsetKey);
378
380
  const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
379
381
  const oldBlobKeys = await this.client.keys(blobPattern);
@@ -399,7 +401,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
399
401
  }
400
402
  }
401
403
  };
402
-
403
404
  //#endregion
404
405
  export { ShallowRedisSaver };
406
+
405
407
  //# sourceMappingURL=shallow.js.map