@langchain/langgraph-checkpoint-redis 1.0.0 → 1.0.2

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 CHANGED
@@ -1,3 +1,4 @@
1
+ import { escapeRediSearchTagValue } from "./utils.js";
1
2
  import { BaseCheckpointSaver, TASKS, copyCheckpoint, maxChannelVersion, uuid6 } from "@langchain/langgraph-checkpoint";
2
3
  import { createClient, createCluster } from "redis";
3
4
 
@@ -129,14 +130,13 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
129
130
  return saver;
130
131
  }
131
132
  async get(config) {
132
- const tuple = await this.getTuple(config);
133
- return tuple?.checkpoint;
133
+ return (await this.getTuple(config))?.checkpoint;
134
134
  }
135
135
  async getTuple(config) {
136
136
  const threadId = config.configurable?.thread_id;
137
137
  const checkpointNs = config.configurable?.checkpoint_ns ?? "";
138
138
  const checkpointId = config.configurable?.checkpoint_id;
139
- if (!threadId) return void 0;
139
+ if (!threadId) return;
140
140
  let key;
141
141
  let jsonDoc;
142
142
  if (checkpointId) {
@@ -145,15 +145,15 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
145
145
  } else {
146
146
  const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;
147
147
  const keys = await this.client.keys(pattern);
148
- if (keys.length === 0) return void 0;
148
+ if (keys.length === 0) return;
149
149
  keys.sort();
150
150
  key = keys[keys.length - 1];
151
151
  jsonDoc = await this.client.json.get(key);
152
152
  }
153
- if (!jsonDoc) return void 0;
153
+ if (!jsonDoc) return;
154
154
  if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) await this.applyTTL(key);
155
155
  const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
156
- return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
156
+ return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
157
157
  }
158
158
  async put(config, checkpoint, metadata, newVersions) {
159
159
  await this.ensureIndexes();
@@ -207,16 +207,21 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
207
207
  }
208
208
  }
209
209
  if (!options?.before && options?.filter) {
210
- for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") queryParts.push(`(@${key}:{${value}})`);
211
- else if (typeof value === "number") queryParts.push(`(@${key}:[${value} ${value}])`);
212
- else if (typeof value === "object" && Object.keys(value).length === 0) {}
210
+ for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") {
211
+ const escapedKey = escapeRediSearchTagValue(key);
212
+ const escapedValue = escapeRediSearchTagValue(value);
213
+ queryParts.push(`(@${escapedKey}:{${escapedValue}})`);
214
+ } else if (typeof value === "number") {
215
+ const escapedKey = escapeRediSearchTagValue(key);
216
+ queryParts.push(`(@${escapedKey}:[${value} ${value}])`);
217
+ } else if (typeof value === "object" && Object.keys(value).length === 0) {}
213
218
  }
214
219
  if (queryParts.length === 0) queryParts.push("*");
215
220
  const query = queryParts.join(" ");
216
221
  const limit = options?.limit ?? 10;
217
222
  try {
218
223
  const fetchLimit = options?.before && !config?.configurable?.thread_id ? 1e3 : options?.before ? limit * 10 : limit;
219
- const results = await this.client.ft.search("checkpoints", query, {
224
+ let documents = (await this.client.ft.search("checkpoints", query, {
220
225
  LIMIT: {
221
226
  from: 0,
222
227
  size: fetchLimit
@@ -225,22 +230,18 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
225
230
  BY: "checkpoint_ts",
226
231
  DIRECTION: "DESC"
227
232
  }
228
- });
229
- let documents = results.documents;
233
+ })).documents;
230
234
  let yieldedCount = 0;
231
235
  for (const doc of documents) {
232
236
  if (yieldedCount >= limit) break;
233
237
  if (options?.before?.configurable?.checkpoint_id) {
234
- const currentCheckpointId = doc.value.checkpoint_id;
235
- const beforeCheckpointId = options.before.configurable.checkpoint_id;
236
- if (currentCheckpointId >= beforeCheckpointId) continue;
238
+ if (doc.value.checkpoint_id >= options.before.configurable.checkpoint_id) continue;
237
239
  }
238
240
  const jsonDoc = doc.value;
239
241
  let matches = true;
240
242
  if ((hasNullFilter || options?.before) && options?.filter) {
241
243
  for (const [filterKey, filterValue] of Object.entries(options.filter)) if (filterValue === null) {
242
- const metadataValue = jsonDoc.metadata?.[filterKey];
243
- if (metadataValue !== null) {
244
+ if (jsonDoc.metadata?.[filterKey] !== null) {
244
245
  matches = false;
245
246
  break;
246
247
  }
@@ -259,7 +260,7 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
259
260
  if (!matches) continue;
260
261
  }
261
262
  const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
262
- yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
263
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
263
264
  yieldedCount++;
264
265
  }
265
266
  return;
@@ -274,15 +275,13 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
274
275
  keys.sort().reverse();
275
276
  let filteredKeys = keys;
276
277
  if (options?.before?.configurable?.checkpoint_id) {
277
- const beforeThreadId = options.before.configurable.thread_id || threadId;
278
- const beforeCheckpointNs = options.before.configurable.checkpoint_ns ?? checkpointNs;
279
- const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;
278
+ const beforeKey = `checkpoint:${options.before.configurable.thread_id || threadId}:${options.before.configurable.checkpoint_ns ?? checkpointNs}:${options.before.configurable.checkpoint_id}`;
280
279
  const beforeIndex = keys.indexOf(beforeKey);
281
280
  if (beforeIndex > 0) filteredKeys = keys.slice(beforeIndex + 1);
282
281
  else if (beforeIndex === 0) filteredKeys = [];
283
282
  }
284
- const limit$1 = options?.limit ?? 10;
285
- const limitedKeys = filteredKeys.slice(0, limit$1);
283
+ const limit = options?.limit ?? 10;
284
+ const limitedKeys = filteredKeys.slice(0, limit);
286
285
  for (const key of limitedKeys) {
287
286
  const jsonDoc = await this.client.json.get(key);
288
287
  if (jsonDoc) {
@@ -301,7 +300,7 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
301
300
  }
302
301
  if (!matches) continue;
303
302
  const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
304
- yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
303
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
305
304
  }
306
305
  }
307
306
  } else {
@@ -317,19 +316,16 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
317
316
  }
318
317
  allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);
319
318
  let yieldedCount = 0;
320
- const limit$1 = options?.limit ?? 10;
319
+ const limit = options?.limit ?? 10;
321
320
  for (const { doc: jsonDoc } of allDocuments) {
322
- if (yieldedCount >= limit$1) break;
321
+ if (yieldedCount >= limit) break;
323
322
  if (options?.before?.configurable?.checkpoint_id) {
324
- const currentCheckpointId = jsonDoc.checkpoint_id;
325
- const beforeCheckpointId = options.before.configurable.checkpoint_id;
326
- if (currentCheckpointId >= beforeCheckpointId) continue;
323
+ if (jsonDoc.checkpoint_id >= options.before.configurable.checkpoint_id) continue;
327
324
  }
328
325
  let matches = true;
329
326
  if (options?.filter) {
330
327
  for (const [filterKey, filterValue] of Object.entries(options.filter)) if (filterValue === null) {
331
- const metadataValue = jsonDoc.metadata?.[filterKey];
332
- if (metadataValue !== null) {
328
+ if (jsonDoc.metadata?.[filterKey] !== null) {
333
329
  matches = false;
334
330
  break;
335
331
  }
@@ -348,7 +344,7 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
348
344
  if (!matches) continue;
349
345
  }
350
346
  const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
351
- yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
347
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
352
348
  yieldedCount++;
353
349
  }
354
350
  }
@@ -399,8 +395,7 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
399
395
  if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
400
396
  }
401
397
  const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
402
- const checkpointExists = await this.client.exists(checkpointKey);
403
- if (checkpointExists) {
398
+ if (await this.client.exists(checkpointKey)) {
404
399
  const currentDoc = await this.client.json.get(checkpointKey);
405
400
  if (currentDoc) {
406
401
  currentDoc.has_writes = "true";
@@ -422,7 +417,7 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
422
417
  async loadPendingWrites(threadId, checkpointNs, checkpointId) {
423
418
  const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;
424
419
  const writeKeys = await this.client.keys(pattern);
425
- if (writeKeys.length === 0) return void 0;
420
+ if (writeKeys.length === 0) return;
426
421
  const writeDocuments = [];
427
422
  for (const writeKey of writeKeys) {
428
423
  const writeDoc = await this.client.json.get(writeKey);
@@ -430,15 +425,18 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
430
425
  }
431
426
  writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));
432
427
  const pendingWrites = [];
433
- for (const writeDoc of writeDocuments) pendingWrites.push([
434
- writeDoc.task_id,
435
- writeDoc.channel,
436
- writeDoc.value
437
- ]);
428
+ for (const writeDoc of writeDocuments) {
429
+ const deserializedValue = await this.serde.loadsTyped("json", JSON.stringify(writeDoc.value));
430
+ pendingWrites.push([
431
+ writeDoc.task_id,
432
+ writeDoc.channel,
433
+ deserializedValue
434
+ ]);
435
+ }
438
436
  return pendingWrites;
439
437
  }
440
438
  async loadCheckpointWithWrites(jsonDoc) {
441
- const checkpoint = { ...jsonDoc.checkpoint };
439
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
442
440
  if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {
443
441
  const actualNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
444
442
  await this.migratePendingSends(checkpoint, jsonDoc.thread_id, actualNs, jsonDoc.parent_checkpoint_id);
@@ -464,8 +462,9 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
464
462
  checkpoint.channel_values[TASKS] = allTasks;
465
463
  checkpoint.channel_versions[TASKS] = Object.keys(checkpoint.channel_versions).length > 0 ? maxChannelVersion(...Object.values(checkpoint.channel_versions)) : 1;
466
464
  }
467
- createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
465
+ async createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
468
466
  const checkpointNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
467
+ const metadata = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.metadata));
469
468
  return {
470
469
  config: { configurable: {
471
470
  thread_id: jsonDoc.thread_id,
@@ -473,7 +472,7 @@ var RedisSaver = class RedisSaver extends BaseCheckpointSaver {
473
472
  checkpoint_id: jsonDoc.checkpoint_id
474
473
  } },
475
474
  checkpoint,
476
- metadata: jsonDoc.metadata,
475
+ metadata,
477
476
  parentConfig: jsonDoc.parent_checkpoint_id ? { configurable: {
478
477
  thread_id: jsonDoc.thread_id,
479
478
  checkpoint_ns: checkpointNs,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["key: string","jsonDoc: CheckpointDocument | null","filteredChannelValues: Record<string, any>","jsonDoc: CheckpointDocument","queryParts: string[]","error: any","limit","allDocuments: { key: string; doc: CheckpointDocument }[]","searchOptions: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }","writeKeys: string[]","zaddArgs: Record<string, number>","writeDocuments: any[]","pendingWrites: Array<[string, string, any]>","pendingWrites: Array<[string, string, any]> | undefined","allTasks: any[]","sortedObj: Record<string, any>","sorted: Record<string, any>"],"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\";\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 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 // Don't escape, just wrap in braces for exact match\n queryParts.push(`(@${key}:{${value}})`);\n } else if (typeof value === \"number\") {\n queryParts.push(`(@${key}:[${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 this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\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 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 this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\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 pendingWrites.push([writeDoc.task_id, writeDoc.channel, writeDoc.value]);\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 // Load checkpoint directly from JSON\n const checkpoint = { ...jsonDoc.checkpoint };\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 createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): CheckpointTuple {\n // Convert back from \"__empty__\" to empty string\n const checkpointNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\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: jsonDoc.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\";\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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"}
package/dist/shallow.cjs CHANGED
@@ -1,6 +1,7 @@
1
- const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
- const __langchain_langgraph_checkpoint = require_rolldown_runtime.__toESM(require("@langchain/langgraph-checkpoint"));
3
- const redis = require_rolldown_runtime.__toESM(require("redis"));
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_utils = require('./utils.cjs');
3
+ let _langchain_langgraph_checkpoint = require("@langchain/langgraph-checkpoint");
4
+ let redis = require("redis");
4
5
 
5
6
  //#region src/shallow.ts
6
7
  function deterministicStringify(obj) {
@@ -99,7 +100,7 @@ const SCHEMAS = [{
99
100
  * - Automatically cleans up old checkpoints and writes when new ones are added
100
101
  * - Reduces storage usage for applications that don't need checkpoint history
101
102
  */
102
- var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_checkpoint.BaseCheckpointSaver {
103
+ var ShallowRedisSaver = class ShallowRedisSaver extends _langchain_langgraph_checkpoint.BaseCheckpointSaver {
103
104
  client;
104
105
  ttlConfig;
105
106
  constructor(client, ttlConfig) {
@@ -115,8 +116,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
115
116
  return saver;
116
117
  }
117
118
  async get(config) {
118
- const tuple = await this.getTuple(config);
119
- return tuple?.checkpoint;
119
+ return (await this.getTuple(config))?.checkpoint;
120
120
  }
121
121
  async put(config, checkpoint, metadata, _newVersions) {
122
122
  await this.ensureIndexes();
@@ -124,7 +124,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
124
124
  const checkpointNs = config.configurable?.checkpoint_ns ?? "";
125
125
  const parentCheckpointId = config.configurable?.checkpoint_id;
126
126
  if (!threadId) throw new Error("thread_id is required");
127
- const checkpointId = checkpoint.id || (0, __langchain_langgraph_checkpoint.uuid6)(0);
127
+ const checkpointId = checkpoint.id || (0, _langchain_langgraph_checkpoint.uuid6)(0);
128
128
  const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;
129
129
  let prevCheckpointData = null;
130
130
  let prevCheckpointId = null;
@@ -161,19 +161,16 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
161
161
  const threadId = config.configurable?.thread_id;
162
162
  const checkpointNs = config.configurable?.checkpoint_ns ?? "";
163
163
  const checkpointId = config.configurable?.checkpoint_id;
164
- if (!threadId) return void 0;
164
+ if (!threadId) return;
165
165
  const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;
166
166
  const jsonDoc = await this.client.json.get(key);
167
- if (!jsonDoc) return void 0;
168
- if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return void 0;
167
+ if (!jsonDoc) return;
168
+ if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return;
169
169
  if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) await this.applyTTL(key);
170
- const checkpoint = {
171
- ...jsonDoc.checkpoint,
172
- channel_values: jsonDoc.checkpoint.channel_values || {}
173
- };
170
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
174
171
  let pendingWrites;
175
172
  if (jsonDoc.has_writes === "true") pendingWrites = await this.loadPendingWrites(jsonDoc.thread_id, jsonDoc.checkpoint_ns, jsonDoc.checkpoint_id);
176
- return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
173
+ return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
177
174
  }
178
175
  async *list(config, options) {
179
176
  await this.ensureIndexes();
@@ -185,8 +182,14 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
185
182
  } else {
186
183
  const queryParts = [];
187
184
  if (options?.filter) {
188
- for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") queryParts.push(`(@${key}:{${value}})`);
189
- else if (typeof value === "number") queryParts.push(`(@${key}:[${value} ${value}])`);
185
+ for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") {
186
+ const escapedKey = require_utils.escapeRediSearchTagValue(key);
187
+ const escapedValue = require_utils.escapeRediSearchTagValue(value);
188
+ queryParts.push(`(@${escapedKey}:{${escapedValue}})`);
189
+ } else if (typeof value === "number") {
190
+ const escapedKey = require_utils.escapeRediSearchTagValue(key);
191
+ queryParts.push(`(@${escapedKey}:[${value} ${value}])`);
192
+ }
190
193
  }
191
194
  if (queryParts.length === 0) queryParts.push("*");
192
195
  const query = queryParts.join(" ");
@@ -213,24 +216,20 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
213
216
  if (options?.filter) {
214
217
  if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
215
218
  }
216
- const checkpoint = {
217
- ...jsonDoc.checkpoint,
218
- channel_values: jsonDoc.checkpoint.channel_values || {}
219
- };
220
- yield this.createCheckpointTuple(jsonDoc, checkpoint);
219
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
220
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint);
221
221
  yieldCount++;
222
222
  }
223
223
  } catch (error) {
224
224
  if (error.message?.includes("no such index")) {
225
- const pattern = `checkpoint:*:*:shallow`;
226
- const keys = await this.client.keys(pattern);
225
+ const keys = await this.client.keys(`checkpoint:*:*:shallow`);
227
226
  if (keys.length === 0) return;
228
227
  keys.sort().reverse();
229
228
  const seenThreads = /* @__PURE__ */ new Set();
230
229
  let yieldCount = 0;
231
- const limit$1 = options?.limit ?? 10;
230
+ const limit = options?.limit ?? 10;
232
231
  for (const key of keys) {
233
- if (yieldCount >= limit$1) break;
232
+ if (yieldCount >= limit) break;
234
233
  const jsonDoc = await this.client.json.get(key);
235
234
  if (!jsonDoc) continue;
236
235
  const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;
@@ -239,11 +238,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
239
238
  if (options?.filter) {
240
239
  if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
241
240
  }
242
- const checkpoint = {
243
- ...jsonDoc.checkpoint,
244
- channel_values: jsonDoc.checkpoint.channel_values || {}
245
- };
246
- yield this.createCheckpointTuple(jsonDoc, checkpoint);
241
+ const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
242
+ yield await this.createCheckpointTuple(jsonDoc, checkpoint);
247
243
  yieldCount++;
248
244
  }
249
245
  return;
@@ -291,8 +287,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
291
287
  if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
292
288
  }
293
289
  const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;
294
- const checkpointExists = await this.client.exists(checkpointKey);
295
- if (checkpointExists) {
290
+ if (await this.client.exists(checkpointKey)) {
296
291
  const currentDoc = await this.client.json.get(checkpointKey);
297
292
  if (currentDoc) {
298
293
  currentDoc.has_writes = "true";
@@ -321,7 +316,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
321
316
  if ("writes" in metadata) jsonDoc.writes = typeof metadata.writes === "object" ? JSON.stringify(metadata.writes) : metadata.writes;
322
317
  if ("score" in metadata) jsonDoc.score = metadata.score;
323
318
  }
324
- createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
319
+ async createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
320
+ const metadata = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.metadata));
325
321
  return {
326
322
  config: { configurable: {
327
323
  thread_id: jsonDoc.thread_id,
@@ -329,7 +325,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
329
325
  checkpoint_id: jsonDoc.checkpoint_id
330
326
  } },
331
327
  checkpoint,
332
- metadata: jsonDoc.metadata,
328
+ metadata,
333
329
  parentConfig: jsonDoc.parent_checkpoint_id ? { configurable: {
334
330
  thread_id: jsonDoc.thread_id,
335
331
  checkpoint_ns: jsonDoc.checkpoint_ns,
@@ -347,15 +343,18 @@ var ShallowRedisSaver = class ShallowRedisSaver extends __langchain_langgraph_ch
347
343
  async loadPendingWrites(threadId, checkpointNs, checkpointId) {
348
344
  const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
349
345
  const writeKeys = await this.client.zRange(zsetKey, 0, -1);
350
- if (writeKeys.length === 0) return void 0;
346
+ if (writeKeys.length === 0) return;
351
347
  const pendingWrites = [];
352
348
  for (const writeKey of writeKeys) {
353
349
  const writeDoc = await this.client.json.get(writeKey);
354
- if (writeDoc) pendingWrites.push([
355
- writeDoc.task_id,
356
- writeDoc.channel,
357
- writeDoc.value
358
- ]);
350
+ if (writeDoc) {
351
+ const deserializedValue = await this.serde.loadsTyped("json", JSON.stringify(writeDoc.value));
352
+ pendingWrites.push([
353
+ writeDoc.task_id,
354
+ writeDoc.channel,
355
+ deserializedValue
356
+ ]);
357
+ }
359
358
  }
360
359
  return pendingWrites;
361
360
  }